Skip to content

Commit ff3e028

Browse files
committed
Optimize scan pipeline by caching filter set and skipping NER when not needed
1 parent e19d62c commit ff3e028

3 files changed

Lines changed: 145 additions & 62 deletions

File tree

internal/scan/detector.go

Lines changed: 82 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@ package scan
22

33
import (
44
"regexp"
5-
"sort"
6-
"strconv"
7-
"strings"
85

96
"github.com/datafog/datafog-api/internal/models"
107
)
@@ -67,78 +64,83 @@ var DefaultEntityConfidences = map[string]float64{
6764
"zip_code": 0.80,
6865
}
6966

67+
var defaultScanEntityTypes = []string{
68+
"api_key",
69+
"credit_card",
70+
"date",
71+
"email",
72+
"ip_address",
73+
"phone",
74+
"ssn",
75+
"zip_code",
76+
}
77+
7078
func ScanText(text string, entityFilter []string) []models.ScanFinding {
71-
requested := map[string]struct{}{}
72-
if len(entityFilter) > 0 {
73-
for _, name := range entityFilter {
74-
requested[strings.ToLower(strings.TrimSpace(name))] = struct{}{}
75-
}
76-
}
79+
requested := requestedEntitySet(entityFilter)
7780

7881
findings := make([]models.ScanFinding, 0)
7982

8083
// Phase 1: Regex engine (fast, always available)
81-
entityTypes := make([]string, 0, len(DefaultEntityPatterns))
82-
for entityType := range DefaultEntityPatterns {
83-
entityTypes = append(entityTypes, entityType)
84-
}
85-
sort.Strings(entityTypes)
86-
87-
for _, entityType := range entityTypes {
88-
pattern := DefaultEntityPatterns[entityType]
89-
if len(requested) > 0 {
90-
if _, ok := requested[entityType]; !ok {
91-
continue
84+
for _, entityType := range defaultScanEntityTypes {
85+
if shouldRunEntityType(requested, entityType) {
86+
pattern := DefaultEntityPatterns[entityType]
87+
idxs := pattern.Re.FindAllStringIndex(text, -1)
88+
for _, idx := range idxs {
89+
if len(idx) != 2 || idx[0] < 0 || idx[1] < idx[0] {
90+
continue
91+
}
92+
value := text[idx[0]:idx[1]]
93+
if pattern.Validate != nil && !pattern.Validate(value) {
94+
continue
95+
}
96+
findings = append(findings, models.ScanFinding{
97+
EntityType: entityType,
98+
Value: value,
99+
Start: idx[0],
100+
End: idx[1],
101+
Confidence: DefaultEntityConfidences[entityType],
102+
})
92103
}
93104
}
105+
}
94106

95-
idxs := pattern.Re.FindAllStringIndex(text, -1)
96-
for _, idx := range idxs {
97-
if len(idx) != 2 || idx[0] < 0 || idx[1] < idx[0] {
98-
continue
99-
}
100-
value := text[idx[0]:idx[1]]
101-
if pattern.Validate != nil && !pattern.Validate(value) {
102-
continue
103-
}
104-
findings = append(findings, models.ScanFinding{
105-
EntityType: entityType,
106-
Value: value,
107-
Start: idx[0],
108-
End: idx[1],
109-
Confidence: DefaultEntityConfidences[entityType],
110-
})
111-
}
107+
if !shouldRunNERForFilter(requested) {
108+
return findings
112109
}
113110

114111
// Phase 2: NER engine (heuristic, when enabled)
115-
nerFindings := ScanNER(text, entityFilter)
116-
findings = append(findings, nerFindings...)
112+
findings = append(findings, scanNERWithFilter(text, requested)...)
117113

118114
return findings
119115
}
120116

121117
// luhnValid implements the Luhn algorithm to validate credit card numbers.
122118
// It strips spaces and dashes before checking.
123119
func luhnValid(s string) bool {
124-
// Strip spaces and dashes
125-
var digits []int
120+
var digits [19]int
121+
n := 0
122+
126123
for _, ch := range s {
127-
if ch >= '0' && ch <= '9' {
128-
digits = append(digits, int(ch-'0'))
129-
} else if ch == ' ' || ch == '-' {
124+
switch {
125+
case ch >= '0' && ch <= '9':
126+
if n == len(digits) {
127+
return false
128+
}
129+
digits[n] = int(ch - '0')
130+
n++
131+
case ch == ' ' || ch == '-':
130132
continue
131-
} else {
133+
default:
132134
return false
133135
}
134136
}
135-
if len(digits) < 13 || len(digits) > 19 {
137+
if n < 13 || n > 19 {
136138
return false
137139
}
138140

139141
sum := 0
140142
double := false
141-
for i := len(digits) - 1; i >= 0; i-- {
143+
for i := n - 1; i >= 0; i-- {
142144
d := digits[i]
143145
if double {
144146
d *= 2
@@ -154,18 +156,44 @@ func luhnValid(s string) bool {
154156

155157
// ipv4Valid checks that each octet is 0-255.
156158
func ipv4Valid(s string) bool {
157-
parts := strings.Split(s, ".")
158-
if len(parts) != 4 {
159+
if len(s) == 0 {
159160
return false
160161
}
161-
for _, part := range parts {
162-
n, err := strconv.Atoi(part)
163-
if err != nil {
162+
163+
octetCount := 0
164+
value := 0
165+
digitsInOctet := 0
166+
167+
for i := 0; i <= len(s); i++ {
168+
if i == len(s) || s[i] == '.' {
169+
if digitsInOctet == 0 {
170+
return false
171+
}
172+
if value < 0 || value > 255 {
173+
return false
174+
}
175+
octetCount++
176+
if octetCount > 4 {
177+
return false
178+
}
179+
if i == len(s) {
180+
break
181+
}
182+
value = 0
183+
digitsInOctet = 0
184+
continue
185+
}
186+
187+
ch := s[i]
188+
if ch < '0' || ch > '9' {
164189
return false
165190
}
166-
if n < 0 || n > 255 {
191+
value = value*10 + int(ch-'0')
192+
digitsInOctet++
193+
if digitsInOctet > 3 {
167194
return false
168195
}
169196
}
170-
return true
197+
198+
return octetCount == 4
171199
}

internal/scan/filters.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package scan
2+
3+
import "strings"
4+
5+
func requestedEntitySet(entityFilter []string) map[string]struct{} {
6+
if len(entityFilter) == 0 {
7+
return nil
8+
}
9+
requested := make(map[string]struct{}, len(entityFilter))
10+
for _, name := range entityFilter {
11+
entity := strings.ToLower(strings.TrimSpace(name))
12+
if entity == "" {
13+
continue
14+
}
15+
requested[entity] = struct{}{}
16+
}
17+
if len(requested) == 0 {
18+
return nil
19+
}
20+
return requested
21+
}
22+
23+
func shouldRunEntityType(requested map[string]struct{}, entityType string) bool {
24+
if len(requested) == 0 {
25+
return true
26+
}
27+
_, ok := requested[entityType]
28+
return ok
29+
}
30+
31+
func shouldRunEntity(requested map[string]struct{}, entityType string) bool {
32+
return shouldRunEntityType(requested, entityType)
33+
}
34+
35+
func shouldRunNERForFilter(requested map[string]struct{}) bool {
36+
if len(requested) == 0 {
37+
return true
38+
}
39+
if _, ok := requested["person"]; ok {
40+
return true
41+
}
42+
if _, ok := requested["organization"]; ok {
43+
return true
44+
}
45+
if _, ok := requested["location"]; ok {
46+
return true
47+
}
48+
return false
49+
}

internal/scan/ner.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,22 @@ func ScanNER(text string, entityFilter []string) []models.ScanFinding {
126126
return nil
127127
}
128128

129-
requested := map[string]struct{}{}
130-
if len(entityFilter) > 0 {
131-
for _, name := range entityFilter {
132-
requested[strings.ToLower(strings.TrimSpace(name))] = struct{}{}
133-
}
129+
requested := requestedEntitySet(entityFilter)
130+
return scanNERWithFilter(text, requested)
131+
}
132+
133+
func scanNERWithFilter(text string, requested map[string]struct{}) []models.ScanFinding {
134+
if !NEREnabled {
135+
return nil
136+
}
137+
138+
if len(requested) > 0 && !shouldRunNERForFilter(requested) {
139+
return nil
134140
}
135141

136-
wantPerson := len(requested) == 0 || hasKey(requested, "person")
137-
wantOrg := len(requested) == 0 || hasKey(requested, "organization")
138-
wantLoc := len(requested) == 0 || hasKey(requested, "location")
142+
wantPerson := shouldRunEntity(requested, "person")
143+
wantOrg := shouldRunEntity(requested, "organization")
144+
wantLoc := shouldRunEntity(requested, "location")
139145

140146
findings := make([]models.ScanFinding, 0)
141147

0 commit comments

Comments
 (0)