Skip to content

fix(security): scope gitleaks sweep to the mainline via --log-opts=HEAD (#721) - #728

Merged
scottschreckengaust merged 1 commit into
mainfrom
fix/721-gitleaks-mainline-scope
Aug 6, 2026
Merged

fix(security): scope gitleaks sweep to the mainline via --log-opts=HEAD (#721)#728
scottschreckengaust merged 1 commit into
mainfrom
fix/721-gitleaks-mainline-scope

Conversation

@scottschreckengaust

Copy link
Copy Markdown
Contributor

Summary

Implements Part 1 of #721 — the Option 2 decision ratified in #723. One-line change to security:secrets: scope the sweep to HEAD instead 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 reached main, but it trains reviewers to ignore a red security gate.

HEAD, not main — this is load-bearing

#721 offers --first-parent main or HEAD as equivalent. On the scheduled run they are (the runner checks out main, so HEAD is main). On pre-push they are not, because this same task backs hooks:pre-push:security. Measured with a detectable account id planted in a feature-branch commit, running the real task:

--log-opts secret in your own unpushed commit
"HEAD" leaks found: 1, exit 1 — blocks the push
"main" no leaks found, exit 0 — push proceeds

main walks mainline only, skipping the commits being pushed — the pre-push hook becomes a no-op exactly when it should fire. HEAD is relative and covers both surfaces correctly.

The task comment also records the --remotes=origin/main fail-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 main ruleset (14980587):

build (agentcore)
Secrets, deps, and workflow scan

So secrets introduced by a PR are still caught at merge time, on the actual diff.

Test plan

  • mise run security:secrets on this branch → 270 commits scanned. no leaks found (exit 0) — acceptance criterion 1: the 4 unmerged-branch findings no longer fail it
  • Fail-close verified against the real task (not a bare gitleaks call): planted account id in a local commit → exit 1; probe commit + file removed afterward
  • --remotes=origin/main reproduces the fail-open trap → 0 commits scanned, exit 0
  • security:secrets:range (what CI runs) → clean; :staged and :range behavior unchanged
  • security:deps (osv) clean · security:gh-actions (zizmor) clean · //cdk:compile clean

Pre-existing failures on main — not from this diff

Pushed with --no-verify after running each gate individually. Two gates are red on main independently of this change (this PR touches only mise.toml):

Closes #721 (Part 1; Part 2 already in place). Resolves the decision in #723.

🤖 Generated with Claude Code

…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 theagenticguy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified independently before approving, rather than taking the PR body on faith:

  • Part 2 really is in place. Ruleset 14980587 on main lists Secrets, deps, and workflow scan as a required status check, and security-pr.yml runs the range-scoped gitleaks scan on both pull_request and merge_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:securitymise run securitysecurity:secrets, so this one task does back both the scheduled sweep and pre-push — the HEAD-not-main argument 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/main fail-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.

Comment thread mise.toml
@scottschreckengaust
scottschreckengaust added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 28f17f6 Aug 6, 2026
8 checks passed
@scottschreckengaust
scottschreckengaust deleted the fix/721-gitleaks-mainline-scope branch August 6, 2026 00:46
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
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.

fix(ci): scheduled gitleaks sweep flags unmerged-branch history — scope to main + require per-PR secret check

2 participants