@blamejs/exceptd-skills 0.19.2 → 0.19.4

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 CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.4 — 2026-08-08
4
+
5
+ `framework-gap` now reports the controls a CVE needs that no framework carries yet. The command already answered half the question — which existing controls are insufficient — and stopped there, so an operator learned which controls fall short without learning what belongs in their place. The catalog had recorded the answer for 302 CVEs since the zero-day learning loop began and no command read it, in either output mode. Each entry names the requirement, its evidence, and the specific framework controls it closes, so it attaches to the gaps listed above it rather than reading as free-standing advice. Both the human-readable report and `--json` carry it, and the summary line counts it.
6
+
7
+ Coverage is partial and visibly so: roughly three hundred of the catalog's CVEs carry these controls today, so many scenarios will report none. A free-text scenario reports none by construction — the requirements are recorded per CVE.
8
+
9
+ ## 0.19.3 — 2026-08-08
10
+
11
+ The SBOM's creation timestamp is a real date. It has to be reproducible, or the currency gate could never compare a committed artifact against a freshly generated one, and that reproducibility had been bought by folding the bundle hash into a date offset. The offset spanned 136 years, so the field routinely landed a century out — the shipped value read `2147-11-20`. It is now the release date the changelog declares for that version, which is both reproducible and true. Tooling that sorts, ages or expiry-checks SBOMs will read a sane value for the first time.
12
+
13
+ Two files that ship in every tarball are now hashed. npm includes `package.json` whether or not the file allowlist names it, and picks up `sources/README.md` on its own, so neither appeared in the inventory — including the one file that declares the executable entry point, the supported runtime floor and the dependency set. Both now carry SHA-256 and SHA3-512 entries and count toward the aggregate bundle digest, so a bundle verified against the SBOM covers 249 of the 268 files an operator receives rather than 247.
14
+
15
+ The SBOM says what it does not cover. The pre-computed index cache under `data/_indexes/` is deliberately excluded from per-file hashing — it is regenerated on demand, and pinning a hash would race any process that rebuilds it — but that reasoning existed only in a source comment, so an operator holding the artifact could not distinguish a partial inventory from a complete one. Two `exceptd:integrity:uncovered:*` properties now name the excluded prefix and the check that covers it instead. The publish gate additionally compares the real `npm pack` file list against the SBOM, so a file npm decides to ship that the allowlist never named fails a gate rather than shipping unhashed; the two files above are what it found.
16
+
17
+ Preparing a release no longer refreshes the test-count baseline before checking it. The refresh writes whatever count it observes, so a release that removed tests rebaselined downward and the shrinkage check — which runs later — then compared the new count against itself and passed. The check now runs first and stops the release on a drop; the refresh still follows it, so growth is still recorded.
18
+
19
+ The secret scanner's offline fallback moves to 8.30.1. It had been pinned to 8.21.2 while upstream reached 8.30.1, so an interruption to the release API would have scanned with a ruleset nine releases old and still reported success. The resolver now compares the two on every run and warns when they diverge, which is the part that was only ever a comment before.
20
+
21
+ The contributor Docker harness reports honestly. Checks that ask git what ships — line-ending coverage, tracked-file resolution — cannot answer in that image, because the repository directory is deliberately kept out of the build context so a contributor's history is never uploaded to a builder or written into an image layer. They were failing rather than saying so, which reads as a defect in the tree. They now skip with the reason stated, and the file-writing checks that were failing on directory ownership, plus the governance check that could not find files the image had excluded, work as intended. A skipped check is not a passing check: run the suite outside Docker, or read continuous integration, for the ones this image cannot cover. The ignore file previously claimed the repository directory was included when it was not, which is what made the gap look like an oversight rather than a decision.
22
+
3
23
  ## 0.19.2 — 2026-08-08
4
24
 
5
25
  EPSS data on 115 catalog entries is corrected. A refresh had updated an entry's score without its percentile — and on 70 entries written a score with no percentile at all — leaving the two fields drawn from different daily publications. Because the percentile is the score's rank within its publication, the result was entries ranked in the top decile on a score that no longer supported it, which is the input exploit-prioritisation leans on hardest. Scores and percentiles are now written together. A new gate rejects any publication whose percentiles do not track its scores, requires an entry to carry both numbers or neither, and checks that the human-readable EPSS note on an entry restates the values the entry actually holds — 76 notes described an earlier publication than the fields beside them. The ordering check is a corruption detector rather than a provenance proof: it catches a stale value that contradicts the new score's rank, which is what a partial refresh produces, but two fields cannot be proven to share a publication from the catalog alone.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-08-08T18:32:36.137Z",
