@blamejs/exceptd-skills 0.14.28 → 0.15.1

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 (61) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +5 -1
  4. package/bin/exceptd.js +0 -147
  5. package/data/_indexes/_meta.json +45 -45
  6. package/data/_indexes/section-offsets.json +804 -795
  7. package/data/_indexes/summary-cards.json +3 -3
  8. package/data/_indexes/token-budget.json +506 -501
  9. package/data/cve-catalog.json +154 -7
  10. package/data/zeroday-lessons.json +1 -1
  11. package/lib/gap-detectors.js +8 -2
  12. package/lib/lint-skills.js +13 -2
  13. package/lib/playbook-runner.js +0 -2
  14. package/lib/validate-cve-catalog.js +35 -12
  15. package/manifest.json +84 -84
  16. package/orchestrator/index.js +49 -5
  17. package/package.json +2 -2
  18. package/sbom.cdx.json +119 -119
  19. package/scripts/check-catalog-gap-budget.js +5 -1
  20. package/scripts/check-test-coverage.js +9 -0
  21. package/scripts/predeploy.js +8 -4
  22. package/skills/age-gates-child-safety/skill.md +7 -7
  23. package/skills/ai-attack-surface/skill.md +1 -1
  24. package/skills/ai-c2-detection/skill.md +3 -3
  25. package/skills/ai-risk-management/skill.md +9 -9
  26. package/skills/api-security/skill.md +4 -4
  27. package/skills/cloud-security/skill.md +7 -7
  28. package/skills/compliance-theater/skill.md +4 -4
  29. package/skills/container-runtime-security/skill.md +6 -6
  30. package/skills/coordinated-vuln-disclosure/skill.md +12 -12
  31. package/skills/defensive-countermeasure-mapping/skill.md +14 -10
  32. package/skills/dlp-gap-analysis/skill.md +3 -3
  33. package/skills/email-security-anti-phishing/skill.md +6 -6
  34. package/skills/exploit-scoring/skill.md +2 -2
  35. package/skills/framework-gap-analysis/skill.md +6 -6
  36. package/skills/fuzz-testing-strategy/skill.md +1 -1
  37. package/skills/global-grc/skill.md +2 -2
  38. package/skills/identity-assurance/skill.md +5 -5
  39. package/skills/idp-incident-response/skill.md +5 -5
  40. package/skills/incident-response-playbook/skill.md +8 -8
  41. package/skills/kernel-lpe-triage/skill.md +4 -4
  42. package/skills/mcp-agent-trust/skill.md +3 -3
  43. package/skills/mlops-security/skill.md +5 -5
  44. package/skills/ot-ics-security/skill.md +7 -7
  45. package/skills/policy-exception-gen/skill.md +2 -2
  46. package/skills/pqc-first/skill.md +2 -2
  47. package/skills/rag-pipeline-security/skill.md +2 -2
  48. package/skills/ransomware-response/skill.md +9 -9
  49. package/skills/researcher/skill.md +11 -11
  50. package/skills/sector-energy/skill.md +6 -6
  51. package/skills/sector-federal-government/skill.md +2 -2
  52. package/skills/sector-financial/skill.md +4 -4
  53. package/skills/sector-healthcare/skill.md +6 -6
  54. package/skills/sector-telecom/skill.md +1 -1
  55. package/skills/security-maturity-tiers/skill.md +4 -4
  56. package/skills/skill-update-loop/skill.md +6 -6
  57. package/skills/supply-chain-integrity/skill.md +1 -1
  58. package/skills/threat-model-currency/skill.md +3 -3
  59. package/skills/threat-modeling-methodology/skill.md +9 -9
  60. package/skills/webapp-security/skill.md +7 -7
  61. package/skills/zeroday-gap-learn/skill.md +8 -8
@@ -2279,7 +2279,30 @@
2279
2279
  ],
2280
2280
  "last_updated": "2026-05-15",
