@blamejs/exceptd-skills 0.13.19 → 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/sbom.cdx.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:1b26544f-8d49-4291-bb51-773688342543",
4
+ "serialNumber": "urn:uuid:e9cd326f-3ca4-4e4f-987e-2d0206d0d1ac",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2040-06-07T22:53:03.000Z",
7
+ "timestamp": "2150-04-20T18:11:59.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.13.19"
12
+ "version": "0.13.21"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.19",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.21",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.13.19",
20
- "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs (312 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 7476 RFCs), 34 jurisdictions, automated gap-detection, pre-computed indexes, Ed25519-signed.",
19
+ "version": "0.13.21",
20
+ "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs (312 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 7476 RFCs), 34 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
23
23
  "license": {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.19",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.21",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "58d63d34273199b99863c98e52a8732b166959cd0067ebf28be5313b70d7e2fb"
32
+ "content": "0008dc2b26d4e59fc6986791da6277d43639c811ac5af5aa10f9b234a155921e"
33
33
  }
34
34
  ],
35
35
  "externalReferences": [
36
36
  {
37
37
  "type": "distribution",
38
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.19"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.21"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "a1264f7196da88786ce3091f3573ec7d6dad161ce8d7fa530fe4f297c400622c"
119
+ "content": "c06c6600a1d4cbf20f87d080d9441658f0b4444023a565e1c3fe478fa35edab4"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "e9b054e7f2b902971f2de812c319d9537f07959aaec0727aafc0935c84af27aff95e463c4b9ff0c5209c263cb8494779d5d05244bb04036f4e525bbcb2fe6b8f"
123
+ "content": "7b16e8b3b6d2a68892f336bd7b07159c2750e9fca29539cba437bdcae3be48e10c66c1c529c7f9a4fe71f0eab9a79d0a596f740ab5bc9f9d20fb6e51d555f17e"
124
124
  }
125
125
  ]
126
126
  },
@@ -311,11 +311,11 @@
311
311
  "hashes": [
312
312
  {
313
313
  "alg": "SHA-256",
314
- "content": "09bd917fe13c23d8a33f6a04978f5c89ea56ee53c8002ad357bd89cfb9ba8981"
314
+ "content": "49b6010b317edd219def135171ea8f3b1bbf1e00e9c5a08bf7237215ff54e2c3"
315
315
  },
316
316
  {
317
317
  "alg": "SHA3-512",
318
- "content": "6d6ac5427d0f38973e4a1ae04a981f64bb253626ec478709568cae712175207c15f97dfc3c0f5767fc36ce43d6c65c60e1962297ee546437a9cba1ea729381b6"
318
+ "content": "5f12e99b1d780523d5b69a317f6851ffc2a5763fb753703fe2d0799c81964bd020aae7d95ebd4e74320a4f3c69f0c0ce4eb14bf6decdef33f41ea539a2b5fc32"
319
319
  }
320
320
  ]
321
321
  },
@@ -326,11 +326,11 @@
326
326
  "hashes": [
327
327
  {
328
328
  "alg": "SHA-256",
329
- "content": "e7cdd8447b271f2f017226cdecb13593348aaccd6e1ab95d13dadb5152c9b568"
329
+ "content": "a09c83af3f9679a7ea73935726a1ff9de2cab94b4ab6321fc017fc147747d7c3"
330
330
  },
331
331
  {
332
332
  "alg": "SHA3-512",
333
- "content": "f8f6f1f9474bb7e2258205834d4f0dd1d8269b0895d9b4dedf9c3fde1bae7dd14e6ea788f342d36b468e63139de272d3a2b13559239119ca3d52919fdb4df9f0"
333
+ "content": "653f3d3cdac2fe56ba01fdf2bb3b3c6edfdc29b667f0ddb99f7755aec1adf577618135e3a68667126be121f8d130d7848a1001bd41878af10446e959b5db2626"
334
334
  }
335
335
  ]
336
336
  },
