@blamejs/exceptd-skills 0.18.11 → 0.18.13
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 +20 -0
- package/bin/exceptd.js +12 -5
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +10 -1
- package/lib/collectors/cred-stores.js +18 -9
- package/lib/collectors/secrets.js +4 -2
- package/lib/cve-regression-watcher.js +5 -1
- package/lib/framework-gap.js +25 -14
- package/lib/job-queue.js +9 -2
- package/lib/lint-skills.js +34 -5
- package/lib/playbook-runner.js +65 -2
- package/lib/prefetch.js +15 -7
- package/lib/rfc-cli.js +7 -2
- package/lib/schemas/skill-frontmatter.schema.json +2 -2
- package/lib/scoring.js +40 -7
- package/lib/source-ghsa.js +8 -2
- package/lib/source-osv.js +19 -3
- package/lib/ttp-mapper.js +15 -0
- package/lib/upstream-check.js +17 -3
- package/lib/validate-cve-catalog.js +16 -4
- package/lib/validate-package.js +9 -2
- package/lib/validate-playbooks.js +35 -10
- package/lib/validate-vendor.js +11 -5
- package/lib/verify.js +35 -34
- package/lib/xml-tokenizer.js +36 -8
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +8 -0
- package/orchestrator/scanner.js +20 -0
- package/package.json +1 -1
- package/sbom.cdx.json +87 -87
- package/scripts/builders/cwe-chains.js +5 -2
- package/scripts/builders/summary-cards.js +12 -4
- package/scripts/check-codebase-patterns-currency.js +1 -0
- package/scripts/check-codebase-patterns.js +42 -0
- package/scripts/check-test-subjects.js +27 -6
- package/scripts/predeploy.js +19 -8
- package/scripts/refresh-upstream-catalogs.js +20 -3
- package/sources/validators/cve-validator.js +7 -1
- package/sources/validators/rfc-validator.js +21 -3
- package/sources/validators/version-pin-validator.js +39 -2
- package/vendor/blamejs/_PROVENANCE.json +3 -2
- package/vendor/blamejs/worker-pool.js +6 -0
|
@@ -49,6 +49,7 @@ const VALID_ALLOW_CLASSES = Object.freeze({
|
|
|
49
49
|
"bidi-codepoint-literal": true,
|
|
50
50
|
"unsorted-marked-array": true,
|
|
51
51
|
"misaligned-marked-run": true,
|
|
52
|
+
"hand-rolled-sql": true,
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
const EXCLUDE_DIRS = new Set([
|
|
@@ -486,6 +487,37 @@ function detectMisalignedMarkedRun(files) {
|
|
|
486
487
|
return hits;
|
|
487
488
|
}
|
|
488
489
|
|
|
490
|
+
// ---- hand-rolled SQL in a file that talks to a database ------------------
|
|
491
|
+
//
|
|
492
|
+
// exceptd ships no database today, so this is a FORWARD guard: the moment a
|
|
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>`.
|
|
500
|
+
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
|
+
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
|
+
// Trailing-concat form tolerates an embedded SQL string-quote inside the
|
|
503
|
+
// clause (e.g. `" WHERE name = 'x' " + id`) by scanning to the first `+`
|
|
504
|
+
// concatenation operator rather than requiring the clause string to close on
|
|
505
|
+
// the same quote with no interior quotes.
|
|
506
|
+
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
|
+
function detectHandRolledSql(files) {
|
|
508
|
+
const hits = [];
|
|
509
|
+
for (const rel of (files || filesUnder(["lib", "orchestrator", "bin/exceptd.js", "scripts"]))) {
|
|
510
|
+
const lines = readLines(rel);
|
|
511
|
+
if (!SQL_DRIVER_IMPORT.test(lines.join("\n"))) continue; // not a DB file — never scan prose
|
|
512
|
+
for (let i = 0; i < lines.length; i++) {
|
|
513
|
+
const code = stripLineComment(lines[i]);
|
|
514
|
+
if (SQL_STMT_START.test(code)) { hits.push({ file: rel, line: i + 1, content: lines[i].trim().slice(0, 110), why: "SQL statement in a string literal" }); continue; }
|
|
515
|
+
if (SQL_CLAUSE_FRAG.test(code)) { hits.push({ file: rel, line: i + 1, content: lines[i].trim().slice(0, 110), why: "SQL clause built by concatenation" }); }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return filterMarkers(hits, "hand-rolled-sql");
|
|
519
|
+
}
|
|
520
|
+
|
|
489
521
|
const CLASSES = [
|
|
490
522
|
{
|
|
491
523
|
id: "process-exit-after-stdout-write",
|
|
@@ -523,6 +555,12 @@ const CLASSES = [
|
|
|
523
555
|
warnOnly: false,
|
|
524
556
|
hint: "a typo'd or reason-less `// allow:<class>` suppresses nothing — fix the class id or add `— <reason>`",
|
|
525
557
|
},
|
|
558
|
+
{
|
|
559
|
+
id: "hand-rolled-sql",
|
|
560
|
+
run: detectHandRolledSql,
|
|
561
|
+
warnOnly: false,
|
|
562
|
+
hint: "a SQL statement/clause assembled as a string in a file that imports a SQL driver is an injection sink — use bound parameters, or `// allow:hand-rolled-sql — <reason>` for a trusted static DDL string",
|
|
563
|
+
},
|
|
526
564
|
];
|
|
527
565
|
|
|
528
566
|
function main() {
|
|
@@ -559,6 +597,10 @@ module.exports = {
|
|
|
559
597
|
detectOrphanAllowClass,
|
|
560
598
|
detectUnsortedMarkedArray,
|
|
561
599
|
detectMisalignedMarkedRun,
|
|
600
|
+
detectHandRolledSql,
|
|
601
|
+
SQL_DRIVER_IMPORT,
|
|
602
|
+
SQL_STMT_START,
|
|
603
|
+
SQL_CLAUSE_FRAG,
|
|
562
604
|
scanUnsortedMarkedArray,
|
|
563
605
|
scanMisalignedMarkedRun,
|
|
564
606
|
requireMainRanges,
|
|
@@ -54,8 +54,17 @@ function deriveSubjects() {
|
|
|
54
54
|
if (!["lib", "scripts", "orchestrator", "bin"].includes(parent)) add(parent + "-" + base, "alias:" + rel);
|
|
55
55
|
const txt = read(rel);
|
|
56
56
|
for (const m of txt.matchAll(/(?:^|\n)\s*(?:async\s+)?(?:function|class)\s+([A-Za-z_$][\w$]*)/g)) add(camelKebab(m[1]), "fn:" + rel);
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Capture the FULL module.exports object via a brace-balanced scan. A
|
|
58
|
+
// non-greedy /\{([\s\S]*?)\}/ stops at the first nested `}` and drops
|
|
59
|
+
// every export name after it (the brace-truncation class), under-deriving
|
|
60
|
+
// subjects so a real export silently has no required test.
|
|
61
|
+
const expAt = txt.search(/module\.exports\s*=\s*\{/);
|
|
62
|
+
if (expAt >= 0) {
|
|
63
|
+
const open = txt.indexOf("{", expAt);
|
|
64
|
+
let depth = 0, end = -1;
|
|
65
|
+
for (let k = open; k < txt.length; k++) { const ch = txt[k]; if (ch === "{") depth++; else if (ch === "}" && --depth === 0) { end = k; break; } }
|
|
66
|
+
if (end > open) for (const m of txt.slice(open + 1, end).matchAll(/([A-Za-z_$][\w$]*)\s*[,:}\n]/g)) add(camelKebab(m[1]), "fn:" + rel);
|
|
67
|
+
}
|
|
59
68
|
}
|
|
60
69
|
}
|
|
61
70
|
["lib", "orchestrator", "scripts", "bin", "sources/validators"].forEach(walkSrc);
|
|
@@ -74,7 +83,14 @@ function deriveSubjects() {
|
|
|
74
83
|
// data catalog files
|
|
75
84
|
for (const e of ls("data")) if (e.isFile() && e.name.endsWith(".json")) add(e.name.replace(/\.json$/, ""), "data");
|
|
76
85
|
// data ENTRY primitives: every CVE id + every playbook
|
|
77
|
-
|
|
86
|
+
// CVE-primitive subjects. An unreadable / malformed / empty catalog must NOT
|
|
87
|
+
// silently derive zero CVE subjects — that would let the reverse-coverage gate
|
|
88
|
+
// PASS with no CVE coverage at all (the absent-input false-pass class). Fail
|
|
89
|
+
// loud instead.
|
|
90
|
+
let cveDerived = 0;
|
|
91
|
+
try { const cat = JSON.parse(read("data/cve-catalog.json")); for (const k of Object.keys(cat)) if (k !== "_meta") { add(k.toLowerCase(), "cve-primitive"); cveDerived++; } }
|
|
92
|
+
catch (e) { throw new Error("check-test-subjects: cannot read/parse data/cve-catalog.json — refusing to derive subjects (reverse coverage would falsely pass with no CVE coverage): " + e.message); }
|
|
93
|
+
if (cveDerived === 0) throw new Error("check-test-subjects: data/cve-catalog.json yielded zero CVE entries — refusing to let reverse coverage pass with no CVE coverage.");
|
|
78
94
|
for (const e of ls("data/playbooks")) if (e.isFile() && e.name.endsWith(".json")) { const b = e.name.replace(/\.json$/, ""); add(b, "playbook-primitive"); add("playbook-" + b, "alias:playbook"); }
|
|
79
95
|
// workflows
|
|
80
96
|
for (const e of ls(".github/workflows")) if (/\.ya?ml$/.test(e.name)) { const b = e.name.replace(/\.ya?ml$/, ""); add(b, "workflow"); add(b + "-workflow", "workflow"); }
|
|
@@ -111,14 +127,19 @@ function run() {
|
|
|
111
127
|
for (const t of testFiles) if (!subjects.has(t.toLowerCase())) forward.push({ file: "tests/" + t + ".test.js", suggested: suggest(t) });
|
|
112
128
|
const reverse = [];
|
|
113
129
|
for (const [s, kind] of subjects) if (!testSet.has(s)) reverse.push({ subject: s, kind });
|
|
114
|
-
|
|
130
|
+
// Reverse-REQUIRED subset: only module / cve / playbook subjects must have a
|
|
131
|
+
// test (aliases, data files, cli verbs, repo artifacts are valid targets but
|
|
132
|
+
// not reverse-required). The exit-code logic gates on THIS, consistently
|
|
133
|
+
// across --worklist and the human/predeploy paths.
|
|
134
|
+
const reverseRequired = reverse.filter((x) => x.kind.startsWith("module:") || x.kind.startsWith("cve-primitive") || x.kind.startsWith("playbook-primitive"));
|
|
135
|
+
return { subjects: subjects.size, forward, reverse, reverseRequired };
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
if (require.main === module) {
|
|
118
139
|
const r = run();
|
|
119
|
-
|
|
140
|
+
const revMods = r.reverseRequired;
|
|
141
|
+
if (process.argv.includes("--worklist")) { process.stdout.write(JSON.stringify(r) + "\n"); process.exitCode = (r.forward.length || revMods.length) ? 1 : 0; }
|
|
120
142
|
else {
|
|
121
|
-
const revMods = r.reverse.filter((x) => x.kind.startsWith("module:") || x.kind.startsWith("cve-primitive") || x.kind.startsWith("playbook-primitive"));
|
|
122
143
|
console.log(`[check-test-subjects] valid subjects=${r.subjects} | FORWARD violations=${r.forward.length} | REVERSE (module/cve/playbook) gaps=${revMods.length}`);
|
|
123
144
|
if (r.forward.length || revMods.length) { console.log("[check-test-subjects] FAIL — run with --worklist for the full list."); process.exitCode = 1; }
|
|
124
145
|
else console.log("[check-test-subjects] ok — every test maps to a subject and every subject has a test.");
|
package/scripts/predeploy.js
CHANGED
|
@@ -335,16 +335,27 @@ function runGate(gate) {
|
|
|
335
335
|
const ceil = typeof gate.informationalMaxExitCode === "number"
|
|
336
336
|
? gate.informationalMaxExitCode
|
|
337
337
|
: Infinity;
|
|
338
|
-
// A
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
338
|
+
// A spawn failure (spawnSync returns r.error set, status:null, signal:null —
|
|
339
|
+
// e.g. the gate command is missing / ENOENT / EACCES) is a crash, not an
|
|
340
|
+
// informational soft-signal. So is a signal kill (status:null with r.signal
|
|
341
|
+
// set — e.g. a 137 OOM kill) and a status that exceeds the soft-signal
|
|
342
|
+
// ceiling. Without surfacing the spawn-error case, an informational gate
|
|
343
|
+
// that never even ran fell through to "informational" and the release
|
|
344
|
+
// proceeded as if the gate had merely produced advisory output. The
|
|
345
|
+
// status===null && !signal case (no error object, but the process never
|
|
346
|
+
// produced an exit code) is treated the same way — a gate that did not
|
|
347
|
+
// exit cleanly cannot be classified as a soft signal.
|
|
348
|
+
const spawnFailed = !!r.error || (r.status === null && !r.signal);
|
|
349
|
+
if (r.error || r.signal || spawnFailed || (r.status !== null && r.status > ceil)) {
|
|
343
350
|
return {
|
|
344
351
|
status: "failed",
|
|
345
|
-
exitCode: r.status,
|
|
346
|
-
message: r.
|
|
352
|
+
exitCode: r.status ?? null,
|
|
353
|
+
message: r.error
|
|
354
|
+
? `informational gate failed to spawn (treated as a crash): ${r.error.message}`
|
|
355
|
+
: r.signal
|
|
347
356
|
? `informational gate killed by signal ${r.signal} (treated as a crash)`
|
|
357
|
+
: r.status === null
|
|
358
|
+
? `informational gate did not exit cleanly (no exit code, no signal) — treated as a crash`
|
|
348
359
|
: `informational gate crashed (exit ${r.status} > informationalMaxExitCode=${ceil})`,
|
|
349
360
|
durationMs,
|
|
350
361
|
warnCount,
|
|
@@ -438,7 +449,7 @@ function main() {
|
|
|
438
449
|
process.exit(failures.length > 0 ? 1 : 0); // allow:process-exit-after-stdout-write — local-only gate runner; output is the human/CI summary written synchronously above, never a piped --json result channel
|
|
439
450
|
}
|
|
440
451
|
|
|
441
|
-
module.exports = { GATES };
|
|
452
|
+
module.exports = { GATES, runGate };
|
|
442
453
|
|
|
443
454
|
if (require.main === module) {
|
|
444
455
|
try {
|
|
@@ -275,8 +275,13 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
|
|
|
275
275
|
const cur = cat[id];
|
|
276
276
|
if (!cur) continue;
|
|
277
277
|
let touched = false;
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
// Mirror the new-add path's `|| e.status` fallback: an upstream status
|
|
279
|
+
// outside RFC_STATUS_MAP must NOT write `undefined` (which would drop the
|
|
280
|
+
// status field on an existing curated row). Fall back to the raw upstream
|
|
281
|
+
// status, and only bump when the mapped value actually differs.
|
|
282
|
+
const mapped = RFC_STATUS_MAP[e.status] || e.status;
|
|
283
|
+
if (cur._auto_imported && mapped && cur.status !== mapped) {
|
|
284
|
+
cur.status = mapped;
|
|
280
285
|
touched = true;
|
|
281
286
|
statusBumped++;
|
|
282
287
|
}
|
|
@@ -654,6 +659,15 @@ function backfillAtlas(cur, fresh) {
|
|
|
654
659
|
if (!cur[key] && val) { cur[key] = val; touched = true; }
|
|
655
660
|
}
|
|
656
661
|
};
|
|
662
|
+
// Parity with backfillAttack: include the short description + tactic in the
|
|
663
|
+
// backfill set. Existing curated ATLAS rows often carry only {name} and need
|
|
664
|
+
// the short description + tactic too, not just description_full/platforms/etc.
|
|
665
|
+
fillIfEmpty("description", fresh.description);
|
|
666
|
+
// tactic: atlasEntryFromStix() emits a STRING for a single-tactic technique
|
|
667
|
+
// and an array for multi-tactic, so backfill both forms. fillIfEmpty only
|
|
668
|
+
// writes when cur is empty, so an existing (string OR array) tactic is never
|
|
669
|
+
// overwritten — the common single-tactic case is no longer left unhydrated.
|
|
670
|
+
fillIfEmpty("tactic", fresh.tactic);
|
|
657
671
|
fillIfEmpty("description_full", fresh.description_full);
|
|
658
672
|
fillIfEmpty("platforms", fresh.platforms);
|
|
659
673
|
fillIfEmpty("detection", fresh.detection);
|
|
@@ -928,5 +942,8 @@ module.exports = {
|
|
|
928
942
|
// Exported for regression tests: fetchUrl's status/redirect handling and
|
|
929
943
|
// writeCatalog's atomicity are load-bearing fail-closed properties.
|
|
930
944
|
fetchUrl,
|
|
931
|
-
writeCatalog
|
|
945
|
+
writeCatalog,
|
|
946
|
+
// Exported for regression tests: backfillAtlas must mirror backfillAttack's
|
|
947
|
+
// description + array-tactic backfill on curated rows.
|
|
948
|
+
backfillAtlas
|
|
932
949
|
};
|
|
@@ -272,7 +272,13 @@ async function validateCve(cveId, localEntry) {
|
|
|
272
272
|
const disputed = (fetched.cve_tags || []).some(t => /disputed/i.test(t)) ||
|
|
273
273
|
/disputed/i.test(fetched.nvd_vuln_status || '');
|
|
274
274
|
if (cveFoundInNvd && disputed) {
|
|
275
|
-
|
|
275
|
+
// `local` is the projected shape (cvss_*, cisa_kev*, epss_*) and never
|
|
276
|
+
// carries a status field — reading local?.status here always yielded null,
|
|
277
|
+
// so the local side of a disputed discrepancy reported nothing. Read the
|
|
278
|
+
// catalog's own status/cve_status off the RAW input so an entry that
|
|
279
|
+
// already records the dispute (or a stale "active" status) is surfaced.
|
|
280
|
+
const localCveStatus = localEntry?.cve_status ?? localEntry?.status ?? null;
|
|
281
|
+
pushDiscrepancy(discrepancies, 'cve_status', localCveStatus, 'disputed', 'high');
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
// --- Compare CVSS (only if NVD reachable & has data) ---
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* drift — at least one of those fields disagrees.
|
|
14
14
|
* missing — Datatracker does not have this RFC / draft.
|
|
15
15
|
* unreachable — network failed or the request timed out.
|
|
16
|
+
* skipped — the catalog key is not an IETF RFC/DRAFT (e.g. CSAF-2.0,
|
|
17
|
+
* ISO-29147, ISO-30111). Datatracker only tracks IETF
|
|
18
|
+
* documents, so these have no upstream to compare against —
|
|
19
|
+
* they are intentionally not validated here, NOT drift.
|
|
16
20
|
*
|
|
17
21
|
* Zero external dependencies. Node 24 stdlib (fetch + AbortController).
|
|
18
22
|
*
|
|
@@ -107,15 +111,26 @@ function compareEntry(local, upstream) {
|
|
|
107
111
|
return discrepancies;
|
|
108
112
|
}
|
|
109
113
|
|
|
114
|
+
// Datatracker only tracks IETF RFCs and Internet-Drafts. A catalog key that is
|
|
115
|
+
// neither (CSAF-2.0, ISO-29147, ISO-30111, …) has no IETF upstream to compare
|
|
116
|
+
// against, so it is out of scope for this validator rather than a discrepancy.
|
|
117
|
+
function isIetfKey(id) {
|
|
118
|
+
return typeof id === 'string' && (id.startsWith('RFC-') || id.startsWith('DRAFT-'));
|
|
119
|
+
}
|
|
120
|
+
|
|
110
121
|
async function validateRfc(id, entry) {
|
|
111
122
|
let docName;
|
|
112
123
|
if (id.startsWith('RFC-')) docName = rfcNumberFromKey(id);
|
|
113
124
|
else if (id.startsWith('DRAFT-')) docName = draftSlugFromKey(id);
|
|
114
125
|
else {
|
|
126
|
+
// Not an IETF document — there is nothing on Datatracker to diff against.
|
|
127
|
+
// Report a distinct, non-drift status with no discrepancies so callers
|
|
128
|
+
// don't surface a permanent false-positive "drift" for every ISO/CSAF
|
|
129
|
+
// reference the catalog carries.
|
|
115
130
|
return {
|
|
116
131
|
id,
|
|
117
|
-
status: '
|
|
118
|
-
discrepancies: [
|
|
132
|
+
status: 'skipped',
|
|
133
|
+
discrepancies: [],
|
|
119
134
|
local: entry,
|
|
120
135
|
fetched: null,
|
|
121
136
|
};
|
|
@@ -150,7 +165,10 @@ async function validateRfc(id, entry) {
|
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
async function validateAllRfcs(refs, { concurrency = 4 } = {}) {
|
|
153
|
-
|
|
168
|
+
// Skip "_"-prefixed meta keys AND any non-IETF reference (ISO/CSAF/etc.):
|
|
169
|
+
// Datatracker has no record of those, so validating them only ever produced
|
|
170
|
+
// permanent false-positive "drift". They are simply out of scope here.
|
|
171
|
+
const ids = Object.keys(refs).filter(k => !k.startsWith('_') && isIetfKey(k));
|
|
154
172
|
const results = [];
|
|
155
173
|
for (let i = 0; i < ids.length; i += concurrency) {
|
|
156
174
|
const batch = ids.slice(i, i + concurrency);
|
|
@@ -7,8 +7,17 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - MITRE ATLAS: GitHub releases (mitre-atlas/atlas-data)
|
|
9
9
|
* - MITRE ATT&CK: GitHub releases (mitre-attack/attack-stix-data)
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* MITRE D3FEND and MITRE CWE do NOT publish tagged GitHub releases — D3FEND
|
|
12
|
+
* ships its ontology from d3fend/d3fend-ontology untagged, and CWE distributes
|
|
13
|
+
* XML from cwe.mitre.org. Their pin currency is tracked on the upstream-check
|
|
14
|
+
* path (lib/upstream-check.js) against those canonical mitre.org endpoints, not
|
|
15
|
+
* via the GitHub releases API. They remain enumerated here for completeness but
|
|
16
|
+
* are flagged `github_releases: false`, so the GitHub-release check is skipped
|
|
17
|
+
* for them and they are reported as `tracked_elsewhere` rather than emitting a
|
|
18
|
+
* spurious `unreachable` (a release API call for a repo with no releases always
|
|
19
|
+
* fails). This keeps the live path checking the same two GitHub-release repos
|
|
20
|
+
* the cache path (pinsDiffFromCache) checks.
|
|
12
21
|
*
|
|
13
22
|
* Each check returns:
|
|
14
23
|
* { pin_name, local_version, latest_version, drift: bool, source_url, error? }
|
|
@@ -29,24 +38,35 @@ const PINS = [
|
|
|
29
38
|
repo: "mitre-atlas/atlas-data",
|
|
30
39
|
local_path_hint: "manifest.json — atlas_version",
|
|
31
40
|
strip_v_prefix: true,
|
|
41
|
+
github_releases: true,
|
|
32
42
|
},
|
|
33
43
|
{
|
|
34
44
|
pin_name: "attack_version",
|
|
35
45
|
repo: "mitre-attack/attack-stix-data",
|
|
36
46
|
local_path_hint: "manifest.json — attack_version",
|
|
37
47
|
strip_v_prefix: true,
|
|
48
|
+
github_releases: true,
|
|
38
49
|
},
|
|
39
50
|
{
|
|
51
|
+
// D3FEND does not tag GitHub releases — currency is tracked via
|
|
52
|
+
// lib/upstream-check.js against the canonical mitre.org endpoint. Listed
|
|
53
|
+
// here for completeness; the GitHub-release check is skipped.
|
|
40
54
|
pin_name: "d3fend_version",
|
|
41
55
|
repo: "d3fend/d3fend-data",
|
|
42
56
|
local_path_hint: "data/d3fend-catalog.json _meta.version",
|
|
43
57
|
strip_v_prefix: true,
|
|
58
|
+
github_releases: false,
|
|
59
|
+
tracked_via: "lib/upstream-check.js (d3fend.mitre.org)",
|
|
44
60
|
},
|
|
45
61
|
{
|
|
62
|
+
// CWE distributes XML from cwe.mitre.org with no tagged GitHub releases —
|
|
63
|
+
// currency is tracked via lib/upstream-check.js. GitHub-release check skipped.
|
|
46
64
|
pin_name: "cwe_version",
|
|
47
65
|
repo: "mitre/cwe",
|
|
48
66
|
local_path_hint: "data/cwe-catalog.json _meta.version",
|
|
49
67
|
strip_v_prefix: true,
|
|
68
|
+
github_releases: false,
|
|
69
|
+
tracked_via: "lib/upstream-check.js (cwe.mitre.org)",
|
|
50
70
|
},
|
|
51
71
|
];
|
|
52
72
|
|
|
@@ -113,6 +133,23 @@ async function checkAllPins(ctx) {
|
|
|
113
133
|
const out = [];
|
|
114
134
|
for (const pin of PINS) {
|
|
115
135
|
const local = normalize(resolveLocalVersion(pin.pin_name, ctx), pin.strip_v_prefix);
|
|
136
|
+
// Pins whose upstream does not publish GitHub releases (D3FEND, CWE) have
|
|
137
|
+
// no release tag to compare against — calling the releases API would always
|
|
138
|
+
// fail and surface a spurious `unreachable`. Report them as tracked
|
|
139
|
+
// elsewhere (drift:null, no error) so the live path checks exactly the same
|
|
140
|
+
// GitHub-release repos as the cache path.
|
|
141
|
+
if (pin.github_releases === false) {
|
|
142
|
+
out.push({
|
|
143
|
+
pin_name: pin.pin_name,
|
|
144
|
+
local_version: local,
|
|
145
|
+
latest_version: null,
|
|
146
|
+
drift: null,
|
|
147
|
+
tracked_elsewhere: true,
|
|
148
|
+
tracked_via: pin.tracked_via || null,
|
|
149
|
+
local_path_hint: pin.local_path_hint,
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
116
153
|
const release = await latestGithubRelease(pin.repo);
|
|
117
154
|
if (release.error) {
|
|
118
155
|
out.push({
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"worker-pool.js": {
|
|
36
36
|
"vendored_path": "vendor/blamejs/worker-pool.js",
|
|
37
|
-
"vendored_sha256": "
|
|
37
|
+
"vendored_sha256": "948549d82e481f241e5babb6bdddd27bf7148fb8ed1c389b9f2f7ad035e4e56b",
|
|
38
38
|
"upstream_path": "lib/worker-pool.js",
|
|
39
39
|
"upstream_sha256_at_pin": "262f99e9cc3d4a8f4eba9ad3e28401e8a1f47f78040afa63c9b17eb998437171",
|
|
40
40
|
"stripped": [
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"worker recycle on uncaught error / timeout / exit"
|
|
51
51
|
],
|
|
52
52
|
"exceptd_deltas": [
|
|
53
|
-
"scriptPath validator rejects Windows UNC + extended-path prefixes (\\\\?\\, \\\\.\\, \\\\<server>\\) — defense-in-depth against worker-spawn from network shares on win32 platforms"
|
|
53
|
+
"scriptPath validator rejects Windows UNC + extended-path prefixes (\\\\?\\, \\\\.\\, \\\\<server>\\) — defense-in-depth against worker-spawn from network shares on win32 platforms",
|
|
54
|
+
"_onWorkerExit respawn branch: call _maybeResolveDrain() after _drainQueue() so drain() settles when a worker recycles with an empty queue (the no-respawn branch already did; the respawn branch could hang pending drain waiters)."
|
|
54
55
|
]
|
|
55
56
|
},
|
|
56
57
|
"codepoint-class.js": {
|
|
@@ -322,6 +322,12 @@ function create(scriptPath, opts) {
|
|
|
322
322
|
if (!terminated && workerSlots.length < size) {
|
|
323
323
|
try { _spawnWorker(); } catch (_e) { /* already audited */ }
|
|
324
324
|
_drainQueue();
|
|
325
|
+
// exceptd delta: also settle pending drain() waiters after a respawn.
|
|
326
|
+
// _drainQueue() only schedules queued tasks; if the queue is already
|
|
327
|
+
// empty (a worker recycled with nothing left to do) the pool is now idle
|
|
328
|
+
// and a caller awaiting drain() must be resolved, exactly as the
|
|
329
|
+
// no-respawn branch below does — otherwise drain() hangs forever.
|
|
330
|
+
_maybeResolveDrain();
|
|
325
331
|
} else {
|
|
326
332
|
_maybeResolveDrain();
|
|
327
333
|
}
|