3
+ "generated_at": "2026-08-08T22:55:59.109Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 64,
6
6
  "source_hashes": {
7
- "manifest.json": "53564f4e897f2d56d4eba1ccd97e9d9cb942c8307221e1ac7a9946cc6c312ab1",
7
+ "manifest.json": "4f92a89e4b15843aed7516645c38e3eaec48993fe3043f74b98e97a42f2d38b4",
8
8
  "README.md": "0fa9b167172f104810e5ee6040f11ed741217f7a51c2552a15c69d2a15a164aa",
9
9
  "data/atlas-ttps.json": "a14089671efcb4f06dd0f82d688fe49276c1370e167e4483d76e9c0a40ef41eb",
10
10
  "data/attack-techniques.json": "6bc0619bcccceccc8d5c12aefa6c11197c625e5d7313e70474f2831e271b23f0",
@@ -182,7 +182,11 @@ function lagScore(frameworkId, controlGaps, globalFrameworks) {
182
182
  * @param {string} threatScenario - Description of the threat or CVE ID
183
183
  * @param {object} controlGaps - Parsed framework-control-gaps.json
184
184
  * @param {object} cveCatalog - Parsed cve-catalog.json (optional)
185
- * @returns {{ frameworks: object, universal_gaps: object[], theater_risks: object[] }}
185
+ * @param {object} opts - { allFrameworks?: boolean, lessons?: object }
186
+ * opts.lessons is the parsed zeroday-lessons.json. Optional: callers
187
+ * that omit it get a report without the new-control section rather than
188
+ * an error, which keeps the existing call signature working.
189
+ * @returns {{ frameworks: object, universal_gaps: object[], new_control_requirements: object[], theater_risks: object[] }}
186
190
  */
187
191
  function gapReport(frameworkIds, threatScenario, controlGaps, cveCatalog = {}, opts = {}) {
188
192
  const scenario = threatScenario.toLowerCase();
@@ -280,6 +284,31 @@ function gapReport(frameworkIds, threatScenario, controlGaps, cveCatalog = {}, o
280
284
  // 8 matching gaps" (every framework's hits, pre-filter).
281
285
  const matchingGapCount = scopedGaps.length;
282
286
 
287
+ // Controls the zero-day lesson for this CVE says no framework carries yet.
288
+ //
289
+ // `frameworks[].gaps` covers the other half of the same question — existing
290
+ // controls that are insufficient — so a report that answered only that half
291
+ // told an operator which controls fall short without ever saying what to put
292
+ // in their place. The lessons catalog had recorded exactly that for 302 CVEs
293
+ // and nothing read the field, so the analysis existed and reached no one.
294
+ //
295
+ // Keyed by CVE, so a free-text scenario legitimately resolves to none.
296
+ const lessons = (opts && opts.lessons) || {};
297
+ const lessonEntry = Object.entries(lessons)
298
+ .find(([id]) => id.toLowerCase() === scenario);
299
+ const newControls = (lessonEntry && Array.isArray(lessonEntry[1].new_control_requirements))
300
+ ? lessonEntry[1].new_control_requirements.map(c => ({
301
+ id: c.id,
302
+ name: c.name,
303
+ requirement: c.description,
304
+ evidence: c.evidence,
305
+ // Which framework controls this is meant to close. Without it the
306
+ // control reads as free-floating advice rather than an answer to a
307
+ // named gap above.
308
+ closes: Array.isArray(c.gap_closes) ? c.gap_closes : [],
309
+ }))
310
+ : [];
311
+
283
312
  return {
284
313
  threat_scenario: threatScenario,
285
314
  frameworks: frameworkResults,
@@ -288,10 +317,12 @@ function gapReport(frameworkIds, threatScenario, controlGaps, cveCatalog = {}, o
288
317
  name: g.control_name,
289
318
  real_requirement: g.real_requirement
290
319
  })),
320
+ new_control_requirements: newControls,
291
321
  theater_risks: theaterRisks,
292
322
  summary: {
293
323
  total_gaps: matchingGapCount,
294
324
  universal_gaps: universalGaps.length,
325
+ new_control_requirements: newControls.length,
295
326
  theater_risk_controls: theaterRisks.length
296
327
  }
297
328
  };
@@ -106,6 +106,47 @@ function runNpmPack() {
106
106
  return first;
107
107
  }
108
108
 
109
+ /* SBOM coverage of the ACTUAL tarball.
110
+ *
111
+ * The SBOM's own completeness gate expands package.json `files`, but npm ships
112
+ * more than `files` names — package.json unconditionally, plus README files it
113
+ * collects itself. Those extras shipped unhashed and nothing noticed, because
114
+ * the generator and its checker both asked `files`: two sides agreeing on the
115
+ * same incomplete question. `npm pack` is the only authority on what actually
116
+ * ships, and this is the one gate holding its output, so the comparison belongs
117
+ * here.
118
+ *
119
+ * Exclusions are read from the SBOM's own declared properties instead of a
120
+ * second copy of the list, so a file can only be uncovered when the published
121
+ * artifact says out loud that it is. The single structural exception is the
122
+ * SBOM itself, which cannot contain its own hash.
123
+ */
124
+ function sbomCoverageIssues(filePaths, sbom) {
125
+ const props = (sbom && sbom.metadata && sbom.metadata.properties) || [];
126
+ const uncoveredProp = props.find((p) => p && p.name === "exceptd:integrity:uncovered:prefix");
127
+ const uncoveredPrefixes = uncoveredProp
128
+ ? String(uncoveredProp.value).split(",").map((s) => s.trim()).filter(Boolean)
129
+ : [];
130
+ const hashed = new Set(
131
+ ((sbom && sbom.components) || [])
132
+ .filter((c) => c && typeof c["bom-ref"] === "string" && c["bom-ref"].startsWith("file:"))
133
+ .map((c) => c.name)
134
+ );
135
+ return filePaths
136
+ .filter(
137
+ (p) =>
138
+ p !== "sbom.cdx.json" &&
139
+ !hashed.has(p) &&
140
+ !uncoveredPrefixes.some((u) => p.startsWith(u))
141
+ )
142
+ .map(
143
+ (p) =>
144
+ `shipped file has no SHA in sbom.cdx.json and is not a declared exclusion: ${p} — ` +
145
+ `add it to ALWAYS_SHIPPED in scripts/refresh-sbom.js (and its mirror in ` +
146
+ `scripts/check-sbom-currency.js), then \`npm run refresh-sbom\``
147
+ );
148
+ }
149
+
109
150
  function main() {
110
151
  const issues = [];
111
152
 
@@ -155,6 +196,13 @@ function main() {
155
196
  }
156
197
  }
157
198
 
199
+ try {
200
+ const sbom = JSON.parse(fs.readFileSync(ABS("sbom.cdx.json"), "utf8"));
201
+ issues.push(...sbomCoverageIssues(filePaths, sbom));
202
+ } catch (err) {
203
+ issues.push(`could not read sbom.cdx.json to check tarball coverage: ${err.message}`);
204
+ }
205
+
158
206
  // Size budget. An absent/non-numeric `size` field is NOT a pass — it means
159
207
  // the gate cannot evaluate the budget at all. Fail loudly instead of
160
208
  // silently skipping (the absent-field-fails-open class, symmetric with the
@@ -187,4 +235,4 @@ function main() {
187
235
 
188
236
  if (require.main === module) main();
189
237
 
190
- module.exports = { main, REQUIRED_PATHS, FORBIDDEN_PATTERNS, SIZE_BUDGET_BYTES };
238
+ module.exports = { main, REQUIRED_PATHS, FORBIDDEN_PATTERNS, SIZE_BUDGET_BYTES, sbomCoverageIssues };
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.19.2",
3
+ "version": "0.19.4",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -57,7 +57,7 @@
57
57
  ],
58
58
  "last_threat_review": "2026-05-15",
59
59
  "signature": "0H+JfyUVmo/pVFEi5rLENATHjlukPVUqnOWmNPEH77wm8svKGK0aNJ46k6QU5GdHb8c9X9pVJKiuhON6AxDjDw==",
60
- "signed_at": "2026-08-08T18:32:35.032Z",
60
+ "signed_at": "2026-08-08T22:55:57.947Z",
61
61
  "cwe_refs": [
62
62
  "CWE-125",
63
63
  "CWE-362",
@@ -133,7 +133,7 @@
133
133
  ],
134
134
  "last_threat_review": "2026-06-10",
135
135
  "signature": "hnKY1LYEKUurrzJIFcvwCZpclpk/XT8fMVJeCKqzrpOpfgov2d3AciuiEky85PhREzAmYGFfk2sLRC6ayJ6UDw==",
136
- "signed_at": "2026-08-08T18:32:35.034Z",
136
+ "signed_at": "2026-08-08T22:55:57.949Z",
137
137
  "cwe_refs": [
138
138
  "CWE-1039",
139
139
  "CWE-1426",
@@ -215,7 +215,7 @@
215
215
  ],
216
216
  "last_threat_review": "2026-06-10",
217
217
  "signature": "uiJt3Oj0lSLr7Nm0OoM2qI/z/oAIAiPwMfP12s97jVYeA7xfOlU21fc7zidT/9YulunfaSjBSAlgv6paWmosBg==",
218
- "signed_at": "2026-08-08T18:32:35.034Z",
218
+ "signed_at": "2026-08-08T22:55:57.949Z",
219
219
  "cwe_refs": [
220
220
  "CWE-22",
221
221
  "CWE-345",
@@ -269,7 +269,7 @@
269
269
  "framework_gaps": [],
270
270
  "last_threat_review": "2026-06-10",
271
271
  "signature": "80LN540R3z/gcIMCF/8H6nh2V6xdudctYKE74Ve7bqecyPUQf2NfCA/8T9L18tIz7BTyBkiI/V5JcG4ClgNyCQ==",
272
- "signed_at": "2026-08-08T18:32:35.035Z"
272
+ "signed_at": "2026-08-08T22:55:57.949Z"
273
273
  },
274
274
  {
275
275
  "name": "compliance-theater",
@@ -301,7 +301,7 @@
301
301
  ],
302
302
  "last_threat_review": "2026-06-10",
303
303
  "signature": "Tsat4OgtCJ6yGJQeIQnz+XTU11B8SLgbPe1zmb2UFOzxqlBnRoKeOzIVwFsv7wM0mP1bXQvQgFFpIOxN2VeTCQ==",
304
- "signed_at": "2026-08-08T18:32:35.035Z"
304
+ "signed_at": "2026-08-08T22:55:57.950Z"
305
305
  },
306
306
  {
307
307
  "name": "exploit-scoring",
@@ -332,7 +332,7 @@
332
332
  ],
333
333
  "last_threat_review": "2026-06-10",
334
334
  "signature": "zxsj3SExhBJ+A7UDZEI4uWl4OCbnn5+ddCRCalhlHKFyRML9fcB9qd1v9a+hDoeVJXGr/OBXHR3MdyD8XEhrDQ==",
335
- "signed_at": "2026-08-08T18:32:35.036Z"
335
+ "signed_at": "2026-08-08T22:55:57.950Z"
336
336
  },
337
337
  {
338
338
  "name": "rag-pipeline-security",
@@ -374,7 +374,7 @@
374
374
  ],
375
375
  "last_threat_review": "2026-06-10",
376
376
  "signature": "lrj8/nJWYzgeOYPF9Ax59A0mxCXWKKhH9QI+88KQNiKly0Hq+pA5wdtL0J9LY24YrVWmqEhpSHueGlj03UNOAA==",
377
- "signed_at": "2026-08-08T18:32:35.036Z",
377
+ "signed_at": "2026-08-08T22:55:57.951Z",
378
378
  "cwe_refs": [
379
379
  "CWE-1395",
380
380
  "CWE-1426"
@@ -441,7 +441,7 @@
441
441
  ],
442
442
  "last_threat_review": "2026-06-10",
443
443
  "signature": "nqqZUnu7ZocLNJKs3KJO6NnoC8MgmS1iGP9TGrv51e+trjqb4Gf2mAjCTe7pINFivWDK00A0ZHazaMAKKbaNCA==",
444
- "signed_at": "2026-08-08T18:32:35.036Z",
444
+ "signed_at": "2026-08-08T22:55:57.951Z",
445
445
  "cwe_refs": [
446
446
  "CWE-918"
447
447
  ],
@@ -482,7 +482,7 @@
482
482
  "framework_gaps": [],
483
483
  "last_threat_review": "2026-06-10",
484
484
  "signature": "ZfFXIDjwGDwlZA30TOMPmNr0IK/sydbsiox7LYU0pGkaRlQW18/oSjkHZRgiQpP5yzQhuFsKdztA7Ec6BDQLAA==",
485
- "signed_at": "2026-08-08T18:32:35.037Z",
485
+ "signed_at": "2026-08-08T22:55:57.951Z",
486
486
  "cwe_refs": [
487
487
  "CWE-1188"
488
488
  ],
@@ -517,7 +517,7 @@
517
517
  "framework_gaps": [],
518
518
  "last_threat_review": "2026-06-10",
519
519
  "signature": "CtiEhtzaqg/hCGX1wmgEGq0O6o+UNf9RAM3xxn0rQVEkFt/IEmyYqsWd1qjpqMdp8kbEXgAsd816AwFwLYMaAA==",
520
- "signed_at": "2026-08-08T18:32:35.037Z",
520
+ "signed_at": "2026-08-08T22:55:57.952Z",
521
521
  "forward_watch": [
522
522
  "New AI attack classes as ATLAS v6 publishes",
523
523
  "Post-quantum adversary capability timeline",
@@ -558,7 +558,7 @@
558
558
  "framework_gaps": [],
559
559
  "last_threat_review": "2026-08-05",
560
560
  "signature": "Eu2AnT4xyZH9n3OKjm5S9vaiHqUE+1N3oLAQbbt78OCp2HnAQMvqBkUn9rgl4Il7Il7yZW8N7c8hq3mkst+EAA==",
561
- "signed_at": "2026-08-08T18:32:35.038Z",
561
+ "signed_at": "2026-08-08T22:55:57.952Z",
562
562
  "d3fend_refs": [
563
563
  "D3-EAL",
564
564
  "D3-EFA",
@@ -600,7 +600,7 @@
600
600
  "framework_gaps": [],
601
601
  "last_threat_review": "2026-06-10",
602
602
  "signature": "tlvk8q1fa1scoDsJMZnigCoRNO3dqoK03Ve8tuYE2/nPwAMAGlofHpoE0mI0BX4pPODPgCfSjzgKEUn6HkcZDw==",
603
- "signed_at": "2026-08-08T18:32:35.038Z",
603
+ "signed_at": "2026-08-08T22:55:57.953Z",
604
604
  "forward_watch": [
605
605
  "New CISA KEV entries",
606
606
  "New ATLAS TTP additions in each ATLAS release",
@@ -669,7 +669,7 @@
669
669
  ],
670
670
  "last_threat_review": "2026-08-05",
671
671
  "signature": "DNXmzLVK71XI/b9mkIBw30KVdVV5Wc9/0o/pP35RkferqtvlsMHjj4WJpoGXpIu8/hJyuMJRQrV7pzfR68iIAg==",
672
- "signed_at": "2026-08-08T18:32:35.038Z",
672
+ "signed_at": "2026-08-08T22:55:57.953Z",
673
673
  "cwe_refs": [
674
674
  "CWE-327"
675
675
  ],
@@ -719,7 +719,7 @@
719
719
  ],
720
720
  "last_threat_review": "2026-06-10",
721
721
  "signature": "AzyfBZd5bxpQpaFb/S5ZJI2/NjeACLuq/u8GzKHehhlggWUqILIrS4aJ6eXn0I3UBhuXRcNBBx1mXriCSmhQAA==",
722
- "signed_at": "2026-08-08T18:32:35.039Z"
722
+ "signed_at": "2026-08-08T22:55:57.954Z"
723
723
  },
724
724
  {
725
725
  "name": "security-maturity-tiers",
@@ -759,7 +759,7 @@
759
759
  ],
760
760
  "last_threat_review": "2026-08-05",
761
761
  "signature": "8JHth6QjMcUuAD069/QvpMxgRMG+L/PVTE68/UdX9kG4SNA20ds7vHU15YzMNBmWWgiUvpLAE6hzctz1N7bWCg==",
762
- "signed_at": "2026-08-08T18:32:35.039Z",
762
+ "signed_at": "2026-08-08T22:55:57.954Z",
763
763
  "cwe_refs": [
764
764
  "CWE-1188"
765
765
  ],
@@ -813,7 +813,7 @@
813
813
  "framework_gaps": [],
814
814
  "last_threat_review": "2026-05-11",
815
815
  "signature": "iJWevUBurLvt2v8X+Ch2eHmZkPWpKeAtIpxTIP4MwbUHyco3igDeBywJCyaR2vURYRx8LkzzIMM8DxQM4LAXBQ==",
816
- "signed_at": "2026-08-08T18:32:35.040Z"
816
+ "signed_at": "2026-08-08T22:55:57.954Z"
817
817
  },
818
818
  {
819
819
  "name": "attack-surface-pentest",
@@ -889,7 +889,7 @@
889
889
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — Microsoft Edge 4-bug sandbox escape by Orange Tsai (DEVCORE); forward-watch only (browser sandbox, out of current playbook scope); track Microsoft Edge security advisory and KEV add"
890
890
  ],
891
891
  "signature": "GuHxmoEU97+lQfU11chYwXVQhSMpddU0OS1iLkYDmEJhBkHRWWaOa2emYJoVUBDB9v/PTKcFEuEraZwYg+8nCw==",
892
- "signed_at": "2026-08-08T18:32:35.040Z"
892
+ "signed_at": "2026-08-08T22:55:57.955Z"
893
893
  },
894
894
  {
895
895
  "name": "fuzz-testing-strategy",
@@ -950,7 +950,7 @@
950
950
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
951
951
  ],
952
952
  "signature": "R2jcBorSMWFuZKHUugWEni9q/W7Qk73Kg0SztXlySYu9+D2Ee43gyYsUy0nSa0oCTS8pyuhFiFv2XN0EaweTBQ==",
953
- "signed_at": "2026-08-08T18:32:35.040Z"
953
+ "signed_at": "2026-08-08T22:55:57.955Z"
954
954
  },
955
955
  {
956
956
  "name": "dlp-gap-analysis",
@@ -1028,7 +1028,7 @@
1028
1028
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
1029
1029
  ],
1030
1030
  "signature": "uIQdYlbKIQw8qQRDeGnXv4D/wuPAvYFRzos3kqkyELx+v9rGmfmT7w5s1608lpJxXzGLIA7BSN2/wLJJAZmXCg==",
1031
- "signed_at": "2026-08-08T18:32:35.041Z"
1031
+ "signed_at": "2026-08-08T22:55:57.955Z"
1032
1032
  },
1033
1033
  {
1034
1034
  "name": "supply-chain-integrity",
@@ -1112,7 +1112,7 @@
1112
1112
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Megatron Bridge path traversal by haehae; AI training-stack file-system trust boundary; track patch and SBOM-attestation impact"
1113
1113
  ],
1114
1114
  "signature": "4EMvqqcYHnM+9fmlS156Utl3T248EqHdiB89zVyW8+wUYHAU/VLUOr27JiiSmj5iZrdY5Prq7t9Hsk3LuVx5Aw==",
1115
- "signed_at": "2026-08-08T18:32:35.041Z"
1115
+ "signed_at": "2026-08-08T22:55:57.956Z"
1116
1116
  },
1117
1117
  {
1118
1118
  "name": "defensive-countermeasure-mapping",
@@ -1169,7 +1169,7 @@
1169
1169
  ],
1170
1170
  "last_threat_review": "2026-06-10",
1171
1171
  "signature": "W8kg2Okd/VF521VmeVFGK2TmXkFPycuwgYdyNxLU2fMN+sR9Aals/X+Xqr/prrGEsqWnZsCInSq1TYzFr0wiDg==",
1172
- "signed_at": "2026-08-08T18:32:35.041Z"
1172
+ "signed_at": "2026-08-08T22:55:57.956Z"
1173
1173
  },
1174
1174
  {
1175
1175
  "name": "identity-assurance",
@@ -1243,7 +1243,7 @@
1243
1243
  ],
1244
1244
  "last_threat_review": "2026-05-11",
1245
1245
  "signature": "UV3458QXSkEpenzrOmdlTTfPHUD4hNyKMDHoeZDq/kiFb4mAG0ghQGTTgI9Ru8cJbSmYM1++m9N5TFIJ6JJPBg==",
1246
- "signed_at": "2026-08-08T18:32:35.042Z"
1246
+ "signed_at": "2026-08-08T22:55:57.956Z"
1247
1247
  },
1248
1248
  {
1249
1249
  "name": "ot-ics-security",
@@ -1302,7 +1302,7 @@
1302
1302
  "d3fend_refs": [],
1303
1303
  "last_threat_review": "2026-06-10",
1304
1304
  "signature": "1PFZXMfQwM0dHsZQfo+rg5hJB00dfeOL41VA3J8gNF6cCUpplw9vQuZfODX/SwXI/WmDQwM9G2sd1X/i7LG9DA==",
1305
- "signed_at": "2026-08-08T18:32:35.042Z"
1305
+ "signed_at": "2026-08-08T22:55:57.957Z"
1306
1306
  },
1307
1307
  {
1308
1308
  "name": "coordinated-vuln-disclosure",
@@ -1362,7 +1362,7 @@
1362
1362
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1363
1363
  ],
