@blamejs/exceptd-skills 0.18.11 → 0.18.12

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:ee3541bb-82de-4e92-a475-4ab086a91c79",
4
+ "serialNumber": "urn:uuid:e985db48-bf9b-413b-9f95-7ae7c5737091",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2152-08-23T09:54:03.000Z",
7
+ "timestamp": "2150-02-25T15:29:12.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.18.11"
12
+ "version": "0.18.12"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.18.11",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.18.12",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.18.11",
19
+ "version": "0.18.12",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (459 CVEs / 181 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.18.11",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.18.12",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "a7b33bc8e119f9e5b4a1f38cbbfbf0a0449dbc57ede8ae66dedc1e73a758c280"
32
+ "content": "cfa66298a26b4767ee33de3ea80dce8fb93eafe7842c818eafcf2bb799db3023"
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.18.11"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.18.12"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "4dacab4a4fb7b2fb4564d3d2bff9bdfbf0a0e00619eee2c7a0ce5e54edc566d7"
119
+ "content": "e6261b4abe62c5fb438605be888655fea1311752fe6a8de44a88d67cc250c66b"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "19e61fac72e079baa46589bbc181f7cc570fe38b34636c451564858b8e80333e31be9038947a2f428f61a22252e5021e15196cd48fcd57ab351f219058ebd00a"
123
+ "content": "c7dd7968a66587e982d7d15f37c46f7c61f0685b11da95a1fe861c997b9806c459b734f0203dd9cc3baf9fb77c4915d8bf1c70852ad2481429177e139d4a8517"
124
124
  }
125
125
  ]
126
126
  },
@@ -281,11 +281,11 @@
281
281
  "hashes": [
282
282
  {
283
283
  "alg": "SHA-256",
284
- "content": "82df5d8cd5e3367bc8f638838f597a33dd40a2abd796941ec0edf2a3c8ba22f8"
284
+ "content": "7c5e618e5bb2ca8544fba021c9dc278d0ed363a42e7285092ec9770056e7d84c"
285
285
  },
286
286
  {
287
287
  "alg": "SHA3-512",
288
- "content": "e1f20c0e63721dfafd4e021ebf62e1b5a63a85ce548fdaadc34cccbbff4d6c83ab3993a8643a3c48572783fe2382749142c7f51c0a82363334a022d968f63592"
288
+ "content": "ffc4158197316118d9ffda5235090961ecce49503e39b940beb32ae66f3885657c73ac45943e2f46afefc00d1a050041e270402d736b2d7af61b5832583bed13"
289
289
  }
290
290
  ]
291
291
  },
@@ -1106,11 +1106,11 @@
1106
1106
  "hashes": [
1107
1107
  {
1108
1108
  "alg": "SHA-256",
1109
- "content": "f9a6371d47ee521ff6f8c22548ae4333a8935e817f9bd505f75eeb1d59467fc3"
1109
+ "content": "b1272fd27e4a13a9d69297dd8fde145eef8a0852c93c1a3172c68c6169fbe5a7"
1110
1110
  },
1111
1111
  {
1112
1112
  "alg": "SHA3-512",
1113
- "content": "1bb5caf2894cc50ee0dbaa2e83d6a8503869737fdb5a9808c4e6a992760c83e5220319e7110a212a075a64d7b9543ac76df6226345442af700a0de6a3aae2476"
1113
+ "content": "d0df4a18564a922064c4d8716c854abe04c8c8271208824b38b7624cf62c1e0c55642a8cbd99b4425cbb2e14cf853913597d509066a5622c66f11a5525273441"
1114
1114
  }
1115
1115
  ]
1116
1116
  },
@@ -1256,11 +1256,11 @@
1256
1256
  "hashes": [
1257
1257
  {
1258
1258
  "alg": "SHA-256",
1259
- "content": "599d3811dc352b4d83ee99d74995bb4a3d1713e85d610c6063caeef994df6031"
1259
+ "content": "8309fb4bac21f191e44453619b9ebe89e2a1aebe0f4db2d62110c4e85f2a2c8b"
1260
1260
  },
1261
1261
  {
1262
1262
  "alg": "SHA3-512",
1263
- "content": "50e8b4795b748d9498f8ff9b0a5026cac6d4baf52ef27113811c95164ff8e598c208039e290d164848613743c87c36c62b8be1d5d2664f213dc105a499d05bd9"
1263
+ "content": "3c788378f09de422662149274074b336a307112b4240e3fa51f312defaa3d65b9d60c88d307de9d5993125cd12cd14e3c4d3ebca82461b2482495bfc6f6598bc"
1264
1264
  }
1265
1265
  ]
1266
1266
  },
