@blamejs/exceptd-skills 0.19.33 → 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 (119) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/exceptd.js +896 -2824
  3. package/data/_indexes/_meta.json +2 -2
  4. package/lib/auto-discovery.js +56 -286
  5. package/lib/canonical-eq.js +7 -40
  6. package/lib/citation-resolve.js +22 -70
  7. package/lib/collectors/ai-api.js +20 -54
  8. package/lib/collectors/cicd-pipeline-compromise.js +40 -108
  9. package/lib/collectors/citation-hygiene.js +72 -210
  10. package/lib/collectors/containers.js +41 -130
  11. package/lib/collectors/cred-stores.js +31 -115
  12. package/lib/collectors/crypto-codebase.js +55 -138
  13. package/lib/collectors/crypto.js +24 -54
  14. package/lib/collectors/hardening.js +20 -78
  15. package/lib/collectors/kernel.js +16 -46
  16. package/lib/collectors/library-author.js +57 -206
  17. package/lib/collectors/mcp.js +24 -70
  18. package/lib/collectors/runtime.js +24 -86
  19. package/lib/collectors/sbom.js +34 -106
  20. package/lib/collectors/scan-excludes.js +31 -138
  21. package/lib/collectors/secrets.js +62 -178
  22. package/lib/cross-ref-api.js +39 -123
  23. package/lib/currency-severity.js +8 -27
  24. package/lib/cve-batch.js +13 -21
  25. package/lib/cve-cli.js +13 -20
  26. package/lib/cve-curation.js +72 -239
  27. package/lib/cve-regression-watcher.js +29 -152
  28. package/lib/cvss.js +13 -54
  29. package/lib/doctor-bucketing.js +3 -19
  30. package/lib/exit-codes.js +10 -42
  31. package/lib/flag-suggest.js +7 -25
  32. package/lib/framework-gap.js +35 -114
  33. package/lib/gap-detectors.js +37 -159
  34. package/lib/id-validation.js +9 -30
  35. package/lib/job-queue.js +13 -36
  36. package/lib/lint-skills.js +64 -232
  37. package/lib/playbook-runner.js +693 -2095
  38. package/lib/prefetch.js +100 -376
  39. package/lib/refresh-external.js +199 -627
  40. package/lib/refresh-network.js +75 -307
  41. package/lib/rfc-cli.js +23 -68
  42. package/lib/scoring.js +77 -145
  43. package/lib/sign.js +43 -229
  44. package/lib/source-advisories.js +43 -194
  45. package/lib/source-ghsa.js +37 -120
  46. package/lib/source-osv.js +94 -266
  47. package/lib/ttp-mapper.js +14 -24
  48. package/lib/upstream-check-cli.js +10 -28
  49. package/lib/upstream-check.js +19 -44
  50. package/lib/validate-catalog-meta.js +17 -61
  51. package/lib/validate-cve-catalog.js +43 -119
  52. package/lib/validate-indexes.js +25 -76
  53. package/lib/validate-package.js +16 -62
  54. package/lib/validate-playbooks.js +69 -275
  55. package/lib/validate-vendor.js +16 -49
  56. package/lib/verify.js +56 -286
  57. package/lib/version-pins.js +5 -34
  58. package/lib/worker-pool.js +11 -30
  59. package/lib/xml-tokenizer.js +47 -152
  60. package/manifest.json +53 -53
  61. package/orchestrator/dispatcher.js +17 -68
  62. package/orchestrator/event-bus.js +11 -74
  63. package/orchestrator/index.js +138 -412
  64. package/orchestrator/pipeline.js +28 -85
  65. package/orchestrator/scanner.js +34 -138
  66. package/orchestrator/scheduler.js +20 -84
  67. package/package.json +1 -1
  68. package/sbom.cdx.json +241 -241
  69. package/scripts/audit-catalog-gaps.js +9 -62
  70. package/scripts/audit-cross-skill.js +5 -31
  71. package/scripts/audit-perf.js +6 -16
  72. package/scripts/backfill-theater-test.js +7 -64
  73. package/scripts/bootstrap.js +12 -44
  74. package/scripts/build-indexes.js +40 -154
  75. package/scripts/builders/activity-feed.js +4 -14
  76. package/scripts/builders/catalog-summaries.js +3 -10
  77. package/scripts/builders/currency.js +7 -20
  78. package/scripts/builders/cwe-chains.js +7 -30
  79. package/scripts/builders/did-ladders.js +6 -13
  80. package/scripts/builders/frequency.js +5 -19
  81. package/scripts/builders/jurisdiction-clocks.js +6 -25
  82. package/scripts/builders/recipes.js +6 -14
  83. package/scripts/builders/section-offsets.js +13 -51
  84. package/scripts/builders/stale-content.js +7 -28
  85. package/scripts/builders/summary-cards.js +8 -29
  86. package/scripts/builders/theater-fingerprints.js +12 -27
  87. package/scripts/builders/token-budget.js +4 -31
  88. package/scripts/check-agents-md-collectors.js +11 -54
  89. package/scripts/check-catalog-gap-budget.js +15 -32
  90. package/scripts/check-changelog-extract.js +18 -48
  91. package/scripts/check-codebase-patterns-currency.js +6 -22
  92. package/scripts/check-codebase-patterns.js +50 -143
  93. package/scripts/check-epss-consistency.js +9 -64
  94. package/scripts/check-framework-gap-coverage.js +13 -31
  95. package/scripts/check-manifest-snapshot.js +13 -73
  96. package/scripts/check-sbom-currency.js +44 -142
  97. package/scripts/check-test-count.js +15 -52
  98. package/scripts/check-test-coverage.js +66 -197
  99. package/scripts/check-test-subjects.js +21 -62
  100. package/scripts/check-ttp-references.js +14 -38
  101. package/scripts/check-ttp-upstream.js +8 -40
  102. package/scripts/check-version-bump.js +9 -61
  103. package/scripts/check-version-tags.js +20 -121
  104. package/scripts/predeploy.js +38 -184
  105. package/scripts/refresh-manifest-snapshot.js +16 -38
  106. package/scripts/refresh-mitre-atlas.js +3 -8
  107. package/scripts/refresh-mitre-attack.js +1 -8
  108. package/scripts/refresh-mitre-d3fend.js +3 -9
  109. package/scripts/refresh-mitre-ics-attack.js +3 -8
  110. package/scripts/refresh-reverse-refs.js +27 -94
  111. package/scripts/refresh-rfc-index.js +2 -10
  112. package/scripts/refresh-sbom.js +31 -161
  113. package/scripts/refresh-upstream-catalogs.js +40 -137
  114. package/scripts/release.js +69 -232
  115. package/scripts/run-e2e-scenarios.js +24 -71
  116. package/scripts/sync-manifest-metadata.js +10 -34
  117. package/scripts/sync-package-description.js +8 -17
  118. package/scripts/validate-vendor-online.js +13 -44
  119. package/scripts/verify-shipped-tarball.js +35 -140
@@ -1,50 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * lib/source-advisories.js — primary-source advisory-feed polling.
4
+ * Polls primary-source advisory feeds that sit EARLIER in the disclosure
5
+ * pipeline than the kev / epss / nvd / ghsa / osv sources, which see only its
6
+ * last step and lag by 3-14 days. Report-only: applyDiff never writes.
5
7
  *
6
- * Why this exists. The post-mortem on CVE-2026-46333 (ssh-keysign-pwn,
7
- * disclosed 2026-05-14, missed by the toolkit at T+0 through T+3) found
8
- * that the existing source set (kev, epss, nvd, rfc, pins, ghsa, osv)
9
- * sits at the END of the disclosure pipeline. Qualys → kernel.org commit
10
- * → distro advisory → NVD enrichment is sequential; the existing pollers
11
- * only see the last step, which lags by 3-14 days.
12
- *
13
- * The 4 feeds below sit much earlier in the pipeline:
14
- *
15
- * Qualys TRU RSS — Qualys-disclosed CVEs at T+0 (the originator of
16
- * the ssh-keysign-pwn class of disclosure)
17
- * Red Hat RHSA RSS — RHEL security advisories at T+1, often before NVD
18
- * Ubuntu USN RSS — Ubuntu security notices at T+1, often before NVD
19
- * ZDI advisories — Zero Day Initiative + Pwn2Own disclosures at T+0
20
- *
21
- * Behaviour. Each call returns a structured REPORT — not a catalog mutation.
22
- * Operators consume the report via `exceptd refresh --check-advisories` and
23
- * decide which advisories warrant a `refresh --advisory <CVE-ID>` auto-import
24
- * to seed a draft entry. The report is informational; nothing is auto-written
25
- * to the catalog. This is the conservative-by-default contract: primary-source
26
- * surfacing must not silently mutate the catalog without operator triage.
27
- *
28
- * Output shape:
29
- * {
30
- * status: 'ok' | 'partial' | 'unreachable',
31
- * diffs: [
32
- * { id: 'CVE-2026-46333', source: 'qualys', advisory_url: '...',
33
- * disclosed_at: '2026-05-14', title: '...', in_catalog: false }
34
- * ],
35
- * summary: '4/4 feeds reachable; 3 new CVE references found'
36
- * }
37
- *
38
- * Each diff is read-only — there is no `applyDiff` that writes the catalog.
39
- * That's by design: a fresh advisory from a primary source has insufficient
40
- * fields to satisfy Hard Rule #1 (CVSS / KEV / PoC / AI-discovery /
41
- * active-exploitation / patch-availability). The operator routes the
42
- * promising ones through `refresh --advisory <CVE-ID>` which goes through
43
- * the existing GHSA / OSV / NVD enrichment path (those pollers are mature).
44
- *
45
- * Cache mode (--from-cache <dir>): expected cache layout is
46
- * <dir>/advisories/<feed>.xml — caller passes `ctx.cacheDir`.
47
- * Fixture mode: `ctx.fixtures.advisories = { qualys: '<xml>', ... }`.
8
+ * Cache mode (--from-cache <dir>) reads <dir>/advisories/<feed>.<ext> via
9
+ * `ctx.cacheDir`; fixture mode reads `ctx.fixtures.advisories[<feed>]`.
48
10
  */
