Skip to content

ci: match the fixture allowlist on the secret, not the whole line - #72

Merged
sumitake merged 1 commit into
mainfrom
dev/claude/gitleaks-narrow-allowlist
Jul 28, 2026
Merged

ci: match the fixture allowlist on the secret, not the whole line#72
sumitake merged 1 commit into
mainfrom
dev/claude/gitleaks-narrow-allowlist

Conversation

@sumitake

Copy link
Copy Markdown
Owner

Summary

Follow-up to #68. The allowlist it added was wider than its own comment claimed, and this narrows it.

#68 used a line-target regex:

regexTarget = "line"
regexes = ['''"lane_token": "3b73a0ffbe3426ef5761f5c09b756ef6"''']

and asserted "any other secret added to that fixture is still reported." That is false. detect.go sets globalAllowlistTarget = currentLine when RegexTarget == "line", and the regex is an unanchored substring match, so two things leak through:

  1. Same-line co-occurrence — a second, unrelated credential on the lane_token line is suppressed too, because that line still contains the fixture literal.
  2. Reformattingjson.dumps without indent, or any compacting rewrite, collapses this fixture to a single line containing the literal. A line-target regex would then match for every finding in the file and silently exempt the whole fixture. This is the serious one: it converts a one-value exemption into a whole-file blind spot on a routine reformat, with no signal.

The fix

Match the anchored literal against the secret (gitleaks' default target when regexTarget is omitted):

regexes = ['''^3b73a0ffbe3426ef5761f5c09b756ef6$''']

Per config/allowlist.go, RegexAllowed() tests finding.Secret when RegexTarget is empty. Anchored ^…$ exempts a finding only when the detected secret is exactly this literal, so layout cannot widen it and no other secret — same line or not — can be caught by it.

Honest scope note: this is not strictly narrower

It is narrower on co-occurrence and on layout, but marginally wider on location: the exact secret is now exempt anywhere in history rather than only on a "lane_token": … line. That residual is theoretical for a fixed 128-bit synthetic value — an unrelated real credential would have to equal it exactly, and any occurrence of this value is by construction the same non-credential fixture token.

A rule-level [[rules.allowlists]] with condition = "AND" plus paths would close even that. I deliberately did not do it: it requires redefining the extended generic-api-key rule and relying on allowlist-merge semantics, and the failure mode of getting that wrong is a silently disabled exemption — precisely the bug class that caused this whole thread (#68's first attempt used top-level [[allowlists]], which gitleaks drops without warning). Not worth the risk to close a theoretical gap. Happy to add it if you'd rather have belt-and-suspenders.

Verification

  • End-to-end: workflow_dispatch of Secret scan on this branch — run 30339346924, same full-history gitleaks detect, 41 commits scanned, no leaks found, both jobs green. This also empirically proves gitleaks' Secret for this finding is the bare token: if it weren't, the anchored regex would not match and the scan would have failed with 3 leaks.
  • python3 scripts/build_skills.py --check — PASS
  • python3 scripts/build_marketplace.py --check — PASS
  • python3 scripts/build-changelog.py --dry-run — PASS (the gate CI applies to fragment-only PRs)
  • python3 scripts/check_release_consistency.py — PASS
  • python3 scripts/secret_scan.py — PASS
  • python3 scripts/check-public-export-safety.py --active-tree — SAFE
  • python3 -m unittest discover -s tests -t . — PASS (Ran 588 tests, OK)
  • python3 -m unittest discover -s scripts -p 'test_*.py' — PASS (Ran 254 tests, OK)
  • git diff --check — clean

build-changelog.py --check fails, but it fails identically on pristine origin/main (verified in a clean worktree) — that is the expected steady state of the fragment convention, where fragments accumulate and CHANGELOG.md compiles at release time. This PR does not touch CHANGELOG.md, so CI applies --dry-run, which passes.

Boundary declaration

  • No provider executor source, raw provider command, credential, private absolute path, retired package tree, downloader, or post-install hook is included.
  • No native-runtime change.
  • Creates no host-specific preset or provider-specific plugin.

Generated and release surfaces

  • Skill specs and generated SKILL.md files are in parity.
  • Claude and Codex marketplaces/manifests are in parity.
  • No changelog.d/ fragment: CI-tooling fix, no user-visible behavior change. CHANGELOG.md untouched.
  • No version bump: no behavior or distributed content changed.

Review and post-condition

Tier 3 — narrows the coverage of a credential-scanning gate on a public repository. Not an operator-reserved path.

Independent cross-family review — Grok (xAI), distinct from the Anthropic-family author:

VERDICT: APPROVE / CONFIDENCE: H

Both reported holes closed: "allowlist matches finding.Secret only. A second credential on the same line has a different secret → not exempted" and "layout never changes the secret." Remaining genuine-credential suppression: "No realistic path in this public repo." 32-hex collision: "Negligible in practice… not a credible threat."

CONCERN 1 — "New form is not strictly narrower on location (exact secret exempted repo-wide, not only under "lane_token": on a line) — residual is theoretical only for this fixed synthetic; optional rule-level path+regex AND if you want belt-and-suspenders."

Concern 1 is integrated as the "Honest scope note" section above rather than silently dropped, with the reason the optional tightener was declined.

Post-condition: confirm the next scheduled Secret scan on main is green, or dispatch it manually on main after merge.

Compliance trace

author: claude
standing_directives: public boundary declaration; independent cross-family review with the reviewer's concern integrated verbatim rather than dropped; full verification-gate run with pre-existing failures distinguished from introduced ones
tier: 3
cross_check: PROCEED — Grok (xAI) governance review, APPROVE, confidence H, 1 concern (location residual) integrated into the PR body; distinct family from the Anthropic-family author
post_condition: next scheduled or manually dispatched Secret scan on main is green
mcp_coverage_gap: NONE
contributor_rights: OWNER-AUTHORED
operator_reserved: no

The line-target regex was wider than the comment claimed. Two ways it
could suppress findings it should not:

1. Same-line co-occurrence -- a second, unrelated credential sharing the
   lane_token line would also be suppressed, because the allowlist regex
   is tested against the entire line and still matches.

2. Reformatting -- json.dumps without indent, or any compacting rewrite,
   collapses this fixture to a single line that contains the literal. A
   line-target regex would then match on every finding in the file and
   silently exempt the whole fixture.

Match the anchored literal against the secret instead (gitleaks' default
target). A finding is suppressed only when the detected secret IS exactly
this value, so layout cannot widen the exemption and no other secret --
same line or not -- can be caught by it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sumitake
sumitake merged commit 12b4c4a into main Jul 28, 2026
17 checks passed
@sumitake
sumitake deleted the dev/claude/gitleaks-narrow-allowlist branch July 28, 2026 09:02
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.

1 participant