@blamejs/exceptd-skills 0.12.20 → 0.12.22

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +137 -6
  2. package/bin/exceptd.js +835 -70
  3. package/data/_indexes/_meta.json +14 -14
  4. package/data/_indexes/activity-feed.json +3 -3
  5. package/data/_indexes/catalog-summaries.json +3 -3
  6. package/data/_indexes/chains.json +15 -0
  7. package/data/_indexes/jurisdiction-map.json +3 -2
  8. package/data/_indexes/section-offsets.json +175 -175
  9. package/data/_indexes/summary-cards.json +1 -1
  10. package/data/_indexes/token-budget.json +83 -83
  11. package/data/cve-catalog.json +169 -2
  12. package/data/exploit-availability.json +16 -0
  13. package/data/playbooks/ai-api.json +18 -0
  14. package/data/playbooks/containers.json +30 -0
  15. package/data/playbooks/cred-stores.json +18 -0
  16. package/data/playbooks/crypto.json +18 -0
  17. package/data/playbooks/hardening.json +26 -1
  18. package/data/playbooks/kernel.json +22 -2
  19. package/data/playbooks/mcp.json +18 -0
  20. package/data/playbooks/runtime.json +22 -1
  21. package/data/playbooks/sbom.json +18 -0
  22. package/data/playbooks/secrets.json +6 -0
  23. package/data/zeroday-lessons.json +102 -0
  24. package/lib/auto-discovery.js +9 -9
  25. package/lib/cross-ref-api.js +43 -10
  26. package/lib/cve-curation.js +4 -4
  27. package/lib/playbook-runner.js +529 -70
  28. package/lib/prefetch.js +3 -3
  29. package/lib/refresh-external.js +13 -2
  30. package/lib/refresh-network.js +22 -17
  31. package/lib/scoring.js +22 -13
  32. package/lib/sign.js +5 -5
  33. package/lib/validate-catalog-meta.js +1 -1
  34. package/lib/validate-cve-catalog.js +2 -2
  35. package/lib/validate-indexes.js +2 -2
  36. package/lib/verify.js +63 -13
  37. package/manifest.json +47 -47
  38. package/package.json +1 -1
  39. package/sbom.cdx.json +6 -6
  40. package/scripts/check-manifest-snapshot.js +1 -1
  41. package/scripts/check-sbom-currency.js +1 -1
  42. package/scripts/predeploy.js +6 -6
  43. package/scripts/refresh-manifest-snapshot.js +2 -2
  44. package/scripts/validate-vendor-online.js +1 -1
  45. package/scripts/verify-shipped-tarball.js +15 -12
  46. package/skills/compliance-theater/skill.md +4 -1
  47. package/skills/exploit-scoring/skill.md +20 -1
  48. package/skills/framework-gap-analysis/skill.md +6 -2
  49. package/skills/kernel-lpe-triage/skill.md +50 -3
  50. package/skills/threat-model-currency/skill.md +7 -5
  51. package/skills/webapp-security/skill.md +1 -1
  52. package/skills/zeroday-gap-learn/skill.md +44 -1
@@ -62,7 +62,8 @@
62
62
  "cve_refs": [
63
63
  "CVE-2026-31431",
64
64
  "CVE-2026-43284",
65
- "CVE-2026-43500"
65
+ "CVE-2026-43500",
66
+ "CVE-2026-46300"
66
67
  ],
67
68
  "cwe_refs": [
68
69
  "CWE-732",
@@ -183,6 +184,30 @@
183
184
  "control_id": "Application Hardening / OS Hardening",
184
185
  "designed_for": "Hardening application + OS with documented baseline.",
185
186
  "insufficient_because": "Maturity Level 1-3 are progressive but each accepts attestation evidence at the time of assessment. No real-time drift requirement at any maturity level."
187
+ },
188
+ {
189
+ "framework": "nis2",
190
+ "control_id": "Art.21(2)(e)",
191
+ "designed_for": "Security in network and information systems acquisition, development and maintenance.",
192
+ "insufficient_because": "Names secure configuration as essential measure. Implementing acts have not bound kernel-level hardening flags or continuous attestation; classical CIS-benchmark posture satisfies the article's text."
193
+ },
194
+ {
195
+ "framework": "dora",
196
+ "control_id": "Art.9",
197
+ "designed_for": "ICT systems, protocols and tools — protective and preventative measures.",
198
+ "insufficient_because": "Article requires 'state-of-the-art' protective measures without enumerating kernel hardening flags. Financial-entity ICT can be DORA-compliant while kernel hardening posture remains weak relative to the current LPE threat landscape."
199
+ },
200
+ {
201
+ "framework": "uk-caf",
202
+ "control_id": "B4 — System security",
203
+ "designed_for": "NCSC CAF outcome that operational systems are protected against cyber attack.",
204
+ "insufficient_because": "Outcome-based. Achievable on a baseline-documented configuration even when live hardening flags (kernel.kptr_restrict, kernel.dmesg_restrict, kernel.yama.ptrace_scope) drift downward between assessments."
205
+ },
206
+ {
207
+ "framework": "au-ism",
208
+ "control_id": "ISM-1417",
209
+ "designed_for": "Australian Government ISM control on hardening operating system configurations.",
210
+ "insufficient_because": "Control evidences a hardened baseline at deployment. Post-deployment sysctl drift, manual override during incident response, or fleet-management config-push regressions are not detected by the ISM's evidence model."
186
211
  }
