@blamejs/exceptd-skills 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +232 -0
- package/ARCHITECTURE.md +267 -0
- package/CHANGELOG.md +616 -0
- package/CONTEXT.md +203 -0
- package/LICENSE +200 -0
- package/NOTICE +82 -0
- package/README.md +307 -0
- package/SECURITY.md +73 -0
- package/agents/README.md +81 -0
- package/agents/report-generator.md +156 -0
- package/agents/skill-updater.md +102 -0
- package/agents/source-validator.md +119 -0
- package/agents/threat-researcher.md +149 -0
- package/bin/exceptd.js +183 -0
- package/data/_indexes/_meta.json +88 -0
- package/data/_indexes/activity-feed.json +362 -0
- package/data/_indexes/catalog-summaries.json +229 -0
- package/data/_indexes/chains.json +7135 -0
- package/data/_indexes/currency.json +359 -0
- package/data/_indexes/did-ladders.json +451 -0
- package/data/_indexes/frequency.json +2072 -0
- package/data/_indexes/handoff-dag.json +476 -0
- package/data/_indexes/jurisdiction-clocks.json +967 -0
- package/data/_indexes/jurisdiction-map.json +536 -0
- package/data/_indexes/recipes.json +319 -0
- package/data/_indexes/section-offsets.json +3656 -0
- package/data/_indexes/stale-content.json +14 -0
- package/data/_indexes/summary-cards.json +1736 -0
- package/data/_indexes/theater-fingerprints.json +381 -0
- package/data/_indexes/token-budget.json +2137 -0
- package/data/_indexes/trigger-table.json +1374 -0
- package/data/_indexes/xref.json +818 -0
- package/data/atlas-ttps.json +282 -0
- package/data/cve-catalog.json +496 -0
- package/data/cwe-catalog.json +1017 -0
- package/data/d3fend-catalog.json +738 -0
- package/data/dlp-controls.json +1039 -0
- package/data/exploit-availability.json +67 -0
- package/data/framework-control-gaps.json +1255 -0
- package/data/global-frameworks.json +2913 -0
- package/data/rfc-references.json +324 -0
- package/data/zeroday-lessons.json +377 -0
- package/keys/public.pem +3 -0
- package/lib/framework-gap.js +328 -0
- package/lib/job-queue.js +195 -0
- package/lib/lint-skills.js +536 -0
- package/lib/prefetch.js +372 -0
- package/lib/refresh-external.js +713 -0
- package/lib/schemas/cve-catalog.schema.json +151 -0
- package/lib/schemas/manifest.schema.json +106 -0
- package/lib/schemas/skill-frontmatter.schema.json +113 -0
- package/lib/scoring.js +149 -0
- package/lib/sign.js +197 -0
- package/lib/ttp-mapper.js +80 -0
- package/lib/validate-catalog-meta.js +198 -0
- package/lib/validate-cve-catalog.js +213 -0
- package/lib/validate-indexes.js +83 -0
- package/lib/validate-package.js +162 -0
- package/lib/validate-vendor.js +85 -0
- package/lib/verify.js +216 -0
- package/lib/worker-pool.js +84 -0
- package/manifest-snapshot.json +1833 -0
- package/manifest.json +2108 -0
- package/orchestrator/README.md +124 -0
- package/orchestrator/dispatcher.js +140 -0
- package/orchestrator/event-bus.js +146 -0
- package/orchestrator/index.js +874 -0
- package/orchestrator/pipeline.js +201 -0
- package/orchestrator/scanner.js +327 -0
- package/orchestrator/scheduler.js +137 -0
- package/package.json +113 -0
- package/sbom.cdx.json +158 -0
- package/scripts/audit-cross-skill.js +261 -0
- package/scripts/audit-perf.js +160 -0
- package/scripts/bootstrap.js +205 -0
- package/scripts/build-indexes.js +721 -0
- package/scripts/builders/activity-feed.js +79 -0
- package/scripts/builders/catalog-summaries.js +67 -0
- package/scripts/builders/currency.js +109 -0
- package/scripts/builders/cwe-chains.js +105 -0
- package/scripts/builders/did-ladders.js +149 -0
- package/scripts/builders/frequency.js +89 -0
- package/scripts/builders/jurisdiction-clocks.js +126 -0
- package/scripts/builders/recipes.js +159 -0
- package/scripts/builders/section-offsets.js +162 -0
- package/scripts/builders/stale-content.js +171 -0
- package/scripts/builders/summary-cards.js +166 -0
- package/scripts/builders/theater-fingerprints.js +198 -0
- package/scripts/builders/token-budget.js +96 -0
- package/scripts/check-manifest-snapshot.js +217 -0
- package/scripts/predeploy.js +267 -0
- package/scripts/refresh-manifest-snapshot.js +57 -0
- package/scripts/refresh-sbom.js +222 -0
- package/skills/age-gates-child-safety/skill.md +456 -0
- package/skills/ai-attack-surface/skill.md +282 -0
- package/skills/ai-c2-detection/skill.md +440 -0
- package/skills/ai-risk-management/skill.md +311 -0
- package/skills/api-security/skill.md +287 -0
- package/skills/attack-surface-pentest/skill.md +381 -0
- package/skills/cloud-security/skill.md +384 -0
- package/skills/compliance-theater/skill.md +365 -0
- package/skills/container-runtime-security/skill.md +379 -0
- package/skills/coordinated-vuln-disclosure/skill.md +473 -0
- package/skills/defensive-countermeasure-mapping/skill.md +300 -0
- package/skills/dlp-gap-analysis/skill.md +337 -0
- package/skills/email-security-anti-phishing/skill.md +206 -0
- package/skills/exploit-scoring/skill.md +331 -0
- package/skills/framework-gap-analysis/skill.md +374 -0
- package/skills/fuzz-testing-strategy/skill.md +313 -0
- package/skills/global-grc/skill.md +564 -0
- package/skills/identity-assurance/skill.md +272 -0
- package/skills/incident-response-playbook/skill.md +546 -0
- package/skills/kernel-lpe-triage/skill.md +303 -0
- package/skills/mcp-agent-trust/skill.md +326 -0
- package/skills/mlops-security/skill.md +325 -0
- package/skills/ot-ics-security/skill.md +340 -0
- package/skills/policy-exception-gen/skill.md +437 -0
- package/skills/pqc-first/skill.md +546 -0
- package/skills/rag-pipeline-security/skill.md +294 -0
- package/skills/researcher/skill.md +310 -0
- package/skills/sector-energy/skill.md +409 -0
- package/skills/sector-federal-government/skill.md +302 -0
- package/skills/sector-financial/skill.md +398 -0
- package/skills/sector-healthcare/skill.md +373 -0
- package/skills/security-maturity-tiers/skill.md +464 -0
- package/skills/skill-update-loop/skill.md +463 -0
- package/skills/supply-chain-integrity/skill.md +318 -0
- package/skills/threat-model-currency/skill.md +404 -0
- package/skills/threat-modeling-methodology/skill.md +312 -0
- package/skills/webapp-security/skill.md +281 -0
- package/skills/zeroday-gap-learn/skill.md +350 -0
- package/vendor/blamejs/LICENSE +201 -0
- package/vendor/blamejs/README.md +54 -0
- package/vendor/blamejs/_PROVENANCE.json +54 -0
- package/vendor/blamejs/retry.js +335 -0
- package/vendor/blamejs/worker-pool.js +418 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/refresh-external.js
|
|
4
|
+
*
|
|
5
|
+
* External-data refresh orchestrator. Pulls the latest from the canonical
|
|
6
|
+
* upstream sources and either reports drift (dry-run, default) or applies
|
|
7
|
+
* the drift as an upsert into the local catalog (--apply).
|
|
8
|
+
*
|
|
9
|
+
* Sources (each is independently pluggable):
|
|
10
|
+
*
|
|
11
|
+
* KEV — CISA Known Exploited Vulnerabilities (per-CVE upsert of
|
|
12
|
+
* cisa_kev / cisa_kev_date)
|
|
13
|
+
* EPSS — FIRST EPSS (per-CVE upsert of epss_score / epss_percentile /
|
|
14
|
+
* epss_date)
|
|
15
|
+
* NVD — NIST NVD 2.0 (per-CVE upsert of cvss_score / cvss_vector)
|
|
16
|
+
* RFC — IETF Datatracker (per-RFC upsert of status)
|
|
17
|
+
* PINS — MITRE ATLAS / ATT&CK / D3FEND / CWE upstream releases
|
|
18
|
+
* (REPORT-ONLY — version bumps need audit per AGENTS.md
|
|
19
|
+
* Hard Rule #12, so they surface as findings, not auto-applied)
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node lib/refresh-external.js # dry-run all sources
|
|
23
|
+
* node lib/refresh-external.js --apply # apply all sources
|
|
24
|
+
* node lib/refresh-external.js --source kev # one source, dry-run
|
|
25
|
+
* node lib/refresh-external.js --apply --source kev,epss
|
|
26
|
+
* node lib/refresh-external.js --from-fixture <path> # use frozen fixture
|
|
27
|
+
* payloads (offline)
|
|
28
|
+
*
|
|
29
|
+
* Exit codes:
|
|
30
|
+
* 0 — dry-run completed (regardless of whether drift was found)
|
|
31
|
+
* 1 — apply mode AND a downstream gate (validate-indexes, lint) failed
|
|
32
|
+
* 2 — unrecoverable runner error
|
|
33
|
+
*
|
|
34
|
+
* The refresh-report.json artifact lives at the repo root and is
|
|
35
|
+
* gitignored. CI uploads it as a workflow artifact.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const fs = require("fs");
|
|
39
|
+
const path = require("path");
|
|
40
|
+
const { execFileSync } = require("child_process");
|
|
41
|
+
|
|
42
|
+
const ROOT = path.join(__dirname, "..");
|
|
43
|
+
const ABS = (p) => path.join(ROOT, p);
|
|
44
|
+
const TODAY = new Date().toISOString().slice(0, 10);
|
|
45
|
+
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const out = {
|
|
48
|
+
apply: false,
|
|
49
|
+
source: null, // comma-separated list or null = all
|
|
50
|
+
fromFixture: null, // path to fixture dir
|
|
51
|
+
fromCache: null, // path to .cache/upstream dir (or default if --from-cache passed bare)
|
|
52
|
+
swarm: false, // fan-out sources across worker threads
|
|
53
|
+
help: false,
|
|
54
|
+
quiet: false,
|
|
55
|
+
};
|
|
56
|
+
for (let i = 2; i < argv.length; i++) {
|
|
57
|
+
const a = argv[i];
|
|
58
|
+
if (a === "--apply") out.apply = true;
|
|
59
|
+
else if (a === "--quiet") out.quiet = true;
|
|
60
|
+
else if (a === "--swarm") out.swarm = true;
|
|
61
|
+
else if (a === "--help" || a === "-h") out.help = true;
|
|
62
|
+
else if (a === "--from-cache") {
|
|
63
|
+
// accept either --from-cache <path> or --from-cache (default path)
|
|
64
|
+
const next = argv[i + 1];
|
|
65
|
+
if (next && !next.startsWith("--")) { out.fromCache = next; i++; }
|
|
66
|
+
else out.fromCache = ".cache/upstream";
|
|
67
|
+
}
|
|
68
|
+
else if (a.startsWith("--from-cache=")) out.fromCache = a.slice("--from-cache=".length);
|
|
69
|
+
else if (a === "--source") out.source = argv[++i];
|
|
70
|
+
else if (a.startsWith("--source=")) out.source = a.slice("--source=".length);
|
|
71
|
+
else if (a === "--from-fixture") out.fromFixture = argv[++i];
|
|
72
|
+
else if (a.startsWith("--from-fixture=")) out.fromFixture = a.slice("--from-fixture=".length);
|
|
73
|
+
else if (a === "--report-out") out.reportOut = argv[++i];
|
|
74
|
+
else if (a.startsWith("--report-out=")) out.reportOut = a.slice("--report-out=".length);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printHelp() {
|
|
80
|
+
console.log(`refresh-external — pull latest upstream data, optionally upsert into local catalogs.
|
|
81
|
+
|
|
82
|
+
Modes:
|
|
83
|
+
(default) dry-run all sources, write refresh-report.json
|
|
84
|
+
--apply apply diffs and rebuild indexes
|
|
85
|
+
--source kev,epss scope to a comma-separated list (kev|epss|nvd|rfc|pins)
|
|
86
|
+
--from-fixture <p> use frozen fixture payloads (tests use this path)
|
|
87
|
+
--from-cache [<p>] read from prefetch cache (default .cache/upstream).
|
|
88
|
+
Combine with --apply to upsert against cached data
|
|
89
|
+
entirely offline.
|
|
90
|
+
--swarm fan out sources across worker threads. Source fetches
|
|
91
|
+
run in parallel rather than sequentially. Best when
|
|
92
|
+
paired with --from-cache (no rate-limit contention).
|
|
93
|
+
|
|
94
|
+
Outputs:
|
|
95
|
+
refresh-report.json (gitignored) — summary of every diff + per-source status.
|
|
96
|
+
|
|
97
|
+
This module never auto-applies version-pin bumps — those require audit per
|
|
98
|
+
AGENTS.md Hard Rule #12 and are surfaced as report-only findings.
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Source modules ----------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Each source module exposes:
|
|
106
|
+
* name: string
|
|
107
|
+
* fetchDiff(ctx, opts) -> Promise<{ status, diffs, errors, summary }>
|
|
108
|
+
* status: "ok" | "unreachable" | "partial"
|
|
109
|
+
* diffs: array of { id, field, before, after, severity? }
|
|
110
|
+
* summary: brief string
|
|
111
|
+
* applyDiff(ctx, diffs) -> Promise<{ updated, errors }>
|
|
112
|
+
* mutates local catalog and writes it
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
const KEV_SOURCE = {
|
|
116
|
+
name: "kev",
|
|
117
|
+
description: "CISA Known Exploited Vulnerabilities",
|
|
118
|
+
applies_to: "data/cve-catalog.json",
|
|
119
|
+
async fetchDiff(ctx) {
|
|
120
|
+
if (ctx.fixtures?.kev) return synthesizeFromFixture(ctx, "kev");
|
|
121
|
+
if (ctx.cacheDir) return kevDiffFromCache(ctx);
|
|
122
|
+
const { validateAllCves } = require("../sources/validators");
|
|
123
|
+
const report = await validateAllCves(ctx.cveCatalog, { concurrency: 4 });
|
|
124
|
+
const diffs = [];
|
|
125
|
+
let errors = 0;
|
|
126
|
+
for (const r of report.results) {
|
|
127
|
+
if (r.status === "unreachable") errors++;
|
|
128
|
+
for (const d of r.discrepancies || []) {
|
|
129
|
+
if (d.field === "cisa_kev" || d.field === "cisa_kev_date") {
|
|
130
|
+
diffs.push({ id: r.cve_id, field: d.field, before: d.local, after: d.fetched, severity: d.severity });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
status: errors === 0 ? "ok" : errors === report.results.length ? "unreachable" : "partial",
|
|
136
|
+
diffs,
|
|
137
|
+
errors,
|
|
138
|
+
summary: `${diffs.length} KEV diffs; ${errors} unreachable / ${report.total} total`,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
async applyDiff(ctx, diffs) {
|
|
142
|
+
let updated = 0;
|
|
143
|
+
const errors = [];
|
|
144
|
+
for (const d of diffs) {
|
|
145
|
+
if (!ctx.cveCatalog[d.id]) {
|
|
146
|
+
errors.push(`KEV: no local entry for ${d.id}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
ctx.cveCatalog[d.id][d.field] = d.after;
|
|
150
|
+
ctx.cveCatalog[d.id].last_verified = TODAY;
|
|
151
|
+
updated++;
|
|
152
|
+
}
|
|
153
|
+
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
154
|
+
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
155
|
+
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
156
|
+
return { updated, errors };
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const EPSS_SOURCE = {
|
|
161
|
+
name: "epss",
|
|
162
|
+
description: "FIRST.org EPSS scores",
|
|
163
|
+
applies_to: "data/cve-catalog.json",
|
|
164
|
+
async fetchDiff(ctx) {
|
|
165
|
+
if (ctx.fixtures?.epss) return synthesizeFromFixture(ctx, "epss");
|
|
166
|
+
if (ctx.cacheDir) return epssDiffFromCache(ctx);
|
|
167
|
+
const { validateAllCves } = require("../sources/validators");
|
|
168
|
+
const report = await validateAllCves(ctx.cveCatalog, { concurrency: 4 });
|
|
169
|
+
const diffs = [];
|
|
170
|
+
let errors = 0;
|
|
171
|
+
for (const r of report.results) {
|
|
172
|
+
if (r.status === "unreachable") errors++;
|
|
173
|
+
for (const d of r.discrepancies || []) {
|
|
174
|
+
if (d.field === "epss_score" || d.field === "epss_percentile") {
|
|
175
|
+
diffs.push({ id: r.cve_id, field: d.field, before: d.local, after: d.fetched, severity: d.severity });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// epss_date refreshes when score does.
|
|
179
|
+
if (r.fetched?.epss?.date && r.local) {
|
|
180
|
+
diffs.push({ id: r.cve_id, field: "epss_date", before: r.local.epss_date, after: r.fetched.epss.date, severity: "low" });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Collapse duplicates: epss_date should appear once per CVE only when
|
|
184
|
+
// an epss field actually moved.
|
|
185
|
+
const epssCves = new Set(diffs.filter((d) => d.field === "epss_score" || d.field === "epss_percentile").map((d) => d.id));
|
|
186
|
+
const filtered = diffs.filter((d) => d.field !== "epss_date" || epssCves.has(d.id));
|
|
187
|
+
return {
|
|
188
|
+
status: errors === 0 ? "ok" : errors === report.results.length ? "unreachable" : "partial",
|
|
189
|
+
diffs: filtered,
|
|
190
|
+
errors,
|
|
191
|
+
summary: `${filtered.length} EPSS diffs; ${errors} unreachable / ${report.total} total`,
|
|
192
|
+
};
|
|
193
|
+
},
|
|
194
|
+
async applyDiff(ctx, diffs) {
|
|
195
|
+
let updated = 0;
|
|
196
|
+
const errors = [];
|
|
197
|
+
for (const d of diffs) {
|
|
198
|
+
if (!ctx.cveCatalog[d.id]) {
|
|
199
|
+
errors.push(`EPSS: no local entry for ${d.id}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
ctx.cveCatalog[d.id][d.field] = d.after;
|
|
203
|
+
ctx.cveCatalog[d.id].last_verified = TODAY;
|
|
204
|
+
updated++;
|
|
205
|
+
}
|
|
206
|
+
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
207
|
+
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
208
|
+
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
209
|
+
return { updated, errors };
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const NVD_SOURCE = {
|
|
214
|
+
name: "nvd",
|
|
215
|
+
description: "NIST NVD 2.0 CVSS metrics",
|
|
216
|
+
applies_to: "data/cve-catalog.json",
|
|
217
|
+
async fetchDiff(ctx) {
|
|
218
|
+
if (ctx.fixtures?.nvd) return synthesizeFromFixture(ctx, "nvd");
|
|
219
|
+
if (ctx.cacheDir) return nvdDiffFromCache(ctx);
|
|
220
|
+
const { validateAllCves } = require("../sources/validators");
|
|
221
|
+
const report = await validateAllCves(ctx.cveCatalog, { concurrency: 4 });
|
|
222
|
+
const diffs = [];
|
|
223
|
+
let errors = 0;
|
|
224
|
+
for (const r of report.results) {
|
|
225
|
+
if (r.status === "unreachable") errors++;
|
|
226
|
+
for (const d of r.discrepancies || []) {
|
|
227
|
+
if (d.field === "cvss_score" || d.field === "cvss_vector") {
|
|
228
|
+
diffs.push({ id: r.cve_id, field: d.field, before: d.local, after: d.fetched, severity: d.severity });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
status: errors === 0 ? "ok" : errors === report.results.length ? "unreachable" : "partial",
|
|
234
|
+
diffs,
|
|
235
|
+
errors,
|
|
236
|
+
summary: `${diffs.length} NVD CVSS diffs; ${errors} unreachable / ${report.total} total`,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
async applyDiff(ctx, diffs) {
|
|
240
|
+
let updated = 0;
|
|
241
|
+
const errors = [];
|
|
242
|
+
for (const d of diffs) {
|
|
243
|
+
if (!ctx.cveCatalog[d.id]) {
|
|
244
|
+
errors.push(`NVD: no local entry for ${d.id}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
ctx.cveCatalog[d.id][d.field] = d.after;
|
|
248
|
+
ctx.cveCatalog[d.id].last_verified = TODAY;
|
|
249
|
+
updated++;
|
|
250
|
+
}
|
|
251
|
+
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
252
|
+
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
253
|
+
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
254
|
+
return { updated, errors };
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const RFC_SOURCE = {
|
|
259
|
+
name: "rfc",
|
|
260
|
+
description: "IETF Datatracker RFC status",
|
|
261
|
+
applies_to: "data/rfc-references.json",
|
|
262
|
+
async fetchDiff(ctx) {
|
|
263
|
+
if (ctx.fixtures?.rfc) return synthesizeFromFixture(ctx, "rfc");
|
|
264
|
+
if (ctx.cacheDir) return rfcDiffFromCache(ctx);
|
|
265
|
+
const { validateAllRfcs } = require("../sources/validators");
|
|
266
|
+
const results = await validateAllRfcs(ctx.rfcCatalog, { concurrency: 4 });
|
|
267
|
+
const diffs = [];
|
|
268
|
+
let errors = 0;
|
|
269
|
+
for (const r of results) {
|
|
270
|
+
if (r.status === "unreachable") {
|
|
271
|
+
errors++;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (r.status === "drift" && r.discrepancies) {
|
|
275
|
+
for (const msg of r.discrepancies) {
|
|
276
|
+
// The current rfc-validator returns discrepancies as strings. We
|
|
277
|
+
// attempt to parse a status drift; fall back to the raw message.
|
|
278
|
+
const m = msg.match(/local "([^"]+)" vs Datatracker "([^"]+)"/);
|
|
279
|
+
if (m) {
|
|
280
|
+
diffs.push({ id: r.id, field: "status", before: m[1], after: m[2], severity: "medium" });
|
|
281
|
+
} else {
|
|
282
|
+
diffs.push({ id: r.id, field: "note", before: null, after: msg, severity: "low" });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
status: errors === 0 ? "ok" : errors === results.length ? "unreachable" : "partial",
|
|
289
|
+
diffs,
|
|
290
|
+
errors,
|
|
291
|
+
summary: `${diffs.length} RFC drifts; ${errors} unreachable / ${results.length} total`,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
async applyDiff(ctx, diffs) {
|
|
295
|
+
let updated = 0;
|
|
296
|
+
const errors = [];
|
|
297
|
+
for (const d of diffs) {
|
|
298
|
+
if (d.field !== "status") continue; // notes are informational
|
|
299
|
+
const entry = ctx.rfcCatalog[d.id];
|
|
300
|
+
if (!entry) {
|
|
301
|
+
errors.push(`RFC: no local entry for ${d.id}`);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
entry.status = d.after;
|
|
305
|
+
entry.last_verified = TODAY;
|
|
306
|
+
updated++;
|
|
307
|
+
}
|
|
308
|
+
ctx.rfcCatalog._meta = ctx.rfcCatalog._meta || {};
|
|
309
|
+
ctx.rfcCatalog._meta.last_updated = TODAY;
|
|
310
|
+
writeJson(ABS("data/rfc-references.json"), ctx.rfcCatalog);
|
|
311
|
+
return { updated, errors };
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const PINS_SOURCE = {
|
|
316
|
+
name: "pins",
|
|
317
|
+
description: "MITRE ATLAS / ATT&CK / D3FEND / CWE upstream release pins",
|
|
318
|
+
applies_to: "manifest.json + data/cwe-catalog.json + data/d3fend-catalog.json",
|
|
319
|
+
report_only: true,
|
|
320
|
+
async fetchDiff(ctx) {
|
|
321
|
+
if (ctx.fixtures?.pins) return synthesizeFromFixture(ctx, "pins");
|
|
322
|
+
if (ctx.cacheDir) return pinsDiffFromCache(ctx);
|
|
323
|
+
const { checkAllPins } = require("../sources/validators/version-pin-validator");
|
|
324
|
+
const results = await checkAllPins({
|
|
325
|
+
manifest: ctx.manifest,
|
|
326
|
+
cweCatalog: ctx.cweCatalog,
|
|
327
|
+
d3fendCatalog: ctx.d3fendCatalog,
|
|
328
|
+
});
|
|
329
|
+
const diffs = [];
|
|
330
|
+
let errors = 0;
|
|
331
|
+
for (const r of results) {
|
|
332
|
+
if (r.unreachable) {
|
|
333
|
+
errors++;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (r.drift) {
|
|
337
|
+
diffs.push({
|
|
338
|
+
id: r.pin_name,
|
|
339
|
+
field: "version",
|
|
340
|
+
before: r.local_version,
|
|
341
|
+
after: r.latest_version,
|
|
342
|
+
severity: "medium",
|
|
343
|
+
source_url: r.source_url,
|
|
344
|
+
local_path_hint: r.local_path_hint,
|
|
345
|
+
note: "Version-pin bump requires audit per AGENTS.md Hard Rule #12. Surface as GitHub issue, do not auto-apply.",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
status: errors === 0 ? "ok" : errors === results.length ? "unreachable" : "partial",
|
|
351
|
+
diffs,
|
|
352
|
+
errors,
|
|
353
|
+
summary: `${diffs.length} pin drifts; ${errors} unreachable / ${results.length} total`,
|
|
354
|
+
};
|
|
355
|
+
},
|
|
356
|
+
async applyDiff() {
|
|
357
|
+
// Version pins are intentionally not auto-applied.
|
|
358
|
+
return { updated: 0, errors: ["pin bumps are report-only — see Hard Rule #12"] };
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
const ALL_SOURCES = {
|
|
363
|
+
kev: KEV_SOURCE,
|
|
364
|
+
epss: EPSS_SOURCE,
|
|
365
|
+
nvd: NVD_SOURCE,
|
|
366
|
+
rfc: RFC_SOURCE,
|
|
367
|
+
pins: PINS_SOURCE,
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// --- Cache-mode helpers ------------------------------------------------
|
|
371
|
+
// When `--from-cache <dir>` is set, the source modules read their inputs
|
|
372
|
+
// from the prefetch cache instead of hitting upstream. The cache layout
|
|
373
|
+
// is fixed by lib/prefetch.js:
|
|
374
|
+
// <cacheDir>/kev/known_exploited_vulnerabilities.json
|
|
375
|
+
// <cacheDir>/nvd/<cve>.json
|
|
376
|
+
// <cacheDir>/epss/<cve>.json
|
|
377
|
+
// <cacheDir>/rfc/<doc-name>.json
|
|
378
|
+
// <cacheDir>/pins/<owner__repo__releases>.json
|
|
379
|
+
//
|
|
380
|
+
// readCachedJson returns null on miss; callers report it as "unreachable"
|
|
381
|
+
// for that entry rather than failing the whole source.
|
|
382
|
+
|
|
383
|
+
function readCachedJson(cacheDir, source, id) {
|
|
384
|
+
const safe = id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
385
|
+
const p = path.join(cacheDir, source, `${safe}.json`);
|
|
386
|
+
if (!fs.existsSync(p)) return null;
|
|
387
|
+
try { return JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
388
|
+
catch { return null; }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function kevDiffFromCache(ctx) {
|
|
392
|
+
const feed = readCachedJson(ctx.cacheDir, "kev", "known_exploited_vulnerabilities");
|
|
393
|
+
if (!feed) {
|
|
394
|
+
return { status: "unreachable", diffs: [], errors: 1, summary: "KEV: no cached feed" };
|
|
395
|
+
}
|
|
396
|
+
const kevSet = new Set();
|
|
397
|
+
const kevDates = new Map();
|
|
398
|
+
for (const v of feed.vulnerabilities || []) {
|
|
399
|
+
if (v && v.cveID) {
|
|
400
|
+
kevSet.add(v.cveID);
|
|
401
|
+
if (v.dateAdded) kevDates.set(v.cveID, v.dateAdded);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const diffs = [];
|
|
405
|
+
for (const [id, entry] of Object.entries(ctx.cveCatalog)) {
|
|
406
|
+
if (!/^CVE-\d{4}-\d{4,7}$/.test(id)) continue;
|
|
407
|
+
const upstream = kevSet.has(id);
|
|
408
|
+
if (typeof entry.cisa_kev === "boolean" && entry.cisa_kev !== upstream) {
|
|
409
|
+
diffs.push({ id, field: "cisa_kev", before: entry.cisa_kev, after: upstream, severity: "high" });
|
|
410
|
+
}
|
|
411
|
+
const upDate = kevDates.get(id) || null;
|
|
412
|
+
if (upDate && entry.cisa_kev_date && entry.cisa_kev_date !== upDate) {
|
|
413
|
+
diffs.push({ id, field: "cisa_kev_date", before: entry.cisa_kev_date, after: upDate, severity: "low" });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { status: "ok", diffs, errors: 0, summary: `${diffs.length} KEV diffs (from cache)` };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function epssDiffFromCache(ctx) {
|
|
420
|
+
const cves = Object.keys(ctx.cveCatalog).filter((k) => /^CVE-\d{4}-\d{4,7}$/.test(k));
|
|
421
|
+
const diffs = [];
|
|
422
|
+
let errors = 0;
|
|
423
|
+
const drift = 0.05;
|
|
424
|
+
for (const id of cves) {
|
|
425
|
+
const payload = readCachedJson(ctx.cacheDir, "epss", id);
|
|
426
|
+
if (!payload) { errors++; continue; }
|
|
427
|
+
const row = (payload.data || []).find((r) => r?.cve === id) || (payload.data || [])[0];
|
|
428
|
+
if (!row) continue;
|
|
429
|
+
const score = row.epss != null ? Number(row.epss) : null;
|
|
430
|
+
const pct = row.percentile != null ? Number(row.percentile) : null;
|
|
431
|
+
const local = ctx.cveCatalog[id];
|
|
432
|
+
if (score != null && local.epss_score != null && Math.abs(score - local.epss_score) > drift) {
|
|
433
|
+
diffs.push({ id, field: "epss_score", before: local.epss_score, after: score, severity: "medium" });
|
|
434
|
+
}
|
|
435
|
+
if (pct != null && local.epss_percentile != null && Math.abs(pct - local.epss_percentile) > drift) {
|
|
436
|
+
diffs.push({ id, field: "epss_percentile", before: local.epss_percentile, after: pct, severity: "medium" });
|
|
437
|
+
}
|
|
438
|
+
if (row.date && local.epss_date && row.date !== local.epss_date) {
|
|
439
|
+
// Only emit a date diff when we also emitted a score/percentile diff for this CVE.
|
|
440
|
+
const moved = diffs.some((d) => d.id === id && (d.field === "epss_score" || d.field === "epss_percentile"));
|
|
441
|
+
if (moved) diffs.push({ id, field: "epss_date", before: local.epss_date, after: row.date, severity: "low" });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const status = errors === 0 ? "ok" : errors === cves.length ? "unreachable" : "partial";
|
|
445
|
+
return { status, diffs, errors, summary: `${diffs.length} EPSS diffs (from cache); ${errors} missing entries` };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function nvdDiffFromCache(ctx) {
|
|
449
|
+
const cves = Object.keys(ctx.cveCatalog).filter((k) => /^CVE-\d{4}-\d{4,7}$/.test(k));
|
|
450
|
+
const diffs = [];
|
|
451
|
+
let errors = 0;
|
|
452
|
+
for (const id of cves) {
|
|
453
|
+
const payload = readCachedJson(ctx.cacheDir, "nvd", id);
|
|
454
|
+
if (!payload) { errors++; continue; }
|
|
455
|
+
const vuln = payload.vulnerabilities?.[0]?.cve;
|
|
456
|
+
if (!vuln) continue;
|
|
457
|
+
const m = vuln.metrics || {};
|
|
458
|
+
const ordered = [...(m.cvssMetricV31 || []), ...(m.cvssMetricV30 || []), ...(m.cvssMetricV2 || [])];
|
|
459
|
+
const primary = ordered.find((x) => x.type === "Primary") || ordered[0];
|
|
460
|
+
const upScore = typeof primary?.cvssData?.baseScore === "number" ? primary.cvssData.baseScore : null;
|
|
461
|
+
const upVector = primary?.cvssData?.vectorString || null;
|
|
462
|
+
const local = ctx.cveCatalog[id];
|
|
463
|
+
if (upScore != null && local.cvss_score != null && Math.abs(upScore - local.cvss_score) > 0.05) {
|
|
464
|
+
diffs.push({ id, field: "cvss_score", before: local.cvss_score, after: upScore, severity: "high" });
|
|
465
|
+
}
|
|
466
|
+
if (upVector && local.cvss_vector && upVector !== local.cvss_vector) {
|
|
467
|
+
diffs.push({ id, field: "cvss_vector", before: local.cvss_vector, after: upVector, severity: "medium" });
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const status = errors === 0 ? "ok" : errors === cves.length ? "unreachable" : "partial";
|
|
471
|
+
return { status, diffs, errors, summary: `${diffs.length} NVD CVSS diffs (from cache); ${errors} missing entries` };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function rfcDiffFromCache(ctx) {
|
|
475
|
+
const STATUS_MAP = {
|
|
476
|
+
std: "Internet Standard", ps: "Proposed Standard", ds: "Draft Standard",
|
|
477
|
+
bcp: "Best Current Practice", inf: "Informational", exp: "Experimental",
|
|
478
|
+
his: "Historic", unkn: "Unknown",
|
|
479
|
+
};
|
|
480
|
+
const ids = Object.keys(ctx.rfcCatalog).filter((k) => !k.startsWith("_"));
|
|
481
|
+
const diffs = [];
|
|
482
|
+
let errors = 0;
|
|
483
|
+
for (const id of ids) {
|
|
484
|
+
let docName;
|
|
485
|
+
if (id.startsWith("RFC-")) docName = `rfc${id.slice(4)}`;
|
|
486
|
+
else if (id.startsWith("DRAFT-")) docName = `draft-${id.slice(6).toLowerCase()}`;
|
|
487
|
+
if (!docName) continue;
|
|
488
|
+
const payload = readCachedJson(ctx.cacheDir, "rfc", docName);
|
|
489
|
+
if (!payload) { errors++; continue; }
|
|
490
|
+
const obj = payload.objects?.[0];
|
|
491
|
+
if (!obj) continue;
|
|
492
|
+
const upStatus = STATUS_MAP[obj.std_level] || null;
|
|
493
|
+
const local = ctx.rfcCatalog[id];
|
|
494
|
+
if (upStatus && local.status && upStatus !== local.status) {
|
|
495
|
+
diffs.push({ id, field: "status", before: local.status, after: upStatus, severity: "medium" });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const status = errors === 0 ? "ok" : errors === ids.length ? "unreachable" : "partial";
|
|
499
|
+
return { status, diffs, errors, summary: `${diffs.length} RFC drifts (from cache); ${errors} missing entries` };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function pinsDiffFromCache(ctx) {
|
|
503
|
+
// Cache layout under pins/: <owner>__<repo>__releases.json arrays.
|
|
504
|
+
const PIN_REPOS = {
|
|
505
|
+
atlas_version: "mitre-atlas__atlas-data__releases",
|
|
506
|
+
attack_version: "mitre-attack__attack-stix-data__releases",
|
|
507
|
+
d3fend_version: "d3fend__d3fend-data__releases",
|
|
508
|
+
cwe_version: "mitre__cwe__releases",
|
|
509
|
+
};
|
|
510
|
+
const localOf = {
|
|
511
|
+
atlas_version: ctx.manifest.atlas_version,
|
|
512
|
+
attack_version: ctx.manifest.attack_version,
|
|
513
|
+
d3fend_version: ctx.d3fendCatalog?._meta?.version || ctx.d3fendCatalog?._meta?.d3fend_version || null,
|
|
514
|
+
cwe_version: ctx.cweCatalog?._meta?.version || ctx.cweCatalog?._meta?.cwe_version || null,
|
|
515
|
+
};
|
|
516
|
+
const diffs = [];
|
|
517
|
+
let errors = 0;
|
|
518
|
+
for (const [pinName, file] of Object.entries(PIN_REPOS)) {
|
|
519
|
+
const payload = readCachedJson(ctx.cacheDir, "pins", file);
|
|
520
|
+
if (!payload || !Array.isArray(payload)) { errors++; continue; }
|
|
521
|
+
const stable = payload.find((r) => !r.draft && !r.prerelease);
|
|
522
|
+
if (!stable) { errors++; continue; }
|
|
523
|
+
const latest = String(stable.tag_name || "").replace(/^v/, "");
|
|
524
|
+
const local = localOf[pinName] != null ? String(localOf[pinName]).replace(/^v/, "") : null;
|
|
525
|
+
if (local && latest && local !== latest) {
|
|
526
|
+
diffs.push({
|
|
527
|
+
id: pinName,
|
|
528
|
+
field: "version",
|
|
529
|
+
before: local,
|
|
530
|
+
after: latest,
|
|
531
|
+
severity: "medium",
|
|
532
|
+
source_url: stable.html_url,
|
|
533
|
+
local_path_hint: pinName === "cwe_version" ? "data/cwe-catalog.json _meta.version"
|
|
534
|
+
: pinName === "d3fend_version" ? "data/d3fend-catalog.json _meta.version"
|
|
535
|
+
: `manifest.json — ${pinName}`,
|
|
536
|
+
note: "Version-pin bump requires audit per AGENTS.md Hard Rule #12. Surface as GitHub issue, do not auto-apply.",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const status = errors === 0 ? "ok" : errors === Object.keys(PIN_REPOS).length ? "unreachable" : "partial";
|
|
541
|
+
return { status, diffs, errors, summary: `${diffs.length} pin drifts (from cache); ${errors} missing entries` };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// --- Fixture-mode helper ----------------------------------------------
|
|
545
|
+
|
|
546
|
+
function synthesizeFromFixture(ctx, sourceName) {
|
|
547
|
+
// The frozen fixture payloads are JSON files that look like:
|
|
548
|
+
// { diffs: [...], errors: 0, summary: "..." }
|
|
549
|
+
// tests/fixtures/refresh/<sourceName>.json drives this path.
|
|
550
|
+
const fp = path.join(ctx.fixtures.dir, `${sourceName}.json`);
|
|
551
|
+
if (!fs.existsSync(fp)) {
|
|
552
|
+
return { status: "ok", diffs: [], errors: 0, summary: `${sourceName}: no fixture` };
|
|
553
|
+
}
|
|
554
|
+
const fx = JSON.parse(fs.readFileSync(fp, "utf8"));
|
|
555
|
+
return {
|
|
556
|
+
status: fx.status || "ok",
|
|
557
|
+
diffs: fx.diffs || [],
|
|
558
|
+
errors: fx.errors || 0,
|
|
559
|
+
summary: fx.summary || `${sourceName}: ${(fx.diffs || []).length} diffs (fixture)`,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// --- IO helpers --------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
function loadCtx(opts) {
|
|
566
|
+
const ctx = {
|
|
567
|
+
manifest: JSON.parse(fs.readFileSync(ABS("manifest.json"), "utf8")),
|
|
568
|
+
cveCatalog: JSON.parse(fs.readFileSync(ABS("data/cve-catalog.json"), "utf8")),
|
|
569
|
+
rfcCatalog: JSON.parse(fs.readFileSync(ABS("data/rfc-references.json"), "utf8")),
|
|
570
|
+
cweCatalog: JSON.parse(fs.readFileSync(ABS("data/cwe-catalog.json"), "utf8")),
|
|
571
|
+
d3fendCatalog: JSON.parse(fs.readFileSync(ABS("data/d3fend-catalog.json"), "utf8")),
|
|
572
|
+
fixtures: null,
|
|
573
|
+
cacheDir: null,
|
|
574
|
+
};
|
|
575
|
+
if (opts.fromFixture) {
|
|
576
|
+
ctx.fixtures = { dir: path.resolve(opts.fromFixture), kev: true, epss: true, nvd: true, rfc: true, pins: true };
|
|
577
|
+
} else if (opts.fromCache) {
|
|
578
|
+
const abs = path.resolve(opts.fromCache);
|
|
579
|
+
ctx.cacheDir = abs;
|
|
580
|
+
if (!fs.existsSync(abs)) {
|
|
581
|
+
throw new Error(`refresh-external: --from-cache path does not exist: ${abs}`);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return ctx;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function writeJson(p, obj) {
|
|
588
|
+
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + "\n", "utf8");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function chosenSources(opts) {
|
|
592
|
+
if (!opts.source) return Object.values(ALL_SOURCES);
|
|
593
|
+
const names = opts.source.split(",").map((s) => s.trim()).filter(Boolean);
|
|
594
|
+
const out = [];
|
|
595
|
+
for (const n of names) {
|
|
596
|
+
if (!ALL_SOURCES[n]) {
|
|
597
|
+
console.error(`refresh-external: unknown source "${n}". Valid: ${Object.keys(ALL_SOURCES).join(", ")}`);
|
|
598
|
+
process.exit(2);
|
|
599
|
+
}
|
|
600
|
+
out.push(ALL_SOURCES[n]);
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
async function main() {
|
|
606
|
+
const opts = parseArgs(process.argv);
|
|
607
|
+
if (opts.help) {
|
|
608
|
+
printHelp();
|
|
609
|
+
process.exit(0);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const ctx = loadCtx(opts);
|
|
613
|
+
const sources = chosenSources(opts);
|
|
614
|
+
const log = (s) => opts.quiet || console.log(s);
|
|
615
|
+
|
|
616
|
+
log(`\nrefresh-external — ${opts.apply ? "APPLY" : "dry-run"} mode${opts.swarm ? " (swarm)" : ""}`);
|
|
617
|
+
log(`Sources: ${sources.map((s) => s.name).join(", ")}`);
|
|
618
|
+
if (opts.fromFixture) log(`Fixture mode: ${opts.fromFixture}`);
|
|
619
|
+
if (opts.fromCache) log(`Cache mode: ${opts.fromCache}`);
|
|
620
|
+
|
|
621
|
+
const report = {
|
|
622
|
+
generated_at: new Date().toISOString(),
|
|
623
|
+
mode: opts.apply ? "apply" : "dry-run",
|
|
624
|
+
fixture_mode: !!opts.fromFixture,
|
|
625
|
+
cache_mode: !!opts.fromCache,
|
|
626
|
+
swarm: !!opts.swarm,
|
|
627
|
+
sources: {},
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
let hadFailure = false;
|
|
631
|
+
|
|
632
|
+
// Source fetches are independently I/O-bound. In normal mode we run them
|
|
633
|
+
// sequentially so log output is interleaved cleanly. --swarm fans them
|
|
634
|
+
// out via Promise.all() — each source already has its own per-source
|
|
635
|
+
// queue with its own rate budget, so parallel sources don't compete
|
|
636
|
+
// against each other for tokens. The two modes produce the same report
|
|
637
|
+
// structure; only wall-clock differs.
|
|
638
|
+
const runOne = async (src) => {
|
|
639
|
+
let diff;
|
|
640
|
+
try {
|
|
641
|
+
diff = await src.fetchDiff(ctx);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
return { src, error: err };
|
|
644
|
+
}
|
|
645
|
+
return { src, diff };
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const outcomes = opts.swarm
|
|
649
|
+
? await Promise.all(sources.map(runOne))
|
|
650
|
+
: await sequential(sources, runOne);
|
|
651
|
+
|
|
652
|
+
for (const { src, diff, error } of outcomes) {
|
|
653
|
+
if (error) {
|
|
654
|
+
log(`\n [${src.name}] ${src.description}`);
|
|
655
|
+
log(` error: ${error.message}`);
|
|
656
|
+
report.sources[src.name] = { status: "error", error: error.message };
|
|
657
|
+
hadFailure = true;
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
log(`\n [${src.name}] ${src.description}`);
|
|
661
|
+
log(` ${diff.summary}`);
|
|
662
|
+
report.sources[src.name] = {
|
|
663
|
+
status: diff.status,
|
|
664
|
+
summary: diff.summary,
|
|
665
|
+
diff_count: diff.diffs.length,
|
|
666
|
+
errors: diff.errors,
|
|
667
|
+
diffs: diff.diffs,
|
|
668
|
+
applies_to: src.applies_to,
|
|
669
|
+
report_only: !!src.report_only,
|
|
670
|
+
};
|
|
671
|
+
if (opts.apply && diff.diffs.length > 0 && !src.report_only) {
|
|
672
|
+
const r = await src.applyDiff(ctx, diff.diffs);
|
|
673
|
+
report.sources[src.name].applied = r.updated;
|
|
674
|
+
report.sources[src.name].apply_errors = r.errors;
|
|
675
|
+
log(` applied: ${r.updated} update(s)`);
|
|
676
|
+
if (r.errors.length) log(` apply errors: ${r.errors.join("; ")}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Persist report — tests can redirect via --report-out so concurrent
|
|
681
|
+
// suites don't race on a shared refresh-report.json at the repo root.
|
|
682
|
+
const reportPath = opts.reportOut ? path.resolve(opts.reportOut) : ABS("refresh-report.json");
|
|
683
|
+
writeJson(reportPath, report);
|
|
684
|
+
log(`\nWrote ${path.relative(ROOT, reportPath)}`);
|
|
685
|
+
|
|
686
|
+
if (opts.apply) {
|
|
687
|
+
// Always regenerate indexes after an apply so validate-indexes passes.
|
|
688
|
+
log(`\nRebuilding indexes (npm run build-indexes)`);
|
|
689
|
+
try {
|
|
690
|
+
execFileSync(process.execPath, [ABS("scripts/build-indexes.js")], { stdio: "inherit", cwd: ROOT });
|
|
691
|
+
} catch (err) {
|
|
692
|
+
console.error(`refresh-external: build-indexes failed: ${err.message}`);
|
|
693
|
+
hadFailure = true;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
process.exit(hadFailure ? 1 : 0);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function sequential(items, fn) {
|
|
701
|
+
const out = [];
|
|
702
|
+
for (const it of items) out.push(await fn(it));
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (require.main === module) {
|
|
707
|
+
main().catch((err) => {
|
|
708
|
+
console.error(`refresh-external: fatal: ${err && err.stack ? err.stack : err}`);
|
|
709
|
+
process.exit(2);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
module.exports = { ALL_SOURCES, loadCtx, parseArgs };
|