Skip to content

security: add gitleaks allowlist — greens the now-real secret gate on main#512

Merged
hyperpolymath merged 3 commits into
mainfrom
security/gitleaks-allowlist
Jul 21, 2026
Merged

security: add gitleaks allowlist — greens the now-real secret gate on main#512
hyperpolymath merged 3 commits into
mainfrom
security/gitleaks-allowlist

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Follow-up to #511main is currently red because of it.

#511 merged at its first commit, so the caller fix landed without this allowlist. main's Secret Scanner therefore went straight from startup_failure (dead — zero jobs, since 2026-07-06) to failure (real gate, 15 false positives). This completes the change.

Net effect of #511 + this PR: the scanner goes from not executing at all, to executing and passing honestly.

The 15 findings — all verified false positives

Where Value Why benign
deno.lock ×8 hex digests SHA-256 integrity hashes of public registry modules
lol/analysis/Project.toml UUID Julia General-registry package UUID
docs/audits/otpiser-…a2ml 40-hex OpenPGP fingerprint — published by design
rhodium…/haskell-registry-design.md Ed25519KeyPair a Haskell type name, not a value
avow-lib/…/example.zig abc123def456... commented // Would be real token
telegram-bot/NEXT-STEPS.md ×2 1234567890_abc123... inside a fenced ASCII terminal mockup
…/examples.ndjson 0a1b2c3d4e5f… ascending-nibble filler walk

This wall of noise is almost certainly what the original continue-on-error: true was papering over — turn the gate on with no allowlist, get 15 reds that are entirely noise, someone reverts, and the estate is back to a scanner that cannot fail. So this allowlist is the unblocker for re-pinning callers estate-wide, not merely local cleanup.

Design rule (documented in the file — please keep it)

Allowlist by pattern class, anchored to the whole value. Never allowlist a path because "that file is fine", never disable a rule. paths is confined to formats that cannot hold a live credential (lockfile integrity hashes, dependency UUIDs).

Every regex is anchored ^...$ deliberately. An earlier draft used unanchored (?i)test|example and canary-testing caught that it silently allowlisted the real AWS key wJalrXUtnFEMI/K7MDENG/bPxRfiCY… purely because "EXAMPLE" appears inside it. Anchoring fixed it.

Verification against origin/main

gitleaks detect --exit-code 1   ->  no leaks found (exit 0)

with planted realistic secrets still detected: github-pat, gitlab-pat, slack-bot-token, stripe-access-token, generic-api-key (AWS).

Also clears all 8 of boj-server-cartridges' equivalent fixture findings with no repo-specific additions (see boj-server-cartridges#99) — the evidence it works as the estate baseline template, not a standards-only patch.

🤖 Generated with Claude Code

hyperpolymath and others added 2 commits July 21, 2026 05:12
Follow-up to #511. That PR merged at its first commit, so the caller fix
landed without this allowlist and main's Secret Scanner went straight from
startup_failure (dead) to failure (real gate, 15 false positives). This
completes the change.

Net effect of #511 + this commit: the scanner goes from not executing at all
to executing and passing honestly.

All 15 findings were verified individually as false positives:

  * deno.lock x8      — SHA-256 integrity hashes of public registry modules
  * Project.toml      — a Julia General-registry package UUID
  * otpiser audit     — a 40-hex OpenPGP fingerprint (published by design)
  * haskell design md — the type name in `data Ed25519KeyPair = Ed25519KeyPair`
  * example.zig       — `abc123def456...`, commented "// Would be real token"
  * NEXT-STEPS.md x2  — `1234567890_abc123...` in a fenced terminal mockup
  * examples.ndjson   — `0a1b2c3d4e5f...`, an ascending-nibble filler walk

This wall-of-noise is almost certainly what the original `continue-on-error:
true` was papering over. So the allowlist is the *unblocker* for re-pinning
callers onto the real gate estate-wide, not merely local cleanup.

