The secret-scan check read 1 of main's 81 commits - #92
Merged
Merged
Conversation
`secret-scan` was `gitleaks/gitleaks-action`, which picks its scan range from the triggering event rather than scanning the repository. Read at the pinned SHA, it runs `gitleaks detect --log-opts=--no-merges --first-parent BASE^..HEAD` on a push and `--log-opts=-1` -- exactly one commit -- when the push carries a single commit. It drops `--log-opts`, and so reads the whole history, only on `schedule` and `workflow_dispatch`; `ci.yml` fires on neither. Every merge here is a squash merge, i.e. a one-commit push, so the job read one commit of 81 and reported success. `fetch-depth: 0` did not prevent that and could not: it governs what actions/checkout puts on disk, not what the scanner is asked to read. The step is now a pinned, checksum-verified gitleaks 8.30.1 binary invoked as `gitleaks git . --no-banner --redact --exit-code 1`, with no `--log-opts`, so it walks every commit reachable from HEAD on every event. `pull-requests: read` and the action's `GITHUB_TOKEN` are dropped: both existed only so the action could list a pull request's commits to scope the scan. The job id and display name are unchanged, because `secret-scan` is a required context in .github/rulesets/main.json. tests/test_secret_scan_reads_history.py holds the invocation, reading ci.yml with comments stripped so the comment naming the removed action cannot satisfy it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What the check actually read
secret-scanwasgitleaks/gitleaks-action@e0c47f4(v3.0.0). Read at that pinned SHA(
src/index.js,src/gitleaks.js), the action does not scan the repository — it picks acommit range out of the event that triggered the run:
push, N commitsgitleaks detect --log-opts=--no-merges --first-parent BASE^..HEADpush, 1 commitgitleaks detect --log-opts=-1— exactly one commitpull_requestschedule/workflow_dispatch--log-optsat all, i.e. the whole historyci.ymltriggers onpushandpull_requestand nothing else. The only two events forwhich that action reads history have never fired in this repository. And every merge into
mainhere is a squash merge, which is a one-commit push — so onmainthe job ran--log-opts=-1and read 1 ofmain's 81 commits, then reported success.A credential added in one commit and deleted in the next is invisible to that. It is
still in the history, and nothing in this repository has ever opened the history to look.
Why
fetch-depth: 0did not prevent itIt cannot.
fetch-depth: 0decides how much historyactions/checkoutputs on disk;what the scanner reads is decided by how it is invoked. A checkout deep enough to scan
and an invocation that declines to scan it is exactly the state this job was in, and the
comment above the step said nothing about it.
fetch-depth: 0stays — it is the necessaryprecondition — and its comment now says plainly that it is not the sufficient one.
The fix
The action is replaced by a pinned, checksum-verified gitleaks 8.30.1 binary invoked as:
No
--log-opts, so it walks every commit reachable from HEAD on every event; the range nolonger depends on how the run was triggered. The release archive is checked against the
gitleaks_..._checksums.txtpublished with it before it is unpacked.--exit-code 1makesa finding fail the job;
--redactkeeps the secret out of the log.pull-requests: readand theGITHUB_TOKENhanded to the action are dropped with it —both existed only so the action could list a pull request's commits in order to scope the
scan, and nothing is scoped now.
contents: readis all that is left.The job id and display name are byte-identical.
secret-scanis one of the fiverequired contexts in
.github/rulesets/main.json, andtests/test_ruleset_evidence.pyderives that list from this workflow; a rename would empty the requirement on the day the
profile is applied.
Posture, stated plainly:
maincarries no ruleset and no branch protection today —gh api repos/ChelseaKR/perimeter/rulesetsreturns[]andbranches/mainreports"protected": false(re-verified 2026-09-13). So this check blocks nothing and never has.It is still the only secret scan this repository has, and per ADR-0004 that is the reason
to make it able to fail, not a reason to leave it.
Negative control
Run entirely locally, in a throwaway clone of this repository with its remote removed.
Nothing below was ever pushed; the planted credential is random, generated for the run,
and never leaves the deleted clone.
main, 81 commits)82f4a65c55bc2a211466b254ca5e3e8a8470ed4ed562f900af55a934ccade6665e85f173f2ad98e60364105399d1ee01f2c46eb828dcb0c6296cc668, present in the object storegit log -S)--log-opts=-1)gitleaks git .)82f4a65c55bc2a211466b254ca5e3e8a8470ed4e— byte-identicalThe guard test was negative-controlled the same way. Three separate sabotages of
ci.yml, each run with__pycache__cleared:--log-opts=-1to the invocationtest_the_scanner_is_not_handed_a_rangefailsgitleaks/gitleaks-actionas a steptest_the_event_driven_action_does_not_come_backfailsfetch-depth: 0from this job onlytest_the_scan_checkout_still_fetches_the_history_it_walksfailsThat third one matters twice over:
ci.ymlholds two realfetch-depth: 0lines and onethat appears inside the new comment, so the test slices the
secret-scanjob out of thejobs:mapping and reads the file with comments stripped. Four conformance checkselsewhere in this portfolio passed because they matched a tool name inside a comment, and
the comment added here names both the action removed and the flag forbidden.
Checks
make verify's Python half runs clean in a fresh worktree: 965 passed, 25 skipped (theaxe-core and Playwright gates skip until
make node-sync/make browser-sync; CI runsboth).
ruff check,ruff format --check,mypy --strict,zizmor 1.29.0over.github/workflows/(online, no findings) and the repository's own semgrep invocation(295 rules, 74 files, 0 findings) were all run locally against this branch.
Prepared with AI assistance; reviewed before submission.
Correction, appended 2026-09-13 — this check is required
The statement above that
mainhas no branch protection and no ruleset was true when it was measured and is false now. Aprotect-mainruleset was created on this repository today, while this PR was being prepared, and it names this job as a required status check.Measured just now:
Note that
GET /repos/.../branches/main/protectionstill answers404 Branch not protected. That endpoint reports only classic branch protection and is blind to rulesets, which is how the original claim was reached honestly and still ended up wrong. Rulesets are the mechanism in use across this portfolio;GET /repos/.../rules/branches/mainis the endpoint that sees them.The correction raises the stakes of this PR rather than changing its content: the scan whose range this fixes is not advisory, it gates merges into
main.The original paragraph is left in place rather than edited, so the record shows what was measured and when.