Skip to content

GH-6334: fix(security): push secret gate — ignore textconv and external diff drivers, show root commits, scan the exact branch ref, no followed tags, scan file paths - #6340

Merged
alekspetrov merged 2 commits into
mainfrom
pilot/GH-6334
Nov 12, 2026

Conversation

@alekspetrov

Copy link
Copy Markdown
Collaborator

Summary

Automated PR created by Pilot for task GH-6334.

Closes #6334

Changes

GitHub Issue GH-6334: fix(security): push secret gate — ignore textconv and external diff drivers, show root commits, scan the exact branch ref, no followed tags, scan file paths

Context

Navigator TASK-650. The push secret gate is CheckSecretGate in internal/executor/secret_gate.go; the scan is CheckPush and CheckPushRange in internal/executor/secretpatterns/pushrange.go; the push methods are Push, PushForceWithLease and PushToMain in internal/executor/git.go. The gate runs in a repository that the model session can write. The run-it review of PR #6331 found five ways to publish a secret past the gate; each one works on main and none came from that PR:

  1. A textconv driver. A config value diff.NAME.textconv plus an attribute line (in the info/attributes file of the .git directory, in a committed .gitattributes file, or through core.attributesFile) makes git log print converted text. The gate scans the converted text, and git runs a command that the session chose.
  2. The config value log.showRoot set to false. An orphan root commit that holds the secret, merged and then deleted in a later commit, shows no patch.
  3. A shadow of the branch name. A file with the branch name inside the .git directory (for example a path pilot/Test: Pilot self-execution demo #9 there) that holds a clean SHA makes the gate scan that commit, while git push sends the branch under refs/heads.
  4. The config value push.followTags set to true. The message of an annotated tag is published and is not scanned.
  5. A secret in a file path. Paths are not scanned. When the content also matches, the refusal error and the log line print the path.

Scope

  1. Every git log, git show and git diff command of the gate passes --no-textconv and --no-ext-diff, and sets log.showRoot to true with a -c argument.
  2. The gate resolves the branch to scan as the full ref name under refs/heads, never as a short name. Every push of the three push methods uses a full refspec with refs/heads on both sides, so that the commit that the gate scans is the commit that git sends. A test compares both SHAs.
  3. Every push of the three push methods passes --no-follow-tags and sets push.followTags to false with a -c argument.
  4. The gate scans the path of each file that the pushed range adds, changes or renames, with the same patterns and registered values as the content scan. A match refuses the push.
  5. The refusal error, the log line and the alert text pass the file path through the same mask as other text, so that a path that holds a secret value is not printed.
  6. Tests with real temporary repositories through the push method (the fixture newPushRefusedFixture shows the pattern). Use the fake secret shape that the present gate tests use. One test per case of Context, each one must end with a refused push and no new commit on the bare remote: the textconv driver through each of the three attribute places; log.showRoot false with the orphan root; the shadow file; an annotated tag message with push.followTags true (the tag must be absent on the remote; the branch push itself may pass when the commits are clean); a secret in a file name alone; a secret in a file name and in the content (assert that the error text and the captured log do not hold the value). One more test: the textconv command must not run (it would create a marker file).
  7. Put a comment // mutation pin: <case> -> this test fails above each test.

Acceptance

  • go test -race -count=1 ./internal/executor/... -run 'SecretGate|PushRange|PushRefused' passes; paste the output into the PR body.
  • Remove --no-textconv -> the three textconv tests and the marker test fail.
  • Remove the log.showRoot argument -> the orphan root test fails.
  • Scan the short branch name again -> the shadow test fails.
  • Remove --no-follow-tags -> the tag test fails.
  • A clean push still passes: the gate tests that exist today pass without a change to their assertions.
  • Do not re-run make build, make test or make lint as evidence.

Out of scope

The trust in remote-tracking refs that the session can write (TASK-651 moves the gate to a repository that the daemon owns). A scan of tag messages. Any change to the secret patterns.

Not verified

  • go test -race -count=1 ./internal/executor/... -run 'SecretGate|PushRange|PushRefused' passes; paste the output into the PR body.
    Reason: Not verified: shell operators are not allowed in evidence commands

  • Remove --no-textconv -> the three textconv tests and the marker test fail.
    Reason: freeform mutation, run manually

  • Remove the log.showRoot argument -> the orphan root test fails.
    Reason: freeform mutation, run manually

  • Scan the short branch name again -> the shadow test fails.
    Reason: freeform mutation, run manually

  • Remove --no-follow-tags -> the tag test fails.
    Reason: freeform mutation, run manually

Pre-PR review

Mode: enforce
Verdict: APPROVE-WITH-NOTES
Fix: none

Defects:

  • [minor] at internal/executor/secret_gate_gh6334_test.go:236 — the pin comment "remove --no-follow-tags -> this test fails" is false: pushArgs also passes -c push.followTags=false (internal/executor/git.go:1137, args := []string{"-c", "push.followTags=false", "push", "--no-follow-tags"}), and a command-line -c value overrides the repository's push.followTags=true; so removing the flag alone leaves the tag off the remote and the test still passes. The issue asks for both arguments (scope 3) and for this mutation to kill the test, and no code can satisfy both, so this is minor.
  • [minor] at internal/executor/secretpatterns/pushrange.go:630 — a deleted empty file, or a change of mode only, has no +++ line, so cur keeps the path from the diff --git line and is scanned. The comment at pushrange.go:609 ("a deleted file has no path to publish") does not hold here: deleting an already-published empty file whose name matches a pattern is refused (a false refusal, not a leak).
  • [minor] at internal/executor/secretpatterns/pushrange.go:643 — git still C-quotes a path that holds ", \ or a control byte even with core.quotepath=false (+++ "b/…"…"). TrimPrefix("b/") then misses, and a registered value with one of those characters appears escaped, so the path scan misses it.
  • [minor] at internal/executor/secretpatterns/pushrange.go:234 — CheckPush qualifies the head again (head = QualifyBranchRef(head)). If only secret_gate.go:254 went back to the short name, the shadow test would still pass. Only TestSecretGate_ScannedCommitIsTheCommitGitSends catches that mutation, through its strings.HasPrefix(scannedHead, "refs/heads/") check.
  • [minor] at internal/executor/secretpatterns/pushrange.go:187 — the push base candidates are still short names (candidates = append(candidates, "origin/"+b), and "origin/main", "origin/master" at :198). A file under $GIT_DIR/origin/ can stand in for them just like the branch shadow. The issue puts trust in remote-tracking refs out of scope (TASK-651), so this is noted, not blocking.
  • [minor] at internal/autopilot/conflict_gomod.go:101 — runGitCmd(ctx, worktreePath, "push", "origin", "HEAD:refs/heads/"+branchName) and conflict_stacked.go:412 push without --no-follow-tags. These are not among the three methods in scope.

Not-verified:

  • Acceptance bullets that depend on mutations (removing --no-textconv, removing log.showRoot, scanning the short name again) were checked by reading the code only: I cannot run tests in this checkout.
  • I did not open the secretGateFixture helpers (newSecretGateFixture, commit, remoteBranches) or the secretGateValue shape; the tests rely on them, and the gates show build and tests passed.
  • The shadow tests depend on the CI git resolving $GIT_DIR/pilot/Test: Pilot self-execution demo #9 before refs/heads/pilot/Test: Pilot self-execution demo #9. I cannot run git here, so I could not confirm that TestSecretGate_ShadowOfBranchNameInGitDirIsNotScanned fails when the qualification is removed.

…ivers, shows root commits, scans the exact branch ref, publishes no followed tags, scans file paths (GH-6334)
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 85.18519% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/executor/secretpatterns/pushrange.go 83.33% 7 Missing ⚠️
internal/executor/git.go 88.88% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@alekspetrov
alekspetrov merged commit b5e3abf into main Nov 12, 2026
9 checks passed
@alekspetrov
alekspetrov deleted the pilot/GH-6334 branch November 12, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants