Skip to content

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

Merged
sc979 merged 28 commits into
mainfrom
SECU-gitleaks-custom
Oct 14, 2026
Merged

sc979 merged 28 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 the gitleaks binary (reused from the runner or installed
and verified) 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.

Includes SECU-1249 (#76, merged into
this branch): gitleaks install aligned on the centreon-ubuntu-26.04 runner images.

Tests

centreon/centreon-documentation#5710
centreon/centreon-documentation#5711
https://github.com/centreon/delivery-tooling/pull/443

Tasks covered

  1. Invalid revision range: event SHAs missing from the clone are re-fetched with
    the job token (env-scoped git config, never on disk nor on a command line), with a
    5-attempt retry and live-tip fallbacks.
  2. Quality gate on the branch's own commits: a PR scans base..head, whatever the
    destination is called; secrets already on the destination never fail it.
  3. Shallow PR checkout (ci(secu): speed up dependency and gitleaks scans #74): only the base side is deepened until the range is
    trustworthy, so no shallow boundary is scanned as a root.
  4. Supported events: pull_request and workflow_dispatch only (whole ref history
    on dispatch); pull_request_target rejected before checkout; any other event fails.
    security-checks.yml runs on these two events only.
  5. Fail closed: leaks, gitleaks errors, unresolvable ranges and unsupported events
    fail the job; .gitattributes cannot hide file contents (--text).
  6. Least privilege: the job token is scoped to contents: read.
  7. gitleaks install (SECU-1249): same version rule as the runner images (latest
    release published at least 4 days ago, read from the public releases feed without
    any token), runner binary reused when it matches, release verified against its
    checksums file, 8.30.1 fallback when the feed cannot be resolved.
  8. Docs: CLAUDE.md describes the workflow and the Ubuntu 26.04 runners from feat(ci): run the centreon scans on the sized ubuntu 26.04 runners #75.

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 rebased on main (after #75): it keeps the runners of #75,
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.

gitleaks install

Same rule as runners/base-ubuntu-26.04/Dockerfile in centreon-images (#390):

  • GITLEAKS_VERSION: "latest" is the newest vX.Y.Z release published at least 4 days
    before the run, read from the public releases feed (releases.atom): no token, no
    GitHub API call
    . A version can still be pinned.
  • If the feed cannot be resolved, GITLEAKS_FALLBACK_VERSION (8.30.1) is used, with a
    gitleaks-version-fallback warning, so the run is not blocked.
  • If the runner's gitleaks version matches, the runner's binary is kept (the
    centreon-ubuntu-26.04-* runners ship it). Otherwise the release for the runner's
    architecture (x64 / arm64) is installed and verified against the release's
    checksums file; a checksum mismatch fails the job.

Runner requirements for the install step: curl, python3, GNU date, sha256sum,
tar (present on ubuntu-26.04 and the centreon-ubuntu-26.04 images; to check on
quanta-security).

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 latest resolution (pre-release tags and releases under 4 days skipped), runner binary kept when it matches (8.30.1 / v8.30.1) and replaced otherwise (8.31.0, missing, distro build), pinned version (no feed read), feed fallback to 8.30.1 with a warning, checksum mismatch / missing checksums file → fail, arm64 tarball, unsupported architecture → fail, no token sent

Result: 131 / 131 passed on the rebased branch. Release downloads are served by a
curl stub from the real 8.30.1 tarball and checksums file; the feed parser also returns
8.30.1 on the real feed.

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
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
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
@sc979
sc979 marked this pull request as ready for review July 29, 2026 09:25
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
@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 4 commits October 12, 2026 17:29
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 and others added 19 commits October 12, 2026 17:29
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
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>
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)
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>
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)
The runner paragraph still named centreon-security and ubuntu-24.04:
describe the runners selected since #75, and that internal
repositories get the self-hosted runner too.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
Some runners (centreon-ubuntu-26.04-small) already ship gitleaks. Keep
it when `gitleaks version` reports the pinned GITLEAKS_VERSION, and
install the pinned release, verified against GITLEAKS_SHA256, in every
other case: gitleaks missing (e.g. the public ubuntu-26.04 runner) or
any other version.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
Use the rule of the centreon-ubuntu-26.04 runner images
(centreon-images, runners/base-ubuntu-26.04/Dockerfile), so the private
runner's preinstalled gitleaks is the expected one and gets reused:
GITLEAKS_VERSION "latest" is the newest stable release published at
least 4 days before the run, and a version can still be pinned. When
the runner's gitleaks differs, the release for the runner's
architecture is installed and verified against its checksums file.

The GitHub API is called with the job token, passed through curl's
config on stdin, to avoid the anonymous rate limit shared behind an
egress IP.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
The latest-release lookup sent the job token to the GitHub API only to
lift the anonymous rate limit. Read the public releases feed instead:
it needs no token and no API quota, and its <updated> date is at or
after the publication date, so the 4-day delay still holds. Drafts are
not in the feed and only vX.Y.Z tags are kept. jq is no longer needed.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
An unreachable releases feed failed the job, blocking every pull
request of the org. Install GITLEAKS_FALLBACK_VERSION (8.30.1) instead,
still verified against its checksums file, and emit a
gitleaks-version-fallback warning so the issue can be found and
investigated.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
Since #75, pull request scans run on centreon-ubuntu-26.04-slim: both
the slim and small runners are built from base-ubuntu-26.04, which
ships gitleaks.

Refs: SECU-1249

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (claude-opus-5-5)
@sc979
sc979 force-pushed the SECU-gitleaks-custom branch from bdd0adb to 56075db Compare October 12, 2026 15:36
@sc979
sc979 marked this pull request as ready for review October 13, 2026 15:59
Comment thread .github/workflows/gitleaks-analysis.yml
Comment thread .github/workflows/gitleaks-analysis.yml Outdated
sc979 and others added 3 commits October 13, 2026 18:04
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Signed-off-by: Stéphane Chapron <34628915+sc979@users.noreply.github.com>
Runners without the expected gitleaks (e.g. GitHub-hosted ubuntu-26.04)
failed the install with a bare curl 404: the runner architecture
detection had been dropped, so the tarball URL had an empty
architecture.

Restore it, and enable nounset in the install step so an undefined
variable fails with an explicit error instead of a broken URL.

Assisted-by: Claude Code (claude-opus-5-5)
@sc979
sc979 merged commit 3d05407 into main Oct 14, 2026
5 checks passed
@sc979
sc979 deleted the SECU-gitleaks-custom branch October 14, 2026 12:51
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