@blamejs/exceptd-skills 0.16.28 → 0.16.29
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 +14 -0
- package/README.md +1 -1
- package/bin/exceptd.js +41 -8
- package/data/_indexes/_meta.json +4 -3
- package/data/_indexes/jurisdiction-map.json +31 -158
- package/lib/auto-discovery.js +8 -0
- package/lib/collectors/library-author.js +26 -9
- package/lib/collectors/secrets.js +8 -1
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +17 -4
- package/lib/refresh-external.js +3 -2
- package/lib/validate-indexes.js +5 -0
- package/manifest.json +53 -53
- package/orchestrator/pipeline.js +16 -4
- package/package.json +1 -1
- package/sbom.cdx.json +57 -42
- package/scripts/build-indexes.js +12 -1
- package/scripts/check-sbom-currency.js +76 -14
- package/scripts/refresh-sbom.js +1 -1
- package/scripts/sync-package-description.js +74 -0
- package/scripts/verify-shipped-tarball.js +18 -7
- package/sources/validators/cve-validator.js +16 -6
package/sbom.cdx.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:39da38cf-85e2-487e-a082-3e9f9e709f66",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
7
|
+
"timestamp": "2056-10-03T19:51:43.000Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "blamejs",
|
|
11
11
|
"name": "scripts/refresh-sbom.js",
|
|
12
|
-
"version": "0.16.
|
|
12
|
+
"version": "0.16.29"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.16.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.16.29",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.16.
|
|
19
|
+
"version": "0.16.29",
|
|
20
20
|
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (439 CVEs / 177 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 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
|
{
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.16.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.16.29",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
32
|
+
"content": "2ef63291c81beb3ce29f3fa1bf8f5effd1ec9da9ff2b5dfa2455cbc63517cfe4"
|
|
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.16.
|
|
38
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.16.29"
|
|
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": "9401b3b84ba8f31ce8e9490b5f8888eceecc15ecd872dfcf31cf9effadfaf3ee"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"alg": "SHA3-512",
|
|
123
|
-
"content": "
|
|
123
|
+
"content": "e47bf0bcd84120c50b9e99c8e891bf4a59cae09f13151fe334a9bce23e0c46ab6fa06407db11e8b4bfc318811428d75aa85cc1aecea726c395a01dbc1d945256"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -176,11 +176,11 @@
|
|
|
176
176
|
"hashes": [
|
|
177
177
|
{
|
|
178
178
|
"alg": "SHA-256",
|
|
179
|
-
"content": "
|
|
179
|
+
"content": "e7b854e7db9a364a1b368b5084b4f0c2a8282f0459ce39800ac1d1dabdc06074"
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"alg": "SHA3-512",
|
|
183
|
-
"content": "
|
|
183
|
+
"content": "8c0af6e6ca9c5753f726b940b5a0cf948abd61c552c3779d4c3b3ee6950657a4713815926c5cda3d743256b8f580865986de9992167ce1f476b3dd0bd5557ee1"
|
|
184
184
|
}
|
|
185
185
|
]
|
|
186
186
|
},
|
|
@@ -281,11 +281,11 @@
|
|
|
281
281
|
"hashes": [
|
|
282
282
|
{
|
|
283
283
|
"alg": "SHA-256",
|
|
284
|
-
"content": "
|
|
284
|
+
"content": "ce80148fd48bcd06575b368461e686aa06aaff746f184a33be3347a352ecf2bf"
|
|
285
285
|
},
|
|
286
286
|
{
|
|
287
287
|
"alg": "SHA3-512",
|
|
288
|
-
"content": "
|
|
288
|
+
"content": "8d480e05acfd1ea7b88ed8bfedb0c1b5fe9bb56ee11a99b92426d15443892d8bb5aacd689059562f28ecf4fcc7bc661d80215bb276353659790951aa7e51a39d"
|
|
289
289
|
}
|
|
290
290
|
]
|
|
291
291
|
},
|
|
@@ -986,11 +986,11 @@
|
|
|
986
986
|
"hashes": [
|
|
987
987
|
{
|
|
988
988
|
"alg": "SHA-256",
|
|
989
|
-
"content": "
|
|
989
|
+
"content": "081583535b98f300966c656ba007858127506f4d2853a62f8f5084984d99a3ef"
|
|
990
990
|
},
|
|
991
991
|
{
|
|
992
992
|
"alg": "SHA3-512",
|
|
993
|
-
"content": "
|
|
993
|
+
"content": "1297d553949f1501518f9c8d4f2c133cb92a0ab49aacde2bd18b3f883809fb340036a960cc4ded99828a351a4756cba38fd37ba3688903049f17888af0f9c411"
|
|
994
994
|
}
|
|
995
995
|
]
|
|
996
996
|
},
|
|
@@ -1181,11 +1181,11 @@
|
|
|
1181
1181
|
"hashes": [
|
|
1182
1182
|
{
|
|
1183
1183
|
"alg": "SHA-256",
|
|
1184
|
-
"content": "
|
|
1184
|
+
"content": "d5962bed4fda430834283e9ec557161ba7e2c4f5f964f8fd45f4a1723a8cd622"
|
|
1185
1185
|
},
|
|
1186
1186
|
{
|
|
1187
1187
|
"alg": "SHA3-512",
|
|
1188
|
-
"content": "
|
|
1188
|
+
"content": "39006cdae4faf916cb1c05d729c29f5a7cb4c1535fedfc59a57a6ac5c38a449e195c05dc23aff1465cff0de720e2efa07a9e8dd5a2e573561b72d411da88e515"
|
|
1189
1189
|
}
|
|
1190
1190
|
]
|
|
1191
1191
|
},
|
|
@@ -1256,11 +1256,11 @@
|
|
|
1256
1256
|
"hashes": [
|
|
1257
1257
|
{
|
|
1258
1258
|
"alg": "SHA-256",
|
|
1259
|
-
"content": "
|
|
1259
|
+
"content": "0f2fadb970f21ee2c04573d97fd22857d26b39925d11ea1a9dff3f07b5c3d0ae"
|
|
1260
1260
|
},
|
|
1261
1261
|
{
|
|
1262
1262
|
"alg": "SHA3-512",
|
|
1263
|
-
"content": "
|
|
1263
|
+
"content": "8a2b59dc57a5dad4e53420d465999b16d9ca949b95846293ee1dc088709df9dcabc7994e47385c0f1471039556f4e2d867c6e7d70f59c51b345c32f52f0e30fa"
|
|
1264
1264
|
}
|
|
1265
1265
|
]
|
|
1266
1266
|
},
|
|
@@ -1451,11 +1451,11 @@
|
|
|
1451
1451
|
"hashes": [
|
|
1452
1452
|
{
|
|
1453
1453
|
"alg": "SHA-256",
|
|
1454
|
-
"content": "
|
|
1454
|
+
"content": "2a488a8dcbb25e0e46dd04e6c8294406f3f75833ff3b2e59fdc9ba3e589df5c8"
|
|
1455
1455
|
},
|
|
1456
1456
|
{
|
|
1457
1457
|
"alg": "SHA3-512",
|
|
1458
|
-
"content": "
|
|
1458
|
+
"content": "39ec71547e6c9c9823749ae0248f886af234358e6937da59001ae5cd3bb846a7134b0cbf15f5a086f80b2eec1d0a9ebee96d01568559930a5ac560235b19a246"
|
|
1459
1459
|
}
|
|
1460
1460
|
]
|
|
1461
1461
|
},
|
|
@@ -1466,11 +1466,11 @@
|
|
|
1466
1466
|
"hashes": [
|
|
1467
1467
|
{
|
|
1468
1468
|
"alg": "SHA-256",
|
|
1469
|
-
"content": "
|
|
1469
|
+
"content": "2f799a7a21280a79e8243e26c2d7291065f7dfe74309c21e1c68d57098d621fe"
|
|
1470
1470
|
},
|
|
1471
1471
|
{
|
|
1472
1472
|
"alg": "SHA3-512",
|
|
1473
|
-
"content": "
|
|
1473
|
+
"content": "9356628e24f64ca47efa6dda108ee9e78cb9f809bf1c2a42b656c102163b82292720ed3184bddf5394ea8942f9872f39e8c5960082e56848d92bc11f52f1e8da"
|
|
1474
1474
|
}
|
|
1475
1475
|
]
|
|
1476
1476
|
},
|
|
@@ -1496,11 +1496,11 @@
|
|
|
1496
1496
|
"hashes": [
|
|
1497
1497
|
{
|
|
1498
1498
|
"alg": "SHA-256",
|
|
1499
|
-
"content": "
|
|
1499
|
+
"content": "89ab64576398f84314444c839076bdc62a444de62e2751b04e44f5395e0d24c2"
|
|
1500
1500
|
},
|
|
1501
1501
|
{
|
|
1502
1502
|
"alg": "SHA3-512",
|
|
1503
|
-
"content": "
|
|
1503
|
+
"content": "a0b2fd6dbf6a5a2a3f0582df57cc1e1cdf3c837a7a6f2f99c29398655f320d7b4ec4634fe460ef65e7ee1c9be3d4d116522197c9d99480fb181304cf12bcaff4"
|
|
1504
1504
|
}
|
|
1505
1505
|
]
|
|
1506
1506
|
},
|
|
@@ -1751,11 +1751,11 @@
|
|
|
1751
1751
|
"hashes": [
|
|
1752
1752
|
{
|
|
1753
1753
|
"alg": "SHA-256",
|
|
1754
|
-
"content": "
|
|
1754
|
+
"content": "b574dffe09c2881e4cae3a4676aa0396950263e755349c677e94343945f435a9"
|
|
1755
1755
|
},
|
|
1756
1756
|
{
|
|
1757
1757
|
"alg": "SHA3-512",
|
|
1758
|
-
"content": "
|
|
1758
|
+
"content": "8cdea5fbb52cf82fd543d1333375359708ca1b7ce172bbaf81fc5cd6374715dedc62b3a2249da685808c02cf817086b795e1510c175b5ea33c25a54e8bdcb4b2"
|
|
1759
1759
|
}
|
|
1760
1760
|
]
|
|
1761
1761
|
},
|
|
@@ -1901,11 +1901,11 @@
|
|
|
1901
1901
|
"hashes": [
|
|
1902
1902
|
{
|
|
1903
1903
|
"alg": "SHA-256",
|
|
1904
|
-
"content": "
|
|
1904
|
+
"content": "7cdbf86213bc03cc55f8cd1ec5516f7c492177f0968c27216beabf68fdd68ef1"
|
|
1905
1905
|
},
|
|
1906
1906
|
{
|
|
1907
1907
|
"alg": "SHA3-512",
|
|
1908
|
-
"content": "
|
|
1908
|
+
"content": "3237cc4431489ce414f974687ad396fd756df4f66da169b260de9ade1d64ff7f942ffb294653c426d804f793d41caec0ca1bf5a8110b31f6f62d73e9b95175c8"
|
|
1909
1909
|
}
|
|
1910
1910
|
]
|
|
1911
1911
|
},
|
|
@@ -1976,11 +1976,11 @@
|
|
|
1976
1976
|
"hashes": [
|
|
1977
1977
|
{
|
|
1978
1978
|
"alg": "SHA-256",
|
|
1979
|
-
"content": "
|
|
1979
|
+
"content": "515a7c8b3ec6814d45c35a8243b30e71c900dfc4016adb5201b9bbecb047d1fd"
|
|
1980
1980
|
},
|
|
1981
1981
|
{
|
|
1982
1982
|
"alg": "SHA3-512",
|
|
1983
|
-
"content": "
|
|
1983
|
+
"content": "58ef4fcb1a6d5cb7f9f75046dfa63fc7c732440932f76acc31981a7edc0f5dbfacea8f5d30709aecbcb05331332f39921ae5e766a068403913c6d753b2136de3"
|
|
1984
1984
|
}
|
|
1985
1985
|
]
|
|
1986
1986
|
},
|
|
@@ -2096,11 +2096,11 @@
|
|
|
2096
2096
|
"hashes": [
|
|
2097
2097
|
{
|
|
2098
2098
|
"alg": "SHA-256",
|
|
2099
|
-
"content": "
|
|
2099
|
+
"content": "9f54c09e25593d82ba36bffcf5ee6b8137eab9e1f5408b7cf6cb681a69806d4c"
|
|
2100
2100
|
},
|
|
2101
2101
|
{
|
|
2102
2102
|
"alg": "SHA3-512",
|
|
2103
|
-
"content": "
|
|
2103
|
+
"content": "9711d20338cd19da03cb50d089aa42ce5c8f45513d92523a84572494337d88654a29bedc991009ba4522346d3b04506621974707a537c1e24ee1d985017d113f"
|
|
2104
2104
|
}
|
|
2105
2105
|
]
|
|
2106
2106
|
},
|
|
@@ -2396,11 +2396,11 @@
|
|
|
2396
2396
|
"hashes": [
|
|
2397
2397
|
{
|
|
2398
2398
|
"alg": "SHA-256",
|
|
2399
|
-
"content": "
|
|
2399
|
+
"content": "3fcc2abf934ee1b70674856ba937833830d45346b4f146d613e500b9a9b4d163"
|
|
2400
2400
|
},
|
|
2401
2401
|
{
|
|
2402
2402
|
"alg": "SHA3-512",
|
|
2403
|
-
"content": "
|
|
2403
|
+
"content": "1cc3b832434aa188fc7136feadb286c68d528e1e35406572bbaaf29a1b21be90ca59f3a8ea96d7173e7365e6c7b9248fdb45dcb6c56bbeec93ede95341ca5720"
|
|
2404
2404
|
}
|
|
2405
2405
|
]
|
|
2406
2406
|
},
|
|
@@ -2606,11 +2606,11 @@
|
|
|
2606
2606
|
"hashes": [
|
|
2607
2607
|
{
|
|
2608
2608
|
"alg": "SHA-256",
|
|
2609
|
-
"content": "
|
|
2609
|
+
"content": "bba34316450d9bb0e6aa60e75dfcb28bdc767487d13ca429dbc654772a18938b"
|
|
2610
2610
|
},
|
|
2611
2611
|
{
|
|
2612
2612
|
"alg": "SHA3-512",
|
|
2613
|
-
"content": "
|
|
2613
|
+
"content": "41e269280701ba73957c7d747f019568c23d5c5e6a19c6f7a5153c0211937a673df33e81b0f7efe286ebd36a50789bbbc45b8d145fb94f7800008021cb7f9530"
|
|
2614
2614
|
}
|
|
2615
2615
|
]
|
|
2616
2616
|
},
|
|
@@ -2674,6 +2674,21 @@
|
|
|
2674
2674
|
}
|
|
2675
2675
|
]
|
|
2676
2676
|
},
|
|
2677
|
+
{
|
|
2678
|
+
"bom-ref": "file:scripts/sync-package-description.js",
|
|
2679
|
+
"type": "file",
|
|
2680
|
+
"name": "scripts/sync-package-description.js",
|
|
2681
|
+
"hashes": [
|
|
2682
|
+
{
|
|
2683
|
+
"alg": "SHA-256",
|
|
2684
|
+
"content": "4110a273512bf43b9246f5ab4c196008c9043778b4137dca8a459df7ca6066de"
|
|
2685
|
+
},
|
|
2686
|
+
{
|
|
2687
|
+
"alg": "SHA3-512",
|
|
2688
|
+
"content": "427e94caa4b48f2a4f0421cc9924e7c9b626032a7d78df684e0e63743331caf16da65a345f88f90c316aa6c0ae95d29924d69d1cb8d92e582bf4e376629428a0"
|
|
2689
|
+
}
|
|
2690
|
+
]
|
|
2691
|
+
},
|
|
2677
2692
|
{
|
|
2678
2693
|
"bom-ref": "file:scripts/validate-vendor-online.js",
|
|
2679
2694
|
"type": "file",
|
|
@@ -2696,11 +2711,11 @@
|
|
|
2696
2711
|
"hashes": [
|
|
2697
2712
|
{
|
|
2698
2713
|
"alg": "SHA-256",
|
|
2699
|
-
"content": "
|
|
2714
|
+
"content": "7541cd702c57e7b966b4b1bfffed748dba16f5bb25ff9176be64a50a92fc8e65"
|
|
2700
2715
|
},
|
|
2701
2716
|
{
|
|
2702
2717
|
"alg": "SHA3-512",
|
|
2703
|
-
"content": "
|
|
2718
|
+
"content": "15c7bb7f853a04a615d247ff47ac53c1b282cfc56875fd0248843b069d71ce04e3731bb3890b01644d58f0f42e37d362016e4f8176e4dd64ba837043c46f5805"
|
|
2704
2719
|
}
|
|
2705
2720
|
]
|
|
2706
2721
|
},
|
|
@@ -3491,11 +3506,11 @@
|
|
|
3491
3506
|
"hashes": [
|
|
3492
3507
|
{
|
|
3493
3508
|
"alg": "SHA-256",
|
|
3494
|
-
"content": "
|
|
3509
|
+
"content": "5410a1ca1ebea4c3d7ff14a9ea5d69dde5d59490f9bae518d1d3b0440a457238"
|
|
3495
3510
|
},
|
|
3496
3511
|
{
|
|
3497
3512
|
"alg": "SHA3-512",
|
|
3498
|
-
"content": "
|
|
3513
|
+
"content": "96872e1b74348e5be3d440e3e2265ab14ce35118a4226d7b2a698c36573374120bb663c8c852a67206169b715b7008944941a9a4ef890f7d1afa94e7d105be71"
|
|
3499
3514
|
}
|
|
3500
3515
|
]
|
|
3501
3516
|
},
|
package/scripts/build-indexes.js
CHANGED
|
@@ -266,6 +266,13 @@ const OUTPUTS = [
|
|
|
266
266
|
for (const s of ctx.skills) {
|
|
267
267
|
const body = ctx.skillBodies[s.name];
|
|
268
268
|
for (const code of codes) {
|
|
269
|
+
// Skip bare 2-letter ISO codes in free-text matching. `\bID\b`,
|
|
270
|
+
// `\bCA\b`, `\bNO\b` etc. collide with prose words ("the ID",
|
|
271
|
+
// "US-based") and control/countermeasure id grammar (`\bCA\b` matches
|
|
272
|
+
// inside `D3-CA`, `\bSA\b` inside `SA-12`), polluting coverage
|
|
273
|
+
// (Indonesia landed on 41/51 skills). These jurisdictions are mapped
|
|
274
|
+
// via the curated NAME_TO_CODE regulation-name table below instead.
|
|
275
|
+
if (code.length <= 2) continue;
|
|
269
276
|
const re = new RegExp("\\b" + code + "\\b");
|
|
270
277
|
if (re.test(body) && !out[code].skills.includes(s.name)) out[code].skills.push(s.name);
|
|
271
278
|
}
|
|
@@ -499,7 +506,7 @@ const OUTPUTS = [
|
|
|
499
506
|
{
|
|
500
507
|
name: "stale-content",
|
|
501
508
|
file: "stale-content.json",
|
|
502
|
-
deps: [isManifest, isAnySkillBody, isAnyCatalog],
|
|
509
|
+
deps: [isManifest, isAnySkillBody, isAnyCatalog, (p) => p === "README.md"],
|
|
503
510
|
build: (ctx) => {
|
|
504
511
|
const { buildStaleContent } = require("./builders/stale-content");
|
|
505
512
|
return buildStaleContent({ root: ctx.root, manifest: ctx.manifest, skills: ctx.skills, catalogFiles: ctx.catalogFiles });
|
|
@@ -518,6 +525,10 @@ function loadPriorMeta() {
|
|
|
518
525
|
function liveSourceSet(ctx) {
|
|
519
526
|
const out = new Set();
|
|
520
527
|
out.add("manifest.json");
|
|
528
|
+
// README.md is consumed by the stale-content builder (badge-count drift), so
|
|
529
|
+
// it must be a hashed source — otherwise a README edit is invisible to
|
|
530
|
+
// --changed and the validate-indexes freshness gate.
|
|
531
|
+
if (fs.existsSync(ABS("README.md"))) out.add("README.md");
|
|
521
532
|
for (const c of ctx.catalogFiles) out.add(c);
|
|
522
533
|
for (const s of ctx.skills) out.add(s.path);
|
|
523
534
|
return out;
|
|
@@ -136,19 +136,42 @@ function checkSbomCurrency(root) {
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
// The "N catalogs" and "N jurisdictions" free-text counts in the same
|
|
140
|
+
// description string were never validated — only the per-catalog entry tokens
|
|
141
|
+
// and the skill count were. Pin them to the live values so a stale
|
|
142
|
+
// description (e.g. after an auto-refresh changed a count) fails the gate.
|
|
143
|
+
const catalogMatch = description.match(/(\d+)\s+catalogs?\b/i);
|
|
144
|
+
if (catalogMatch && Number(catalogMatch[1]) !== liveCatalogs) {
|
|
145
|
+
errors.push(
|
|
146
|
+
`SBOM description catalog count is ${Number(catalogMatch[1])} but live data/ has ${liveCatalogs} catalogs — description is stale; update package.json.description and \`npm run refresh-sbom\``
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const liveJurisdictions = (() => {
|
|
150
|
+
try {
|
|
151
|
+
const gf = JSON.parse(fs.readFileSync(path.join(dataDir, "global-frameworks.json"), "utf8"));
|
|
152
|
+
// Non-underscore top-level keys — the canonical jurisdiction count the
|
|
153
|
+
// README badge and catalog-summaries use.
|
|
154
|
+
return Object.keys(gf).filter((k) => !k.startsWith("_")).length;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
const jurisdictionMatch = description.match(/(\d+)\s+jurisdictions?\b/i);
|
|
160
|
+
if (liveJurisdictions !== null && jurisdictionMatch && Number(jurisdictionMatch[1]) !== liveJurisdictions) {
|
|
161
|
+
errors.push(
|
|
162
|
+
`SBOM description jurisdiction count is ${Number(jurisdictionMatch[1])} but live global-frameworks.json has ${liveJurisdictions} — description is stale; update package.json.description and \`npm run refresh-sbom\``
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Component-level cross-check (defense-in-depth). In normal operation
|
|
167
|
+
// refresh-sbom emits NO per-skill "skill:" components — skill drift is caught
|
|
168
|
+
// by the file:skills/<name>/skill.md and file:manifest.json content hashes in
|
|
169
|
+
// the file: component pass below (a bumped or renamed skill changes those
|
|
170
|
+
// bytes). This branch is therefore not exercised by a clean SBOM, but it is
|
|
171
|
+
// retained as a tamper guard: a forged or buggy SBOM that injected a skill
|
|
172
|
+
// component with a stale version (or a skill name no longer in the manifest)
|
|
173
|
+
// is still caught here. Vendor components are validated against
|
|
174
|
+
// vendor/blamejs/_PROVENANCE.json.
|
|
152
175
|
const components = Array.isArray(sbom.components) ? sbom.components : [];
|
|
153
176
|
const skillByName = new Map(
|
|
154
177
|
(manifest.skills || []).map((s) => [s.name, s])
|
|
@@ -281,6 +304,45 @@ function checkSbomCurrency(root) {
|
|
|
281
304
|
fileComponentsChecked++;
|
|
282
305
|
}
|
|
283
306
|
|
|
307
|
+
// Completeness + bundle-digest integrity. The per-file pass above verifies
|
|
308
|
+
// every RECORDED file: component, but never checked that every SHIPPED file
|
|
309
|
+
// (the package.json.files expansion) actually HAS a component — a
|
|
310
|
+
// newly-shipped file would ship unhashed and silent. And the aggregate
|
|
311
|
+
// bundle digest in metadata.component.hashes[] was never recomputed. Reuse
|
|
312
|
+
// refresh-sbom's exact allowlist expansion + digest so the gate can't drift
|
|
313
|
+
// from the generator.
|
|
314
|
+
try {
|
|
315
|
+
const { expandAllowlist, bundleDigest } = require("./refresh-sbom");
|
|
316
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
|
|
317
|
+
const expected = expandAllowlist(pkg.files || []);
|
|
318
|
+
const fileComps = components.filter(
|
|
319
|
+
(c) => typeof c["bom-ref"] === "string" && c["bom-ref"].startsWith("file:")
|
|
320
|
+
);
|
|
321
|
+
const fileCompNames = new Set(fileComps.map((c) => c.name));
|
|
322
|
+
for (const rel of expected) {
|
|
323
|
+
if (!fileCompNames.has(rel)) {
|
|
324
|
+
errors.push(
|
|
325
|
+
`Shipped file "${rel}" (package.json.files) has no file: component in the SBOM — run \`npm run refresh-sbom\``
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Recompute the aggregate bundle digest from the file: components' recorded
|
|
330
|
+
// SHA-256 hashes and compare to metadata.component.hashes[] (the per-file
|
|
331
|
+
// pass already tied each recorded hash to live bytes).
|
|
332
|
+
const compHashes = (sbom.metadata && sbom.metadata.component && sbom.metadata.component.hashes) || [];
|
|
333
|
+
const recorded = (compHashes.find((h) => h && h.alg === "SHA-256") || {}).content;
|
|
334
|
+
if (recorded && fileComps.length) {
|
|
335
|
+
const recomputed = bundleDigest(fileComps);
|
|
336
|
+
if (recomputed !== recorded) {
|
|
337
|
+
errors.push(
|
|
338
|
+
`SBOM bundle digest mismatch: metadata.component.hashes SHA-256 ${String(recorded).slice(0, 12)}… != recomputed ${recomputed.slice(0, 12)}… from file: components — run \`npm run refresh-sbom\``
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (e) {
|
|
343
|
+
errors.push(`SBOM completeness/bundle-digest check failed: ${e.message}`);
|
|
344
|
+
}
|
|
345
|
+
|
|
284
346
|
return {
|
|
285
347
|
ok: errors.length === 0,
|
|
286
348
|
errors,
|
|
@@ -310,6 +372,6 @@ function main() {
|
|
|
310
372
|
);
|
|
311
373
|
}
|
|
312
374
|
|
|
313
|
-
module.exports = { checkSbomCurrency, resolveRoot };
|
|
375
|
+
module.exports = { checkSbomCurrency, resolveRoot, DESCRIPTION_ENTRY_TOKENS, catalogEntryCount };
|
|
314
376
|
|
|
315
377
|
if (require.main === module) main();
|
package/scripts/refresh-sbom.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/sync-package-description.js
|
|
5
|
+
*
|
|
6
|
+
* Regenerate the count-bearing tokens embedded in package.json.description from
|
|
7
|
+
* the live catalogs + manifest, so the description stays in sync when an
|
|
8
|
+
* auto-refresh changes an entry count. refresh-sbom copies the description into
|
|
9
|
+
* sbom.cdx.json, and check-sbom-currency validates every token against the live
|
|
10
|
+
* counts — without this sync, the first refresh that changes a count would fail
|
|
11
|
+
* the SBOM description-token gate on the auto-PR.
|
|
12
|
+
*
|
|
13
|
+
* Targeted, format-preserving: replaces only the integer in each known
|
|
14
|
+
* "<N> <label>" token (skills / catalogs / jurisdictions / per-catalog entry
|
|
15
|
+
* counts). Reuses check-sbom-currency's token table so the two can't drift.
|
|
16
|
+
*
|
|
17
|
+
* Run before refresh-sbom in the refresh apply path (and idempotent locally).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
|
|
23
|
+
const { DESCRIPTION_ENTRY_TOKENS, catalogEntryCount } = require('./check-sbom-currency');
|
|
24
|
+
|
|
25
|
+
function syncPackageDescription(root = path.join(__dirname, '..')) {
|
|
26
|
+
const pkgPath = path.join(root, 'package.json');
|
|
27
|
+
const dataDir = path.join(root, 'data');
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
29
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'manifest.json'), 'utf8'));
|
|
30
|
+
|
|
31
|
+
const before = pkg.description || '';
|
|
32
|
+
let desc = before;
|
|
33
|
+
|
|
34
|
+
const liveSkills = Array.isArray(manifest.skills) ? manifest.skills.length : 0;
|
|
35
|
+
const liveCatalogs = fs.readdirSync(dataDir).filter((f) => f.endsWith('.json')).length;
|
|
36
|
+
let liveJurisdictions = null;
|
|
37
|
+
try {
|
|
38
|
+
const gf = JSON.parse(fs.readFileSync(path.join(dataDir, 'global-frameworks.json'), 'utf8'));
|
|
39
|
+
liveJurisdictions = Object.keys(gf).filter((k) => !k.startsWith('_')).length;
|
|
40
|
+
} catch { /* leave null — skip the jurisdiction token */ }
|
|
41
|
+
|
|
42
|
+
// Replace only the integer in "<N> <label>"; `labelRe` is the same (already
|
|
43
|
+
// regex-escaped) pattern check-sbom-currency matches, and $2 preserves the
|
|
44
|
+
// matched label text verbatim.
|
|
45
|
+
const sub = (n, labelRe) => {
|
|
46
|
+
if (n == null) return;
|
|
47
|
+
desc = desc.replace(new RegExp('(\\d+)(\\s+' + labelRe + '\\b)'), String(n) + '$2');
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
sub(liveSkills, 'skills');
|
|
51
|
+
sub(liveCatalogs, 'catalogs?');
|
|
52
|
+
sub(liveJurisdictions, 'jurisdictions?');
|
|
53
|
+
for (const { file, label } of DESCRIPTION_ENTRY_TOKENS) {
|
|
54
|
+
sub(catalogEntryCount(dataDir, file), label);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const changed = desc !== before;
|
|
58
|
+
if (changed) {
|
|
59
|
+
pkg.description = desc;
|
|
60
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
61
|
+
}
|
|
62
|
+
return { changed, description: desc };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (require.main === module) {
|
|
66
|
+
const r = syncPackageDescription();
|
|
67
|
+
process.stdout.write(
|
|
68
|
+
r.changed
|
|
69
|
+
? `package.json description synced from live counts:\n ${r.description}\n`
|
|
70
|
+
: 'package.json description already in sync with live counts.\n'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { syncPackageDescription };
|
|
@@ -119,9 +119,15 @@ module.exports = {
|
|
|
119
119
|
const ROOT = path.resolve(__dirname, "..");
|
|
120
120
|
|
|
121
121
|
function emit(msg) { process.stdout.write(`[verify-shipped-tarball] ${msg}\n`); }
|
|
122
|
+
// Sentinel thrown by fail() so the script body's try/finally still runs its
|
|
123
|
+
// temp-dir cleanup. process.exit() would preempt the finally, leaking the
|
|
124
|
+
// npm-pack temp dir on every run (predeploy gate + `npm test`). Abort by
|
|
125
|
+
// throwing instead and set the exit code via process.exitCode.
|
|
126
|
+
const ABORT = Symbol("verify-shipped-tarball:abort");
|
|
122
127
|
function fail(msg, code = 1) {
|
|
123
128
|
process.stderr.write(`[verify-shipped-tarball] FAIL: ${msg}\n`);
|
|
124
|
-
process.
|
|
129
|
+
process.exitCode = code;
|
|
130
|
+
throw ABORT;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
// Gate the script body behind require.main === module so tests can
|
|
@@ -374,15 +380,20 @@ try {
|
|
|
374
380
|
emit(`tarball verify result: ${pass}/${total} pass, ${fail_count} fail, ${miss} missing`);
|
|
375
381
|
if (fail_count === 0 && miss === 0 && pass === total) {
|
|
376
382
|
emit(`PASS — shipped tarball is internally consistent`);
|
|
377
|
-
process.
|
|
383
|
+
process.exitCode = 0;
|
|
384
|
+
} else {
|
|
385
|
+
for (const f of failures.slice(0, 10)) emit(` - ${f}`);
|
|
386
|
+
if (failures.length > 10) emit(` ... and ${failures.length - 10} more`);
|
|
387
|
+
emit(`FAIL — shipped tarball would be broken on every fresh install. Refusing to publish.`);
|
|
388
|
+
process.exitCode = 1;
|
|
378
389
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
390
|
+
} catch (e) {
|
|
391
|
+
// ABORT is the fail() sentinel — cleanup still runs via finally below. Any
|
|
392
|
+
// other error is unexpected: let finally run, then re-propagate it.
|
|
393
|
+
if (e !== ABORT) throw e;
|
|
383
394
|
} finally {
|
|
384
395
|
// Best-effort cleanup; leave on failure for diagnostics.
|
|
385
|
-
if (process.exitCode === 0) {
|
|
396
|
+
if (process.exitCode === 0 || process.exitCode === undefined) {
|
|
386
397
|
try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch {}
|
|
387
398
|
} else {
|
|
388
399
|
emit(`temp dir preserved for inspection: ${tmpRoot}`);
|
|
@@ -39,7 +39,7 @@ const KEV_FEED = 'https://www.cisa.gov/sites/default/files/feeds/known_exploited
|
|
|
39
39
|
const EPSS_API = 'https://api.first.org/data/v1/epss?cve=';
|
|
40
40
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
41
41
|
|
|
42
|
-
const { selectNvdCvss } = require('../../lib/cvss');
|
|
42
|
+
const { selectNvdCvss, cvssVersionOf } = require('../../lib/cvss');
|
|
43
43
|
const EPSS_DRIFT_THRESHOLD = 0.05; // |Δscore| or |Δpercentile| > 0.05 flags drift
|
|
44
44
|
const USER_AGENT = 'exceptd-security/cve-validator (+https://exceptd.com)';
|
|
45
45
|
|
|
@@ -269,13 +269,23 @@ async function validateCve(cveId, localEntry) {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
// --- Compare CVSS (only if NVD reachable & has data) ---
|
|
272
|
-
|
|
273
|
-
|
|
272
|
+
// Guard against cross-version downgrades: NVD often carries only a legacy v2
|
|
273
|
+
// metric for older CVEs while the catalog is curated to CVSS:3.1. Surfacing
|
|
274
|
+
// (and, on `refresh --apply`, writing) the lower v2 score/vector over a
|
|
275
|
+
// curated v3.x value is a downgrade, not a drift. Mirror the cache path's
|
|
276
|
+
// suppression (lib/refresh-external.js nvdDiffFromCache) so the live and
|
|
277
|
+
// cache refresh paths converge; a same-version re-score still flows through.
|
|
278
|
+
const localCvssVersion = cvssVersionOf(local.cvss_vector);
|
|
279
|
+
const fetchedCvssVersion = cvssVersionOf(fetched.cvss_vector);
|
|
280
|
+
const cvssIsDowngrade =
|
|
281
|
+
fetchedCvssVersion != null && localCvssVersion != null && fetchedCvssVersion < localCvssVersion;
|
|
282
|
+
if (cveFoundInNvd && !cvssIsDowngrade) {
|
|
283
|
+
if (fetched.cvss_score !== null && local.cvss_score !== null &&
|
|
284
|
+
Math.abs(fetched.cvss_score - local.cvss_score) > 0.05) {
|
|
274
285
|
pushDiscrepancy(discrepancies, 'cvss_score', local.cvss_score, fetched.cvss_score, 'high');
|
|
275
286
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (fetched.cvss_vector !== local.cvss_vector) {
|
|
287
|
+
if (fetched.cvss_vector && local.cvss_vector &&
|
|
288
|
+
fetched.cvss_vector !== local.cvss_vector) {
|
|
279
289
|
pushDiscrepancy(discrepancies, 'cvss_vector', local.cvss_vector, fetched.cvss_vector, 'medium');
|
|
280
290
|
}
|
|
281
291
|
}
|