Skip to content

fix: the pre-push range scan ignored its own config and scanned whole histories - #77

Open
p4gs wants to merge 3 commits into
mainfrom
fix/pre-push-range-scan-config-and-bounds
Open

p4gs wants to merge 3 commits into
mainfrom
fix/pre-push-range-scan-config-and-bounds

Conversation

@p4gs

@p4gs p4gs commented Sep 12, 2026

Copy link
Copy Markdown
Owner

Root cause fix for a defect that surfaced in four downstream repositories

While onboarding four repositories with sscsb on 2026-09-12 — ADE-Bootstrapper, flawde-submission-sample, VCF and OCEAN — all four hit the same two defects in range_secret_scan (src/hooks.rs, the pre-push path). This is the upstream fix, not a per-repo workaround. No repository's secrets policy is changed by this PR; .sscsb/config.toml is untouched.

1. The range scan never read [controls.secrets]

The pre-commit path, run_secret_scan_staged, reads cfg.control_opt_bool("secrets", "trufflehog") and "gitleaks" before deciding what to run. The pre-push range path beside it read neither, and ran whichever tool happened to be on PATH.

So a repository with gitleaks = false in its own .sscsb/config.toml was still gitleaks-scanned on every push — and blocked by the tool it had explicitly turned off. A gate that contradicts the config file it ships with is not enforcing policy, it is overriding it.

range_secret_scan now takes the Config and honours both switches exactly as the pre-commit path does, including the per-tool degrade messages and the fail-closed "no secret scanner ran" bail when nothing is left to run.

2. A new branch had its entire history scanned as if it were outgoing

A branch with no remote counterpart arrives on pre-push stdin with the zero sha as remote_sha. Both scanners read that literally as "no lower bound":

  • gitleaks got --log-opts=<tip> — every commit reachable from the tip
  • trufflehog got no --since-commit at all

OCEAN's reproduction is the shape that matters. The real outgoing range (f565d53..b50b020) was clean under both scanners, while the actual git push on that same new branch raised ~20 gitleaks findings out of years-old strings in vendored Go source and lockfile digests that appear nowhere in the diff being pushed. Blocking a legitimate push over content the push does not contain is how a team learns to reach for --no-verify, which costs far more than those findings were ever worth.

The range is now bounded by <tip> --not --remotes: everything reachable from the tip that is not already on a remote-tracking ref. That is the definition commits_in_range already used for the signing gate, so the two pre-push gates now answer for one range rather than two different ones.

TruffleHog takes a single commit rather than a revision set, so the new range_since_commit bounds it by the first parent of the oldest unpublished commit — the point the branch was cut from — and leaves it unbounded only when the outgoing range genuinely reaches a root commit, where the branch really is its own whole history.

Tests

Two regression tests in hooks::tests, both verified RED against the pre-fix logic before being accepted (the production logic was reverted in place, keeping the tests compiling, and re-run):

Test Red against pre-fix code
the_pre_push_range_scan_obeys_the_secrets_control_switches [controls.secrets] gitleaks = false must keep gitleaks out of the pre-push range scan, but it was invoked with ["version", "git", …, "--log-opts=9d56ed6… --not --remotes", "--report-format", "json", "--report-path", …]
a_brand_new_branch_scans_only_its_own_commits_not_the_whole_history left (6 commits, the whole fixture history) vs right (the 2 that are actually outgoing)

Notes on their construction:

  • The config test asserts in both directions deliberately. The "did not run" half alone would pass just as happily if the fake scanner were never reachable on PATH — a vacuous green — so the first half proves the shim is wired before the second half proves the switch is honoured.
  • The new-branch test asserts on the revision set, resolved by git rev-list itself, not on the spelling of the --log-opts argument. It builds a real repo: four commits published to refs/remotes/origin/main, then a feature branch with two more.
  • Both drive range_secret_scan directly rather than through the pre_push wrapper, because they install fake scanners on PATH and must own the environment lock across the whole call. with_env and serialized both acquire that lock and it is not reentrant, so nesting them would panic by design — the composition rules documented in src/testutil.rs.
  • The scanner shims record one argument per line and answer tools::detect's version probe, so "was it invoked at all" is distinguishable from "was it probed".

Verification

  • cargo fmt --check clean, cargo clippy --all-targets clean.
  • cargo test: 950 passed / 9 failed, against a clean-main baseline on the same machine of 949 passed / 8 failed. The delta is exactly the two new tests, both passing.
  • All 8 baseline failures are local-environment, not code: seven hooks:: plus one signers:: test fail with error: 1Password: invalid ssh public key … fatal: failed to write commit object, because this machine's global gpg.ssh.program points at 1Password's op-ssh-sign, which rejects the tests' own ephemeral SSH keys.
  • The ninth, signing_setup::tests::setup_agent_refuses_identity_blur_and_writes_nothing, is a pre-existing flake unrelated to this change — reproduced 8 times in 10 running cargo test --lib -- signing_setup alone, with no hooks:: test in the run at all. It reads the real machine's git config --global user.email via git_global while holding no environment lock, then setup_agent_claude_code reads it a second time; a sibling FakeMachine fixture repointing GIT_CONFIG_GLOBAL between those two reads makes the guard see two different values. That is exactly the documented race class in src/testutil.rs. Worth its own PR; deliberately out of scope here.

Scope

.sscsb/config.toml and this repository's own secrets-control policy are untouched. This is a code-correctness fix, independent of any policy question about where gitleaks stays enabled.

🤖 Generated with Claude Code

https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm

p4gs and others added 3 commits September 12, 2026 11:26
… histories

