7
7
"math"
8
8
"os"
9
9
"regexp"
10
+ "slices"
10
11
"strings"
11
12
"unicode"
12
13
"unicode/utf8"
@@ -15,6 +16,7 @@ import (
15
16
"gopkg.in/yaml.v3"
16
17
17
18
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
19
+ "github.com/trufflesecurity/trufflehog/v3/pkg/log"
18
20
)
19
21
20
22
var (
@@ -39,6 +41,13 @@ type AllowlistEntry struct {
39
41
Values []string `yaml:"values"` // List of secret patterns/regexes to allowlist
40
42
}
41
43
44
+ // CompiledAllowlist holds both exact string matches and compiled regex patterns for efficient matching
45
+ type CompiledAllowlist struct {
46
+ ExactMatches map [string ]struct {} // For exact string matching (O(1) lookup)
47
+ CompiledRegexes []* regexp.Regexp // Pre-compiled regex patterns
48
+ RegexPatterns []string // Original regex patterns (for logging/debugging)
49
+ }
50
+
42
51
var (
43
52
filter * ahocorasick.Trie
44
53
@@ -213,46 +222,41 @@ func FilterKnownFalsePositives(ctx context.Context, detector Detector, results [
213
222
// FilterAllowlistedSecrets filters out results that match allowlisted secrets.
214
223
// This allows users to specify known safe secrets that should not be reported.
215
224
// Supports regex patterns.
216
- func FilterAllowlistedSecrets (ctx context.Context , results []Result , allowlistedSecrets map [ string ] struct {} ) []Result {
217
- if len (allowlistedSecrets ) == 0 {
225
+ func FilterAllowlistedSecrets (ctx context.Context , results []Result , allowlist * CompiledAllowlist ) []Result {
226
+ if allowlist == nil || ( len (allowlist . ExactMatches ) == 0 && len ( allowlist . CompiledRegexes ) == 0 ) {
218
227
return results
219
228
}
220
229
221
- var filteredResults []Result
222
- for _ , result := range results {
230
+ return slices .DeleteFunc (results , func (result Result ) bool {
223
231
if len (result .Raw ) == 0 {
224
- filteredResults = append (filteredResults , result )
225
- continue
232
+ return false // Keep results with empty Raw
226
233
}
227
234
228
- isAllowlisted := false
229
- var matchReason string
230
-
231
235
// Check if the raw secret matches any allowlisted secret
232
236
rawSecret := string (result .Raw )
233
- if isAllowlisted , matchReason = isSecretAllowlisted (rawSecret , allowlistedSecrets ); isAllowlisted {
234
- ctx .Logger ().V (4 ).Info ("Skipping result: allowlisted secret" , "result" , maskSecret (rawSecret ), "reason" , matchReason )
235
- continue
237
+ log .RedactGlobally (rawSecret )
238
+ if isAllowlisted , matchReason := isSecretAllowlisted (rawSecret , allowlist ); isAllowlisted {
239
+ ctx .Logger ().V (4 ).Info ("Skipping result: allowlisted secret" , "result" , rawSecret , "reason" , matchReason )
240
+ return true // Delete this result
236
241
}
237
242
238
243
// Also check RawV2 if present
239
244
if result .RawV2 != nil {
240
245
rawV2Secret := string (result .RawV2 )
241
- if isAllowlisted , matchReason = isSecretAllowlisted (rawV2Secret , allowlistedSecrets ); isAllowlisted {
242
- ctx .Logger ().V (4 ).Info ("Skipping result: allowlisted secret" , "result" , maskSecret ( rawV2Secret ) , "reason" , matchReason )
243
- continue
246
+ if isAllowlisted , matchReason : = isSecretAllowlisted (rawV2Secret , allowlist ); isAllowlisted {
247
+ ctx .Logger ().V (4 ).Info ("Skipping result: allowlisted secret" , "result" , rawV2Secret , "reason" , matchReason )
248
+ return true // Delete this result
244
249
}
245
250
}
246
251
247
- filteredResults = append (filteredResults , result )
248
- }
249
-
250
- return filteredResults
252
+ return false // Keep this result
253
+ })
251
254
}
252
255
253
256
// LoadAllowlistedSecrets loads secrets from a YAML file that should be allowlisted.
254
257
// The YAML format supports multiline secrets and includes optional descriptions.
255
- func LoadAllowlistedSecrets (yamlFile string ) (map [string ]struct {}, error ) {
258
+ // Returns a CompiledAllowlist with pre-compiled regex patterns for efficient matching.
259
+ func LoadAllowlistedSecrets (yamlFile string ) (* CompiledAllowlist , error ) {
256
260
file , err := os .Open (yamlFile )
257
261
if err != nil {
258
262
return nil , fmt .Errorf ("failed to open allowlist file: %w" , err )
@@ -270,49 +274,62 @@ func LoadAllowlistedSecrets(yamlFile string) (map[string]struct{}, error) {
270
274
content = append (content , '\n' )
271
275
}
272
276
273
- var entries []AllowlistEntry
274
- if err := yaml .Unmarshal (content , & entries ); err != nil {
277
+ var allowList []AllowlistEntry
278
+ if err := yaml .Unmarshal (content , & allowList ); err != nil {
275
279
return nil , fmt .Errorf ("failed to parse YAML allowlist file: %w" , err )
276
280
}
281
+ // Use the shared compilation function
282
+ allowlist := CompileAllowlistPatterns (allowList )
283
+ return allowlist , nil
284
+ }
277
285
278
- allowlistedSecrets := make (map [string ]struct {})
279
- for _ , entry := range entries {
280
- for _ , value := range entry .Values {
281
- if strings .TrimSpace (value ) != "" { // Skip empty values
282
- allowlistedSecrets [value ] = struct {}{}
286
+ // CompileAllowlistPatterns compiles a list of patterns into a CompiledAllowlist.
287
+ // All patterns are first attempted to be compiled as regex. If compilation fails,
288
+ // they are treated as exact string matches.
289
+ func CompileAllowlistPatterns (allowList []AllowlistEntry ) * CompiledAllowlist {
290
+ allowlist := & CompiledAllowlist {
291
+ ExactMatches : make (map [string ]struct {}),
292
+ }
293
+
294
+ for _ , entry := range allowList {
295
+ for _ , pattern := range entry .Values {
296
+ pattern = strings .TrimSpace (pattern )
297
+ if pattern == "" {
298
+ continue // Skip empty patterns
299
+ }
300
+
301
+ // Always try to compile as regex first
302
+ if compiledRegex , err := regexp .Compile (pattern ); err == nil {
303
+ // Successfully compiled as regex
304
+ allowlist .CompiledRegexes = append (allowlist .CompiledRegexes , compiledRegex )
305
+ allowlist .RegexPatterns = append (allowlist .RegexPatterns , pattern )
306
+ } else {
307
+ // Invalid regex, treat as exact string match
308
+ allowlist .ExactMatches [pattern ] = struct {}{}
283
309
}
284
310
}
285
311
}
286
312
287
- return allowlistedSecrets , nil
313
+ return allowlist
288
314
}
289
315
290
316
// isSecretAllowlisted checks if a secret matches any allowlisted pattern (exact string or regex)
291
- func isSecretAllowlisted (secret string , allowlistedSecrets map [string ]struct {}) (bool , string ) {
292
- // First, try exact string matching for performance
293
- if _ , isAllowlisted := allowlistedSecrets [secret ]; isAllowlisted {
317
+ func isSecretAllowlisted (secret string , allowlist * CompiledAllowlist ) (bool , string ) {
318
+ if allowlist == nil {
319
+ return false , ""
320
+ }
321
+
322
+ // First, try exact string matching for performance (O(1) lookup)
323
+ if _ , isAllowlisted := allowlist .ExactMatches [secret ]; isAllowlisted {
294
324
return true , "exact match"
295
325
}
296
326
297
- // Try regex matching
298
- for pattern := range allowlistedSecrets {
299
- if regex , err := regexp .Compile (pattern ); err == nil {
300
- if regex .MatchString (secret ) {
301
- return true , "regex match: " + pattern
302
- }
327
+ // Try pre-compiled regex patterns
328
+ for i , compiledRegex := range allowlist .CompiledRegexes {
329
+ if compiledRegex .MatchString (secret ) {
330
+ return true , "regex match: " + allowlist .RegexPatterns [i ]
303
331
}
304
332
}
305
333
306
334
return false , ""
307
335
}
308
-
309
- // maskSecret masks a secret for safe logging by showing only the first and last few characters
310
- func maskSecret (secret string ) string {
311
- if len (secret ) <= 8 {
312
- return "***"
313
- }
314
- if len (secret ) <= 16 {
315
- return secret [:2 ] + "***" + secret [len (secret )- 2 :]
316
- }
317
- return secret [:4 ] + "***" + secret [len (secret )- 4 :]
318
- }
0 commit comments