@@ -1391,11 +1391,11 @@
1391
1391
  "hashes": [
1392
1392
  {
1393
1393
  "alg": "SHA-256",
1394
- "content": "b24f79ea67461c11ecc1c5bc1c7479da943834b3877f2a99b5c842c2d9cd3b1d"
1394
+ "content": "37f52c31384de08cf405d38333ca3cbd78f82c1ec6f45573ef2593a2244dd0f6"
1395
1395
  },
1396
1396
  {
1397
1397
  "alg": "SHA3-512",
1398
- "content": "12a47e5823c3511d6b5d82fe6ac476872e0a21d7c5d8fd4840725fe372103825fc088e658327b59951bcd6ab411e0e867324b4e617b073579724b6251dfb50c1"
1398
+ "content": "0b2be40b780436b801fbb702cb62d0272e69f219adfd27d9af39fbc5ef8dc4c4842d2ec63ffe39958c9fbe26c37fa7c8502ea0478261049c4699de227d31d73c"
1399
1399
  }
1400
1400
  ]
1401
1401
  },
@@ -1466,11 +1466,11 @@
1466
1466
  "hashes": [
1467
1467
  {
1468
1468
  "alg": "SHA-256",
1469
- "content": "0a424fbd9a67fa8625e22cdf5a6b32778d43b0d60d07a14f1329f5cc8a8e1531"
1469
+ "content": "22ce7d7579a0789404896389c5d20b248e4cb35aa8a318954ec73c10ca95b95c"
1470
1470
  },
1471
1471
  {
1472
1472
  "alg": "SHA3-512",
1473
- "content": "42c37314e6f240c20852c8e7a8c37cab7d1dda5cf2478903fa3deee91621961bd646713e07cf5252c381b6a285a06d2ac9e758baf1f95b883815ec4f057cd50e"
1473
+ "content": "2d316c8286fad5cf33cb99973f3a80057414c4ccd06a54262df49cc367e2e84bc69891557cea6ab34a478476c0a597329fcd2024044582a3a25c4b24619c748c"
1474
1474
  }
1475
1475
  ]
1476
1476
  },
@@ -1526,11 +1526,11 @@
1526
1526
  "hashes": [
1527
1527
  {
1528
1528
  "alg": "SHA-256",
1529
- "content": "5e0d01cb485c1c0a59831fbb6b3ab9edf728d246e01fca15e318fd5c5a18edc6"
1529
+ "content": "92799efe9d7c301960fc16540880bb239ff4c90eaa3015cfbc50219a16f3b9b0"
1530
1530
  },
1531
1531
  {
1532
1532
  "alg": "SHA3-512",
1533
- "content": "a7910a48e297da451cd1ebc8f74bc9abdb46edb5d96bab8eafd1df60729206175c7db1d5d0aea4b789045cda69e2bc243d19d4c5bd05b7bb431072a3e6fe2caa"
1533
+ "content": "43394aaa7d0d1b86d57fe3853215e5d5bb1b7d61056cff6427ed8b30633aafc6e0537bedc55e04ccf11048faf8867e2f3251a37b1d54873e2f9956a5ccef2d5a"
1534
1534
  }
1535
1535
  ]
1536
1536
  },
@@ -1601,11 +1601,11 @@
1601
1601
  "hashes": [
1602
1602
  {
1603
1603
  "alg": "SHA-256",
1604
- "content": "46319d7b3e28a527c9c8afea98c1c53e17e4655d763d717838514fa274fa00b5"
1604
+ "content": "dd01792b8233450ddf903449667192ff0f92ae56aa3b2c6b50fe4f1b3b6aedaf"
1605
1605
  },
1606
1606
  {
1607
1607
  "alg": "SHA3-512",
1608
- "content": "898d66635fc871f2e84d1160a381b78b74b0f07899104cba70268b544961f60ea9ea6c287a60f477b112805e01717451764ec7ba7fcd61266e7914d2060ff994"
1608
+ "content": "f22734817e47e96112b88fad776901a63a33fefe8c24a2be6cd90cf3e4ff4370882ab13d5ef484b54caf1ed1ab77ea0c07ce886c025fed0ba48ce475391191af"
1609
1609
  }
1610
1610
  ]
1611
1611
  },
@@ -1811,11 +1811,11 @@
1811
1811
  "hashes": [
1812
1812
  {
1813
1813
  "alg": "SHA-256",
1814
- "content": "206abe5fa863fa8d032ba256e8a8b5808f44e1b31dc6ceef48f08a783bcc6815"
1814
+ "content": "4288523bb4e258c6cbb6ac72b5e4f55436bc048421b2ca6c6d5cea8538e7aa35"
1815
1815
  },
1816
1816
  {
1817
1817
  "alg": "SHA3-512",
1818
- "content": "61836d3d62c94d91a8ca62c6d5ee413b187377ba1926b1222e578dae866e9d798f97d893c91588371d1bb7504972b8632c5153b36a72ac1e81b58dffebc4e3be"
1818
+ "content": "930c044d3ca02e2322008610d1d409725d72b71e8389408ef6fe31f87e9139bf3aae4d4b456a8ed592ca512e2c663fa753a97fca71bed2e349cab15f29123c25"
1819
1819
  }
1820
1820
  ]
1821
1821
  },