2281
2281
  "discovery_attribution_note": "Discovered by Rory McNamara of Snyk Security Labs as part of the four-vulnerability Leaky Vessels disclosure (CVE-2024-21626 + CVE-2024-23651/23652/23653) published January 2024. Named human researcher; no AI-tool credited. Source: https://labs.snyk.io/resources/leaky-vessels-docker-runc-container-breakout-vulnerabilities/.",
2282
- "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: +5 (75 -> 80)."
2282
+ "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: +5 (75 -> 80).",
2283
+ "vendor_advisories": [
2284
+ {
2285
+ "vendor": "NVD",
2286
+ "advisory_id": "CVE-2024-21626",
2287
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-21626",
2288
+ "severity": "high",
2289
+ "published_date": "2024-01-31"
2290
+ },
2291
+ {
2292
+ "vendor": "Snyk",
2293
+ "advisory_id": null,
2294
+ "url": "https://snyk.io/blog/leaky-vessels-docker-runc-container-breakout-vulnerabilities/",
2295
+ "severity": "high",
2296
+ "published_date": "2024-01-31"
2297
+ },
2298
+ {
2299
+ "vendor": "CISA KEV",
2300
+ "advisory_id": "CVE-2024-21626",
2301
+ "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
2302
+ "severity": "high",
2303
+ "published_date": "2024-04-08"
2304
+ }
2305
+ ]
2283
2306
  },
2284
2307
  "CVE-2024-3094": {
2285
2308
  "ai_assisted_weaponization": false,
@@ -2349,7 +2372,30 @@
2349
2372
  ],
2350
2373
  "last_updated": "2026-05-15",
2351
2374
  "discovery_attribution_note": "Discovered by Andres Freund (Microsoft engineer, PostgreSQL developer) on 2024-03-28 via a 0.5-second SSH-login latency regression traced to liblzma symbol resolution; reported to oss-security. Named human researcher; no AI tooling involved. Source: https://en.wikipedia.org/wiki/XZ_Utils_backdoor.",
2352
- "rwep_correction_note": "v0.12.30: canonicalized rwep_factors to satisfy Shape B invariant (Σ factors === rwep_score). Prior values used non-canonical weights and/or blast_radius > 30 (over-cap). Stored rwep_score unchanged; factor block now reproducible from canonical RWEP_WEIGHTS + operational fields."
2375
+ "rwep_correction_note": "v0.12.30: canonicalized rwep_factors to satisfy Shape B invariant (Σ factors === rwep_score). Prior values used non-canonical weights and/or blast_radius > 30 (over-cap). Stored rwep_score unchanged; factor block now reproducible from canonical RWEP_WEIGHTS + operational fields.",
2376
+ "vendor_advisories": [
2377
+ {
2378
+ "vendor": "NVD",
2379
+ "advisory_id": "CVE-2024-3094",
2380
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-3094",
2381
+ "severity": "critical",
2382
+ "published_date": "2024-03-29"
2383
+ },
2384
+ {
2385
+ "vendor": "openwall oss-security",
2386
+ "advisory_id": null,
2387
+ "url": "https://www.openwall.com/lists/oss-security/2024/03/29/4",
2388
+ "severity": "critical",
2389
+ "published_date": "2024-03-29"
2390
+ },
2391
+ {
2392
+ "vendor": "CISA KEV",
2393
+ "advisory_id": "CVE-2024-3094",
2394
+ "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
2395
+ "severity": "critical",
2396
+ "published_date": "2024-04-03"
2397
+ }
2398
+ ]
2353
2399
  },
2354
2400
  "CVE-2024-3154": {
2355
2401
  "ai_assisted_weaponization": false,
@@ -2536,7 +2582,23 @@
2536
2582
  "https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-352a"
2537
2583
  ],
2538
2584
  "last_updated": "2026-05-15",
