Skip to content

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 key
  • docs/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:

gitleaks detect ... --log-opts="$CI_MERGE_REQUEST_DIFF_BASE_SHA..$CI_COMMIT_SHA"

$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 the gitleaks job 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 set GIT_DEPTH: 0 themselves. 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 cicd repo's own MR pipeline exercises the MR-scoped path ($CI_MERGE_REQUEST_DIFF_BASE_SHA is set) and passes (no secrets in range).
  • main/branch pipelines exercise the full-history fallback and pass as before.