Files
gstack/test/gate-secret-scan.test.ts
T
8c75496391 feat(ci): supply-chain hygiene — secret gate on every PR diff, dependency review, OSV, dependabot, evidence-bar PR template
The repo owned a redaction engine and had zero CI-side secret scanning.
quality-gate.yml now pipes every PR diff's ADDED lines through our own
bin/gstack-redact (gate-secret-scan.mjs, taken from the fork — it dogfoods
the engine): HIGH findings fail the check, MEDIUM prints an advisory count
only (no human in CI to confirm), planted-bug fixtures excluded by pathspec.
Live-verified both directions: PEM key fails, clean diff and MEDIUM shapes
pass; ShellCheck (errors) covers the setup/build shell boundary and passes
today; bun audit gates critical advisories. Trigger is pull_request, never
pull_request_target.

dependency-review.yml adopts the hardened never-merged prior-art branch
(fail-on-severity high, workflow paths watched, tight perms) — verify the
dependency graph parses bun.lock with a canary bump before trusting the
gate. dependabot: weekly, grouped per ecosystem, capped PR counts; and
evals.yml image build/push now skips dependabot actors, whose read-only
GITHUB_TOKEN made every lockfile bump a permanently red check. OSV scans
weekly with a reasoned ignore file. All new workflow actions SHA-pinned.
Scorecard deliberately not taken (no consumer for the score).

The PR template front-loads the evidence bar (live proof, liveness
screenshot, no-ETHOS/voice-changes checklist); the unenforced DCO line is
dropped. bin/gstack-verify-gate ships OPT-IN (never registered by ./setup —
a Stop hook running the project's verify command after every turn is the
user's call), with the fork's tests adapted to pin exactly that.

Ported from time-attack/gstack (GStack 2) + our own prior-art branch.

Co-authored-by: Sina Matian <sina@time-attack.dev>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 13:18:37 -07:00

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/);
});
});