187
212
  ]
188
213
  },
@@ -70,12 +70,14 @@
70
70
  "cve_refs": [
71
71
  "CVE-2026-31431",
72
72
  "CVE-2026-43284",
73
- "CVE-2026-43500"
73
+ "CVE-2026-43500",
74
+ "CVE-2026-46300"
74
75
  ],
75
76
  "cwe_refs": [
76
77
  "CWE-416",
77
78
  "CWE-362",
78
- "CWE-787"
79
+ "CWE-787",
80
+ "CWE-672"
79
81
  ],
80
82
  "d3fend_refs": [
81
83
  "D3-KBPI",
@@ -186,6 +188,24 @@
186
188
  "control_id": "Art.21(2)(c)",
187
189
  "designed_for": "Vulnerability handling and disclosure for essential entities.",
188
190
  "insufficient_because": "Specifies process, not tempo. Permits patch-handling procedures that don't differentiate weaponized-in-hours kernel LPEs from theoretical issues."
191
+ },
192
+ {
193
+ "framework": "uk-caf",
194
+ "control_id": "B4 — System security",
195
+ "designed_for": "NCSC CAF outcome that operational systems are protected against cyber attack, including timely patching.",
196
+ "insufficient_because": "Outcome-based — does not bind a tempo to KEV-listed kernel LPEs. Regulator interpretation of 'timely' varies; a 30-day reading is defensible under CAF while the public PoC weaponizes in hours."
197
+ },
198
+ {
199
+ "framework": "au-essential-8",
200
+ "control_id": "Strategy 2 — Patch operating systems",
201
+ "designed_for": "ASD mitigation strategy for OS patching, with maturity levels 1-3.",
202
+ "insufficient_because": "Maturity Level 3 requires patching within 48 hours of vendor release for internet-facing services and 1 month for workstations. Neither tempo references CISA KEV or PoC availability; a kernel LPE with a same-day public exploit can satisfy ML3 while remaining exposed."
203
+ },
204
+ {
205
+ "framework": "au-ism",
206
+ "control_id": "ISM-1493",
207
+ "designed_for": "Australian Government ISM control for applying patches, updates, or vendor mitigations within 48 hours of release when an exploit exists.",
208
+ "insufficient_because": "48-hour clock anchors to vendor patch availability, not KEV listing. Kernels with live-patch capability go undocumented as a deployed mitigation; the control does not require live-patching as a fallback when reboot windows exceed 48 hours."
189
209
  }
190
210
  ]
191
211
  },
@@ -289,6 +289,24 @@
289
289
  "control_id": "Art.13 / Annex I",
290
290
  "designed_for": "Essential cybersecurity requirements for products with digital elements.",
291
291
  "insufficient_because": "Manufacturer of an MCP server is captured, but tool sideloading by developers is not — and the CRA's manufacturer obligations don't translate cleanly to maintainer-signed packages on npm/PyPI."
292
+ },
293
+ {
294
+ "framework": "uk-caf",
295
+ "control_id": "A4 — Supply chain",
296
+ "designed_for": "NCSC CAF outcome that supply-chain risks to the essential function are understood and managed.",
297
+ "insufficient_because": "Outcome is scoped to suppliers the org has a relationship with. Developer-installed MCP servers reach the engineering environment without ever being onboarded as a supplier; the outcome cannot be assessed against an inventory that does not include them."
298
+ },
299
+ {
300
+ "framework": "au-essential-8",
301
+ "control_id": "Strategy 3 — Application control",
302
+ "designed_for": "ASD mitigation strategy restricting execution to an approved set of applications.",
303
+ "insufficient_because": "Application control gates classical executables. MCP servers run inside the developer's IDE or AI client as plugin modules — they bypass the strategy's enforcement surface entirely on standard ML2/ML3 implementations."
304
+ },
305
+ {
306
+ "framework": "au-ism",
307
+ "control_id": "ISM-1546",
308
+ "designed_for": "Australian Government ISM control on software supply-chain risk management for sourced software.",
309
+ "insufficient_because": "Targets sourced software with a procurement trail. Developer-initiated MCP plugin installation produces no procurement artefact, so the control's evidence requirements cannot bind."
292
310
  }
293
311
  ]
294
312
  },
@@ -69,7 +69,10 @@
69
69
  "T1548.003"
70
70
  ],
71
71
  "cve_refs": [
72
- "CVE-2026-31431"
72
+ "CVE-2026-31431",
73
+ "CVE-2026-43284",
74
+ "CVE-2026-43500",
75
+ "CVE-2026-46300"
73
76
  ],
74
77
  "cwe_refs": [
75
78
  "CWE-269",
@@ -190,6 +193,24 @@
190
193
  "control_id": "Art.21(2)(i)",
191
194
  "designed_for": "Access control policies, including privileged accounts.",
192
195
  "insufficient_because": "Policy-shaped, not state-shaped. Permits policy compliance without state-of-host evidence."
196
+ },
197
+ {
198
+ "framework": "uk-caf",
199
+ "control_id": "B2 — Identity and access control",
200
+ "designed_for": "NCSC CAF outcome that the org understands, documents and manages access to networks and systems supporting the essential function.",
201
+ "insufficient_because": "Outcome is achievable on documented intent. State-of-host enumeration (live sudoers, SUID, world-writable, cron/timer) is not a required artefact; sustained drift between documented and actual access is invisible to the CAF assessor."
202
+ },
203
+ {
204
+ "framework": "au-essential-8",
205
+ "control_id": "Strategy 5 — Restrict admin privileges",
206
+ "designed_for": "ASD mitigation strategy limiting administrative privilege scope and re-validation cadence.",
207
+ "insufficient_because": "Targets explicit administrative accounts. SUID binaries, sudoers wildcards, and cron-as-root entries grant equivalent privilege without appearing in the strategy's admin-account inventory."
208
+ },
209
+ {
210
+ "framework": "au-ism",
211
+ "control_id": "ISM-1382",
212
+ "designed_for": "Australian Government ISM control on privileged-account management and audit.",
213
+ "insufficient_because": "Audit cadence anchors on account creation/modification events. Persistent SUID/cron drift between audits does not trigger an event, so the control's audit trail can be clean while the host state is not."
193
214
  }
194
215
  ]
195
216
  },
@@ -354,6 +354,24 @@
354
354
  "control_id": "Art.14",
355
355
  "designed_for": "Actively-exploited-vulnerability 24-hour notification.",
356
356
  "insufficient_because": "Notification window is tight; many orgs lack the SBOM-to-active-exploit correlation infrastructure to detect within window. Process gap, not control-text gap."
357
+ },
358
+ {
359
+ "framework": "uk-caf",
360
+ "control_id": "A4 — Supply chain",
361
+ "designed_for": "NCSC CAF outcome that supply-chain risks to the essential function are understood and managed.",
362
+ "insufficient_because": "Outcome is met by an SBOM-of-record at a point in time. Continuous transitive drift, VEX correlation, and AI-codegen provenance fall outside the outcome's reporting cadence; an annual CAF review can return 'achieved' against a stale inventory."
363
+ },
364
+ {
365
+ "framework": "au-essential-8",
366
+ "control_id": "Strategy 1 — Patch applications",
367
+ "designed_for": "ASD mitigation strategy for application patching, with maturity levels 1-3.",
368
+ "insufficient_because": "Strategy tempo (48-hour ML3 for internet-facing) assumes the org can enumerate which applications carry a CVE. Without continuous SBOM-to-CVE correlation, the patch clock does not start until the org happens to notice."
369
+ },
370
+ {
371
+ "framework": "au-ism",
372
+ "control_id": "ISM-1546",
373
+ "designed_for": "Australian Government ISM control on software supply-chain risk management for sourced software.",
374
+ "insufficient_because": "Control evidence is sourced-software-centric. Open-source transitive dependencies and AI-coding-assistant-emitted code are not 'sourced' under the ISM's evidence model; the control cannot bind their integrity."
357
375
  }
