@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
package/lib/prefetch.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* lib/prefetch.js
|
|
4
|
+
*
|
|
5
|
+
* Pre-downloads every upstream artifact the project queries (CISA KEV,
|
|
6
|
+
* NIST NVD per-CVE, FIRST EPSS per-CVE, IETF Datatracker per-RFC, MITRE
|
|
7
|
+
* GitHub releases) into a local cache directory. Operators behind an air
|
|
8
|
+
* gap can run this once on a connected host and ship `.cache/upstream/`
|
|
9
|
+
* across the boundary. CI runs use it as a warm cache so each refresh job
|
|
10
|
+
* doesn't re-pay full network latency.
|
|
11
|
+
*
|
|
12
|
+
* Cache layout (`.cache/upstream/` by default — gitignored):
|
|
13
|
+
*
|
|
14
|
+
* _index.json — per-entry fetch metadata
|
|
15
|
+
* kev/known_exploited_vulnerabilities.json — full KEV feed
|
|
16
|
+
* nvd/<cve-id>.json — NVD 2.0 per-CVE response
|
|
17
|
+
* epss/<cve-id>.json — EPSS per-CVE response
|
|
18
|
+
* ietf/<doc-name>.json — IETF Datatracker doc record
|
|
19
|
+
* github/<owner>__<repo>__releases.json — releases listing
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* node lib/prefetch.js # fetch everything not fresh
|
|
23
|
+
* node lib/prefetch.js --max-age 12h # re-fetch entries older than 12h
|
|
24
|
+
* node lib/prefetch.js --source kev,nvd # scope by source
|
|
25
|
+
* node lib/prefetch.js --force # ignore freshness, refetch all
|
|
26
|
+
* node lib/prefetch.js --no-network # report-only: list what would be fetched
|
|
27
|
+
*
|
|
28
|
+
* Every fetch routes through lib/job-queue.js so per-source rate budgets
|
|
29
|
+
* (NVD 5 req/30s anon, GitHub 60/h anon, etc.) are respected.
|
|
30
|
+
*
|
|
31
|
+
* Zero npm deps. Node 24 stdlib only.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const fs = require("fs");
|
|
35
|
+
const path = require("path");
|
|
36
|
+
const crypto = require("crypto");
|
|
37
|
+
const { JobQueue } = require("./job-queue");
|
|
38
|
+
|
|
39
|
+
const ROOT = path.join(__dirname, "..");
|
|
40
|
+
const DEFAULT_CACHE = path.join(ROOT, ".cache", "upstream");
|
|
41
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
42
|
+
const USER_AGENT = "exceptd-security/prefetch (+https://exceptd.com)";
|
|
43
|
+
|
|
44
|
+
const SOURCES = {
|
|
45
|
+
kev: {
|
|
46
|
+
description: "CISA Known Exploited Vulnerabilities (single feed)",
|
|
47
|
+
rate: { tokens: 6, windowMs: 60_000 }, // very gentle
|
|
48
|
+
concurrency: 1,
|
|
49
|
+
expand: () => [{ id: "known_exploited_vulnerabilities", url: "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" }],
|
|
50
|
+
},
|
|
51
|
+
nvd: {
|
|
52
|
+
description: "NIST NVD 2.0 per-CVE responses",
|
|
53
|
+
rate: { tokens: 5, windowMs: 30_000 }, // anon budget; NVD_API_KEY lifts to 50
|
|
54
|
+
rate_with_key: { tokens: 50, windowMs: 30_000 },
|
|
55
|
+
concurrency: 4,
|
|
56
|
+
expand: (ctx) => Object.keys(ctx.cveCatalog)
|
|
57
|
+
.filter((k) => /^CVE-\d{4}-\d{4,7}$/.test(k))
|
|
58
|
+
.map((id) => ({ id, url: `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${encodeURIComponent(id)}` })),
|
|
59
|
+
},
|
|
60
|
+
epss: {
|
|
61
|
+
description: "FIRST.org EPSS per-CVE responses",
|
|
62
|
+
rate: { tokens: 30, windowMs: 60_000 },
|
|
63
|
+
concurrency: 4,
|
|
64
|
+
expand: (ctx) => Object.keys(ctx.cveCatalog)
|
|
65
|
+
.filter((k) => /^CVE-\d{4}-\d{4,7}$/.test(k))
|
|
66
|
+
.map((id) => ({ id, url: `https://api.first.org/data/v1/epss?cve=${encodeURIComponent(id)}` })),
|
|
67
|
+
},
|
|
68
|
+
rfc: {
|
|
69
|
+
description: "IETF Datatracker per-RFC/doc records",
|
|
70
|
+
rate: { tokens: 30, windowMs: 60_000 },
|
|
71
|
+
concurrency: 4,
|
|
72
|
+
expand: (ctx) => Object.keys(ctx.rfcCatalog)
|
|
73
|
+
.filter((k) => !k.startsWith("_"))
|
|
74
|
+
.map((id) => {
|
|
75
|
+
let docName;
|
|
76
|
+
if (id.startsWith("RFC-")) docName = `rfc${id.slice(4)}`;
|
|
77
|
+
else if (id.startsWith("DRAFT-")) docName = `draft-${id.slice(6).toLowerCase()}`;
|
|
78
|
+
return docName ? { id: docName, url: `https://datatracker.ietf.org/api/v1/doc/document/?name=${encodeURIComponent(docName)}&format=json` } : null;
|
|
79
|
+
})
|
|
80
|
+
.filter(Boolean),
|
|
81
|
+
},
|
|
82
|
+
pins: {
|
|
83
|
+
description: "MITRE GitHub releases for ATLAS / ATT&CK / D3FEND / CWE pin checks",
|
|
84
|
+
rate: { tokens: 30, windowMs: 60 * 60_000 }, // anon: 60/h, leave headroom
|
|
85
|
+
rate_with_key: { tokens: 500, windowMs: 60 * 60_000 },
|
|
86
|
+
concurrency: 2,
|
|
87
|
+
expand: () => [
|
|
88
|
+
{ id: "mitre-atlas__atlas-data__releases", url: "https://api.github.com/repos/mitre-atlas/atlas-data/releases?per_page=5" },
|
|
89
|
+
{ id: "mitre-attack__attack-stix-data__releases", url: "https://api.github.com/repos/mitre-attack/attack-stix-data/releases?per_page=5" },
|
|
90
|
+
{ id: "d3fend__d3fend-data__releases", url: "https://api.github.com/repos/d3fend/d3fend-data/releases?per_page=5" },
|
|
91
|
+
{ id: "mitre__cwe__releases", url: "https://api.github.com/repos/mitre/cwe/releases?per_page=5" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const out = { maxAgeMs: 24 * 3600 * 1000, source: null, force: false, noNetwork: false, cacheDir: DEFAULT_CACHE, quiet: false, help: false };
|
|
98
|
+
for (let i = 2; i < argv.length; i++) {
|
|
99
|
+
const a = argv[i];
|
|
100
|
+
if (a === "--force") out.force = true;
|
|
101
|
+
else if (a === "--no-network" || a === "--dry-run") out.noNetwork = true;
|
|
102
|
+
else if (a === "--quiet") out.quiet = true;
|
|
103
|
+
else if (a === "--help" || a === "-h") out.help = true;
|
|
104
|
+
else if (a === "--source") out.source = argv[++i];
|
|
105
|
+
else if (a.startsWith("--source=")) out.source = a.slice("--source=".length);
|
|
106
|
+
else if (a === "--max-age") out.maxAgeMs = parseDuration(argv[++i]);
|
|
107
|
+
else if (a.startsWith("--max-age=")) out.maxAgeMs = parseDuration(a.slice("--max-age=".length));
|
|
108
|
+
else if (a === "--cache-dir") out.cacheDir = path.resolve(argv[++i]);
|
|
109
|
+
else if (a.startsWith("--cache-dir=")) out.cacheDir = path.resolve(a.slice("--cache-dir=".length));
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseDuration(s) {
|
|
115
|
+
if (!s) return 0;
|
|
116
|
+
const m = String(s).match(/^(\d+)\s*([smhd])?$/);
|
|
117
|
+
if (!m) throw new Error(`prefetch: invalid duration "${s}"`);
|
|
118
|
+
const n = Number(m[1]);
|
|
119
|
+
const unit = (m[2] || "h").toLowerCase();
|
|
120
|
+
const mult = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 }[unit];
|
|
121
|
+
return n * mult;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function printHelp() {
|
|
125
|
+
console.log(`prefetch — warm a local cache of every upstream artifact this project consumes.
|
|
126
|
+
|
|
127
|
+
Sources:
|
|
128
|
+
kev CISA Known Exploited Vulnerabilities
|
|
129
|
+
nvd NIST NVD 2.0 per-CVE
|
|
130
|
+
epss FIRST EPSS per-CVE
|
|
131
|
+
ietf IETF Datatracker per-RFC
|
|
132
|
+
github MITRE GitHub releases (ATLAS / ATT&CK / D3FEND / CWE)
|
|
133
|
+
|
|
134
|
+
Options:
|
|
135
|
+
--max-age <dur> skip entries fresher than this (e.g. 12h, 1d). Default: 24h.
|
|
136
|
+
--source kev,nvd scope by comma-separated source list.
|
|
137
|
+
--force ignore freshness; re-fetch every entry.
|
|
138
|
+
--no-network report-only; list what would be fetched.
|
|
139
|
+
--cache-dir <path> override cache root (default .cache/upstream).
|
|
140
|
+
--quiet suppress per-entry log lines.
|
|
141
|
+
|
|
142
|
+
Use NVD_API_KEY / GITHUB_TOKEN env vars to lift rate limits.
|
|
143
|
+
|
|
144
|
+
Outputs:
|
|
145
|
+
<cache-dir>/_index.json — per-entry metadata
|
|
146
|
+
<cache-dir>/<source>/<id>.json — raw upstream payloads
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function timedFetch(url, headers = {}) {
|
|
151
|
+
const ac = new AbortController();
|
|
152
|
+
const t = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS);
|
|
153
|
+
try {
|
|
154
|
+
const res = await fetch(url, {
|
|
155
|
+
signal: ac.signal,
|
|
156
|
+
headers: { "User-Agent": USER_AGENT, Accept: "application/json", ...headers },
|
|
157
|
+
});
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
const err = new Error(`HTTP ${res.status}`);
|
|
160
|
+
err.status = res.status;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
const etag = res.headers.get("etag") || null;
|
|
164
|
+
const lastModified = res.headers.get("last-modified") || null;
|
|
165
|
+
const json = await res.json();
|
|
166
|
+
return { json, etag, lastModified };
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(t);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function loadIndex(cacheDir) {
|
|
173
|
+
const p = path.join(cacheDir, "_index.json");
|
|
174
|
+
if (!fs.existsSync(p)) return { entries: {}, generated_at: null };
|
|
175
|
+
try {
|
|
176
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
177
|
+
} catch {
|
|
178
|
+
return { entries: {}, generated_at: null };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function saveIndex(cacheDir, idx) {
|
|
183
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
184
|
+
fs.writeFileSync(path.join(cacheDir, "_index.json"), JSON.stringify(idx, null, 2) + "\n", "utf8");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function entryKey(source, id) {
|
|
188
|
+
return `${source}/${id}`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function entryPath(cacheDir, source, id) {
|
|
192
|
+
// Sanitize id for filesystem.
|
|
193
|
+
const safe = id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
194
|
+
return path.join(cacheDir, source, `${safe}.json`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isFresh(idx, source, id, maxAgeMs) {
|
|
198
|
+
const e = idx.entries[entryKey(source, id)];
|
|
199
|
+
if (!e) return false;
|
|
200
|
+
if (!e.fetched_at) return false;
|
|
201
|
+
return Date.now() - new Date(e.fetched_at).getTime() < maxAgeMs;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function authHeadersForSource(source) {
|
|
205
|
+
if (source === "nvd" && process.env.NVD_API_KEY) return { apiKey: process.env.NVD_API_KEY };
|
|
206
|
+
if (source === "github" && process.env.GITHUB_TOKEN) return { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` };
|
|
207
|
+
return {};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function prefetch(options = {}) {
|
|
211
|
+
const opts = { maxAgeMs: 24 * 3600 * 1000, source: null, force: false, noNetwork: false, cacheDir: DEFAULT_CACHE, quiet: false, ...options };
|
|
212
|
+
const ctx = loadCtx();
|
|
213
|
+
const chosen = opts.source
|
|
214
|
+
? opts.source.split(",").map((s) => s.trim()).filter(Boolean)
|
|
215
|
+
: Object.keys(SOURCES);
|
|
216
|
+
for (const n of chosen) {
|
|
217
|
+
if (!SOURCES[n]) throw new Error(`prefetch: unknown source "${n}"`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Build the queue with per-source budgets. NVD / GitHub upgrade if env-key
|
|
221
|
+
// is present.
|
|
222
|
+
const sources = {};
|
|
223
|
+
for (const n of chosen) {
|
|
224
|
+
const cfg = SOURCES[n];
|
|
225
|
+
const rate = (n === "nvd" && process.env.NVD_API_KEY && cfg.rate_with_key)
|
|
226
|
+
|| (n === "pins" && process.env.GITHUB_TOKEN && cfg.rate_with_key)
|
|
227
|
+
|| cfg.rate
|
|
228
|
+
|| null;
|
|
229
|
+
sources[n] = { concurrency: cfg.concurrency, ...(rate ? { rate } : {}) };
|
|
230
|
+
}
|
|
231
|
+
const queue = new JobQueue({ sources });
|
|
232
|
+
|
|
233
|
+
const idx = loadIndex(opts.cacheDir);
|
|
234
|
+
if (!fs.existsSync(opts.cacheDir)) fs.mkdirSync(opts.cacheDir, { recursive: true });
|
|
235
|
+
|
|
236
|
+
const plan = [];
|
|
237
|
+
for (const sourceName of chosen) {
|
|
238
|
+
const cfg = SOURCES[sourceName];
|
|
239
|
+
const entries = cfg.expand(ctx);
|
|
240
|
+
for (const e of entries) {
|
|
241
|
+
const fresh = !opts.force && isFresh(idx, sourceName, e.id, opts.maxAgeMs);
|
|
242
|
+
plan.push({ source: sourceName, id: e.id, url: e.url, fresh });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const log = (s) => opts.quiet || console.log(s);
|
|
247
|
+
log(`\nprefetch — ${opts.noNetwork ? "DRY-RUN" : "fetching"} ${plan.length} item(s) across ${chosen.length} source(s)`);
|
|
248
|
+
log(`Cache dir: ${path.relative(ROOT, opts.cacheDir)}`);
|
|
249
|
+
log(`Max age: ${(opts.maxAgeMs / 3_600_000).toFixed(1)}h${opts.force ? " (forced)" : ""}`);
|
|
250
|
+
|
|
251
|
+
const result = { fetched: 0, skipped_fresh: 0, errors: 0, by_source: {} };
|
|
252
|
+
for (const s of chosen) result.by_source[s] = { fetched: 0, skipped_fresh: 0, errors: 0 };
|
|
253
|
+
|
|
254
|
+
if (opts.noNetwork) {
|
|
255
|
+
for (const item of plan) {
|
|
256
|
+
const tag = item.fresh ? "FRESH (skip)" : "STALE (would fetch)";
|
|
257
|
+
log(` [${item.source}] ${item.id} — ${tag}`);
|
|
258
|
+
if (item.fresh) {
|
|
259
|
+
result.skipped_fresh++;
|
|
260
|
+
result.by_source[item.source].skipped_fresh++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const jobPromises = plan.map((item) => {
|
|
267
|
+
if (item.fresh) {
|
|
268
|
+
result.skipped_fresh++;
|
|
269
|
+
result.by_source[item.source].skipped_fresh++;
|
|
270
|
+
return Promise.resolve();
|
|
271
|
+
}
|
|
272
|
+
const headers = authHeadersForSource(item.source);
|
|
273
|
+
// NVD takes its key in a custom header.
|
|
274
|
+
const reqHeaders = item.source === "nvd" && headers.apiKey ? { apiKey: headers.apiKey } : (item.source === "pins" ? headers : {});
|
|
275
|
+
return queue
|
|
276
|
+
.add({
|
|
277
|
+
source: item.source,
|
|
278
|
+
priority: priorityFor(item.source),
|
|
279
|
+
run: () => timedFetch(item.url, reqHeaders),
|
|
280
|
+
meta: { id: item.id },
|
|
281
|
+
})
|
|
282
|
+
.then((res) => {
|
|
283
|
+
const dir = path.dirname(entryPath(opts.cacheDir, item.source, item.id));
|
|
284
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
285
|
+
fs.writeFileSync(entryPath(opts.cacheDir, item.source, item.id), JSON.stringify(res.json, null, 2) + "\n", "utf8");
|
|
286
|
+
idx.entries[entryKey(item.source, item.id)] = {
|
|
287
|
+
fetched_at: new Date().toISOString(),
|
|
288
|
+
etag: res.etag,
|
|
289
|
+
last_modified: res.lastModified,
|
|
290
|
+
url: item.url,
|
|
291
|
+
sha256: crypto.createHash("sha256").update(JSON.stringify(res.json)).digest("hex"),
|
|
292
|
+
};
|
|
293
|
+
result.fetched++;
|
|
294
|
+
result.by_source[item.source].fetched++;
|
|
295
|
+
log(` [${item.source}] ${item.id} — ok`);
|
|
296
|
+
})
|
|
297
|
+
.catch((err) => {
|
|
298
|
+
result.errors++;
|
|
299
|
+
result.by_source[item.source].errors++;
|
|
300
|
+
log(` [${item.source}] ${item.id} — error: ${err.message}`);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await Promise.all(jobPromises);
|
|
305
|
+
await queue.drain();
|
|
306
|
+
idx.generated_at = new Date().toISOString();
|
|
307
|
+
saveIndex(opts.cacheDir, idx);
|
|
308
|
+
|
|
309
|
+
log(`\nprefetch summary: ${result.fetched} fetched, ${result.skipped_fresh} fresh, ${result.errors} error(s)`);
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function priorityFor(source) {
|
|
314
|
+
// KEV is operationally most urgent; pins are least.
|
|
315
|
+
return { kev: 10, nvd: 8, epss: 6, rfc: 4, pins: 2 }[source] || 0;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function loadCtx() {
|
|
319
|
+
return {
|
|
320
|
+
manifest: JSON.parse(fs.readFileSync(path.join(ROOT, "manifest.json"), "utf8")),
|
|
321
|
+
cveCatalog: JSON.parse(fs.readFileSync(path.join(ROOT, "data/cve-catalog.json"), "utf8")),
|
|
322
|
+
rfcCatalog: JSON.parse(fs.readFileSync(path.join(ROOT, "data/rfc-references.json"), "utf8")),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// --- Cache-read helpers (consumed by validate-cves / validate-rfcs / refresh)
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Read a cached entry, returning `null` if absent or stale.
|
|
330
|
+
*
|
|
331
|
+
* @param {string} cacheDir cache root
|
|
332
|
+
* @param {string} source "kev" | "nvd" | "epss" | "ietf" | "github"
|
|
333
|
+
* @param {string} id entry id (CVE-id, doc-name, etc.)
|
|
334
|
+
* @param {object} opts { maxAgeMs?: number; allowStale?: boolean }
|
|
335
|
+
* defaults: 24h fresh, allowStale=false
|
|
336
|
+
* @returns {{ data: object, age_ms: number, meta: object } | null}
|
|
337
|
+
*/
|
|
338
|
+
function readCached(cacheDir, source, id, opts = {}) {
|
|
339
|
+
const maxAgeMs = opts.maxAgeMs ?? 24 * 3600 * 1000;
|
|
340
|
+
const idx = loadIndex(cacheDir);
|
|
341
|
+
const meta = idx.entries[entryKey(source, id)];
|
|
342
|
+
if (!meta) return null;
|
|
343
|
+
const ageMs = Date.now() - new Date(meta.fetched_at).getTime();
|
|
344
|
+
if (!opts.allowStale && ageMs > maxAgeMs) return null;
|
|
345
|
+
const p = entryPath(cacheDir, source, id);
|
|
346
|
+
if (!fs.existsSync(p)) return null;
|
|
347
|
+
try {
|
|
348
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
349
|
+
return { data, age_ms: ageMs, meta };
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function main() {
|
|
356
|
+
const opts = parseArgs(process.argv);
|
|
357
|
+
if (opts.help) {
|
|
358
|
+
printHelp();
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const result = await prefetch(opts);
|
|
363
|
+
process.exit(result.errors > 0 ? 1 : 0);
|
|
364
|
+
} catch (err) {
|
|
365
|
+
console.error(`prefetch: fatal: ${err.message}`);
|
|
366
|
+
process.exit(2);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (require.main === module) main();
|
|
371
|
+
|
|
372
|
+
module.exports = { prefetch, readCached, parseArgs, SOURCES, DEFAULT_CACHE };
|