Skip to content

fix(gitleaks): resolve the org ruleset at the caller's pinned ref - #37

Merged
CameronBrooks11 merged 3 commits into
mainfrom
fix/pin-gitleaks-config-to-caller-ref
Oct 17, 2026
Merged

CameronBrooks11 merged 3 commits into
mainfrom
fix/pin-gitleaks-config-to-caller-ref

Conversation

@CameronBrooks11

@CameronBrooks11 CameronBrooks11 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

The mechanism, precisely

gitleaks.yml is consumed by every active repo in the org but this one, and
almost all of them pin it by digest:

$ for every non-archived repo: read .github/workflows/secret-scan.yml, extract the `uses:` ref
     63  gitleaks.yml@3fa1141   (2026-10-05)
      2  gitleaks.yml@df3d01b   (2026-07-06)
      2  gitleaks.yml@a53251c   (2026-09-28)
      1  gitleaks.yml@fdd0291   (2026-09-05)
      1  gitleaks.yml@main
     ---
     69 callers across 70 non-archived repos; the 70th is this repo, via a local `./` ref

That digest does freeze the detector: gitleaks-version is a
workflow_call input default: inside the pinned file, so a repo pinned at
df3d01b runs gitleaks 8.21.2 and one pinned at 3fa1141 runs 8.30.1.
Measured:

3fa1141  binary=default: "8.30.1"   pit-rule-present=1   cfg=4913 bytes
df3d01b  binary=default: "8.21.2"   pit-rule-present=0   cfg=1399 bytes
a53251c  binary=default: "8.30.1"   pit-rule-present=1   cfg=4913 bytes
fdd0291  binary=default: "8.21.2"   pit-rule-present=1   cfg=4913 bytes

The digest did not freeze the rules that detector ran with. The fetch step
was hardcoded to a branch:

curl -fsSL "https://raw.githubusercontent.com/FlowMatrix-AI/.github/main/.gitleaks.toml"

So the pin was half a pin, and the uncovered half failed in the open direction.
Broadening the shared [allowlist] here shrank detection in every calling repo
on its next scan — no commit, no PR, no review and no reviewer in any of those
repos, and every required gitleaks / gitleaks check green throughout. For a
required security check that is the wrong direction to fail: the thing that
changed was invisible precisely where it mattered.

Is this a deliberate design with a recorded reason?

Checked before changing it, because the hazard is written down —
eng-governance/github/security-config.md has a section "The config channel:
.github is load-bearing for the whole fleet" listing "three consequences,
recorded so they are decisions rather than surprises", and this repo's
README.md and AGENTS.md both describe it.