@@ -1901,11 +1901,11 @@
1901
1901
  "hashes": [
1902
1902
  {
1903
1903
  "alg": "SHA-256",
1904
- "content": "c1c37465664024760c34e6c236592ee071e5347d3fb736dba3a495fee6768126"
1904
+ "content": "d897efd048acdde209213af36857c5385c036a650096531408ca978590e38226"
1905
1905
  },
1906
1906
  {
1907
1907
  "alg": "SHA3-512",
1908
- "content": "6e3fd76d2644e148080a889f6b59702c55f55cbdf2be4c223e3b9fc0e705df0753f7fa3fd6cdbae36e41bcca0191d90da8ee0577dc6b26a5687254abd0122dfe"
1908
+ "content": "4ba393a76294e87ca9621d29b6e17f4902a38361ddc28d11362efb9d99ae4ebf1f67829818194f18f3b546102ca1cabad16f8b6ec81d022b9eb0f08136174b57"
1909
1909
  }
1910
1910
  ]
1911
1911
  },
@@ -1931,11 +1931,11 @@
1931
1931
  "hashes": [
1932
1932
  {
1933
1933
  "alg": "SHA-256",
1934
- "content": "856217d2d89c7cd6f43414c3f4f8f0dc889842a394f145f90860bc05f2d8d7d9"
1934
+ "content": "f4d69adf7d3c4585b54345e646679dffd23ec2c1004a32f8434ee29039c6154e"
1935
1935
  },
1936
1936
  {
1937
1937
  "alg": "SHA3-512",
1938
- "content": "6458e83e9631d092e4bc729a8495aa0bcb1ba8f4662b7eedff47a16b79863ec2d159a613955b01230f372172dee3528abdb762ddf5f05190037bab1f7436228c"
1938
+ "content": "a7074f26bdb5a67350c73669c61621cbef6ad7e090414db275276477cb01b37ca78457129e33f8de657997abfcbee66f4f26eba43f489fbe6bfd92aa860c369f"
1939
1939
  }
1940
1940
  ]
1941
1941
  },
@@ -1991,11 +1991,11 @@
1991
1991
  "hashes": [
1992
1992
  {
1993
1993
  "alg": "SHA-256",
1994
- "content": "d6257d10d981772942cfc33603ae92ccfc3413f8ba370db0c23ed61f9abc7e26"
1994
+ "content": "ab8b918db00dfa5a913270dd2ece56cce6f14033952aaa00a7597c0e914eba79"
1995
1995
  },
1996
1996
  {
1997
1997
  "alg": "SHA3-512",
1998
- "content": "1b079ffd96fe58dd3fa1e5ecd2ed203d478b1892c12a3980cc6b882f3aa11c9c742fda746a35aef5c6a515b5d0d225b9ad51dc7520b7b1f3969e71e05534a20b"
1998
+ "content": "344075411acc757b4cd96d35b47daed95976d5e697476a87cea57631977c7e767164c7274165da6f12ab585c29566b7bf65eaa78918e7726433fe8af489e7229"
1999
1999
  }
2000
2000
  ]
2001
2001
  },
@@ -2156,11 +2156,11 @@
2156
2156
  "hashes": [
2157
2157
  {
2158
2158
  "alg": "SHA-256",
2159
- "content": "6305a2e2e2f414f53ed6095075c2410b8e59c6c22d014179682c12d5231415e2"
2159
+ "content": "f5fe53a465f45ee96d50993df753e5a89fd0320326ba136f7330f0009e1c7505"
2160
2160
  },
2161
2161
  {
2162
2162
  "alg": "SHA3-512",
2163
- "content": "fc82f0ba8ce4f1ca8098928ca15dae678c3a5ef44b6c99652bf45b017c7601b38974060a1809a00a2a72cea5ca49aad75be4afe43141b5ffa83de83e92c3b03e"
2163
+ "content": "6507475deb0094e4b02f4584d3bf769027a3b14eb036490fd6e2441b41710c3040b43e7c1a4aaebab3e89d22c8a3874a03a5fdaf9dcb875347efc1429128c468"
2164
2164
  }
2165
2165
  ]
2166
2166
  },
@@ -2261,11 +2261,11 @@
2261
2261
  "hashes": [
2262
2262
  {
2263
2263
  "alg": "SHA-256",
2264
- "content": "4980206044777812f2aec8bc141e87204420ed898ddcc8f0cfffc63df9b4a694"
2264
+ "content": "78022f325cce2a4941ea69bd0e2fc3080d6cc9fdb64d970ef97ac97e9938eae8"
2265
2265
  },
2266
2266
  {
2267
2267
  "alg": "SHA3-512",
2268
- "content": "b99cc93be85738aa542b7b8ebdb5a3543b8f30f3a9dad1991f3a4bba7b69c113c472456834c547ae25b36122bea12c9055ab13ba2dc80ad068b6b67d9557b493"
2268
+ "content": "2b09f565125e6466a9974ccc7472b2e927ed3e36bc1d0e3968292d7293bb27bc14e5fbef3af750b1fdf02353989217f710809221313e8e7d151daf54a9dff59c"
2269
2269
  }
2270
2270
  ]
