@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.
- package/CHANGELOG.md +10 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +2 -2
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +1 -1
- package/sbom.cdx.json +241 -241
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
package/lib/source-advisories.js
CHANGED
|
@@ -1,50 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
182
|
-
//
|
|
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
|
-
//
|
|
210
|
-
//
|
|
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
|
-
*
|
|
224
|
-
*
|
|
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
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
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
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
358
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// diffs[]
|
|
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
|
-
//
|
|
441
|
-
//
|
|
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
|
-
//
|
|
461
|
-
//
|
|
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
|
-
//
|
|
482
|
-
// the
|
|
483
|
-
//
|
|
484
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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,
|