Design rule, documented in the file: allowlist by pattern class, anchored to
the whole value; never allowlist a path because "that file is fine" and never
disable a rule. `paths` is confined to formats that cannot hold a live
credential (lockfile integrity hashes, dependency UUIDs).

Every regex is anchored ^...$ deliberately. An earlier draft used unanchored
`(?i)test|example`, and canary-testing caught that it silently allowlisted the
real AWS key wJalrXUtnFEMI/K7MDENG/bPxRfiCY... purely because "EXAMPLE"
appears inside it. Anchoring fixed it. Verified against origin/main:

  scan                      -> no leaks found (exit 0)
  planted realistic secrets -> still detected: github-pat, gitlab-pat,
                               slack-bot-token, stripe-access-token,
                               generic-api-key (AWS)

Also clears all 8 of boj-server-cartridges' equivalent fixture findings with
no repo-specific additions (see that repo's PR #99), which is the evidence it
stands up as the estate baseline template rather than a standards-only patch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hypatia flagged .gitleaks.toml itself — "Secret found: GitLab PAT" — because
the comment documenting the repeated-character mask contained a literal
masked token. The allowlist file tripping the estate's own secret detector is
a neat demonstration of the problem it exists to solve.

Describe the shape in prose instead, and record the rule in the file so the
next editor does not reintroduce a literal example token.

Behaviour is unchanged: the regex is untouched. Re-verified — standards scans
clean (exit 0) and planted github-pat / gitlab-pat / slack-bot-token /
stripe-access-token values are all still detected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hyperpolymath

Copy link
Copy Markdown
Owner Author

Independent verification — this config holds up, and the anchoring warning is not theoretical.

I built a .gitleaks.toml for #504 in a parallel session before seeing this PR, then discarded mine in favour of yours. Recording the evidence, since it corroborates the specific design rule you documented.

My draft had the exact hole you describe. I used regexTarget = "line" with unanchored alternations (example[_-]?(key|token|secret), and a rule for trailing ...). Canary-tested against gitleaks 8.18.4 — the exact CI build, sha256sum matched to the pinned tarball:

fixture gitleaks defaults my draft
ghp_16C7e42F292c6912E7710c838347Ae178B4a caught caught
same PAT, line also contains EXAMPLE_KEY caught suppressed
same PAT, line ends # ... caught suppressed

A live GitHub PAT silently allowlisted because the line mentioned "EXAMPLE". Matching the line instead of the value is the root error; your ^…$-on-the-secret approach is structurally immune to it, not merely tidier.

Your config, same harness:

  • all four adversarial fixtures still caught (clean, EXAMPLE_KEY-tainted, trailing-..., plus slack-bot-token + stripe-access-token)
  • gitleaks detect --source . --no-git --exit-code 1 against the branch → no leaks found, exit 0

So it clears the 15 without weakening detection on any case I could construct. Deferring to this PR; no competing file from me.

One note for whoever re-pins callers afterwards: paths here covers deno.lock, package-lock.json, yarn.lock, Cargo.lock, Project.toml, Manifest.toml. Estate repos using pnpm, Nix, Composer, Bundler, Poetry or Hex will surface the same lockfile-digest class under pnpm-lock.yaml / flake.lock / composer.lock / Gemfile.lock / poetry.lock / mix.lock. Worth adding pre-emptively rather than one red repo at a time — same justification, no new risk surface.

Context: #514 fixes the two remaining main-red roots (Hypatia startup_failure #451, and the scorecard verify false-red #381). It deliberately ships no .gitleaks.toml.

hyperpolymath added a commit that referenced this pull request Jul 21, 2026
…this repo only) + scorecard-verify false-red (new) (#514)

Completes the run to green on `main`, after #511 (secret-scanner caller
+ SPDX) and #512 (gitleaks allowlist). Two remaining causes, independent
of each other and of those PRs.

## 1. Hypatia has never run — the caller under-grants (#451)

`hypatia-scan-reusable.yml` declares `security-events: write` (it
uploads SARIF). `hypatia-scan.yml` granted `security-events: **read**`.

A called workflow may never request more than its caller grants. When it
does, GitHub rejects the run at **startup** — before any job exists:

```
completed  startup_failure  Hypatia Security Scan  main  push  1s
$ gh run view 29764318317 --log-failed
failed to get run log: log not found          # <- there is no log; no job ran
```

Granting `read` did not degrade the scan to a no-upload mode. **It
stopped the scan from existing.**

The reusable carried this comment:

```yaml
continue-on-error: true    # callers granting only security-events:read skip gracefully
```

That is a false mental model, and it is load-bearing — it is the reason
the mismatch looked intentional. Nothing skips gracefully in a job that
was never created. Corrected in place.

This is exactly the #451 pattern that startup-fails reusable-workflow
callers estate-wide. **Fixed here on the canon's own caller only** —
deliberately demonstrated on one repo before any propagation is
proposed.

## 2. Seven "fake passes" that were every one of them real (#381)

`registry-verify.yml` runs `build-scorecards.sh --verify`, which
**executes** every pass-row's check. The job installed no tools, and
`ubuntu-latest` ships neither ripgrep nor xmllint:

| rows | mechanism | observed |
|---|---|---|
| `k9-svc/M6` | calls `xmllint` directly | exit **127** |
| 6 × `release-pre-flight/*` | invoke `v1-audit.sh`, which hard-exits 2
without ripgrep, so the greps match nothing | exit **1** |

The verifier reported all seven as **"claimed PASS but the pass is not
real"**. Reproduced locally by stripping `rg` from `PATH` — identical
output.

Measured with the tools present:

```
── verify: 87 grounded pass · 0 broken pass · 0 unrunnable · 0 self-asserted · 0 stale-fail
EXIT=0
```

**All seven claims were true.** The checks had never run. A false-red is
not harmless: it trains maintainers to ignore a gate — the same end
state as the fake-green gates #500 removed, reached from the opposite
direction.

Two changes, since installing the tools alone leaves the trap armed for
the next missing dependency:

- `registry-verify.yml` installs `ripgrep` + `libxml2-utils`.
- `run_verify()` **refuses to judge pass-rows at all** in an incomplete
environment, and reports genuinely unrunnable checks as `unrunnable` —
still failing, since an unverifiable pass must never go green — rather
than as false claims.

### The required-tool list is declared, not inferred

An earlier draft parsed check strings to discover their commands. It
misparsed quoted regexes (`"a\|b\|c"`) and `[ $(…) -ge 25 ]`, inventing
**35** failures — precisely the sin the guard exists to prevent.
Replaced with a measured, documented list: `xmllint` is named by 4
checks, `jq` by 1, and `rg` by none but is required transitively via
`v1-audit.sh`.

## Verification

- complete env → `87 grounded · 0 broken · 0 unrunnable`, exit 0
- incomplete env → fails fast, judges nothing, names the missing tool
- `k9-svc/mime/k9.xml` + `k9.uti.plist` independently confirmed
well-formed and the two companion files present — so installing xmllint
turns that row **genuinely** green rather than exposing a real fault
- all three workflow files parse

## Not included, on purpose

`.gitleaks.toml` — **#512 covers it and is the better implementation.**
I built one independently and then discarded it: my regexes matched
against the whole line unanchored, and canary-testing showed that
suppressed real tokens.

| planted secret | gitleaks defaults | my draft |
|---|---|---|
| `ghp_…` PAT | caught | caught |
| same PAT, line also contains `EXAMPLE_KEY` | caught | **suppressed** |
| same PAT, line ends `# ...` | caught | **suppressed** |

#512's anchored `^…$` design has no such hole. Verified independently
against gitleaks 8.18.4 (the exact CI build, checksum-matched): all four
adversarial fixtures still caught, and `0` findings on the tree. Adopt
#512.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit 8813ecf into main Jul 21, 2026
20 checks passed
@hyperpolymath
hyperpolymath deleted the security/gitleaks-allowlist branch July 21, 2026 04:48
@sonarqubecloud

Copy link
Copy Markdown

hyperpolymath added a commit that referenced this pull request Jul 21, 2026
…518)