2271
2271
  },
@@ -2351,11 +2351,11 @@
2351
2351
  "hashes": [
2352
2352
  {
2353
2353
  "alg": "SHA-256",
2354
- "content": "7ef5ed5d9a6dd68b2d43419d7fa2d0408eb8b57b69588e51db136aa0f29e82a6"
2354
+ "content": "c44c30392d352cae0c4ad8c7580671ed1a866a9646141934a260482068d4b53f"
2355
2355
  },
2356
2356
  {
2357
2357
  "alg": "SHA3-512",
2358
- "content": "b96dad01e1385bc17bb1fa54d2f0d86558063856c12401fca1d96a01869de984b87cf5c54ded025a5f93762c109385ef6aed5552abff36070e281810ae063db8"
2358
+ "content": "dfb113f3fffa19e51471a1d1ac494565aa38c95285cff82c46646666271cdcc174a0986c1a2d2f5e8c8d2acbd88027f709c3dccfa64d0ec07bcd0343d8c73bf1"
2359
2359
  }
2360
2360
  ]
2361
2361
  },
@@ -2366,11 +2366,11 @@
2366
2366
  "hashes": [
2367
2367
  {
2368
2368
  "alg": "SHA-256",
2369
- "content": "d44909884c4cf6dd112d740c1e0694c5be20daabbaa19ae5b49a3fbf477742ac"
2369
+ "content": "b6464c13d976e0bdfb9dff5214c0a8813e1cb0eb433ea12a5039f24e402e56be"
2370
2370
  },
2371
2371
  {
2372
2372
  "alg": "SHA3-512",
2373
- "content": "881ffec3e25bebe51be493d7a98e40ca875192acf78415f07d7e359d18dd029911629cae9baea6bdb1e43c49d33b4d368c47b752e93dceb7f6d31e1be2740036"
2373
+ "content": "19fff44e232d44dd7a81c6fe4ab50183407490676ac9eb708765dfce6488aa4270e24475161ae4d170a10b3959d8866196f2a335f9d7e052052ae950718a5c66"
2374
2374
  }
2375
2375
  ]
2376
2376
  },
@@ -2456,11 +2456,11 @@
2456
2456
  "hashes": [
2457
2457
  {
2458
2458
  "alg": "SHA-256",
2459
- "content": "0106f392e198441bea77d0c21bc8658cc86cd60b62efa8311c5d2a312b3d30c0"
2459
+ "content": "3669807579f07112810f80378e424ae0192d96814f66e8234644147465a0117d"
2460
2460
  },
2461
2461
  {
2462
2462
  "alg": "SHA3-512",
2463
- "content": "99055df006b6b6c8ddb3971ef956575f7f1e6c1102fe6a53e3c9da742c7ac0cad6732f34e38044df7befc4325b57c035e7db9882ca690cfb174afce041aa9866"
2463
+ "content": "c880ec5a6ca8bc733321eaa9b23d04409dd381b2f9681becb0862fd99a1f4dbc17407b0d28fc29b90a4ef95d9238b89ec3dcfb3f15ffed92049385de7d3a58a0"
2464
2464
  }
2465
2465
  ]
2466
2466
  },
@@ -2516,11 +2516,11 @@
2516
2516
  "hashes": [
2517
2517
  {
2518
2518
  "alg": "SHA-256",
2519
- "content": "ab7f0a6de25bedf91d74ef56db43fd901a31af3de837dcea4a0509b3308a4add"
2519
+ "content": "9a78740b83a08ed8e45be5fc2c0116e2d961f7b6e487b6432fabdad7935b3295"
2520
2520
  },
2521
2521
  {
2522
2522
  "alg": "SHA3-512",
2523
- "content": "59917cf7a44b8388a40738db4a4b07099512fa5642860b96f1d0797642639e84bbfb94fedbdb89a20a9f6e362380ac5471453a4226c098dd372472bbcd59faa3"
2523
+ "content": "c432b0c2a4040b47566b32f8315674af371bb6b187576ee8c820c80714e3d00a2493cd6c7566cdbb3755a974531e5ae1323220efcf777fee5fc3058c9bb67b85"
2524
2524
  }
2525
2525
  ]
2526
2526
  },
@@ -2651,11 +2651,11 @@
2651
2651
  "hashes": [
2652
2652
  {
2653
2653
  "alg": "SHA-256",
2654
- "content": "7a13c90f36da49436fbbae8162a1a408006964c2a6dba201aa1255296d3971fb"
2654
+ "content": "cadc1689a63b92fb24dd770f3f13d59343d2bac3b7e876a916742b353b6830e0"
2655
2655
  },
2656
2656
  {
2657
2657
  "alg": "SHA3-512",
2658
- "content": "a08907ec81b09cf2bd2c088dbba91f93809db18ff80fdede94e86c757cf50286fc3220777e973763ddca141b4d1bd545b3e01813ee8b28a6709b18182bc207f3"
2658
+ "content": "d29a4290f0cbd415806f74835cd43f24af5a4baeb8c69af2484b99d32a8abe9b3c00d5f22ba583e727af0b6791acba6f3f46b36a7741d1b2f2b8ac2b00ffee3a"
2659
2659
  }
2660
2660
  ]
