@@ -39,7 +39,7 @@ var providerNamePatterns = []*regexp.Regexp{
3939 regexp .MustCompile (`(?i)DEEPSEEK` ),
4040 regexp .MustCompile (`(?i)PERPLEXITY` ),
4141 regexp .MustCompile (`(?i)CEREBRAS` ),
42- regexp .MustCompile (`(?i)(^|_)XAI_` ), // (^|_) avoids TAXI_KEY, PROXAI_TOKEN while matching XAI_API_KEY, MY_XAI_KEY
42+ regexp .MustCompile (`(?i)(^|_)XAI_` ), // (^|_) anchor avoids mid-word false positives
4343 regexp .MustCompile (`(?i)ASSEMBLYAI` ),
4444 regexp .MustCompile (`(?i)AI21` ),
4545 regexp .MustCompile (`(?i)NVIDIA_NIM` ),
@@ -48,7 +48,7 @@ var providerNamePatterns = []*regexp.Regexp{
4848 // Google AI (Gemini, Vertex AI, PaLM)
4949 regexp .MustCompile (`(?i)GEMINI` ),
5050 regexp .MustCompile (`(?i)VERTEX` ),
51- regexp .MustCompile (`(?i)(^|_)PALM_` ), // (^|_) avoids NAPALM_MODE while matching MY_PALM_KEY
51+ regexp .MustCompile (`(?i)(^|_)PALM_` ), // (^|_) anchor avoids mid-word false positives
5252 // AWS AI
5353 regexp .MustCompile (`(?i)BEDROCK` ),
5454 // Azure AI
@@ -80,18 +80,18 @@ var providerNamePatterns = []*regexp.Regexp{
8080 regexp .MustCompile (`(?i)CLOUDFLARE` ),
8181 regexp .MustCompile (`(?i)HEROKU` ),
8282 regexp .MustCompile (`(?i)RAILWAY` ),
83- regexp .MustCompile (`(?i)(^|_)FLY_` ), // (^|_) avoids BUTTERFLY_KEY, FLYWEIGHT_INDEX while matching MY_FLY_TOKEN
83+ regexp .MustCompile (`(?i)(^|_)FLY_` ), // (^|_) anchor avoids mid-word false positives
8484 // Source control
8585 regexp .MustCompile (`(?i)GITHUB` ),
8686 regexp .MustCompile (`(?i)GITLAB` ),
8787 regexp .MustCompile (`(?i)BITBUCKET` ),
8888 // Productivity / project tools
89- regexp .MustCompile (`(?i)(^|_)LINEAR_` ), // (^|_) avoids BILINEAR_FILTER while matching MY_LINEAR_TOKEN
89+ regexp .MustCompile (`(?i)(^|_)LINEAR_` ), // (^|_) anchor avoids mid-word false positives
9090 regexp .MustCompile (`(?i)NOTION` ),
9191 regexp .MustCompile (`(?i)AIRTABLE` ),
9292 // Database-as-a-service
9393 regexp .MustCompile (`(?i)SUPABASE` ),
94- regexp .MustCompile (`(?i)(^|_)NEON_` ), // (^|_) avoids ANEMONE_CONFIG, NEONLIGHTS_COLOR while matching MY_NEON_KEY
94+ regexp .MustCompile (`(?i)(^|_)NEON_` ), // (^|_) anchor avoids mid-word false positives
9595 regexp .MustCompile (`(?i)PLANETSCALE` ),
9696}
9797
@@ -195,11 +195,8 @@ func NewAPIKeyScannerWithConfig(cfg config.Config) *APIKeyScanner {
195195 }
196196}
197197
198- // Name returns the canonical scanner ID.
199198func (s * APIKeyScanner ) Name () string { return "api_keys" }
200199
201- // Scan detects high-risk API keys in env vars and credential file presence.
202- // Implements Scanner. Never returns skipped=true.
203200func (s * APIKeyScanner ) Scan () models.ScanResult {
204201 var findings []models.Finding
205202 // seenEnvNames is shared across all three env-scanning passes so that any variable
@@ -234,7 +231,12 @@ func (s *APIKeyScanner) scanEnvKeys(seenEnvNames map[string]bool) []models.Findi
234231 for _ , key := range keys {
235232 if os .Getenv (key ) != "" {
236233 seenEnvNames [key ] = true
237- findings = append (findings , envKeyFinding (key ))
234+ findings = append (findings , models.Finding {
235+ Scanner : "api_keys" ,
236+ Resource : key ,
237+ Severity : models .SeverityHigh ,
238+ Description : "Can be used to make authenticated API calls." ,
239+ })
238240 }
239241 }
240242
@@ -249,14 +251,45 @@ func (s *APIKeyScanner) scanEnvKeys(seenEnvNames map[string]bool) []models.Findi
249251 }
250252 if os .Getenv (key ) != "" {
251253 seenEnvNames [key ] = true
252- findings = append (findings , envKeyFinding (key ))
254+ findings = append (findings , models.Finding {
255+ Scanner : "api_keys" ,
256+ Resource : key ,
257+ Severity : models .SeverityHigh ,
258+ Description : "Can be used to make authenticated API calls." ,
259+ })
253260 }
254261 }
255262 }
256263
257264 return findings
258265}
259266
267+ // envEntry holds a parsed, non-empty environment variable that has not yet been claimed.
268+ type envEntry struct {
269+ name string
270+ value string
271+ }
272+
273+ // unclaimedEnvEntries returns non-empty env vars that are not in HighRiskEnvKeys and not
274+ // already present in seenEnvNames. It is used by scanNameRegex and scanValuePatterns to
275+ // avoid repeating the same iteration and filtering logic in both methods.
276+ func unclaimedEnvEntries (seenEnvNames map [string ]bool ) []envEntry {
277+ var entries []envEntry
278+ for _ , raw := range os .Environ () {
279+ idx := strings .IndexByte (raw , '=' )
280+ if idx < 0 {
281+ continue
282+ }
283+ name := raw [:idx ]
284+ value := raw [idx + 1 :]
285+ if HighRiskEnvKeys [name ] || value == "" || seenEnvNames [name ] {
286+ continue
287+ }
288+ entries = append (entries , envEntry {name : name , value : value })
289+ }
290+ return entries
291+ }
292+
260293// scanNameRegex checks env var names against known provider keywords and generic
261294// credential terms. It catches non-standard names like MY_OPENAI_KEY that are
262295// missed by the exact-match HighRiskEnvKeys pass. Key names only are reported;
@@ -265,49 +298,29 @@ func (s *APIKeyScanner) scanEnvKeys(seenEnvNames map[string]bool) []models.Findi
265298func (s * APIKeyScanner ) scanNameRegex (seenEnvNames map [string ]bool ) []models.Finding {
266299 var findings []models.Finding
267300
268- for _ , entry := range os .Environ () {
269- idx := strings .IndexByte (entry , '=' )
270- if idx < 0 {
271- continue
272- }
273- name := entry [:idx ]
274- value := entry [idx + 1 :]
275-
276- // Skip if already covered by the exact-match HighRiskEnvKeys pass.
277- if HighRiskEnvKeys [name ] {
278- continue
279- }
280- // Skip if value is empty — key exists but no credential is set.
281- if value == "" {
282- continue
283- }
284- // Skip if already claimed by a prior pass or earlier in this pass.
285- if seenEnvNames [name ] {
286- continue
287- }
288-
301+ for _ , e := range unclaimedEnvEntries (seenEnvNames ) {
289302 matched := false
290303 // Provider patterns require the name to also contain a credential suffix.
291304 for _ , re := range providerNamePatterns {
292- if re .MatchString (name ) && credentialSuffixRe .MatchString (name ) {
305+ if re .MatchString (e . name ) && credentialSuffixRe .MatchString (e . name ) {
293306 matched = true
294307 break
295308 }
296309 }
297310 // Credential suffix patterns match standalone.
298311 if ! matched {
299312 for _ , re := range credentialSuffixPatterns {
300- if re .MatchString (name ) {
313+ if re .MatchString (e . name ) {
301314 matched = true
302315 break
303316 }
304317 }
305318 }
306319 if matched {
307- seenEnvNames [name ] = true
320+ seenEnvNames [e . name ] = true
308321 findings = append (findings , models.Finding {
309322 Scanner : "api_keys" ,
310- Resource : name ,
323+ Resource : e . name ,
311324 Severity : models .SeverityHigh ,
312325 Description : "Can be used to make authenticated API calls." ,
313326 })
@@ -322,33 +335,13 @@ func (s *APIKeyScanner) scanNameRegex(seenEnvNames map[string]bool) []models.Fin
322335func (s * APIKeyScanner ) scanValuePatterns (seenEnvNames map [string ]bool ) []models.Finding {
323336 var findings []models.Finding
324337
325- for _ , entry := range os .Environ () {
326- idx := strings .IndexByte (entry , '=' )
327- if idx < 0 {
328- continue
329- }
330- name := entry [:idx ]
331- value := entry [idx + 1 :]
332-
333- // Skip if already covered by the exact-match HighRiskEnvKeys pass.
334- if HighRiskEnvKeys [name ] {
335- continue
336- }
337- // Skip empty values.
338- if value == "" {
339- continue
340- }
341- // Skip if already claimed by scanNameRegex or an earlier iteration of this pass.
342- if seenEnvNames [name ] {
343- continue
344- }
345-
338+ for _ , e := range unclaimedEnvEntries (seenEnvNames ) {
346339 for _ , p := range valuePatterns {
347- if strings .HasPrefix (value , p .prefix ) && len (value ) == p .totalLen {
348- seenEnvNames [name ] = true
340+ if strings .HasPrefix (e . value , p .prefix ) && len (e . value ) == p .totalLen {
341+ seenEnvNames [e . name ] = true
349342 findings = append (findings , models.Finding {
350343 Scanner : "api_keys" ,
351- Resource : name , // env var NAME, never the value
344+ Resource : e . name , // env var NAME, never the value
352345 Severity : p .severity ,
353346 Description : fmt .Sprintf ("Value matches %s API key format." , p .providerTag ),
354347 })
@@ -365,10 +358,14 @@ func (s *APIKeyScanner) scanValuePatterns(seenEnvNames map[string]bool) []models
365358func (s * APIKeyScanner ) scanCredentialFiles () []models.Finding {
366359 var findings []models.Finding
367360
368- // KEYS-02: Built-in credential files.
369361 // If home directory cannot be resolved, skip all ~-based paths to avoid
370362 // scanning incorrect root-relative paths (e.g. /.aws/credentials).
371- homeDir := s .resolveHomeDir ()
363+ homeDir := s .HomeDir
364+ if homeDir == "" {
365+ if h , err := os .UserHomeDir (); err == nil {
366+ homeDir = h
367+ }
368+ }
372369 // seenPath deduplicates built-in and extra paths.
373370 allCredFiles := append (credentialFiles , s .ExtraCredentialFiles ... )
374371 seenPath := make (map [string ]bool , len (allCredFiles ))
@@ -394,27 +391,6 @@ func (s *APIKeyScanner) scanCredentialFiles() []models.Finding {
394391 return findings
395392}
396393
397- func envKeyFinding (key string ) models.Finding {
398- return models.Finding {
399- Scanner : "api_keys" ,
400- Resource : key ,
401- Severity : models .SeverityHigh ,
402- Description : "Can be used to make authenticated API calls." ,
403- }
404- }
405-
406- // resolveHomeDir returns the effective home directory for credential file expansion.
407- func (s * APIKeyScanner ) resolveHomeDir () string {
408- if s .HomeDir != "" {
409- return s .HomeDir
410- }
411- home , err := os .UserHomeDir ()
412- if err != nil {
413- return ""
414- }
415- return home
416- }
417-
418394// expandHome replaces a leading ~ with the given homeDir.
419395func expandHome (path , homeDir string ) string {
420396 if len (path ) == 0 {
0 commit comments