1364
1364
  "signature": "5b3J9mk/iEi6OzeC0O2hw/K/ofTrH8519l+ZUpK8gqB/v0XbDFceIIBPB8TQM8wCct5iG0Krb7OCDSIbU0jVBw==",
1365
- "signed_at": "2026-08-08T18:32:35.042Z"
1365
+ "signed_at": "2026-08-08T22:55:57.957Z"
1366
1366
  },
1367
1367
  {
1368
1368
  "name": "threat-modeling-methodology",
@@ -1416,7 +1416,7 @@
1416
1416
  "PASTA v2 updates incorporating AI/ML application threats"
1417
1417
  ],
1418
1418
  "signature": "fibgNRd+STEyN3ZlnIxJddfaOXISYosdjoxkQ/oNVTP3GKhN1A4c3Jh0HZPbhTdjhrbJM/mE5ufiIOs7+RCMCQ==",
1419
- "signed_at": "2026-08-08T18:32:35.043Z"
1419
+ "signed_at": "2026-08-08T22:55:57.957Z"
1420
1420
  },
1421
1421
  {
1422
1422
  "name": "webapp-security",
@@ -1499,7 +1499,7 @@
1499
1499
  ],
1500
1500
  "last_threat_review": "2026-06-10",
1501
1501
  "signature": "3qtC0X8QR9YufDfeL/AUiw3EeJwvSfSrUWojndEZVmeJ3MtM12I94OJmaQT1AWaTRWVa9f91owgLl6AgoTNVAA==",
1502
- "signed_at": "2026-08-08T18:32:35.043Z",
1502
+ "signed_at": "2026-08-08T22:55:57.958Z",
1503
1503
  "forward_watch": [
1504
1504
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; AI-assisted discovery angle; track for active-exploitation confirmation and patch advisory affecting front-door web app deployments"
1505
1505
  ]
@@ -1559,7 +1559,7 @@
1559
1559
  ],
1560
1560
  "last_threat_review": "2026-05-15",
1561
1561
  "signature": "SBB7c3wNYfIdkyOp4g4nW0WP7xS+YokMzg32aaeJdbf14LTGQRzQUvSqb2TCj2HFUSHESOyKT1JpkAfyHLSQBQ==",
1562
- "signed_at": "2026-08-08T18:32:35.043Z"
1562
+ "signed_at": "2026-08-08T22:55:57.958Z"
1563
1563
  },
1564
1564
  {
1565
1565
  "name": "sector-healthcare",
@@ -1626,7 +1626,7 @@
1626
1626
  ],
1627
1627
  "last_threat_review": "2026-06-10",
1628
1628
  "signature": "Z/lPfIH5ARAOoeR81ATj6lQlekZP/8tzg23Pr1H9BxxdQedRsLGDNVIHu81qNuoA7GNRRMtxXpwrS7fXbMh5Cw==",
1629
- "signed_at": "2026-08-08T18:32:35.044Z"
1629
+ "signed_at": "2026-08-08T22:55:57.959Z"
1630
1630
  },
1631
1631
  {
1632
1632
  "name": "sector-financial",
@@ -1715,7 +1715,7 @@
1715
1715
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1716
1716
  ],
1717
1717
  "signature": "kPV8cEpIUAMKY2USvFxAn6JkkRMlBAM2mGLy6ViS9O3rv+3dsAgBqKgcBTBbuSoMZgY2KKruoG+SkrR3aoViAg==",
1718
- "signed_at": "2026-08-08T18:32:35.044Z"
1718
+ "signed_at": "2026-08-08T22:55:57.960Z"
1719
1719
  },
1720
1720
  {
1721
1721
  "name": "sector-federal-government",
@@ -1792,7 +1792,7 @@
1792
1792
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1793
1793
  ],
1794
1794
  "signature": "CbxW5qhlaiqf+P3xIemSHlMz9OoWat2srh7F3+nAIsa0Y4nBr6WCXZXZlUUjrjptZ3Ny+Cw485HMLWRO9qbcBw==",
1795
- "signed_at": "2026-08-08T18:32:35.045Z"
1795
+ "signed_at": "2026-08-08T22:55:57.960Z"
1796
1796
  },
1797
1797
  {
1798
1798
  "name": "sector-energy",
@@ -1866,7 +1866,7 @@
1866
1866
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1867
1867
  ],
1868
1868
  "signature": "lVHORFJzvkxNqJfT34inW28BzlA8dCWDfo0JS9tsiNW3PUxdijZS22g4GMbQJgaSKGv7F3ag5rWk75O+IChUAw==",
1869
- "signed_at": "2026-08-08T18:32:35.045Z"
1869
+ "signed_at": "2026-08-08T22:55:57.961Z"
1870
1870
  },
1871
1871
  {
1872
1872
  "name": "sector-telecom",
@@ -1952,7 +1952,7 @@
1952
1952
  "O-RAN SFG / WG11 security specifications"
1953
1953
  ],
1954
1954
  "signature": "NAtyzfLPXlUuB78Snb9nWmbZalC1CNlIYN9rYhdEmtB/xQGC6vVnThgrEAHlm7v/jMCFuknvEpUHKdscUnUADw==",
1955
- "signed_at": "2026-08-08T18:32:35.045Z"
1955
+ "signed_at": "2026-08-08T22:55:57.961Z"
1956
1956
  },
1957
1957
  {
1958
1958
  "name": "api-security",
@@ -2031,7 +2031,7 @@
2031
2031
  ],
2032
2032
  "last_threat_review": "2026-06-10",
2033
2033
  "signature": "hyLAsPCAkUkhNeaaFtgSg5XsKs2L8HuFoPka/Urzel54XPMQ0F3MwRD7j/ZURzIHQxvrAxmS3OCXEsBrgcFxAw==",
2034
- "signed_at": "2026-08-08T18:32:35.046Z",
2034
+ "signed_at": "2026-08-08T22:55:57.961Z",
2035
2035
  "forward_watch": [
2036
2036
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; track for active-exploitation confirmation and patch advisory affecting API gateway / reverse-proxy deployments",
2037
2037
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — LiteLLM 3-bug SSRF + Code Injection chain by k3vg3n; LLM-proxy API surface; track upstream patch and CVE assignments",
@@ -2126,7 +2126,7 @@
2126
2126
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
2127
2127
  ],
2128
2128
  "signature": "MfijBq1nzlyPQWf5/EcTn5Xx7ThdZ9BT8/W/+daxhATfMSoSTrMn9pYh1R83Chr9n2ahQeMoPUkSxL7Cr9V3Dw==",
2129
- "signed_at": "2026-08-08T18:32:35.046Z"
2129
+ "signed_at": "2026-08-08T22:55:57.962Z"
2130
2130
  },
2131
2131
  {
2132
2132
  "name": "container-runtime-security",
@@ -2198,7 +2198,7 @@
2198
2198
  ],
2199
2199
  "last_threat_review": "2026-06-10",
2200
2200
  "signature": "QJ8RF3ifPFc0mr3gYkloHqg5nuYe1GS4h+xbKYDCkM+HbA3iTd5keJ59P2P3z8TwVdOt8XM8WOsx6aRUrFL0Cw==",
2201
- "signed_at": "2026-08-08T18:32:35.046Z",
2201
+ "signed_at": "2026-08-08T22:55:57.962Z",
2202
2202
  "forward_watch": [
2203
2203
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Container Toolkit container escape ($50K award) by chompie / IBM X-Force XOR; high-severity container/hypervisor boundary break; track patch and KEV add post-embargo"
2204
2204
  ]
@@ -2279,7 +2279,7 @@
2279
2279
  "MITRE ATLAS v2026.07 (released June 2026) is additive over v2026.05 — it adds AML.T0113 (Steal Web Session Cookie), AML.T0091.001 (Use Alternate Authentication Material: Web Session Cookie), and AML.T0114 (AI Service Web Interface) and refreshes AML.T0054 (LLM Jailbreak), with no technique-ID removals. Earlier releases shipped the AML.T0010 sub-technique expansion and agentic-AI techniques (\"Publish Poisoned AI Agent Tool\", \"Escape to Host\") this forecast tracked. Forward watch: subsequent monthly ATLAS releases — track next-cadence updates to agentic-AI TTPs and MLOps-pipeline-specific techniques"
2280
2280
  ],
2281
2281
  "signature": "e4uV8JvtEbC/lKGqJ6Y3ugEetF6aEv7R7CwJ/Y9N4thWNg8fconRcOPDS+045PW8xmcAqAbkSi+I4N1E6EPTAA==",
2282
- "signed_at": "2026-08-08T18:32:35.047Z"
2282
+ "signed_at": "2026-08-08T22:55:57.962Z"
2283
2283
  },
2284
2284
  {
2285
2285
  "name": "incident-response-playbook",
@@ -2353,7 +2353,7 @@
2353
2353
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
2354
2354
  ],