358
376
  ]
359
377
  },
@@ -204,6 +204,12 @@
204
204
  "control_id": "164.312(a)(2)(iv)",
205
205
  "designed_for": "Encryption + decryption — mechanism to encrypt and decrypt ePHI.",
206
206
  "insufficient_because": "Targets encryption mechanism, not key material storage in source artifacts."
207
+ },
208
+ {
209
+ "framework": "uk-caf",
210
+ "control_id": "B3 — Data security",
211
+ "designed_for": "NCSC CAF outcome that data important to the essential function is protected from compromise, including credential and key material.",
212
+ "insufficient_because": "Outcome can be assessed against managed secret-store contents. Plaintext credentials leaked into source-code repositories, CI logs, IaC files, and AI assistant context windows do not register on the outcome's evidence surface."
207
213
  }
208
214
  ]
209
215
  },
@@ -688,5 +688,107 @@
688
688
  "basis": "MCP ecosystem patch hygiene lags traditional CVE timelines. Most AI-agent operators do not maintain an explicit MCP tool allowlist; SI-10 audits accept the MCP plugin as a vendored dependency without auditing its argv handling.",
689
689
  "theater_pattern": "vendored_mcp_plugin_inherits_vendor_trust"
690
690
  }
691
+ },
692
+ "CVE-2026-46300": {
693
+ "name": "Fragnesia",
694
+ "lesson_date": "2026-05-14",
695
+ "attack_vector": {
696
+ "description": "Page-cache corruption via XFRM ESP-in-TCP skb coalescing in the Linux kernel. skb_try_coalesce() drops the SKBFL_SHARED_FRAG marker when coalescing paged fragments, so the kernel loses track of externally-backed fragments (page-cache pages spliced from a file). Unprivileged local user can overwrite read-only file data in the kernel page cache without touching the on-disk file. Public PoC targets /usr/bin/su for a deterministic root shell.",
697
+ "privileges_required": "unprivileged local user or container process",
698
+ "complexity": "deterministic, no race condition",
699
+ "ai_factor": "Human-discovered by William Bowling (V12 security team). Not AI-discovered, but lives in the same primitive class as the AI-discoverable Dirty Frag family — the upstream patch for Dirty Frag introduced this sibling bug."
700
+ },
701
+ "defense_chain": {
702
+ "prevention": {
703
+ "what_would_have_worked": "Module-unload mitigation: blacklist esp4 / esp6 / rxrpc in /etc/modprobe.d/ where IPsec and Kerberos-AFS are not required. Identical mitigation set to Dirty Frag (CVE-2026-43284 / CVE-2026-43500) — any host already mitigated for Dirty Frag by module blacklist is already mitigated for Fragnesia.",
704
+ "was_this_required": false,
705
+ "framework_requiring_it": null,
706
+ "adequacy": "Eliminates the attack surface on hosts that don't need ESP or RxRPC. Full kernel update via vendor channels closes the bug everywhere else; live-patch is the non-reboot path."
707
+ },
708
+ "detection": {
709
+ "what_would_have_worked": "Page-cache integrity monitoring — read a setuid binary through the page cache (`vmtouch` + `sha256sum`) and compare to a freshly-read-from-disk copy after `echo 3 > /proc/sys/vm/drop_caches`. Mismatch is the primary forensic signature.",
710
+ "was_this_required": false,
711
+ "framework_requiring_it": null,
712
+ "adequacy": "File-integrity tools that hash on-disk bytes (AIDE, Tripwire, IMA in measure-only mode) miss this entirely — the on-disk file is unchanged. Detection coverage requires page-cache-aware integrity checks, which no major framework requires."
713
+ },
714
+ "response": {
715
+ "what_would_have_worked": "Module unload + kernel update + venv-style page-cache flush of long-running processes that hold a setuid binary mapped read-only.",
716
+ "was_this_required": false,
717
+ "framework_requiring_it": null,
718
+ "adequacy": "Reduces blast radius post-exploitation. The corrupted page-cache entry survives until the affected page is evicted or the process re-maps the file — drop_caches is the operational reset."
719
+ }
720
+ },
721
+ "framework_coverage": {
722
+ "NIST-800-53-SI-2": {
723
+ "covered": true,
724
+ "adequate": false,
725
+ "gap": "30-day SLA is exploitation window for a deterministic public PoC. Module-unload mitigation is non-reboot and available immediately but is not a required compensating control under SI-2."
726
+ },
727
+ "ISO-27001-2022-A.8.8": {
728
+ "covered": true,
729
+ "adequate": false,
730
+ "gap": "Appropriate timescales undefined; standard 30-day interpretation is unsafe for deterministic LPE with public PoC."
731
+ },
732
+ "NIS2-Art21-2c": {
733
+ "covered": true,
734
+ "adequate": false,
735
+ "gap": "Patch-management measures are undefined for fast-cycle kernel LPEs with public PoC."
736
+ },
737
+ "DORA-Art9": {
738
+ "covered": true,
739
+ "adequate": false,
740
+ "gap": "ICT incident management presumes vendor-patch cadence; module-unload as immediate mitigation has no place in the typical DORA evidence pack."
741
+ },
742
+ "UK-CAF-B4": {
743
+ "covered": true,
744
+ "adequate": false,
745
+ "gap": "System security principle is silent on subsystem module disable as a compensating control for unpatched kernel LPE."
746
+ },
747
+ "AU-ISM-1546": {
748
+ "covered": true,
749
+ "adequate": "partial",
750
+ "gap": "Essential 8 patch-applications maturity ladder anchors on advisory date, not on PoC availability. ML3 48h is still long for a deterministic public exploit."
751
+ },
752
+ "FILE-INTEGRITY-CONTROLS": {
753
+ "covered": false,
754
+ "adequate": false,
755
+ "gap": "AIDE / Tripwire / IMA in measure-only mode hash on-disk bytes and miss page-cache-resident corruption entirely. No framework requires page-cache-aware integrity verification."
756
+ }
757
+ },
758
+ "new_control_requirements": [
759
+ {
760
+ "id": "NEW-CTRL-016",
761
+ "name": "PAGE-CACHE-INTEGRITY-VERIFICATION",
762
+ "description": "For setuid binaries on production hosts: periodically (or on alert) read the binary through the page cache, drop caches, re-read from disk, and compare hashes. Mismatch indicates page-cache-resident corruption that on-disk-only file-integrity tools cannot detect.",
763
+ "evidence": "CVE-2026-46300 (Fragnesia) — corrupts the page-cache copy of /usr/bin/su without modifying the on-disk file. AIDE / Tripwire / IMA measure-only show clean.",
764
+ "gap_closes": [
765
+ "FILE-INTEGRITY-CONTROLS"
766
+ ]
767
+ },
768
+ {
769
+ "id": "NEW-CTRL-017",
770
+ "name": "BUG-FAMILY-MITIGATION-PERSISTENCE",
771
+ "description": "When a CVE patch lands, retain the pre-patch compensating controls (module blacklists, sysctl restrictions) until the patched code has soaked for a stated review period. Patches for one bug in a primitive class can introduce sibling bugs in the same class; the original compensating control may still apply to the sibling.",
772
+ "evidence": "CVE-2026-46300 (Fragnesia) — introduced by the patch for CVE-2026-43284 / CVE-2026-43500 (Dirty Frag). The esp4 / esp6 / rxrpc blacklist used for Dirty Frag mitigation is identically effective against Fragnesia.",
773
+ "gap_closes": [
774
+ "NIST-800-53-SI-2",
775
+ "NIS2-Art21-2c"
776
+ ]
777
+ },
778
+ {
779
+ "id": "NEW-CTRL-018",
780
+ "name": "SCANNER-PAPER-COMPLIANCE-TEST",
781
+ "description": "A vulnerability scanner that reports 'patched' based on kernel package version alone is paper compliance. The operational test: does the scan account for the module-unload mitigation surface, AND does it verify the kernel is on a build that includes the specific Fragnesia patch (not just any version newer than the Dirty Frag patch that introduced Fragnesia)?",
782
+ "evidence": "CVE-2026-46300 — kernels patched for Dirty Frag but predating the Fragnesia patch report 'patched for CVE-2026-43284' while still exposed to CVE-2026-46300.",
783
+ "gap_closes": [
784
+ "ISO-27001-2022-A.8.8"
785
+ ]
786
+ }
787
+ ],
788
+ "compliance_exposure_score": {
789
+ "percent_audit_passing_orgs_still_exposed": 75,
790
+ "basis": "Operators who already blacklisted esp4 / esp6 / rxrpc for Dirty Frag are already mitigated. Operators who relied on kernel-package-version alone (a scanner saying 'patched') with vanilla SI-2 / A.8.8 SLAs are exposed during the patch window. Exposure drops sharply if CISA KEV-lists the CVE (federal 21-day SLA fires) or if active exploitation is observed.",
791
+ "theater_pattern": "patch_management"
792
+ }
691
793
  }
