@blamejs/exceptd-skills 0.13.126 → 0.14.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.
@@ -0,0 +1,226 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * lib/citation-resolve.js
5
+ *
6
+ * Answers "is this CVE/RFC citation valid?" so an agent gets the answer FROM
7
+ * exceptd instead of researching each citation against NVD / the IETF
8
+ * datatracker by hand. Offline-first:
9
+ *
10
+ * CVE: local catalog -> resolved cache -> (opt-in) one NVD lookup, cached.
11
+ * RFC: local index -> resolved cache -> (opt-in) one datatracker lookup.
12
+ *
13
+ * The resolved cache lives at .cache/upstream/resolved/<kind>/<id>.json with a
14
+ * 7-day TTL. The FIRST agent to resolve an uncatalogued id pays one network
15
+ * call and writes the cache; sibling agents (and later offline runs) read it —
16
+ * turning N agents x M citations of redundant lookups into one lookup per id.
17
+ *
18
+ * Network is opt-out: --air-gap / EXCEPTD_AIR_GAP=1 / { noNetwork:true } make
19
+ * resolution offline-only (catalog + cache), returning status "unknown" with a
20
+ * reason rather than reaching out. Network-resolved records are transient
21
+ * (cache only) and are never written into the signed catalog.
22
+ */
23
+
24
+ const fs = require("node:fs");
25
+ const path = require("node:path");
26
+
27
+ const PKG_ROOT = path.join(__dirname, "..");
28
+ const CVE_CATALOG = process.env.EXCEPTD_CVE_CATALOG || path.join(PKG_ROOT, "data", "cve-catalog.json");
29
+ const RFC_INDEX = process.env.EXCEPTD_RFC_INDEX || path.join(PKG_ROOT, "data", "rfc-references.json");
30
+ const RESOLVE_CACHE_DIR = process.env.EXCEPTD_RESOLVE_CACHE_DIR || path.join(PKG_ROOT, ".cache", "upstream", "resolved");
31
+ const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; // matches the prefetch freshness window
32
+
33
+ const CVE_RE = /^CVE-\d{4}-\d{4,}$/;
34
+ const RFC_RE = /^(?:RFC[-\s]?)?(\d+)$/i;
35
+
36
+ let _cve = null;
37
+ let _rfc = null;
38
+ function cveCatalog() {
39
+ if (!_cve) _cve = JSON.parse(fs.readFileSync(CVE_CATALOG, "utf8"));
40
+ return _cve;
41
+ }
42
+ function rfcIndex() {
43
+ if (!_rfc) _rfc = JSON.parse(fs.readFileSync(RFC_INDEX, "utf8"));
44
+ return _rfc;
45
+ }
46
+
47
+ // --- resolved-id cache (atomic JSON files, TTL-bounded, best-effort) ---
48
+ function cachePath(kind, id) {
49
+ // Read the env at call time so tests can isolate the cache per-case.
50
+ const dir = process.env.EXCEPTD_RESOLVE_CACHE_DIR || RESOLVE_CACHE_DIR;
51
+ const safe = id.replace(/[^A-Za-z0-9._-]/g, "_");
52
+ return path.join(dir, kind, `${safe}.json`);
53
+ }
54
+ function cacheGet(kind, id) {
55
+ try {
56
+ const p = cachePath(kind, id);
57
+ const st = fs.statSync(p);
58
+ if (Date.now() - st.mtimeMs > CACHE_TTL_MS) return null;
59
+ return JSON.parse(fs.readFileSync(p, "utf8"));
60
+ } catch { return null; }
61
+ }
62
+ function cachePut(kind, id, record) {
63
+ try {
64
+ const p = cachePath(kind, id);
65
+ fs.mkdirSync(path.dirname(p), { recursive: true });
66
+ const tmp = `${p}.${process.pid}.tmp`;
67
+ fs.writeFileSync(tmp, JSON.stringify(record));
68
+ fs.renameSync(tmp, p); // atomic — concurrent agents can't read a half-written file
69
+ } catch { /* cache is an optimization, never fatal */ }
70
+ }
71
+
72
+ function isAirGap(opts) {
73
+ return !!(opts && opts.airGap) || process.env.EXCEPTD_AIR_GAP === "1";
74
+ }
75
+
76
+ /**
77
+ * Resolve a CVE citation. Returns { id, kind:"cve", status, from, ... }.
78
+ * status: published | rejected | disputed | fabricated | nonexistent | unknown
79
+ * from: format | catalog | cache | network | offline | error
80
+ */
81
+ async function resolveCve(id, opts = {}) {
82
+ const cveId = String(id || "").toUpperCase();
83
+ const base = { id: cveId, kind: "cve" };
84
+
85
+ if (!CVE_RE.test(cveId)) {
86
+ return { ...base, status: "fabricated", from: "format",
87
+ reason: "not the canonical CVE-YYYY-NNNN form — a non-numeric tail is a fabricated identifier" };
88
+ }
89
+
90
+ // 1. curated catalog (offline, authoritative for the ids it covers)
91
+ const entry = cveCatalog()[cveId];
92
+ if (entry && typeof entry === "object") {
93
+ return {
94
+ ...base,
95
+ status: entry.status || "published",
96
+ cvss: entry.cvss_score ?? null,
97
+ kev: entry.cisa_kev ?? null,
98
+ product: entry.name || entry.type || null,
99
+ exploitation: entry.active_exploitation ?? null,
100
+ from: "catalog",
101
+ };
102
+ }
103
+
104
+ // 2. resolved cache (offline, warmed by a prior agent's lookup)
105
+ const cached = cacheGet("cve", cveId);
106
+ if (cached) return { ...cached, from: "cache" };
107
+
108
+ // 3. offline / air-gap: cannot resolve uncatalogued ids without network
109
+ if (isAirGap(opts)) {
110
+ return { ...base, status: "unknown", from: "offline",
111
+ reason: "air-gap: not in local catalog and no cached resolution — verify against NVD when online" };
112
+ }
113
+ if (opts.noNetwork) {
114
+ return { ...base, status: "unknown", from: "offline",
115
+ reason: "not in local catalog and no cached resolution (network disabled)" };
116
+ }
117
+
118
+ // 4. resolve once via NVD, then cache for sibling agents.
119
+ // opts._validateCve is a test seam (inject a fake validator); production uses
120
+ // the real NVD-backed validator.
121
+ let validateCve = opts._validateCve;
122
+ if (!validateCve) {
123
+ try { ({ validateCve } = require("../sources/validators/cve-validator.js")); }
124
+ catch { return { ...base, status: "unknown", from: "error", reason: "cve validator unavailable" }; }
125
+ }
126
+ let v;
127
+ try { v = await validateCve(cveId, {}); }
128
+ catch (e) { return { ...base, status: "unknown", from: "error", reason: e.message }; }
129
+
130
+ if (v.status === "unreachable") {
131
+ return { ...base, status: "unknown", from: "offline", reason: "NVD unreachable — retry online" };
132
+ }
133
+ // NVD is the authority for a CVE's existence and lifecycle. validateCve only
134
+ // returns "unreachable" when EVERY source fails — if NVD is down but KEV/EPSS
135
+ // answer, it returns match/drift with sources.nvd.reachable === false. Do NOT
136
+ // declare "published" on KEV/EPSS alone during an NVD outage; that would
137
+ // falsely validate an unconfirmed (or nonexistent) identifier.
138
+ const nvd = v.fetched && v.fetched.sources && v.fetched.sources.nvd;
139
+ if (!nvd || nvd.reachable !== true) {
140
+ return { ...base, status: "unknown", from: "offline",
141
+ reason: "NVD unreachable — CVE existence/status unconfirmed; retry online" };
142
+ }
143
+ let status;
144
+ if (v.status === "rejected") status = "rejected";
145
+ else if (v.status === "missing" || nvd.found !== true) status = "nonexistent";
146
+ else if ((v.fetched?.cve_tags || []).some(t => /disputed/i.test(t)) || /disputed/i.test(v.fetched?.nvd_vuln_status || "")) status = "disputed";
147
+ else status = "published";
148
+
149
+ const record = {
150
+ id: cveId, kind: "cve", status,
151
+ cvss: v.fetched?.cvss_score ?? null,
152
+ kev: v.fetched?.in_kev ?? null,
153
+ // NVD English description — carries the product/scope a citation must match,
154
+ // so an agent can confirm status=published applies to the right product
155
+ // without a second manual NVD lookup.
156
+ product: v.fetched?.description ?? null,
157
+ nvd_vuln_status: v.fetched?.nvd_vuln_status ?? null,
158
+ cve_tags: v.fetched?.cve_tags || [],
159
+ source: "nvd",
160
+ resolved_at: new Date().toISOString(),
161
+ };
162
+ cachePut("cve", cveId, record);
163
+ return { ...record, from: "network" };
164
+ }
165
+
166
+ /**
167
+ * Resolve an RFC citation. Returns { id, kind:"rfc", number, title, rfc_status,
168
+ * found, from, ... }. The local index covers the whole current RFC series, so
169
+ * number->title resolution is fully offline. Obsoleted/historic RFCs are
170
+ * excluded from the index, so a not-found number is either obsoleted or
171
+ * nonexistent; the optional network step disambiguates.
172
+ */
173
+ async function resolveRfc(id, opts = {}) {
174
+ const raw = String(id || "").trim();
175
+ const m = raw.match(RFC_RE);
176
+ const base = { id: raw, kind: "rfc" };
177
+ if (!m) {
178
+ return { ...base, found: false, status: "unknown", from: "format",
179
+ reason: "not an RFC number — expected `RFC <n>` or a bare number" };
180
+ }
181
+ const num = Number(m[1]);
182
+ const key = `RFC-${num}`;
183
+
184
+ // 1. local index (offline, whole current series)
185
+ const entry = rfcIndex()[key];
186
+ if (entry && typeof entry === "object") {
187
+ return {
188
+ ...base, number: num, found: true,
189
+ title: entry.title || null,
190
+ rfc_status: entry.status || null,
191
+ published: entry.published || null,
192
+ obsoleted_by: entry.obsoleted_by || null,
193
+ from: "index",
194
+ };
195
+ }
196
+
197
+ // 2. resolved cache
198
+ const cached = cacheGet("rfc", String(num));
199
+ if (cached) return { ...cached, from: "cache" };
200
+
201
+ // 3. offline: report the ambiguity rather than guessing
202
+ if (isAirGap(opts) || opts.noNetwork) {
203
+ return { ...base, number: num, found: false, status: "unknown", from: "offline",
204
+ reason: "not in the local RFC index — likely obsoleted/historic (excluded from the index) or nonexistent; verify at datatracker.ietf.org when online" };
205
+ }
206
+
207
+ // 4. disambiguate obsoleted vs nonexistent via the datatracker, once + cached
208
+ let validateRfc;
209
+ try { ({ validateRfc } = require("../sources/validators/rfc-validator.js")); }
210
+ catch { return { ...base, number: num, found: false, status: "unknown", from: "error", reason: "rfc validator unavailable" }; }
211
+ let v;
212
+ try { v = await validateRfc(key, {}); }
213
+ catch (e) { return { ...base, number: num, found: false, status: "unknown", from: "error", reason: e.message }; }
214
+ if (v.status === "unreachable") {
215
+ return { ...base, number: num, found: false, status: "unknown", from: "offline", reason: "datatracker unreachable — retry online" };
216
+ }
217
+ const record = v.status === "missing"
218
+ ? { id: raw, kind: "rfc", number: num, found: false, status: "nonexistent", source: "datatracker", resolved_at: new Date().toISOString() }
219
+ : { id: raw, kind: "rfc", number: num, found: true, status: "obsoleted-or-historic",
220
+ title: v.fetched?.title || null, source: "datatracker", resolved_at: new Date().toISOString(),
221
+ note: "resolves at the datatracker but is absent from the local index (obsoleted/historic RFCs are excluded)" };
222
+ cachePut("rfc", String(num), record);
223
+ return { ...record, from: "network" };
224
+ }
225
+
226
+ module.exports = { resolveCve, resolveRfc };
@@ -21,6 +21,7 @@
21
21
 
22
22
  const fs = require("node:fs");
23
23
  const path = require("node:path");
24
+ const { isLinkedWorktreeDir } = require("./scan-excludes");
24
25
 
25
26
  const COLLECTOR_ID = "cicd-pipeline-compromise";
26
27
 
@@ -219,7 +220,15 @@ function scanOidcPolicies(root) {
219
220
  for (const e of entries) {
220
221
  if (e.name === "node_modules" || e.name === ".git") continue;
221
222
  const full = path.join(dir, e.name);
222
- if (e.isDirectory()) { walk(full, depth + 1); continue; }
223
+ if (e.isDirectory()) {
224
+ // Skip linked git worktrees (gitdir-pointer `.git` file), e.g.
225
+ // agent-created repo copies under `.claude/worktrees/<id>/`
226
+ // nested below a scanned policy/infra dir — rescanning them
227
+ // double-counts the same OIDC trust documents.
228
+ if (isLinkedWorktreeDir(full)) continue;
229
+ walk(full, depth + 1);
230
+ continue;
231
+ }
223
232
  if (!e.isFile() || !/\.json$/i.test(e.name)) continue;
224
233
  const text = readSafe(full);
225
234
  if (!text) continue;