mandrel-platform 0.17.2 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +254 -34
- package/config/commitlint.base.mjs +36 -0
- package/config/edge-security/rate-limit.mjs +103 -20
- package/config/repo-settings.schema.json +78 -0
- package/default.json +4 -19
- package/package.json +2 -1
- package/scripts/apply-uptime-monitors.mjs +378 -0
- package/scripts/apply-uptime-monitors.test.mjs +372 -0
- package/scripts/audit-check.mjs +321 -180
- package/scripts/audit-check.test.mjs +263 -0
- package/scripts/check-action-pins.mjs +106 -173
- package/scripts/check-coverage-threshold.mjs +44 -6
- package/scripts/check-coverage-threshold.test.mjs +43 -0
- package/scripts/check-docs-staleness.mjs +130 -81
- package/scripts/check-docs-staleness.test.mjs +130 -0
- package/scripts/check-pin-drift.mjs +61 -110
- package/scripts/check-pin-drift.test.mjs +175 -3
- package/scripts/check-repo-settings.mjs +363 -0
- package/scripts/check-repo-settings.test.mjs +320 -0
- package/scripts/check-required-contexts.mjs +247 -129
- package/scripts/check-required-contexts.test.mjs +137 -0
- package/scripts/check-ruleset.mjs +435 -0
- package/scripts/check-ruleset.test.mjs +439 -0
- package/scripts/check-workflow-portability.mjs +163 -118
- package/scripts/check-workflow-portability.test.mjs +199 -0
- package/scripts/check-wrangler-baseline.mjs +514 -0
- package/scripts/check-wrangler-baseline.test.mjs +454 -0
- package/scripts/edge-security.test.mjs +81 -1
- package/scripts/lib/args.mjs +93 -0
- package/scripts/lib/args.test.mjs +152 -0
- package/scripts/lib/gh-json.mjs +119 -0
- package/scripts/lib/semver-duration.mjs +84 -0
- package/scripts/lib/uses-pins.mjs +220 -0
- package/scripts/lib/uses-pins.test.mjs +219 -0
- package/scripts/lib/walk.mjs +74 -0
- package/scripts/platform-repair.mjs +9 -3
- package/scripts/platform-sync.mjs +533 -5
- package/scripts/platform-sync.test.mjs +477 -0
- package/scripts/update-semgrep-rules.mjs +76 -5
- package/templates/runbooks/README.md +9 -5
- package/templates/runbooks/branch-protection-setup.md +9 -3
- package/templates/workflows/deploy-staging.yml +86 -0
- package/templates/workflows/uptime-apply.yml +54 -0
package/scripts/audit-check.mjs
CHANGED
|
@@ -11,6 +11,13 @@
|
|
|
11
11
|
* whose expiry has passed are treated as un-suppressed and will cause
|
|
12
12
|
* the script to exit non-zero.
|
|
13
13
|
*
|
|
14
|
+
* Fail-closed contract:
|
|
15
|
+
* When `pnpm audit` exits non-zero AND the report it produced cannot be
|
|
16
|
+
* interpreted as a recognizable advisories document, the gate exits
|
|
17
|
+
* non-zero. A non-zero audit exit is a signal that something is wrong;
|
|
18
|
+
* an uninterpretable report means the gate cannot prove the graph is
|
|
19
|
+
* clean, so it must fail closed rather than wave the build through.
|
|
20
|
+
*
|
|
14
21
|
* Usage:
|
|
15
22
|
* node scripts/audit-check.mjs
|
|
16
23
|
* node scripts/audit-check.mjs --allowlist path/to/allowlist.json
|
|
@@ -18,8 +25,9 @@
|
|
|
18
25
|
* Exit codes:
|
|
19
26
|
* 0 — no blocking vulnerabilities (all High/Critical suppressed with
|
|
20
27
|
* valid, non-expired allowlist entries, or none found)
|
|
21
|
-
* 1 — one or more unsuppressed High/Critical CVEs,
|
|
22
|
-
* entries were encountered
|
|
28
|
+
* 1 — one or more unsuppressed High/Critical CVEs, expired allowlist
|
|
29
|
+
* entries were encountered, or the audit report was uninterpretable
|
|
30
|
+
* while pnpm audit exited non-zero
|
|
23
31
|
*
|
|
24
32
|
* Allowlist format (JSON):
|
|
25
33
|
* [
|
|
@@ -40,170 +48,92 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
40
48
|
import { resolve } from "node:path";
|
|
41
49
|
|
|
42
50
|
// ---------------------------------------------------------------------------
|
|
43
|
-
//
|
|
51
|
+
// Pure core (unit-testable — no process.exit, no filesystem, no child process)
|
|
44
52
|
// ---------------------------------------------------------------------------
|
|
45
53
|
|
|
46
|
-
const
|
|
47
|
-
let allowlistPath = null;
|
|
48
|
-
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
if (args[i] === "--allowlist" && args[i + 1]) {
|
|
51
|
-
allowlistPath = resolve(process.cwd(), args[i + 1]);
|
|
52
|
-
i++;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (allowlistPath === null) {
|
|
57
|
-
allowlistPath = resolve(process.cwd(), "audit-allowlist.json");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Allowlist loading and validation
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
54
|
+
const BLOCKING_SEVERITIES = new Set(["high", "critical"]);
|
|
63
55
|
|
|
64
56
|
/**
|
|
65
|
-
* @typedef {{ id: string; reason
|
|
57
|
+
* @typedef {{ id: string; reason?: string; expires: string }} AllowlistEntry
|
|
66
58
|
*/
|
|
67
59
|
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Partition allowlist entries into the active (non-expired) suppression set
|
|
62
|
+
* and the list of expired entries, relative to `today` (a `YYYY-MM-DD`
|
|
63
|
+
* string). Entries missing a required `id` or `expires` field are surfaced
|
|
64
|
+
* in `invalid` so the caller can fail closed on a malformed allowlist.
|
|
65
|
+
*
|
|
66
|
+
* @param {AllowlistEntry[]} allowlist
|
|
67
|
+
* @param {string} today `YYYY-MM-DD`
|
|
68
|
+
* @returns {{ suppressed: Set<string>; expired: AllowlistEntry[]; invalid: AllowlistEntry[] }}
|
|
69
|
+
*/
|
|
70
|
+
export function partitionAllowlist(allowlist, today) {
|
|
71
|
+
/** @type {Set<string>} */
|
|
72
|
+
const suppressed = new Set();
|
|
73
|
+
/** @type {AllowlistEntry[]} */
|
|
74
|
+
const expired = [];
|
|
75
|
+
/** @type {AllowlistEntry[]} */
|
|
76
|
+
const invalid = [];
|
|
77
|
+
|
|
78
|
+
for (const entry of allowlist) {
|
|
79
|
+
if (!entry || !entry.id || !entry.expires) {
|
|
80
|
+
invalid.push(entry);
|
|
81
|
+
continue;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
93
|
-
|
|
94
|
-
/** @type {Set<string>} Active (non-expired) suppressed advisory IDs */
|
|
95
|
-
const suppressed = new Set();
|
|
96
|
-
/** @type {AllowlistEntry[]} */
|
|
97
|
-
const expiredEntries = [];
|
|
98
|
-
|
|
99
|
-
for (const entry of allowlist) {
|
|
100
|
-
if (!entry.id || !entry.expires) {
|
|
101
|
-
console.error(
|
|
102
|
-
`[audit-check] ERROR: Allowlist entry missing required "id" or "expires" field: ${JSON.stringify(entry)}`,
|
|
103
|
-
);
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (entry.expires < today) {
|
|
108
|
-
expiredEntries.push(entry);
|
|
109
|
-
} else {
|
|
110
|
-
suppressed.add(entry.id);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (expiredEntries.length > 0) {
|
|
115
|
-
console.error("[audit-check] EXPIRED allowlist entries detected:");
|
|
116
|
-
for (const entry of expiredEntries) {
|
|
117
|
-
console.error(
|
|
118
|
-
` - ${entry.id} (expired ${entry.expires}): ${entry.reason ?? "no reason recorded"}`,
|
|
119
|
-
);
|
|
84
|
+
if (entry.expires < today) {
|
|
85
|
+
expired.push(entry);
|
|
86
|
+
} else {
|
|
87
|
+
suppressed.add(entry.id);
|
|
88
|
+
}
|
|
120
89
|
}
|
|
121
|
-
console.error(
|
|
122
|
-
"[audit-check] Renew or remove expired entries to proceed. Exit 1.",
|
|
123
|
-
);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
90
|
|
|
127
|
-
|
|
128
|
-
// Run pnpm audit (production graph only)
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
|
-
console.log("[audit-check] Running pnpm audit --prod --json ...");
|
|
132
|
-
|
|
133
|
-
let auditOutput = "";
|
|
134
|
-
let auditExitCode = 0;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
auditOutput = execSync("pnpm audit --prod --json 2>/dev/null", {
|
|
138
|
-
encoding: "utf8",
|
|
139
|
-
});
|
|
140
|
-
} catch (err) {
|
|
141
|
-
// pnpm audit exits non-zero when vulnerabilities are found.
|
|
142
|
-
// We want the JSON regardless of the exit code.
|
|
143
|
-
const execError = /** @type {{ stdout?: string; status?: number }} */ (err);
|
|
144
|
-
auditOutput = execError.stdout ?? "";
|
|
145
|
-
auditExitCode = execError.status ?? 1;
|
|
91
|
+
return { suppressed, expired, invalid };
|
|
146
92
|
}
|
|
147
93
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
auditExitCode +
|
|
166
|
-
").",
|
|
94
|
+
/**
|
|
95
|
+
* True when `report` has the recognizable pnpm-audit shape: an object with
|
|
96
|
+
* an `advisories` object. This is the discriminator the fail-closed contract
|
|
97
|
+
* hangs on — a parsed-but-unrecognizable report (e.g. an error envelope) is
|
|
98
|
+
* NOT interpretable.
|
|
99
|
+
*
|
|
100
|
+
* @param {unknown} report
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function isInterpretableReport(report) {
|
|
104
|
+
return (
|
|
105
|
+
report !== null &&
|
|
106
|
+
typeof report === "object" &&
|
|
107
|
+
"advisories" in report &&
|
|
108
|
+
/** @type {Record<string, unknown>} */ (report).advisories !== null &&
|
|
109
|
+
typeof (/** @type {Record<string, unknown>} */ (report).advisories) ===
|
|
110
|
+
"object"
|
|
167
111
|
);
|
|
168
|
-
console.error(auditOutput.slice(0, 2000));
|
|
169
|
-
process.exit(1);
|
|
170
112
|
}
|
|
171
113
|
|
|
172
|
-
// ---------------------------------------------------------------------------
|
|
173
|
-
// Extract advisories
|
|
174
|
-
// ---------------------------------------------------------------------------
|
|
175
|
-
|
|
176
114
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
* },
|
|
189
|
-
* "metadata": { ... }
|
|
190
|
-
* }
|
|
115
|
+
* Extract the blocking (unsuppressed High/Critical) advisories from an
|
|
116
|
+
* interpretable pnpm-audit report. An advisory is suppressed when any of its
|
|
117
|
+
* ids (GHSA id or CVE ids) is present in `suppressed`.
|
|
118
|
+
*
|
|
119
|
+
* Callers MUST gate this behind `isInterpretableReport` — an
|
|
120
|
+
* uninterpretable report yields an empty array here, which is exactly the
|
|
121
|
+
* fail-open trap the CLI guards against separately.
|
|
122
|
+
*
|
|
123
|
+
* @param {unknown} report
|
|
124
|
+
* @param {Set<string>} suppressed active (non-expired) suppressed ids
|
|
125
|
+
* @returns {Array<{ id: string; severity: string; title: string; url: string }>}
|
|
191
126
|
*/
|
|
127
|
+
export function extractBlockingAdvisories(report, suppressed) {
|
|
128
|
+
/** @type {Array<{ id: string; severity: string; title: string; url: string }>} */
|
|
129
|
+
const blocking = [];
|
|
192
130
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const blocking = [];
|
|
131
|
+
if (!isInterpretableReport(report)) {
|
|
132
|
+
return blocking;
|
|
133
|
+
}
|
|
197
134
|
|
|
198
|
-
if (
|
|
199
|
-
report !== null &&
|
|
200
|
-
typeof report === "object" &&
|
|
201
|
-
"advisories" in report &&
|
|
202
|
-
report.advisories !== null &&
|
|
203
|
-
typeof report.advisories === "object"
|
|
204
|
-
) {
|
|
205
135
|
const advisories = /** @type {Record<string, unknown>} */ (
|
|
206
|
-
report.advisories
|
|
136
|
+
/** @type {Record<string, unknown>} */ (report).advisories
|
|
207
137
|
);
|
|
208
138
|
|
|
209
139
|
for (const [, advisory] of Object.entries(advisories)) {
|
|
@@ -240,48 +170,259 @@ if (
|
|
|
240
170
|
});
|
|
241
171
|
}
|
|
242
172
|
}
|
|
173
|
+
|
|
174
|
+
return blocking;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Pure evaluation of a parsed audit report against the active suppression
|
|
179
|
+
* set and the pnpm-audit exit code. This is the fail-closed decision core,
|
|
180
|
+
* lifted out of the CLI so it is unit-testable without spawning pnpm.
|
|
181
|
+
*
|
|
182
|
+
* @param {unknown} report parsed audit JSON (or `null`)
|
|
183
|
+
* @param {number} auditExitCode pnpm audit exit code
|
|
184
|
+
* @param {Set<string>} suppressed active (non-expired) suppressed ids
|
|
185
|
+
* @returns {{ exitCode: number; reason: "clean" | "uninterpretable-failclosed" | "unsuppressed" | "clean-no-advisories"; blocking: Array<{ id: string; severity: string; title: string; url: string }> }}
|
|
186
|
+
*/
|
|
187
|
+
export function evaluateReport(report, auditExitCode, suppressed) {
|
|
188
|
+
if (!isInterpretableReport(report)) {
|
|
189
|
+
if (auditExitCode !== 0) {
|
|
190
|
+
return {
|
|
191
|
+
exitCode: 1,
|
|
192
|
+
reason: "uninterpretable-failclosed",
|
|
193
|
+
blocking: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { exitCode: 0, reason: "clean-no-advisories", blocking: [] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const blocking = extractBlockingAdvisories(report, suppressed);
|
|
200
|
+
if (blocking.length === 0) {
|
|
201
|
+
return { exitCode: 0, reason: "clean", blocking };
|
|
202
|
+
}
|
|
203
|
+
return { exitCode: 1, reason: "unsuppressed", blocking };
|
|
243
204
|
}
|
|
244
205
|
|
|
245
206
|
// ---------------------------------------------------------------------------
|
|
246
|
-
//
|
|
207
|
+
// CLI
|
|
247
208
|
// ---------------------------------------------------------------------------
|
|
248
209
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Parse the CLI argv (minus `node` and the script path) into options.
|
|
212
|
+
*
|
|
213
|
+
* @param {string[]} argv
|
|
214
|
+
* @param {string} [cwd]
|
|
215
|
+
* @returns {{ allowlistPath: string }}
|
|
216
|
+
*/
|
|
217
|
+
export function parseArgs(argv, cwd = process.cwd()) {
|
|
218
|
+
let allowlistPath = null;
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < argv.length; i++) {
|
|
221
|
+
if (argv[i] === "--allowlist" && argv[i + 1]) {
|
|
222
|
+
allowlistPath = resolve(cwd, argv[i + 1]);
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (allowlistPath === null) {
|
|
228
|
+
allowlistPath = resolve(cwd, "audit-allowlist.json");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { allowlistPath };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Load and JSON-parse the allowlist file. Returns `[]` when the file is
|
|
236
|
+
* absent. Throws with a descriptive message on parse failure or when the
|
|
237
|
+
* top-level value is not an array — the CLI turns these into exit 1.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} allowlistPath
|
|
240
|
+
* @returns {AllowlistEntry[]}
|
|
241
|
+
*/
|
|
242
|
+
export function loadAllowlist(allowlistPath) {
|
|
243
|
+
if (!existsSync(allowlistPath)) {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const raw = readFileSync(allowlistPath, "utf8");
|
|
248
|
+
const parsed = JSON.parse(raw);
|
|
249
|
+
|
|
250
|
+
if (!Array.isArray(parsed)) {
|
|
251
|
+
throw new Error(`Allowlist at ${allowlistPath} must be a JSON array.`);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return parsed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Run `pnpm audit --prod --json`, returning the raw stdout and exit code.
|
|
259
|
+
* pnpm audit exits non-zero when vulnerabilities are found; we want the JSON
|
|
260
|
+
* regardless of the exit code.
|
|
261
|
+
*
|
|
262
|
+
* @returns {{ output: string; exitCode: number }}
|
|
263
|
+
*/
|
|
264
|
+
function runPnpmAudit() {
|
|
265
|
+
try {
|
|
266
|
+
const output = execSync("pnpm audit --prod --json 2>/dev/null", {
|
|
267
|
+
encoding: "utf8",
|
|
268
|
+
});
|
|
269
|
+
return { output, exitCode: 0 };
|
|
270
|
+
} catch (err) {
|
|
271
|
+
const execError = /** @type {{ stdout?: string; status?: number }} */ (err);
|
|
272
|
+
return { output: execError.stdout ?? "", exitCode: execError.status ?? 1 };
|
|
273
|
+
}
|
|
254
274
|
}
|
|
255
275
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
276
|
+
/**
|
|
277
|
+
* CLI entrypoint. Returns the process exit code (0 clean, 1 blocking).
|
|
278
|
+
*
|
|
279
|
+
* @param {string[]} argv argv minus `node` and the script path
|
|
280
|
+
* @returns {number}
|
|
281
|
+
*/
|
|
282
|
+
export function runCli(argv) {
|
|
283
|
+
const { allowlistPath } = parseArgs(argv);
|
|
284
|
+
|
|
285
|
+
// --- Load & validate the allowlist ---------------------------------------
|
|
286
|
+
|
|
287
|
+
/** @type {AllowlistEntry[]} */
|
|
288
|
+
let allowlist;
|
|
289
|
+
try {
|
|
290
|
+
allowlist = loadAllowlist(allowlistPath);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error(
|
|
293
|
+
`[audit-check] ERROR: ${err instanceof Error ? err.message : String(err)}`,
|
|
294
|
+
);
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
|
299
|
+
const { suppressed, expired, invalid } = partitionAllowlist(allowlist, today);
|
|
300
|
+
|
|
301
|
+
if (invalid.length > 0) {
|
|
302
|
+
for (const entry of invalid) {
|
|
303
|
+
console.error(
|
|
304
|
+
`[audit-check] ERROR: Allowlist entry missing required "id" or "expires" field: ${JSON.stringify(entry)}`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (expired.length > 0) {
|
|
311
|
+
console.error("[audit-check] EXPIRED allowlist entries detected:");
|
|
312
|
+
for (const entry of expired) {
|
|
313
|
+
console.error(
|
|
314
|
+
` - ${entry.id} (expired ${entry.expires}): ${entry.reason ?? "no reason recorded"}`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
console.error(
|
|
318
|
+
"[audit-check] Renew or remove expired entries to proceed. Exit 1.",
|
|
319
|
+
);
|
|
320
|
+
return 1;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Run pnpm audit (production graph only) ------------------------------
|
|
324
|
+
|
|
325
|
+
console.log("[audit-check] Running pnpm audit --prod --json ...");
|
|
326
|
+
const { output: auditOutput, exitCode: auditExitCode } = runPnpmAudit();
|
|
327
|
+
|
|
328
|
+
// --- Parse audit JSON ----------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/** @type {unknown} */
|
|
331
|
+
let report;
|
|
332
|
+
try {
|
|
333
|
+
report = JSON.parse(auditOutput);
|
|
334
|
+
} catch {
|
|
335
|
+
if (auditExitCode === 0) {
|
|
336
|
+
// No JSON and a clean exit means nothing to audit — clean.
|
|
337
|
+
console.log("[audit-check] No vulnerabilities found. Exit 0.");
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
console.error(
|
|
341
|
+
"[audit-check] ERROR: pnpm audit produced non-JSON output (exit code " +
|
|
342
|
+
auditExitCode +
|
|
343
|
+
").",
|
|
344
|
+
);
|
|
345
|
+
console.error(auditOutput.slice(0, 2000));
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// --- Evaluate: fail closed on an uninterpretable report + non-zero exit --
|
|
350
|
+
//
|
|
351
|
+
// The report parsed as JSON. If it lacks a recognizable `advisories` shape
|
|
352
|
+
// (e.g. an error envelope) AND pnpm audit exited non-zero, we cannot prove
|
|
353
|
+
// the graph is clean — fail closed. A zero exit with no advisories key is
|
|
354
|
+
// the genuine "clean, nothing to report" case and passes.
|
|
355
|
+
const { exitCode, reason, blocking } = evaluateReport(
|
|
356
|
+
report,
|
|
357
|
+
auditExitCode,
|
|
358
|
+
suppressed,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (reason === "uninterpretable-failclosed") {
|
|
362
|
+
console.error(
|
|
363
|
+
"[audit-check] ERROR: pnpm audit exited non-zero (" +
|
|
364
|
+
auditExitCode +
|
|
365
|
+
") and produced a report without a recognizable `advisories` shape. Failing closed.",
|
|
366
|
+
);
|
|
367
|
+
console.error(auditOutput.slice(0, 2000));
|
|
368
|
+
return exitCode;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (reason === "clean-no-advisories") {
|
|
372
|
+
console.log("[audit-check] No vulnerabilities found. Exit 0.");
|
|
373
|
+
return exitCode;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (blocking.length === 0) {
|
|
377
|
+
console.log(
|
|
378
|
+
`[audit-check] No unsuppressed High/Critical vulnerabilities in the prod graph. Exit 0.`,
|
|
379
|
+
);
|
|
380
|
+
return exitCode;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
console.error(
|
|
384
|
+
`[audit-check] ${blocking.length} unsuppressed High/Critical CVE(s) found in prod dependency graph:`,
|
|
385
|
+
);
|
|
259
386
|
|
|
260
|
-
for (const vuln of blocking) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
387
|
+
for (const vuln of blocking) {
|
|
388
|
+
console.error(
|
|
389
|
+
` [${vuln.severity.toUpperCase()}] ${vuln.id}: ${vuln.title}`,
|
|
390
|
+
);
|
|
391
|
+
if (vuln.url) {
|
|
392
|
+
console.error(` → ${vuln.url}`);
|
|
393
|
+
}
|
|
264
394
|
}
|
|
395
|
+
|
|
396
|
+
console.error(
|
|
397
|
+
"\n[audit-check] To suppress a known/accepted CVE, add a dated entry to audit-allowlist.json:",
|
|
398
|
+
);
|
|
399
|
+
console.error(
|
|
400
|
+
JSON.stringify(
|
|
401
|
+
[
|
|
402
|
+
{
|
|
403
|
+
id: blocking[0]?.id ?? "GHSA-xxxx-xxxx-xxxx",
|
|
404
|
+
reason: "Describe why this is accepted and any mitigations in place",
|
|
405
|
+
expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000)
|
|
406
|
+
.toISOString()
|
|
407
|
+
.slice(0, 10),
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
null,
|
|
411
|
+
2,
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
console.error("\n[audit-check] Exit 1.");
|
|
416
|
+
return 1;
|
|
265
417
|
}
|
|
266
418
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.toISOString()
|
|
278
|
-
.slice(0, 10),
|
|
279
|
-
},
|
|
280
|
-
],
|
|
281
|
-
null,
|
|
282
|
-
2,
|
|
283
|
-
),
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
console.error("\n[audit-check] Exit 1.");
|
|
287
|
-
process.exit(1);
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// Direct-invocation guard (skipped when imported by the test suite)
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
const invokedDirectly =
|
|
424
|
+
process.argv[1] && resolve(process.argv[1]).endsWith("audit-check.mjs");
|
|
425
|
+
|
|
426
|
+
if (invokedDirectly) {
|
|
427
|
+
process.exit(runCli(process.argv.slice(2)));
|
|
428
|
+
}
|