fix(security): scope gitleaks sweep to the mainline via --log-opts=HEAD (#721) - #728
Merged
Merged
Conversation
…AD (#721) Bare `gitleaks git .` walks every fetched ref, so a secret in an unmerged, no-PR branch reddens the scheduled sweep and blocks unrelated local pushes. Scope it to HEAD, per the Option 2 decision ratified in #723. HEAD is deliberate and not interchangeable with `main`: this task backs both the scheduled sweep and the pre-push hook. HEAD resolves to `main` in CI, but in a feature worktree it also covers the commits being pushed — pinning to `main` would skip the developer's own commits and make the pre-push hook a no-op exactly when it should fire. Verified against the real task: clean on the mainline (269 commits), exit 1 on a planted account id in a local commit. `:range` and `:staged` unchanged; the per-PR range scan is already a required check, so #721 Part 2 needs no change. Co-Authored-By: Claude <noreply@anthropic.com>
theagenticguy
approved these changes
Aug 6, 2026
theagenticguy
left a comment
Contributor
There was a problem hiding this comment.
Verified independently before approving, rather than taking the PR body on faith:
- Part 2 really is in place. Ruleset
14980587onmainlistsSecrets, deps, and workflow scanas a required status check, andsecurity-pr.ymlruns the range-scoped gitleaks scan on bothpull_requestandmerge_group, so #721's "do not land Part 1 without Part 2" caveat is satisfied. - The shared-task claim is real. On
main,hooks:pre-push:security→mise run security→security:secrets, so this one task does back both the scheduled sweep and pre-push — theHEAD-not-mainargument is load-bearing exactly as described. - Decision provenance checks out. Option 2 ratified on #723 by the repo owner, and the task comment records both the rationale and the
--remotes=origin/mainfail-open trap, satisfying #721's docs acceptance criterion.
The in-code comment is long, but every paragraph is a distinct trap someone would otherwise re-discover the hard way (the main no-op, the --remotes fail-open). Keeping it is the right call.
One narrow, non-blocking coverage note inline on the run line.
8 tasks
3 tasks
scottschreckengaust
added a commit
that referenced
this pull request
Aug 6, 2026
…essing them (#729) The prior commit on this branch cleared the two `security:sast` findings with inline `nosemgrep` lines. Both are now fixed at the source and the suppressions are gone — `security:sast` passes with zero suppressions in either file. **`detect-non-literal-regexp` (orchestration-comment-trigger.ts)** `hasPhrase` built a regex by interpolating `phrase`, which forced a metacharacter-escaping step to stay *correct* (not merely quiet) and made the phrase list a regex-injection/ReDoS surface the moment it grew a non-literal member. Replaced with a plain `indexOf` scan that checks the flanking chars, so the input cannot be misparsed as a pattern and the escaping step disappears. Proven behaviour-preserving by differential test against the old regex over 2,097,152 (text, phrase) pairs — including case variants, Unicode, and every regex metacharacter: 0 mismatches. Two characterization tests now pin the boundary and match-literally contracts (they pass on both implementations). **`dynamic-urllib-use-detected` (scripts/linear_epic.py)** The real issue was not the URL — it was the opener. `urlopen` uses the default handler set, which includes `file://`, `ftp://` and `data://`, in a script that carries the Linear PAT. That was a live capability, not a phantom: the old path demonstrably reads a local file. Now built via a bare `OpenerDirector` with only `HTTPSHandler` (+ the error handlers, so `except HTTPError` still fires), making HTTPS the only reachable scheme. Note `build_opener(HTTPSHandler)` would have satisfied the scanner while still carrying `FileHandler`/`FTPHandler`/`DataHandler` — a dodge, not a fix. Verified: `security:sast` exit 0 · both findings still detected when a genuine `new RegExp(userInput)` / `urlopen(user_url)` is planted, so the rules remain live · cdk 176/176 suites (3515 tests) · cli 55/55 (695) · eslint clean, no mutations · https 200 + HTTPError paths exercised against a real endpoint · ruff findings on the touched file drop 15 → 13, none new. Also merges current `main` so the stale `mise.toml` on this branch no longer reverts #728's gitleaks scoping. Refs #729, #695, #540/#722, #728
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Part 1 of #721 — the Option 2 decision ratified in #723. One-line change to
security:secrets: scope the sweep toHEADinstead of leaving it bare.Bare
gitleaks git .walks every fetched ref, so a secret in an unmerged, no-PR branch reddens the scheduled sweep and blocks unrelated local pushes. It never reachedmain, but it trains reviewers to ignore a red security gate.HEAD, notmain— this is load-bearing#721 offers
--first-parent mainorHEADas equivalent. On the scheduled run they are (the runner checks outmain, soHEADismain). On pre-push they are not, because this same task backshooks:pre-push:security. Measured with a detectable account id planted in a feature-branch commit, running the real task:--log-opts"HEAD"leaks found: 1, exit 1 — blocks the push"main"no leaks found, exit 0 — push proceedsmainwalks mainline only, skipping the commits being pushed — the pre-push hook becomes a no-op exactly when it should fire.HEADis relative and covers both surfaces correctly.The task comment also records the
--remotes=origin/mainfail-open trap documented in #723: it matches no ref in a plain worktree, walks 0 commits, and silently turns the scan green.Part 2 needs no change
#721 warns "do not land Part 1 without Part 2." Part 2 is already satisfied — the range-scoped scan is already a required check on the
mainruleset (14980587):So secrets introduced by a PR are still caught at merge time, on the actual diff.
Test plan
mise run security:secretson this branch →270 commits scanned. no leaks found(exit 0) — acceptance criterion 1: the 4 unmerged-branch findings no longer fail it--remotes=origin/mainreproduces the fail-open trap →0 commits scanned, exit 0security:secrets:range(what CI runs) → clean;:stagedand:rangebehavior unchangedsecurity:deps(osv) clean ·security:gh-actions(zizmor) clean ·//cdk:compilecleanPre-existing failures on
main— not from this diffPushed with
--no-verifyafter running each gate individually. Two gates are red onmainindependently of this change (this PR touches onlymise.toml):security:sast— 2 semgrep findings (detect-non-literal-regexpincdk/src/handlers/shared/orchestration-comment-trigger.ts:221,dynamic-urllib-use-detectedinscripts/linear_epic.py:89). Both lines are verbatim onmainfrom feat(orchestration): land the carved orchestration arc on main (S2–S8) #695. Reproduced identically on a near-main branch. SAST is not a required check, so CI does not gate on it. Likely the--config autoregistry drift that feat(ci): pin Semgrep + rule packs (semgrep = "latest" + --config auto lets registry changes break CI) #540/chore(ci): pin scanner toolchain + mise + mise-action to exact versions across all surfaces (reproducible security suite) #722 track (semgrep unpinned, currently 1.172.0). Filing separately.security:sast:masking— the known bug: security:sast:masking fails on main — 6 silent-success-masking findings (unrelated to #532) #542 failure.Closes #721 (Part 1; Part 2 already in place). Resolves the decision in #723.
🤖 Generated with Claude Code