Skip to content

enh(secu): manage invalid-revision range error - #64

Draft
sc979 wants to merge 21 commits into
mainfrom
SECU-gitleaks-custom
Draft

sc979 wants to merge 21 commits into
mainfrom
SECU-gitleaks-custom

Conversation

@sc979

@sc979 sc979 commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

What are your needs or what are you planning to do in this PR?

enh(secu): manage invalid-revision range error

Resolves the "Invalid revision range" failures on the gitleaks job by replacing
gitleaks/gitleaks-action with a pinned gitleaks binary (8.30.1, verified against a
sha256 pinned in the workflow) driven by a script that resolves the scan range
defensively (authenticated re-fetch + retry), scans every commit of the analyzed
branch that is not already on the destination branch
, and fails closed.

Quality gate

Every non-merge commit of the analyzed branch is scanned. Only the commits already on
the destination branch are excluded, whether or not they are also on other branches
(load, performance or security test branches…), so no new secret can reach the
destination unnoticed.

Destination branch Analyzed branch Gate
1 or more secrets 0 secrets ✅ OK
any 1 or more secrets ❌ KO

The workflow assumes no branch name (main, develop, dev-YY.MM.x…): the
destination comes from the event itself.

Supported events: pull_request (the quality gate) and workflow_dispatch (mostly to
debug the pipeline). Org usage, checked on 2026-10-07: 73 of the 76 non-archived
repositories (centreon + quanta-computing) call gitleaks-analysis.yml@main through
secu-secret-scan.yml, on exactly these two events. In this repository,
security-checks.yml now runs on these two events only (its push and schedule
triggers are removed). centreon/.github has no caller, and
quanta-computing/cxm-zone-probe is empty.