2355
2355
  "signature": "asDHEy7Y2Bj4ikCkpP5MHRe33TIY8aQ1a+KN1UZgl89g9LyLcXzSg/weACafxJNzhOUkXnKBAtBrQIKW6p6nDQ==",
2356
- "signed_at": "2026-08-08T18:32:35.047Z"
2356
+ "signed_at": "2026-08-08T22:55:57.963Z"
2357
2357
  },
2358
2358
  {
2359
2359
  "name": "ransomware-response",
@@ -2433,7 +2433,7 @@
2433
2433
  ],
2434
2434
  "last_threat_review": "2026-06-10",
2435
2435
  "signature": "UTDsSMDvW5cpuSDBgZO5AzKowXWAR3WqeUeqyHXaikxslC6YBDL/gIojm9Km3/mgOS6pD5JKb/LLcfb4T/r4BQ==",
2436
- "signed_at": "2026-08-08T18:32:35.047Z"
2436
+ "signed_at": "2026-08-08T22:55:57.963Z"
2437
2437
  },
2438
2438
  {
2439
2439
  "name": "email-security-anti-phishing",
@@ -2501,7 +2501,7 @@
2501
2501
  ],
2502
2502
  "last_threat_review": "2026-05-18",
2503
2503
  "signature": "FVBn4ex2qPIo9SHMVJ6tntoz4tVwjbIq3m6wDjjZyv2JODlS+90GBYCOkNamxxkmw/6de6SMs0YHQiF/xjo/DQ==",
2504
- "signed_at": "2026-08-08T18:32:35.047Z"
2504
+ "signed_at": "2026-08-08T22:55:57.963Z"
2505
2505
  },
2506
2506
  {
2507
2507
  "name": "age-gates-child-safety",
@@ -2576,7 +2576,7 @@
2576
2576
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2577
2577
  ],
2578
2578
  "signature": "l/jeAAnb7H2GQzp1x05bsdZwzC0NNTBi3ZuiwgDly5EVSoikbnVWpx921bT5G3OPkx4dqs2PohlvTG0gLxCrAA==",
2579
- "signed_at": "2026-08-08T18:32:35.048Z"
2579
+ "signed_at": "2026-08-08T22:55:57.964Z"
2580
2580
  },
2581
2581
  {
2582
2582
  "name": "cloud-iam-incident",
@@ -2656,7 +2656,7 @@
2656
2656
  ],
2657
2657
  "last_threat_review": "2026-05-15",
2658
2658
  "signature": "vrMvYLdYJLlEAC8YvOb/g3LJ8gU1YmYewZ94J5AFgxxh2EiDyszqo3D/WVpt5NTrDSS/N5OgOf5Vwom/KGlMCA==",
2659
- "signed_at": "2026-08-08T18:32:35.048Z",
2659
+ "signed_at": "2026-08-08T22:55:57.964Z",
2660
2660
  "forward_watch": [
2661
2661
  "AWS IAM Identity Center session-policy refresh and step-up-on-admin enforcement (anticipated 2026-H2 release)",
2662
2662
  "GCP Workload Identity Federation principal-set attribute mapping tightening (post-2026 Q3 Federation hardening guide)",
@@ -2750,7 +2750,7 @@
2750
2750
  ],
2751
2751
  "last_threat_review": "2026-05-15",
2752
2752
  "signature": "9mfDtMApMAg9V/lmwpniNxo/6gNZoOEoYDfyFvyWvKrPMtc7H9F8uz06FVoARe/J49saAKTVXOurNE1D/KtpCQ==",
2753
- "signed_at": "2026-08-08T18:32:35.049Z",
2753
+ "signed_at": "2026-08-08T22:55:57.964Z",
2754
2754
  "forward_watch": [
2755
2755
  "Entra ID conditional access evolution post-Midnight Blizzard — Microsoft's 2025-2026 commitments on legacy-tenant MFA enforcement and OAuth-app consent gating",
2756
2756
  "Okta IPSIE (Interoperability Profile for Secure Identity in the Enterprise) OpenID Foundation working-group output and adoption timeline",
@@ -2822,7 +2822,7 @@
2822
2822
  "d3fend_refs": [],
2823
2823
  "last_threat_review": "2026-06-02",
2824
2824
  "signature": "dOFgANMtHm64uSFRjMfeA0fWZS9ISwMTt59I2CbjaF1PgTtbtb8yg/f+3pC4eXcXIET0KPGxRRmENycfIz3rAw==",
2825
- "signed_at": "2026-08-08T18:32:35.049Z"
2825
+ "signed_at": "2026-08-08T22:55:57.965Z"
2826
2826
  },
2827
2827
  {
2828
2828
  "name": "mail-server-hardening",
@@ -2881,7 +2881,7 @@
2881
2881
  "d3fend_refs": [],
2882
2882
  "last_threat_review": "2026-06-02",
2883
2883
  "signature": "5x8RGzKo/T2IbYnFkYMZs16ThD9a5FEdrKHsnwvtMuGxdyyfZsKNIJVWw2I8O0um0UOXwwkl17Z3zhch26ymCQ==",
2884
- "signed_at": "2026-08-08T18:32:35.049Z"
2884
+ "signed_at": "2026-08-08T22:55:57.965Z"
2885
2885
  },
2886
2886
  {
2887
2887
  "name": "network-trust",
@@ -2937,7 +2937,7 @@
2937
2937
  "d3fend_refs": [],
2938
2938
  "last_threat_review": "2026-06-02",
2939
2939
  "signature": "C/RdmnGjXKreKiGkwJfUESIxUwQzisGJ9A7KBdE9cBRBHBYHkAcScV89L+Z3kOG032Qax6LeIQai7Lr3nrQBCA==",
2940
- "signed_at": "2026-08-08T18:32:35.049Z"
2940
+ "signed_at": "2026-08-08T22:55:57.965Z"
2941
2941
  },
2942
2942
  {
2943
2943
  "name": "audit-log-integrity",
@@ -2992,7 +2992,7 @@
2992
2992
  "d3fend_refs": [],
2993
2993
  "last_threat_review": "2026-06-02",
2994
2994
  "signature": "T7sFREX7P2QYrau+eTNXKkrU2aJ91dsX6GQWVJeexJ9SqbyFHtAnIKo4r6F6VdELADyVjaRXKk/fLuyUaCkGBA==",
2995
- "signed_at": "2026-08-08T18:32:35.049Z"
2995
+ "signed_at": "2026-08-08T22:55:57.965Z"
2996
2996
  },
2997
2997
  {
2998
2998
  "name": "self-update-integrity",
@@ -3046,7 +3046,7 @@
3046
3046
  "d3fend_refs": [],
3047
3047
  "last_threat_review": "2026-06-02",
3048
3048
  "signature": "BOQZ8G9LdyG/tfpz6rCU4THz5Dq2HGJkUV7uCqSTCMrqnXH6seNXKM41FWawYo0s2VUlXtRgaO7ETp5qgxw+CQ==",
3049
- "signed_at": "2026-08-08T18:32:35.050Z"
3049
+ "signed_at": "2026-08-08T22:55:57.966Z"
3050
3050
  },
3051
3051
  {
3052
3052
  "name": "multitenancy-isolation",
@@ -3104,7 +3104,7 @@
3104
3104
  "d3fend_refs": [],
3105
3105
  "last_threat_review": "2026-06-02",
3106
3106
  "signature": "bncN0VE4ns0lxtPe48xDR33lerw54BPVkSwX7XlrLQcMHTrV94lmjO8z4ZP4Ko61n+KpuP2SNU8v9JT0bGMmAA==",
3107
- "signed_at": "2026-08-08T18:32:35.050Z"
3107
+ "signed_at": "2026-08-08T22:55:57.966Z"
3108
3108
  },
3109
3109
  {
3110
3110
  "name": "decompression-dos",
@@ -3162,7 +3162,7 @@
3162
3162
  "d3fend_refs": [],
3163
3163
  "last_threat_review": "2026-06-02",
3164
3164
  "signature": "03i0ZO5ScHIOjGvDKUEsws6m+20fnlJrEhSBtMU7PJzQutrPpmW9gULE0wbayTD5H9URh5nSKZuuVjvjVhZNCg==",
3165
- "signed_at": "2026-08-08T18:32:35.050Z"
3165
+ "signed_at": "2026-08-08T22:55:57.966Z"
3166
3166
  },
3167
3167
  {
3168
3168
  "name": "log-injection-telemetry",
@@ -3217,7 +3217,7 @@
3217
3217
  "d3fend_refs": [],
3218
3218
  "last_threat_review": "2026-06-02",
3219
3219
  "signature": "twTmA2l+Am2k2PCzB2DNxDQwoYlGopJp/3QgQwBsP7h2LF+uNudisqfuOVV/bYnWfrUhuYZUWbsR3mhzbJCUBw==",
3220
- "signed_at": "2026-08-08T18:32:35.050Z"
3220
+ "signed_at": "2026-08-08T22:55:57.966Z"
3221
3221
  },
3222
3222
  {
3223
3223
  "name": "privacy-consent-ops",
@@ -3272,11 +3272,11 @@
3272
3272
  "d3fend_refs": [],
3273
3273
  "last_threat_review": "2026-06-02",
3274
3274
  "signature": "0sfKSVrIf555c9bDGkXB96fUClN52fMAMYoQsNQ9eUUSzDiWUwdou+q0gYt43nObtj3m9Yb9X6q3oCXDVMgUDg==",
3275
- "signed_at": "2026-08-08T18:32:35.050Z"
3275
+ "signed_at": "2026-08-08T22:55:57.967Z"
3276
3276
  }
3277
3277
  ],
3278
3278
  "manifest_signature": {
3279
3279
  "algorithm": "Ed25519",
3280
- "signature_base64": "8rlFUjIeZCWHk21jocMUiVm4pPWrjwZqrHhGKsuELNoXCPxILZo2DBFwyNQszlp/oQrhDhfu2i38SxbJOGZ/CA=="
3280
+ "signature_base64": "AKfV9CWcR3vzj8gXIpq76/+lHaka2UVFM0n/tPcxdWJjHMWddbOaObQRvMjyRh9/jdXQXw8uvrI7Z6BKuv5MDw=="
3281
3281
  }
3282
3282
  }
@@ -194,10 +194,11 @@ Examples:
194
194
  }
195
195
 
196
196
  const root = path.join(__dirname, '..');
197
- let controlGaps, cveCatalog;
197
+ let controlGaps, cveCatalog, lessons;
198
198
  try {
199
199
  controlGaps = JSON.parse(fs.readFileSync(path.join(root, 'data', 'framework-control-gaps.json'), 'utf8'));
200
200
  cveCatalog = JSON.parse(fs.readFileSync(path.join(root, 'data', 'cve-catalog.json'), 'utf8'));
201
+ lessons = JSON.parse(fs.readFileSync(path.join(root, 'data', 'zeroday-lessons.json'), 'utf8'));
201
202
  } catch (err) {
202
203
  // Honor --json on the catalog-read failure path too: a JSON consumer must
203
204
  // get the same structured ok:false envelope the other error exits emit.
@@ -254,7 +255,7 @@ Examples:
254
255
  const scenario = args[1];
255
256
 
256
257
  const allFrameworks = args[0].toLowerCase() === 'all';
257
- const report = gapReport(requested, scenario, controlGaps, cveCatalog, { allFrameworks });
258
+ const report = gapReport(requested, scenario, controlGaps, cveCatalog, { allFrameworks, lessons });
258
259
  const theater = theaterCheck(controlGaps, cveCatalog);
259
260
 
260
261
  if (jsonOut) {
@@ -286,6 +287,18 @@ Examples:
286
287
  console.log();
287
288
  }
288
289
 
290
+ if (report.new_control_requirements.length > 0) {
291
+ console.log(`### New controls this CVE requires (no framework carries them) — ${report.new_control_requirements.length}`);
292
+ for (const c of report.new_control_requirements) {
293
+ const req = c.requirement || '';
294
+ console.log(` - ${c.id} ${c.name}: ${req.length > 140 ? req.slice(0, 140) + '…' : req}`);
295
+ // Naming the gaps it closes is what separates this from generic advice —
296
+ // it ties the control back to the insufficient framework controls above.
297
+ if (c.closes.length) console.log(` closes: ${c.closes.join(', ')}`);
298
+ }
299
+ console.log();
300
+ }
301
+
289
302
  if (report.theater_risks.length > 0) {
290
303
  console.log(`### Theater risk controls (compliant but exposed) — ${report.theater_risks.length}`);
291
304
  for (const t of report.theater_risks) {
@@ -294,7 +307,7 @@ Examples:
294
307
  console.log();
295
308
  }
296
309
 
297
- console.log(`Summary: ${report.summary.total_gaps} matching gaps, ${report.summary.universal_gaps} universal, ${report.summary.theater_risk_controls} theater-risk controls`);
310
+ console.log(`Summary: ${report.summary.total_gaps} matching gaps, ${report.summary.universal_gaps} universal, ${report.summary.new_control_requirements} new controls required, ${report.summary.theater_risk_controls} theater-risk controls`);
298
311
  }
299
312
 
300
313
  // --- command implementations ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.19.2",
3
+ "version": "0.19.4",
4
4
  "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.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:e541fe5e-916a-4fb4-90d8-936737db409e",
4
+ "serialNumber": "urn:uuid:39690a6e-923e-46d5-b483-72282a0e499c",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2147-11-20T10:43:42.000Z",
7
+ "timestamp": "2026-08-08T00:00:00.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.19.2"
12
+ "version": "0.19.4"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.19.2",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.19.4",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.19.2",
19
+ "version": "0.19.4",
20
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
  {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.19.2",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.19.4",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "ccc66fa2356a4cadafc9dd29f405338bb7800da6f0069792280e6ba25d5b25cf"
32
+ "content": "4ef2a3fc4e88dbfdf286ca61af32628049f178d1f525e204fec3baeb622dd2a5"
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.2"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.19.4"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -60,6 +60,14 @@
60
60
  "name": "exceptd:integrity:method",
61
61
  "value": "Ed25519 per-skill (lib/sign.js)"
62
62
  },
63
+ {
64
+ "name": "exceptd:integrity:uncovered:prefix",
65
+ "value": "data/_indexes/"
66
+ },
67
+ {
68
+ "name": "exceptd:integrity:uncovered:rationale",
69
+ "value": "Regenerated by `npm run build-indexes` and mutated by the test suite, so a pinned per-file hash would race any run between generation and verification. Covered instead by the pre-computed-index freshness gate in `npm run predeploy`."
70
+ },
63
71
  {
64
72
  "name": "exceptd:runtime:dependency:count",
65
73
  "value": "0"
@@ -116,11 +124,11 @@
116
124
  "hashes": [
117
125
  {
118
126
  "alg": "SHA-256",
119
- "content": "ad80c0728a249b3888aa3f55c3d3bb710c8e583e15155a95689168612d2ff5d4"
127
+ "content": "6e0ba7961dc4fed4d9e18db5521fa38cd11e03be6a81689b94deb649c7519c84"
120
128
  },
121
129
  {
122
130
  "alg": "SHA3-512",
123
- "content": "630a8640fe48b549daaafe0a5d8aae24aab094e575db0e602b0849ffd8222d39f367f01a70e377f834416e7f0255f64871e3b90f2041b360b683490ed0145f22"
131
+ "content": "295dd1023cd697f7a63586b84c07f43a9fec6a1c519b5e2cd3844dc403deab115b30110f4d645fa9076e8d0ec4c609a189b48516fbe653358d007ac063ee3779"
124
132
  }
125
133
  ]
126
134
  },
@@ -1436,11 +1444,11 @@
1436
1444
  "hashes": [
1437
1445
  {
1438
1446
  "alg": "SHA-256",
1439
- "content": "b424f20ced1c3dd04ddbe9b619589d15612afed8ede27cbd0e5e607c4413bc0c"
1447
+ "content": "ea9bf984d82bc6704f254efc64f29b68e4849eb4b36dc03abefffe82553e295b"
1440
1448
  },
1441
1449
  {
1442
1450
  "alg": "SHA3-512",
1443
- "content": "2fad9d2dd2098f3ee6ecab410bd217cfe626d237fce5ae3b637e3c4d9c6385f3fcfb53ebfa6a35c4f8a9e16d7ef79277fdae8a9235fb3f4b6cc4cbdcd15294f7"
1451
+ "content": "ab49455842d2de7b7ad87129b46d343b96b2e50f032385b42335cb52cd5e03513bd2cf212c32ed5b188bda51c8a4925d991486ac7fe1b34bcdce6460124c2c32"
1444
1452
  }
1445
1453
  ]
1446
1454
  },
@@ -1811,11 +1819,11 @@
1811
1819
  "hashes": [
1812
1820
  {
1813
1821
  "alg": "SHA-256",
1814
- "content": "541bfb66f6dd9802e157d6d12418bb2402a1d6c66f80b3024a362bccd35d2c21"
1822
+ "content": "5aeebc43db5477b9a342f783d72a5a28a9632205b8aab6e08ea346d3310eac47"
1815
1823
  },
1816
1824
  {
1817
1825
  "alg": "SHA3-512",
1818
- "content": "d1a8189afaeefecd6c8993b9f0739b6c2adc6058cb793051d7f6e99d9a4bda978827e81e5f37ad3384b14a5a45089a073bf8adbe85bb982d0bbc84ae6569f852"
1826
+ "content": "08b23fc717d26133ee455690692b79c3a5bb8386cb4aaa54d6ba6f3e8705adab8e217881b3418534d38bf18b53761f76d8f6ccd1a20567ec5bc39e715a207304"
1819
1827
  }
1820
1828
  ]
