Skip to content

fix(credential-analyzer): gate AST-CRED-001 on credential-format substring (#164)#167

Merged
thebenignhacker merged 1 commit intomainfrom
fix/credential-fp-settings-json-164
Apr 29, 2026
Merged

fix(credential-analyzer): gate AST-CRED-001 on credential-format substring (#164)#167
thebenignhacker merged 1 commit intomainfrom
fix/credential-fp-settings-json-164

Conversation

@thebenignhacker
Copy link
Copy Markdown
Contributor

Closes #164.

Summary

.claude/settings.json was firing AST-CRED-001 MEDIUM ("Credentials in Non-Environment Context") on every project that uses Claude Code's hook protection. The upstream compiler tags any artifact whose lowercased content includes the word "credential" or "session" with a dataType: 'credentials' access pattern, so a defensive .claude/settings.json containing "Read(.aws/credentials)" deny-rules and $VAR placeholder env values was being flagged despite holding no credential value.

Implementation

  1. Content-format gate in checkCredentialsInNonEnvContext: only fire when the artifact content contains a credential-format substring per a curated regex (vendor prefixes sk-, sk_live_, ghp_, gho_, github_pat_, xox[abprs]-, eyJ JWT, AKIA…, AIza…, plus a high-entropy 40+ word-character fallback). The regex is shared with AST-CRED-003's evidence-text gate.
  2. file:line + evidence population from the credential-format match position, with the evidence field masked at the analyzer layer so the raw token never reaches finding output, JSON, telemetry, or Registry sync. Mask preserves recognizable prefixes (sk-ant-api03-sk-ant-api03-****************).
  3. Governance-amplifier suppression on a curated host-tool config allowlist (.claude/settings.json, .vscode/settings.json, package.json, tsconfig.json, .eslintrc.json, etc.) — NOT every .json/.yaml file. MCP configs and A2A agent cards still get the amplifier text because they ARE agent-definition surfaces.

Phase 4.5 adversarial review

A general-purpose subagent investigated 8 risk categories pre-merge. Three findings actioned in this PR:

  • R-Bonus (HIGH): raw credential value was leaking into evidence — fixed by masking in findFirstCredentialFormat.
  • R4 (MEDIUM): governance-amplifier extension-match was over-suppressing on .cursor/mcp.json / agent.json / agent-config.yaml — replaced with curated host-tool basename allowlist.
  • R2 (HIGH): malicious .clinerules in adversarial corpus no longer fires AST-CRED-001 (used to fire on credential-keyword narrative alone). Accepted with rationale: the same fixture still fires AST-CRED-002 CRITICAL ("credential forwarding to external endpoint") and SEM-INST-001 / SEM-INST-002 HIGH on lines 1–3. System-level detection of the malicious artifact is preserved (release-smoke corpus 12/12 unchanged).

Out of scope (filed as follow-ups):

  • R1/R7 regex gaps (Stripe pk_live_, Twilio AC…, Mailgun key-, Heroku UUID, short sk- < 20 chars, URL-safe base64 with -//). Pre-existing in evidenceShowsCredentialFormat shared regex.
  • Migrate the credential-format regex into @opena2a/credential-patterns for cross-tool sync.

Verification

  • npm test 2025/2051 pass (was 2022/2048; +3 new b15 / b16 / b15-positive tests).
  • npm run release-smoke:corpus 12/12.
  • HMA self-scan 84 → 89 (FP at .claude/settings.json suppressed).
  • secure ./hackmyagent/ no longer fires MEDIUM on .claude/settings.json.
  • AST-CRED-001 still fires on .cursorrules:7 in the adversarial kitchen-sink corpus, with masked sk-ant-api03-**************** evidence.
  • analyzeCredentials Go-source regression test (source-code-preprocessor.test.ts:272) updated to pass artifactContent so AST-CRED-001 still fires on a real sk-ant-api03-… value.

Phase 4.5 score-jump classification

84 → 89 (+5):

  • At the .claude/settings.json suppression site: (a) preserved-detection FP-suppress (real credentials still fire; only keyword-only mentions in host-tool configs are suppressed).
  • At the .clinerules unit-level site: (b) narrowed-detection at the AST-CRED-001 layer, but .clinerules malicious fixtures are still flagged CRITICAL via AST-CRED-002 + HIGH via SEM-INST-* — system-level detection preserved; documented above as "accepted with rationale."

Test plan

  • npm test green (2025/2051)
  • npm run release-smoke:corpus 12/12
  • secure ./hackmyagent/ does not fire MEDIUM on .claude/settings.json
  • secure ~/.opena2a/corpus/repo/malicious/kitchen-sink still fires AST-CRED-001 on .cursorrules:7 (masked evidence) AND AST-CRED-002 / SEM-INST-* on .clinerules lines 1–3
  • b15-positive: real Slack token in agent-config.yaml fires with line: 4 + masked xoxb-****… evidence
  • Go-source AST-CRED-001 regression test still passes after passing artifactContent

Related

Bundled with #162 / #163 fixes for the 0.22.1 release.

…tring (#164)

`.claude/settings.json` was firing AST-CRED-001 MEDIUM on every project
that uses Claude Code's hook protection. The upstream compiler tags any
artifact whose lowercased content includes the substring "credential" or
"session" with a `dataType: 'credentials'` access pattern, so a
`.claude/settings.json` containing "Read(.aws/credentials)" deny-rules
and `$VAR` placeholder env values was being flagged despite holding no
credential value.

Changes:

- Content-format gate at `checkCredentialsInNonEnvContext`. The finding
  only fires when the artifact content contains a credential-format
  substring per a curated regex (vendor prefixes `sk-`, `sk_live_`,
  `ghp_`, `gho_`, `github_pat_`, `xox[abprs]-`, `eyJ` JWT, `AKIA…`,
  `AIza…`, plus a high-entropy 40+ word-character fallback). The regex
  is shared with AST-CRED-003's evidence-text gate.
- File:line population from the credential-format match position, plus
  an `evidence` field that is **masked at the analyzer layer** so the
  raw token never reaches finding output, JSON, telemetry, or Registry
  sync. Mask preserves recognizable prefixes (`sk-ant-api03-` →
  `sk-ant-api03-****************`).
- Governance-amplifier prose ("This artifact has no inline governance
  constraints, amplifying the risk of this finding") is now suppressed
  on a curated host-tool config allowlist (`.claude/settings.json`,
  `.vscode/settings.json`, `package.json`, `tsconfig.json`,
  `.eslintrc.json`, etc.) — NOT on every `.json`/`.yaml` file. MCP
  configs and A2A agent cards still get the amplifier text because
  they ARE agent-definition surfaces.
- New tests b15 / b16 (FP suppressed on `.claude/settings.json` with
  `$VAR` and `${VAR}` placeholders) and b15-positive (real Slack token
  in `agent-config.yaml` with credentials keyword still fires
  AST-CRED-001 with line:4 + masked-prefix evidence).
- Updated existing test in `source-code-preprocessor.test.ts` to pass
  `artifactContent` so AST-CRED-001 continues to fire on Go source
  with a real `sk-ant-api03-…` value.

Phase 4.5 adversarial review (subagent) caught:

- R-Bonus (HIGH): raw credential value was leaking into the `evidence`
  field — fixed by masking in `findFirstCredentialFormat`.
- R4 (MEDIUM): governance-amplifier extension-match was too broad and
  was over-suppressing on `.cursor/mcp.json` / `agent.json` /
  `agent-config.yaml` — replaced with curated host-tool allowlist.
- R2 (HIGH): malicious `.clinerules` in adversarial corpus no longer
  fires AST-CRED-001 (it used to fire on credential-keyword narrative
  alone). Accepted with rationale: the same fixture still fires
  AST-CRED-002 CRITICAL ("credential forwarding to external endpoint")
  and SEM-INST-001 / SEM-INST-002 HIGH on lines 1-3, so system-level
  detection of the malicious artifact is preserved. Score bands on the
  malicious corpus are unchanged (release-smoke 12/12).

Verification:

- `npm test` 2025/2051 pass (was 2022/2048; +3 new b15/b16/b15-positive).
- `npm run release-smoke:corpus` 12/12.
- HMA self-scan 84 → 89: classification (a) preserved-detection
  FP-suppress at the .claude/settings.json site; classification (b)
  narrowed-detection at the .clinerules site — but the file is still
  flagged CRITICAL via AST-CRED-002 + HIGH via SEM-INST-*.
- `.cursorrules:7` AST-CRED-001 still fires on adversarial corpus with
  masked `sk-ant-api03-****************` evidence (regex unchanged in
  detection power; only the rendered evidence is masked).

Out of scope (filed as follow-ups):

- R1 / R7 regex gaps (Stripe `pk_live_`, Twilio `AC…`, Mailgun `key-`,
  Heroku UUID, short `sk-` < 20 chars, URL-safe base64 with `-`/`/`).
  These are pre-existing gaps in `evidenceShowsCredentialFormat`
  inherited by the new gate. Tracked separately.
- Migrate the credential-format regex into `@opena2a/credential-
  patterns` for cross-tool sync. Tracked separately.

Closes #164.
@thebenignhacker thebenignhacker enabled auto-merge (squash) April 29, 2026 21:02
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

VERDICT: APPROVE

SUMMARY: This PR addresses a real false-positive issue (#164) where .claude/settings.json files with defensive deny-rules and env-var placeholders were incorrectly triggering AST-CRED-001. The implementation adds three layers of defense: (1) a content-format gate requiring actual credential-format substrings (vendor prefixes or high-entropy tokens) before emitting AST-CRED-001, (2) credential masking at the analyzer layer to prevent raw tokens from leaking into findings/telemetry, and (3) governance-amplifier suppression on a curated allowlist of host-tool configs. The regex patterns are shared between AST-CRED-001 and AST-CRED-003 for consistency. All changes are well-tested with comprehensive regression coverage including positive controls, and the credential masking properly prevents information disclosure.

Verification checklist completed:

  1. Regex DoS (buildCredentialFormatRegex): Examined line 569-585. All patterns are linear-time — anchored literals (sk-, AKIA, etc.) followed by fixed-width or unbounded quantifiers without nesting. The high-entropy fallback \b[A-Za-z0-9+=_]{40,}\b is word-boundary anchored and uses a simple character class, not nested quantifiers. No catastrophic backtracking possible. ✓

  2. Credential leakage (maskCredentialValue): Verified lines 620-650. Function masks tokens at the analyzer layer before they reach finding.evidence (line 147). Known prefixes preserve only the recognizable format (e.g., sk-ant-api03-*****), unknown shapes show first 8 chars + asterisks. The raw matched value from re.exec(content) at line 598 is immediately masked before being returned. Test b15-positive (line 693) validates the masked output never contains raw token bytes. ✓

  3. Array access safety (findFirstCredentialFormat): Lines 596-606. Uses re.exec() which returns null on no match (checked at line 599). Line iteration from 0 to m.index is safe because m.index is always a valid non-negative integer when match exists. No unchecked array indexing. ✓

  4. Allowlist completeness (isStructuredConfigPath): Lines 650-682. Uses explicit path suffix matching (.claude/settings.json, .vscode/settings.json) and exact basename Set membership. Does NOT match agent-definition surfaces like .cursor/mcp.json, agent.json, or agent-config.yaml per the design intent (line 647). The allowlist is deliberately conservative. ✓

  5. Test coverage: benign-fp-regression.test.ts adds 4 test cases covering the fix (b15, b16, b15-positive) plus comprehensive assertions on line numbers, evidence masking, and positive controls. Existing test at source-code-preprocessor.test.ts:275 updated to pass artifact content. ✓

No unmitigated security or correctness issues found. The changes are defensive, well-scoped, and include appropriate test coverage.


Reviewed 4 files changed (19510 bytes)

@thebenignhacker thebenignhacker merged commit 410f70a into main Apr 29, 2026
1 check passed
@thebenignhacker thebenignhacker deleted the fix/credential-fp-settings-json-164 branch April 29, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

secure: Credentials in Non-Environment Context on .claude/settings.json has no file:line, no Verify; FPs on real-world dev configs

1 participant