Skip to content

fix(ci): scope gitleaks scan to PR commits, not all branches#224

Merged
scottschreckengaust merged 2 commits into
mainfrom
fix/gitleaks-pr-scope
Jul 7, 2026
Merged

fix(ci): scope gitleaks scan to PR commits, not all branches#224
scottschreckengaust merged 2 commits into
mainfrom
fix/gitleaks-pr-scope

Conversation

@scottschreckengaust

@scottschreckengaust scottschreckengaust commented Jul 7, 2026

Copy link
Copy Markdown
Member

Fixes #223

Problem

The gitleaks job in .github/workflows/security-scanners.yml runs gitleaks git ... . with no commit range. In git mode, gitleaks walks the entire object graph reachable from all refs in the clone. Because actions/checkout uses fetch-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) in tools/semgrep/r-all.active.yaml on the unmerged feat/vendor-semgrep-rules branch (PR #220) — a file that isn't in main or in PR #222 at all. The CI log shows 169 commits scanned (far more than the PR's own commits) → leaks found: 3.

The local mise run build (security:gitleaks task) had the identical broad-scope problem and failed the same way.

Fix

CI (security-scanners.yml) — scope the scan by event type:

Event Range Rationale
pull_request origin/<base_ref>..HEAD Only the commits the PR introduces — cannot fail on another branch's content
push / schedule / workflow_dispatch HEAD Full history reachable from HEAD — preserves complete coverage of main

Local (mise.toml security:gitleaks task) — add --log-opts=HEAD so the local scan matches CI scope (current-branch history only, not sideways branches).

github.base_ref is bound to an env: var and referenced as "$PR_BASE_REF" (never interpolated into the shell) to avoid the script-injection class being addressed in #222.

Verification

  • YAML parses.
  • Local mise run security:gitleaks now passes: 119 commits scanned (HEAD ancestry) vs. 221 (all refs) → no leaks found.
  • Scoped scans confirm isolation: origin/main history → 0 leaks; all-refs default → 3 leaks (the other branch's fixtures).
  • checkout already fetches full history (fetch-depth: 0), so origin/<base_ref> is present without an extra fetch.

Notes / follow-ups

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.

scottschreckengaust and others added 2 commits July 7, 2026 19:37
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>
@scottschreckengaust scottschreckengaust marked this pull request as ready for review July 7, 2026 19:46
@scottschreckengaust scottschreckengaust requested review from a team, krokoko and theagenticguy July 7, 2026 19:46

@theagenticguy theagenticguy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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..HEAD scopes the scan to exactly the commits the PR introduces. With actions/checkout@v6 + fetch-depth: 0 the base is already present as origin/<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: HEAD walks full ancestry, so main keeps complete coverage. The trade-off called out in the description (PR-range won't re-catch a secret already in main) is real but correctly mitigated by this branch of the conditional.
  • mise.toml --log-opts=HEAD matches 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.

@scottschreckengaust scottschreckengaust requested review from a team July 7, 2026 19:58

@yananliw yananliw left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM

@scottschreckengaust scottschreckengaust added this pull request to the merge queue Jul 7, 2026
Merged via the queue into main with commit 7d134d9 Jul 7, 2026
31 checks passed
@scottschreckengaust scottschreckengaust deleted the fix/gitleaks-pr-scope branch July 7, 2026 20:48

@isadeks isadeks left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_ref is bound to env: and only dereferenced as "$PR_BASE_REF", never interpolated into the run: block — consistent with the #222 hardening.
  • Coverage tradeoff is sound and reversible: full-history coverage of main is 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

  1. Consider pull_request.base.sha for the range (as the sibling semgrep job already does): LOG_OPTS="${BASE_SHA}..HEAD". It's the exact fetched commit — immune to origin/main advancing and not dependent on the origin/<base_ref> remote-tracking ref being materialized. Worth confirming origin/main actually resolves in a real PR run, since the runner checks out a detached merge ref and the PR's verification was done locally.
  2. The mise.toml comment 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

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.

gitleaks CI fails PRs on unrelated branches' content (scan scope too broad)

4 participants