feat(secret-scan): SF_SECURITY_FAST contract for the regex-only fast path

Codifies AC4 of sf-mp4w2dij-xm6cwj: the regex-only path is the
today-default fast mode. SF_SECURITY_FAST=1 is the explicit opt-in for
callers that want to assert "regex-only, no LLM escalation, sub-100ms"
regardless of any future tiered reviewer landing in the script.

Today the env var changes only the trailing status line so operators
can verify the contract is observable. When the LLM-backed review hook
(AC1) lands, the absence of SF_SECURITY_FAST becomes the trigger for
escalation; setting it=1 keeps offline / pre-commit callers on the
fast path. Locked in by tests in both the .sh and .mjs scanners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-14 07:57:02 +02:00
parent 001740680b
commit fa9baf71d5
3 changed files with 50 additions and 2 deletions

View file

@ -8,6 +8,16 @@ const YELLOW = "\x1b[1;33m";
const NC = "\x1b[0m";
const IGNORE_FILE = ".secretscanignore";
// Fast-mode contract (sf-mp4w2dij-xm6cwj AC4): the regex-only path is the
// today-default fast path. SF_SECURITY_FAST=1 is the explicit opt-in for
// callers that want to assert "regex-only, no LLM escalation, sub-100ms"
// regardless of any future tiered reviewer landing in this script. When the
// LLM-backed review hook (AC1) lands, the absence of this env var becomes
// the trigger for escalation; setting it=1 keeps the offline / pre-commit
// fast path. Today the variable changes only the trailing status line so
// operators can verify the contract is observable.
const FAST_MODE = process.env.SF_SECURITY_FAST === "1";
const PATTERNS = [
{ label: "AWS Access Key", regex: /AKIA[0-9A-Z]{16}/g },
{
@ -251,4 +261,10 @@ if (findings > 0) {
process.exit(1);
}
process.stdout.write("secret-scan: no secrets detected ✓\n");
if (FAST_MODE) {
process.stdout.write(
"secret-scan: fast mode (regex-only, no LLM escalation) ✓\n",
);
} else {
process.stdout.write("secret-scan: no secrets detected ✓\n");
}

View file

@ -215,6 +215,11 @@ if [[ $FINDINGS -gt 0 ]]; then
echo -e "${RED}Commit blocked. Remove the secrets or add exceptions${NC}"
echo -e "${RED}to .secretscanignore if these are false positives.${NC}"
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
elif [[ "${SF_SECURITY_FAST:-0}" == "1" ]]; then
# AC4 contract (sf-mp4w2dij-xm6cwj): SF_SECURITY_FAST=1 is the explicit
# opt-in for the regex-only fast path. When LLM escalation lands, this
# env var becomes the documented opt-out for offline/pre-commit callers.
echo "secret-scan: fast mode (regex-only, no LLM escalation) ✓"
else
echo "secret-scan: no secrets detected ✓"
fi

View file

@ -18,6 +18,7 @@ const scanScript = join(projectRoot, "scripts", "secret-scan.sh");
function scanContent(
content: string,
filename = "test-file.ts",
extraEnv: Record<string, string> = {},
): { status: number; stdout: string; stderr: string } {
const dir = mkdtempSync(join(tmpdir(), "secret-scan-test-"));
try {
@ -38,7 +39,7 @@ function scanContent(
const result = spawnSync("bash", [scanScript], {
cwd: dir,
encoding: "utf-8",
env: { ...process.env, TERM: "dumb" },
env: { ...process.env, TERM: "dumb", ...extraEnv },
});
return {
@ -51,6 +52,32 @@ function scanContent(
}
}
// ── Fast-mode contract (sf-mp4w2dij-xm6cwj AC4) ─────────────────────
test(
"SF_SECURITY_FAST=1 prints fast-mode status on clean scan",
{ skip: isWindows },
() => {
const result = scanContent("const x = 1;", "clean.ts", {
SF_SECURITY_FAST: "1",
});
assert.equal(result.status, 0, `should pass: ${result.stdout}`);
assert.match(result.stdout, /fast mode \(regex-only, no LLM escalation\)/);
},
);
test(
"SF_SECURITY_FAST unset prints default clean status",
{ skip: isWindows },
() => {
const result = scanContent("const x = 1;", "clean.ts");
assert.equal(result.status, 0, `should pass: ${result.stdout}`);
assert.match(result.stdout, /no secrets detected/);
// Must NOT carry the fast-mode marker when unset
assert.doesNotMatch(result.stdout, /fast mode/);
},
);
// ── Detection tests ──────────────────────────────────────────────────
test("detects AWS access key", { skip: isWindows }, () => {