Spec: Scope gitleaks scans to pipeline commits (v0.10.3)¶
- Repository:
gitlab.com/phpboyscout/cicd - Components touched:
go-security,rust-security,tofu-security - Release: patch —
fix:→ v0.10.3
Problem¶
go-security, rust-security and tofu-security each run a gitleaks
job whose script is, in effect:
gitleaks detect --source . --verbose --redact # go / rust
gitleaks detect --source=<paths> --no-banner --redact # tofu
gitleaks detect with no --log-opts walks the entire git history
reachable in the runner's checkout. GitLab's default GIT_STRATEGY:
fetch reuses the runner's working directory between jobs, so a runner
that previously built a different branch still has that branch's refs
and objects locally. gitleaks (via git log) sees them and reports
secrets that exist only on another branch — not in the branch under
test.
Observed failure¶
On phpboyscout/go-tool-base MR !31 (spec/server-options, branched
from main), the gitleaks job failed with two findings, both at
commits belonging to the unrelated spec/keys-mint branch:
internal/cmd/keys/keys_test.go— a throwaway test private keydocs/development/specs/2026-06-08-keys-mint-command.md— an illustrative PEM example
Neither commit is reachable from the MR's branch (git merge-base
--is-ancestor confirms), and neither file exists in main. The MR
introduced no secrets, yet its pipeline was blocked. Two runs reproduced
the identical findings, so it is deterministic, not a transient.
Decision¶
Scope each gitleaks detect to the commits the current pipeline
introduces, using the GitLab-provided merge-request diff base:
$CI_MERGE_REQUEST_DIFF_BASE_SHA is the merge-base of the source and
target branches, so BASE..$CI_COMMIT_SHA is exactly the set of commits
the MR adds — independent of any other refs cached on the runner. This
is the canonical gitleaks-in-MR pattern.
$CI_MERGE_REQUEST_DIFF_BASE_SHA is only set in merge-request pipelines.
The gitleaks jobs in go-security/rust-security are MR-only by
default (inputs.if), but a consumer may override inputs.if, and
tofu-security's job runs when: on_success (branch and tag pipelines
included). So fall back to the existing full-history scan when the
variable is empty:
if [ -n "$CI_MERGE_REQUEST_DIFF_BASE_SHA" ]; then
gitleaks detect ... --log-opts="$CI_MERGE_REQUEST_DIFF_BASE_SHA..$CI_COMMIT_SHA"
else
gitleaks detect ... # unchanged: full history
fi
Why not --no-git (filesystem-only scan)¶
--no-git scans the checked-out tree and ignores history entirely,
which also dodges cross-branch contamination. Rejected because it weakens
the gate: a secret added and then removed within the same MR would no
longer be caught. Range-scoped history scanning preserves the original
"scan the history" intent while fixing the false positives.
Scope & non-goals¶
- In scope: the
script:of thegitleaksjob in the three security components. No input shape changes, no new inputs, no global keywords (component-authoring rule 1). - Non-goals: clone-depth tuning (
GIT_DEPTH). MR pipelines fetch the diff base, so the range resolves; if a consumer runs an extremely deep MR on a shallow clone they can setGIT_DEPTH: 0themselves. Not changing the other scanners (osv-scanner, semgrep, trivy, govulncheck).
Behaviour change¶
| Pipeline context | Before | After |
|---|---|---|
| Merge-request pipeline | scans full history (all cached refs) | scans BASE..HEAD (MR commits only) |
| Branch / tag / non-MR | scans full history | unchanged — scans full history |
No input changes; existing consumers are unaffected apart from the removal of cross-branch false positives on MR pipelines.
Testing¶
The existing self-tests (tests/<x>-security/.gitlab-ci.yml) include the
components with inputs.if: 'true' and do not allow_failure the
gitleaks job — it is a real green-check on the clean cicd repo. Under
this change:
- The
cicdrepo's own MR pipeline exercises the MR-scoped path ($CI_MERGE_REQUEST_DIFF_BASE_SHAis set) and passes (no secrets in range). main/branch pipelines exercise the full-history fallback and pass as before.