@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.
- package/CHANGELOG.md +22 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +8 -8
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +60118 -0
- package/data/attack-techniques.json +267 -7
- package/data/cve-catalog.json +9991 -3
- package/data/cwe-catalog.json +109 -2
- package/data/framework-control-gaps.json +578 -3
- package/data/zeroday-lessons.json +8330 -1
- 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 +2 -2
- package/sbom.cdx.json +253 -253
- 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
|
@@ -1,62 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
79
|
-
*
|
|
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
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
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
|
-
//
|
|
115
|
-
//
|
|
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
|
-
//
|
|
129
|
-
//
|
|
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
|
-
//
|
|
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
|
|
190
|
-
//
|
|
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
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
308
|
-
//
|
|
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
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
-
//
|
|
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
|
|
43
|
-
*
|
|
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
|
-
*
|
|
60
|
-
*
|
|
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
|
-
*
|
|
76
|
-
*
|
|
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 || {};
|
package/lib/doctor-bucketing.js
CHANGED
|
@@ -1,20 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Buckets `exceptd doctor` per-check results into warning and error lists.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
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()
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
|
-
//
|
|
91
|
-
//
|
|
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
|
}
|
package/lib/flag-suggest.js
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
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];
|