@blamejs/exceptd-skills 0.13.20 → 0.13.21
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 +31 -0
- package/data/_indexes/_meta.json +3 -3
- package/data/attack-techniques.json +2 -3
- package/lib/gap-detectors.js +555 -0
- package/manifest.json +44 -44
- package/package.json +4 -3
- package/sbom.cdx.json +49 -19
- package/scripts/audit-catalog-gaps.js +63 -11
- package/scripts/check-catalog-gap-budget.js +133 -0
- package/scripts/predeploy.js +14 -0
|
@@ -278,13 +278,42 @@ function emitPretty(report) {
|
|
|
278
278
|
const pct = r.entries === 0 ? 0 : ((r.auto_imported / r.entries) * 100).toFixed(1);
|
|
279
279
|
lines.push(` ${r.catalog.padEnd(28)} ${r.auto_imported} / ${r.entries} (${pct}%)`);
|
|
280
280
|
}
|
|
281
|
+
// v0.13.21 extended findings sections.
|
|
282
|
+
const ext = report.extended_findings || {};
|
|
283
|
+
const extClasses = Object.keys(ext).sort();
|
|
284
|
+
if (extClasses.length > 0) {
|
|
285
|
+
lines.push("\nExtended findings (v0.13.21):");
|
|
286
|
+
for (const cls of extClasses) {
|
|
287
|
+
const arr = ext[cls];
|
|
288
|
+
lines.push(`\n [${cls}] ${arr.length} finding(s)`);
|
|
289
|
+
for (const f of arr.slice(0, 5)) {
|
|
290
|
+
const id = f.id || f.target_id || "(no id)";
|
|
291
|
+
const ctx = f.catalog ? `${f.catalog} ${id}` : id;
|
|
292
|
+
lines.push(` ${ctx} — ${f.reason || f.rule || "(no reason)"}`);
|
|
293
|
+
}
|
|
294
|
+
if (arr.length > 5) lines.push(` ... +${arr.length - 5} more`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
281
297
|
return lines.join("\n");
|
|
282
298
|
}
|
|
283
299
|
|
|
284
|
-
// Valid finding-class names for the `--class` filter.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
|
|
300
|
+
// Valid finding-class names for the `--class` filter. v0.13.21 added 7
|
|
301
|
+
// extended detection classes for gaps the v0.13.19 detector did not
|
|
302
|
+
// surface (content-quality / temporal-staleness / logical-consistency /
|
|
303
|
+
// cross-ref-completeness / schema-evolution / operator-action-sla /
|
|
304
|
+
// unused-orphan). Each is implemented in lib/gap-detectors.js.
|
|
305
|
+
const VALID_CLASSES = new Set([
|
|
306
|
+
"missing-context", "dangling-ref", "draft-debt",
|
|
307
|
+
"content-quality", "temporal-staleness", "logical-consistency",
|
|
308
|
+
"cross-ref-completeness", "schema-evolution", "operator-action-sla",
|
|
309
|
+
"unused-orphan"
|
|
310
|
+
]);
|
|
311
|
+
const EXTENDED_CLASS_NAMES = new Set([
|
|
312
|
+
"content-quality", "temporal-staleness", "logical-consistency",
|
|
313
|
+
"cross-ref-completeness", "schema-evolution", "operator-action-sla",
|
|
314
|
+
"unused-orphan"
|
|
315
|
+
]);
|
|
316
|
+
const EXTENDED_DETECTORS = require("../lib/gap-detectors.js");
|
|
288
317
|
|
|
289
318
|
function main() {
|
|
290
319
|
const opts = parseArgs(process.argv);
|
|
@@ -309,27 +338,47 @@ function main() {
|
|
|
309
338
|
for (const k of Object.keys(SPEC)) if (!allLoaded[k]) allLoaded[k] = loadCatalog(SPEC[k].file);
|
|
310
339
|
const dangling = opts.catalog && opts.catalog !== "cve-catalog" ? [] : inspectRefs(allLoaded);
|
|
311
340
|
|
|
341
|
+
// v0.13.21 extended detectors. --catalog scoping mutes them (they're
|
|
342
|
+
// cross-catalog by nature); --class scoping filters down to one.
|
|
343
|
+
const extendedFindings = opts.catalog
|
|
344
|
+
? []
|
|
345
|
+
: EXTENDED_DETECTORS.runAllDetectors(allLoaded, {});
|
|
346
|
+
|
|
312
347
|
// Apply the --class filter before counts + strict-exit gating.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// surfaces draft-debt but it does not fail strict mode by design).
|
|
316
|
-
const filteredPerCatalog = opts.klass === "dangling-ref" || opts.klass === "draft-debt"
|
|
348
|
+
const filteredPerCatalog = (opts.klass === "dangling-ref" || opts.klass === "draft-debt" ||
|
|
349
|
+
EXTENDED_CLASS_NAMES.has(opts.klass))
|
|
317
350
|
? perCatalog.map((r) => ({ ...r, missing_context: [] }))
|
|
318
351
|
: perCatalog;
|
|
319
|
-
const filteredDangling = opts.klass === "missing-context" || opts.klass === "draft-debt"
|
|
352
|
+
const filteredDangling = (opts.klass === "missing-context" || opts.klass === "draft-debt" ||
|
|
353
|
+
EXTENDED_CLASS_NAMES.has(opts.klass))
|
|
320
354
|
? []
|
|
321
355
|
: dangling;
|
|
356
|
+
const filteredExtended = opts.klass
|
|
357
|
+
? (EXTENDED_CLASS_NAMES.has(opts.klass)
|
|
358
|
+
? extendedFindings.filter((f) => f.class === opts.klass)
|
|
359
|
+
: [])
|
|
360
|
+
: extendedFindings;
|
|
361
|
+
|
|
362
|
+
const extendedByClass = {};
|
|
363
|
+
for (const f of filteredExtended) {
|
|
364
|
+
if (!extendedByClass[f.class]) extendedByClass[f.class] = [];
|
|
365
|
+
extendedByClass[f.class].push(f);
|
|
366
|
+
}
|
|
322
367
|
|
|
323
368
|
const report = {
|
|
324
369
|
generated_at: TODAY,
|
|
325
370
|
class_filter: opts.klass || null,
|
|
326
371
|
per_catalog: filteredPerCatalog,
|
|
327
372
|
dangling_refs: filteredDangling,
|
|
373
|
+
extended_findings: extendedByClass,
|
|
328
374
|
totals: {
|
|
329
375
|
catalogs: filteredPerCatalog.length,
|
|
330
376
|
entries: filteredPerCatalog.reduce((n, r) => n + r.entries, 0),
|
|
331
377
|
missing_context: filteredPerCatalog.reduce((n, r) => n + r.missing_context.length, 0),
|
|
332
|
-
dangling_refs: filteredDangling.length
|
|
378
|
+
dangling_refs: filteredDangling.length,
|
|
379
|
+
extended: Object.fromEntries(
|
|
380
|
+
Object.entries(extendedByClass).map(([cls, arr]) => [cls, arr.length])
|
|
381
|
+
)
|
|
333
382
|
}
|
|
334
383
|
};
|
|
335
384
|
if (opts.pretty) {
|
|
@@ -337,7 +386,10 @@ function main() {
|
|
|
337
386
|
} else {
|
|
338
387
|
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
339
388
|
}
|
|
340
|
-
|
|
389
|
+
const extendedTotal = Object.values(report.totals.extended || {}).reduce((n, v) => n + v, 0);
|
|
390
|
+
if (opts.strict && (report.totals.missing_context > 0 ||
|
|
391
|
+
report.totals.dangling_refs > 0 ||
|
|
392
|
+
extendedTotal > 0)) {
|
|
341
393
|
process.exitCode = 1;
|
|
342
394
|
}
|
|
343
395
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* scripts/check-catalog-gap-budget.js
|
|
5
|
+
*
|
|
6
|
+
* Predeploy / CI gate that runs the v0.13.21 extended gap detectors
|
|
7
|
+
* and asserts no class exceeds its budget. Mirrors the budget in
|
|
8
|
+
* tests/shipped-catalog-integrity.test.js but runs as a standalone
|
|
9
|
+
* predeploy gate so the check is visible in the gate summary even
|
|
10
|
+
* when the broader test suite is skipped (or is the gate that's
|
|
11
|
+
* failing for an unrelated reason).
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 — every extended class within budget
|
|
15
|
+
* 1 — at least one class regressed
|
|
16
|
+
* 2 — internal error
|
|
17
|
+
*
|
|
18
|
+
* The budget is intentionally duplicated (here + integrity test) for
|
|
19
|
+
* fail-loud-at-two-levels. Operators see the regression in BOTH the
|
|
20
|
+
* test-suite output AND the predeploy gate-summary table.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require("path");
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const ROOT = path.join(__dirname, "..");
|
|
26
|
+
|
|
27
|
+
let D;
|
|
28
|
+
try {
|
|
29
|
+
D = require(path.join(ROOT, "lib", "gap-detectors.js"));
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.error("[check-catalog-gap-budget] failed to load lib/gap-detectors.js:", e.message);
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadAll() {
|
|
36
|
+
const data = path.join(ROOT, "data");
|
|
37
|
+
const read = (name) => JSON.parse(fs.readFileSync(path.join(data, name), "utf8"));
|
|
38
|
+
return {
|
|
39
|
+
"cve-catalog": read("cve-catalog.json"),
|
|
40
|
+
"cwe-catalog": read("cwe-catalog.json"),
|
|
41
|
+
"attack-techniques": read("attack-techniques.json"),
|
|
42
|
+
"atlas-ttps": read("atlas-ttps.json"),
|
|
43
|
+
"d3fend-catalog": read("d3fend-catalog.json"),
|
|
44
|
+
"rfc-references": read("rfc-references.json"),
|
|
45
|
+
"framework-control-gaps": read("framework-control-gaps.json"),
|
|
46
|
+
"zeroday-lessons": read("zeroday-lessons.json")
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Per-class regression budgets. Kept in sync with the canonical version
|
|
51
|
+
// in tests/shipped-catalog-integrity.test.js.
|
|
52
|
+
const BUDGET = {
|
|
53
|
+
"content-quality": 12,
|
|
54
|
+
"temporal-staleness": 260,
|
|
55
|
+
"logical-consistency": 5,
|
|
56
|
+
"cross-ref-completeness": 5,
|
|
57
|
+
"schema-evolution": 0,
|
|
58
|
+
"operator-action-sla": 0,
|
|
59
|
+
"unused-orphan": 1400
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
const all = D.runAllDetectors(loadAll(), {});
|
|
64
|
+
const byClass = {};
|
|
65
|
+
for (const f of all) byClass[f.class] = (byClass[f.class] || 0) + 1;
|
|
66
|
+
const regressions = [];
|
|
67
|
+
|
|
68
|
+
// Fail-closed contract (codex P2 PR #61): every class actually
|
|
69
|
+
// emitted by the detector must have a budget entry. If a future
|
|
70
|
+
// 8th detector lands without a budget update, the gate fires with
|
|
71
|
+
// an unbudgeted-class error instead of silently passing.
|
|
72
|
+
const unbudgeted = [];
|
|
73
|
+
for (const cls of Object.keys(byClass)) {
|
|
74
|
+
if (!(cls in BUDGET)) {
|
|
75
|
+
unbudgeted.push({ class: cls, count: byClass[cls] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Inverse check: every class declared by the detector module's
|
|
79
|
+
// canonical class list must appear in BUDGET (covers the case where
|
|
80
|
+
// a new class produces zero findings on this run but still needs
|
|
81
|
+
// an explicit budget so a future regression caps fail-closed).
|
|
82
|
+
const missingBudget = [];
|
|
83
|
+
if (Array.isArray(D.DETECTOR_CLASSES)) {
|
|
84
|
+
for (const cls of D.DETECTOR_CLASSES) {
|
|
85
|
+
if (!(cls in BUDGET)) missingBudget.push(cls);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const cls of Object.keys(BUDGET)) {
|
|
90
|
+
const actual = byClass[cls] || 0;
|
|
91
|
+
const allowed = BUDGET[cls];
|
|
92
|
+
if (actual > allowed) {
|
|
93
|
+
regressions.push({ class: cls, allowed, actual, delta: actual - allowed });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const summary = Object.keys(BUDGET).map((cls) => {
|
|
97
|
+
const actual = byClass[cls] || 0;
|
|
98
|
+
const allowed = BUDGET[cls];
|
|
99
|
+
const mark = actual > allowed ? "✗" : "✓";
|
|
100
|
+
return ` ${mark} ${cls.padEnd(28)} actual=${actual} budget=${allowed}`;
|
|
101
|
+
}).join("\n");
|
|
102
|
+
console.log("[check-catalog-gap-budget] extended detection classes:");
|
|
103
|
+
console.log(summary);
|
|
104
|
+
|
|
105
|
+
if (unbudgeted.length > 0) {
|
|
106
|
+
console.error("\n[check-catalog-gap-budget] UNBUDGETED detector classes — fail-closed:");
|
|
107
|
+
for (const u of unbudgeted) {
|
|
108
|
+
console.error(` ${u.class}: ${u.count} finding(s), no BUDGET entry`);
|
|
109
|
+
}
|
|
110
|
+
console.error("Add an explicit budget entry in both:");
|
|
111
|
+
console.error(" scripts/check-catalog-gap-budget.js");
|
|
112
|
+
console.error(" tests/shipped-catalog-integrity.test.js");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
if (missingBudget.length > 0) {
|
|
116
|
+
console.error("\n[check-catalog-gap-budget] BUDGET missing entries for declared classes:");
|
|
117
|
+
for (const c of missingBudget) console.error(` ${c}: declared by lib/gap-detectors.js DETECTOR_CLASSES, no BUDGET entry`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (regressions.length > 0) {
|
|
121
|
+
console.error("\n[check-catalog-gap-budget] REGRESSION beyond budget:");
|
|
122
|
+
for (const r of regressions) {
|
|
123
|
+
console.error(` ${r.class}: actual=${r.actual} > budget=${r.allowed} (delta +${r.delta})`);
|
|
124
|
+
}
|
|
125
|
+
console.error("\nClose the gap in this PR (preferred) or update BUDGET in both:");
|
|
126
|
+
console.error(" scripts/check-catalog-gap-budget.js");
|
|
127
|
+
console.error(" tests/shipped-catalog-integrity.test.js");
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
console.log("[check-catalog-gap-budget] all classes within budget; every class is budgeted.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
main();
|
package/scripts/predeploy.js
CHANGED
|
@@ -193,6 +193,20 @@ const GATES = [
|
|
|
193
193
|
// shares the integrity-tier framing with manifest-snapshot etc.
|
|
194
194
|
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
195
195
|
},
|
|
196
|
+
{
|
|
197
|
+
// v0.13.21: catalog-gap budget gate. Runs the seven extended
|
|
198
|
+
// detection classes added in v0.13.21 (content-quality,
|
|
199
|
+
// temporal-staleness, logical-consistency, cross-ref-completeness,
|
|
200
|
+
// schema-evolution, operator-action-sla, unused-orphan) against
|
|
201
|
+
// the shipped catalog and fails if any class regresses beyond its
|
|
202
|
+
// documented budget. Mirrors the budget enforced by
|
|
203
|
+
// tests/shipped-catalog-integrity.test.js so the regression
|
|
204
|
+
// surfaces in BOTH the gate-summary table AND the test output.
|
|
205
|
+
name: "Catalog-gap budget (v0.13.21 extended detection classes)",
|
|
206
|
+
command: process.execPath,
|
|
207
|
+
args: [path.join(ROOT, "scripts", "check-catalog-gap-budget.js")],
|
|
208
|
+
ciJobName: "Data integrity (catalog + manifest snapshot)",
|
|
209
|
+
},
|
|
196
210
|
];
|
|
197
211
|
|
|
198
212
|
function runGate(gate) {
|