@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/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:2eefe0b3-a311-474a-9600-c6f1d3c9189f",
4
+ "serialNumber": "urn:uuid:39da38cf-85e2-487e-a082-3e9f9e709f66",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2050-12-15T06:22:43.000Z",
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.28"
12
+ "version": "0.16.29"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.16.28",
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.28",
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",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.16.29",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "f57896f38f87ddfbf94c435d4231c4101c7803acad61487eea8b1161045876a5"
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.28"
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": "615a8b962b2fbcefafc27353e90490d437c93ea5929fda0ad08764f3c8579674"
119
+ "content": "9401b3b84ba8f31ce8e9490b5f8888eceecc15ecd872dfcf31cf9effadfaf3ee"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "7500cd8b9dae1b4e967d0489cddfb7afdb461d00e956dd411f044e7f302327a23d6ab2d9315d784082c3a6eba9bbe9cfa6e09b935ef31f4e30983cf9ba169e89"
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": "6e9322e4eaf27c972ff06508673b3e5bd161cf86f407c0c5c9d953a2f6b2da9b"
179
+ "content": "e7b854e7db9a364a1b368b5084b4f0c2a8282f0459ce39800ac1d1dabdc06074"
180
180
  },
181
181
  {
182
182
  "alg": "SHA3-512",
183
- "content": "9bd89f3abf9b68844dfec01694de45fcdb70e9ae0225d17c1a31839d0d0512a87f5b305ec1890357d0625b2feafb6dc292c3c6d3454cabbb7dc05f43fce07a0d"
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": "669897636b4b0e4ea13d0ebd0fba261c09188a0b3a1a06d4596fa77f24ea497a"
284
+ "content": "ce80148fd48bcd06575b368461e686aa06aaff746f184a33be3347a352ecf2bf"
285
285
  },
286
286
  {
287
287
  "alg": "SHA3-512",
288
- "content": "a8770edb4e20e5d1995aad8ba73cdf22db9f476012238d750dd4bf3008a557e75d70333b6c4993b653201469557af18a59286d8a3c268c8bdda06f6ea535b900"
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": "c84a7d815cba16b8cce07e782400853fb3e9adfe56fcb8cdfe4a6e784e8c619f"
989
+ "content": "081583535b98f300966c656ba007858127506f4d2853a62f8f5084984d99a3ef"
990
990
  },
991
991
  {
992
992
  "alg": "SHA3-512",
993
- "content": "d1c21684b8db41e278e58c7bbc80e79bbfbebcc6993e7527c3d6b32d83754c335647aa590fbf7f72d524f66e8366310c2157e030b1ddabbcf3bd29f63e3763e7"
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": "a9b7fd5e3b4fae07ac1734d59dada55b904aac727197298efb3394c59feb9469"
1184
+ "content": "d5962bed4fda430834283e9ec557161ba7e2c4f5f964f8fd45f4a1723a8cd622"
1185
1185
  },
1186
1186
  {
1187
1187
  "alg": "SHA3-512",
1188
- "content": "6e4216872af0f849a64163ce9f8963b4ca875208a9868ba0f55cb6319362194bb6f5db657f11cd636c0da58ac9460ad8df638c5b5a8d34a5855dc07cf6d574a9"
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": "9cc57e688ef2812e687db325264f9c1d7b57d7bcb907e697bbd98e02fa01e09d"
1259
+ "content": "0f2fadb970f21ee2c04573d97fd22857d26b39925d11ea1a9dff3f07b5c3d0ae"
1260
1260
  },
1261
1261
  {
1262
1262
  "alg": "SHA3-512",
1263
- "content": "337b7ce0afcf1fa521286d7f5506fc06db348ab6cabddfa5f20c4a72ac8b329e6be5d429119f550371709606f9fba7d88bec65c2643a579d334903c2f295e8ae"
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": "b6b3ac05cb9b5d0c86a1966656e4e0ea5b7d2b187c12f73853c2fedba1d61185"
1454
+ "content": "2a488a8dcbb25e0e46dd04e6c8294406f3f75833ff3b2e59fdc9ba3e589df5c8"
1455
1455
  },
