@blamejs/exceptd-skills 0.19.1 → 0.19.2
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/AGENTS.md +3 -3
- package/CHANGELOG.md +11 -1
- package/CONTEXT.md +6 -6
- package/README.md +1 -1
- package/data/_indexes/_meta.json +15 -15
- package/data/_indexes/activity-feed.json +1 -1
- package/data/_indexes/catalog-summaries.json +1 -1
- package/data/_indexes/chains.json +175 -83
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/section-offsets.json +65 -65
- package/data/_indexes/token-budget.json +35 -35
- package/data/atlas-ttps.json +60 -16
- package/data/attack-techniques.json +1 -1
- package/data/cve-catalog.json +432 -324
- package/data/d3fend-catalog.json +5 -5
- package/data/dlp-controls.json +1 -1
- package/data/zeroday-lessons.json +2 -2
- package/manifest.json +58 -58
- package/package.json +2 -2
- package/sbom.cdx.json +75 -45
- package/scripts/check-epss-consistency.js +189 -0
- package/scripts/check-ttp-references.js +152 -0
- package/scripts/check-version-tags.js +50 -6
- package/scripts/predeploy.js +26 -0
- package/skills/defensive-countermeasure-mapping/skill.md +1 -1
- package/skills/incident-response-playbook/skill.md +5 -5
- package/skills/mlops-security/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +1 -1
- package/skills/ransomware-response/skill.md +1 -1
package/sbom.cdx.json
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.6",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:e541fe5e-916a-4fb4-90d8-936737db409e",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "
|
|
7
|
+
"timestamp": "2147-11-20T10:43:42.000Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "blamejs",
|
|
11
11
|
"name": "scripts/refresh-sbom.js",
|
|
12
|
-
"version": "0.19.
|
|
12
|
+
"version": "0.19.2"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"component": {
|
|
16
|
-
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.19.
|
|
16
|
+
"bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.19.2",
|
|
17
17
|
"type": "application",
|
|
18
18
|
"name": "@blamejs/exceptd-skills",
|
|
19
|
-
"version": "0.19.
|
|
20
|
-
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (1025 CVEs / 232 CWEs / 794 ATT&CK + ICS /
|
|
19
|
+
"version": "0.19.2",
|
|
20
|
+
"description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 51 skills, 11 catalogs (1025 CVEs / 232 CWEs / 794 ATT&CK + ICS / 174 ATLAS / 468 D3FEND / 8889 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
|
{
|
|
23
23
|
"license": {
|
|
@@ -25,17 +25,17 @@
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
|
-
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.19.
|
|
28
|
+
"purl": "pkg:npm/%40blamejs/exceptd-skills@0.19.2",
|
|
29
29
|
"hashes": [
|
|
30
30
|
{
|
|
31
31
|
"alg": "SHA-256",
|
|
32
|
-
"content": "
|
|
32
|
+
"content": "ccc66fa2356a4cadafc9dd29f405338bb7800da6f0069792280e6ba25d5b25cf"
|
|
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.19.
|
|
38
|
+
"url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.19.2"
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
"type": "vcs",
|
|
@@ -86,11 +86,11 @@
|
|
|
86
86
|
"hashes": [
|
|
87
87
|
{
|
|
88
88
|
"alg": "SHA-256",
|
|
89
|
-
"content": "
|
|
89
|
+
"content": "043939a747f3262127c7ab12a689af5fe81dce394059fdba502d37936f5c5c4b"
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
"alg": "SHA3-512",
|
|
93
|
-
"content": "
|
|
93
|
+
"content": "c72920fd325dcc0bf50b09b20b1832cd7a48ccc60be755ba5736eab61ceed714cd1686e9bdfc8dfc4b225f616b7474ad8d2aff2f6d6b89ef39bc38991e48fd1d"
|
|
94
94
|
}
|
|
95
95
|
]
|
|
96
96
|
},
|
|
@@ -116,11 +116,11 @@
|
|
|
116
116
|
"hashes": [
|
|
117
117
|
{
|
|
118
118
|
"alg": "SHA-256",
|
|
119
|
-
"content": "
|
|
119
|
+
"content": "ad80c0728a249b3888aa3f55c3d3bb710c8e583e15155a95689168612d2ff5d4"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"alg": "SHA3-512",
|
|
123
|
-
"content": "
|
|
123
|
+
"content": "630a8640fe48b549daaafe0a5d8aae24aab094e575db0e602b0849ffd8222d39f367f01a70e377f834416e7f0255f64871e3b90f2041b360b683490ed0145f22"
|
|
124
124
|
}
|
|
125
125
|
]
|
|
126
126
|
},
|
|
@@ -131,11 +131,11 @@
|
|
|
131
131
|
"hashes": [
|
|
132
132
|
{
|
|
133
133
|
"alg": "SHA-256",
|
|
134
|
-
"content": "
|
|
134
|
+
"content": "ce0272bf7bfeb46ec24f2852953b5fa1cd2409e58ee4d4afab31395c566df2c3"
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"alg": "SHA3-512",
|
|
138
|
-
"content": "
|
|
138
|
+
"content": "6f36bd5552bff87dcd3af1d6b38ae0a5be7cf3abc576033db7a2dcc978b3d2be9eeb98dabaf6c8d8fc1afdfb5860d595d0156e7edcebaf43b0c69fe87130d39d"
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
},
|
|
@@ -176,11 +176,11 @@
|
|
|
176
176
|
"hashes": [
|
|
177
177
|
{
|
|
178
178
|
"alg": "SHA-256",
|
|
179
|
-
"content": "
|
|
179
|
+
"content": "0fa9b167172f104810e5ee6040f11ed741217f7a51c2552a15c69d2a15a164aa"
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"alg": "SHA3-512",
|
|
183
|
-
"content": "
|
|
183
|
+
"content": "952e6b7436e34fc28df676c6eb81f73fe62f0c702c634acf154f2bb36145ffbae9655ab36360b99c17f9aa5b5822ff157cb0ecb8ce6151973979587c493b61c1"
|
|
184
184
|
}
|
|
185
185
|
]
|
|
186
186
|
},
|
|
@@ -296,11 +296,11 @@
|
|
|
296
296
|
"hashes": [
|
|
297
297
|
{
|
|
298
298
|
"alg": "SHA-256",
|
|
299
|
-
"content": "
|
|
299
|
+
"content": "a14089671efcb4f06dd0f82d688fe49276c1370e167e4483d76e9c0a40ef41eb"
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
"alg": "SHA3-512",
|
|
303
|
-
"content": "
|
|
303
|
+
"content": "b11841d54798d111d3124125a5adc0627244f0c363ae686b668ee9afc8b387d8d334f45b0c02f07b8eab14a9893456bfb16bc4b0dc81f0e5d2f26c46b88ac962"
|
|
304
304
|
}
|
|
305
305
|
]
|
|
306
306
|
},
|
|
@@ -311,11 +311,11 @@
|
|
|
311
311
|
"hashes": [
|
|
312
312
|
{
|
|
313
313
|
"alg": "SHA-256",
|
|
314
|
-
"content": "
|
|
314
|
+
"content": "6bc0619bcccceccc8d5c12aefa6c11197c625e5d7313e70474f2831e271b23f0"
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"alg": "SHA3-512",
|
|
318
|
-
"content": "
|
|
318
|
+
"content": "9a17e810705eb143ee311305c15b6b40255b5bd29e56e37af5ecfda6e6d39288b450d00fe76bd451a8822951fe334cb468da145f69e7ce98865f532e5aeb0a35"
|
|
319
319
|
}
|
|
320
320
|
]
|
|
321
321
|
},
|
|
@@ -326,11 +326,11 @@
|
|
|
326
326
|
"hashes": [
|
|
327
327
|
{
|
|
328
328
|
"alg": "SHA-256",
|
|
329
|
-
"content": "
|
|
329
|
+
"content": "0f440663a16dfffa241e173eb8ba8613c790015e2eb5f81a5a33ea88975ca1d5"
|
|
330
330
|
},
|
|
331
331
|
{
|
|
332
332
|
"alg": "SHA3-512",
|
|
333
|
-
"content": "
|
|
333
|
+
"content": "7ef98f11c8d421831592481693241618c7e16c8d8f44d528fd5b10de28b75555c6fa2bd2bceb8ec4f091f652655602ce716cceece2cf2683caf989a2295950c0"
|
|
334
334
|
}
|
|
335
335
|
]
|
|
336
336
|
},
|
|
@@ -356,11 +356,11 @@
|
|
|
356
356
|
"hashes": [
|
|
357
357
|
{
|
|
358
358
|
"alg": "SHA-256",
|
|
359
|
-
"content": "
|
|
359
|
+
"content": "9a4a356a1446d7db8be8b6684fa789802405a96cdaf769f06db4136bd07db6ac"
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
362
|
"alg": "SHA3-512",
|
|
363
|
-
"content": "
|
|
363
|
+
"content": "b60ab890bb6075800ddf571976fb2ef1a563d1f187c243ac3f29b96a3355c62c9ecca40e660742ff6bc1a7ba1a5b9374a05cca2d8b6dbacf7c224e050f6ef105"
|
|
364
364
|
}
|
|
365
365
|
]
|
|
366
366
|
},
|
|
@@ -371,11 +371,11 @@
|
|
|
371
371
|
"hashes": [
|
|
372
372
|
{
|
|
373
373
|
"alg": "SHA-256",
|
|
374
|
-
"content": "
|
|
374
|
+
"content": "791f6a46b19b0c0fa2f46630ac8b398afb7ec9c2f2019056828c8820fe255f19"
|
|
375
375
|
},
|
|
376
376
|
{
|
|
377
377
|
"alg": "SHA3-512",
|
|
378
|
-
"content": "
|
|
378
|
+
"content": "06c3aa8961bb08694c07ec65e791f0005d802385145ed53db92cc88301e2d339dfe33a3934509879dc70d1a9529028b50dcc44ff5c2ccbaa5d3302a639744228"
|
|
379
379
|
}
|
|
380
380
|
]
|
|
381
381
|
},
|
|
@@ -941,11 +941,11 @@
|
|
|
941
941
|
"hashes": [
|
|
942
942
|
{
|
|
943
943
|
"alg": "SHA-256",
|
|
944
|
-
"content": "
|
|
944
|
+
"content": "9bc4f85c15d1618ef438a9221346c6913f823c5b8368acc2b2d5756c3a07c21e"
|
|
945
945
|
},
|
|
946
946
|
{
|
|
947
947
|
"alg": "SHA3-512",
|
|
948
|
-
"content": "
|
|
948
|
+
"content": "43dac02cda23b8a73d857f474fe37e7754e61c57d5c882c06af239f6262fc5505b42181a88c44ebeaf5f5e79f0d73e01f39af27c418e92371193a06f2c22e220"
|
|
949
949
|
}
|
|
950
950
|
]
|
|
951
951
|
},
|
|
@@ -1946,11 +1946,11 @@
|
|
|
1946
1946
|
"hashes": [
|
|
1947
1947
|
{
|
|
1948
1948
|
"alg": "SHA-256",
|
|
1949
|
-
"content": "
|
|
1949
|
+
"content": "53564f4e897f2d56d4eba1ccd97e9d9cb942c8307221e1ac7a9946cc6c312ab1"
|
|
1950
1950
|
},
|
|
1951
1951
|
{
|
|
1952
1952
|
"alg": "SHA3-512",
|
|
1953
|
-
"content": "
|
|
1953
|
+
"content": "e1ff542b725df94d2c40d25b212f71dca8d6f9a555e2d8a97a7c543713c67d52a9f321153cfbd2cdbd5bf0f39247d4ebca54b100ac054ffe9bf97ef4491677e3"
|
|
1954
1954
|
}
|
|
1955
1955
|
]
|
|
1956
1956
|
},
|
|
@@ -2419,6 +2419,21 @@
|
|
|
2419
2419
|
}
|
|
2420
2420
|
]
|
|
2421
2421
|
},
|
|
2422
|
+
{
|
|
2423
|
+
"bom-ref": "file:scripts/check-epss-consistency.js",
|
|
2424
|
+
"type": "file",
|
|
2425
|
+
"name": "scripts/check-epss-consistency.js",
|
|
2426
|
+
"hashes": [
|
|
2427
|
+
{
|
|
2428
|
+
"alg": "SHA-256",
|
|
2429
|
+
"content": "6054e10941a8e397d3ec666f08e464fe4d1df1750165c0b59543d878a9474cfa"
|
|
2430
|
+
},
|
|
2431
|
+
{
|
|
2432
|
+
"alg": "SHA3-512",
|
|
2433
|
+
"content": "c787f5fb7be2bad1b62a87e90d1d69b78e6577d426e9a6d3b0f10d29035ccb2c75aeaad07fb4a019c74d4731f4570374e645a4c9884e3288c7b52d51c754952f"
|
|
2434
|
+
}
|
|
2435
|
+
]
|
|
2436
|
+
},
|
|
2422
2437
|
{
|
|
2423
2438
|
"bom-ref": "file:scripts/check-framework-gap-coverage.js",
|
|
2424
2439
|
"type": "file",
|
|
@@ -2524,6 +2539,21 @@
|
|
|
2524
2539
|
}
|
|
2525
2540
|
]
|
|
2526
2541
|
},
|
|
2542
|
+
{
|
|
2543
|
+
"bom-ref": "file:scripts/check-ttp-references.js",
|
|
2544
|
+
"type": "file",
|
|
2545
|
+
"name": "scripts/check-ttp-references.js",
|
|
2546
|
+
"hashes": [
|
|
2547
|
+
{
|
|
2548
|
+
"alg": "SHA-256",
|
|
2549
|
+
"content": "86861618b14f086180d32d123affe21be9d331ecc67b22fde17017f7d49aa054"
|
|
2550
|
+
},
|
|
2551
|
+
{
|
|
2552
|
+
"alg": "SHA3-512",
|
|
2553
|
+
"content": "d0bff495ccf1672f1c90bc9f8deeca57c1597c4c59d929c9a209985a140385c5ffe90e72833f607ccc18be97a7daa270d19a87e4591a3d36cdd008c3380b2fcc"
|
|
2554
|
+
}
|
|
2555
|
+
]
|
|
2556
|
+
},
|
|
2527
2557
|
{
|
|
2528
2558
|
"bom-ref": "file:scripts/check-ttp-upstream.js",
|
|
2529
2559
|
"type": "file",
|
|
@@ -2561,11 +2591,11 @@
|
|
|
2561
2591
|
"hashes": [
|
|
2562
2592
|
{
|
|
2563
2593
|
"alg": "SHA-256",
|
|
2564
|
-
"content": "
|
|
2594
|
+
"content": "bc7aa74003392c3d8693ca1c9ee7c38718d96caa97e50cedff65556c26f5ceb9"
|
|
2565
2595
|
},
|
|
2566
2596
|
{
|
|
2567
2597
|
"alg": "SHA3-512",
|
|
2568
|
-
"content": "
|
|
2598
|
+
"content": "320c1691433f81aa16761d04a20f271fd4054b5b17da2a5c2cb08d7146c8f6ad970c6cc2d7b295e2cc58f1a0abdccf6b1402d8b5b89b0ef029bdf2b01c7265c5"
|
|
2569
2599
|
}
|
|
2570
2600
|
]
|
|
2571
2601
|
},
|
|
@@ -2591,11 +2621,11 @@
|
|
|
2591
2621
|
"hashes": [
|
|
2592
2622
|
{
|
|
2593
2623
|
"alg": "SHA-256",
|
|
2594
|
-
"content": "
|
|
2624
|
+
"content": "b641f8f91c21ecda0bd35b647f1be19ca4564e4d5801467105122f262c13e973"
|
|
2595
2625
|
},
|
|
2596
2626
|
{
|
|
2597
2627
|
"alg": "SHA3-512",
|
|
2598
|
-
"content": "
|
|
2628
|
+
"content": "e98f5af374a1a40fc876d0d564a092ecbef2c9e93fb6653b6905c4b44332d12be78aa5ee4b550b4c1bad5270f2480cc862b02fac36b14266b74ec26abb391690"
|
|
2599
2629
|
}
|
|
2600
2630
|
]
|
|
2601
2631
|
},
|
|
@@ -3026,11 +3056,11 @@
|
|
|
3026
3056
|
"hashes": [
|
|
3027
3057
|
{
|
|
3028
3058
|
"alg": "SHA-256",
|
|
3029
|
-
"content": "
|
|
3059
|
+
"content": "e182f3130fe78e403db6c81c24757bc81dfbc86997e0b5f304f05d5572f7abb2"
|
|
3030
3060
|
},
|
|
3031
3061
|
{
|
|
3032
3062
|
"alg": "SHA3-512",
|
|
3033
|
-
"content": "
|
|
3063
|
+
"content": "4c8391b78968dddfa405d7e5a4d5bac7b30c156b7d86cc44a3d9f3ab3e5cbf80c350ce2c96968b6f76eabc7c4255d03e511092c55135cee4657a2a95c111013e"
|
|
3034
3064
|
}
|
|
3035
3065
|
]
|
|
3036
3066
|
},
|
|
@@ -3161,11 +3191,11 @@
|
|
|
3161
3191
|
"hashes": [
|
|
3162
3192
|
{
|
|
3163
3193
|
"alg": "SHA-256",
|
|
3164
|
-
"content": "
|
|
3194
|
+
"content": "5a4de978515f974cb88cb4dcfb69d02eefa7a0d29a22a2f8a732fb7f8dd5d107"
|
|
3165
3195
|
},
|
|
3166
3196
|
{
|
|
3167
3197
|
"alg": "SHA3-512",
|
|
3168
|
-
"content": "
|
|
3198
|
+
"content": "05b0c3def0635de0202bc93a54ff30acc9197e684d81c48c27cca5fa2299489ed410983d65268702fa3c467932ce89d33db3329c2b5d1788e0e5080244b1ed26"
|
|
3169
3199
|
}
|
|
3170
3200
|
]
|
|
3171
3201
|
},
|
|
@@ -3236,11 +3266,11 @@
|
|
|
3236
3266
|
"hashes": [
|
|
3237
3267
|
{
|
|
3238
3268
|
"alg": "SHA-256",
|
|
3239
|
-
"content": "
|
|
3269
|
+
"content": "156965bdf43f8d3973ac9bc95f71d39d36a8c1a72e4833eb83faee3d4e5168eb"
|
|
3240
3270
|
},
|
|
3241
3271
|
{
|
|
3242
3272
|
"alg": "SHA3-512",
|
|
3243
|
-
"content": "
|
|
3273
|
+
"content": "7381a02c760258e16513bbb3c9d7210130f49232b366b4e70e3e66d39b0a8258d72bff2410edb9fa2aae000cc66506ecda8a70fa05a136b5cf1c905a15a81911"
|
|
3244
3274
|
}
|
|
3245
3275
|
]
|
|
3246
3276
|
},
|
|
@@ -3341,11 +3371,11 @@
|
|
|
3341
3371
|
"hashes": [
|
|
3342
3372
|
{
|
|
3343
3373
|
"alg": "SHA-256",
|
|
3344
|
-
"content": "
|
|
3374
|
+
"content": "360caa6c03913625214e4f2f8430e7306f64ce5f44e8cc3d52af1368c7d8a6b4"
|
|
3345
3375
|
},
|
|
3346
3376
|
{
|
|
3347
3377
|
"alg": "SHA3-512",
|
|
3348
|
-
"content": "
|
|
3378
|
+
"content": "4e1a8cee480cc389a3eb56ce3773b645f4d09353cfc34f65aeecc84c18aae0ccc15f6ce4665c39306b7cdcf638413d240960b641a92146ea270189ed07f63282"
|
|
3349
3379
|
}
|
|
3350
3380
|
]
|
|
3351
3381
|
},
|
|
@@ -3356,11 +3386,11 @@
|
|
|
3356
3386
|
"hashes": [
|
|
3357
3387
|
{
|
|
3358
3388
|
"alg": "SHA-256",
|
|
3359
|
-
"content": "
|
|
3389
|
+
"content": "ed9855c8ce1fb2830f1cf69a4554bf7600964f32fd75898b41f8a23003ae9a6d"
|
|
3360
3390
|
},
|
|
3361
3391
|
{
|
|
3362
3392
|
"alg": "SHA3-512",
|
|
3363
|
-
"content": "
|
|
3393
|
+
"content": "3eb5148995be926186f2ec4f61eb3b5ccb64645c0d513d74ed780ed7ed356a299632059285484a37399d64dc3f68d1d44c44ccd113e480b8d3fe2b95790b509d"
|
|
3364
3394
|
}
|
|
3365
3395
|
]
|
|
3366
3396
|
},
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* EPSS score/percentile consistency gate.
|
|
6
|
+
*
|
|
7
|
+
* EPSS publishes a score and a percentile for every CVE, refreshed daily. The
|
|
8
|
+
* percentile is the score's RANK within that day's publication: if A scores
|
|
9
|
+
* higher than B on a given day, A's percentile is at least B's. That makes the
|
|
10
|
+
* pair self-checking with no network access — sort a day's entries by score and
|
|
11
|
+
* the percentiles must come out sorted too.
|
|
12
|
+
*
|
|
13
|
+
* The failure this catches is updating one field without the other. A refresh
|
|
14
|
+
* that writes a new score but keeps yesterday's percentile leaves a plausible
|
|
15
|
+
* entry: both numbers are in range, both look like EPSS values, and nothing
|
|
16
|
+
* downstream can tell they describe different days. The result is a CVE ranked
|
|
17
|
+
* as top-decile urgency on a score that no longer supports it, which is exactly
|
|
18
|
+
* the input the prioritisation model trusts most.
|
|
19
|
+
*
|
|
20
|
+
* What the ordering check does and does not establish. Monotonicity is a
|
|
21
|
+
* necessary condition, not a sufficient one: it catches a stale percentile that
|
|
22
|
+
* contradicts the new score's rank, which is what a partial refresh usually
|
|
23
|
+
* produces, but a stale value that happens to preserve the ordering passes.
|
|
24
|
+
* Proving two fields share a publication would require the fetched row, and
|
|
25
|
+
* that is not recoverable from the catalog offline. So this is a corruption
|
|
26
|
+
* detector, not a provenance proof — the provenance guarantee has to come from
|
|
27
|
+
* the write side, by fetching and writing score, percentile and date together.
|
|
28
|
+
*
|
|
29
|
+
* Three checks run:
|
|
30
|
+
* - within each epss_date, sorting by score must sort by percentile;
|
|
31
|
+
* - an entry carries both numeric fields or neither;
|
|
32
|
+
* - epss_note, which is derived text, must restate the current fields.
|
|
33
|
+
*
|
|
34
|
+
* Entries are grouped by epss_date so each publication is checked against
|
|
35
|
+
* itself; comparing across days is meaningless because the whole distribution
|
|
36
|
+
* shifts.
|
|
37
|
+
*
|
|
38
|
+
* Exit codes: 0 consistent, 1 inconsistent.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require("node:fs");
|
|
42
|
+
const path = require("node:path");
|
|
43
|
+
|
|
44
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Percentiles are published rounded to five decimals, so two scores that are
|
|
48
|
+
* adjacent in the ranking can round to percentiles that invert by a hair. That
|
|
49
|
+
* is arithmetic, not drift. Real mismatches are pairs pulled from different
|
|
50
|
+
* days, which land orders of magnitude above this: the widest rounding artifact
|
|
51
|
+
* observed in the catalog is 8e-5, while a stale-field mismatch runs 0.05-0.9.
|
|
52
|
+
* The threshold sits between the two, far enough from each that neither
|
|
53
|
+
* classification is a close call.
|
|
54
|
+
*/
|
|
55
|
+
const ROUNDING_TOLERANCE = 1e-3;
|
|
56
|
+
|
|
57
|
+
function loadCatalog(file) {
|
|
58
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ordinal(n) {
|
|
62
|
+
const suffixes = ["th", "st", "nd", "rd"];
|
|
63
|
+
const v = n % 100;
|
|
64
|
+
return `${n}${suffixes[(v - 20) % 10] || suffixes[v] || suffixes[0]}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The operator-readable restatement of an entry's EPSS fields. Entries carry it
|
|
69
|
+
* as `epss_note`, and it is derived, not authored — so it must be rebuilt
|
|
70
|
+
* whenever the numbers move.
|
|
71
|
+
*/
|
|
72
|
+
function renderNote(entry) {
|
|
73
|
+
const pct = Math.round(entry.epss_percentile * 100);
|
|
74
|
+
return `FIRST EPSS ${entry.epss_score} (${ordinal(pct)} percentile) as of ${entry.epss_date}.`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Group entries by publication date. Entries missing either field are reported
|
|
79
|
+
* separately rather than skipped — a half-populated pair is its own defect, and
|
|
80
|
+
* silently ignoring it would let the gate pass on the very shape it exists to
|
|
81
|
+
* catch.
|
|
82
|
+
*/
|
|
83
|
+
function cohorts(catalog) {
|
|
84
|
+
const groups = new Map();
|
|
85
|
+
const incomplete = [];
|
|
86
|
+
|
|
87
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
88
|
+
const hasScore = typeof entry.epss_score === "number";
|
|
89
|
+
const hasPercentile = typeof entry.epss_percentile === "number";
|
|
90
|
+
|
|
91
|
+
if (!hasScore && !hasPercentile) continue;
|
|
92
|
+
if (!hasScore || !hasPercentile) {
|
|
93
|
+
incomplete.push({
|
|
94
|
+
id,
|
|
95
|
+
missing: hasScore ? "epss_percentile" : "epss_score",
|
|
96
|
+
});
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const date = entry.epss_date || "(no epss_date)";
|
|
101
|
+
if (!groups.has(date)) groups.set(date, []);
|
|
102
|
+
groups.get(date).push({ id, score: entry.epss_score, percentile: entry.epss_percentile });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { groups, incomplete };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Sort a cohort by score and report every place the percentile goes backwards
|
|
110
|
+
* by more than rounding can explain.
|
|
111
|
+
*/
|
|
112
|
+
function inversions(rows) {
|
|
113
|
+
const sorted = [...rows].sort((a, b) => a.score - b.score);
|
|
114
|
+
const found = [];
|
|
115
|
+
|
|
116
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
117
|
+
const prev = sorted[i - 1];
|
|
118
|
+
const cur = sorted[i];
|
|
119
|
+
const delta = prev.percentile - cur.percentile;
|
|
120
|
+
if (delta > ROUNDING_TOLERANCE) {
|
|
121
|
+
found.push({ lower: cur, higher: prev, delta });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return found;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function check(catalogFile) {
|
|
129
|
+
const catalog = loadCatalog(catalogFile);
|
|
130
|
+
const { groups, incomplete } = cohorts(catalog);
|
|
131
|
+
const failures = [];
|
|
132
|
+
|
|
133
|
+
for (const [date, rows] of groups) {
|
|
134
|
+
for (const bad of inversions(rows)) {
|
|
135
|
+
failures.push(
|
|
136
|
+
`${date}: ${bad.lower.id} scores ${bad.lower.score} but ranks ` +
|
|
137
|
+
`${bad.lower.percentile}, below ${bad.higher.id} which scores ` +
|
|
138
|
+
`${bad.higher.score} at ${bad.higher.percentile} ` +
|
|
139
|
+
`(gap ${bad.delta.toFixed(5)}) — the two fields are from different publications`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const row of incomplete) {
|
|
145
|
+
failures.push(`${row.id}: has one EPSS field but not ${row.missing} — write both together or neither`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The prose restatement drifts the same way the percentile does: a refresh
|
|
149
|
+
// moves the numbers and leaves the sentence describing the previous
|
|
150
|
+
// publication, so the entry states two different scores as of two different
|
|
151
|
+
// dates. It is derived text, so it can be checked exactly.
|
|
152
|
+
for (const [id, entry] of Object.entries(catalog)) {
|
|
153
|
+
if (typeof entry.epss_note !== "string") continue;
|
|
154
|
+
if (typeof entry.epss_score !== "number" || typeof entry.epss_percentile !== "number") continue;
|
|
155
|
+
const expected = renderNote(entry);
|
|
156
|
+
if (entry.epss_note !== expected) {
|
|
157
|
+
failures.push(
|
|
158
|
+
`${id}: epss_note reads ${JSON.stringify(entry.epss_note)} but the fields say ` +
|
|
159
|
+
`${JSON.stringify(expected)} — regenerate the note when the numbers move`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { failures, cohortCount: groups.size, entryCount: [...groups.values()].reduce((n, r) => n + r.length, 0) };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function main() {
|
|
168
|
+
const file = process.argv[2] || path.join(ROOT, "data", "cve-catalog.json");
|
|
169
|
+
const { failures, cohortCount, entryCount } = check(file);
|
|
170
|
+
|
|
171
|
+
if (failures.length) {
|
|
172
|
+
console.error("EPSS consistency: FAIL");
|
|
173
|
+
for (const f of failures) console.error(` ${f}`);
|
|
174
|
+
console.error(
|
|
175
|
+
`\n${failures.length} inconsistent ${failures.length === 1 ? "entry" : "entries"}. ` +
|
|
176
|
+
`Re-fetch score and percentile together from api.first.org and write them in one pass.`
|
|
177
|
+
);
|
|
178
|
+
process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(
|
|
183
|
+
`EPSS consistency: PASS — ${entryCount} entries across ${cohortCount} publication ${cohortCount === 1 ? "date" : "dates"}, every cohort ranks monotonically`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (require.main === module) main();
|
|
188
|
+
|
|
189
|
+
module.exports = { check, cohorts, inversions, renderNote, ROUNDING_TOLERANCE };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TTP reference-integrity gate.
|
|
6
|
+
*
|
|
7
|
+
* data/attack-techniques.json and data/atlas-ttps.json are the pinned copies of
|
|
8
|
+
* ATT&CK and ATLAS. Every other file that names a technique — countermeasure
|
|
9
|
+
* maps, DLP controls, playbooks, skill bodies, CVE entries — is referring INTO
|
|
10
|
+
* those two catalogs. This gate proves those references resolve.
|
|
11
|
+
*
|
|
12
|
+
* The failure it exists for: MITRE retires and renumbers techniques between
|
|
13
|
+
* releases. When a pin is bumped, the two source catalogs get remapped, but
|
|
14
|
+
* references living in other files are easy to miss — nothing dereferences them
|
|
15
|
+
* at runtime, so a stale id keeps rendering in operator output as if it were
|
|
16
|
+
* current. It points at a MITRE page that no longer resolves, and any control
|
|
17
|
+
* claiming to counter it is now mapped to nothing (AGENTS.md Hard Rule #4: no
|
|
18
|
+
* orphaned controls). A bumped pin left exactly this residue in the D3FEND and
|
|
19
|
+
* DLP maps, invisible to every other gate.
|
|
20
|
+
*
|
|
21
|
+
* The check is offline: it resolves references against the pinned catalogs, not
|
|
22
|
+
* against MITRE. That is deliberate — scripts/check-ttp-upstream.js is the
|
|
23
|
+
* network check that asks whether the PINS are current, and it cannot block a
|
|
24
|
+
* release because it needs connectivity. This one can block, because a
|
|
25
|
+
* reference that does not resolve against the pin we ship is broken no matter
|
|
26
|
+
* what upstream says.
|
|
27
|
+
*
|
|
28
|
+
* Exit codes: 0 all references resolve, 1 unresolved references found.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require("node:fs");
|
|
32
|
+
const path = require("node:path");
|
|
33
|
+
|
|
34
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ATT&CK ids are TNNNN[.NNN]; ATLAS ids are AML.TNNNN[.NNN]. The lookbehind
|
|
38
|
+
* matters: without it the ATT&CK alternative matches the "T0017" inside
|
|
39
|
+
* "AML.T0017" and reports a phantom bare-ATT&CK reference for every ATLAS id in
|
|
40
|
+
* the tree. It covers the hyphen as well, because ATLAS ids also appear in
|
|
41
|
+
* filenames, where the dot is not a legal separator.
|
|
42
|
+
*/
|
|
43
|
+
const TTP_PATTERN = /\bAML\.T\d{4}(?:\.\d{3})?\b|(?<!AML[.-])\bT\d{4}(?:\.\d{3})?\b/g;
|
|
44
|
+
|
|
45
|
+
/** Files whose ids are definitions, not references. */
|
|
46
|
+
const SOURCE_CATALOGS = ["data/attack-techniques.json", "data/atlas-ttps.json"];
|
|
47
|
+
|
|
48
|
+
const SEARCH_ROOTS = ["data", "playbooks", "skills", "lib", "orchestrator", "bin"];
|
|
49
|
+
const SEARCH_EXTS = new Set([".json", ".md", ".js"]);
|
|
50
|
+
const SKIP_DIRS = new Set(["node_modules", "_indexes", "vendor", ".git"]);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Tokens that match the pattern without being references to a technique.
|
|
54
|
+
* Every entry needs a reason: an unexplained allowlist is how a real stale id
|
|
55
|
+
* eventually gets parked here to make the gate green.
|
|
56
|
+
*/
|
|
57
|
+
const NOT_REFERENCES = [
|
|
58
|
+
{
|
|
59
|
+
id: "T1234",
|
|
60
|
+
file: "lib/gap-detectors.js",
|
|
61
|
+
why: "placeholder in a comment describing the reference-extraction pattern itself, not a claim about a technique",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function loadKnownIds() {
|
|
66
|
+
const known = new Set();
|
|
67
|
+
for (const rel of SOURCE_CATALOGS) {
|
|
68
|
+
const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, rel), "utf8"));
|
|
69
|
+
for (const key of Object.keys(catalog)) {
|
|
70
|
+
if (key !== "_meta") known.add(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return known;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function* walk(dir) {
|
|
77
|
+
let names;
|
|
78
|
+
try {
|
|
79
|
+
names = fs.readdirSync(dir);
|
|
80
|
+
} catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
85
|
+
const full = path.join(dir, name);
|
|
86
|
+
let stat;
|
|
87
|
+
try {
|
|
88
|
+
stat = fs.statSync(full);
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (stat.isDirectory()) {
|
|
93
|
+
yield* walk(full);
|
|
94
|
+
} else if (SEARCH_EXTS.has(path.extname(name))) {
|
|
95
|
+
yield full;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function allowed(id, rel) {
|
|
101
|
+
return NOT_REFERENCES.some((e) => e.id === id && e.file === rel);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scan(root = ROOT) {
|
|
105
|
+
const known = loadKnownIds();
|
|
106
|
+
const sources = new Set(SOURCE_CATALOGS);
|
|
107
|
+
const unresolved = new Map();
|
|
108
|
+
|
|
109
|
+
for (const dirName of SEARCH_ROOTS) {
|
|
110
|
+
const dir = path.join(root, dirName);
|
|
111
|
+
if (!fs.existsSync(dir)) continue;
|
|
112
|
+
|
|
113
|
+
for (const file of walk(dir)) {
|
|
114
|
+
const rel = path.relative(root, file).replace(/\\/g, "/");
|
|
115
|
+
if (sources.has(rel)) continue;
|
|
116
|
+
|
|
117
|
+
const text = fs.readFileSync(file, "utf8");
|
|
118
|
+
for (const id of text.match(TTP_PATTERN) || []) {
|
|
119
|
+
if (known.has(id) || allowed(id, rel)) continue;
|
|
120
|
+
if (!unresolved.has(id)) unresolved.set(id, new Set());
|
|
121
|
+
unresolved.get(id).add(rel);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { unresolved, knownCount: known.size };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function main() {
|
|
130
|
+
const { unresolved, knownCount } = scan();
|
|
131
|
+
|
|
132
|
+
if (unresolved.size) {
|
|
133
|
+
console.error("TTP reference integrity: FAIL");
|
|
134
|
+
for (const [id, files] of [...unresolved].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
135
|
+
console.error(` ${id} — not defined in the pinned catalogs`);
|
|
136
|
+
for (const f of [...files].sort()) console.error(` ${f}`);
|
|
137
|
+
}
|
|
138
|
+
console.error(
|
|
139
|
+
`\n${unresolved.size} unresolved ${unresolved.size === 1 ? "reference" : "references"}. ` +
|
|
140
|
+
`Either the id was retired upstream and these files still name it — repoint them at the ` +
|
|
141
|
+
`successor — or it is a technique the pinned catalogs do not carry yet, in which case import it.`
|
|
142
|
+
);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
console.log(`TTP reference integrity: PASS — every referenced technique resolves against the ${knownCount} pinned ids`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (require.main === module) main();
|
|
151
|
+
|
|
152
|
+
module.exports = { scan, TTP_PATTERN, NOT_REFERENCES, loadKnownIds };
|