@@ -401,11 +401,11 @@
401
401
  "hashes": [
402
402
  {
403
403
  "alg": "SHA-256",
404
- "content": "a478387473633484849b86bea0fc0e71ad5165f3aef582a4340e14d3c7fe7fac"
404
+ "content": "b63fe398c3de068093871e8bbca11e16b6567fb15482648d0bbce06939c34104"
405
405
  },
406
406
  {
407
407
  "alg": "SHA3-512",
408
- "content": "84ca007b6c9ab902317e49bf303f6f073d41b704b78db953ea01f109b349cc22ef1f02194d558b188db01bc71095d7a672cdcd058224e77c7fefd10130e43c30"
408
+ "content": "e64c9e219f6e77fbb235ed875b1775ff6150bffc49fae5903600b15d3799f0d8a27dd553869646e3d0311c5b7fbe456686d9f7bf7e10c0c4cbc71079bcd943ff"
409
409
  }
410
410
  ]
411
411
  },
@@ -791,11 +791,11 @@
791
791
  "hashes": [
792
792
  {
793
793
  "alg": "SHA-256",
794
- "content": "4aeb6dff37b39cef4246f7b2454be009e32a8b2d512229c428c537699a3810d3"
794
+ "content": "7242a7349ac79a74813bf2b7486b6000c0c877e71cec17e2d68df33bc4007b93"
795
795
  },
796
796
  {
797
797
  "alg": "SHA3-512",
798
- "content": "2b8773e3834f174222aa10219f61768dac306365e184e5d500eb52ffcf5a281898e351bf309a8e22d93f378e75ea183ad8264808e7681e696d923d110115e16e"
798
+ "content": "eaa89dcdafec3f8f3b5bb3f7a34ae6709681dca278a0eacf43fe8c5fb9cc1e533ba64f46df15152316a505572cf891bc20b7682e505474209a0e3e7aa97dea82"
799
799
  }
800
800
  ]
801
801
  },
@@ -844,6 +844,21 @@
844
844
  }
845
845
  ]
846
846
  },
847
+ {
848
+ "bom-ref": "file:lib/canonical-eq.js",
849
+ "type": "file",
850
+ "name": "lib/canonical-eq.js",
851
+ "hashes": [
852
+ {
853
+ "alg": "SHA-256",
854
+ "content": "eb962f83a7b82a4f895553d13191cd933c87d29116591d76c8550c7cb00ebebd"
855
+ },
856
+ {
857
+ "alg": "SHA3-512",
858
+ "content": "f09e77b2f58aeb0d09c4758738de42d657345a9ef09f28529682f2daa7ebcfb84602eb811a76e85640e45e6ce13274918b994e760099ff457869ccccf740a437"
859
+ }
860
+ ]
861
+ },
847
862
  {
848
863
  "bom-ref": "file:lib/cross-ref-api.js",
849
864
  "type": "file",
@@ -881,11 +896,11 @@
881
896
  "hashes": [
882
897
  {
883
898
  "alg": "SHA-256",
884
- "content": "3f6f7d0a9487b0b7b89af7c848559381fea053594f5a1cb0b85e7e63e81c2168"
899
+ "content": "b5278eddccec3e9cf2c68f8bdd8a142de8f95c61d61826570fa5d293e81055f2"
885
900
  },
886
901
  {
887
902
  "alg": "SHA3-512",
888
- "content": "68c34e8db139b94951e3b68f8e6dc8945599ea84a4bed028e2145f45d9cfc84f5e8f05cbdfd06ad3ba95c6dbf974b4bc4641b1a0169167a347cf92762418cf1f"
903
+ "content": "ba4f8de9981e8a3066b014e52984e9917c4e003a7feebc6098f5cd32ed22a0dca82b97dbfcabf31809e3f5de7145e2cb9f30432c6a30564642a4e798c4f796cd"
889
904
  }
890
905
  ]
891
906
  },
@@ -949,6 +964,21 @@
949
964
  }
950
965
  ]
951
966
  },