1456
1456
  {
1457
1457
  "alg": "SHA3-512",
1458
- "content": "256ca28b696c261766f47c57328088dc87c89d56998064d10072ee6f1ffe597d8f4ff565e6599ca947d0ede47020a272e7ce5f565fa5ded54f1e95b11a5b4022"
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": "50efed9848abc76cc27b89f2d3ec1539e7945ead8f782dd6a50b8bd9e50aeea1"
1469
+ "content": "2f799a7a21280a79e8243e26c2d7291065f7dfe74309c21e1c68d57098d621fe"
1470
1470
  },
1471
1471
  {
1472
1472
  "alg": "SHA3-512",
1473
- "content": "2cf21e6aaf8fd15d6ea64a25cc142c05d45919f670f324eb95ae47f6e8b07832d768e5c2c61946b5673678831fc5278f6b2e579159a264715c91865bf2191e6f"
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": "c344332e0b7650fe54e07041726c73395749c825906fc82e8fb8cc82105ab871"
1499
+ "content": "89ab64576398f84314444c839076bdc62a444de62e2751b04e44f5395e0d24c2"
1500
1500
  },
1501
1501
  {
1502
1502
  "alg": "SHA3-512",
1503
- "content": "6f13b6997a326ef7e6b6435102b2c33d03016b4c5e27208971ae18215e96cc7093f56ae220a827e835c69a0c45533aa580205849e9425878ce6d9aef8625b4af"
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": "41a71d0bf1cff279a5b501a9a40a1874ab5a7c6a5e66ded23f8761899f24fc50"
1754
+ "content": "b574dffe09c2881e4cae3a4676aa0396950263e755349c677e94343945f435a9"
1755
1755
  },
1756
1756
  {
1757
1757
  "alg": "SHA3-512",
1758
- "content": "d3ee7b39c607f47b1172aee89bc99af55ba4342e609232005a540270ff88a2f1b7ff6852d00eb260560bbd88fad6ececf9fd78967ae32fecb949f66438cd4e1a"
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": "21f2f84671301efb38afbaad51f8d06fa46137747d078d94c439defa669c114b"
1904
+ "content": "7cdbf86213bc03cc55f8cd1ec5516f7c492177f0968c27216beabf68fdd68ef1"
1905
1905
  },
1906
1906
  {
1907
1907
  "alg": "SHA3-512",
1908
- "content": "d6794e16570d6739b56e4124598ad36b4f9c2057d138942e89d867f91479ba40064ed5ed186744afe1347e22b7aab464f3f61bdf92bce62d6714554379de2b85"
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": "5f403396c73e40ca6963fe3fe71d4a2dd5637710d2d72d616d3f2b76aa46a21a"
1979
+ "content": "515a7c8b3ec6814d45c35a8243b30e71c900dfc4016adb5201b9bbecb047d1fd"
1980
1980
  },
1981
1981
  {
1982
1982
  "alg": "SHA3-512",
1983
- "content": "8d4253e780b5a3a7a84185b4eee8ca5905303419445a669d8ed6c6a6a1531f17012af8f187f7a99620047276a7b5828679ecb37ada184515579b95a9a736f85e"
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": "6e2b39f275866812938a5e9b7105eb7dfb201a45c2cb6ea64ba60e5ad37600e6"
2099
+ "content": "9f54c09e25593d82ba36bffcf5ee6b8137eab9e1f5408b7cf6cb681a69806d4c"
2100
2100
  },
2101
2101
  {
2102
2102
  "alg": "SHA3-512",
2103
- "content": "4eaee5df87cc6565d8b4e708bd0219135a901f570f34bc8e15f37e9f42d7270a464ffee895743a05b9b4fd446b4147a6f8fd36de546342e5724e7dbed0536e8f"
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": "0dfd0589ba86278f1b04d1deb98429b64f5714e3a9d51c35b1bff1eca2e11ac5"
2399
+ "content": "3fcc2abf934ee1b70674856ba937833830d45346b4f146d613e500b9a9b4d163"
2400
2400
  },
