@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/CHANGELOG.md +72 -0
- package/data/_indexes/_meta.json +6 -6
- package/data/attack-techniques.json +2 -3
- package/data/cve-catalog.json +301 -3792
- package/data/framework-control-gaps.json +168 -504
- package/data/zeroday-lessons.json +5 -3029
- package/lib/canonical-eq.js +88 -0
- package/lib/cve-regression-watcher.js +130 -9
- package/lib/gap-detectors.js +555 -0
- package/lib/source-advisories.js +9 -34
- package/lib/version-pins.js +73 -0
- package/lib/xml-tokenizer.js +344 -0
- package/manifest.json +44 -44
- package/package.json +4 -3
- package/sbom.cdx.json +108 -33
- package/scripts/audit-catalog-gaps.js +74 -13
- package/scripts/check-catalog-gap-budget.js +133 -0
- package/scripts/check-test-coverage.js +16 -18
- package/scripts/predeploy.js +14 -0
- package/scripts/refresh-upstream-catalogs.js +13 -0
package/sbom.cdx.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:e9cd326f-3ca4-4e4f-987e-2d0206d0d1ac",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
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.
|
|
12
|
+
"version": "0.13.21"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.
|
|
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.
|
|
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,
|
|
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.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.21",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
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.
|
|
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": "
|
|
119
|
+
"content": "c06c6600a1d4cbf20f87d080d9441658f0b4444023a565e1c3fe478fa35edab4"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"alg": "SHA3-512",
|
|
123
|
-
"content": "
|
|
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": "
|
|
314
|
+
"content": "49b6010b317edd219def135171ea8f3b1bbf1e00e9c5a08bf7237215ff54e2c3"
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"alg": "SHA3-512",
|
|
318
|
-
"content": "
|
|
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": "
|
|
329
|
+
"content": "a09c83af3f9679a7ea73935726a1ff9de2cab94b4ab6321fc017fc147747d7c3"
|
|
330
330
|
},
|
|
331
331
|
{
|
|
332
332
|
"alg": "SHA3-512",
|
|
333
|
-
"content": "
|
|
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": "
|
|
404
|
+
"content": "b63fe398c3de068093871e8bbca11e16b6567fb15482648d0bbce06939c34104"
|
|
405
405
|
},
|
|
406
406
|
{
|
|
407
407
|
"alg": "SHA3-512",
|
|
408
|
-
"content": "
|
|
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": "
|
|
794
|
+
"content": "7242a7349ac79a74813bf2b7486b6000c0c877e71cec17e2d68df33bc4007b93"
|
|
795
795
|
},
|
|
796
796
|
{
|
|
797
797
|
"alg": "SHA3-512",
|
|
798
|
-
"content": "
|
|
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": "
|
|
899
|
+
"content": "b5278eddccec3e9cf2c68f8bdd8a142de8f95c61d61826570fa5d293e81055f2"
|
|
885
900
|
},
|
|
886
901
|
{
|
|
887
902
|
"alg": "SHA3-512",
|
|
888
|
-
"content": "
|
|
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": "
|
|
1184
|
+
"content": "d4da578b2f7d45f3d22eac52c1c9196cd3dbaf23414b5932d1f5c52d713236b8"
|
|
1155
1185
|
},
|
|
1156
1186
|
{
|
|
1157
1187
|
"alg": "SHA3-512",
|
|
1158
|
-
"content": "
|
|
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": "
|
|
1454
|
+
"content": "b8676a67b642f623974dccf3ba0f1df95836541b7c1dcd0c63cef2b71fa1a88a"
|
|
1395
1455
|
},
|
|
1396
1456
|
{
|
|
1397
1457
|
"alg": "SHA3-512",
|
|
1398
|
-
"content": "
|
|
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": "
|
|
1574
|
+
"content": "006588fc0995ad3c02a1f035dfd956c0d20c0497284f0c3e30490eb86bcb03a3"
|
|
1515
1575
|
},
|
|
1516
1576
|
{
|
|
1517
1577
|
"alg": "SHA3-512",
|
|
1518
|
-
"content": "
|
|
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": "
|
|
1934
|
+
"content": "d126c48bee7da18f03cefba681dcae5b931a0472658e4d7e0227ce3e29461026"
|
|
1860
1935
|
},
|
|
1861
1936
|
{
|
|
1862
1937
|
"alg": "SHA3-512",
|
|
1863
|
-
"content": "
|
|
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": "
|
|
1964
|
+
"content": "306e02cc802bc1ca6fe98fb1e4e57c895c7f458895cb2acc459dc6f7ba0127c9"
|
|
1890
1965
|
},
|
|
1891
1966
|
{
|
|
1892
1967
|
"alg": "SHA3-512",
|
|
1893
|
-
"content": "
|
|
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": "
|
|
2099
|
+
"content": "4c2d6ba4c75dea4c92409908db8ffcd357b7208f765c532a3ac976a8cf785acf"
|
|
2025
2100
|
},
|
|
2026
2101
|
{
|
|
2027
2102
|
"alg": "SHA3-512",
|
|
2028
|
-
"content": "
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
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
|
-
|
|
305
|
-
|
|
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
|
-
|
|
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
|
|
340
|
-
//
|
|
341
|
-
//
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
}
|