967
+ {
968
+ "bom-ref": "file:lib/gap-detectors.js",
969
+ "type": "file",
970
+ "name": "lib/gap-detectors.js",
971
+ "hashes": [
972
+ {
973
+ "alg": "SHA-256",
974
+ "content": "e81dd33acfffe2ff28da4b278bcdc11b7e4616dbc6ff4cd1c92f0f99baf97c8a"
975
+ },
976
+ {
977
+ "alg": "SHA3-512",
978
+ "content": "a0f8c147fefd6c91640d2fbe0c606c371c4f83c8a2f3b380c8d212ec92d8616ee1da220269b42638d60d75fcc730e822cb29e1e87f408e9dd553eed76b7aee02"
979
+ }
980
+ ]
981
+ },
952
982
  {
953
983
  "bom-ref": "file:lib/id-validation.js",
954
984
  "type": "file",
@@ -1151,11 +1181,11 @@
1151
1181
  "hashes": [
1152
1182
  {
1153
1183
  "alg": "SHA-256",
1154
- "content": "35b6a6f5a2ae2a833bf0c2ccc21dff47f2f72147f0af7fdd2e484cd0b195334f"
1184
+ "content": "d4da578b2f7d45f3d22eac52c1c9196cd3dbaf23414b5932d1f5c52d713236b8"
1155
1185
  },
1156
1186
  {
1157
1187
  "alg": "SHA3-512",
1158
- "content": "223e0efb65b0cc18b96ff3267f08170aa1c91c722c5220c6cf26a109eab4c9009330c72507273ad777a8f7ab8de090a9d1d6517797c922cb81a659391777af82"
1188
+ "content": "1f5add189b0d6bfd18cd85b28ff7ce64f1b52fec573098ccfbd71fc9245cbcca624a0cda515315e6881779d46eb0ca27fbefb4eda74cc5ff22c2309a1a97d5de"
1159
1189
  }
1160
1190
  ]
1161
1191
  },
@@ -1339,6 +1369,21 @@
1339
1369
  }
1340
1370
  ]
1341
1371
  },
1372
+ {
1373
+ "bom-ref": "file:lib/version-pins.js",
1374
+ "type": "file",
1375
+ "name": "lib/version-pins.js",
1376
+ "hashes": [
1377
+ {
1378
+ "alg": "SHA-256",
1379
+ "content": "7cbdc502a689614200f009d74e8d82544bf6b442452dfd970437596742a5e885"
1380
+ },
1381
+ {
1382
+ "alg": "SHA3-512",
1383
+ "content": "eeb9b1cd050195693e425a48967a536f775f016ebe6563c6b6da30b3f90ff436e34e41bd2c860d61f2d343156358193c7c1c7454b4651301e777e37d671039b0"
1384
+ }
1385
+ ]
1386
+ },
1342
1387
  {
1343
1388
  "bom-ref": "file:lib/worker-pool.js",
1344
1389
  "type": "file",
@@ -1354,6 +1399,21 @@
1354
1399
  }
1355
1400
  ]
1356
1401
  },
1402
+ {
1403
+ "bom-ref": "file:lib/xml-tokenizer.js",
1404
+ "type": "file",
1405
+ "name": "lib/xml-tokenizer.js",
1406
+ "hashes": [
1407
+ {
1408
+ "alg": "SHA-256",
1409
+ "content": "1c2e2a92fe90ce90a7dcb7c95979e1b9c6e18f6bce1cfd160de9ce644fddb889"
1410
+ },
1411
+ {
1412
+ "alg": "SHA3-512",
1413
+ "content": "0bd564faff3173e095ceecff3e9bf70fc6c7ed2a8d4bb8b89b066f33b2a95513488408bc3f5bd6d8571eb1dcfb96710b2b45589b2d22238a04a790be246cb00f"
1414
+ }
1415
+ ]
1416
+ },
1357
1417
  {
1358
1418
  "bom-ref": "file:manifest-snapshot.json",
1359
1419
  "type": "file",
@@ -1391,11 +1451,11 @@
1391
1451
  "hashes": [
1392
1452
  {
1393
1453
  "alg": "SHA-256",
1394
- "content": "9c6e95ea598a89ca5806a0fda511514667eaa05e8ad9638614e8bba8d793dacf"
1454
+ "content": "b8676a67b642f623974dccf3ba0f1df95836541b7c1dcd0c63cef2b71fa1a88a"
1395
1455
  },
1396
1456
  {
1397
1457
  "alg": "SHA3-512",
1398
- "content": "e70008672707cb064316b6ffd60f1f6f36a135a11655295f102cab2432bb16ad40d6a58d5b0141f068b9a16e1fbf6fe290b41600bd08af5d17a3d84023346885"
1458
+ "content": "fc5556bf27093bda786f564caaba523dec5d39f92e1b09ce9e5011f8411cf3add4892cd28330ec4f0b08e8bb6ba61c7a1849cc4ecdad64bd8df7b03afc58650d"
1399
1459
  }
1400
1460
  ]
1401
1461
  },
@@ -1511,11 +1571,11 @@
1511
1571
  "hashes": [
1512
1572
  {
1513
1573
  "alg": "SHA-256",
1514
- "content": "2ba06a35debf730b687928e53e53f6c5952486f607cebde04170cc4dde729f45"
1574
+ "content": "006588fc0995ad3c02a1f035dfd956c0d20c0497284f0c3e30490eb86bcb03a3"
1515
1575
  },
1516
1576
  {
1517
1577
  "alg": "SHA3-512",
1518
- "content": "86e6fdd1768bd63be0a8b0eec1ed19844815e2904ac9055b2561c1ef57ab99b3e060043af400598d1eed929c3f4d828bb75490f05dd916a0000b1e453b7c37ba"
1578
+ "content": "340f6275b34071790cc40dbcdf3c74afb2ad033ef49eeca3a0778544494e886c930a5e3bab65137302bdfb9cf114e891fccfe06e05ddedcb04718131f3c333f8"
1519
1579
  }
1520
1580
  ]
1521
1581
  },
@@ -1789,6 +1849,21 @@
1789
1849
  }
1790
1850
  ]
1791
1851
  },
1852
+ {
1853
+ "bom-ref": "file:scripts/check-catalog-gap-budget.js",
1854
+ "type": "file",
1855
+ "name": "scripts/check-catalog-gap-budget.js",
1856
+ "hashes": [
1857
+ {
1858
+ "alg": "SHA-256",
1859
+ "content": "ef47f63889341675c90bc07d835f5cfab137b83ab44820799de715e4762c1c74"
1860
+ },
1861
+ {
1862
+ "alg": "SHA3-512",
1863
+ "content": "ba197b6fce1c871d80ac2becadb0fff9161b790d78ed5a9409d7100da55e73a6a50ea644771b550bfd9654a69953c8b905fc609d3078d8fe9542d629dde5e9d4"
1864
+ }
1865
+ ]
1866
+ },
1792
1867
  {
1793
1868
  "bom-ref": "file:scripts/check-manifest-snapshot.js",
1794
1869
  "type": "file",
@@ -1856,11 +1931,11 @@
1856
1931
  "hashes": [
1857
1932
  {
1858
1933
  "alg": "SHA-256",
1859
- "content": "1e8e98cc7ac46a72a57d775d0fa6e96276948eb64c1967c343d33ea6002e54a3"
1934
+ "content": "d126c48bee7da18f03cefba681dcae5b931a0472658e4d7e0227ce3e29461026"
1860
1935
  },
1861
1936
  {
1862
1937
  "alg": "SHA3-512",
1863
- "content": "4f46bb910321992a3460cd2b12474a95ec2fd3bfbe2d1c75551d2dc65fcb0414c65fa03c802404d659c6a2db2e260f2b313d43259bda0b3dbf9b5680c73d234e"
1938
+ "content": "1ba4ffa94b0bf0bdb7e0bd0b537d016424da070d7c6991b90446dc8991dc005d13d5d306cc69d10ad73f765154eb09716b68cad38ab609069f105974006f4d3e"
1864
1939
  }
1865
1940
  ]
1866
1941
  },
@@ -1886,11 +1961,11 @@
1886
1961
  "hashes": [
1887
1962
  {
1888
1963
  "alg": "SHA-256",
1889
- "content": "6a7766b986988fd14105d92f3488052333afff8b72eda17460e193ca58b2d60a"
1964
+ "content": "306e02cc802bc1ca6fe98fb1e4e57c895c7f458895cb2acc459dc6f7ba0127c9"
1890
1965
  },
1891
1966
  {
1892
1967
  "alg": "SHA3-512",
1893
- "content": "8494c74fa0bd122d8f4ad5dbdd7eebd933cd88d79797a330c80e832121fa44b6ff794d65fb08f24a1f3e3858d6c21487a4001194a8b30dccbb82fe85ec60e708"
1968
+ "content": "e851348e5f7bec18a3a2a3757d1f5720cc0892c0bf527304e5e79dfc9931c7309ba6fd53918fcd63c2e7cd880487642af8f06d0b1195e0620e2aa9fadf643ba6"
1894
1969
  }
1895
1970
  ]
1896
1971
  },
@@ -2021,11 +2096,11 @@
2021
2096
  "hashes": [
2022
2097
  {
2023
2098
  "alg": "SHA-256",
2024
- "content": "6334d3a25d18ff82c638b6176b16b6e4c2fa90e8a59446a28714b721b23e30d9"
2099
+ "content": "4c2d6ba4c75dea4c92409908db8ffcd357b7208f765c532a3ac976a8cf785acf"
2025
2100
  },
2026
2101
  {
2027
2102
  "alg": "SHA3-512",
2028
- "content": "8808212cedc40fe24fb3f857e7ddc9314ecd93682c8e3529dd36675760324a8669bfb6d448b9ab42ec95db2665459992cf6c40add15ec92149f4acf70ed1a8eb"
2103
+ "content": "29d02c2280fde260d95d215ef68ca4f668906c31d240a174195141b7568bd17a203af50c25d8f0a861d76247ee13984d0843ccab5b5e75f52b6ad77e69f06521"
2029
2104
  }
2030
2105
  ]
2031
2106
  },
@@ -133,7 +133,12 @@ const SPEC = {
133
133
  { field: "control_name", check: (v) => typeof v === "string" && v.length > 0, label: "control_name" },
134
134
  { field: "real_requirement", check: (v) => typeof v === "string" && v.length > 20, label: "real_requirement (>20 chars)" },
135
135
  { field: "theater_test", check: (v) => v && typeof v.claim === "string" && typeof v.test === "string", label: "theater_test{claim,test}" },
136
- { field: "evidence_cves", check: (v) => Array.isArray(v) && v.length > 0, label: "evidence_cves" }
136
+ // evidence_cves is required UNLESS the entry declares forward_looking:true.
137
+ // v0.13.19 used per-entry _gap_skip annotations on 84 framework gaps;
138
+ // v0.13.20 replaces that with a first-class schema field operators can
139
+ // see in the JSON. The check honors forward_looking via the entry
140
+ // parameter — see the SCHEMA_FORWARD_LOOKING block in inspect().
141
+ { field: "evidence_cves", check: (v, entry) => (entry && entry.forward_looking === true) || (Array.isArray(v) && v.length > 0), label: "evidence_cves (or forward_looking:true)" }
137
142
  ],
138
143
  refs: []
139
144
  },
@@ -175,7 +180,11 @@ function inspect(catalogKey) {
175
180
  const skip = e._gap_skip && Array.isArray(e._gap_skip.fields) ? new Set(e._gap_skip.fields) : new Set();
176
181
  for (const r of spec.required_context) {
177
182
  if (skip.has(r.field)) continue;
178
- if (!r.check(e[r.field])) {
183
+ // Pass the entry as the second argument so per-field checks can
184
+ // inspect class-level schema flags (forward_looking, etc.). The
185
+ // legacy check-functions only consumed the value; new ones can
186
+ // opt into entry-aware evaluation.
187
+ if (!r.check(e[r.field], e)) {
179
188
  report.missing_context.push({ id, field: r.field, label: r.label });
180
189
  }
181
190
  }
@@ -269,13 +278,42 @@ function emitPretty(report) {
269
278
  const pct = r.entries === 0 ? 0 : ((r.auto_imported / r.entries) * 100).toFixed(1);
270
279
  lines.push(` ${r.catalog.padEnd(28)} ${r.auto_imported} / ${r.entries} (${pct}%)`);
271
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
+ }
272
297
  return lines.join("\n");
273
298
  }
274
299
 
275
- // Valid finding-class names for the `--class` filter. The pretty + JSON
276
- // emitters always include every section, but counts and strict-exit
277
- // gating respect the active filter.
278
- const VALID_CLASSES = new Set(["missing-context", "dangling-ref", "draft-debt"]);
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");
279
317
 
280
318
  function main() {
281
319
  const opts = parseArgs(process.argv);
@@ -300,27 +338,47 @@ function main() {
300
338
  for (const k of Object.keys(SPEC)) if (!allLoaded[k]) allLoaded[k] = loadCatalog(SPEC[k].file);
301
339
  const dangling = opts.catalog && opts.catalog !== "cve-catalog" ? [] : inspectRefs(allLoaded);
302
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
+
303
347
  // Apply the --class filter before counts + strict-exit gating.
304
- // Missing-context findings on per_catalog and dangling_refs are the
305
- // two policed classes; draft-debt is informational-only (the audit
306
- // surfaces draft-debt but it does not fail strict mode by design).
307
- 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))
308
350
  ? perCatalog.map((r) => ({ ...r, missing_context: [] }))
309
351
  : perCatalog;
310
- 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))
311
354
  ? []
312
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
+ }
313
367
 