692
794
  }
@@ -31,7 +31,7 @@ const fs = require("fs");
31
31
  const path = require("path");
32
32
  const { scoreCustom, RWEP_WEIGHTS, ACTIVE_EXPLOITATION_LADDER } = require("./scoring");
33
33
 
34
- // audit M P1-C: stored rwep_factors must reproduce the stored rwep_score.
34
+ // C: stored rwep_factors must reproduce the stored rwep_score.
35
35
  // `buildScoringInputs` is the single source of truth for both — it captures
36
36
  // the conservative defaults applied to a freshly-imported KEV draft (CISA
37
37
  // only lists vulnerabilities with documented exploitation, so we assume a
@@ -56,7 +56,7 @@ function buildScoringInputs(kevEntry /*, nvdPayload */) {
56
56
  };
57
57
  }
58
58
 
59
- // audit X P1: cve-catalog.schema.json's `rwep_factors` requires the post-weight
59
+ // cve-catalog.schema.json's `rwep_factors` requires the post-weight
60
60
  // numeric shape (cisa_kev: 0|25, poc_available: 0|20, ai_factor: 0|15,
61
61
  // active_exploitation: 0|5|10|20, blast_radius: 0..30, patch_available: 0|-15,
62
62
  // live_patch_available: 0|-10, reboot_required: 0|5). Pre-fix the auto-discovery
