mirror of https://github.com/garrytan/gstack.git
59 lines
2.1 KiB
TypeScript
59 lines
2.1 KiB
TypeScript
/**
|
|
* CI secret gate contract (R4/R9, fork port wave 2).
|
|
*
|
|
* .github/scripts/gate-secret-scan.mjs pipes a unified diff's ADDED lines
|
|
* into bin/gstack-redact and enforces: HIGH fails (exit 1), MEDIUM is an
|
|
* advisory count only (no human in CI to confirm, so it must never fail
|
|
* the check), clean passes. The workflow-level pathspec excludes keep the
|
|
* planted-bug fixtures out of the diff entirely; this pins the script's
|
|
* own exit contract with live subprocess runs.
|
|
*/
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import { spawnSync } from "child_process";
|
|
import { join } from "path";
|
|
|
|
const ROOT = join(import.meta.dir, "..");
|
|
const SCRIPT = join(ROOT, ".github", "scripts", "gate-secret-scan.mjs");
|
|
|
|
function scan(diff: string): { code: number; out: string } {
|
|
const res = spawnSync("node", [SCRIPT], {
|
|
cwd: ROOT,
|
|
input: diff,
|
|
encoding: "utf-8",
|
|
timeout: 60_000,
|
|
});
|
|
return { code: res.status ?? -1, out: `${res.stdout}${res.stderr}` };
|
|
}
|
|
|
|
describe("gate-secret-scan.mjs exit contract", () => {
|
|
test("clean added lines pass", () => {
|
|
const r = scan("+const x = 1;\n+++ b/file.ts\n+// harmless\n");
|
|
expect(r.code).toBe(0);
|
|
expect(r.out).toContain("0 high");
|
|
});
|
|
|
|
test("a HIGH credential in an added line fails the gate", () => {
|
|
const r = scan(
|
|
"+-----BEGIN RSA PRIVATE KEY-----\n+MIIEowIBAAKCAQEA\n+-----END RSA PRIVATE KEY-----\n",
|
|
);
|
|
expect(r.code).toBe(1);
|
|
expect(r.out).toContain("1 high");
|
|
});
|
|
|
|
test("removed lines and context are ignored — only additions are scanned", () => {
|
|
const r = scan(
|
|
"------BEGIN RSA PRIVATE KEY-----\n-MIIEowIBAAKCAQEA\n-----END RSA PRIVATE KEY-----\n+just an addition\n",
|
|
);
|
|
expect(r.code).toBe(0);
|
|
});
|
|
|
|
test("MEDIUM findings are advisory only — never fail CI", () => {
|
|
// A Stripe publishable-key shape sits at MEDIUM in the taxonomy
|
|
// (context-variable; a human confirms interactively, CI cannot).
|
|
const r = scan(`+const key = "pk_live_${"a".repeat(24)}";\n`);
|
|
expect(r.code).toBe(0);
|
|
expect(r.out).toMatch(/\d+ advisory/);
|
|
});
|
|
});
|