Found while verifying the 201-repo secret-scanner re-pin sweep. Two
defects in `rust-secrets`.

## 1. Silent no-scan — the serious one

```bash
grep -rn --include="*.rs" -E "$pattern" src/     # <- root src/ only
```

A Cargo **workspace** keeps its code in `crates/*/src`, so `src/` does
not exist at the root. `grep` exits **2**, the enclosing `if` reads that
as *no match*, and the job passes.

**Measured across the 64 Rust repos in the sweep: 8 (12.5%) have no root
`src/` and were never scanned at all.**

Proven on a fixture — a planted secret in `crates/core/src/lib.rs`:

| | old job | this PR |
|---|---|---|
| `pub const SERVICE_TOKEN: &str = "…"` in `crates/core/src/lib.rs` |
**exit 0** ✅ green | detected |

Another gate that could not fail.

## 2. False positive on the correct pattern

`let.*api_key.*=.*"` matches any `let api_key =` line containing a quote
— including:

```rust
let api_key = env::var("CLOUDGUARD_API_KEY").ok();
```

…which is *precisely* the practice this job exists to enforce. This was
the **only** `rust-secrets` failure in the entire sweep
(`cloudguard-server` `src/main.rs:59`). A gate whose sole route to green
is to stop reading from the environment is inverted.

## Exemptions

Mirror the four layers already documented on `shell-secrets`, and match
on the **value** or an explicit pragma — **never on a file path**,
because a path exemption blinds the job to a real secret in the same
file (the failure mode `.gitleaks.toml` was rewritten to avoid in #512).

| Layer | Rationale |
|---|---|
| env-lookup RHS | the correct pattern. Anchored to the assignment and
stopped at `;`, so a literal secret with an unrelated `env::var` mention
later on the line **still trips** |
| URL values | an OAuth endpoint is public, not a credential (`const
MS_TOKEN_URL = "https://login.microsoftonline.com/…"` tripped
`const.*TOKEN.*=.*"` 4× in one repo) |
| comment lines | documentation of a pattern is not a live secret |
| `// scanner-allow: rust-secrets` | explicit, per-line, auditable |

## Warn-first on the widened scope

Hits **outside `./src`** are newly visible, so they warn until
**2026-08-21** — the same cutoff and idiom as `check-package-policy.sh`
/ `check-docs-presence.sh` — rather than reddening repos that were never
actually being scanned. Hits **inside `./src`** block exactly as before,
so there is no regression.

The advisory branch prints the finding and says `NOT YET ENFORCED`; it
never claims a pass. A malformed cutoff **refuses to run** rather than
defaulting to warn forever.

## Verified, not assumed

Canary suite run against the script **extracted from this YAML**
(`yaml.safe_load` → execute), not a copy:

```
PASS  env::var + comment + pragma + OAuth URL -> ALLOWED
PASS  secret in ./src                        -> BLOCKS
PASS  secret in crates/                      -> ADVISORY today
PASS  secret in crates/                      -> BLOCKS after cutoff
PASS  literal + env mention after ;          -> BLOCKS  (exemption not over-broad)
PASS  malformed cutoff                       -> refuses to run
6 passed, 0 failed
```

Replayed over **all 64 Rust repos** in the sweep: **0 regressions today,
1 fixed** (`cloudguard-server`).

## Follow-up for repo owners (before 2026-08-21)

Three repos carry fixture-shaped hits outside `./src` and need a
one-line pragma; each is named in the run output today:

- `echidnabot` — `fuzz/fuzz_targets/fuzz_hmac.rs:17` (fuzz corpus)
- `filesoup` — `plugins/secret-scanner/src/lib.rs:159` (the plugin's own
detection fixture)
- `heterogenous-mobile-computing` — `examples/basic_usage.rs:61` (demo
of a *blocked* query)

Pre-existing and unchanged by this PR: `aerie` and `conative-gating`
fail under both old and new (`request["api_key"]` dynamic lookup; a
scanner's own `r#"…"#` test fixtures).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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