mirror of https://github.com/garrytan/gstack.git
fix(ci): SHA-pin dependency-review; the secret gate fails closed without a report
dependency-review.yml rode mutable refs (@v4 resolves to a BRANCH on that repo) inside the one workflow whose job is supply-chain hygiene — now commit-pinned like its siblings, with dependabot keeping the pins fresh. gate-secret-scan.mjs crashed with an unhandled EPIPE on oversize diffs (the designed report.oversize branch was unreachable: the scanner emits no JSON on refusal) — the pipe write now tolerates early exit and a missing report is an explicit fail-closed exit 1. Oversize + broken-scanner legs pinned.
This commit is contained in:
parent
13b6c5c87b
commit
2fd506a4e0
|
|
@ -16,6 +16,12 @@ process.stdin.once("end", () => {
|
|||
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
||||
.map((line) => line.slice(1))
|
||||
.join("\n");
|
||||
// The scanner may exit before consuming an oversize payload (it refuses
|
||||
// stdin over --max-bytes and reports oversize:true). EPIPE here is that
|
||||
// refusal in flight, not a failure — the report + exit code carry the verdict.
|
||||
child.stdin.on("error", (error) => {
|
||||
if (error.code !== "EPIPE") throw error;
|
||||
});
|
||||
child.stdin.end(additions);
|
||||
});
|
||||
let stdout = "";
|
||||
|
|
@ -23,7 +29,16 @@ child.stdout.setEncoding("utf8");
|
|||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
||||
child.once("error", (error) => { throw error; });
|
||||
child.once("close", (code) => {
|
||||
const report = JSON.parse(stdout);
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(stdout);
|
||||
} catch {
|
||||
// No parseable report: the oversize refusal prints only to stderr and
|
||||
// exits 3, and a crashed scanner emits nothing. Both fail closed.
|
||||
console.log(`credential scan: 1 high, 0 advisory (scanner emitted no report, exit ${code} — fail-closed)`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const high = Number(report.counts?.HIGH ?? 0);
|
||||
const medium = Number(report.counts?.MEDIUM ?? 0);
|
||||
console.log(`credential scan: ${high} high, ${medium} advisory`);
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ jobs:
|
|||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/dependency-review-action@v4
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
with:
|
||||
fail-on-severity: high
|
||||
fail-on-scopes: runtime, development
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@
|
|||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { spawnSync } from "child_process";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
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 } {
|
||||
function scan(diff: string, cwd: string = ROOT): { code: number; out: string } {
|
||||
const res = spawnSync("node", [SCRIPT], {
|
||||
cwd: ROOT,
|
||||
cwd,
|
||||
input: diff,
|
||||
encoding: "utf-8",
|
||||
timeout: 60_000,
|
||||
|
|
@ -56,3 +58,61 @@ describe("gate-secret-scan.mjs exit contract", () => {
|
|||
expect(r.out).toMatch(/\d+ advisory/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gate-secret-scan.mjs fail-closed legs", () => {
|
||||
test("oversize diff (report.oversize) fails the gate", () => {
|
||||
// The script pins --max-bytes 16000000; bin/gstack-redact refuses to scan
|
||||
// anything larger and reports oversize:true (fail-closed). The gate must
|
||||
// exit 1 rather than pass unscanned bytes. ~17MB of added lines guarantees
|
||||
// the joined additions exceed the cap.
|
||||
const line = `+${"a".repeat(8190)}\n`;
|
||||
const r = scan(line.repeat(2100));
|
||||
expect(r.code).toBe(1);
|
||||
// Proves the failure came from the parsed report (the engine surfaces
|
||||
// oversize as a fail-closed HIGH), not from a crashed subprocess.
|
||||
expect(r.out).toContain("1 high");
|
||||
}, 60_000);
|
||||
|
||||
test("unexpected gstack-redact exit code fails the gate even when the report is clean", () => {
|
||||
// Stub bin/gstack-redact that emits a CLEAN JSON report but exits 1 —
|
||||
// not one of the contract codes (0 clean / 2 MEDIUM / 3 HIGH). The gate
|
||||
// must treat the unexpected exit as failure: a broken scanner reporting
|
||||
// "all clear" is exactly the fail-open shape this leg guards against.
|
||||
const dir = mkdtempSync(join(tmpdir(), "gate-secret-scan-stub-"));
|
||||
try {
|
||||
mkdirSync(join(dir, "bin"));
|
||||
writeFileSync(
|
||||
join(dir, "bin", "gstack-redact"),
|
||||
[
|
||||
"#!/usr/bin/env bun",
|
||||
'let input = "";',
|
||||
'process.stdin.setEncoding("utf8");',
|
||||
'process.stdin.on("data", (c) => { input += c; });',
|
||||
'process.stdin.on("end", () => {',
|
||||
' console.log(JSON.stringify({ findings: [], counts: { HIGH: 0, MEDIUM: 0, LOW: 0, WARN: 0 }, repoVisibility: "public", oversize: false }));',
|
||||
" process.exit(1);",
|
||||
"});",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
const r = scan("+const x = 1;\n", dir);
|
||||
expect(r.out).toContain("0 high"); // the clean report WAS parsed...
|
||||
expect(r.code).toBe(1); // ...and the gate still failed on the exit code
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("missing gstack-redact (spawn crash, empty stdout) exits nonzero — never fail-open", () => {
|
||||
// cwd with no bin/gstack-redact at all: bun exits module-not-found with
|
||||
// empty stdout. Whatever the exact failure shape, the gate must not
|
||||
// report success.
|
||||
const dir = mkdtempSync(join(tmpdir(), "gate-secret-scan-absent-"));
|
||||
try {
|
||||
const r = scan("+const x = 1;\n", dir);
|
||||
expect(r.code).not.toBe(0);
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue