@blamejs/exceptd-skills 0.19.32 → 0.19.34

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 (127) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +8 -8
  4. package/data/_indexes/activity-feed.json +2 -2
  5. package/data/_indexes/catalog-summaries.json +7 -7
  6. package/data/_indexes/chains.json +60118 -0
  7. package/data/attack-techniques.json +267 -7
  8. package/data/cve-catalog.json +9991 -3
  9. package/data/cwe-catalog.json +109 -2
  10. package/data/framework-control-gaps.json +578 -3
  11. package/data/zeroday-lessons.json +8330 -1
  12. package/lib/auto-discovery.js +56 -286
  13. package/lib/canonical-eq.js +7 -40
  14. package/lib/citation-resolve.js +22 -70
  15. package/lib/collectors/ai-api.js +20 -54
  16. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  17. package/lib/collectors/citation-hygiene.js +72 -210
  18. package/lib/collectors/containers.js +41 -130
  19. package/lib/collectors/cred-stores.js +31 -115
  20. package/lib/collectors/crypto-codebase.js +55 -138
  21. package/lib/collectors/crypto.js +24 -54
  22. package/lib/collectors/hardening.js +20 -78
  23. package/lib/collectors/kernel.js +16 -46
  24. package/lib/collectors/library-author.js +57 -206
  25. package/lib/collectors/mcp.js +24 -70
  26. package/lib/collectors/runtime.js +24 -86
  27. package/lib/collectors/sbom.js +34 -106
  28. package/lib/collectors/scan-excludes.js +31 -138
  29. package/lib/collectors/secrets.js +62 -178
  30. package/lib/cross-ref-api.js +39 -123
  31. package/lib/currency-severity.js +8 -27
  32. package/lib/cve-batch.js +13 -21
  33. package/lib/cve-cli.js +13 -20
  34. package/lib/cve-curation.js +72 -239
  35. package/lib/cve-regression-watcher.js +29 -152
  36. package/lib/cvss.js +13 -54
  37. package/lib/doctor-bucketing.js +3 -19
  38. package/lib/exit-codes.js +10 -42
  39. package/lib/flag-suggest.js +7 -25
  40. package/lib/framework-gap.js +35 -114
  41. package/lib/gap-detectors.js +37 -159
  42. package/lib/id-validation.js +9 -30
  43. package/lib/job-queue.js +13 -36
  44. package/lib/lint-skills.js +64 -232
  45. package/lib/playbook-runner.js +693 -2095
  46. package/lib/prefetch.js +100 -376
  47. package/lib/refresh-external.js +199 -627
  48. package/lib/refresh-network.js +75 -307
  49. package/lib/rfc-cli.js +23 -68
  50. package/lib/scoring.js +77 -145
  51. package/lib/sign.js +43 -229
  52. package/lib/source-advisories.js +43 -194
  53. package/lib/source-ghsa.js +37 -120
  54. package/lib/source-osv.js +94 -266
  55. package/lib/ttp-mapper.js +14 -24
  56. package/lib/upstream-check-cli.js +10 -28
  57. package/lib/upstream-check.js +19 -44
  58. package/lib/validate-catalog-meta.js +17 -61
  59. package/lib/validate-cve-catalog.js +43 -119
  60. package/lib/validate-indexes.js +25 -76
  61. package/lib/validate-package.js +16 -62
  62. package/lib/validate-playbooks.js +69 -275
  63. package/lib/validate-vendor.js +16 -49
  64. package/lib/verify.js +56 -286
  65. package/lib/version-pins.js +5 -34
  66. package/lib/worker-pool.js +11 -30
  67. package/lib/xml-tokenizer.js +47 -152
  68. package/manifest.json +53 -53
  69. package/orchestrator/dispatcher.js +17 -68
  70. package/orchestrator/event-bus.js +11 -74
  71. package/orchestrator/index.js +138 -412
  72. package/orchestrator/pipeline.js +28 -85
  73. package/orchestrator/scanner.js +34 -138
  74. package/orchestrator/scheduler.js +20 -84
  75. package/package.json +2 -2
  76. package/sbom.cdx.json +253 -253
  77. package/scripts/audit-catalog-gaps.js +9 -62
  78. package/scripts/audit-cross-skill.js +5 -31
  79. package/scripts/audit-perf.js +6 -16
  80. package/scripts/backfill-theater-test.js +7 -64
  81. package/scripts/bootstrap.js +12 -44
  82. package/scripts/build-indexes.js +40 -154
  83. package/scripts/builders/activity-feed.js +4 -14
  84. package/scripts/builders/catalog-summaries.js +3 -10
  85. package/scripts/builders/currency.js +7 -20
  86. package/scripts/builders/cwe-chains.js +7 -30
  87. package/scripts/builders/did-ladders.js +6 -13
  88. package/scripts/builders/frequency.js +5 -19
  89. package/scripts/builders/jurisdiction-clocks.js +6 -25
  90. package/scripts/builders/recipes.js +6 -14
  91. package/scripts/builders/section-offsets.js +13 -51
  92. package/scripts/builders/stale-content.js +7 -28
  93. package/scripts/builders/summary-cards.js +8 -29
  94. package/scripts/builders/theater-fingerprints.js +12 -27
  95. package/scripts/builders/token-budget.js +4 -31
  96. package/scripts/check-agents-md-collectors.js +11 -54
  97. package/scripts/check-catalog-gap-budget.js +15 -32
  98. package/scripts/check-changelog-extract.js +18 -48
  99. package/scripts/check-codebase-patterns-currency.js +6 -22
  100. package/scripts/check-codebase-patterns.js +50 -143
  101. package/scripts/check-epss-consistency.js +9 -64
  102. package/scripts/check-framework-gap-coverage.js +13 -31
  103. package/scripts/check-manifest-snapshot.js +13 -73
  104. package/scripts/check-sbom-currency.js +44 -142
  105. package/scripts/check-test-count.js +15 -52
  106. package/scripts/check-test-coverage.js +66 -197
  107. package/scripts/check-test-subjects.js +21 -62
  108. package/scripts/check-ttp-references.js +14 -38
  109. package/scripts/check-ttp-upstream.js +8 -40
  110. package/scripts/check-version-bump.js +9 -61
  111. package/scripts/check-version-tags.js +20 -121
  112. package/scripts/predeploy.js +38 -184
  113. package/scripts/refresh-manifest-snapshot.js +16 -38
  114. package/scripts/refresh-mitre-atlas.js +3 -8
  115. package/scripts/refresh-mitre-attack.js +1 -8
  116. package/scripts/refresh-mitre-d3fend.js +3 -9
  117. package/scripts/refresh-mitre-ics-attack.js +3 -8
  118. package/scripts/refresh-reverse-refs.js +27 -94
  119. package/scripts/refresh-rfc-index.js +2 -10
  120. package/scripts/refresh-sbom.js +31 -161
  121. package/scripts/refresh-upstream-catalogs.js +40 -137
  122. package/scripts/release.js +69 -232
  123. package/scripts/run-e2e-scenarios.js +24 -71
  124. package/scripts/sync-manifest-metadata.js +10 -34
  125. package/scripts/sync-package-description.js +8 -17
  126. package/scripts/validate-vendor-online.js +13 -44
  127. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,62 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * lib/cve-regression-watcher.js — NEW-CTRL-074 detection method.
5
- *
6
- * The MiniPlasma class. A researcher republishes a working PoC against a
7
- * historical CVE (CVE-2020-17103, originally fixed December 2020) and
8
- * demonstrates the fix has been silently reverted or never landed. The
9
- * vendor does not issue a new CVE — the original ID is treated as
10
- * authoritative. NVD / KEV / OSV feeds will never surface this as a
11
- * current threat. Vendor advisory feeds (RHSA, USN, ZDI) won't either.
12
- * Vendor security blogs won't unless Microsoft Security Blog elects to
13
- * post about it (which, for a re-regression of their own fix, is a
14
- * commercial-incentive misalignment).
15
- *
16
- * What this catches: a poller diff (from lib/source-advisories.js) whose
17
- * extracted CVE-IDs include historical IDs (year <= currentYear - 2) that
18
- * may be silently regressed against current shipping product. The
19
- * detection method is signal-correlation, not source-extension:
20
- *
21
- * 1. Pull the union of cve_ids extracted from poller diffs in a given run.
22
- * 2. Filter to historical IDs (CVE-YYYY-NNN where YYYY <= currentYear - 2).
23
- * 3. For each historical hit, check whether the catalog has a parallel
24
- * "*-REREGRESSION-<year>" key whose `aliases[]` references it OR a
25
- * `discovery_attribution_note` that names a researcher republishing
26
- * the historical PoC.
27
- * 4. Surface the unmatched historical hits as candidate regressions —
28
- * operators triage and decide whether to (a) create a *-REREGRESSION-
29
- * catalog entry (the MiniPlasma path) or (b) annotate the existing
30
- * historical entry with a re-regression-confirmed flag.
31
- *
32
- * Output shape:
33
- * {
34
- * candidates: [
35
- * {
36
- * historical_cve: 'CVE-2020-17103',
37
- * surfaced_by: ['bleepingcomputer-security', 'thehackernews'],
38
- * first_seen_titles: ['MiniPlasma — Windows ...', ...],
39
- * existing_regression_key: 'CVE-2020-17103-REREGRESSION-2026' | null,
40
- * action: 'annotate' | 'create-regression-entry' | 'already-covered',
41
- * }
42
- * ],
43
- * historical_id_threshold_year: 2024,
44
- * evaluated_diffs: N,
45
- * }
46
- *
47
- * Design notes:
48
- *
49
- * - The threshold (year <= currentYear - 2) is deliberately permissive.
50
- * A re-regression of a 2024 CVE is still a regression. The threshold
51
- * keeps the watcher from firing on every fresh CVE poller diff.
52
- * - The watcher is REPORT-ONLY. It does not mutate the catalog. Operators
53
- * route candidates through `exceptd refresh --advisory <CVE-ID> --apply`
54
- * or via manual triage of a new *-REREGRESSION-<year> entry. This
55
- * matches ADVISORIES_SOURCE's conservative-by-default contract.
56
- * - The CVE_RE matcher (lib/source-advisories.js) already handles the
57
- * historical-CVE extraction step — the watcher consumes diffs, not raw
58
- * feed bodies. This keeps the watcher decoupled from feed-fetch logic
59
- * so it works in fixture mode, cache mode, and live mode identically.
4
+ * NEW-CTRL-074 detection: a historical CVE whose fix has been silently reverted
5
+ * and whose PoC still works against shipping product. The vendor issues no new
6
+ * CVE, so NVD, KEV, OSV and vendor advisory feeds never surface it. REPORT-ONLY:
7
+ * it never mutates the catalog; each candidate is triage input.
60
8
  */
61
9
 
62
10
  const path = require('path');
@@ -65,8 +13,7 @@ const fs = require('fs');
65
13
  const CVE_ID_RE = /^CVE-((?:19|20)\d{2})-\d{4,7}$/;
66
14
 
67
15
  /**
68
- * Extract the year from a CVE-YYYY-NNN identifier. Returns null for non-CVE
69
- * IDs (e.g. MAL-*, GHSA-*, the HANDLE:* shape from the github-events parser).
16
+ * Year from a CVE-YYYY-NNN identifier; null for other shapes (MAL-*, GHSA-*, HANDLE:*).
70
17
  */
71
18
  function cveYear(id) {
72
19
  if (typeof id !== 'string') return null;
@@ -75,10 +22,8 @@ function cveYear(id) {
75
22
  }
76
23
 
77
24
  /**
78
- * Find any catalog entry whose `aliases[]` includes the historical CVE-ID
79
- * OR whose key derives from it via the *-REREGRESSION-<year> convention.
80
- *
81
- * Returns the key (e.g. 'CVE-2020-17103-REREGRESSION-2026') or null.
25
+ * The catalog key covering `historicalId` — itself, a `<id>-REREGRESSION-<year>`
26
+ * key, or an entry listing it in `aliases[]`. Null when nothing covers it.
82
27
  */
83
28
  function findRegressionEntry(catalog, historicalId) {
84
29
  for (const key of Object.keys(catalog)) {
@@ -94,26 +39,13 @@ function findRegressionEntry(catalog, historicalId) {
94
39
  }
95
40
 
96
41
  /**
97
- * Walk a list of poller diffs (the shape lib/source-advisories.js produces)
98
- * and surface historical-CVE-ID references that are NOT yet covered by a
99
- * regression entry in the catalog.
100
- *
101
- * @param {Array<{id: string, source?: string, title?: string, sources?: string[]}>} diffs
102
- * @param {Object} catalog — data/cve-catalog.json shape, _meta key tolerated
103
- * @param {Object} opts — { now?: Date, threshold_years_ago?: number }
104
- * @returns {Object} report — { candidates, historical_id_threshold_year, evaluated_diffs }
42
+ * Surfaces historical-CVE references from poller diffs the catalog does not cover.
43
+ * @param {Object} catalog — data/cve-catalog.json shape, `_meta` key tolerated
44
+ * @param {Object} opts — { now?: Date, threshold_years_ago?: number } (default 2)
105
45
  */
106
- // v0.13.20 — content-pattern signals layered on top of the CVE-ID match.
107
- // The audit-class-2.4 problem: pre-v0.13.20, the watcher detected only
108
- // when a poller diff carried an extracted CVE-YYYY-NNN identifier. If a
109
- // researcher's writeup announces "the 2020 Forshaw fix is silently
110
- // reverted" without typing the CVE ID, the watcher missed the class
111
- // entirely. v0.13.20 adds content-pattern signals so the watcher can
112
- // flag candidates from prose alone.
113
46
 
114
- // Historical-regression language. Phrases that indicate a researcher is
115
- // claiming a fix was silently reverted, downgrade-rolled-back, or
116
- // otherwise re-broken.
47
+ // Content patterns layer on top of the CVE-id match: a writeup claiming the fix
48
+ // was silently reverted without typing the id is otherwise invisible.
117
49
  const HISTORICAL_REGRESSION_PHRASES = [
118
50
  /silently (re-?broken|reverted|regressed|rolled back)/i,
119
51
  /(fix|patch|mitigation) (was|is)? ?(silently )?(reverted|undone|removed|missing)/i,
@@ -125,11 +57,8 @@ const HISTORICAL_REGRESSION_PHRASES = [
125
57
  /vendor (declined|refused|never issued) (a )?new CVE/i,
126
58
  ];
127
59
 
128
- // Named-researcher patterns. Operator-curated names that have a prior
129
- // catalog-grade drop are tracked elsewhere (NEW-CTRL-073 handle tracker),
130
- // but the regression-watcher also looks for the names in poller-diff
131
- // content as an additional signal — a familiar handle re-disclosing an
132
- // old CVE is a higher-confidence regression candidate.
60
+ // A familiar handle re-disclosing an old CVE raises confidence; handles proper
61
+ // are tracked by NEW-CTRL-073, these names are only an extra signal.
133
62
  const RESEARCHER_NAME_PATTERNS = [
134
63
  /Nightmare-Eclipse/i,
135
64
  /Chaotic Eclipse/i,
@@ -140,8 +69,7 @@ const RESEARCHER_NAME_PATTERNS = [
140
69
  /Jann Horn/i,
141
70
  ];
142
71
 
143
- // Component-string detection — when a poller diff text mentions one of
144
- // these in conjunction with a regression phrase, flag as candidate.
72
+ // A candidate needs one of these alongside a regression phrase.
145
73
  const TRACKED_COMPONENT_TOKENS = [
146
74
  /cldflt\.sys/i,
147
75
  /\bldfltrl\.sys/i,
@@ -158,17 +86,14 @@ const TRACKED_COMPONENT_TOKENS = [
158
86
  function scanContentSignals(text) {
159
87
  if (typeof text !== "string" || !text) return {};
160
88
  const signals = {};
161
- // Historical-regression language hit.
162
89
  for (const re of HISTORICAL_REGRESSION_PHRASES) {
163
90
  const m = text.match(re);
164
91
  if (m) { signals.regression_language = m[0]; break; }
165
92
  }
166
- // Researcher-name hit.
167
93
  for (const re of RESEARCHER_NAME_PATTERNS) {
168
94
  const m = text.match(re);
169
95
  if (m) { signals.researcher = m[0]; break; }
170
96
  }
171
- // Component-token hit.
172
97
  const components = [];
173
98
  for (const re of TRACKED_COMPONENT_TOKENS) {
174
99
  const m = text.match(re);
@@ -186,24 +111,14 @@ function findRegressionCandidates(diffs, catalog, opts) {
186
111
 
187
112
  // Group historical-CVE refs by id so multi-feed surfacing collapses.
188
113
  const byHistoricalId = new Map();
189
- // Content-only candidates — surfaced by language/component pattern
190
- // matching even when no CVE ID was extracted from the diff text. Grouped
191
- // by a stable key (the diff id when present, else a composite of the
192
- // matched signal) so N duplicate rows for the same regression claim
193
- // across feeds collapse to one candidate with a merged surfaced_by — the
194
- // same group-by-merge-sources pattern the historical-CVE branch uses.
114
+ // Content-only candidates: keyed on the diff id when there is one, else on the
115
+ // matched signal, so duplicate rows for one claim collapse with merged surfaced_by.
195
116
  const byContentKey = new Map();
196
117
  for (const d of (diffs || [])) {
197
118
  if (!d || typeof d.id !== 'string') continue;
198
- // Title field name depends on input shape:
199
- // - ADVISORIES_SOURCE diffs[] carry `title` (post-dedupe string).
200
- // - ADVISORIES_SOURCE observations[] carry `first_title` (also a
201
- // string — the first occurrence across feeds). Pre-v0.13.20
202
- // fix (codex P1 PR #60): the watcher only read `title`, which
203
- // is undefined on observations[], so the content-pattern layer
204
- // never fired in the primary production path.
205
- // Advisory URL is `advisory_url` (string) on raw per-feed diffs and
206
- // `advisory_urls` (array) after dedupe in both shapes.
119
+ // The title is `title` on diffs[] and `first_title` on observations[] — the
120
+ // production path passes observations, so reading only `title` kills the content
121
+ // layer. URLs are `advisory_url` per feed, `advisory_urls` after dedupe.
207
122
  const titleField = d.title || d.first_title || '';
208
123
  const urls = Array.isArray(d.advisory_urls)
209
124
  ? d.advisory_urls.join(' ')
@@ -214,7 +129,6 @@ function findRegressionCandidates(diffs, catalog, opts) {
214
129
  (signals.researcher && signals.components));
215
130
  const year = cveYear(d.id);
216
131
  if (year !== null && year <= thresholdYear) {
217
- // CVE-ID-bearing historical reference (the original v0.13.17 path).
218
132
  if (!byHistoricalId.has(d.id)) byHistoricalId.set(d.id, { sources: new Set(), titles: [], signals: {} });
219
133
  const slot = byHistoricalId.get(d.id);
220
134
  if (Array.isArray(d.sources)) {
@@ -222,28 +136,15 @@ function findRegressionCandidates(diffs, catalog, opts) {
222
136
  } else if (typeof d.source === 'string') {
223
137
  slot.sources.add(d.source);
224
138
  }
225
- // Title may be carried as `title` (diffs[]) or `first_title`
226
- // (observations[]) — accept either to keep the historical-
227
- // candidate title list populated under both input shapes.
228
139
  const titleStr = (typeof d.title === 'string' && d.title) ? d.title
229
140
  : (typeof d.first_title === 'string' && d.first_title) ? d.first_title
230
141
  : '';
231
- // Dedupe titles before pushing — mirrors the content-only branch.
232
- // Without this, duplicate titles across feeds (a common multi-feed
233
- // surfacing pattern) consume distinct slots and evict genuinely
234
- // distinct titles from the 5-cap below.
142
+ // Dedupe before pushing, or repeated titles evict distinct ones from the 5-cap.
235
143
  if (titleStr && !slot.titles.includes(titleStr)) slot.titles.push(titleStr);
236
- // Merge content signals — the strongest signal wins.
237
144
  Object.assign(slot.signals, signals);
238
145
  continue;
239
146
  }
240
- // No historical CVE-ID in this diff. If content signals fire, still
241
- // surface as a content-only candidate so an operator can triage.
242
- // Group by a stable key so duplicate rows for the same claim across
243
- // multiple feeds merge their surfaced_by — mirrors byHistoricalId.
244
- // The diff id (a current-year / non-historical CVE id) is the cleanest
245
- // key when present; ID-less prose rows key off the matched signal
246
- // (regression phrase + researcher + sorted component tokens).
147
+ // No historical CVE id: a content signal is the only way the claim reaches triage.
247
148
  if (hasRegressionSignal) {
248
149
  const key = (typeof d.id === 'string' && d.id)
249
150
  ? `id:${d.id}`
@@ -266,9 +167,7 @@ function findRegressionCandidates(diffs, catalog, opts) {
266
167
  const existing = findRegressionEntry(catalog, id);
267
168
  let action;
268
169
  if (existing) {
269
- // The historical CVE is the catalog key itself — annotate it.
270
170
  if (existing === id) action = 'annotate';
271
- // Already covered by a *-REREGRESSION-<year> entry — nothing to do.
272
171
  else action = 'already-covered';
273
172
  } else {
274
173
  action = 'create-regression-entry';
@@ -284,9 +183,6 @@ function findRegressionCandidates(diffs, catalog, opts) {
284
183
  }
285
184
 
286
185
  candidates.sort((a, b) => a.historical_cve.localeCompare(b.historical_cve));
287
- // Emit one content-only candidate per grouped key, with surfaced_by as
288
- // the sorted union of every feed that surfaced the same regression claim
289
- // and the title list capped at 5 (matching the historical branch).
290
186
  const contentCandidates = [];
291
187
  for (const slot of byContentKey.values()) {
292
188
  contentCandidates.push({
@@ -304,43 +200,25 @@ function findRegressionCandidates(diffs, catalog, opts) {
304
200
  candidates,
305
201
  historical_id_threshold_year: thresholdYear,
306
202
  evaluated_diffs: evaluated,
307
- // Stamp from the same clock that derives the threshold year, so both
308
- // date-derived report fields share one instant. Falls back to call-time
309
- // `new Date()` (via the `now` default) when no clock is injected — never
310
- // a module-load constant, which would go stale in a long-lived process.
203
+ // The same clock that derives the threshold year; a module-load constant would
204
+ // go stale in a long-lived process.
311
205
  generated_at: now.toISOString().slice(0, 10),
312
206
  control_ref: 'NEW-CTRL-074',
313
207
  };
314
208
  }
315
209
 
316
210
  /**
317
- * Source-style wrapper so the watcher plugs into the refresh pipeline as a
318
- * peer of ADVISORIES_SOURCE. fetchDiff() consumes the prior
319
- * ADVISORIES_SOURCE.fetchDiff() result via ctx.advisoriesObservations
320
- * (preferred — the full extracted-CVE list including IDs already in
321
- * catalog, so the annotate verdict can fire on the MiniPlasma class) and
322
- * falls back to ctx.advisoriesDiffs when the advisories source ran on a
323
- * pre-v0.13.17 build that did not emit observations.
324
- *
325
- * Chaining is explicit — the watcher does not poll feeds itself. The
326
- * refresh orchestrator's sequential runner (lib/refresh-external.js#main)
327
- * threads the resolved advisories fetchDiff() result onto
328
- * ctx.advisoriesObservations + ctx.advisoriesDiffs immediately after the
329
- * advisories source resolves and BEFORE the next source's fetchDiff is
330
- * invoked, so order matters: advisories must run before
331
- * cve-regression-watcher in a multi-source invocation. Under --swarm
332
- * (Promise.all) the two sources cannot share ctx mid-flight, so the
333
- * orchestrator runs the watcher in a second pass after the parallel batch
334
- * resolves, reading observations from the resolved advisories outcome.
211
+ * Source-style wrapper so the watcher plugs into the refresh pipeline, reading
212
+ * ADVISORIES_SOURCE's result off `ctx.advisoriesObservations` or
213
+ * `ctx.advisoriesDiffs`. Ordering is load-bearing: advisories must run BEFORE
214
+ * this, and under --swarm the orchestrator runs it in a second pass.
335
215
  */
336
216
  const REGRESSION_WATCHER_SOURCE = {
337
217
  name: 'cve-regression-watcher',
338
218
  description: 'NEW-CTRL-074 detection method — surfaces poller-diff historical-CVE references that may indicate silent vendor regression (the MiniPlasma class). Report-only; depends on ADVISORIES_SOURCE observations as input.',
339
219
  applies_to: 'data/cve-catalog.json',
340
220
  async fetchDiff(ctx) {
341
- // Prefer observations (v0.13.17+ ADVISORIES_SOURCE return shape) over
342
- // diffs[] (pre-v0.13.17). observations[] preserves in-catalog CVE
343
- // refs which the annotate verdict requires.
221
+ // Observations preserve the in-catalog CVE refs the annotate verdict needs.
344
222
  const input = (ctx && ctx.advisoriesObservations) || (ctx && ctx.advisoriesDiffs) || [];
345
223
  const catalog = (ctx && ctx.cveCatalog) || {};
346
224
  const report = findRegressionCandidates(input, catalog, {});
@@ -357,7 +235,6 @@ const REGRESSION_WATCHER_SOURCE = {
357
235
  },
358
236
  };
359
237
  },
360
- // Report-only.
361
238
  applyDiff(_ctx, _diffs) {
362
239
  return {
363
240
  updated: 0,
package/lib/cvss.js CHANGED
@@ -1,51 +1,21 @@
1
1
  "use strict";
2
2
  /**
3
- * lib/cvss.js
4
- *
5
- * Shared CVSS metric-selection and vector-normalization helpers for every
6
- * site that ingests NIST NVD scoring (the cache-backed refresh diff, the
7
- * live per-CVE validator, and the KEV auto-discovery importer).
8
- *
9
- * Two properties of NVD's data make naive ingestion lossy:
10
- *
11
- * 1. NVD tags the legacy CVSS v2 metric as `type: "Primary"` on pre-v3
12
- * CVEs, while a modern v3.1 re-score (often supplied by a CNA) rides as
13
- * `type: "Secondary"`. Selecting a metric by `type === "Primary"` alone
14
- * therefore picks the *older* v2 score over a newer v3.1 one — silently
15
- * downgrading a curated v3.1 entry to v2 on every refresh.
16
- *
17
- * 2. NVD's `cvssMetricV2` entries carry a bare base vector
18
- * ("AV:N/AC:L/Au:N/C:C/I:C/A:C") with no "CVSS:2.0/" prefix, whereas
19
- * v3.x/v4.0 carry the prefix. The catalog schema (and
20
- * validate-cve-catalog --strict) require the canonical "CVSS:<x.y>/"
21
- * prefix, so writing a bare v2 vector produces an invalid entry.
22
- *
23
- * `selectNvdCvss` resolves (1) by preferring the newest CVSS version present
24
- * and choosing Primary only *within* that version; it resolves (2) by
25
- * normalizing the returned vector. Callers additionally guard against
26
- * cross-version downgrades using `cvssVersionOf` on the locally-curated
27
- * vector.
28
- *
29
- * Zero npm deps. Node stdlib only.
3
+ * CVSS metric selection and vector normalization for NVD ingestion. NVD tags
4
+ * its legacy v2 metric `type: "Primary"` on pre-v3 CVEs while a newer v3.1
5
+ * re-score rides as "Secondary", so selecting on `type` alone downgrades a
6
+ * curated entry to v2. `cvssMetricV2` also omits the "CVSS:2.0/" prefix the
7
+ * catalog schema requires.
30
8
  */
31
9
 
32
- // A bare (unprefixed) CVSS v2 base vector. v2 is the only version NVD emits
33
- // without a "CVSS:x/" prefix, and its grammar carries the Au: (Authentication)
34
- // metric that v3/v4 dropped — the unambiguous discriminator for a bare v2.
10
+ // A bare (unprefixed) CVSS v2 base vector; Au: is the metric v3/v4 dropped.
35
11
  const BARE_V2_RE = /^AV:[NAL]\/AC:[HML]\/Au:[MSN]\//;
36
12
 
37
- // The four canonical version prefixes the catalog accepts (mirrors
38
- // validate-cve-catalog.js STRICT_CVSS_PATTERN).
13
+ // Version prefixes the catalog accepts; mirrors STRICT_CVSS_PATTERN in validate-cve-catalog.js.
39
14
  const PREFIXED_RE = /^CVSS:(2\.0|3\.0|3\.1|4\.0)\//;
40
15
 
41
16
  /**
42
- * The CVSS version a vector declares, as a comparable number (2.0 < 3.0 < 3.1
43
- * < 4.0). Recognizes the four canonical "CVSS:x.y/" prefixes plus NVD's bare
44
- * v2 base vector. Returns null for anything unrecognized so callers can treat
45
- * an unknown version as "do not block" rather than mis-suppressing a diff.
46
- *
47
- * @param {string} vector
48
- * @returns {number|null}
17
+ * The CVSS version a vector declares, as a comparable number. Null when
18
+ * unrecognized, so callers treat an unknown version as "do not block".
49
19
  */
50
20
  function cvssVersionOf(vector) {
51
21
  if (typeof vector !== "string" || vector.length === 0) return null;
@@ -56,13 +26,8 @@ function cvssVersionOf(vector) {
56
26
  }
57
27
 
58
28
  /**
59
- * Ensure a vector carries a canonical "CVSS:x.y/" prefix. A bare v2 base
60
- * vector is prefixed with "CVSS:2.0/"; already-prefixed vectors (and anything
61
- * unrecognized) pass through unchanged. The output of a recognized vector
62
- * always satisfies validate-cve-catalog --strict.
63
- *
64
- * @param {string} vector
65
- * @returns {string}
29
+ * Prefixes a bare v2 base vector with "CVSS:2.0/"; already-prefixed and
30
+ * unrecognized vectors pass through unchanged.
66
31
  */
67
32
  function normalizeCvssVector(vector) {
68
33
  if (typeof vector !== "string" || vector.length === 0) return vector;
@@ -72,14 +37,8 @@ function normalizeCvssVector(vector) {
72
37
  }
73
38
 
74
39
  /**
75
- * Select the most authoritative CVSS metric from an NVD `metrics` object.
76
- * Prefers the newest CVSS version present (4.0 > 3.1 > 3.0 > 2.0); within the
77
- * chosen version prefers NVD's "Primary" analyst score over a "Secondary"
78
- * (CNA) one, falling back to the first entry when no Primary exists in that
79
- * version. The returned vector is normalized to the canonical prefix form.
80
- *
81
- * @param {object} metrics The `vulnerabilities[0].cve.metrics` object.
82
- * @returns {{version:number|null, baseScore:number|null, vector:string|null, source:string|null}|null}
40
+ * The most authoritative metric from an NVD `metrics` object: newest version
41
+ * wins, "Primary" beats "Secondary". Null when no CVSS metric is present.
83
42
  */
84
43
  function selectNvdCvss(metrics) {
85
44
  const m = metrics || {};
@@ -1,20 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  /**
4
- * lib/doctor-bucketing.js
4
+ * Buckets `exceptd doctor` per-check results into warning and error lists.
5
5
  *
6
- * Pure function used by the `exceptd doctor` verb to bucket per-check
7
- * results into "errors" vs "warnings" lists.
8
- *
9
- * v0.13.11 — extracted from bin/exceptd.js so the bucketing rule is
10
- * testable in isolation. The bug it fixes: severity governs bucketing,
11
- * not the `ok` field alone. A check that sets `ok: false` with
12
- * `severity: "warn"` (the signing-status check on a non-contributor
13
- * install — a nudge that the operator can enable signing if they want,
14
- * not a release-blocker) was previously routed to `failed_checks` and
15
- * tripped `all_green: false` with `issues_count: 1`, contradicting the
16
- * `[!! warn]` icon shown in the human-readable text mode. The fix:
17
- * `severity === "warn"` always wins, regardless of `ok`.
6
+ * Severity decides the bucket, not `ok`: `severity: "warn"` routes to warnings
7
+ * and `severity: "info"` routes to neither list, whatever `ok` says.
18
8
  */
19
9
 
20
10
  function bucketChecks(checks) {
@@ -22,12 +12,6 @@ function bucketChecks(checks) {
22
12
  const errorList = [];
23
13
  for (const [k, v] of Object.entries(checks || {})) {
24
14
  if (!v || typeof v !== "object") continue;
25
- // v0.13.13: severity:info is informational only — never routes to
26
- // either bucket regardless of `ok`. Lets a check report ok:false +
27
- // severity:info to mean "this surface is intentionally not enabled
28
- // here, not a problem" (consumer install with no private key, an
29
- // air-gap probe deliberately skipped, etc.) without polluting
30
- // warning or failure counts.
31
15
  if (v.severity === "info") continue;
32
16
  if (v.severity === "warn") {
33
17
  warnList.push(k);
package/lib/exit-codes.js CHANGED
@@ -1,18 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Canonical exit-code constants for every CLI verb.
5
- *
6
- * Every `process.exitCode = N` / `process.exit(N)` site in `bin/exceptd.js`
7
- * (and any library that wants to set an exit code via emit() ok:false bodies)
8
- * should reference one of these constants rather than a bare number literal.
9
- * The map is the source of truth for help text — `exceptd doctor --exit-codes`
10
- * dumps it as JSON so operator-facing docs cannot drift from runtime.
11
- *
12
- * History: prior to v0.12.24 codes were bare magic numbers scattered across
13
- * ~30 sites. Code 3 in particular meant both "session-id collision" (cmdRun)
14
- * and "ran-but-no-evidence" (cmdCi) — two semantics, one code, no doc surface.
15
- * v0.12.24 splits them and centralises so a new verb cannot regress by typo.
4
+ * Canonical exit-code constants for every CLI verb. Exit sites reference a
5
+ * constant, not a literal; `exceptd doctor --exit-codes` dumps this map as
6
+ * JSON, so help text cannot drift from runtime.
16
7
  */
17
8
 
18
9
  const EXIT_CODES = Object.freeze({
@@ -30,10 +21,6 @@ const EXIT_CODES = Object.freeze({
30
21
  WATCH_LOCK_CONTENTION: 75,
31
22
  });
32
23
 
33
- /**
34
- * Human-readable + machine-stable description per code. Source for the
35
- * `exceptd doctor --exit-codes` dump and for help-text rendering.
36
- */
37
24
  const EXIT_CODE_DESCRIPTIONS = Object.freeze({
38
25
  0: { name: 'SUCCESS', summary: 'Verb completed successfully.' },
39
26
  1: { name: 'GENERIC_FAILURE', summary: 'Unhandled error or validation failure.' },
@@ -49,17 +36,11 @@ const EXIT_CODE_DESCRIPTIONS = Object.freeze({
49
36
  75: { name: 'WATCH_LOCK_CONTENTION', summary: 'Watch daemon lock is held by a live process; retry after it exits (sysexits EX_TEMPFAIL). Distinct from LOCK_CONTENTION (8), the per-playbook attestation lock.' },
50
37
  });
51
38
 
52
- /**
53
- * Return the human-readable name for a numeric exit code.
54
- */
55
39
  function exitCodeName(code) {
56
40
  const e = EXIT_CODE_DESCRIPTIONS[code];
57
41
  return e ? e.name : 'UNKNOWN';
58
42
  }
59
43
 
60
- /**
61
- * Return all exit codes as a stable-shape array suitable for JSON dump.
62
- */
63
44
  function listExitCodes() {
64
45
  return Object.entries(EXIT_CODE_DESCRIPTIONS).map(([code, info]) => ({
65
46
  code: Number(code),
@@ -69,28 +50,15 @@ function listExitCodes() {
69
50
  }
70
51
 
71
52
  /**
72
- * Set the process exit code WITHOUT calling process.exit(). Returns to the
73
- * caller so any pending stdout/stderr writes drain on natural event-loop
74
- * shutdown.
75
- *
76
- * `process.exit(N)` terminates the process synchronously, which truncates
77
- * buffered async stdout when stdout is piped (CI, test harnesses, --json
78
- * consumers). The v0.11.10 CI #100 fix established the exitCode-then-return
79
- * idiom for that reason; every subsequent regression that re-introduced
80
- * bare process.exit() in the dispatch surface was the same class of bug.
81
- *
82
- * Callers SHOULD prefer this helper for any exit-after-stdout-write path.
83
- * Long-running daemons / tests that need synchronous termination can still
84
- * use process.exit() directly — that's intentional and not what this guards.
85
- *
86
- * @param {number} code Exit code (use the EXIT_CODES constants)
87
- * @returns {void}
53
+ * Set the process exit code WITHOUT calling process.exit(), so buffered
54
+ * stdout drains on natural event-loop shutdown; process.exit() terminates
55
+ * synchronously and truncates a piped stdout write. Use on any
56
+ * exit-after-stdout-write path — daemons and tests that need synchronous
57
+ * termination still call process.exit() directly.
88
58
  */
89
59
  function safeExit(code) {
90
- // Only override exitCode when it isn't already set to a non-zero value —
91
- // matches the emit() ok:false fallback contract so a caller that already
92
- // set BLOCKED (4) before emit() doesn't get overwritten by a later
93
- // GENERIC_FAILURE (1).
60
+ // A non-zero code already set wins: emit()'s ok:false fallback must not
61
+ // overwrite a caller's BLOCKED (4) with GENERIC_FAILURE (1).
94
62
  if (!process.exitCode || process.exitCode === 0) {
95
63
  process.exitCode = code;
96
64
  }
@@ -1,19 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * Levenshtein-distance flag-typo suggestions.
4
+ * Levenshtein-distance flag-typo suggestions against a verb-scoped allowlist.
5
5
  *
6
- * Operator typos `--evidnce` / `--csaf-stats` / `--bundle-epohc` were silently
7
- * absorbed by the argv parser, falling through as boolean true flags with no
8
- * value, then producing cryptic downstream errors. This helper compares an
9
- * unknown flag to a verb-scoped allowlist and returns the closest match at
10
- * distance ≤ 2 AND ≤ floor(flag.length / 2).
11
- *
12
- * Per-verb allowlists are the canonical CLI surface. Adding a new flag to a
13
- * verb means appending to the allowlist here AND updating the
14
- * printPlaybookVerbHelp block; keep the two in sync. doctor maintains its own
15
- * KNOWN_DOCTOR_FLAGS set in bin/exceptd.js — keep VERB_FLAG_ALLOWLIST.doctor
16
- * aligned with it (tests/lib-flag-suggest.test.js pins the shared flags).
6
+ * The allowlists are the canonical CLI surface: a new flag must be appended
7
+ * here AND to the printPlaybookVerbHelp block, and VERB_FLAG_ALLOWLIST.doctor
8
+ * must stay aligned with KNOWN_DOCTOR_FLAGS in bin/exceptd.js.
17
9
  */
18
10
 
19
11
  function editDistance(a, b) {
@@ -39,11 +31,8 @@ function editDistance(a, b) {
39
31
  }
40
32
 
41
33
  /**
42
- * Suggest the closest allowlisted flag to a given unknown flag.
43
- *
44
- * @param {string} flag - operator-supplied flag name without leading --
45
- * @param {string[]} allowlist - known flag names for the active verb
46
- * @returns {string|null} the suggested flag name or null when no close match
34
+ * The closest allowlisted flag, or null when none is close. `flag` is the
35
+ * operator-supplied name WITHOUT the leading `--`.
47
36
  */
48
37
  function suggestFlag(flag, allowlist) {
49
38
  if (typeof flag !== 'string' || flag.length === 0) return null;
@@ -62,11 +51,7 @@ function suggestFlag(flag, allowlist) {
62
51
  return best;
63
52
  }
64
53
 
65
- /**
66
- * Per-verb known-flag allowlist. Every operator-facing flag should appear
67
- * exactly once per verb where it is consumed. Flags consumed by every verb
68
- * (e.g. `pretty`, `json`, `help`) live under '_global'.
69
- */
54
+ /** Flags accepted by every verb live under '_global'. */
70
55
  const VERB_FLAG_ALLOWLIST = Object.freeze({
71
56
  _global: ['help', 'pretty', 'json', 'verbose', 'quiet'],
72
57
  run: [
@@ -118,9 +103,6 @@ const VERB_FLAG_ALLOWLIST = Object.freeze({
118
103
  prefetch: ['source', 'cache-dir', 'max-age', 'force', 'no-network'],
119
104
  });
120
105
 
121
- /**
122
- * Return the allowlist for a verb (global flags always included).
123
- */
124
106
  function flagsFor(verb) {
125
107
  const verbFlags = VERB_FLAG_ALLOWLIST[verb] || [];
126
108
  return [...VERB_FLAG_ALLOWLIST._global, ...verbFlags];