1821
1829
  },
@@ -1946,11 +1954,11 @@
1946
1954
  "hashes": [
1947
1955
  {
1948
1956
  "alg": "SHA-256",
1949
- "content": "53564f4e897f2d56d4eba1ccd97e9d9cb942c8307221e1ac7a9946cc6c312ab1"
1957
+ "content": "4f92a89e4b15843aed7516645c38e3eaec48993fe3043f74b98e97a42f2d38b4"
1950
1958
  },
1951
1959
  {
1952
1960
  "alg": "SHA3-512",
1953
- "content": "e1ff542b725df94d2c40d25b212f71dca8d6f9a555e2d8a97a7c543713c67d52a9f321153cfbd2cdbd5bf0f39247d4ebca54b100ac054ffe9bf97ef4491677e3"
1961
+ "content": "33cef53e15a6270cb030af5ac31eb7c875f613e720ef794f1a8d4540fe11112ff1f754a9711565aa27865a15e74ac35b74acd433e6445e61edd99c166362855c"
1954
1962
  }
1955
1963
  ]
1956
1964
  },
@@ -2006,11 +2014,11 @@
2006
2014
  "hashes": [
2007
2015
  {
2008
2016
  "alg": "SHA-256",
2009
- "content": "e37061623bdda98926f5ee19d035415d773a7da346740cff29a381ddfc903ef8"
2017
+ "content": "6374fcc3e8071c608dda8dc73657c7fef2adac1c5f954e70fde438de34224cdc"
2010
2018
  },
2011
2019
  {
2012
2020
  "alg": "SHA3-512",
2013
- "content": "3d6199d057bcd2ac7934e451823f7baf4d8af7a98ba0b81db763879254d0ab4909bbe0e43c62460763de6502bb17cbc515b32d85bb6ee8dcbb70507660440e5c"
2021
+ "content": "230426ea74b17e4e4d5eeee9873beebe6e5c5fa9956a78ba654271f53c5511ec51ddfd6b19a041df7efcf33db862a53764190f28bdde292702b36c7976386b14"
2014
2022
  }
2015
2023
  ]
2016
2024
  },
@@ -2059,6 +2067,21 @@
2059
2067
  }
2060
2068
  ]
2061
2069
  },
2070
+ {
2071
+ "bom-ref": "file:package.json",
2072
+ "type": "file",
2073
+ "name": "package.json",
2074
+ "hashes": [
2075
+ {
2076
+ "alg": "SHA-256",
2077
+ "content": "304331c5a4233174becbc9e73d9fafe4c781a0d2dbcf438a198bfd6e7f75c859"
2078
+ },
2079
+ {
2080
+ "alg": "SHA3-512",
2081
+ "content": "2f6b0bc9b7f996370d228c6a59681b3394d859910e3437dac2769a3665af1b6fcda6ea195af6995fcbbd7ce2031bd065cd09c5427ea4aa0083db0d18a478a41a"
2082
+ }
2083
+ ]
2084
+ },
2062
2085
  {
2063
2086
  "bom-ref": "file:scripts/audit-catalog-gaps.js",
2064
2087
  "type": "file",
@@ -2471,11 +2494,11 @@
2471
2494
  "hashes": [
2472
2495
  {
2473
2496
  "alg": "SHA-256",
2474
- "content": "25f1498d8baa74657947cbea6aca4e9d88c2d59d7dadf534ac6c1026ff2efd5a"
2497
+ "content": "c360d8fce5cb264662a355b05224fa2a3f16a25ff3d6199ad5db1c08919754e2"
2475
2498
  },
2476
2499
  {
2477
2500
  "alg": "SHA3-512",
2478
- "content": "995b8b7814d8d7f32bad272a44663a31dab6c4b0cbb678814f78cc731a9f9d47516f77a61519402aa4588fd0d06935fcd208b8eeff6958b37b021dda8f48eedd"
2501
+ "content": "f364223d705ed8b4dcf6e54d234891b3508cd79c0fe5832706514c9dc3079b594322fb63e16ea9fe1d135181b93e0c153ad073fb92cdc40d59386d781006eed0"
2479
2502
  }
2480
2503
  ]
2481
2504
  },
@@ -2741,11 +2764,11 @@
2741
2764
  "hashes": [
2742
2765
  {
2743
2766
  "alg": "SHA-256",
2744
- "content": "7ddfd780bbc37f9c4d10d66eb9f620ef5f26d35c50655a753020ca3bb9b89c6a"
2767
+ "content": "c5f38a759980ae125f1f30195abd60dae2c4d9547895ee6e8290f47c80bab0c4"
2745
2768
  },
2746
2769
  {
2747
2770
  "alg": "SHA3-512",
2748
- "content": "ab187eeeb0b8a666a0a65d80ebd792b6c477ace0da4f2d956f92d4def4caf6ead1dd32f3dcbebfbdaf422744ff449ae4a39d6c0dcb0c081f254291aa70d189af"
2771
+ "content": "a5b7a4bed681c7c066aa51316bb1d602134721906eeada0ab6ce8a67227f09eb191dce0e530a6c15486a1f308bc71b304916d45dec15ec8fc53b6f0aebf910e3"
2749
2772
  }
2750
2773
  ]
2751
2774
  },
@@ -2771,11 +2794,11 @@
2771
2794
  "hashes": [
2772
2795
  {
2773
2796
  "alg": "SHA-256",
2774
- "content": "f3f1b3893d805a45ecd2e887231ab25191417cc5e183c5e7005e316b3ecdc0ba"
2797
+ "content": "7425bd4cefc0185b889e7389a1bdb952ad4f576cab03e63928ad4b991f48ccc1"
2775
2798
  },
2776
2799
  {
2777
2800
  "alg": "SHA3-512",
2778
- "content": "1f6df4143f06877cbb2f3bb8ff1245bf0f5dfec9fac7511bd4576bdbe95e4061ddd042bfd9e47bd029d4dfd07439b8d53a3e0adb64f38036ea3a7b760db5e1c0"
2801
+ "content": "4b169deaefb58b617f0748a65409277c0592db50b7024f03cc290eda17299ec4aa4d682a294ba2f2251f015331ebbe07b3ca88549d354d5ee9b51589bd778ce7"
2779
2802
  }
2780
2803
  ]
2781
2804
  },
@@ -3619,6 +3642,21 @@
3619
3642
  }
3620
3643
  ]
3621
3644
  },