49
11
 
50
12
  const path = require('path');
@@ -53,8 +15,7 @@ const { withRetry } = require('../vendor/blamejs/retry.js');
53
15
 
54
16
  const TODAY = new Date().toISOString().slice(0, 10);
55
17
 
56
- // Feed registry. Each entry has a kind (rss | json), a URL, and a parser.
57
- // Parsers return [{ cve_ids: [...], title, link, published }, ...].
18
+ // Feed registry; `kind` selects the parser checkFeed() applies.
58
19
  const FEEDS = [
59
20
  {
60
21
  name: 'qualys',
@@ -80,8 +41,6 @@ const FEEDS = [
80
41
  kind: 'rss',
81
42
  description: 'Zero Day Initiative — vendor-acknowledged advisories from ZDI + Pwn2Own pipeline',
82
43
  },
83
- // v0.13.3 additions — extend coverage to 4 more primary-source venues
84
- // identified in the v0.13.1 post-mortem follow-up:
85
44
  {
86
45
  name: 'kernel-org',
87
46
  url: 'https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/atom?h=master',
@@ -106,17 +65,6 @@ const FEEDS = [
106
65
  kind: 'rss',
107
66
  description: 'CISA cybersecurity advisories feed — federal-vendor coordinated disclosures (separate from KEV which captures only exploited-in-the-wild items)',
108
67
  },
109
- // v0.13.14 additions — closes the "silent kernel patch + delayed-research-
110
- // disclosure" intake gap surfaced by DirtyDecrypt (CVE-2026-31635). That
111
- // CVE was patched in mainline 2026-04-25, the kernel.org Atom-feed rolling
112
- // window rotated past the fix commit before the daily intake noticed, the
113
- // V12 rediscovery on 2026-05-09 went to maintainers privately rather than
114
- // to oss-security@openwall, and the PoC publication on 2026-05-17 surfaced
115
- // on vendor security blogs (Microsoft / Sysdig / Trail of Bits) that the
116
- // 8-feed primary-source set did not cover. Vendor security blogs are the
117
- // canonical signal channel for "kernel-class CVE patched silently, then
118
- // class-of-bug research published weeks later" — adding them closes the
119
- // class without polluting the catalog with news-aggregator noise.
120
68
  {
121
69
  name: 'microsoft-security-blog',
122
70
  url: 'https://www.microsoft.com/en-us/security/blog/feed/',
@@ -141,22 +89,6 @@ const FEEDS = [
141
89
  kind: 'rss',
142
90
  description: 'Embrace the Red (Johann Rehberger) — AI-tool prompt-injection + agentic-AI research. Anchored CVE-2025-53773 (Copilot YOLO mode) and the agentic-IDE host-execution class.',
143
91
  },
144
- // v0.13.17 additions — closes the "researcher GitHub-release drop +
145
- // tech-press writeup" intake gap surfaced by the four Nightmare-Eclipse
146
- // catalog entries (MiniPlasma / YellowKey / GreenPlasma / UnDefend).
147
- // The 12-feed set polled advisory venues + vendor research blogs but
148
- // had no coverage for either (a) named researcher handles whose prior
149
- // drops are already in the catalog (BlueHammer / CVE-2026-33825 was in,
150
- // four sibling drops were not) nor (b) the canonical tech-press
151
- // venues that surface "researcher dropped PoC on GitHub, no advisory
152
- // yet" events. The bleepingcomputer-security + thehackernews feeds
153
- // close (b); the nightmare-eclipse-gitlab feed closes (a) as the
154
- // anchor handle for the v0.13.17 backfill. NEW-CTRL-073 generalises
155
- // the handle-tracker control — additional handles get registered as
156
- // their drops land in the catalog. See also NEW-CTRL-074 (CVE-
157
- // regression-watcher) in lib/cve-regression-watcher.js for the
158
- // complementary detection method that catches re-broken historical
159
- // CVEs without depending on a new ID being assigned.
160
92
  {
161
93
  name: 'bleepingcomputer-security',
162
94
  url: 'https://www.bleepingcomputer.com/feed/',
@@ -178,15 +110,10 @@ const FEEDS = [
178
110
  },
179
111
  ];
180
112
 
181
- // Permissive CVE-ID matcher. The official format is CVE-YYYY-NNNN+ but
182
- // some feeds embed CVEs in arbitrary surrounding markup AND occasionally
183
- // emit lowercase "cve-yyyy-nnnn" in URLs or filenames. Case-insensitive
184
- // match, then uppercase + dedupe in extractCveIds().
113
+ // Permissive: some feeds emit lowercase "cve-yyyy-nnnn" in URLs and filenames,
114
+ // so the match is case-insensitive and extractCveIds() uppercases and dedupes.
185
115
  const CVE_RE = /CVE-(?:19|20)\d{2}-\d{4,7}/gi;
186
116
 
187
- /**
188
- * Extract CVE IDs from a string blob. De-duplicates within the blob.
189
- */
190
117
  function extractCveIds(text) {
191
118
  if (typeof text !== 'string' || text.length === 0) return [];
192
119
  const matches = text.match(CVE_RE);
@@ -194,23 +121,10 @@ function extractCveIds(text) {
194
121
  return [...new Set(matches.map((s) => s.toUpperCase()))];
195
122
  }
196
123
 
197
- /**
198
- * RSS / Atom parser. v0.13.20 replaces the original regex-based parser
199
- * (which silently failed on XML namespaces, nested CDATA, self-closing
200
- * tags, HTML entities, and multi-line content) with a proper streaming
201
- * XML tokenizer defined in lib/xml-tokenizer.js. Parser errors surface
202
- * via the second `errors` argument so consumers can observe parse
203
- * failures instead of receiving a silent empty array.
204
- *
205
- * Returns [{ title, link, published, body }, ...].
206
- */
207
124
  const { parseFeedDetailed: tokenizerParseFeedDetailed } = require('./xml-tokenizer');
208
125
 
209
- // Parser errors are ALWAYS collected and surfaced — the tokenizer's loud-error
210
- // contract is no longer opt-in. The optional `errors` array is filled when a
211
- // caller passes one (so a reachable-but-unparsable feed reads 'partial' in the
212
- // refresh report instead of '0 new CVEs'). A caller that forgets the array
213
- // still triggers the always-on collection via parseFeedDetailed.
126
+ // Returns [{ title, link, published, body }, ...]. Passing `errors` copies the
127
+ // tokenizer's parse errors out, so an unparsable feed reads 'partial', not '0'.
214
128
  function parseRssAtom(xml, errors = null) {
215
129
  const { items, errors: collected } = tokenizerParseFeedDetailed(xml);
216
130
  if (Array.isArray(errors)) {
@@ -220,11 +134,8 @@ function parseRssAtom(xml, errors = null) {
220
134
  }
221
135
 
222
136
  /**
223
- * CSAF index parser — Red Hat ships a plain-text index of advisory JSON
224
- * files under data/csaf/v2/advisories/YYYY/index.txt. Each line is a
225
- * relative filename. We don't fetch the per-advisory JSON in v0.13.1
226
- * (would blow the polling budget); we surface the advisory IDs that
227
- * mention CVE-YYYY-NNNN inline.
137
+ * Red Hat ships a plain-text index whose lines are advisory JSON filenames. The
138
+ * per-advisory JSON is never fetched, so only CVE IDs in a filename surface.
228
139
  */
229
140
  function parseCsafIndex(text) {
230
141
  if (typeof text !== 'string') return [];
@@ -236,20 +147,10 @@ function parseCsafIndex(text) {
236
147
  }
237
148
 
238
149
  /**
239
- * GitHub public-events parser — NEW-CTRL-073 researcher-handle tracker.
240
- *
241
- * Input is the JSON body of `https://api.github.com/users/<handle>/events/public`
242
- * (array of event objects). Output flattens each event into a standard
243
- * { title, link, published, body, researcher_handle } shape that
244
- * extractCveIds() can scan and the diff-deduplicator can route. Events of
245
- * interest: PushEvent (release commits), ReleaseEvent (tagged drops),
246
- * CreateEvent (tag / branch creation often signals an upcoming drop),
247
- * PublicEvent (a private repo flipped public — frequent drop pattern).
248
- *
249
- * The diff carries `researcher_handle` so callers + tests can assert the
250
- * handle-tracker fired even when no CVE ID is present in the commit text
251
- * (the MiniPlasma class — drop names a researcher-coined alias rather
252
- * than a CVE ID until MITRE / vendor assigns).
150
+ * Researcher-handle tracker (NEW-CTRL-073) over the JSON body of
151
+ * `https://api.github.com/users/<handle>/events/public`. Flattens each
152
+ * PushEvent / ReleaseEvent / CreateEvent / PublicEvent into the standard
153
+ * { title, link, published, body, researcher_handle } shape.
253
154
  */
254
155
  function parseGitHubEvents(body, feed) {
255
156
  let arr;
@@ -287,25 +188,15 @@ function parseGitHubEvents(body, feed) {
287
188
  }
288
189
 
289
190
  /**
290
- * GitLab public-activity parser — NEW-CTRL-073 researcher-handle tracker.
291
- *
292
- * GitLab publishes a public activity Atom feed at
293
- * https://gitlab.com/<handle>.atom. Each <entry> describes one activity
294
- * (pushed a tag, created a project/repository, pushed commits). We parse it
295
- * with the shared Atom tokenizer and classify the entry title into the same
296
- * event_type vocabulary the GitHub tracker emits, so the handle-drop diff in
297
- * checkFeed fires identically across both sources. researcher_handle comes
298
- * from the feed's explicit field (authoritative; the .atom URL also carries
299
- * the handle as a fallback). This is the migration target after the handle's
300
- * GitHub account was removed — the .atom feed needs no API token and the
301
- * existing parseRssAtom tokenizer already handles its XML.
191
+ * Researcher-handle tracker over a GitLab public-activity Atom feed
192
+ * (https://gitlab.com/<handle>.atom). Entry titles are classified into the same
193
+ * event_type vocabulary parseGitHubEvents emits, so checkFeed's handle-drop diff
194
+ * fires identically for both sources.
302
195
  */
303
196
  function parseGitLabActivity(body, feed, errorsOut = null) {
304
197
  const errors = [];
305
198
  const entries = parseRssAtom(body, errors);
306
- // Thread the Atom parse errors back to the caller's channel (checkFeed)
307
- // instead of dropping them — a reachable-but-unparsable GitLab activity feed
308
- // must read 'partial' in the refresh report, same as the RSS/Atom path.
199
+ // Thread the Atom parse errors back so this feed reads 'partial' like RSS.
309
200
  if (Array.isArray(errorsOut)) {
310
201
  for (const e of errors) errorsOut.push(e);
311
202
  }
@@ -319,8 +210,7 @@ function parseGitLabActivity(body, feed, errorsOut = null) {
319
210
  else if (/created (?:repository|project)|imported project/i.test(t)) event_type = 'PublicEvent';
320
211
  else if (/pushed (?:new commits|to)/i.test(t)) event_type = 'PushEvent';
321
212
  else if (/created branch/i.test(t)) event_type = 'CreateEvent';
322
- // Project path: GitLab activity titles read "<handle> pushed to project
323
- // <ns>/<repo>"; the entry link path is the secondary source.
213
+ // Activity titles read "<handle> pushed to project <ns>/<repo>"; link is fallback.
324
214
  let repo_name = '';
325
215
  const titleM = t.match(/project\s+(\S+\/\S+)/i);
326
216
  if (titleM) repo_name = titleM[1];
@@ -340,9 +230,6 @@ function parseGitLabActivity(body, feed, errorsOut = null) {
340
230
  });
341
231
  }
342
232
 
343
- /**
344
- * Fetch a feed body. In fixture / cache modes, read from disk.
345
- */
346
233
  async function fetchFeed(feed, ctx) {
347
234
  if (ctx.fixtures && ctx.fixtures.advisories && ctx.fixtures.advisories[feed.name]) {
348
235
  return { ok: true, body: ctx.fixtures.advisories[feed.name] };
@@ -354,10 +241,8 @@ async function fetchFeed(feed, ctx) {
354
241
  return { ok: true, body: fs.readFileSync(p, 'utf8') };
355
242
  }
356
243
  if (typeof fetch !== 'function') return { ok: false, error: 'fetch() not available — Node 18+ required' };
357
- // Adopt the vendored blamejs retry primitive (BJS-02): a transient feed
358
- // failure (HTTP 429/5xx, timeout/abort, network reset) is retried with
359
- // backoff + jitter instead of dropping the feed on the first hiccup. A
360
- // permanent 4xx is not retried. Each attempt gets a fresh abort timer.
244
+ // A transient failure (429/5xx, timeout/abort, network reset) is retried with
245
+ // backoff + jitter; a permanent 4xx is not. Each attempt gets its own timer.
361
246
  const attempt = async () => {
362
247
  const ac = new AbortController();
363
248
  const timer = setTimeout(() => ac.abort(), 8000);
@@ -384,21 +269,17 @@ async function fetchFeed(feed, ctx) {
384
269
  }
385
270
 
386
271
  /**
387
- * Walk one feed: fetch, parse, extract CVE IDs, compare to local catalog.
388
- * Returns { diffs, errors, status }.
272
+ * Walk one feed: fetch, parse, extract CVE IDs, compare to the local catalog.
273
+ * Returns { diffs, observations, errors, status, parse_errors }.
389
274
  */
390
275
  async function checkFeed(feed, ctx) {
391
276
  const res = await fetchFeed(feed, ctx);
392
277
  if (!res.ok) return { diffs: [], errors: 1, status: 'unreachable', _why: res.error };
393
278
  let items;
394
- // Parse errors are collected on the XML-parsing feed kinds so a reachable-
395
- // but-unparsable feed reads 'partial' in the report instead of silently
396
- // returning 0 new CVEs (the loud-error contract was opt-in and the live
397
- // path never opted in).
279
+ // Collected on the XML feed kinds so an unparsable feed reads 'partial'.
398
280
  const parseErrors = [];
399
281
  if (feed.kind === 'csaf-index') {
400
282
  items = parseCsafIndex(res.body);
401
- // Flatten cves_from_filename onto cve_ids field uniformly.
402
283
  items = items.map((it) => ({ ...it, cve_ids: it.cves_from_filename || [] }));
403
284
  } else if (feed.kind === 'github-events') {
404
285
  items = parseGitHubEvents(res.body, feed);
@@ -411,17 +292,11 @@ async function checkFeed(feed, ctx) {
411
292
  items = items.map((it) => ({ ...it, cve_ids: extractCveIds(`${it.title} ${it.body} ${it.link}`) }));
412
293
  }
413
294
  const diffs = [];
414
- // v0.13.17: surface EVERY extracted CVE-ID — including IDs already in
415
- // catalog — for NEW-CTRL-074 (cve-regression-watcher) consumption. The
416
- // diffs[] array remains filtered to NOT-IN-CATALOG so existing callers
417
- // see no behavior change, but a parallel observations[] carries the
418
- // full set with the in_catalog flag preserved. The regression watcher
419
- // routes historical-CVE references (e.g. CVE-2020-17103) through this
420
- // path so its action: "annotate" verdict can fire even when the
421
- // historical entry IS a catalog key.
295
+ // observations carries EVERY extracted CVE ID, in-catalog included, for the
296
+ // regression watcher (lib/cve-regression-watcher.js), whose "annotate" verdict
297
+ // fires on a catalog key. diffs[] stays filtered to not-in-catalog.
422
298
  const observations = [];
423
299
  for (const it of items) {
424
- // Standard CVE-ID-bearing diff (every feed kind).
425
300
  for (const cveId of it.cve_ids) {
426
301
  const inCatalog = !!ctx.cveCatalog[cveId];
427
302
  const baseRecord = {
@@ -437,11 +312,8 @@ async function checkFeed(feed, ctx) {
437
312
  diffs.push({ ...baseRecord });
438
313
  }
439
314
  }
440
- // NEW-CTRL-073 — researcher-handle diff. Github-events surfaces drop
441
- // activity even when no CVE ID is present yet (the MiniPlasma class:
442
- // researcher publishes alias name, vendor advisory + CVE assignment
443
- // arrive days / weeks later). The diff carries handle + event_type
444
- // so downstream triage can prioritise without waiting for an ID.
315
+ // Handle-drop diff: a researcher publishes an alias name before the advisory
316
+ // and CVE assignment, so the drop surfaces with handle + event_type.
445
317
  if ((feed.kind === 'github-events' || feed.kind === 'gitlab-activity') && it.cve_ids.length === 0 && (it.event_type === 'ReleaseEvent' || it.event_type === 'PublicEvent')) {
446
318
  diffs.push({
447
319
  id: `HANDLE:${it.researcher_handle || feed.name}:${it.repo_name || 'unknown'}@${(it.published || '').slice(0, 10)}`,
@@ -457,10 +329,8 @@ async function checkFeed(feed, ctx) {
457
329
  });
458
330
  }
459
331
  }
460
- // Fold reachable-but-unparsable into a 'partial' status via a NEW channel.
461
- // The integer `errors` field stays the unreachable count (0 here — the feed
462
- // WAS reached) so the aggregate unreachable===FEEDS.length math and the
463
- // refresh-* assertions that key off it keep working untouched.
332
+ // `errors` stays the UNREACHABLE count (0 here — the feed WAS reached), so the
333
+ // aggregate math keeps working. A parse failure travels on parse_errors.
464
334
  return {
465
335
  diffs,
466
336
  observations,
@@ -471,22 +341,15 @@ async function checkFeed(feed, ctx) {
471
341
  };
472
342
  }
473
343
 
474
- /**
475
- * The exported SOURCE definition, matching the shape ALL_SOURCES expects.
476
- */
344
+ /** The exported SOURCE definition, in the shape ALL_SOURCES expects. */
477
345
  const ADVISORIES_SOURCE = {
478
346
  name: 'advisories',
479
347
  description: 'Primary-source advisory feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, Zero Day Initiative / ZDI) — surfaces CVE IDs disclosed at T+0 to T+1 that lag NVD enrichment. Report-only — does not auto-write the catalog.',
480
348
  applies_to: 'data/cve-catalog.json',
481
- // The no-op applyDiff below already refuses to write, but the flag is what
482
- // the *callers* read. refresh-external only counts a source toward the
483
- // upsert total when `!report_only`, and the refresh workflow gates its apply
484
- // step and its entire open-pr job on that total being non-zero. This feed
485
- // reports every CVE seen across USN / RHSA / ZDI / CISA that the catalog
486
- // does not track — structurally in the thousands, and never zero, because
487
- // the catalog deliberately does not carry every Ubuntu kernel CVE. Leaving
488
- // the flag off therefore pinned the upsert count permanently above zero and
489
- // made "open a PR only when something changed" unconditional.
349
+ // Callers read this flag, not the no-op applyDiff: refresh-external counts a
350
+ // source toward the upsert total only when `!report_only`, and the refresh
351
+ // workflow gates its apply step and open-pr job on that total. This feed never
352
+ // reports zero, so clearing the flag opens a PR unconditionally, forever.
490
353
  report_only: true,
491
354
  async fetchDiff(ctx) {
492
355
  const results = await Promise.all(FEEDS.map((feed) => checkFeed(feed, ctx)));
@@ -508,9 +371,7 @@ const ADVISORIES_SOURCE = {
508
371
  }
509
372
  }
510
373
  }
511
- // Deduplicate by CVE-ID across feeds — multiple advisories for the
512
- // same CVE collapse to one entry with sources[] array of contributing
513
- // feed names.
374
+ // Collapse per CVE across feeds; sources[] lists every contributing feed.
514
375
  const byCve = new Map();
515
376
  for (const d of allDiffs) {
516
377
  if (!byCve.has(d.id)) {
@@ -526,18 +387,10 @@ const ADVISORIES_SOURCE = {
526
387
  delete d.advisory_url;
527
388
  return d;
528
389
  });
529
- // v0.13.17: observations are the FULL per-feed extraction list,
530
- // including in-catalog CVE-IDs. NEW-CTRL-074 cve-regression-watcher
531
- // consumes this so an "annotate" verdict can fire on historical IDs
532
- // (CVE-2020-17103) that are already keys in the catalog — diffs[]
533
- // alone would have filtered them out and broken the watcher's
534
- // annotate path. Dedupe + sources[] follow the diffs[] pattern so
535
- // downstream consumers see one record per CVE with all observing
536
- // feed names listed.
390
+ // Deduped like diffs[], but keeping in-catalog IDs for the annotate path.
537
391
  const obsByCve = new Map();
538
392
  for (const o of allObservations) {
539
- // The github-events handle-drop diffs use a HANDLE:* id shape and
540
- // are not CVE observations — skip them.
393
+ // HANDLE:* ids are handle drops, not CVE observations.
541
394
  if (!/^CVE-/.test(o.id)) continue;
542
395
  if (!obsByCve.has(o.id)) {
543
396
  obsByCve.set(o.id, {
@@ -554,9 +407,7 @@ const ADVISORIES_SOURCE = {
554
407
  }
555
408
  }
556
409
  const observations = Array.from(obsByCve.values());
557
- // Status ladder folds reachable-but-unparsable into 'partial'. The integer
558
- // `errors` field stays the unreachable count (downstream refresh math keys
559
- // off it); parse errors are surfaced via the separate parse_errors channel.
410
+ // Reachable-but-unparsable folds into 'partial'; `errors` stays unreachable-only.
560
411
  const status =
561
412
  unreachable === FEEDS.length ? 'unreachable' :
562
413
  (unreachable > 0 || parseErrorFeeds > 0) ? 'partial' :
@@ -573,8 +424,6 @@ const ADVISORIES_SOURCE = {
573
424
  summary,
574
425
  };
575
426
  },
576
- // Report-only: no applyDiff. Operators route promising CVE IDs through
577
- // `exceptd refresh --advisory <CVE-ID>` (GHSA / OSV / NVD enrichment).
578
427
  applyDiff(_ctx, _diffs) {
579
428
  return {
580
429
  updated: 0,