From e708bb0fef906c5598cfaa2f30b3642715c7e2bd Mon Sep 17 00:00:00 2001 From: Rome-1 Date: Thu, 1 Oct 2026 17:17:59 -0700 Subject: [PATCH] fix(secrets): reject a --diff ref that starts with "-" The --diff value is passed to `git diff` as a positional argument, and git parses anything starting with "-" as one of its own options. Reject such a value with exit 2 (invalid ref) before running git, in both the Node and Python CLIs, for `rafter secrets` and `rafter agent scan`. Each runtime gets an end-to-end test that passes an option-shaped ref and asserts exit 2 with the target file left untouched. --- node/src/commands/agent/scan.ts | 5 +++++ node/tests/secret-scanning-e2e.test.ts | 12 ++++++++++++ python/rafter_cli/commands/agent.py | 8 ++++++++ python/rafter_cli/commands/scan.py | 2 ++ python/tests/test_secret_scanning_e2e.py | 14 ++++++++++++++ 5 files changed, 41 insertions(+) diff --git a/node/src/commands/agent/scan.ts b/node/src/commands/agent/scan.ts index f18db67e..40232815 100644 --- a/node/src/commands/agent/scan.ts +++ b/node/src/commands/agent/scan.ts @@ -432,6 +432,11 @@ async function scanDiffFiles( scanPath?: string, suppressions: Suppression[] = [], ): Promise { + // git would parse a leading "-" as one of its own options, not a ref. + if (ref.startsWith("-")) { + console.error(`Error: invalid ref "${ref}" (a git ref cannot start with "-")`); + process.exit(2); + } await runGitAddedLineScan( ["diff", "-U0", "--no-color", "--diff-filter=ACM", ref], opts, diff --git a/node/tests/secret-scanning-e2e.test.ts b/node/tests/secret-scanning-e2e.test.ts index 630dd0b9..6d11b759 100644 --- a/node/tests/secret-scanning-e2e.test.ts +++ b/node/tests/secret-scanning-e2e.test.ts @@ -489,6 +489,18 @@ describe("E2E: git --diff scanning", () => { expect(parsed.results[0].matches[0].pattern.name).toBe("AWS Access Key ID"); }); + it("rejects a --diff ref that git would parse as an option", () => { + const target = path.join(tmpDir, "untouched.txt"); + fs.writeFileSync(target, "keep\n"); + + const r = rafter( + ["scan", "local", tmpDir, "--diff", `--output=${target}`, "--engine", "patterns", "--quiet"], + { cwd: tmpDir }, + ); + expect(r.exitCode).toBe(2); + expect(fs.readFileSync(target, "utf-8")).toBe("keep\n"); + }); + it("exits 0 when changed files are clean", () => { const initialCommit = git("rev-parse HEAD"); diff --git a/python/rafter_cli/commands/agent.py b/python/rafter_cli/commands/agent.py index 58acacdd..38fcf686 100644 --- a/python/rafter_cli/commands/agent.py +++ b/python/rafter_cli/commands/agent.py @@ -1736,6 +1736,13 @@ def _output_empty_diff_scan( ) +def _reject_option_like_ref(ref: str) -> None: + """git would parse a leading "-" as one of its own options, not a ref.""" + if ref.startswith("-"): + print(f'Error: invalid ref "{ref}" (a git ref cannot start with "-")', file=sys.stderr) + raise typer.Exit(code=2) + + def _run_git_added_line_scan( git_args: list[str], git_cwd: str | None, @@ -2176,6 +2183,7 @@ def scan( # --diff if diff: + _reject_option_like_ref(diff) _run_git_added_line_scan( ["diff", "-U0", "--no-color", "--diff-filter=ACM", diff], git_cwd, diff --git a/python/rafter_cli/commands/scan.py b/python/rafter_cli/commands/scan.py index e4365872..eb9dfdee 100644 --- a/python/rafter_cli/commands/scan.py +++ b/python/rafter_cli/commands/scan.py @@ -117,6 +117,7 @@ def scan_local( _apply_exclude_paths, _load_baseline_entries, _run_git_added_line_scan, + _reject_option_like_ref, ) from ..core.config_manager import ConfigManager from ..core.custom_patterns import load_suppressions, policy_ignore_to_suppressions @@ -146,6 +147,7 @@ def scan_local( # --diff if diff: + _reject_option_like_ref(diff) _run_git_added_line_scan( ["diff", "-U0", "--no-color", "--diff-filter=ACM", diff], git_cwd, diff --git a/python/tests/test_secret_scanning_e2e.py b/python/tests/test_secret_scanning_e2e.py index 74818694..0de0171f 100644 --- a/python/tests/test_secret_scanning_e2e.py +++ b/python/tests/test_secret_scanning_e2e.py @@ -9,6 +9,7 @@ import json import os import subprocess +import sys from pathlib import Path import pytest @@ -391,6 +392,19 @@ def test_detects_secrets_in_changed_files(self, tmp_path): assert len(results) == 1 assert results[0].matches[0].pattern.name == "AWS Access Key ID" + def test_rejects_a_diff_ref_that_git_would_parse_as_an_option(self, tmp_path): + target = tmp_path / "untouched.txt" + target.write_text("keep\n") + + result = subprocess.run( + [sys.executable, "-m", "rafter_cli", "secrets", self.repo, + "--diff", f"--output={target}", "--engine", "patterns", "--quiet"], + capture_output=True, text=True, cwd=self.repo, timeout=60, + env={**os.environ, "HOME": str(tmp_path)}, + ) + assert result.returncode == 2 + assert target.read_text() == "keep\n" + def test_clean_changed_files_produce_no_results(self, tmp_path): initial = _git("rev-parse HEAD", self.repo)