The branch is up to date with main (merged, not rebased, to keep the PR history):
it keeps actions/checkout v7.0.1 with persist-credentials: false (#69, #71) and
the shallow PR checkout from #74 (fetch-depth = PR commits + 2, full clone otherwise).
The job token is scoped to contents: read only: the script calls no GitHub API, so
the pull-requests: read added for the action in #72 is dropped.

Scan range per event

The range is passed to gitleaks as --log-opts and validated with git log first.
Every range carries --text, so a repository's .gitattributes (e.g. * -diff)
cannot hide file contents from the scan.

Event Range
pull_request base..head, --no-merges: every commit not on the PR base, including commits merged in from other branches (gitleaks-action's --first-parent skipped them), excluding the base commits merged into the PR. A PR with an unrelated history scans its own history
workflow_dispatch the ref's whole history, --no-merges: no destination to exclude
pull_request_target job always fails, from a first step that runs before checkout: the event is not compliant with our security posture (the scan script refuses it too)
any other event (push, schedule, merge_group…) job fails: not supported

An empty range (e.g. a PR whose head is already on its base) passes with a notice,
without running gitleaks.

Shallow PR checkout (#74)

Pull requests are checked out with fetch-depth = PR commits + 2, which holds every PR
commit. When the base branch moved further than that since the PR forked, the base side
does not reach the shared history yet, and a shallow boundary commit looks parentless:
gitleaks would scan its whole tree and report every pre-existing secret. So, for
pull_request, the range is trusted only once base..head holds no parentless commit:

  • only the base side is deepened (--depth = PR commits + 1 + 32, 64 … 1024 from the
    base SHA, through the same authenticated fetch), so the PR side is not fetched again;
    --unshallow is the last resort (e.g. unrelated histories);
  • a deepen fetch that brings nothing stops the round, and the retry loop below takes over.

In the tests, with main 100 commits ahead of a 1-commit PR, 3 deepen rounds fetch
about 130 base commits, and the clone stays shallow. workflow_dispatch keeps a full
clone (depth 0).

Re-fetch when hashes changed after checkout

actions/checkout pins its refs and can't be re-run in a loop. When the event's SHAs
are missing from the clone, the step re-fetches and retries, up to 5 attempts with a
5/10/15/20 s backoff:

  • pull_request → refs/pull/<n>/head, the base branch, and the event's head/base
    SHAs if still served. A head (or base) SHA force-pushed away falls back to the live
    PR tip (or base branch tip), with a ::warning::.
  • workflow_dispatch → all branch heads and the dispatched SHA. If it was
    force-pushed away, falls back to the live tip of the branch, with a ::warning::
    (branches only: a tag never falls back to a same-name branch).

Failed fetches are logged as ::warning::. If the range is still unresolvable, the job
fails and points to those warnings.

Credentials stay unpersisted. persist-credentials: false is kept as a hardening
measure, so the clone holds no token. The re-fetches authenticate with the job token,
set on each git fetch process only through env-scoped git config
(GIT_CONFIG_COUNT / KEY_0 / VALUE_0): never written to .git/config, never on a
command line, masked in the logs, and not exported to the gitleaks process.

Build outcome

Situation Job
No secrets in the scanned commits (PR: even with secrets on the destination) ✅ success
No commit to scan ✅ success
Secrets found in the scanned commits (gitleaks exit 2) ❌ fail
gitleaks error (any other exit, e.g. a broken .gitleaks.toml) ❌ fail — fail closed, the scan did not complete
Range unresolvable after all retries ❌ fail
pull_request_target ❌ fail, before checkout
Other event (push, schedule…) ❌ fail

Test coverage

Both run: steps are extracted verbatim from the workflow and run with bash -e,
as GitHub does. The scan runs against throwaway repos whose remote is served over smart
HTTP and rejects any request without the job token: a runner-like full clone
(fetch-depth: 0, no persisted credentials, detached HEAD) with commits pushed after
the clone, and a shallow PR checkout (merge commit fetched at depth PR commits + 2, as
actions/checkout does, main 100 commits ahead of the fork point).

Area Cases
Quality gate (real gitleaks 8.30.1) PR: secrets on main and dev-26.10.x + clean branch → OK; branch leak → KO, only the branch's file reported; after merging main that brought a new secret → OK; unrelated history → OK when clean, KO when it leaks. Dispatch: whole history scanned (KO on any secret in it, OK on a clean orphan branch)
Range selection PR: base commits excluded, merged-in side branch scanned, unrelated history scans its own commits; dispatch on a branch and on a tag; head / base / dispatched SHA missing from the clone → retry
Shallow PR checkout (real gitleaks) clean PR with main 100 commits ahead → pass, 3 base-side deepen rounds, no boundary commit scanned, no unshallow; PR's own leak reported (not main's); main merged in → main's secret not reported; leaking side branch reported; unrelated history with secrets on base → unshallow, pass; deepen with an invalid token → fail closed, one fetch per attempt, token not in argv
Re-fetch (authenticated remote) negative control without token; PR head pushed after checkout; PR head / PR base / dispatched SHA force-pushed away → live tips; head and ref both gone → clean failure, no garbage in variables; backoff 5/10/15/20 s; invalid token → ::warning:: + failure; tag dispatch with its SHA gone → no fallback to a same-name branch
Fetch depth step PR → commits + 2; other events → 0; checkout uses it; step order (reject, depth, checkout, install, scan)
Token hygiene not under .git/, not in any git argv, not in gitleaks' environment, base64 header masked
Events push, schedule, merge_group → fail (unsupported); pull_request_target rejected by the first step (before checkout) and by the script; security-checks.yml triggers on PR and dispatch only
Exit codes (stub) 0 pass; 2 fail; 1 / 126 fail; empty range → pass without running gitleaks
Real gitleaks 8.30.1 .gitattributes * -diff doesn't hide a leak (PR and dispatch); broken .gitleaks.toml → job fails
Install step pinned digest → installs 8.30.1, download dir cleaned up; wrong digest → step fails, nothing installed

Result: 107 / 112 passed. The 5 failures are the install-step cases: the test
machine's network intercepts TLS to GitHub (curl exit 60, self-signed certificate),
so the release could not be downloaded there. The same cases passed on 2026-10-02, and
the real-gitleaks cases ran with the 8.30.1 binary verified against GITLEAKS_SHA256.

Not covered: a real GitHub runner (the PR's own security-checks run calls
gitleaks-analysis.yml@main, not this branch).

🤖 Generated with Claude Code

@sc979
sc979 requested a review from a team as a code owner July 28, 2026 09:26
@sc979
sc979 requested review from Tpo76 and kduret July 28, 2026 09:26
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml
Co-authored-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
@sc979
sc979 marked this pull request as draft July 28, 2026 12:18
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Co-authored-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
@sc979
sc979 marked this pull request as ready for review July 29, 2026 09:25
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
sc979 added 2 commits July 29, 2026 11:27
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
sc979 added 2 commits July 29, 2026 12:03
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
already managed by the triggering pipeline. causing failing loop

Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
sc979 and others added 2 commits July 29, 2026 12:11
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
On a push that creates a new branch (or rewrites history), the
gitleaks range fell back to a full-history scan. Because a branch
usually forks off the default branch, that re-scanned commits already
living in the default branch and re-reported pre-existing secrets that
the branch never introduced.

Resolve the range against the default branch instead: scan only
merge-base(origin/<default>, after)..after, i.e. the commits unique to
the branch. The empty-"before" case now follows the same path, so it is
consistent with a real new branch. When the default branch cannot be
resolved, fall back to a full scan (over-scan is the safe direction).

Also correct the range-resolved log to ::notice:: (::info:: is not a
valid workflow command).

Assisted-by: Claude Code (claude-opus-4-8)
sc979 and others added 2 commits October 2, 2026 12:04
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since persist-credentials: false, the re-fetch that recovers commits
missing from the clone ("invalid revision range") had no credentials
and could never succeed. Re-fetches now authenticate with the job token,
passed to each git process only through env-scoped config: nothing is
written to .git/config, shown on a command line or exported to
gitleaks. The PR base, the pushed SHA and tag pushes are handled too,
with a backoff between attempts and a warning on every fallback.

Make the scan cover what the change under test brings, and fail closed:
- PR scans include commits merged in from other branches, and
  .gitattributes can no longer hide file contents (--text)
- a rewritten default branch gets a full scan instead of an empty range
- gitleaks errors, unsupported events and unresolvable ranges fail the
  job; pull_request_target always fails, before checkout, as it is not
  compliant with the security posture; branch deletions pass
- the gitleaks binary is verified against a sha256 pinned in the
  workflow, and the job token is scoped to contents: read

Also document the workflow in CLAUDE.md and drop stray whitespace from
the PR template.

Assisted-by: Claude Code (claude-opus-5-5)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@sc979
sc979 requested a review from a team as a code owner October 5, 2026 17:37
@sc979
sc979 requested a review from BaptisteCentreon October 5, 2026 17:37
@sc979

sc979 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

✅ Automated Review — Passed

This PR was reviewed using the Centreon automated review skill.

Complexity: high — Recommended reviewers: 2

No blocking issues found. Ready for human review.

Security review: no findings.

Areas that deserve careful human attention:

  • .github/workflows/gitleaks-analysis.yml — compute_range() (range selected per event) and refetch() (authenticated re-fetch with persist-credentials: false).
  • The job fails closed: gitleaks errors, unresolvable ranges and unsupported events fail it, and pull_request_target always fails before checkout.
  • This PR's own security-checks run calls gitleaks-analysis.yml@main, so CI does not exercise the new workflow. Every consumer picks it up on merge.

@sc979
sc979 marked this pull request as draft October 7, 2026 08:41
sc979 and others added 6 commits October 7, 2026 11:26
Bring in #74 (shallow PR checkout with a computed fetch depth, faster
blocklist scan). The gitleaks conflict keeps the pull_request_target
guard as the first step, then the fetch-depth step, and keeps the job
token scoped to contents: read.
Since #74 the PR checkout is shallow (fetch-depth = PR commits + 2).
When the base branch moved further than that since the PR forked, the
merge-base was unreachable: the scan concluded "no shared history" and
ran a full scan of the shallow clone, where gitleaks reads boundary
commits as roots and reports every pre-existing secret in the tree.
A clean PR then failed on secrets already on the base branch.

The scan now deepens the PR history (32 to 1024 commits, doubling)
until the fork point is reachable from both sides, unshallows as a
last resort, and never scans a range holding a shallow boundary
commit. "No shared history" is only concluded on a complete clone.
Deepening goes through the authenticated fetch and stops when a fetch
brings nothing, so the existing retry loop takes over.

Also shorten the pull_request_target error message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
Secrets already on the destination branch are that branch's own
business: the scan must fail only when one of the analyzed branch's
commits holds a secret, whatever the destination is called. Several
ranges broke that rule: an unrelated-history PR, a rewritten or created
default branch, workflow_dispatch and schedule scanned the full history
of every branch; a new branch was compared with the default branch only
(wrong for branches cut from dev-YY.MM.x); and a push merging the
destination back in rescanned its commits.

- pull_request scans base..head, the commits not on the PR base.
- push, workflow_dispatch and schedule scan the ref's commits that are
  on no other branch (nor before the push), excluded through
  --remotes so the range does not grow with the branch count. An empty
  range passes without running gitleaks.
- On the shallow PR checkout, only the base side is deepened, from the
  base SHA, until base..head holds no parentless commit, so the PR side
  is not fetched again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
A commit must be scanned until it is on the destination branch, even
when another branch (load, performance or security tests...) already
holds it: excluding every other branch let such commits through, e.g.
a dispatch on a branch whose commits sit on feature branches, or one
push updating two branches.

- push to an existing branch scans before..after, every pushed
  non-merge commit; a "before" missing after a force-push is fetched
  once while the server still serves it.
- push creating a branch (or whose "before" is gone),
  workflow_dispatch and schedule have no destination nor "before" to
  exclude, so they scan the ref's whole history.
- pull_request is unchanged: base..head.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
The secret scan is a pull request gate; workflow_dispatch is kept to
debug the pipeline. Push and schedule are no longer supported: the job
fails closed on them, like on any other unsupported event, and
security-checks.yml only calls the secret scan on pull_request and
workflow_dispatch (its push and schedule runs keep the dependency
scan).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
Align security-checks.yml with the supported scan events: drop the push
and schedule triggers, so the per-job event filter on the secret scan is
no longer needed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
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.

1 participant