fix(ci): scope gitleaks scan to PR commits, not all branches#224
Conversation
The gitleaks job ran `gitleaks git ... .` with no commit range. In git mode gitleaks walks the entire object graph reachable from ALL refs, and because actions/checkout uses fetch-depth: 0 the runner fetches every branch. As a result, pull requests failed on secrets that exist only on other, unmerged branches (e.g. example rule fixtures on feat/vendor-semgrep-rules) — not in the PR and not in main. Scope the scan by event: - pull_request: `--log-opts=origin/<base_ref>..HEAD` scans only the commits the PR introduces, so it can never fail on another branch's content. - push/schedule/workflow_dispatch: `--log-opts=HEAD` scans the full history reachable from HEAD, preserving full coverage of main. github.base_ref is bound to an env var and referenced as "$PR_BASE_REF" (never interpolated into the shell) to avoid script injection. Fixes #223 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mise security:gitleaks task had the same broad-scope problem as CI: `gitleaks git` with no --log-opts walks all refs and fails `mise run build` on secrets that live only on other, unmerged branches. Add `--log-opts=HEAD` so the local scan only covers history reachable from the current branch, matching the CI scan scope. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
43629c7 to
1c0e446
Compare
theagenticguy
left a comment
There was a problem hiding this comment.
Approved.
Clean, correctly-scoped fix for #223. Verified end to end:
Range logic is correct on every event path
pull_request:origin/$PR_BASE_REF..HEADscopes the scan to exactly the commits the PR introduces. Withactions/checkout@v6+fetch-depth: 0the base is already present asorigin/<base_ref>, so the "no extra fetch needed" comment holds. Sideways/unmerged branches are excluded by construction — this is precisely what was leaking PR #220's fixtures into unrelated PRs.push/schedule/workflow_dispatch:HEADwalks full ancestry, somainkeeps complete coverage. The trade-off called out in the description (PR-range won't re-catch a secret already inmain) is real but correctly mitigated by this branch of the conditional.mise.toml--log-opts=HEADmatches the CI local scope. TOML parses clean.
Injection-safe
github.base_ref is bound to PR_BASE_REF and referenced as "$PR_BASE_REF", never interpolated into the shell — consistent with the hardening in #222.
CI
All 24 checks green, including the gitleaks job itself now running on the scoped range (no leaks found), plus SonarQube/CodeQL/Semgrep.
Non-blocking: none. Agree with the follow-up to baseline the example fixtures on #220. Ship it.
isadeks
left a comment
There was a problem hiding this comment.
Correct root-cause fix, safe implementation, well documented. The diagnosis is right: gitleaks' git source defaults to --all when --log-opts is empty, so with fetch-depth: 0 it walks every ref — scoping via --log-opts is exactly the right lever.
Strengths
- Injection-safe:
github.base_refis bound toenv:and only dereferenced as"$PR_BASE_REF", never interpolated into therun:block — consistent with the #222 hardening. - Coverage tradeoff is sound and reversible: full-history coverage of
mainis preserved on push/schedule, and secrets added within the PR's own commits still sit in an intermediate commit inside the range, so per-commit scanning still catches them. - Clear comments +
::notice::echoing the effective range.
Non-blocking suggestions
- Consider
pull_request.base.shafor the range (as the siblingsemgrepjob already does):LOG_OPTS="${BASE_SHA}..HEAD". It's the exact fetched commit — immune toorigin/mainadvancing and not dependent on theorigin/<base_ref>remote-tracking ref being materialized. Worth confirmingorigin/mainactually resolves in a real PR run, since the runner checks out a detached merge ref and the PR's verification was done locally. - The
mise.tomlcomment says--log-opts=HEAD"Matches the CI scan scope" — it matches only the push/schedule scope, not the PR scope (origin/<base_ref>..HEAD). Suggest "Matches the CI push/schedule scan scope."
Both are optional polish. Approving.
🤖 Generated with Claude Code
Fixes #223
Problem
The
gitleaksjob in.github/workflows/security-scanners.ymlrunsgitleaks git ... .with no commit range. In git mode, gitleaks walks the entire object graph reachable from all refs in the clone. Becauseactions/checkoutusesfetch-depth: 0, the runner fetches every branch — so the scan reaches sideways into unrelated branches.Consequence: pull requests fail on secrets that exist only on other, unmerged branches. Concretely, PR #222's gitleaks check is red because of example rule fixtures (
slack-webhook-url,private-key) intools/semgrep/r-all.active.yamlon the unmergedfeat/vendor-semgrep-rulesbranch (PR #220) — a file that isn't inmainor in PR #222 at all. The CI log shows169 commits scanned(far more than the PR's own commits) →leaks found: 3.The local
mise run build(security:gitleakstask) had the identical broad-scope problem and failed the same way.Fix
CI (
security-scanners.yml) — scope the scan by event type:pull_requestorigin/<base_ref>..HEADpush/schedule/workflow_dispatchHEADmainLocal (
mise.tomlsecurity:gitleakstask) — add--log-opts=HEADso the local scan matches CI scope (current-branch history only, not sideways branches).github.base_refis bound to anenv:var and referenced as"$PR_BASE_REF"(never interpolated into the shell) to avoid the script-injection class being addressed in #222.Verification
mise run security:gitleaksnow passes: 119 commits scanned (HEAD ancestry) vs. 221 (all refs) →no leaks found.origin/mainhistory → 0 leaks; all-refs default → 3 leaks (the other branch's fixtures).checkoutalready fetches full history (fetch-depth: 0), soorigin/<base_ref>is present without an extra fetch.Notes / follow-ups
mainis retained via the push/schedule branch of the conditional (CI) and remains available locally onmain.Generated with Claude Code
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.