3645
+ {
3646
+ "bom-ref": "file:sources/README.md",
3647
+ "type": "file",
3648
+ "name": "sources/README.md",
3649
+ "hashes": [
3650
+ {
3651
+ "alg": "SHA-256",
3652
+ "content": "37356d40c4db4a0b5060911cd20f7a4ec3fc96c8b394308de646ba17e69493b7"
3653
+ },
3654
+ {
3655
+ "alg": "SHA3-512",
3656
+ "content": "963c00719163eedb931bcc4713be3f2aca2377f3d6492ff44c2d3e8dd37c6d6ffc4411ecfeda697c0406fffc2bb8eaeb38195b230d9e6a0ecfbfe4671ac0b7c3"
3657
+ }
3658
+ ]
3659
+ },
3622
3660
  {
3623
3661
  "bom-ref": "file:sources/validators/atlas-validator.js",
3624
3662
  "type": "file",
@@ -59,6 +59,10 @@ function catalogEntryCount(dataDir, file) {
59
59
  // emits one for.
60
60
  const SBOM_SELF_EXCLUDED = new Set(["sbom.cdx.json"]);
61
61
  const SBOM_DERIVABLE_PREFIXES = ["data/_indexes/"];
62
+ // Mirrors refresh-sbom's ALWAYS_SHIPPED: npm adds package.json to every tarball
63
+ // without it appearing in `files`, so the completeness check has to expect a
64
+ // component for it the same way the generator now emits one.
65
+ const SBOM_ALWAYS_SHIPPED = ["package.json", "sources/README.md"];
62
66
 
63
67
  function sbomIsDerivable(rel) {
64
68
  return SBOM_DERIVABLE_PREFIXES.some(
@@ -93,7 +97,7 @@ function walkFilesAbs(absDir) {
93
97
  // module-level root.
94
98
  function expandAllowlistAt(allowlist, root) {
95
99
  const abs = [];
96
- for (const entry of allowlist) {
100
+ for (const entry of [...allowlist, ...SBOM_ALWAYS_SHIPPED]) {
97
101
  const full = path.join(root, entry);
98
102
  let stat;
99
103
  try { stat = fs.statSync(full); }
@@ -12,9 +12,14 @@
12
12
  * - bomFormat / specVersion CycloneDX 1.6
13
13
  * - serialNumber urn:uuid v4 derived from a stable
14
14
  * hash of (project name + version +
15
- * timestamp) so reruns produce a new
16
- * UUID per refresh.
17
- * - metadata.timestamp ISO 8601 of generation
15
+ * bundle digest), so identical content
16
+ * reproduces the identical UUID and a
17
+ * rerun that changed nothing is a no-op
18
+ * rather than a spurious diff.
19
+ * - metadata.timestamp the release date this version's
20
+ * CHANGELOG heading declares — NOT the
21
+ * wall-clock moment of generation, which
22
+ * would make the artifact irreproducible
18
23
  * - metadata.tools this script itself, version pulled
19
24
  * from package.json at refresh time
20
25
  * - metadata.component application entry for exceptd-skills,
@@ -145,13 +150,31 @@ const SELF_EXCLUDED = new Set(['sbom.cdx.json']);
145
150
  * side check for the cache. */
146
151
  const DERIVABLE_PREFIXES = ['data/_indexes/'];
147
152
 
153
+ /* Files npm puts in every tarball regardless of the `files` allowlist.
154
+ * package.json is never listed in `files` (npm adds it unconditionally), so
155
+ * expanding the allowlist alone left the one file that declares the bin
156
+ * entrypoint, the engines floor and the dependency set outside the hashed
157
+ * inventory — the SBOM described 247 of the 268 files an operator receives and
158
+ * said nothing about the gap. It is not derivable and not self-referential, so
159
+ * nothing but the omission kept it out. README.md and LICENSE get the same
160
+ * unconditional treatment from npm but are already named in `files`, so a
161
+ * union covers the general rule without double-counting them. sources/README.md
162
+ * is here for the same reason: npm collects README files it finds, `files` never
163
+ * names that directory, and it shipped unhashed.
164
+ *
165
+ * This list is maintained by hand, which is only safe because something else
166
+ * checks it: lib/validate-package.js compares the real `npm pack` output against
167
+ * this SBOM, so a file npm decides to ship that is missing here fails a gate
168
+ * instead of shipping silently. */
169
+ const ALWAYS_SHIPPED = ['package.json', 'sources/README.md'];
170
+
148
171
  function isDerivable(rel) {
149
172
  return DERIVABLE_PREFIXES.some((p) => rel === p.replace(/\/$/, '') || rel.startsWith(p));
150
173
  }
151
174
 
152
175
  function expandAllowlist(allowlist) {
153
176
  const abs = [];
154
- for (const entry of allowlist) {
177
+ for (const entry of [...allowlist, ...ALWAYS_SHIPPED]) {
155
178
  const full = path.join(REPO_ROOT, entry);
156
179
  if (!fs.existsSync(full)) continue; // tolerate a stale entry; predeploy gate flags
157
180
  const stat = fs.statSync(full);
@@ -171,6 +194,38 @@ function expandAllowlist(allowlist) {
171
194
  return rel;
172
195
  }
173
196
 
197
+ /* metadata.timestamp — the release date this version's CHANGELOG heading
198
+ * declares, as an ISO-8601 instant.
199
+ *
200
+ * The field has to be deterministic: the SBOM-currency gate compares the
201
+ * committed artifact against a freshly generated one, so a wall-clock value
202
+ * would differ on every run and the comparison could never mean anything.
203
+ * The previous approach kept determinism by folding the bundle hash into a
204
+ * date offset, but the offset was a full uint32 of SECONDS — a 136-year
205
+ * spread — so the field routinely landed a century or more in the future
206
+ * (the shipped value read 2147-11-20). Deterministic, and untrue: consumers
207
+ * that sort or age-check SBOMs read that as a real creation date.
208
+ *
209
+ * The CHANGELOG heading is both deterministic and true. Its format is already
210
+ * enforced by scripts/check-changelog-extract.js, which the release flow runs
211
+ * before this script, so the date is guaranteed present by the time a release
212
+ * regenerates the SBOM. Refusing is deliberate when it is absent — inventing a
213
+ * placeholder is what produced the 2147 date in the first place. */
214
+ function releaseTimestamp(version) {
215
+ const changelogPath = path.join(REPO_ROOT, 'CHANGELOG.md');
216
+ const text = fs.readFileSync(changelogPath, 'utf8');
217
+ const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
218
+ const m = text.match(new RegExp('^## ' + escaped + ' [—-] (\\d{4}-\\d{2}-\\d{2})\\s*$', 'm'));
219
+ if (!m) {
220
+ throw new Error(
221
+ `refresh-sbom: CHANGELOG.md has no dated heading for ${version}. ` +
222
+ `Add "## ${version} — YYYY-MM-DD" before regenerating — metadata.timestamp ` +
223
+ `is the release date, and there is no honest value to fall back to.`,
224
+ );
225
+ }
226
+ return `${m[1]}T00:00:00.000Z`;
227
+ }
228
+
174
229
  function toPosixRel(absPath) {
175
230
  return path
176
231
  .relative(REPO_ROOT, absPath)
@@ -303,12 +358,7 @@ function buildSbom() {
303
358
  // time of a refresh should read the file's mtime or refresh-report.json.
304
359
  const seed = `${pkg.name}@${pkg.version}@${bundleSha}`;
305
360
  const serialNumber = 'urn:uuid:' + uuidV4FromSeed(seed);
306
- // Synthetic ISO timestamp derived from the seed — preserves the
307
- // CycloneDX 1.6 metadata.timestamp schema requirement (must be an
308
- // ISO-8601 string) while remaining content-stable.
309
- const seedHash = crypto.createHash('sha256').update(seed).digest();
310
- const offsetSeconds = seedHash.readUInt32BE(0); // deterministic offset
311
- const timestamp = new Date(Date.UTC(2026, 0, 1) + offsetSeconds * 1000).toISOString();
361
+ const timestamp = releaseTimestamp(pkg.version);
312
362
 
313
363
  const dataflowInput = catalogs
314
364
  .map((c) => `data/${c}`)
@@ -366,6 +416,22 @@ function buildSbom() {
366
416
  name: 'exceptd:integrity:method',
367
417
  value: 'Ed25519 per-skill (lib/sign.js)',
368
418
  },
419
+ // An operator verifying the bundle holds the SBOM, not this script.
420
+ // Without these two properties the only record of what the inventory
421
+ // deliberately omits was a source comment they never see, so a partial
422
+ // inventory was indistinguishable from a complete one. State the
423
+ // uncovered prefix and the check that covers it instead.
424
+ {
425
+ name: 'exceptd:integrity:uncovered:prefix',
426
+ value: DERIVABLE_PREFIXES.join(','),
427
+ },
428
+ {
429
+ name: 'exceptd:integrity:uncovered:rationale',
430
+ value:
431
+ 'Regenerated by `npm run build-indexes` and mutated by the test suite, so a ' +
432
+ 'pinned per-file hash would race any run between generation and verification. ' +
433
+ 'Covered instead by the pre-computed-index freshness gate in `npm run predeploy`.',
434
+ },
369
435
  {
370
436
  name: 'exceptd:runtime:dependency:count',
371
437
  value: String(Object.keys(pkg.dependencies || {}).length),
@@ -409,4 +475,11 @@ if (require.main === module) {
409
475
  main();
410
476
  }
411
477
 
412
- module.exports = { buildSbom, expandAllowlist, bundleDigest };
478
+ module.exports = {
479
+ buildSbom,
480
+ expandAllowlist,
481
+ bundleDigest,
482
+ releaseTimestamp,
483
+ ALWAYS_SHIPPED,
484
+ DERIVABLE_PREFIXES,
485
+ };
@@ -295,8 +295,15 @@ function cmdPrepare(opts) {
295
295
  _ok("signed + indexes + snapshot + sbom regenerated");
296
296
 
297
297
  _section("test-count baseline");
298
- // Growth is fine (the gate only fails on shrinkage), but refreshing keeps
299
- // the canonical-count guard meaningful when a release adds test files.
298
+ // Check BEFORE refreshing. `--update-baseline` writes whatever it observes,
299
+ // so calling it unconditionally rebaselined a shrunken suite downward and the
300
+ // shrinkage gate — which runs later, in `gates` — then compared the new count
301
+ // against itself and passed. Releasing was the one path that disarmed the
302
+ // guard, which is the path it exists to guard. Run the gate first so a drop
303
+ // stops the release here; refresh only once it has agreed nothing was lost,
304
+ // which still captures growth. A deliberate removal is re-baselined by hand
305
+ // with the command the gate's own failure message prints.
306
+ _run("node", ["scripts/check-test-count.js"]);
300
307
  _run("node", ["scripts/check-test-count.js", "--update-baseline"]);
301
308
 
302
309
  _section("codebase-patterns currency (advisory)");