It reads as an acknowledged hazard, not a ratified design. The other two
consequences in that list are explicitly accepted ("the visibility choice is
effectively one-way", "the fetch is deliberately fail-closed … Accepted
risk:
…"). This one is not: it states the fail-open and then records that the
mitigation is not in place — "code-owner review is not yet required there
(that is a ruleset decision, tracked on #111)". Nothing anywhere gives a reason
to prefer a floating config; the one place it is reasoned about names a
different, unimplemented fix. So: changed, and the governance doc needs a
follow-up edit that cannot be made from this repo.

What changed, and the trade-off

Fetch the ruleset at job.workflow_sha — the commit SHA of the workflow
file that defines the running job, i.e. this file at the ref the caller asked
for — instead of from main.

The three options, and why this one:

Option Integrity Cost
Resolve at the caller's ref (chosen) A consumer's detection behaviour cannot change without a change to a ref that consumer controls. A ruleset fix reaches a pinned repo only when its pin moves (Renovate's digest PR).
Vendor the ruleset into the workflow file Identical — the digest covers it either way. Strictly worse ergonomics: .gitleaks.toml stops being a standalone reviewable file with its own CODEOWNERS entry, and .pre-commit-config.yaml can no longer run the same rules locally. No integrity gain over the above.
Keep it floating, add a loud summary line None. A summary line is a notice after the fact, in a log nobody reads on a green check. It does not stop a weakening from landing fleet-wide unreviewed. Cheap, and that is all.

Chosen because it is the only one of the three in which a weakening of
detection becomes visible
: it has to arrive as a commit in the consuming
repo. It also makes the pin internally consistent — the detector and the rules
it runs are now frozen by the same digest, instead of one being frozen and the
other floating.

The summary line is kept as well, but as a record rather than the control: each
run now writes the ruleset commit and its sha256 to the job summary, so what a
scan ran with is legible from the run itself.

Unchanged for unpinned callers: job.workflow_sha on a branch ref resolves to
that branch's head, so a @main caller behaves exactly as before. Only a
pinned caller changes, and it changes to what its pin already claimed.

Cost, measured rather than asserted

67 of the 69 callers (the 63 at 3fa1141, the 2 at a53251c, the one at
fdd0291, and the @main one) resolve a byte-identical ruleset today —
4913 bytes, gohighlevel-pit present — so for them this is a no-op that
removes a hazard.

The exceptions are the two repos pinned at df3d01b (2026-07-06), which would
resolve the July ruleset: 1399 bytes, no gohighlevel-pit rule. Those two
need a repin
— they are identifiable from that digest. Note what their pin
already does to them: they run gitleaks 8.21.2, from October 2024. Under the old
behaviour they got current rules executed by a year-old binary and a green
check; the staleness was real either way, it was just split across two places
with only one of them visible. Repinning fixes both halves at once.

Also fail-closed, more so

An empty job.workflow_sha now aborts the step rather than falling back to a
branch, because an unknown ruleset version is the one thing the scan must not
run with. curl -f with no fallback is unchanged.

Dry-run of the step body, three cases:

# 1. a real pinned sha, no repo-local config
EXIT 0
| workflow ref        | `FlowMatrix-AI/.github/.github/workflows/gitleaks.yml@3fa1141` |
| org ruleset commit  | `3fa114141c2441d4a631ec579af388168d824bf2` |
| org ruleset sha256  | `c71afc1e51fa0cdfdee20818a1388356a639bbe15dbc58ce2e015a5cb08eca3d` |
| repo-local .gitleaks.toml | no |
  -> .gitleaks.org.toml (4913 bytes) and an identical .gitleaks.toml

# 2. empty job.workflow_sha
CONFIG_SHA: job.workflow_sha was empty; refusing to resolve the org ruleset
EXIT 1

# 3. unresolvable sha
curl: (22) The requested URL returned error: 404
EXIT 22

The repo-local-config branch is preserved: a repo that already has
.gitleaks.toml keeps it and inherits the org one through
[extend] path = ".gitleaks.org.toml", exactly as before.

raw.githubusercontent.com serves a commit SHA that is not on a branch tip,
including a pull request's merge commit — checked against both the head and
merge SHAs of an open PR in this repo (200 for each), so the pull_request
case resolves.

Validated: the workflow parses as YAML, all three run: blocks pass bash -n,
and this PR's own secret-scan run exercises the change through the local ./
ref — which is the live measurement of job.workflow_sha, since the step fails
closed if it is empty.

Docs corrected in the same PR

README.md and AGENTS.md both asserted the old behaviour in prose, so they are
updated in the same commit rather than left to contradict the workflow.

A second commit corrects an unrelated false claim found while measuring the
callers: AGENTS.md said 68 of 70 active repos call the workflow and named one
repo as a non-caller. That repo does call it, pinned at 3fa1141; 69 of 70
carry a caller and the only repo without one is this one. Reworded to the
invariant rather than a count that rots.

Review notes

  • This changes detection behaviour across the fleet. Auto-merge has
    deliberately not been enabled
    — the repo allows auto-merge and requires 0
    approving reviews, so enabling it would self-merge on green.
  • Merge order matters. feat(gitleaks): detect gohighlevel webhook capability urls #36 adds a detection rule. Under today's behaviour it
    reaches every caller the moment it merges; once this PR merges, it reaches a
    pinned caller only when that pin moves. Merge feat(gitleaks): detect gohighlevel webhook capability urls #36 first.
  • Follow-ups this PR cannot do: repin the two repos still on df3d01b, and
    update eng-governance/github/security-config.md consequence 2, which still
    documents the unpinned fetch.

The reusable workflow fetched `.gitleaks.toml` from `main` on every run
while the detector version it runs with is an input default in the
workflow file itself. A caller's `gitleaks.yml@<digest>` therefore froze
the binary but not the rules, and the uncovered half failed open:
broadening the shared allowlist shrank detection in every calling repo
on its next scan, with no commit, no PR and no review in any of them,
and every required check stayed green.

Fetch the ruleset at `job.workflow_sha` — the commit of the workflow
file defining this job, which is the caller's ref — so the pin covers
detection behaviour. A caller on `@main` or any branch is unchanged,
since that resolves to the branch head; only a pinned caller changes,
and it changes to what its pin already claimed.

Keep the fetch fail-closed and widen it: an empty `job.workflow_sha`
aborts the step rather than falling back to a branch, because an unknown
ruleset version is the one thing the scan must not run with.

Also write the ruleset commit and its sha256 to the job summary, so what
a run scanned with is legible from the run and not only from a diff in
another repository.

The cost: a ruleset fix reaches a pinned repo only when its pin moves.
Measured across the 69 callers — 67 resolve a byte-identical ruleset
today, and the 2 that do not are pinned to a July commit that also
freezes them on gitleaks 8.21.2, so they were already scanning with a
year-old binary against current rules. Those two need a repin.

Update README.md and AGENTS.md, which both asserted the old behaviour.
AGENTS.md said 68 of 70 active repos call the reusable workflow and
named one repo as a non-caller. Measured while verifying the pin change:
69 of the 70 non-archived repos carry a caller, that one included, and
the only repo without one is this one, which calls it through a local
ref.

Reworded so the claim is the invariant — every active repo but this one
— rather than a count that rots as repos are added.
The job summary records which ruleset a run scanned with, but a summary
is not reachable over the API. Print the same line to the log as well,
so the ruleset in force can be audited across every calling repo from
run logs instead of by opening each run page.
@CameronBrooks11
CameronBrooks11 force-pushed the fix/pin-gitleaks-config-to-caller-ref branch from 8aa9e0c to f9ce275 Compare October 17, 2026 20:30
@CameronBrooks11
CameronBrooks11 merged commit 6874375 into main Oct 17, 2026
2 checks passed
@CameronBrooks11
CameronBrooks11 deleted the fix/pin-gitleaks-config-to-caller-ref branch October 17, 2026 21:09
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