2539
- "discovery_attribution_note": "Discovered during the SUNBURST incident-response investigation by FireEye / Mandiant analysts (publicly attributed to the Mandiant team rather than a single researcher) and corroborated by SolarWinds engineering. Documented in CISA AA20-352A and the CERT/CC VU#843464. Named human teams; pre-AI-tooling era for vendor-side attribution. Source: https://kb.cert.org/vuls/id/843464."
2585
+ "discovery_attribution_note": "Discovered during the SUNBURST incident-response investigation by FireEye / Mandiant analysts (publicly attributed to the Mandiant team rather than a single researcher) and corroborated by SolarWinds engineering. Documented in CISA AA20-352A and the CERT/CC VU#843464. Named human teams; pre-AI-tooling era for vendor-side attribution. Source: https://kb.cert.org/vuls/id/843464.",
2586
+ "vendor_advisories": [
2587
+ {
2588
+ "vendor": "NVD",
2589
+ "advisory_id": "CVE-2020-10148",
2590
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-10148",
2591
+ "severity": "critical",
2592
+ "published_date": "2020-12-29"
2593
+ },
2594
+ {
2595
+ "vendor": "CISA",
2596
+ "advisory_id": "AA20-352A",
2597
+ "url": "https://www.cisa.gov/news-events/cybersecurity-advisories/aa20-352a",
2598
+ "severity": "critical",
2599
+ "published_date": "2020-12-17"
2600
+ }
2601
+ ]
2540
2602
  },
2541
2603
  "CVE-2023-3519": {
2542
2604
  "ai_assisted_weaponization": false,
@@ -2599,7 +2661,23 @@
2599
2661
  ],
2600
2662
  "last_updated": "2026-05-15",
2601
2663
  "discovery_attribution_note": "Independent security researchers via Citrix coordinated disclosure (CTX561482, 2023-07-18); no individual researcher named in the Citrix advisory. NSA/CISA AA23-201A documents in-wild exploitation by Chinese state-sponsored actors. No AI-tool credited. Source: https://support.citrix.com/article/CTX561482/ and https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-201a.",
2602
- "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: +5 (75 -> 80)."
2664
+ "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: +5 (75 -> 80).",
2665
+ "vendor_advisories": [
2666
+ {
2667
+ "vendor": "NVD",
2668
+ "advisory_id": "CVE-2023-3519",
2669
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2023-3519",
2670
+ "severity": "critical",
2671
+ "published_date": "2023-07-19"
2672
+ },
2673
+ {
2674
+ "vendor": "Citrix",
2675
+ "advisory_id": "CTX561482",
2676
+ "url": "https://support.citrix.com/article/CTX561482",
2677
+ "severity": "critical",
2678
+ "published_date": "2023-07-18"
2679
+ }
2680
+ ]
2603
2681
  },
2604
2682
  "CVE-2024-1709": {
2605
2683
  "ai_assisted_weaponization": false,
@@ -2659,7 +2737,30 @@
2659
2737
  "https://www.connectwise.com/company/trust/security-bulletins/connectwise-screenconnect-23.9.8"
2660
2738
  ],
2661
2739
  "last_updated": "2026-05-15",
2662
- "discovery_attribution_note": "Discovered by ConnectWise security engineering and externally reported by Huntress + GreyNoise via in-wild exploitation telemetry within 24 hours of the 2024-02 Patch Tuesday. No individual researcher byline; vendor-internal discovery. No AI-tool credited. Source: https://www.upguard.com/blog/screenconnect-cve-2024."
2740
+ "discovery_attribution_note": "Discovered by ConnectWise security engineering and externally reported by Huntress + GreyNoise via in-wild exploitation telemetry within 24 hours of the 2024-02 Patch Tuesday. No individual researcher byline; vendor-internal discovery. No AI-tool credited. Source: https://www.upguard.com/blog/screenconnect-cve-2024.",
2741
+ "vendor_advisories": [
2742
+ {
2743
+ "vendor": "NVD",
2744
+ "advisory_id": "CVE-2024-1709",
2745
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1709",
2746
+ "severity": "critical",
2747
+ "published_date": "2024-02-19"
2748
+ },
2749
+ {
2750
+ "vendor": "ConnectWise",
2751
+ "advisory_id": null,
2752
+ "url": "https://www.connectwise.com/company/trust/security-bulletins/connectwise-screenconnect-23.9.8",
2753
+ "severity": "critical",
2754
+ "published_date": "2024-02-19"
2755
+ },
2756
+ {
2757
+ "vendor": "CISA KEV",
2758
+ "advisory_id": "CVE-2024-1709",
2759
+ "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
2760
+ "severity": "critical",
2761
+ "published_date": "2024-02-22"
2762
+ }
2763
+ ]
2663
2764
  },
2664
2765
  "CVE-2026-20182": {
2665
2766
  "ai_assisted_weaponization": false,
@@ -2721,7 +2822,30 @@
2721
2822
  ],
2722
2823
  "last_updated": "2026-05-15",
2723
2824
  "discovery_attribution_note": "Discovered by Stephen Fewer (Senior Principal Security Researcher) and Jonah Burgess (Senior Security Researcher), both at Rapid7, while researching the related CVE-2026-20127 vdaemon authentication-bypass. Named human researchers; no AI-tool credited. Source: https://www.rapid7.com/blog/post/ve-cve-2026-20182-critical-authentication-bypass-cisco-catalyst-sd-wan-controller-fixed/.",
2724
- "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: 0."
2825
+ "rwep_correction_note": "v0.12.30: canonicalized rwep_factors AND rwep_score to satisfy Shape B invariant. The prior stored rwep_score was internally inconsistent with its rwep_factors block; both now derived from canonical RWEP_WEIGHTS + operational fields. Delta from prior stored: 0.",
2826
+ "vendor_advisories": [
2827
+ {
2828
+ "vendor": "NVD",
2829
+ "advisory_id": "CVE-2026-20182",
2830
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-20182",
2831
+ "severity": "critical",
2832
+ "published_date": null
2833
+ },
2834
+ {
2835
+ "vendor": "Cisco",
2836
+ "advisory_id": null,
2837
+ "url": "https://sec.cloudapps.cisco.com/security/center/publicationListing.x",
2838
+ "severity": "critical",
2839
+ "published_date": null
2840
+ },
2841
+ {
2842
+ "vendor": "CISA KEV",
2843
+ "advisory_id": "CVE-2026-20182",
2844
+ "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
2845
+ "severity": "critical",
2846
+ "published_date": "2026-05-14"
2847
+ }
2848
+ ]
2725
2849
  },
2726
2850
  "CVE-2024-40635": {
2727
2851
  "ai_assisted_weaponization": false,
@@ -4418,7 +4542,30 @@
4418
4542
  ],
4419
4543
  "_draft": false,
4420
4544
  "last_updated": "2026-05-17",
4421
- "discovery_attribution_note": "Vendor-internal discovery by Fortinet PSIRT, disclosed 2024-02-08 via advisory FG-IR-24-015. No external researcher byline. CISA KEV-listed 2024-02-09 with a 7-day federal remediation deadline. Post-exploitation symlink-persistence technique documented in Fortinet's 2025-04-11 advisory after operators reported residual filesystem access on devices patched after compromise."
4545
+ "discovery_attribution_note": "Vendor-internal discovery by Fortinet PSIRT, disclosed 2024-02-08 via advisory FG-IR-24-015. No external researcher byline. CISA KEV-listed 2024-02-09 with a 7-day federal remediation deadline. Post-exploitation symlink-persistence technique documented in Fortinet's 2025-04-11 advisory after operators reported residual filesystem access on devices patched after compromise.",
4546
+ "vendor_advisories": [
4547
+ {
4548
+ "vendor": "NVD",
4549
+ "advisory_id": "CVE-2024-21762",
4550
+ "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-21762",
4551
+ "severity": "critical",
4552
+ "published_date": "2024-02-08"
4553
+ },
4554
+ {
4555
+ "vendor": "Fortinet",
4556
+ "advisory_id": "FG-IR-24-015",
4557
+ "url": "https://www.fortiguard.com/psirt/FG-IR-24-015",
4558
+ "severity": "critical",
4559
+ "published_date": "2024-02-08"
4560
+ },
4561
+ {
4562
+ "vendor": "CISA KEV",
4563
+ "advisory_id": "CVE-2024-21762",
4564
+ "url": "https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
4565
+ "severity": "critical",
4566
+ "published_date": "2024-02-09"
4567
+ }
4568
+ ]
4422
4569
  },