314
368
  const report = {
315
369
  generated_at: TODAY,
316
370
  class_filter: opts.klass || null,
317
371
  per_catalog: filteredPerCatalog,
318
372
  dangling_refs: filteredDangling,
373
+ extended_findings: extendedByClass,
319
374
  totals: {
320
375
  catalogs: filteredPerCatalog.length,
321
376
  entries: filteredPerCatalog.reduce((n, r) => n + r.entries, 0),
322
377
  missing_context: filteredPerCatalog.reduce((n, r) => n + r.missing_context.length, 0),
323
- dangling_refs: filteredDangling.length
378
+ dangling_refs: filteredDangling.length,
379
+ extended: Object.fromEntries(
380
+ Object.entries(extendedByClass).map(([cls, arr]) => [cls, arr.length])
381
+ )
324
382
  }
325
383
  };
326
384
  if (opts.pretty) {
@@ -328,7 +386,10 @@ function main() {
328
386
  } else {
329
387
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
330
388
  }
331
- if (opts.strict && (report.totals.missing_context > 0 || report.totals.dangling_refs > 0)) {
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)) {
332
393
  process.exitCode = 1;
333
394
  }
334
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();
@@ -329,6 +329,15 @@ function extractPlaybookIds(content) {
329
329
  return { indicators: ind, artifacts: arts };
330
330
  }
331
331
 
332
+ // Canonical-form recursive equality replaces JSON.stringify comparison.
333
+ // Pre-v0.13.20 the comparator was JSON.stringify(before.iocs) !==
334
+ // JSON.stringify(after.iocs) — non-canonical: key order, trailing
335
+ // whitespace, and numeric format differences all flagged as "changed"
336
+ // when the operator made no semantic change. Symptoms were patched
337
+ // twice with skip rules (_auto_imported, _iocs_stub) instead of fixing
338
+ // the comparator. v0.13.20 fixes the root cause.
339
+ const { canonicalEqual } = require("../lib/canonical-eq");
340
+
332
341
  function extractCveIocChanges(beforeStr, afterStr) {
333
342
  const before = safeParse(beforeStr) || {};
334
343
  const after = safeParse(afterStr) || {};
@@ -336,27 +345,16 @@ function extractCveIocChanges(beforeStr, afterStr) {
336
345
  const ids = new Set([...Object.keys(before), ...Object.keys(after)]);
337
346
  for (const id of ids) {
338
347
  if (!/^CVE-\d{4}-\d+/.test(id)) continue;
339
- // v0.13.18: skip bulk-imported entries. Auto-imported rows carry stub
340
- // IoCs by design; their per-entry IoCs are not the operator-curated
341
- // surface the diff-coverage gate is designed to police.
348
+ // v0.13.18 retained skip rule: bulk-imported rows whose IoCs are
349
+ // stub-by-design on both sides pure intake-class events, not
350
+ // operator curation. Removing this would surface every fresh KEV
351
+ // bulk-import as a per-CVE iocs-modified finding.
342
352
  const beforeAuto = !!(before[id] && before[id]._auto_imported);
343
353
  const afterAuto = !!(after[id] && after[id]._auto_imported);
344
354
  if (beforeAuto && afterAuto) continue;
345
- // v0.13.19: also skip operator-curated rows whose IoCs are flagged
346
- // as stubs (`_iocs_stub: true` — generic placeholder added by the
347
- // gap-fix pass when an entry was missing iocs entirely). When an
348
- // operator later curates real IoCs the diff-coverage check fires
349
- // normally because the curation step removes _iocs_stub.
350
- const beforeStub = !!(before[id] && before[id]._iocs_stub);
351
- const afterStub = !!(after[id] && after[id]._iocs_stub);
352
- // Existing entry going stub→curated (afterStub=false, beforeStub=true)
353
- // is what we WANT to flag for review. Existing entry going non-stub→
354
- // stub or stub→stub or absent→stub are all auto-fill events the
355
- // diff-coverage gate should not police.
356
- if (afterStub) continue;
357
- const b = JSON.stringify((before[id] && before[id].iocs) || null);
358
- const a = JSON.stringify((after[id] && after[id].iocs) || null);
359
- if (b !== a) changed.add(id);
355
+ const bIocs = (before[id] && before[id].iocs) || null;
356
+ const aIocs = (after[id] && after[id].iocs) || null;
357
+ if (!canonicalEqual(bIocs, aIocs)) changed.add(id);
360
358
  }
361
359
  return changed;
362
360
  }