@@ -89,7 +89,7 @@ function toPostWeightFactors(inputs) {
89
89
  }
90
90
 
91
91
  /**
92
- * Audit M P3-O — diff severity nuance for KEV-discovered drafts.
92
+ * O — diff severity nuance for KEV-discovered drafts.
93
93
  *
94
94
  * Pre-fix every KEV-derived diff carried `severity: "high"`. Operators
95
95
  * scanning the diff stream had no way to distinguish "patch in 21 days"
@@ -199,13 +199,13 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
199
199
  const knownRansomware =
200
200
  String(kevEntry.knownRansomwareCampaignUse || "").toLowerCase() === "known";
201
201
 
202
- // audit M P1-C: stored rwep_factors and computed rwep_score MUST agree.
202
+ // C: stored rwep_factors and computed rwep_score MUST agree.
203
203
  // Previously rwep_factors held nulls (for unknown poc/ai/reboot) but
204
204
  // rwep_score was computed from concrete defaults (poc=true, reboot=true).
205
205
  // `scoring.validate()` then flagged every auto-imported draft for
206
206
  // divergence > 5. Now: one canonical input object → both surfaces.
207
207
  //
208
- // audit X P1: the catalog's JSON-schema for `rwep_factors` requires the
208
+ // the catalog's JSON-schema for `rwep_factors` requires the
209
209
  // POST-WEIGHT numeric shape (ai_factor / numeric ladder contributions /
210
210
  // numeric ±deductions) — not the SHAPE-A boolean + string-ladder shape
211
211
  // that scoreCustom consumes. Pre-fix the boolean shape was stored
@@ -264,11 +264,11 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
264
264
  "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
265
265
  kevEntry.notes ? String(kevEntry.notes) : null,
266
266
  ].filter(Boolean),
267
- // v0.12.15 (audit M P1-B): schema requires source_verified to be a
267
+ // v0.12.15 (B): schema requires source_verified to be a
268
268
  // YYYY-MM-DD string; the prior `false` boolean (then null) produced
269
269
  // entries that failed strict catalog validation.
270
270
  //
271
- // audit X P1: the CISA KEV listing IS the verification source for a
271
+ // the CISA KEV listing IS the verification source for a
272
272
  // KEV-discovered draft — the entry's `verification_sources` array
273
273
  // already points to the KEV catalog URL, and KEV's appearance is what
274
274
  // triggered the auto-import. Pre-fix the field stayed null, which
@@ -282,7 +282,7 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
282
282
  source_verified: TODAY,
283
283
  last_updated: TODAY,
284
284
  last_verified: TODAY,
285
- // v0.12.15 (audit M P1-D): `_auto_imported` must be the boolean `true`
285
+ // v0.12.15 (D): `_auto_imported` must be the boolean `true`
286
286
  // for lib/validate-cve-catalog.js's draft-recognition check (strict
287
287
  // `=== true` comparison). The prior object-shape was non-recognizable
288
288
  // and the strict validator treated KEV-discovered drafts as
@@ -577,7 +577,7 @@ async function discoverNewRfcs(ctx, opts = {}) {
577
577
  skills_referencing: [],
578
578
  errata_count: null,
579
579
  last_verified: TODAY,
580
- // v0.12.15 (audit M P1-D, P3-T): boolean `_auto_imported: true` for
580
+ // v0.12.15 (D, P3-T): boolean `_auto_imported: true` for
581
581
  // strict-validator recognition; provenance moved to sibling
582
582
  // `_auto_imported_meta`. Errata-URL hint converted to a real template
583
583
  // literal so the rfc number actually interpolates (the previous double-
@@ -19,9 +19,16 @@ const ROOT = path.join(__dirname, '..');
19
19
  const DATA_DIR = process.env.EXCEPTD_DATA_DIR || path.join(ROOT, 'data');
20
20
  const INDEX_DIR = path.join(DATA_DIR, '_indexes');
21
21
 
22
+ // DD P1-1: cache entries store the parsed payload AND the mtimeMs of the
23
+ // source file at parse-time. Each load call re-stats the file; if mtime
24
+ // matches, the cached value is returned (one syscall, no parse). If mtime
25
+ // changed, re-parse + repopulate. If stat fails (file vanished mid-run,
26
+ // permission glitch), fall back to the cached value. Pre-fix the cache was
27
+ // process-lifetime — long-running `orchestrator watch` processes never saw
28
+ // `data/cve-catalog.json` mutations driven by `exceptd refresh --apply`.
22
29
  const _cache = new Map();
23
30
 
24
- // v0.12.14 (audit C-F7): catalog corruption no longer crashes the runner
31
+ // v0.12.14: catalog corruption no longer crashes the runner
25
32
  // uncaught. A malformed JSON file in data/ used to produce a SyntaxError
26
33
  // at require-time of any consumer (lib/playbook-runner.js), which threw
27
34
  // out of the run() entrypoint without honoring AGENTS.md's "non-zero
@@ -30,42 +37,56 @@ const _cache = new Map();
30
37
  // can inspect.
31
38
  const _loadErrors = [];
32
39
 
40
+ function _statMtime(p) {
41
+ try { return fs.statSync(p).mtimeMs; }
42
+ catch { return null; }
43
+ }
44
+
33
45
  function loadCatalog(filename) {
34
- if (_cache.has(filename)) return _cache.get(filename);
35
46
  const full = path.join(DATA_DIR, filename);
47
+ const mtime = _statMtime(full);
48
+ const cached = _cache.get(filename);
49
+ if (cached && (mtime === null || cached.mtime === mtime)) {
50
+ return cached.value;
51
+ }
36
52
  if (!fs.existsSync(full)) {
37
- _cache.set(filename, {});
53
+ _cache.set(filename, { value: {}, mtime });
38
54
  return {};
39
55
  }
40
56
  try {
41
57
  const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
42
- _cache.set(filename, parsed);
58
+ _cache.set(filename, { value: parsed, mtime });
43
59
  return parsed;
44
60
  } catch (e) {
45
61
  _loadErrors.push({ kind: 'catalog', file: filename, error: e.message });
46
62
  const stub = {};
47
63
  Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
48
- _cache.set(filename, stub);
64
+ _cache.set(filename, { value: stub, mtime });
49
65
  return stub;
50
66
  }
51
67
  }
52
68
 
53
69
  function loadIndex(filename) {
54
- if (_cache.has('idx:' + filename)) return _cache.get('idx:' + filename);
55
70
  const full = path.join(INDEX_DIR, filename);
71
+ const mtime = _statMtime(full);
72
+ const key = 'idx:' + filename;
73
+ const cached = _cache.get(key);
74
+ if (cached && (mtime === null || cached.mtime === mtime)) {
75
+ return cached.value;
76
+ }
56
77
  if (!fs.existsSync(full)) {
57
- _cache.set('idx:' + filename, {});
78
+ _cache.set(key, { value: {}, mtime });
58
79
  return {};
59
80
  }
60
81
  try {
61
82
  const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
62
- _cache.set('idx:' + filename, parsed);
83
+ _cache.set(key, { value: parsed, mtime });
63
84
  return parsed;
64
85
  } catch (e) {
65
86
  _loadErrors.push({ kind: 'index', file: filename, error: e.message });
66
87
  const stub = {};
67
88
  Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
68
- _cache.set('idx:' + filename, stub);
89
+ _cache.set(key, { value: stub, mtime });
69
90
  return stub;
70
91
  }
71
92
  }
@@ -84,11 +105,23 @@ function entries(catalog) {
84
105
  * Full correlation for a CVE ID. Returns the catalog entry plus everything
85
106
  * that references it across skills, framework gaps, theater fingerprints,
86
107
  * recipes, and zero-day lessons.
108
+ *
109
+ * FF P1-4: auto-imported drafts (entries with `_auto_imported === true`) are
110
+ * EXCLUDED by default. Drafts carry conservative-default mechanical fields
111
+ * and null analytical fields pending curation; downstream analyze / bundle
112
+ * emitters that assume `byCve()` returns curated data would treat the draft's
113
+ * placeholders as authoritative. The cve-curation flow (which surfaces the
114
+ * editorial questionnaire) opts in via `byCve(id, { include_drafts: true })`.
115
+ * Every other caller stays on the default exclude path.
87
116
  */