2401
2401
  {
2402
2402
  "alg": "SHA3-512",
2403
- "content": "ee4b3a028a1b98c3a5d8071f25ff254cbe12c21d46292f3de42f2ac92b5e70ae2da1062a94cdf45ac0fd68d01e6641e824e6175aa5423d1f5957cdf9b83ec296"
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": "c70973d1c3ecf755361539423b6a41d9d598f25527c64a116c63a5bab240bcbd"
2609
+ "content": "bba34316450d9bb0e6aa60e75dfcb28bdc767487d13ca429dbc654772a18938b"
2610
2610
  },
2611
2611
  {
2612
2612
  "alg": "SHA3-512",
2613
- "content": "b5356776cd94758a64c4b8d7e66e2b8eaf027136c878fe0fe1a404e8ce22fd2826346b5b547e3db67e1c73c001e09be007a0a1b4c968ca6c8e58235a28032233"
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": "fd04f3e4122b4f3ca7e9a266c763dd4e954710d496da8072f5c2e500a4dbe32c"
2714
+ "content": "7541cd702c57e7b966b4b1bfffed748dba16f5bb25ff9176be64a50a92fc8e65"
2700
2715
  },
2701
2716
  {
2702
2717
  "alg": "SHA3-512",
2703
- "content": "9b606b4d5afcc79a7a39650c3f425af0a8abb21f484e70e5469431a0be26b27969108bd51f167cab23b41d6e0ec7d14b8e6d82d597ca6166942605e15fc016c0"
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": "722ac72d21a820786c1c4cf1d6f5c0838a30267f60ee973ae9b736f15aeebf78"
3509
+ "content": "5410a1ca1ebea4c3d7ff14a9ea5d69dde5d59490f9bae518d1d3b0440a457238"
3495
3510
  },
3496
3511
  {
3497
3512
  "alg": "SHA3-512",
3498
- "content": "105ae5fc896d58bc751fdb484a3a36cad9fa652e89ff27eb00b2877d336314c5f110e0b5ce88bb822915fce31792d71bd2cf8bbad7193bfd0efd77df57fe8d18"
3513
+ "content": "96872e1b74348e5be3d440e3e2265ab14ce35118a4226d7b2a698c36573374120bb663c8c852a67206169b715b7008944941a9a4ef890f7d1afa94e7d105be71"
3499
3514
  }
3500
3515
  ]
3501
3516
  },
@@ -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
- // component-level cross-check. A renamed or version-bumped
140
- // skill that never made it into the SBOM refresh will pass the count
141
- // check (the cardinality is unchanged) but the per-component name +
142
- // version comparison surfaces it. Two component classes are recognised:
143
- //
144
- // 1. Skill components bom-ref begins with "skill:" OR the component
145
- // name matches a manifest.skills[].name. Each one must exist in
146
- // manifest.skills with the same version.
147
- // 2. Vendor components — bom-ref begins with "vendor:". Validated
148
- // against vendor/blamejs/_PROVENANCE.json when present.
149
- //
150
- // Components that don't fit either pattern are surfaced as warnings
151
- // (not errors) so the gate isn't brittle against future component types.
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();
@@ -409,4 +409,4 @@ if (require.main === module) {
409
409
  main();
410
410
  }
411
411
 
412
- module.exports = { buildSbom };
412
+ module.exports = { buildSbom, expandAllowlist, bundleDigest };
@@ -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.exit(code);
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.exit(0);
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
- for (const f of failures.slice(0, 10)) emit(` - ${f}`);
380
- if (failures.length > 10) emit(` ... and ${failures.length - 10} more`);
381
- emit(`FAIL shipped tarball would be broken on every fresh install. Refusing to publish.`);
382
- process.exit(1);
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
- if (cveFoundInNvd && fetched.cvss_score !== null && local.cvss_score !== null) {
273
- if (Math.abs(fetched.cvss_score - local.cvss_score) > 0.05) {
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
- if (cveFoundInNvd && fetched.cvss_vector && local.cvss_vector) {
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
  }