Skip to content

Commit c7d8160

Browse files
committed
refactor: simplify apikeys — inline helpers, deduplicate env iteration, trim comments
1 parent 14a9589 commit c7d8160

1 file changed

Lines changed: 58 additions & 82 deletions

File tree

internal/scan/apikeys.go

Lines changed: 58 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
199198
func (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.
203200
func (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
265298
func (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
322335
func (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
365358
func (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.
419395
func expandHome(path, homeDir string) string {
420396
if len(path) == 0 {

0 commit comments

Comments
 (0)