@blamejs/exceptd-skills 0.13.18 → 0.13.20
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 +79 -0
- package/data/_indexes/_meta.json +9 -9
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/_indexes/chains.json +14 -0
- package/data/_indexes/frequency.json +1 -0
- package/data/attack-techniques.json +2600 -109
- package/data/cve-catalog.json +147 -2678
- package/data/cwe-catalog.json +60 -1
- package/data/framework-control-gaps.json +252 -84
- package/data/rfc-references.json +286 -125
- package/data/zeroday-lessons.json +17 -2909
- package/lib/canonical-eq.js +88 -0
- package/lib/cve-regression-watcher.js +130 -9
- 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 +6 -2
- package/sbom.cdx.json +108 -33
- package/scripts/audit-catalog-gaps.js +347 -0
- package/scripts/check-test-coverage.js +16 -10
- package/scripts/refresh-mitre-ics-attack.js +15 -0
- package/scripts/refresh-upstream-catalogs.js +171 -54
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:cdadd1f3-ca1b-4a82-8e4c-ed5bfcb8bc0d",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
7
|
+
"timestamp": "2135-05-08T21:32:35.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.20"
|
|
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.20",
|
|
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 /
|
|
19
|
+
"version": "0.13.20",
|
|
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, real XML parser + canonical-form diff comparison + 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.20",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
32
|
+
"content": "3aca821664c27f90892a0aaa7602e3813f62e71aca39e0c87eca4a4621584d85"
|
|
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.20"
|
|
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": "004c3a9617a5290ac345779a48588eed519b6e042102b0a1384881d2a28c23f6"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"alg": "SHA3-512",
|
|
123
|
-
"content": "
|
|
123
|
+
"content": "5631e188a25811b9b009fdef576e6ea9ad3890ba1206d9a3e7d9b8c606e1c401d21b153ad6e4b3f8110385a6389351172b3a8a10fcf75ed9eebdd750c0a974a3"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -311,11 +311,11 @@
|
|
|
311
311
|
"hashes": [
|
|
312
312
|
{
|
|
313
313
|
"alg": "SHA-256",
|
|
314
|
-
"content": "
|
|
314
|
+
"content": "09bd917fe13c23d8a33f6a04978f5c89ea56ee53c8002ad357bd89cfb9ba8981"
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"alg": "SHA3-512",
|
|
318
|
-
"content": "
|
|
318
|
+
"content": "6d6ac5427d0f38973e4a1ae04a981f64bb253626ec478709568cae712175207c15f97dfc3c0f5767fc36ce43d6c65c60e1962297ee546437a9cba1ea729381b6"
|
|
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
|
},
|
|
@@ -341,11 +341,11 @@
|
|
|
341
341
|
"hashes": [
|
|
342
342
|
{
|
|
343
343
|
"alg": "SHA-256",
|
|
344
|
-
"content": "
|
|
344
|
+
"content": "c56e74b8c9290583b1d6fdd21b54bd65a254c58890c5f683379788ca7b080e9d"
|
|
345
345
|
},
|
|
346
346
|
{
|
|
347
347
|
"alg": "SHA3-512",
|
|
348
|
-
"content": "
|
|
348
|
+
"content": "24a77d58aa84b7c91c5067dbb28c73ddfa42a0229c6f45b9e36aea58d65ab3bc17a0a759323bdfad7d6a7fad8f21eff8a9ea2efc118f069035a6e5c0373380f6"
|
|
349
349
|
}
|
|
350
350
|
]
|
|
351
351
|
},
|
|
@@ -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
|
},
|
|
@@ -776,11 +776,11 @@
|
|
|
776
776
|
"hashes": [
|
|
777
777
|
{
|
|
778
778
|
"alg": "SHA-256",
|
|
779
|
-
"content": "
|
|
779
|
+
"content": "926ea25892e052fc6a8b9952afc1d8e2bd06c4aec223a1a7aa79ef1dfd7b7bb5"
|
|
780
780
|
},
|
|
781
781
|
{
|
|
782
782
|
"alg": "SHA3-512",
|
|
783
|
-
"content": "
|
|
783
|
+
"content": "6632e67313409bf5fdbde7e6ba199e4e61c437edfc00d8276a448e33240d78820927be85bccde6877b5f86c8f16abbe5b037147ed41366b9599963537accff0c"
|
|
784
784
|
}
|
|
785
785
|
]
|
|
786
786
|
},
|
|
@@ -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
|
},
|
|
@@ -1151,11 +1166,11 @@
|
|
|
1151
1166
|
"hashes": [
|
|
1152
1167
|
{
|
|
1153
1168
|
"alg": "SHA-256",
|
|
1154
|
-
"content": "
|
|
1169
|
+
"content": "d4da578b2f7d45f3d22eac52c1c9196cd3dbaf23414b5932d1f5c52d713236b8"
|
|
1155
1170
|
},
|
|
1156
1171
|
{
|
|
1157
1172
|
"alg": "SHA3-512",
|
|
1158
|
-
"content": "
|
|
1173
|
+
"content": "1f5add189b0d6bfd18cd85b28ff7ce64f1b52fec573098ccfbd71fc9245cbcca624a0cda515315e6881779d46eb0ca27fbefb4eda74cc5ff22c2309a1a97d5de"
|
|
1159
1174
|
}
|
|
1160
1175
|
]
|
|
1161
1176
|
},
|
|
@@ -1339,6 +1354,21 @@
|
|
|
1339
1354
|
}
|
|
1340
1355
|
]
|
|
1341
1356
|
},
|
|
1357
|
+
{
|
|
1358
|
+
"bom-ref": "file:lib/version-pins.js",
|
|
1359
|
+
"type": "file",
|
|
1360
|
+
"name": "lib/version-pins.js",
|
|
1361
|
+
"hashes": [
|
|
1362
|
+
{
|
|
1363
|
+
"alg": "SHA-256",
|
|
1364
|
+
"content": "7cbdc502a689614200f009d74e8d82544bf6b442452dfd970437596742a5e885"
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
"alg": "SHA3-512",
|
|
1368
|
+
"content": "eeb9b1cd050195693e425a48967a536f775f016ebe6563c6b6da30b3f90ff436e34e41bd2c860d61f2d343156358193c7c1c7454b4651301e777e37d671039b0"
|
|
1369
|
+
}
|
|
1370
|
+
]
|
|
1371
|
+
},
|
|
1342
1372
|
{
|
|
1343
1373
|
"bom-ref": "file:lib/worker-pool.js",
|
|
1344
1374
|
"type": "file",
|
|
@@ -1354,6 +1384,21 @@
|
|
|
1354
1384
|
}
|
|
1355
1385
|
]
|
|
1356
1386
|
},
|
|
1387
|
+
{
|
|
1388
|
+
"bom-ref": "file:lib/xml-tokenizer.js",
|
|
1389
|
+
"type": "file",
|
|
1390
|
+
"name": "lib/xml-tokenizer.js",
|
|
1391
|
+
"hashes": [
|
|
1392
|
+
{
|
|
1393
|
+
"alg": "SHA-256",
|
|
1394
|
+
"content": "1c2e2a92fe90ce90a7dcb7c95979e1b9c6e18f6bce1cfd160de9ce644fddb889"
|
|
1395
|
+
},
|
|
1396
|
+
{
|
|
1397
|
+
"alg": "SHA3-512",
|
|
1398
|
+
"content": "0bd564faff3173e095ceecff3e9bf70fc6c7ed2a8d4bb8b89b066f33b2a95513488408bc3f5bd6d8571eb1dcfb96710b2b45589b2d22238a04a790be246cb00f"
|
|
1399
|
+
}
|
|
1400
|
+
]
|
|
1401
|
+
},
|
|
1357
1402
|
{
|
|
1358
1403
|
"bom-ref": "file:manifest-snapshot.json",
|
|
1359
1404
|
"type": "file",
|
|
@@ -1391,11 +1436,11 @@
|
|
|
1391
1436
|
"hashes": [
|
|
1392
1437
|
{
|
|
1393
1438
|
"alg": "SHA-256",
|
|
1394
|
-
"content": "
|
|
1439
|
+
"content": "d9f9002ad675655e952674403fb030798e7ad0c98c65e6ad586960d7d7915af9"
|
|
1395
1440
|
},
|
|
1396
1441
|
{
|
|
1397
1442
|
"alg": "SHA3-512",
|
|
1398
|
-
"content": "
|
|
1443
|
+
"content": "41a61a493083024dd45323b21dd5d243eefdcbf212a1a23948ff180318ebb7644f25e4ea7954b9ff1ecb2650b68ce8050fefae545db14e894cb82e7c1166cedc"
|
|
1399
1444
|
}
|
|
1400
1445
|
]
|
|
1401
1446
|
},
|
|
@@ -1504,6 +1549,21 @@
|
|
|
1504
1549
|
}
|
|
1505
1550
|
]
|
|
1506
1551
|
},
|
|
1552
|
+
{
|
|
1553
|
+
"bom-ref": "file:scripts/audit-catalog-gaps.js",
|
|
1554
|
+
"type": "file",
|
|
1555
|
+
"name": "scripts/audit-catalog-gaps.js",
|
|
1556
|
+
"hashes": [
|
|
1557
|
+
{
|
|
1558
|
+
"alg": "SHA-256",
|
|
1559
|
+
"content": "ab99b9dc0a40f6a1914f40678c0b9b15880895515ddbb27526703f4d11400370"
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
"alg": "SHA3-512",
|
|
1563
|
+
"content": "d7f2b1d775d45c12e651d4da2cfd1f1e118eeb3bd9bd72f562417b06429f8868ddaa2c34aa66bcbe19214f44a673770d2cfc24fcfaf2be66510d6b42889f56d9"
|
|
1564
|
+
}
|
|
1565
|
+
]
|
|
1566
|
+
},
|
|
1507
1567
|
{
|
|
1508
1568
|
"bom-ref": "file:scripts/audit-cross-skill.js",
|
|
1509
1569
|
"type": "file",
|
|
@@ -1841,11 +1901,11 @@
|
|
|
1841
1901
|
"hashes": [
|
|
1842
1902
|
{
|
|
1843
1903
|
"alg": "SHA-256",
|
|
1844
|
-
"content": "
|
|
1904
|
+
"content": "d126c48bee7da18f03cefba681dcae5b931a0472658e4d7e0227ce3e29461026"
|
|
1845
1905
|
},
|
|
1846
1906
|
{
|
|
1847
1907
|
"alg": "SHA3-512",
|
|
1848
|
-
"content": "
|
|
1908
|
+
"content": "1ba4ffa94b0bf0bdb7e0bd0b537d016424da070d7c6991b90446dc8991dc005d13d5d306cc69d10ad73f765154eb09716b68cad38ab609069f105974006f4d3e"
|
|
1849
1909
|
}
|
|
1850
1910
|
]
|
|
1851
1911
|
},
|
|
@@ -1939,6 +1999,21 @@
|
|
|
1939
1999
|
}
|
|
1940
2000
|
]
|
|
1941
2001
|
},
|
|
2002
|
+
{
|
|
2003
|
+
"bom-ref": "file:scripts/refresh-mitre-ics-attack.js",
|
|
2004
|
+
"type": "file",
|
|
2005
|
+
"name": "scripts/refresh-mitre-ics-attack.js",
|
|
2006
|
+
"hashes": [
|
|
2007
|
+
{
|
|
2008
|
+
"alg": "SHA-256",
|
|
2009
|
+
"content": "014fd472f805193662698020c53d8ed08532ea2d8cbd2c9d8f33f162728d0ad2"
|
|
2010
|
+
},
|
|
2011
|
+
{
|
|
2012
|
+
"alg": "SHA3-512",
|
|
2013
|
+
"content": "8437f443eff9e5caca4414c2e7e7454c18b7dfcb8acfc690ac9565aef775d6c562c174f4cbe87a577b9616ebea189008bc62d04df5a8bfef791e5b9b81feaa53"
|
|
2014
|
+
}
|
|
2015
|
+
]
|
|
2016
|
+
},
|
|
1942
2017
|
{
|
|
1943
2018
|
"bom-ref": "file:scripts/refresh-reverse-refs.js",
|
|
1944
2019
|
"type": "file",
|
|
@@ -1991,11 +2066,11 @@
|
|
|
1991
2066
|
"hashes": [
|
|
1992
2067
|
{
|
|
1993
2068
|
"alg": "SHA-256",
|
|
1994
|
-
"content": "
|
|
2069
|
+
"content": "4c2d6ba4c75dea4c92409908db8ffcd357b7208f765c532a3ac976a8cf785acf"
|
|
1995
2070
|
},
|
|
1996
2071
|
{
|
|
1997
2072
|
"alg": "SHA3-512",
|
|
1998
|
-
"content": "
|
|
2073
|
+
"content": "29d02c2280fde260d95d215ef68ca4f668906c31d240a174195141b7568bd17a203af50c25d8f0a861d76247ee13984d0843ccab5b5e75f52b6ad77e69f06521"
|
|
1999
2074
|
}
|
|
2000
2075
|
]
|
|
2001
2076
|
},
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* scripts/audit-catalog-gaps.js
|
|
5
|
+
*
|
|
6
|
+
* Walks every data/*.json catalog and surfaces three classes of gap:
|
|
7
|
+
*
|
|
8
|
+
* 1. missing-context entries that exist but lack one of the
|
|
9
|
+
* documented context-search fields (e.g. RFC
|
|
10
|
+
* without abstract; ATT&CK technique without
|
|
11
|
+
* platforms; CVE without iocs)
|
|
12
|
+
*
|
|
13
|
+
* 2. dangling-ref forward references from one catalog into
|
|
14
|
+
* another that do not resolve (e.g. CVE
|
|
15
|
+
* entry's cwe_refs cites CWE-XXX but the
|
|
16
|
+
* local cwe-catalog does not carry that ID)
|
|
17
|
+
*
|
|
18
|
+
* 3. draft-debt per-catalog count of _auto_imported rows
|
|
19
|
+
* relative to operator-curated rows. High
|
|
20
|
+
* draft-debt = bulk-imported surface that has
|
|
21
|
+
* not been refined yet.
|
|
22
|
+
*
|
|
23
|
+
* Output: structured JSON to stdout (default) or human-readable summary
|
|
24
|
+
* with `--pretty`. Returns exit 0 in --warn-only mode (default); exit
|
|
25
|
+
* 1 in --strict mode if any class triggers.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node scripts/audit-catalog-gaps.js # JSON
|
|
29
|
+
* node scripts/audit-catalog-gaps.js --pretty # human
|
|
30
|
+
* node scripts/audit-catalog-gaps.js --strict # exit 1 on gap
|
|
31
|
+
* node scripts/audit-catalog-gaps.js --catalog cve # one catalog
|
|
32
|
+
* node scripts/audit-catalog-gaps.js --class missing-context
|
|
33
|
+
*
|
|
34
|
+
* npm: `npm run audit-catalog-gaps`
|
|
35
|
+
*
|
|
36
|
+
* Design note: the gap analyzer is a separate detection plane from
|
|
37
|
+
* lib/validate-cve-catalog.js (schema validation, predeploy gate) and
|
|
38
|
+
* scripts/refresh-reverse-refs.js (forward/reverse-ref currency). The
|
|
39
|
+
* validator polices what's strictly required by the schema; the gap
|
|
40
|
+
* analyzer polices the recommended-but-not-required context envelope
|
|
41
|
+
* that lets an AI consumer find an entry by topic instead of by ID.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require("fs");
|
|
45
|
+
const path = require("path");
|
|
46
|
+
|
|
47
|
+
const ROOT = path.join(__dirname, "..");
|
|
48
|
+
const DATA = path.join(ROOT, "data");
|
|
49
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
50
|
+
|
|
51
|
+
// Per-catalog required context fields. Each entry in the array is a
|
|
52
|
+
// field path (dot-separated for nested) and a non-emptiness predicate.
|
|
53
|
+
// Pillar / Class / Pillar-abstraction CWEs and similar can opt out via
|
|
54
|
+
// the suppression key on the entry (_gap_skip: { fields: [...] }).
|
|
55
|
+
const SPEC = {
|
|
56
|
+
"cve-catalog": {
|
|
57
|
+
file: "cve-catalog.json",
|
|
58
|
+
idShape: /^(CVE-|MAL-|BUG-|GHSA-|SNYK-)/,
|
|
59
|
+
required_context: [
|
|
60
|
+
{ field: "iocs", check: (v) => v && (
|
|
61
|
+
(Array.isArray(v.payload_artifacts) && v.payload_artifacts.length) ||
|
|
62
|
+
(Array.isArray(v.behavioral) && v.behavioral.length)
|
|
63
|
+
), label: "iocs.payload_artifacts or iocs.behavioral" },
|
|
64
|
+
{ field: "framework_control_gaps", check: (v) => v && Object.keys(v).length > 0, label: "framework_control_gaps" },
|
|
65
|
+
{ field: "attack_refs", check: (v) => Array.isArray(v) && v.length > 0, label: "attack_refs" },
|
|
66
|
+
{ field: "cwe_refs", check: (v) => Array.isArray(v) && v.length > 0, label: "cwe_refs" },
|
|
67
|
+
{ field: "verification_sources", check: (v) => Array.isArray(v) && v.length > 0, label: "verification_sources" }
|
|
68
|
+
],
|
|
69
|
+
refs: [
|
|
70
|
+
{ field: "cwe_refs", target: "cwe-catalog.json", item: true },
|
|
71
|
+
{ field: "attack_refs", target: "attack-techniques.json", item: true },
|
|
72
|
+
{ field: "atlas_refs", target: "atlas-ttps.json", item: true },
|
|
73
|
+
{ field: "framework_control_gaps", target: "framework-control-gaps.json", keys: true }
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"cwe-catalog": {
|
|
77
|
+
file: "cwe-catalog.json",
|
|
78
|
+
idShape: /^CWE-\d+$/,
|
|
79
|
+
required_context: [
|
|
80
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
81
|
+
{ field: "abstraction", check: (v) => typeof v === "string" && v.length > 0, label: "abstraction" },
|
|
82
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 20, label: "description (>20 chars)" }
|
|
83
|
+
],
|
|
84
|
+
refs: []
|
|
85
|
+
},
|
|
86
|
+
"attack-techniques": {
|
|
87
|
+
file: "attack-techniques.json",
|
|
88
|
+
idShape: /^T\d{4}(\.\d{3})?$/,
|
|
89
|
+
required_context: [
|
|
90
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
91
|
+
{ field: "tactic", check: (v) => (Array.isArray(v) ? v.length > 0 : typeof v === "string" && v.length > 0), label: "tactic" },
|
|
92
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description (short)" },
|
|
93
|
+
{ field: "platforms", check: (v) => Array.isArray(v) && v.length > 0, label: "platforms" }
|
|
94
|
+
],
|
|
95
|
+
refs: []
|
|
96
|
+
},
|
|
97
|
+
"atlas-ttps": {
|
|
98
|
+
file: "atlas-ttps.json",
|
|
99
|
+
idShape: /^AML\.T\d{4}(\.\d{3})?$/,
|
|
100
|
+
required_context: [
|
|
101
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
102
|
+
{ field: "tactic", check: (v) => (Array.isArray(v) ? v.length > 0 : typeof v === "string" && v.length > 0), label: "tactic" },
|
|
103
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description" }
|
|
104
|
+
],
|
|
105
|
+
refs: []
|
|
106
|
+
},
|
|
107
|
+
"d3fend-catalog": {
|
|
108
|
+
file: "d3fend-catalog.json",
|
|
109
|
+
idShape: /^D3-/,
|
|
110
|
+
required_context: [
|
|
111
|
+
{ field: "name", check: (v) => typeof v === "string" && v.length > 0, label: "name" },
|
|
112
|
+
{ field: "tactic", check: (v) => typeof v === "string" && v.length > 0, label: "tactic" },
|
|
113
|
+
{ field: "description", check: (v) => typeof v === "string" && v.length > 0, label: "description" }
|
|
114
|
+
],
|
|
115
|
+
refs: []
|
|
116
|
+
},
|
|
117
|
+
"rfc-references": {
|
|
118
|
+
file: "rfc-references.json",
|
|
119
|
+
idShape: /^(RFC-\d+|DRAFT-|ISO-|CSAF-)/,
|
|
120
|
+
required_context: [
|
|
121
|
+
{ field: "title", check: (v) => typeof v === "string" && v.length > 0, label: "title" },
|
|
122
|
+
{ field: "status", check: (v) => typeof v === "string" && v.length > 0, label: "status" },
|
|
123
|
+
{ field: "abstract", check: (v) => typeof v === "string" && v.length > 20, label: "abstract (>20 chars)" }
|
|
124
|
+
],
|
|
125
|
+
refs: []
|
|
126
|
+
},
|
|
127
|
+
"framework-control-gaps": {
|
|
128
|
+
file: "framework-control-gaps.json",
|
|
129
|
+
idShape: /^[A-Z]/,
|
|
130
|
+
required_context: [
|
|
131
|
+
{ field: "framework", check: (v) => typeof v === "string" && v.length > 0, label: "framework" },
|
|
132
|
+
{ field: "control_id", check: (v) => typeof v === "string" && v.length > 0, label: "control_id" },
|
|
133
|
+
{ field: "control_name", check: (v) => typeof v === "string" && v.length > 0, label: "control_name" },
|
|
134
|
+
{ field: "real_requirement", check: (v) => typeof v === "string" && v.length > 20, label: "real_requirement (>20 chars)" },
|
|
135
|
+
{ field: "theater_test", check: (v) => v && typeof v.claim === "string" && typeof v.test === "string", label: "theater_test{claim,test}" },
|
|
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)" }
|
|
142
|
+
],
|
|
143
|
+
refs: []
|
|
144
|
+
},
|
|
145
|
+
"zeroday-lessons": {
|
|
146
|
+
file: "zeroday-lessons.json",
|
|
147
|
+
idShape: /^(CVE-|MAL-|BUG-)/,
|
|
148
|
+
required_context: [
|
|
149
|
+
{ field: "attack_vector", check: (v) => v && typeof v.description === "string" && v.description.length > 20, label: "attack_vector.description" },
|
|
150
|
+
{ field: "framework_coverage", check: (v) => v && Object.keys(v).length > 0, label: "framework_coverage" },
|
|
151
|
+
{ field: "new_control_requirements", check: (v) => Array.isArray(v) && v.length > 0, label: "new_control_requirements" }
|
|
152
|
+
],
|
|
153
|
+
refs: []
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
function loadCatalog(name) {
|
|
158
|
+
return JSON.parse(fs.readFileSync(path.join(DATA, name), "utf8"));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function inspect(catalogKey) {
|
|
162
|
+
const spec = SPEC[catalogKey];
|
|
163
|
+
if (!spec) throw new Error(`unknown catalog: ${catalogKey}`);
|
|
164
|
+
const cat = loadCatalog(spec.file);
|
|
165
|
+
const ids = Object.keys(cat).filter((k) => k !== "_meta");
|
|
166
|
+
const report = {
|
|
167
|
+
catalog: catalogKey,
|
|
168
|
+
entries: ids.length,
|
|
169
|
+
auto_imported: 0,
|
|
170
|
+
operator_curated: 0,
|
|
171
|
+
missing_context: [],
|
|
172
|
+
dangling_refs: []
|
|
173
|
+
};
|
|
174
|
+
for (const id of ids) {
|
|
175
|
+
if (!spec.idShape.test(id)) continue;
|
|
176
|
+
const e = cat[id];
|
|
177
|
+
if (!e) continue;
|
|
178
|
+
if (e._auto_imported) report.auto_imported++;
|
|
179
|
+
else report.operator_curated++;
|
|
180
|
+
const skip = e._gap_skip && Array.isArray(e._gap_skip.fields) ? new Set(e._gap_skip.fields) : new Set();
|
|
181
|
+
for (const r of spec.required_context) {
|
|
182
|
+
if (skip.has(r.field)) continue;
|
|
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)) {
|
|
188
|
+
report.missing_context.push({ id, field: r.field, label: r.label });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return report;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inspectRefs(allCatalogs) {
|
|
196
|
+
const findings = [];
|
|
197
|
+
const cveCat = allCatalogs["cve-catalog"];
|
|
198
|
+
const cweCat = allCatalogs["cwe-catalog"];
|
|
199
|
+
const attCat = allCatalogs["attack-techniques"];
|
|
200
|
+
const atlCat = allCatalogs["atlas-ttps"];
|
|
201
|
+
const fwCat = allCatalogs["framework-control-gaps"];
|
|
202
|
+
// Build presence sets keyed by id (sans _meta).
|
|
203
|
+
const cweSet = new Set(Object.keys(cweCat).filter((k) => k !== "_meta"));
|
|
204
|
+
const attSet = new Set(Object.keys(attCat).filter((k) => k !== "_meta"));
|
|
205
|
+
const atlSet = new Set(Object.keys(atlCat).filter((k) => k !== "_meta"));
|
|
206
|
+
const fwSet = new Set(Object.keys(fwCat).filter((k) => k !== "_meta"));
|
|
207
|
+
for (const id of Object.keys(cveCat)) {
|
|
208
|
+
if (id === "_meta") continue;
|
|
209
|
+
const e = cveCat[id];
|
|
210
|
+
if (!e) continue;
|
|
211
|
+
for (const ref of (e.cwe_refs || [])) {
|
|
212
|
+
if (!cweSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "cwe-catalog", missing: ref });
|
|
213
|
+
}
|
|
214
|
+
for (const ref of (e.attack_refs || [])) {
|
|
215
|
+
if (!attSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "attack-techniques", missing: ref });
|
|
216
|
+
}
|
|
217
|
+
for (const ref of (e.atlas_refs || [])) {
|
|
218
|
+
if (!atlSet.has(ref)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "atlas-ttps", missing: ref });
|
|
219
|
+
}
|
|
220
|
+
const fcg = e.framework_control_gaps || {};
|
|
221
|
+
for (const key of Object.keys(fcg)) {
|
|
222
|
+
if (!fwSet.has(key)) findings.push({ kind: "dangling-ref", source_catalog: "cve-catalog", source_id: id, target_catalog: "framework-control-gaps", missing: key });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return findings;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function parseArgs(argv) {
|
|
229
|
+
const out = { pretty: false, strict: false, catalog: null, klass: null };
|
|
230
|
+
for (let i = 2; i < argv.length; i++) {
|
|
231
|
+
const a = argv[i];
|
|
232
|
+
if (a === "--pretty") out.pretty = true;
|
|
233
|
+
else if (a === "--strict") out.strict = true;
|
|
234
|
+
else if (a === "--catalog") out.catalog = argv[++i];
|
|
235
|
+
else if (a === "--class") out.klass = argv[++i];
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function emitPretty(report) {
|
|
241
|
+
const lines = [];
|
|
242
|
+
lines.push("Catalog gap audit");
|
|
243
|
+
lines.push("=================");
|
|
244
|
+
for (const r of report.per_catalog) {
|
|
245
|
+
lines.push(`\n[${r.catalog}] entries=${r.entries} auto-imported=${r.auto_imported} operator-curated=${r.operator_curated}`);
|
|
246
|
+
if (r.missing_context.length === 0) {
|
|
247
|
+
lines.push(" ✓ context complete on every entry");
|
|
248
|
+
} else {
|
|
249
|
+
// Group by field for tidier output.
|
|
250
|
+
const byField = new Map();
|
|
251
|
+
for (const m of r.missing_context) {
|
|
252
|
+
if (!byField.has(m.field)) byField.set(m.field, []);
|
|
253
|
+
byField.get(m.field).push(m.id);
|
|
254
|
+
}
|
|
255
|
+
for (const [field, ids] of byField) {
|
|
256
|
+
lines.push(` missing ${field} on ${ids.length} entries: ${ids.slice(0, 5).join(", ")}${ids.length > 5 ? ` ... +${ids.length - 5}` : ""}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
lines.push("\nCross-catalog dangling refs:");
|
|
261
|
+
if (report.dangling_refs.length === 0) {
|
|
262
|
+
lines.push(" ✓ every cross-ref resolves");
|
|
263
|
+
} else {
|
|
264
|
+
const byTarget = new Map();
|
|
265
|
+
for (const f of report.dangling_refs) {
|
|
266
|
+
const key = `${f.source_catalog}.${f.target_catalog}`;
|
|
267
|
+
if (!byTarget.has(key)) byTarget.set(key, []);
|
|
268
|
+
byTarget.get(key).push(`${f.source_id} → ${f.missing}`);
|
|
269
|
+
}
|
|
270
|
+
for (const [k, list] of byTarget) {
|
|
271
|
+
lines.push(` ${k}: ${list.length} dangling`);
|
|
272
|
+
for (const l of list.slice(0, 5)) lines.push(` ${l}`);
|
|
273
|
+
if (list.length > 5) lines.push(` ... +${list.length - 5}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
lines.push("\nDraft debt (auto-imported / total):");
|
|
277
|
+
for (const r of report.per_catalog) {
|
|
278
|
+
const pct = r.entries === 0 ? 0 : ((r.auto_imported / r.entries) * 100).toFixed(1);
|
|
279
|
+
lines.push(` ${r.catalog.padEnd(28)} ${r.auto_imported} / ${r.entries} (${pct}%)`);
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Valid finding-class names for the `--class` filter. The pretty + JSON
|
|
285
|
+
// emitters always include every section, but counts and strict-exit
|
|
286
|
+
// gating respect the active filter.
|
|
287
|
+
const VALID_CLASSES = new Set(["missing-context", "dangling-ref", "draft-debt"]);
|
|
288
|
+
|
|
289
|
+
function main() {
|
|
290
|
+
const opts = parseArgs(process.argv);
|
|
291
|
+
if (opts.klass && !VALID_CLASSES.has(opts.klass)) {
|
|
292
|
+
console.error(`unknown class: ${opts.klass} valid: ${[...VALID_CLASSES].join(", ")}`);
|
|
293
|
+
process.exitCode = 2;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const catalogKeys = opts.catalog ? [opts.catalog] : Object.keys(SPEC);
|
|
297
|
+
const perCatalog = [];
|
|
298
|
+
const allLoaded = {};
|
|
299
|
+
for (const k of catalogKeys) {
|
|
300
|
+
if (!SPEC[k]) {
|
|
301
|
+
console.error(`unknown catalog: ${k} valid: ${Object.keys(SPEC).join(", ")}`);
|
|
302
|
+
process.exitCode = 2;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
perCatalog.push(inspect(k));
|
|
306
|
+
allLoaded[k] = loadCatalog(SPEC[k].file);
|
|
307
|
+
}
|
|
308
|
+
// Load all needed catalogs for cross-ref pass even when --catalog scoped.
|
|
309
|
+
for (const k of Object.keys(SPEC)) if (!allLoaded[k]) allLoaded[k] = loadCatalog(SPEC[k].file);
|
|
310
|
+
const dangling = opts.catalog && opts.catalog !== "cve-catalog" ? [] : inspectRefs(allLoaded);
|
|
311
|
+
|
|
312
|
+
// Apply the --class filter before counts + strict-exit gating.
|
|
313
|
+
// Missing-context findings on per_catalog and dangling_refs are the
|
|
314
|
+
// two policed classes; draft-debt is informational-only (the audit
|
|
315
|
+
// surfaces draft-debt but it does not fail strict mode by design).
|
|
316
|
+
const filteredPerCatalog = opts.klass === "dangling-ref" || opts.klass === "draft-debt"
|
|
317
|
+
? perCatalog.map((r) => ({ ...r, missing_context: [] }))
|
|
318
|
+
: perCatalog;
|
|
319
|
+
const filteredDangling = opts.klass === "missing-context" || opts.klass === "draft-debt"
|
|
320
|
+
? []
|
|
321
|
+
: dangling;
|
|
322
|
+
|
|
323
|
+
const report = {
|
|
324
|
+
generated_at: TODAY,
|
|
325
|
+
class_filter: opts.klass || null,
|
|
326
|
+
per_catalog: filteredPerCatalog,
|
|
327
|
+
dangling_refs: filteredDangling,
|
|
328
|
+
totals: {
|
|
329
|
+
catalogs: filteredPerCatalog.length,
|
|
330
|
+
entries: filteredPerCatalog.reduce((n, r) => n + r.entries, 0),
|
|
331
|
+
missing_context: filteredPerCatalog.reduce((n, r) => n + r.missing_context.length, 0),
|
|
332
|
+
dangling_refs: filteredDangling.length
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
if (opts.pretty) {
|
|
336
|
+
process.stdout.write(emitPretty(report) + "\n");
|
|
337
|
+
} else {
|
|
338
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
339
|
+
}
|
|
340
|
+
if (opts.strict && (report.totals.missing_context > 0 || report.totals.dangling_refs > 0)) {
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (require.main === module) main();
|
|
346
|
+
|
|
347
|
+
module.exports = { SPEC, inspect, inspectRefs };
|