2661
2661
  },
@@ -81,11 +81,14 @@ function buildCweChains({ skills, cweCatalog, atlasTtps, cveCatalog, frameworkGa
81
81
  };
82
82
 
83
83
  // Related CVEs: walk evidence_cves on the framework_gaps that the
84
- // referencing skills cite. Inner join via the skill graph.
84
+ // referencing skills cite. Inner join via the skill graph. Skip draft
85
+ // (_draft) CVEs so this CWE half agrees with the CVE half (build-indexes.js)
86
+ // and the reverse-ref index — all three must reflect the same curated,
87
+ // operator-queryable truth.
85
88
  const relatedCves = new Set();
86
89
  for (const gap of accum.framework_gaps) {
87
90
  for (const ev of (frameworkGaps[gap]?.evidence_cves || [])) {
88
- if (cveCatalog[ev]) relatedCves.add(ev);
91
+ if (cveCatalog[ev] && cveCatalog[ev]._draft !== true) relatedCves.add(ev);
89
92
  }
90
93
  }
91
94
 
@@ -106,10 +106,18 @@ function firstChunkAfterHeader(body, headerRegex, maxChars = 600) {
106
106
  }
107
107
 
108
108
  function handoffTargets(body, allSkillNames, selfName) {
109
- // Look in the Hand-Off section; backtick-quoted skill names count as a target.
110
- const handoffStart = body.search(/^## Hand-?Off/m);
111
- if (handoffStart < 0) return [];
112
- const slice = body.slice(handoffStart);
109
+ // Look ONLY in the Hand-Off section; backtick-quoted skill names count as a
110
+ // target. Bound the scan to [Hand-Off header, next real H2) so unrelated
111
+ // later sections are not mis-attributed as hand-off targets. Header
112
+ // detection and the section boundary are both fence-aware (a `## ` line
113
+ // inside a ```...``` block is not a real H2).
114
+ const lines = body.split(/\r?\n/);
115
+ const h2 = findRealH2Indices(lines);
116
+ const handoffIdx = h2.find((i) => /^## Hand-?Off/.test(lines[i]));
117
+ if (handoffIdx == null) return [];
118
+ const nextH2 = h2.find((i) => i > handoffIdx);
119
+ const sectionEnd = nextH2 != null ? nextH2 : lines.length;
120
+ const slice = lines.slice(handoffIdx, sectionEnd).join("\n");
113
121
  const targets = new Set();
114
122
  for (const name of allSkillNames) {
115
123
  if (name === selfName) continue;
@@ -47,6 +47,7 @@ const UPSTREAM_TRIAGED = Object.freeze([ // keep-sorted
47
47
  "from-base64url-untrapped",
48
48
  "fs-path-from-operator-identifier-without-traversal-refusal",
49
49
  "gitleaks-entropy",
50
+ "hand-rolled-sql",
50
51
  "handrolled-buffer-collect",
51
52
  "handrolled-debounce",
52
53
  "hostname-compare-trailing-dot",
@@ -49,6 +49,7 @@ const VALID_ALLOW_CLASSES = Object.freeze({
49
49
  "bidi-codepoint-literal": true,
50
50
  "unsorted-marked-array": true,
51
51
  "misaligned-marked-run": true,
52
+ "hand-rolled-sql": true,
52
53
  });
53
54
 
54
55
  const EXCLUDE_DIRS = new Set([
@@ -486,6 +487,37 @@ function detectMisalignedMarkedRun(files) {
486
487
  return hits;
487
488
  }
488
489
 
490
+ // ---- hand-rolled SQL in a file that talks to a database ------------------
491
+ //
492
+ // exceptd ships no database today, so this is a FORWARD guard: the moment a
493
+ // file imports a SQL driver, any SQL STATEMENT (`"SELECT …"`) or CLAUSE built
494
+ // by string concatenation (`… + " WHERE " + …`) is a parameterization/injection
495
+ // sink and must use the driver's bound-parameter API instead. The SQL-driver
496
+ // import is the gate — a SQL-looking string in a file with no driver executes
497
+ // nothing, so prose that merely begins with "Update …" / "Delete …" in a
498
+ // non-DB file is never scanned (no false positives). A trusted static DDL
499
+ // string can opt out with `// allow:hand-rolled-sql — <reason>`.
500
+ const SQL_DRIVER_IMPORT = /require\(\s*["'](?:node:sqlite|better-sqlite3|sqlite3|sqlite|pg|mysql2?|knex|sequelize|drizzle-orm|postgres|@libsql\/[\w.-]+)(?:\/[^"']*)?["']\s*\)|\bfrom\s+["'](?:node:sqlite|better-sqlite3|pg|mysql2?|knex|sequelize|drizzle-orm)(?:\/[^"']*)?["']/;
501
+ const SQL_STMT_START = /(["'`])\s*(?:SELECT\b|INSERT\s+(?:INTO|OR)\b|REPLACE\s+INTO\b|UPDATE\s+["'`]?[A-Za-z_]|DELETE\s+FROM\b|CREATE\s+(?:TABLE|UNIQUE\s+INDEX|INDEX|TRIGGER|VIRTUAL\s+TABLE)\b|ALTER\s+TABLE\b|DROP\s+(?:TABLE|TRIGGER|INDEX)\b|MERGE\s+INTO\b)/i;
502
+ // Trailing-concat form tolerates an embedded SQL string-quote inside the
503
+ // clause (e.g. `" WHERE name = 'x' " + id`) by scanning to the first `+`
504
+ // concatenation operator rather than requiring the clause string to close on
505
+ // the same quote with no interior quotes.
506
+ const SQL_CLAUSE_FRAG = /(?:\+\s*["'`]\s*(?:SET|FROM|WHERE|VALUES|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|LIMIT|OFFSET|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b|["'`]\s*(?:SET|FROM|WHERE|VALUES\s*\(|ORDER\s+BY|GROUP\s+BY|HAVING|RETURNING|ON\s+CONFLICT|(?:INNER\s+|LEFT\s+|RIGHT\s+|CROSS\s+)?JOIN)\b[^+]*\+)/i;
507
+ function detectHandRolledSql(files) {
508
+ const hits = [];
509
+ for (const rel of (files || filesUnder(["lib", "orchestrator", "bin/exceptd.js", "scripts"]))) {
510
+ const lines = readLines(rel);
511
+ if (!SQL_DRIVER_IMPORT.test(lines.join("\n"))) continue; // not a DB file — never scan prose
512
+ for (let i = 0; i < lines.length; i++) {
513
+ const code = stripLineComment(lines[i]);
514
+ if (SQL_STMT_START.test(code)) { hits.push({ file: rel, line: i + 1, content: lines[i].trim().slice(0, 110), why: "SQL statement in a string literal" }); continue; }
515
+ if (SQL_CLAUSE_FRAG.test(code)) { hits.push({ file: rel, line: i + 1, content: lines[i].trim().slice(0, 110), why: "SQL clause built by concatenation" }); }
516
+ }
517
+ }
518
+ return filterMarkers(hits, "hand-rolled-sql");
519
+ }
520
+
489
521
  const CLASSES = [
490
522
  {
491
523
  id: "process-exit-after-stdout-write",
@@ -523,6 +555,12 @@ const CLASSES = [
523
555
  warnOnly: false,
524
556
  hint: "a typo'd or reason-less `// allow:<class>` suppresses nothing — fix the class id or add `— <reason>`",
525
557
  },
558
+ {
559
+ id: "hand-rolled-sql",
560
+ run: detectHandRolledSql,
561
+ warnOnly: false,
562
+ hint: "a SQL statement/clause assembled as a string in a file that imports a SQL driver is an injection sink — use bound parameters, or `// allow:hand-rolled-sql — <reason>` for a trusted static DDL string",
563
+ },
526
564
  ];
527
565
 
528
566
  function main() {
@@ -559,6 +597,10 @@ module.exports = {
559
597
  detectOrphanAllowClass,
560
598
  detectUnsortedMarkedArray,
561
599
  detectMisalignedMarkedRun,
600
+ detectHandRolledSql,
601
+ SQL_DRIVER_IMPORT,
602
+ SQL_STMT_START,
603
+ SQL_CLAUSE_FRAG,
562
604
  scanUnsortedMarkedArray,
563
605
  scanMisalignedMarkedRun,
564
606
  requireMainRanges,
@@ -54,8 +54,17 @@ function deriveSubjects() {
54
54
  if (!["lib", "scripts", "orchestrator", "bin"].includes(parent)) add(parent + "-" + base, "alias:" + rel);
55
55
  const txt = read(rel);
56
56
  for (const m of txt.matchAll(/(?:^|\n)\s*(?:async\s+)?(?:function|class)\s+([A-Za-z_$][\w$]*)/g)) add(camelKebab(m[1]), "fn:" + rel);
57
- const exp = txt.match(/module\.exports\s*=\s*\{([\s\S]*?)\}/);
58
- if (exp) for (const k of exp[1].matchAll(/([A-Za-z_$][\w$]*)\s*[,:}\n]/g)) add(camelKebab(k[1]), "fn:" + rel);
57
+ // Capture the FULL module.exports object via a brace-balanced scan. A
58
+ // non-greedy /\{([\s\S]*?)\}/ stops at the first nested `}` and drops
59
+ // every export name after it (the brace-truncation class), under-deriving
60
+ // subjects so a real export silently has no required test.
61
+ const expAt = txt.search(/module\.exports\s*=\s*\{/);
62
+ if (expAt >= 0) {
63
+ const open = txt.indexOf("{", expAt);
64
+ let depth = 0, end = -1;
65
+ for (let k = open; k < txt.length; k++) { const ch = txt[k]; if (ch === "{") depth++; else if (ch === "}" && --depth === 0) { end = k; break; } }
66
+ if (end > open) for (const m of txt.slice(open + 1, end).matchAll(/([A-Za-z_$][\w$]*)\s*[,:}\n]/g)) add(camelKebab(m[1]), "fn:" + rel);
67
+ }
59
68
  }
60
69
  }
61
70
  ["lib", "orchestrator", "scripts", "bin", "sources/validators"].forEach(walkSrc);
@@ -74,7 +83,14 @@ function deriveSubjects() {
74
83
  // data catalog files
75
84
  for (const e of ls("data")) if (e.isFile() && e.name.endsWith(".json")) add(e.name.replace(/\.json$/, ""), "data");
76
85
  // data ENTRY primitives: every CVE id + every playbook
77
- try { const cat = JSON.parse(read("data/cve-catalog.json")); for (const k of Object.keys(cat)) if (k !== "_meta") add(k.toLowerCase(), "cve-primitive"); } catch {}
86
+ // CVE-primitive subjects. An unreadable / malformed / empty catalog must NOT
87
+ // silently derive zero CVE subjects — that would let the reverse-coverage gate
88
+ // PASS with no CVE coverage at all (the absent-input false-pass class). Fail
89
+ // loud instead.
90
+ let cveDerived = 0;
91
+ try { const cat = JSON.parse(read("data/cve-catalog.json")); for (const k of Object.keys(cat)) if (k !== "_meta") { add(k.toLowerCase(), "cve-primitive"); cveDerived++; } }
92
+ catch (e) { throw new Error("check-test-subjects: cannot read/parse data/cve-catalog.json — refusing to derive subjects (reverse coverage would falsely pass with no CVE coverage): " + e.message); }
93
+ if (cveDerived === 0) throw new Error("check-test-subjects: data/cve-catalog.json yielded zero CVE entries — refusing to let reverse coverage pass with no CVE coverage.");
78
94
  for (const e of ls("data/playbooks")) if (e.isFile() && e.name.endsWith(".json")) { const b = e.name.replace(/\.json$/, ""); add(b, "playbook-primitive"); add("playbook-" + b, "alias:playbook"); }
79
95
  // workflows
80
96
  for (const e of ls(".github/workflows")) if (/\.ya?ml$/.test(e.name)) { const b = e.name.replace(/\.ya?ml$/, ""); add(b, "workflow"); add(b + "-workflow", "workflow"); }
@@ -111,14 +127,19 @@ function run() {
111
127
  for (const t of testFiles) if (!subjects.has(t.toLowerCase())) forward.push({ file: "tests/" + t + ".test.js", suggested: suggest(t) });
112
128
  const reverse = [];
113
129
  for (const [s, kind] of subjects) if (!testSet.has(s)) reverse.push({ subject: s, kind });
114
- return { subjects: subjects.size, forward, reverse };
130
+ // Reverse-REQUIRED subset: only module / cve / playbook subjects must have a
131
+ // test (aliases, data files, cli verbs, repo artifacts are valid targets but
132
+ // not reverse-required). The exit-code logic gates on THIS, consistently
133
+ // across --worklist and the human/predeploy paths.
134
+ const reverseRequired = reverse.filter((x) => x.kind.startsWith("module:") || x.kind.startsWith("cve-primitive") || x.kind.startsWith("playbook-primitive"));
135
+ return { subjects: subjects.size, forward, reverse, reverseRequired };
115
136
  }
116
137
 
117
138
  if (require.main === module) {
118
139
  const r = run();
119
- if (process.argv.includes("--worklist")) { process.stdout.write(JSON.stringify(r) + "\n"); process.exitCode = (r.forward.length || r.reverse.length) ? 1 : 0; }
140
+ const revMods = r.reverseRequired;
141
+ if (process.argv.includes("--worklist")) { process.stdout.write(JSON.stringify(r) + "\n"); process.exitCode = (r.forward.length || revMods.length) ? 1 : 0; }
120
142
  else {
121
- const revMods = r.reverse.filter((x) => x.kind.startsWith("module:") || x.kind.startsWith("cve-primitive") || x.kind.startsWith("playbook-primitive"));
122
143
  console.log(`[check-test-subjects] valid subjects=${r.subjects} | FORWARD violations=${r.forward.length} | REVERSE (module/cve/playbook) gaps=${revMods.length}`);
123
144
  if (r.forward.length || revMods.length) { console.log("[check-test-subjects] FAIL — run with --worklist for the full list."); process.exitCode = 1; }
124
145
  else console.log("[check-test-subjects] ok — every test maps to a subject and every subject has a test.");
@@ -335,16 +335,27 @@ function runGate(gate) {
335
335
  const ceil = typeof gate.informationalMaxExitCode === "number"
336
336
  ? gate.informationalMaxExitCode
337
337
  : Infinity;
338
- // A signal kill (spawnSync returns status:null with r.signal set — e.g. a
339
- // 137 OOM kill) is a crash, not an informational soft-signal. Without this,
340
- // an OOM-killed informational gate fell through to "informational" and the
341
- // release proceeded as if the gate had merely produced advisory output.
342
- if (r.signal || (r.status !== null && r.status > ceil)) {
338
+ // A spawn failure (spawnSync returns r.error set, status:null, signal:null —
339
+ // e.g. the gate command is missing / ENOENT / EACCES) is a crash, not an
340
+ // informational soft-signal. So is a signal kill (status:null with r.signal
341
+ // set — e.g. a 137 OOM kill) and a status that exceeds the soft-signal
342
+ // ceiling. Without surfacing the spawn-error case, an informational gate
343
+ // that never even ran fell through to "informational" and the release
344
+ // proceeded as if the gate had merely produced advisory output. The
345
+ // status===null && !signal case (no error object, but the process never
346
+ // produced an exit code) is treated the same way — a gate that did not
347
+ // exit cleanly cannot be classified as a soft signal.
348
+ const spawnFailed = !!r.error || (r.status === null && !r.signal);
349
+ if (r.error || r.signal || spawnFailed || (r.status !== null && r.status > ceil)) {
343
350
  return {
344
351
  status: "failed",
345
- exitCode: r.status,
346
- message: r.signal
352
+ exitCode: r.status ?? null,
353
+ message: r.error
354
+ ? `informational gate failed to spawn (treated as a crash): ${r.error.message}`
355
+ : r.signal
347
356
  ? `informational gate killed by signal ${r.signal} (treated as a crash)`
357
+ : r.status === null
358
+ ? `informational gate did not exit cleanly (no exit code, no signal) — treated as a crash`
348
359
  : `informational gate crashed (exit ${r.status} > informationalMaxExitCode=${ceil})`,
349
360
  durationMs,
350
361
  warnCount,
@@ -438,7 +449,7 @@ function main() {
438
449
  process.exit(failures.length > 0 ? 1 : 0); // allow:process-exit-after-stdout-write — local-only gate runner; output is the human/CI summary written synchronously above, never a piped --json result channel
439
450
  }
440
451
 
441
- module.exports = { GATES };
452
+ module.exports = { GATES, runGate };
442
453
 
443
454
  if (require.main === module) {
444
455
  try {
@@ -275,8 +275,13 @@ async function refreshRfc({ dry = false, _deps = {} } = {}) {
275
275
  const cur = cat[id];
276
276
  if (!cur) continue;
277
277
  let touched = false;
278
- if (cur._auto_imported && cur.status !== RFC_STATUS_MAP[e.status]) {
279
- cur.status = RFC_STATUS_MAP[e.status];
278
+ // Mirror the new-add path's `|| e.status` fallback: an upstream status
279
+ // outside RFC_STATUS_MAP must NOT write `undefined` (which would drop the
280
+ // status field on an existing curated row). Fall back to the raw upstream
281
+ // status, and only bump when the mapped value actually differs.
282
+ const mapped = RFC_STATUS_MAP[e.status] || e.status;
283
+ if (cur._auto_imported && mapped && cur.status !== mapped) {
284
+ cur.status = mapped;
280
285
  touched = true;
281
286
  statusBumped++;
282
287
  }
@@ -654,6 +659,15 @@ function backfillAtlas(cur, fresh) {
654
659
  if (!cur[key] && val) { cur[key] = val; touched = true; }
655
660
  }
656
661
  };
662
+ // Parity with backfillAttack: include the short description + tactic in the
663
+ // backfill set. Existing curated ATLAS rows often carry only {name} and need
664
+ // the short description + tactic too, not just description_full/platforms/etc.
665
+ fillIfEmpty("description", fresh.description);
666
+ // tactic: atlasEntryFromStix() emits a STRING for a single-tactic technique
667
+ // and an array for multi-tactic, so backfill both forms. fillIfEmpty only
668
+ // writes when cur is empty, so an existing (string OR array) tactic is never
669
+ // overwritten — the common single-tactic case is no longer left unhydrated.
670
+ fillIfEmpty("tactic", fresh.tactic);
657
671
  fillIfEmpty("description_full", fresh.description_full);
658
672
  fillIfEmpty("platforms", fresh.platforms);
659
673
  fillIfEmpty("detection", fresh.detection);
@@ -928,5 +942,8 @@ module.exports = {
928
942
  // Exported for regression tests: fetchUrl's status/redirect handling and
929
943
  // writeCatalog's atomicity are load-bearing fail-closed properties.
930
944
  fetchUrl,
931
- writeCatalog
945
+ writeCatalog,
946
+ // Exported for regression tests: backfillAtlas must mirror backfillAttack's
947
+ // description + array-tactic backfill on curated rows.
948
+ backfillAtlas
932
949
  };