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:
parent
001740680b
commit
fa9baf71d5
3 changed files with 50 additions and 2 deletions
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }, () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue