@blamejs/exceptd-skills 0.19.33 → 0.19.35
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/CHANGELOG.md +28 -0
- package/bin/exceptd.js +895 -2828
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +170 -76
- package/lib/collectors/cicd-pipeline-compromise.js +113 -136
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +198 -211
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +130 -118
- package/lib/collectors/scan-excludes.js +33 -139
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -155
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +39 -113
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +88 -236
- package/lib/playbook-runner.js +759 -2107
- package/lib/prefetch.js +101 -376
- package/lib/refresh-external.js +199 -633
- package/lib/refresh-network.js +78 -311
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +85 -146
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +28 -27
- package/lib/upstream-check-cli.js +36 -29
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +52 -121
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +78 -286
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -413
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +242 -242
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +29 -28
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +21 -31
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +26 -57
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +63 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +62 -81
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +83 -198
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +7 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +7 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +7 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +63 -148
- package/scripts/release.js +69 -234
- package/scripts/run-e2e-scenarios.js +26 -73
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -141
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* class as a single numbered report instead of dying on the first hit.
|
|
4
|
+
* Grep gate for code-shape bug classes that recur across releases; CLASSES
|
|
5
|
+
* below is the registry.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
* - process-exit-after-stdout-write : a library-callable function writes to
|
|
10
|
-
* the result channel (process.stdout.write / console.log) and then calls
|
|
11
|
-
* process.exit(), which truncates the buffered write when stdout is
|
|
12
|
-
* piped. Route through `safeExit(EXIT_CODES.X); return;` (lib/exit-codes).
|
|
13
|
-
* This is the stdout-flush-truncation class the validate-cves fix closed by hand.
|
|
14
|
-
* - dynamic-regex : `new RegExp(<non-literal>)` — a ReDoS sink when the
|
|
15
|
-
* pattern derives from operator input. Use a static literal, or anchor +
|
|
16
|
-
* length-cap the input, or mark the site `// allow:dynamic-regex —
|
|
17
|
-
* <reason>` when the source is a trusted bundled schema.
|
|
18
|
-
* - orphan-allow-class : an `// allow:<class>` marker whose class is not in
|
|
19
|
-
* VALID_ALLOW_CLASSES, or is missing the `— <reason>` tail. A typo'd
|
|
20
|
-
* marker suppresses nothing, so the underlying violation would ship
|
|
21
|
-
* unflagged — this meta-guard keeps the marker mechanism trustworthy.
|
|
22
|
-
* - unsorted-marked-array : a flat string array tagged `// keep-sorted` that
|
|
23
|
-
* drifted out of alphabetical order. Opt-in — only marked arrays are
|
|
24
|
-
* checked, so a one-time allowlist sort becomes a standing guarantee.
|
|
25
|
-
* - misaligned-marked-run : a `// keep-aligned` const/weight table whose
|
|
26
|
-
* `=`/`:` assignment columns are not all equal. Opt-in, same shape.
|
|
27
|
-
*
|
|
28
|
-
* Exceptions live at the violation site, not in this file:
|
|
7
|
+
* Exceptions live at the violation site:
|
|
29
8
|
* - file-level, in the first 50 lines: // codebase-patterns:allow-file <class> — <reason>
|
|
30
9
|
* - per-line, on the same line or up to 2 lines above: // allow:<class> — <reason>
|
|
31
10
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* - internal-path leaks in operator output -> tests/operator-leak-grep.test.js
|
|
11
|
+
* Owned elsewhere: phase/version vocabulary (check-version-tags.js), test
|
|
12
|
+
* assertions (check-test-coverage.js), and — both now in tests/cli.test.js,
|
|
13
|
+
* which absorbed the separate safe-exit-grep and operator-leak-grep files —
|
|
14
|
+
* the per-file CLI-dispatch process.exit ban and operator-output path leaks.
|
|
37
15
|
*/
|
|
38
16
|
|
|
39
17
|
const fs = require("node:fs");
|
|
@@ -41,8 +19,8 @@ const path = require("node:path");
|
|
|
41
19
|
|
|
42
20
|
const ROOT = path.resolve(__dirname, "..");
|
|
43
21
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
22
|
+
// Classes that accept an `// allow:<class>` marker. orphan-allow-class is the
|
|
23
|
+
// meta-guard itself, so it is not markable.
|
|
46
24
|
const VALID_ALLOW_CLASSES = Object.freeze({
|
|
47
25
|
"process-exit-after-stdout-write": true,
|
|
48
26
|
"dynamic-regex": true,
|
|
@@ -57,8 +35,6 @@ const EXCLUDE_DIRS = new Set([
|
|
|
57
35
|
"data", ".test-output", ".keys", "keys", "coverage",
|
|
58
36
|
]);
|
|
59
37
|
|
|
60
|
-
// ---- file walk -----------------------------------------------------------
|
|
61
|
-
|
|
62
38
|
function relPath(abs) {
|
|
63
39
|
return path.relative(ROOT, abs).split(path.sep).join("/");
|
|
64
40
|
}
|
|
@@ -104,18 +80,15 @@ function readLines(rel) {
|
|
|
104
80
|
return lines;
|
|
105
81
|
}
|
|
106
82
|
|
|
107
|
-
// Strip a trailing `//` line comment
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
// scanner skips string contents — otherwise the rest of the line, including a
|
|
111
|
-
// real `process.exit(...)` / `new RegExp(...)`, was silently truncated away and
|
|
112
|
-
// the detector never fired.
|
|
83
|
+
// Strip a trailing `//` line comment so a class name mentioned in a comment
|
|
84
|
+
// can't arm a detector. String-aware: a `//` inside a quoted string (a `http://`
|
|
85
|
+
// URL) is not a comment — truncating there hides a real hit later on the line.
|
|
113
86
|
function stripLineComment(line) {
|
|
114
87
|
let inStr = null; // active quote char, or null
|
|
115
88
|
for (let i = 0; i < line.length; i++) {
|
|
116
89
|
const ch = line[i];
|
|
117
90
|
if (inStr) {
|
|
118
|
-
if (ch === "\\") { i++; continue; }
|
|
91
|
+
if (ch === "\\") { i++; continue; }
|
|
119
92
|
if (ch === inStr) inStr = null;
|
|
120
93
|
} else if (ch === "'" || ch === '"' || ch === "`") {
|
|
121
94
|
inStr = ch;
|
|
@@ -126,8 +99,6 @@ function stripLineComment(line) {
|
|
|
126
99
|
return line;
|
|
127
100
|
}
|
|
128
101
|
|
|
129
|
-
// ---- allow-marker engine -------------------------------------------------
|
|
130
|
-
|
|
131
102
|
function hasFileAllow(rel, cls) {
|
|
132
103
|
const head = readLines(rel).slice(0, 50);
|
|
133
104
|
const re = new RegExp("codebase-patterns:allow-file\\s+" + cls + "\\b");
|
|
@@ -147,23 +118,10 @@ function filterMarkers(hits, cls) {
|
|
|
147
118
|
return hits.filter((h) => !hasFileAllow(h.file, cls) && !hasLineAllow(h.file, h.line, cls));
|
|
148
119
|
}
|
|
149
120
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
// lines. Braces inside a single/double/template string, a `//` line comment,
|
|
155
|
-
// or a `/* */` block comment do NOT affect depth — otherwise a `{` or `}`
|
|
156
|
-
// typed inside a string literal in the require.main block miscounts the brace
|
|
157
|
-
// balance and the computed block range slides onto an unrelated later function
|
|
158
|
-
// (whose process.exit() is then wrongly treated as a CLI-entry exit and not
|
|
159
|
-
// flagged). `inTemplate` and `inBlockComment` are the cross-line states a
|
|
160
|
-
// per-line stripper cannot model, so the tokenizer state object is threaded
|
|
161
|
-
// line-to-line by the caller.
|
|
162
|
-
//
|
|
163
|
-
// `state` is mutated in place: { inSingle, inDouble, inTemplate, inBlock,
|
|
164
|
-
// templateDepth } — `templateDepth` tracks `${ … }` interpolation nesting so
|
|
165
|
-
// the closing `}` of an interpolation is treated as template punctuation, not
|
|
166
|
-
// a code brace, while braces INSIDE the interpolation expression still count.
|
|
121
|
+
// Counts `{` / `}` in REAL CODE context only: a brace inside a string, template
|
|
122
|
+
// or comment must not move the depth, or the computed require.main range slides
|
|
123
|
+
// onto a later function. `state` is mutated in place and threaded line to line,
|
|
124
|
+
// since `inTemplate` / `inBlock` are cross-line states.
|
|
167
125
|
function countCodeBraces(line, state) {
|
|
168
126
|
let delta = 0;
|
|
169
127
|
for (let i = 0; i < line.length; i++) {
|
|
@@ -190,13 +148,12 @@ function countCodeBraces(line, state) {
|
|
|
190
148
|
// Enter an interpolation expression: braces inside ARE code.
|
|
191
149
|
state.templateExpr.push(0);
|
|
192
150
|
state.inTemplate = false;
|
|
193
|
-
i++;
|
|
151
|
+
i++;
|
|
194
152
|
continue;
|
|
195
153
|
}
|
|
196
154
|
continue;
|
|
197
155
|
}
|
|
198
|
-
|
|
199
|
-
if (ch === "/" && next === "/") break; // `//` — rest of the line is a comment
|
|
156
|
+
if (ch === "/" && next === "/") break;
|
|
200
157
|
if (ch === "/" && next === "*") { state.inBlock = true; i++; continue; }
|
|
201
158
|
if (ch === "'") { state.inSingle = true; continue; }
|
|
202
159
|
if (ch === '"') { state.inDouble = true; continue; }
|
|
@@ -222,17 +179,12 @@ function newBraceState() {
|
|
|
222
179
|
return { inSingle: false, inDouble: false, inTemplate: false, inBlock: false, templateExpr: [] };
|
|
223
180
|
}
|
|
224
181
|
|
|
225
|
-
// Line ranges (1-based, inclusive) of `if (require.main === module) { ... }`
|
|
226
|
-
//
|
|
227
|
-
// is correct. process.exit there is owned by tests/safe-exit-grep.test.js and
|
|
228
|
-
// is not a library-surface concern.
|
|
182
|
+
// Line ranges (1-based, inclusive) of `if (require.main === module) { ... }` blocks,
|
|
183
|
+
// where print-then-exit is correct — owned by tests/safe-exit-grep.test.js.
|
|
229
184
|
function requireMainRanges(lines) {
|
|
230
185
|
const ranges = [];
|
|
231
186
|
for (let i = 0; i < lines.length; i++) {
|
|
232
187
|
if (/\brequire\.main\s*===\s*module\b/.test(lines[i])) {
|
|
233
|
-
// Find the opening brace (same line or next few), then balance —
|
|
234
|
-
// string/comment/template-aware so braces inside literals don't skew the
|
|
235
|
-
// depth (see countCodeBraces).
|
|
236
188
|
let depth = 0;
|
|
237
189
|
let started = false;
|
|
238
190
|
let j = i;
|
|
@@ -252,19 +204,24 @@ function inRanges(ranges, lineNo) {
|
|
|
252
204
|
return ranges.some(([a, b]) => lineNo >= a && lineNo <= b);
|
|
253
205
|
}
|
|
254
206
|
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
// function). The bare-identifier (third) alternative matches a declaration /
|
|
260
|
-
// method-shorthand opener (`foo() {`, `async bar() {`), but it must REFUSE
|
|
261
|
-
// control-flow openers (`for (…) {`, `if (…) {`, `while/switch/catch (…) {`):
|
|
262
|
-
// a control-flow block sitting between a stdout write and a process.exit() is
|
|
263
|
-
// inside the SAME function, so stopping the backward scan there would wrongly
|
|
264
|
-
// leave the exit unflagged. The negative lookahead excludes the control-flow
|
|
265
|
-
// keywords; `function` and arrow alternatives are unchanged.
|
|
207
|
+
// Opens a new function body, so the backward stdout-write scan stops at the
|
|
208
|
+
// enclosing function. The bare-identifier alternative must REFUSE control-flow
|
|
209
|
+
// openers (`if (…) {`, `for (…) {`): those sit inside the SAME function, and
|
|
210
|
+
// stopping there leaves a real exit-after-write unflagged.
|
|
266
211
|
const FUNCTION_START = /(^|[^.\w])function\b|=>\s*\{?\s*$|^\s*(async\s+)?(?!(?:if|for|while|switch|catch|do|else|with|finally|return)\b)[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/;
|
|
267
212
|
|
|
213
|
+
// Scope, stated so it is not mistaken for full coverage of the class: the
|
|
214
|
+
// backward scan recognises a result-channel write only where it is written
|
|
215
|
+
// LITERALLY — `process.stdout.write(` or `console.log(`. A write reached
|
|
216
|
+
// INDIRECTLY, through a helper called from the exiting function (`printHelp()`,
|
|
217
|
+
// `renderSummary()`), is invisible to it, so an exit-after-write of that shape
|
|
218
|
+
// passes this gate and has to be caught by review or a per-file test.
|
|
219
|
+
//
|
|
220
|
+
// Not closed by matching call sites of same-file writer functions: measured over
|
|
221
|
+
// lib/, orchestrator/, scripts/ and bin/, that heuristic cannot tell a helper
|
|
222
|
+
// that writes to STDOUT from one that writes to stderr via console.error, and it
|
|
223
|
+
// fires on exits that are correct. Closing it properly needs call-graph
|
|
224
|
+
// resolution of the write target, not another regex.
|
|
268
225
|
function detectProcessExitAfterStdout(files) {
|
|
269
226
|
const hits = [];
|
|
270
227
|
for (const rel of (files || filesUnder(["bin/exceptd.js", "lib", "orchestrator", "scripts"]))) {
|
|
@@ -275,8 +232,7 @@ function detectProcessExitAfterStdout(files) {
|
|
|
275
232
|
if (!/\bprocess\.exit\s*\(/.test(code)) continue;
|
|
276
233
|
const lineNo = i + 1;
|
|
277
234
|
if (inRanges(mainRanges, lineNo)) continue; // CLI-entry block: legitimate
|
|
278
|
-
// Scan backward within the enclosing function for a result-channel
|
|
279
|
-
// write (console.log / process.stdout.write). Stop at a function start.
|
|
235
|
+
// Scan backward within the enclosing function for a result-channel write.
|
|
280
236
|
let sawStdout = false;
|
|
281
237
|
for (let k = i - 1; k >= 0 && k >= i - 60; k--) {
|
|
282
238
|
const prev = stripLineComment(lines[k]);
|
|
@@ -291,11 +247,8 @@ function detectProcessExitAfterStdout(files) {
|
|
|
291
247
|
return filterMarkers(hits, "process-exit-after-stdout-write");
|
|
292
248
|
}
|
|
293
249
|
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// a backtick template) is operator-derivable and flagged. Backtick is NOT
|
|
297
|
-
// exempt — a template literal can interpolate operator input, so it must be
|
|
298
|
-
// flagged the same as a bare identifier.
|
|
250
|
+
// A first arg opening with `"`, `'` or `/` is a literal, so static and safe.
|
|
251
|
+
// Backtick is NOT exempt — a template literal can interpolate operator input.
|
|
299
252
|
function isStaticRegexFirstChar(ch) {
|
|
300
253
|
return ch === '"' || ch === "'" || ch === "/";
|
|
301
254
|
}
|
|
@@ -312,23 +265,17 @@ function detectDynamicRegex(files) {
|
|
|
312
265
|
hits.push({ file: rel, line: i + 1, content: lines[i].trim() });
|
|
313
266
|
continue;
|
|
314
267
|
}
|
|
315
|
-
// Multi-line form:
|
|
316
|
-
//
|
|
317
|
-
// line. The single-line match above can't see the first-arg char, so it
|
|
318
|
-
// would silently pass a dynamic RegExp whose argument starts next line.
|
|
319
|
-
// Look ahead, skipping blank and comment-only lines (capped at 5), and
|
|
320
|
-
// inspect the first code line's first non-whitespace char.
|
|
268
|
+
// Multi-line form: the pattern arg starts on a later line, so look ahead
|
|
269
|
+
// past blank and comment-only lines, capped at 5.
|
|
321
270
|
if (!/\bnew RegExp\s*\(\s*$/.test(code)) continue;
|
|
322
271
|
let firstChar = null;
|
|
323
272
|
for (let k = i + 1; k <= i + 5 && k < lines.length; k++) {
|
|
324
273
|
const ahead = stripLineComment(lines[k]).replace(/^\s+/, "");
|
|
325
|
-
if (ahead === "") continue;
|
|
274
|
+
if (ahead === "") continue;
|
|
326
275
|
firstChar = ahead[0];
|
|
327
276
|
break;
|
|
328
277
|
}
|
|
329
|
-
//
|
|
330
|
-
// suspicious — flag conservatively. Otherwise apply the SAME literal
|
|
331
|
-
// exemption as the single-line path.
|
|
278
|
+
// Nothing parseable within the cap is suspicious — flag it.
|
|
332
279
|
if (firstChar !== null && isStaticRegexFirstChar(firstChar)) continue;
|
|
333
280
|
hits.push({ file: rel, line: i + 1, content: lines[i].trim() });
|
|
334
281
|
}
|
|
@@ -336,13 +283,9 @@ function detectDynamicRegex(files) {
|
|
|
336
283
|
return filterMarkers(hits, "dynamic-regex");
|
|
337
284
|
}
|
|
338
285
|
|
|
339
|
-
// Raw bidi-override / zero-width / invisible / null codepoints
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
// should emit them programmatically (via vendor/blamejs/codepoint-class) or
|
|
343
|
-
// escape them (\uXXXX), never type them literally. The range table holds only
|
|
344
|
-
// numeric codepoints + the regex is built from escapes, so this detector's own
|
|
345
|
-
// source is clean (and the file self-skips below regardless).
|
|
286
|
+
// Raw bidi-override / zero-width / invisible / null codepoints typed as literals
|
|
287
|
+
// — the Trojan-Source class (CVE-2021-42574). Source emits them via
|
|
288
|
+
// vendor/blamejs/codepoint-class or a \uXXXX escape instead.
|
|
346
289
|
const _BIDI_LITERAL_RANGES = [
|
|
347
290
|
[0x202A, 0x202E], [0x2066, 0x2069], 0x200E, 0x200F, 0x061C, // bidi overrides + isolates
|
|
348
291
|
0x200B, 0x200C, 0x200D, 0x00AD, 0x2060, 0xFEFF, // zero-width / invisible
|
|
@@ -378,13 +321,8 @@ function detectOrphanAllowClass(files) {
|
|
|
378
321
|
const cmt = lines[i].indexOf("//");
|
|
379
322
|
if (cmt === -1) continue;
|
|
380
323
|
const comment = lines[i].slice(cmt);
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
// file-level: codebase-patterns:allow-file <class> — <reason>
|
|
384
|
-
// The file-level form is the broadest exemption (it suppresses every hit
|
|
385
|
-
// of its class in the file), so a reason-less or unknown-class file-level
|
|
386
|
-
// marker must be caught here too — otherwise it would suppress silently
|
|
387
|
-
// and never reach the per-line orphan check.
|
|
324
|
+
// Both marker forms carry the same class + reason rules. The file-level
|
|
325
|
+
// form suppresses every hit of its class, so it must be caught here.
|
|
388
326
|
const fileLevel = comment.match(/\bcodebase-patterns:allow-file\s+([a-z0-9-]+)\b(.*)$/);
|
|
389
327
|
const perLine = comment.match(/\ballow:([a-z0-9-]+)\b(.*)$/);
|
|
390
328
|
const m = fileLevel || perLine;
|
|
@@ -402,16 +340,11 @@ function detectOrphanAllowClass(files) {
|
|
|
402
340
|
return hits;
|
|
403
341
|
}
|
|
404
342
|
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
// code is never flagged. They turn a one-time cleanup (sorting an allowlist,
|
|
408
|
-
// aligning a const table) into a standing guarantee: mark the cleaned site and
|
|
409
|
-
// the gate keeps it clean.
|
|
343
|
+
// The two detectors below fire only on sites that opt in via a marker, so
|
|
344
|
+
// unmarked code is never flagged.
|
|
410
345
|
|
|
411
|
-
// `// keep-sorted` marks a flat string-literal array that must stay
|
|
412
|
-
//
|
|
413
|
-
// carries the marker are checked; arrays containing object/nested elements are
|
|
414
|
-
// skipped (not a flat string list).
|
|
346
|
+
// `// keep-sorted` marks a flat string-literal array that must stay alphabetically
|
|
347
|
+
// sorted; an array with object or nested elements is skipped.
|
|
415
348
|
function scanUnsortedMarkedArray(rel, lines) {
|
|
416
349
|
const hits = [];
|
|
417
350
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -428,7 +361,7 @@ function scanUnsortedMarkedArray(rel, lines) {
|
|
|
428
361
|
body += " " + seg;
|
|
429
362
|
if (started && depth <= 0) break;
|
|
430
363
|
}
|
|
431
|
-
if (/[{]/.test(body)) continue;
|
|
364
|
+
if (/[{]/.test(body)) continue;
|
|
432
365
|
const strs = [];
|
|
433
366
|
const re = /(['"])((?:\\.|(?!\1).)*)\1/g;
|
|
434
367
|
let m;
|
|
@@ -452,9 +385,8 @@ function detectUnsortedMarkedArray(files) {
|
|
|
452
385
|
}
|
|
453
386
|
|
|
454
387
|
// `// keep-aligned` marks a contiguous run of `IDENT = value` / `IDENT: value`
|
|
455
|
-
// lines
|
|
456
|
-
//
|
|
457
|
-
// line. Opt-in, so only deliberately-aligned tables are enforced.
|
|
388
|
+
// lines whose assignment columns must all line up. The run starts at the line
|
|
389
|
+
// after the marker and ends at the first blank or non-assignment line.
|
|
458
390
|
function scanMisalignedMarkedRun(rel, lines) {
|
|
459
391
|
const hits = [];
|
|
460
392
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -487,22 +419,13 @@ function detectMisalignedMarkedRun(files) {
|
|
|
487
419
|
return hits;
|
|
488
420
|
}
|
|
489
421
|
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
// file imports a SQL driver, any SQL STATEMENT (`"SELECT …"`) or CLAUSE built
|
|
494
|
-
// by string concatenation (`… + " WHERE " + …`) is a parameterization/injection
|
|
495
|
-
// sink and must use the driver's bound-parameter API instead. The SQL-driver
|
|
496
|
-
// import is the gate — a SQL-looking string in a file with no driver executes
|
|
497
|
-
// nothing, so prose that merely begins with "Update …" / "Delete …" in a
|
|
498
|
-
// non-DB file is never scanned (no false positives). A trusted static DDL
|
|
499
|
-
// string can opt out with `// allow:hand-rolled-sql — <reason>`.
|
|
422
|
+
// Forward guard: the driver import is the gate, so prose merely beginning
|
|
423
|
+
// "Update …" in a non-DB file is never scanned. In a file that does import one,
|
|
424
|
+
// a statement or a concatenated clause is an injection sink.
|
|
500
425
|
const SQL_DRIVER_IMPORT = /require\(\s*["'](?:node:sqlite|better-sqlite3|sqlite3|sqlite|pg|mysql2?|knex|sequelize|drizzle-orm|postgres|@libsql\/[\w.-]+)(?:\/[^"']*)?["']\s*\)|\bfrom\s+["'](?:node:sqlite|better-sqlite3|pg|mysql2?|knex|sequelize|drizzle-orm)(?:\/[^"']*)?["']/;
|
|
501
426
|
const SQL_STMT_START = /(["'`])\s*(?:SELECT\b|INSERT\s+(?:INTO|OR)\b|REPLACE\s+INTO\b|UPDATE\s+["'`]?[A-Za-z_]|DELETE\s+FROM\b|CREATE\s+(?:TABLE|UNIQUE\s+INDEX|INDEX|TRIGGER|VIRTUAL\s+TABLE)\b|ALTER\s+TABLE\b|DROP\s+(?:TABLE|TRIGGER|INDEX)\b|MERGE\s+INTO\b)/i;
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
// concatenation operator rather than requiring the clause string to close on
|
|
505
|
-
// the same quote with no interior quotes.
|
|
427
|
+
// The trailing-concat form tolerates a quote inside the clause
|
|
428
|
+
// (`" WHERE name = 'x' " + id`) by scanning to the first `+`.
|
|
506
429
|
const SQL_CLAUSE_FRAG = /(?:\+\s*["'`]\s*(?:SET|FROM|WHERE|VALUES|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|LIMIT|OFFSET|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b|["'`]\s*(?:SET|FROM|WHERE|VALUES\s*\(|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b[^+]*\+)/i;
|
|
507
430
|
function detectHandRolledSql(files) {
|
|
508
431
|
const hits = [];
|
|
@@ -564,11 +487,8 @@ const CLASSES = [
|
|
|
564
487
|
];
|
|
565
488
|
|
|
566
489
|
function main() {
|
|
567
|
-
// Fail closed
|
|
568
|
-
//
|
|
569
|
-
// unreadable/empty — so a wholesale missing source tree would make every
|
|
570
|
-
// class report "clean" and the gate pass without scanning anything (the
|
|
571
|
-
// absent-input false-pass class). Refuse to call zero-files-scanned "clean".
|
|
490
|
+
// Fail closed on an empty scan universe: each detector silently finds no hits
|
|
491
|
+
// when its roots are unreadable. Zero files scanned is not "clean".
|
|
572
492
|
const universe = filesUnder(["bin/exceptd.js", "lib", "orchestrator", "scripts"]);
|
|
573
493
|
if (universe.length === 0) {
|
|
574
494
|
console.error("[check-codebase-patterns] FAIL — zero source files found under bin/lib/orchestrator/scripts; refusing to report clean without scanning anything.");
|
|
@@ -2,40 +2,10 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* EPSS score/percentile consistency gate.
|
|
5
|
+
* EPSS score/percentile consistency gate. Exit 0 consistent, 1 inconsistent.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* higher than B on a given day, A's percentile is at least B's. That makes the
|
|
10
|
-
* pair self-checking with no network access — sort a day's entries by score and
|
|
11
|
-
* the percentiles must come out sorted too.
|
|
12
|
-
*
|
|
13
|
-
* The failure this catches is updating one field without the other. A refresh
|
|
14
|
-
* that writes a new score but keeps yesterday's percentile leaves a plausible
|
|
15
|
-
* entry: both numbers are in range, both look like EPSS values, and nothing
|
|
16
|
-
* downstream can tell they describe different days. The result is a CVE ranked
|
|
17
|
-
* as top-decile urgency on a score that no longer supports it, which is exactly
|
|
18
|
-
* the input the prioritisation model trusts most.
|
|
19
|
-
*
|
|
20
|
-
* What the ordering check does and does not establish. Monotonicity is a
|
|
21
|
-
* necessary condition, not a sufficient one: it catches a stale percentile that
|
|
22
|
-
* contradicts the new score's rank, which is what a partial refresh usually
|
|
23
|
-
* produces, but a stale value that happens to preserve the ordering passes.
|
|
24
|
-
* Proving two fields share a publication would require the fetched row, and
|
|
25
|
-
* that is not recoverable from the catalog offline. So this is a corruption
|
|
26
|
-
* detector, not a provenance proof — the provenance guarantee has to come from
|
|
27
|
-
* the write side, by fetching and writing score, percentile and date together.
|
|
28
|
-
*
|
|
29
|
-
* Three checks run:
|
|
30
|
-
* - within each epss_date, sorting by score must sort by percentile;
|
|
31
|
-
* - an entry carries both numeric fields or neither;
|
|
32
|
-
* - epss_note, which is derived text, must restate the current fields.
|
|
33
|
-
*
|
|
34
|
-
* Entries are grouped by epss_date so each publication is checked against
|
|
35
|
-
* itself; comparing across days is meaningless because the whole distribution
|
|
36
|
-
* shifts.
|
|
37
|
-
*
|
|
38
|
-
* Exit codes: 0 consistent, 1 inconsistent.
|
|
7
|
+
* The percentile is the score's rank within one publication, so a cohort must
|
|
8
|
+
* sort the same way by both. Monotonicity is necessary, not sufficient.
|
|
39
9
|
*/
|
|
40
10
|
|
|
41
11
|
const fs = require("node:fs");
|
|
@@ -44,13 +14,8 @@ const path = require("node:path");
|
|
|
44
14
|
const ROOT = path.resolve(__dirname, "..");
|
|
45
15
|
|
|
46
16
|
/**
|
|
47
|
-
* Percentiles
|
|
48
|
-
*
|
|
49
|
-
* is arithmetic, not drift. Real mismatches are pairs pulled from different
|
|
50
|
-
* days, which land orders of magnitude above this: the widest rounding artifact
|
|
51
|
-
* observed in the catalog is 8e-5, while a stale-field mismatch runs 0.05-0.9.
|
|
52
|
-
* The threshold sits between the two, far enough from each that neither
|
|
53
|
-
* classification is a close call.
|
|
17
|
+
* Percentiles publish rounded to five decimals, so adjacent scores can invert by
|
|
18
|
+
* a hair. Rounding artifacts reach 8e-5; a stale-field mismatch runs 0.05-0.9.
|
|
54
19
|
*/
|
|
55
20
|
const ROUNDING_TOLERANCE = 1e-3;
|
|
56
21
|
|
|
@@ -58,24 +23,10 @@ function loadCatalog(file) {
|
|
|
58
23
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
59
24
|
}
|
|
60
25
|
|
|
61
|
-
/**
|
|
62
|
-
* The operator-readable restatement of an entry's EPSS fields. Entries carry it
|
|
63
|
-
* as `epss_note`, and it is derived, not authored — so it must be rebuilt
|
|
64
|
-
* whenever the numbers move.
|
|
65
|
-
*
|
|
66
|
-
* The renderer lives in lib/cve-enrich.js because the refresh writer rebuilds
|
|
67
|
-
* the note from the same definition. A second copy here would let the gate and
|
|
68
|
-
* the writer disagree about the exact wording, which shows up as this gate
|
|
69
|
-
* failing on data that was refreshed correctly.
|
|
70
|
-
*/
|
|
26
|
+
/** Shared with the refresh writer so gate and writer cannot disagree on wording. */
|
|
71
27
|
const { renderEpssNote: renderNote } = require("../lib/cve-enrich.js");
|
|
72
28
|
|
|
73
|
-
/**
|
|
74
|
-
* Group entries by publication date. Entries missing either field are reported
|
|
75
|
-
* separately rather than skipped — a half-populated pair is its own defect, and
|
|
76
|
-
* silently ignoring it would let the gate pass on the very shape it exists to
|
|
77
|
-
* catch.
|
|
78
|
-
*/
|
|
29
|
+
/** Group entries by publication date; a half-populated pair is reported, not skipped. */
|
|
79
30
|
function cohorts(catalog) {
|
|
80
31
|
const groups = new Map();
|
|
81
32
|
const incomplete = [];
|
|
@@ -101,10 +52,7 @@ function cohorts(catalog) {
|
|
|
101
52
|
return { groups, incomplete };
|
|
102
53
|
}
|
|
103
54
|
|
|
104
|
-
/**
|
|
105
|
-
* Sort a cohort by score and report every place the percentile goes backwards
|
|
106
|
-
* by more than rounding can explain.
|
|
107
|
-
*/
|
|
55
|
+
/** Sort by score; report every percentile step backwards that rounding cannot explain. */
|
|
108
56
|
function inversions(rows) {
|
|
109
57
|
const sorted = [...rows].sort((a, b) => a.score - b.score);
|
|
110
58
|
const found = [];
|
|
@@ -141,10 +89,7 @@ function check(catalogFile) {
|
|
|
141
89
|
failures.push(`${row.id}: has one EPSS field but not ${row.missing} — write both together or neither`);
|
|
142
90
|
}
|
|
143
91
|
|
|
144
|
-
//
|
|
145
|
-
// moves the numbers and leaves the sentence describing the previous
|
|
146
|
-
// publication, so the entry states two different scores as of two different
|
|
147
|
-
// dates. It is derived text, so it can be checked exactly.
|
|
92
|
+
// epss_note is derived text, so it can be checked exactly against the fields.
|
|
148
93
|
for (const [id, entry] of Object.entries(catalog)) {
|
|
149
94
|
if (typeof entry.epss_note !== "string") continue;
|
|
150
95
|
if (typeof entry.epss_score !== "number" || typeof entry.epss_percentile !== "number") continue;
|
|
@@ -1,31 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/*
|
|
3
|
-
*
|
|
3
|
+
* Enforces global-first coverage (AGENTS.md Hard Rule #5): every curated CVE in
|
|
4
|
+
* data/cve-catalog.json must declare a framework_control_gaps statement for all
|
|
5
|
+
* five jurisdiction buckets, so a multi-jurisdiction operator reading the
|
|
6
|
+
* offline catalog gets more than a US-centric subset.
|
|
4
7
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* for all five jurisdiction buckets:
|
|
8
|
+
* Draft entries (_auto_imported) are exempt — they carry raw NVD data, and the
|
|
9
|
+
* curation bar requires promotion before shipping, at which point this applies.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* AU — AU-Essential-8-* / AU-ISM-*
|
|
13
|
-
* ISO — ISO-27001-2022-*
|
|
14
|
-
*
|
|
15
|
-
* A multi-jurisdiction operator reading the offline catalog's framework-gap
|
|
16
|
-
* output must get coverage for every required jurisdiction on every CVE, not
|
|
17
|
-
* a US-centric subset. Before this gate, two-thirds of the corpus mapped only
|
|
18
|
-
* a subset; a codex review surfaced it, the corpus was completed, and this
|
|
19
|
-
* gate keeps it from regressing when new CVEs are curated.
|
|
20
|
-
*
|
|
21
|
-
* Draft entries (_auto_imported === true) are exempt — they carry raw NVD
|
|
22
|
-
* data that has not been through curation yet. The curation bar (elsewhere)
|
|
23
|
-
* requires drafts to be promoted before shipping, at which point this gate
|
|
24
|
-
* applies.
|
|
25
|
-
*
|
|
26
|
-
* Exit code: 0 when every curated entry is complete, 1 when any entry omits a
|
|
27
|
-
* required bucket (the list is printed). Uses process.exitCode (not
|
|
28
|
-
* process.exit) so buffered stdout drains before the process ends.
|
|
11
|
+
* Exits 0 when every curated entry is complete, 1 when any omits a bucket.
|
|
12
|
+
* `process.exitCode`, never `process.exit()`: the failure list is a buffered
|
|
13
|
+
* stdout write that the exit can truncate.
|
|
29
14
|
*/
|
|
30
15
|
|
|
31
16
|
'use strict';
|
|
@@ -34,19 +19,16 @@ const fs = require('node:fs');
|
|
|
34
19
|
const path = require('node:path');
|
|
35
20
|
|
|
36
21
|
const ROOT = path.resolve(__dirname, '..');
|
|
37
|
-
//
|
|
38
|
-
// a fixture; defaults to the shipped catalog for the real predeploy run.
|
|
22
|
+
// argv[2] overrides the catalog so the gate's own test can point at a fixture.
|
|
39
23
|
const CATALOG = process.argv[2] || path.join(ROOT, 'data', 'cve-catalog.json');
|
|
40
24
|
|
|
41
25
|
const REQUIRED = ['NIST', 'EU', 'UK', 'AU', 'ISO'];
|
|
42
26
|
|
|
43
27
|
function bucketOf(key) {
|
|
44
28
|
if (/^NIST/i.test(key)) return 'NIST';
|
|
45
|
-
// EU bucket is the security-regulation family
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
// GDPR still owes NIS2/DORA/EU-AI coverage, so GDPR does not satisfy this
|
|
49
|
-
// bucket. Note EU-AI / EU-CRA match here; EU-GDPR deliberately does not.
|
|
29
|
+
// The EU bucket is the security-regulation family Hard Rule #5 names. GDPR is
|
|
30
|
+
// data protection and does NOT satisfy it: an entry mapped only to GDPR still
|
|
31
|
+
// owes NIS2/DORA/EU-AI coverage, so EU-GDPR must not match here.
|
|
50
32
|
if (/^(NIS2|DORA|EU-AI|EU-CRA)/i.test(key)) return 'EU';
|
|
51
33
|
if (/^UK-CAF/i.test(key)) return 'UK';
|
|
52
34
|
if (/^(AU-Essential-?8|AU-ISM|Essential-?8)/i.test(key)) return 'AU';
|