Skip to content

Commit 788fb7e

Browse files
committed
Use slices.DeleteFunc and pre-compile regexes
1 parent fa75aea commit 788fb7e

File tree

5 files changed

+451
-267
lines changed

5 files changed

+451
-267
lines changed

main.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,13 +520,14 @@ func run(state overseer.State) {
520520
verificationCacheMetrics := verificationcache.InMemoryMetrics{}
521521

522522
// Load allowlisted secrets if specified
523-
var allowlistedSecrets map[string]struct{}
523+
var allowlistedSecrets *detectors.CompiledAllowlist
524524
if *allowlistSecretsFile != "" {
525525
allowlistedSecrets, err = detectors.LoadAllowlistedSecrets(*allowlistSecretsFile)
526526
if err != nil {
527527
logFatal(err, "failed to load allowlisted secrets")
528528
}
529-
logger.Info("loaded allowlisted secrets", "count", len(allowlistedSecrets), "file", *allowlistSecretsFile)
529+
totalCount := len(allowlistedSecrets.ExactMatches) + len(allowlistedSecrets.CompiledRegexes)
530+
logger.Info("loaded allowlisted secrets", "exact_matches", len(allowlistedSecrets.ExactMatches), "regex_patterns", len(allowlistedSecrets.CompiledRegexes), "total", totalCount, "file", *allowlistSecretsFile)
530531
}
531532

532533
engConf := engine.Config{

pkg/detectors/falsepositives.go

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math"
88
"os"
99
"regexp"
10+
"slices"
1011
"strings"
1112
"unicode"
1213
"unicode/utf8"
@@ -15,6 +16,7 @@ import (
1516
"gopkg.in/yaml.v3"
1617

1718
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
19+
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
1820
)
1921

2022
var (
@@ -39,6 +41,13 @@ type AllowlistEntry struct {
3941
Values []string `yaml:"values"` // List of secret patterns/regexes to allowlist
4042
}
4143

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+
4251
var (
4352
filter *ahocorasick.Trie
4453

@@ -213,46 +222,41 @@ func FilterKnownFalsePositives(ctx context.Context, detector Detector, results [
213222
// FilterAllowlistedSecrets filters out results that match allowlisted secrets.
214223
// This allows users to specify known safe secrets that should not be reported.
215224
// 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) {
218227
return results
219228
}
220229

221-
var filteredResults []Result
222-
for _, result := range results {
230+
return slices.DeleteFunc(results, func(result Result) bool {
223231
if len(result.Raw) == 0 {
224-
filteredResults = append(filteredResults, result)
225-
continue
232+
return false // Keep results with empty Raw
226233
}
227234

228-
isAllowlisted := false
229-
var matchReason string
230-
231235
// Check if the raw secret matches any allowlisted secret
232236
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
236241
}
237242

238243
// Also check RawV2 if present
239244
if result.RawV2 != nil {
240245
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
244249
}
245250
}
246251

247-
filteredResults = append(filteredResults, result)
248-
}
249-
250-
return filteredResults
252+
return false // Keep this result
253+
})
251254
}
252255

253256
// LoadAllowlistedSecrets loads secrets from a YAML file that should be allowlisted.
254257
// 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) {
256260
file, err := os.Open(yamlFile)
257261
if err != nil {
258262
return nil, fmt.Errorf("failed to open allowlist file: %w", err)
@@ -270,49 +274,62 @@ func LoadAllowlistedSecrets(yamlFile string) (map[string]struct{}, error) {
270274
content = append(content, '\n')
271275
}
272276

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 {
275279
return nil, fmt.Errorf("failed to parse YAML allowlist file: %w", err)
276280
}
281+
// Use the shared compilation function
282+
allowlist := CompileAllowlistPatterns(allowList)
283+
return allowlist, nil
284+
}
277285

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{}{}
283309
}
284310
}
285311
}
286312

287-
return allowlistedSecrets, nil
313+
return allowlist
288314
}
289315

290316
// 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 {
294324
return true, "exact match"
295325
}
296326

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]
303331
}
304332
}
305333

306334
return false, ""
307335
}
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

Comments
 (0)