Two defects in `range_secret_scan`, both found the same day by four separate
repositories being onboarded with sscsb — ADE-Bootstrapper, flawde-submission-
sample, VCF and OCEAN all hit them.

1. The range scan never read `[controls.secrets]`. The pre-commit path,
`run_secret_scan_staged`, reads `control_opt_bool("secrets", "trufflehog")` and
`"gitleaks"` before deciding what to run; the pre-push range path beside it read
neither and ran whichever tool happened to be on PATH. A repository that had
turned gitleaks OFF in its own `.sscsb/config.toml` was still gitleaks-scanned
on every push, and blocked by the tool it had explicitly disabled. A gate that
contradicts the config file it ships with is not enforcing policy; it is
overriding it. `range_secret_scan` now takes the `Config` and honours both
switches the same way, including the degrade messages and the fail-closed
"nothing could run" bail the pre-commit path already had.

2. A branch with no remote counterpart had its ENTIRE history scanned as if it
were outgoing. A new branch arrives on pre-push stdin with the zero sha as
`remote_sha`, and both scanners read that literally as "no lower bound":
gitleaks got `--log-opts=<tip>`, which is every commit reachable from the tip,
and trufflehog got no `--since-commit` at all. OCEAN's reproduction is the shape
that matters — the real outgoing range (`f565d53..b50b020`) was clean under both
scanners, while the actual `git push` on that same new branch raised ~20
gitleaks findings out of years-old strings in vendored Go source and lockfile
digests that appear nowhere in the diff being pushed. Blocking a legitimate push
over content the push does not contain is how a team learns to reach for
`--no-verify`, which costs far more than those findings were ever worth.

The range is now bounded by `<tip> --not --remotes`: everything reachable from
the tip that is not already on a remote-tracking ref. That is the definition
`commits_in_range` already used for the signing gate, so the two pre-push gates
now answer for one range rather than two different ones. TruffleHog takes a
single commit rather than a revision set, so `range_since_commit` bounds it by
the first parent of the oldest unpublished commit — the point the branch was cut
from — and leaves it unbounded only when the outgoing range genuinely reaches a
root commit, where the branch really is its own whole history.

Both fixes carry a regression test, and both tests were verified RED against the
pre-fix logic before being accepted. With the config read reverted, the gitleaks
test reports the shim being invoked with a full argv despite `gitleaks = false`.
With the range bound reverted, the new-branch test resolves `--log-opts` through
git itself and gets all 6 commits in the fixture where 2 are outgoing. The
config test asserts in both directions on purpose: the "did not run" half alone
would pass just as happily if the fake scanner were never reachable on PATH,
which is a vacuous green. Both tests drive `range_secret_scan` directly rather
than through the `pre_push` wrapper, because they install fake scanners on PATH
and must own the environment lock across the whole call — `with_env` and
`serialized` both acquire that lock and it is not reentrant, per the composition
rules in src/testutil.rs.

AI-Assisted: true
AI-Tool: Claude Code
AI-Model: claude-sonnet-5
AI-Role: draft
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm
rustls 0.23.41 -> 0.23.45 and rustls-webpki 0.103.13 -> 0.103.15. The advisory was published after this branch's last CI run; osv-scanner flagged it on the next one.

AI-Assisted: true
AI-Tool: Claude Code
AI-Model: Opus 5.5
AI-Role: draft
AI-Dependency-Review: approved
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hwWMsTiv9Uf2gNbMh75uk
@socket-security

socket-security Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

Ignoring alerts on:

  • rustls@0.23.45

View full report

@p4gs

p4gs commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

Socket alert triage: cargo/rustls@0.23.45 (two AI-signal alerts)

Each alert was checked against the published crate (rustls-0.23.45.crate, sha256 0d41d731…d634, matching this PR's Cargo.lock) and the published ureq-2.12.1 manifest (sha256 02d1a662…c01d, matching). Two independent reviews tried to overturn each verdict; neither did.

1. HPKE nonce-counter exhaustion (98%): a real defect, not compiled in this build.
The bug Socket describes is real. In src/crypto/aws_lc_rs/hpke.rs, seq_num is a u32 (line 735), but increment_seq_num (lines 764-782) compares it against 2^96 - 1, a limit a u32 can never reach. So after 2^32 messages on one HPKE context the counter wraps and a nonce would repeat. That file sits behind rustls's aws_lc_rs feature (crypto/mod.rs:29-30). This build gets rustls only through ureq 2.12.1, which sets default-features = false with ring, logging, std, tls12. aws-lc appears 0 times in Cargo.lock, so hpke.rs is compiled on no target and in no profile, and sscsb calls no rustls API. Upstream has already fixed it on main: rustls-aws-lc-rs/src/hpke.rs now uses a u128 counter with a seq_num_does_not_wrap test. Tripwire: if aws-lc-rs ever appears in Cargo.lock, re-check this before merging.

2. TLS key log via SSLKEYLOGFILE (78%): intended, opt-in, and never enabled here.
rustls writes key material only when an application installs KeyLogFile. The default ClientConfig uses a no-op key log. Neither ureq 2.12.1 nor sscsb installs KeyLogFile, so setting SSLKEYLOGFILE cannot make this binary write TLS secrets.

There is no reachable vulnerability, so nothing in this PR changes. rustls 0.23.45 is also what fixes RUSTSEC-2026-0285, and no newer stable release exists. Dismissing these alerts for this PR only, below.

@p4gs

p4gs commented Oct 7, 2026

Copy link
Copy Markdown
Owner Author

@SocketSecurity ignore cargo/rustls@0.23.45

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