88
- function byCve(cveId) {
117
+ function byCve(cveId, opts) {
118
+ const includeDrafts = !!(opts && opts.include_drafts);
89
119
  const catalog = loadCatalog('cve-catalog.json');
90
120
  const entry = catalog[cveId];
91
121
  if (!entry) return { found: false, cve_id: cveId };
122
+ if (!includeDrafts && entry._auto_imported === true) {
123
+ return { found: false, cve_id: cveId, _draft_excluded: true };
124
+ }
92
125
 
93
126
  const xref = loadIndex('xref.json');
94
127
  const recipes = loadIndex('recipes.json');
@@ -43,7 +43,7 @@ const path = require("path");
43
43
  // before deciding promotion.
44
44
  const { withCatalogLock } = require("./refresh-external");
45
45
  const { validate: validateAgainstSchema } = require("./validate-cve-catalog");
46
- // audit J F3: derive rwep_score via the canonical scoring helper rather
46
+ // derive rwep_score via the canonical scoring helper rather
47
47
  // than a blind `Object.values(...).reduce(sum)`. The helper detects shape
48
48
  // (boolean inputs → scoreCustom; post-weight numeric inputs → sum + clamp)
49
49
  // so the curation apply-path produces a score that matches whatever the
@@ -58,7 +58,7 @@ let _cveSchemaCache = null;
58
58
  function loadCveEntrySchema() {
59
59
  if (_cveSchemaCache) return _cveSchemaCache;
60
60
  try {
61
- // v0.12.15 (audit M P1-A): the prior version of this function looked for
61
+ // v0.12.15 (A): the prior version of this function looked for
62
62
  // either `root.patternProperties["^CVE-\\d{4}-\\d+$"]` or an object
63
63
  // `root.additionalProperties`. The actual schema at lib/schemas/cve-
64
64
  // catalog.schema.json has NEITHER — its top level IS the entry shape
@@ -279,7 +279,7 @@ function buildQuestionnaire(cveId, draft) {
279
279
  // Pull candidate catalogs. Each is optional — missing catalogs are skipped
280
280
  // gracefully. J7 makes these one-shot loads per process.
281
281
  const atlas = loadJson("data/atlas-ttps.json");
282
- // v0.12.15 (audit M P1-E): the catalog ships as data/attack-techniques.json
282
+ // v0.12.15 (E): the catalog ships as data/attack-techniques.json
283
283
  // (renamed from data/attack-ttps.json before the v0.12.12 release; the
284
284
  // canonical file path is also what lib/validate-cve-catalog.js consumes).
285
285
  // The prior `data/attack-ttps.json` lookup silently fell back to an empty
@@ -570,7 +570,7 @@ function applyAnswersUnderLock(cveId, catalog, catalogPath, answers) {
570
570
  appliedFields.push(field);
571
571
  }
572
572
 
573
- // audit J F3: derive rwep_score via the canonical scoring helper rather
573
+ // derive rwep_score via the canonical scoring helper rather
574
574
  // than a blind sum. deriveRwepFromFactors detects shape (boolean inputs
575
575
  // → scoreCustom; post-weight numeric inputs → sum + clamp) and routes
576
576
  // accordingly, so the apply-path produces a score that agrees with