@@ -35,12 +35,16 @@ var nameRegexPatterns = []*regexp.Regexp{
3535 regexp .MustCompile (`(?i)DEEPSEEK` ),
3636 regexp .MustCompile (`(?i)PERPLEXITY` ),
3737 regexp .MustCompile (`(?i)CEREBRAS` ),
38+ regexp .MustCompile (`(?i)XAI` ),
39+ regexp .MustCompile (`(?i)ASSEMBLYAI` ),
40+ regexp .MustCompile (`(?i)AI21` ),
41+ regexp .MustCompile (`(?i)NVIDIA_NIM` ),
3842 // Secrets managers
3943 regexp .MustCompile (`(?i)DOPPLER` ),
4044 // Google AI (Gemini, Vertex AI, PaLM)
4145 regexp .MustCompile (`(?i)GEMINI` ),
4246 regexp .MustCompile (`(?i)VERTEX` ),
43- regexp .MustCompile (`(?i)\bPALM_ ` ), // word boundary + underscore avoids NAPALM_MODE, PALM_BEACH_PROPERTY
47+ regexp .MustCompile (`(?i)(^|_)PALM_ ` ), // (^|_) avoids NAPALM_MODE while matching MY_PALM_KEY
4448 // AWS AI
4549 regexp .MustCompile (`(?i)BEDROCK` ),
4650 // Azure AI
@@ -72,7 +76,7 @@ var nameRegexPatterns = []*regexp.Regexp{
7276 regexp .MustCompile (`(?i)CLOUDFLARE` ),
7377 regexp .MustCompile (`(?i)HEROKU` ),
7478 regexp .MustCompile (`(?i)RAILWAY` ),
75- regexp .MustCompile (`(?i)\bFLY_ ` ), // word boundary prevents false positives (BUTTERFLY_KEY)
79+ regexp .MustCompile (`(?i)(^|_)FLY_ ` ), // (^|_) avoids BUTTERFLY_KEY, FLYWEIGHT_INDEX while matching MY_FLY_TOKEN
7680 // Source control
7781 regexp .MustCompile (`(?i)GITHUB` ),
7882 regexp .MustCompile (`(?i)GITLAB` ),
@@ -83,7 +87,7 @@ var nameRegexPatterns = []*regexp.Regexp{
8387 regexp .MustCompile (`(?i)AIRTABLE` ),
8488 // Database-as-a-service (API keys / connection tokens)
8589 regexp .MustCompile (`(?i)SUPABASE` ),
86- regexp .MustCompile (`(?i)\bNEON_ ` ), // word boundary + underscore avoids ANEMONE_CONFIG, NEON_LIGHTS_COLOR
90+ regexp .MustCompile (`(?i)(^|_)NEON_ ` ), // (^|_) avoids ANEMONE_CONFIG, NEONLIGHTS_COLOR while matching MY_NEON_KEY
8791 regexp .MustCompile (`(?i)PLANETSCALE` ),
8892 // Generic credential terms
8993 regexp .MustCompile (`(?i)API_KEY` ),
@@ -194,11 +198,15 @@ func (s *APIKeyScanner) Name() string { return "api_keys" }
194198// Implements Scanner. Never returns skipped=true.
195199func (s * APIKeyScanner ) Scan () models.ScanResult {
196200 var findings []models.Finding
197- // seenEnvNames is shared across the name-regex and value-pattern passes so that a
198- // variable matching both (e.g. CUSTOM_STRIPE_KEY=sk_live_...) produces exactly one
199- // finding — the name-regex pass runs first and claims it.
201+ // seenEnvNames is shared across all three env-scanning passes so that any variable
202+ // claimed by an earlier pass is not re-reported by a later one. Order:
203+ // 1. scanEnvKeys — exact-match built-in + user-configured extra keys
204+ // 2. scanNameRegex — name-pattern heuristics (MY_OPENAI_KEY etc.)
205+ // 3. scanValuePatterns — prefix+length value matching
206+ // A variable in ExtraEnvKeys that also matches a nameRegex pattern therefore produces
207+ // exactly one finding (from scanEnvKeys, the highest-priority pass).
200208 seenEnvNames := make (map [string ]bool )
201- findings = append (findings , s .scanEnvKeys ()... )
209+ findings = append (findings , s .scanEnvKeys (seenEnvNames )... )
202210 findings = append (findings , s .scanNameRegex (seenEnvNames )... )
203211 findings = append (findings , s .scanValuePatterns (seenEnvNames )... )
204212 findings = append (findings , s .scanCredentialFiles ()... )
@@ -210,7 +218,9 @@ func (s *APIKeyScanner) Scan() models.ScanResult {
210218
211219// scanEnvKeys checks built-in and extra environment variable key names for presence.
212220// Key names only are reported; values are never read or stored.
213- func (s * APIKeyScanner ) scanEnvKeys () []models.Finding {
221+ // seenEnvNames is the shared cross-pass dedup set; matched names are added to it so
222+ // that scanNameRegex and scanValuePatterns will skip variables already claimed here.
223+ func (s * APIKeyScanner ) scanEnvKeys (seenEnvNames map [string ]bool ) []models.Finding {
214224 var findings []models.Finding
215225
216226 // KEYS-01: Built-in high-risk env vars (sorted for deterministic output).
@@ -222,6 +232,7 @@ func (s *APIKeyScanner) scanEnvKeys() []models.Finding {
222232 for _ , key := range keys {
223233 if val := os .Getenv (key ); val != "" {
224234 _ = val // value is intentionally discarded; presence only
235+ seenEnvNames [key ] = true
225236 findings = append (findings , envKeyFinding (key ))
226237 }
227238 }
@@ -235,12 +246,13 @@ func (s *APIKeyScanner) scanEnvKeys() []models.Finding {
235246 copy (extraKeys , s .ExtraEnvKeys )
236247 sort .Strings (extraKeys )
237248 for _ , key := range extraKeys {
238- if HighRiskEnvKeys [key ] || seenExtra [key ] {
239- continue // already covered by built-in check or earlier extra
249+ if HighRiskEnvKeys [key ] || seenExtra [key ] || seenEnvNames [ key ] {
250+ continue // already covered by built-in check, earlier extra, or another pass
240251 }
241252 seenExtra [key ] = true
242253 if val := os .Getenv (key ); val != "" {
243254 _ = val // value is intentionally discarded; presence only
255+ seenEnvNames [key ] = true
244256 findings = append (findings , envKeyFinding (key ))
245257 }
246258 }
0 commit comments