4423
4570
  "CVE-2025-10585": {
4424
4571
  "id": "CVE-2025-10585",
@@ -17,7 +17,7 @@
17
17
  "rebuild_after_days": 365,
18
18
  "note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
19
19
  },
20
- "entry_count": 68
20
+ "entry_count": 422
21
21
  },
22
22
  "CVE-2026-31431": {
23
23
  "name": "Copy Fail",
@@ -158,8 +158,14 @@ function temporalStalenessFindings(loaded, opts = {}) {
158
158
  }
159
159
 
160
160
  // CISA KEV due-date passed without remediation status — surfaces
161
- // operationally-stale entries the operator should re-verify.
162
- if (e.cisa_kev === true && typeof e.cisa_kev_due_date === "string") {
161
+ // operationally-stale CURATED entries the operator should re-verify.
162
+ // Auto-imported drafts are excluded: a KEV due-date passing with wall-clock
163
+ // time on the un-curated bulk-import backlog is expected (and grows the
164
+ // count purely by calendar drift, which would mechanically breach the
165
+ // budget gate on a no-op release). The finding is actionable only once the
166
+ // entry is curated, so it is scoped to non-draft entries.
167
+ const isDraft = e._auto_imported === true || e._draft === true;
168
+ if (!isDraft && e.cisa_kev === true && typeof e.cisa_kev_due_date === "string") {
163
169
  const sinceDue = daysSince(e.cisa_kev_due_date, now);
164
170
  if (sinceDue !== null && sinceDue > 0) {
165
171
  out.push({ class: "temporal-staleness", catalog: "cve-catalog", id,
@@ -119,7 +119,7 @@ const KEBAB_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
119
119
  const JSON_FILENAME_RE = /^[A-Za-z0-9._-]+\.json$/;
120
120
 
121
121
  function parseArgs(argv) {
122
- const opts = { skill: null, quiet: false };
122
+ const opts = { skill: null, quiet: false, strict: false };
123
123
  for (let i = 2; i < argv.length; i++) {
124
124
  const a = argv[i];
125
125
  if (a === '--skill') {
@@ -128,6 +128,11 @@ function parseArgs(argv) {
128
128
  opts.skill = a.slice('--skill='.length);
129
129
  } else if (a === '--quiet' || a === '-q') {
130
130
  opts.quiet = true;
131
+ } else if (a === '--strict') {
132
+ // Promote warnings (header-only sections, unresolved draft refs,
133
+ // playbook air-gap gaps) to release-blocking failures. Used by the
134
+ // predeploy gate so a warned regression cannot scroll past.
135
+ opts.strict = true;
131
136
  } else if (a === '--help' || a === '-h') {
132
137
  printHelp();
133
138
  process.exit(0);
@@ -856,7 +861,13 @@ function main() {
856
861
  console.log(
857
862
  `\n${passed}/${total} skills passed${warnSummary}${failed ? `, ${failed} failed` : ''}${orphanSummary}${airGapSummary}.`,
858
863
  );
859
- process.exit(failed === 0 && orphans.length === 0 ? 0 : 1);
864
+ // --strict treats any warning (per-skill or playbook air-gap) as a
865
+ // release-blocking failure so a warned regression cannot ship silently.
866
+ const strictFail = opts.strict && (warned > 0 || (airGapWarnings && airGapWarnings.length > 0));
867
+ if (strictFail) {
868
+ console.log(`[lint-skills] --strict: ${warned + (airGapWarnings ? airGapWarnings.length : 0)} warning(s) treated as failures.`);
869
+ }
870
+ process.exit(failed === 0 && orphans.length === 0 && !strictFail ? 0 : 1);
860
871
  }
861
872
 
862
873
  // Export the minimal frontmatter parser for downstream consumers
@@ -2912,8 +2912,6 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals,
2912
2912
  rwep_adjusted: analyze.rwep?.adjusted || 0,
2913
2913
  rwep_threshold_escalate: analyze.rwep?.threshold?.escalate || null,
2914
2914
  blast_radius_score: analyze.blast_radius_score || 0,
2915
- feeds_into: null, // populated by close()
2916
- jurisdiction_clocks_active: null, // populated by close()
2917
2915
  remediation_recommended: validate.selected_remediation?.id || null,
2918
2916
  }
2919
2917
  };
@@ -272,6 +272,21 @@ function additionalChecks(key, entry, ctx) {
272
272
  }
273
273
  }
274
274
 
275
+ // V5 — KEV status must carry its date (AGENTS.md Hard Rule #1: a KEV flag
276
+ // without a date is an incomplete threat-intel claim; RWEP scoring and the
277
+ // jurisdiction-clock SLAs key off the KEV listing date). cisa_kev=true with a
278
+ // null/missing/invalid cisa_kev_date is flagged; promoted to a hard error
279
+ // under --strict (predeploy). 0 violations in the shipped catalog today.
280
+ if (entry.cisa_kev === true) {
281
+ const d = entry.cisa_kev_date;
282
+ const dateOk = typeof d === 'string' && isUsableDate(d).ok;
283
+ if (!dateOk) {
284
+ warnings.push(
285
+ `${key}: cisa_kev=true but cisa_kev_date is ${JSON.stringify(d)} (KEV status must carry a valid listing date — Hard Rule #1)`,
286
+ );
287
+ }
288
+ }
289
+
275
290
  // V4 — Impossible-date guard.
276
291
  for (const f of DATE_FIELDS) {
277
292
  const v = entry[f];
@@ -352,18 +367,26 @@ function main() {
352
367
  }
353
368
  }
354
369
 
355
- // Guard the hand-maintained framework-control-gaps _meta.entry_count against
356
- // silent drift (it declared 184 while the file held 192 — 8 entries were
357
- // added without bumping the counter, and nothing caught it). Assert the
358
- // declared count equals the actual non-_meta entry count.
359
- if (frameworks && frameworks._meta && typeof frameworks._meta.entry_count === 'number') {
360
- const actualGapCount = Object.keys(frameworks).filter((k) => !k.startsWith('_')).length;
361
- if (frameworks._meta.entry_count !== actualGapCount) {
362
- process.stderr.write(
363
- `[validate-cve-catalog] FAIL: framework-control-gaps _meta.entry_count (${frameworks._meta.entry_count}) ` +
364
- `!= actual entry count (${actualGapCount}). Update _meta.entry_count to ${actualGapCount}.\n`,
365
- );
366
- process.exit(1);
370
+ // Guard hand-maintained _meta.entry_count fields against silent drift. The
371
+ // framework-control-gaps counter once declared 184 while the file held 192
372
+ // and nothing caught it; the zeroday-lessons counter drifted to 68 while the
373
+ // file held 422 because only framework-control-gaps was gated. This now
374
+ // checks EVERY loaded catalog that declares a numeric _meta.entry_count, so a
375
+ // new catalog with the field is covered automatically.
376
+ const ENTRY_COUNT_CATALOGS = [
377
+ { name: 'framework-control-gaps', catalog: frameworks },
378
+ { name: 'zeroday-lessons', catalog: lessons },
379
+ ];
380
+ for (const { name, catalog: cat } of ENTRY_COUNT_CATALOGS) {
381
+ if (cat && cat._meta && typeof cat._meta.entry_count === 'number') {
382
+ const actual = Object.keys(cat).filter((k) => !k.startsWith('_')).length;
383
+ if (cat._meta.entry_count !== actual) {
384
+ process.stderr.write(
385
+ `[validate-cve-catalog] FAIL: ${name} _meta.entry_count (${cat._meta.entry_count}) ` +
386
+ `!= actual entry count (${actual}). Update _meta.entry_count to ${actual}.\n`,
387
+ );
388
+ process.exit(1);
389
+ }
367
390
  }
368
391
  }
369
392