@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/CHANGELOG.md +12 -0
- package/bin/exceptd.js +12 -5
- package/data/_indexes/_meta.json +2 -2
- package/lib/collectors/cred-stores.js +18 -9
- package/lib/collectors/secrets.js +4 -2
- package/lib/framework-gap.js +25 -14
- package/lib/playbook-runner.js +65 -2
- package/lib/rfc-cli.js +7 -2
- package/lib/scoring.js +40 -7
- package/lib/verify.js +35 -34
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +8 -0
- package/orchestrator/scanner.js +20 -0
- package/package.json +1 -1
- package/sbom.cdx.json +46 -46
- package/scripts/builders/cwe-chains.js +5 -2
- package/scripts/builders/summary-cards.js +12 -4
- package/scripts/check-codebase-patterns-currency.js +1 -0
- package/scripts/check-codebase-patterns.js +42 -0
- package/scripts/check-test-subjects.js +27 -6
- package/scripts/predeploy.js +19 -8
- package/scripts/refresh-upstream-catalogs.js +20 -3
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:e985db48-bf9b-413b-9f95-7ae7c5737091",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
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.
|
|
12
|
+
"version": "0.18.12"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.18.
|
|
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.
|
|
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.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.18.12",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
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.
|
|
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": "
|
|
119
|
+
"content": "e6261b4abe62c5fb438605be888655fea1311752fe6a8de44a88d67cc250c66b"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"alg": "SHA3-512",
|
|
123
|
-
"content": "
|
|
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": "
|
|
284
|
+
"content": "7c5e618e5bb2ca8544fba021c9dc278d0ed363a42e7285092ec9770056e7d84c"
|
|
285
285
|
},
|
|
286
286
|
{
|
|
287
287
|
"alg": "SHA3-512",
|
|
288
|
-
"content": "
|
|
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": "
|
|
1109
|
+
"content": "b1272fd27e4a13a9d69297dd8fde145eef8a0852c93c1a3172c68c6169fbe5a7"
|
|
1110
1110
|
},
|
|
1111
1111
|
{
|
|
1112
1112
|
"alg": "SHA3-512",
|
|
1113
|
-
"content": "
|
|
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": "
|
|
1259
|
+
"content": "8309fb4bac21f191e44453619b9ebe89e2a1aebe0f4db2d62110c4e85f2a2c8b"
|
|
1260
1260
|
},
|
|
1261
1261
|
{
|
|
1262
1262
|
"alg": "SHA3-512",
|
|
1263
|
-
"content": "
|
|
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": "
|
|
1394
|
+
"content": "37f52c31384de08cf405d38333ca3cbd78f82c1ec6f45573ef2593a2244dd0f6"
|
|
1395
1395
|
},
|
|
1396
1396
|
{
|
|
1397
1397
|
"alg": "SHA3-512",
|
|
1398
|
-
"content": "
|
|
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": "
|
|
1469
|
+
"content": "22ce7d7579a0789404896389c5d20b248e4cb35aa8a318954ec73c10ca95b95c"
|
|
1470
1470
|
},
|
|
1471
1471
|
{
|
|
1472
1472
|
"alg": "SHA3-512",
|
|
1473
|
-
"content": "
|
|
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": "
|
|
1529
|
+
"content": "92799efe9d7c301960fc16540880bb239ff4c90eaa3015cfbc50219a16f3b9b0"
|
|
1530
1530
|
},
|
|
1531
1531
|
{
|
|
1532
1532
|
"alg": "SHA3-512",
|
|
1533
|
-
"content": "
|
|
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": "
|
|
1604
|
+
"content": "dd01792b8233450ddf903449667192ff0f92ae56aa3b2c6b50fe4f1b3b6aedaf"
|
|
1605
1605
|
},
|
|
1606
1606
|
{
|
|
1607
1607
|
"alg": "SHA3-512",
|
|
1608
|
-
"content": "
|
|
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": "
|
|
1814
|
+
"content": "4288523bb4e258c6cbb6ac72b5e4f55436bc048421b2ca6c6d5cea8538e7aa35"
|
|
1815
1815
|
},
|
|
1816
1816
|
{
|
|
1817
1817
|
"alg": "SHA3-512",
|
|
1818
|
-
"content": "
|
|
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": "
|
|
1904
|
+
"content": "d897efd048acdde209213af36857c5385c036a650096531408ca978590e38226"
|
|
1905
1905
|
},
|
|
1906
1906
|
{
|
|
1907
1907
|
"alg": "SHA3-512",
|
|
1908
|
-
"content": "
|
|
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": "
|
|
1934
|
+
"content": "f4d69adf7d3c4585b54345e646679dffd23ec2c1004a32f8434ee29039c6154e"
|
|
1935
1935
|
},
|
|
1936
1936
|
{
|
|
1937
1937
|
"alg": "SHA3-512",
|
|
1938
|
-
"content": "
|
|
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": "
|
|
1994
|
+
"content": "ab8b918db00dfa5a913270dd2ece56cce6f14033952aaa00a7597c0e914eba79"
|
|
1995
1995
|
},
|
|
1996
1996
|
{
|
|
1997
1997
|
"alg": "SHA3-512",
|
|
1998
|
-
"content": "
|
|
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": "
|
|
2159
|
+
"content": "f5fe53a465f45ee96d50993df753e5a89fd0320326ba136f7330f0009e1c7505"
|
|
2160
2160
|
},
|
|
2161
2161
|
{
|
|
2162
2162
|
"alg": "SHA3-512",
|
|
2163
|
-
"content": "
|
|
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": "
|
|
2264
|
+
"content": "78022f325cce2a4941ea69bd0e2fc3080d6cc9fdb64d970ef97ac97e9938eae8"
|
|
2265
2265
|
},
|
|
2266
2266
|
{
|
|
2267
2267
|
"alg": "SHA3-512",
|
|
2268
|
-
"content": "
|
|
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": "
|
|
2354
|
+
"content": "c44c30392d352cae0c4ad8c7580671ed1a866a9646141934a260482068d4b53f"
|
|
2355
2355
|
},
|
|
2356
2356
|
{
|
|
2357
2357
|
"alg": "SHA3-512",
|
|
2358
|
-
"content": "
|
|
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": "
|
|
2369
|
+
"content": "b6464c13d976e0bdfb9dff5214c0a8813e1cb0eb433ea12a5039f24e402e56be"
|
|
2370
2370
|
},
|
|
2371
2371
|
{
|
|
2372
2372
|
"alg": "SHA3-512",
|
|
2373
|
-
"content": "
|
|
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": "
|
|
2459
|
+
"content": "3669807579f07112810f80378e424ae0192d96814f66e8234644147465a0117d"
|
|
2460
2460
|
},
|
|
2461
2461
|
{
|
|
2462
2462
|
"alg": "SHA3-512",
|
|
2463
|
-
"content": "
|
|
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": "
|
|
2519
|
+
"content": "9a78740b83a08ed8e45be5fc2c0116e2d961f7b6e487b6432fabdad7935b3295"
|
|
2520
2520
|
},
|
|
2521
2521
|
{
|
|
2522
2522
|
"alg": "SHA3-512",
|
|
2523
|
-
"content": "
|
|
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": "
|
|
2654
|
+
"content": "cadc1689a63b92fb24dd770f3f13d59343d2bac3b7e876a916742b353b6830e0"
|
|
2655
2655
|
},
|
|
2656
2656
|
{
|
|
2657
2657
|
"alg": "SHA3-512",
|
|
2658
|
-
"content": "
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.");
|
package/scripts/predeploy.js
CHANGED
|
@@ -335,16 +335,27 @@ function runGate(gate) {
|
|
|
335
335
|
const ceil = typeof gate.informationalMaxExitCode === "number"
|
|
336
336
|
? gate.informationalMaxExitCode
|
|
337
337
|
: Infinity;
|
|
338
|
-
// A
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
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.
|
|
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
|
-
|
|
279
|
-
|
|
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
|
};
|