@blamejs/exceptd-skills 0.16.23 → 0.16.25
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/agents/report-generator.md +2 -2
- package/bin/exceptd.js +72 -25
- package/data/_indexes/_meta.json +2 -2
- package/data/_indexes/chains.json +354 -177
- package/data/_indexes/section-offsets.json +35 -35
- package/lib/collectors/ai-api.js +112 -7
- package/lib/collectors/citation-hygiene.js +27 -0
- package/lib/collectors/crypto-codebase.js +25 -0
- package/lib/collectors/kernel.js +32 -2
- package/lib/collectors/library-author.js +30 -0
- package/lib/collectors/runtime.js +38 -3
- package/lib/collectors/sbom.js +21 -2
- package/lib/collectors/secrets.js +125 -0
- package/lib/cve-regression-watcher.js +5 -2
- package/lib/playbook-runner.js +16 -3
- package/lib/refresh-external.js +40 -15
- package/manifest.json +53 -53
- package/orchestrator/README.md +1 -1
- package/orchestrator/index.js +17 -3
- package/package.json +1 -1
- package/sbom.cdx.json +52 -52
- package/scripts/builders/cwe-chains.js +1 -0
- package/scripts/builders/section-offsets.js +10 -2
- package/scripts/builders/token-budget.js +3 -3
- package/scripts/check-changelog-extract.js +38 -1
- package/scripts/check-version-tags.js +5 -0
|
@@ -106,6 +106,21 @@ function isWorldWritable(p) {
|
|
|
106
106
|
} catch { return false; }
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
// Classify a world-writable hit against the two deterministic
|
|
110
|
+
// false_positive_checks_required entries for world-writable-in-trusted-path:
|
|
111
|
+
// [0] sticky-bit (1777-style) dirs/files intentionally permit per-user write
|
|
112
|
+
// [1] 0-byte stamp / unix-socket / FIFO documented for the application
|
|
113
|
+
// Returns { stickyBit, special } so the caller can both keep only genuine
|
|
114
|
+
// hits and attest exactly the checks it ran.
|
|
115
|
+
function classifyWorldWritable(p) {
|
|
116
|
+
try {
|
|
117
|
+
const s = fs.lstatSync(p);
|
|
118
|
+
const stickyBit = (s.mode & 0o1000) !== 0;
|
|
119
|
+
const special = s.isSocket() || s.isFIFO() || (s.isFile() && s.size === 0);
|
|
120
|
+
return { stickyBit, special };
|
|
121
|
+
} catch { return { stickyBit: false, special: false }; }
|
|
122
|
+
}
|
|
123
|
+
|
|
109
124
|
function readProcPid(pid, procRoot) {
|
|
110
125
|
// Returns { pid, ppid, uid, exe } or null.
|
|
111
126
|
try {
|
|
@@ -240,11 +255,22 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
240
255
|
const passwdContent = readFileSafe(P.passwd);
|
|
241
256
|
const uid0 = passwdContent ? parsePasswdUidZero(passwdContent) : null;
|
|
242
257
|
|
|
243
|
-
// World-writable files under trusted paths.
|
|
258
|
+
// World-writable files under trusted paths. Split into genuine hits
|
|
259
|
+
// (regular non-empty files without the sticky bit) and benign carriers
|
|
260
|
+
// the two false_positive_checks_required entries demote (sticky-bit
|
|
261
|
+
// per-user-write dirs; 0-byte stamps / sockets / FIFOs). Only the
|
|
262
|
+
// genuine hits flip the indicator; the split records which FP checks
|
|
263
|
+
// the collector deterministically ran.
|
|
244
264
|
const worldWritableFiles = [];
|
|
265
|
+
let sawStickyBitCarrier = false;
|
|
266
|
+
let sawSpecialCarrier = false;
|
|
245
267
|
for (const tp of P.trustedPaths) {
|
|
246
268
|
for (const f of walkShallow(tp, TRUSTED_PATH_MAX_DEPTH)) {
|
|
247
|
-
if (isWorldWritable(f))
|
|
269
|
+
if (!isWorldWritable(f)) continue;
|
|
270
|
+
const { stickyBit, special } = classifyWorldWritable(f);
|
|
271
|
+
if (stickyBit) { sawStickyBitCarrier = true; continue; }
|
|
272
|
+
if (special) { sawSpecialCarrier = true; continue; }
|
|
273
|
+
worldWritableFiles.push(f);
|
|
248
274
|
}
|
|
249
275
|
}
|
|
250
276
|
|
|
@@ -266,7 +292,16 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
266
292
|
try { fs.readdirSync(tp); return true; } catch { return false; }
|
|
267
293
|
});
|
|
268
294
|
if (anyTpReadable) {
|
|
269
|
-
|
|
295
|
+
const wwHit = worldWritableFiles.length > 0;
|
|
296
|
+
signal_overrides["world-writable-in-trusted-path"] = wwHit ? "hit" : "miss";
|
|
297
|
+
// Attest the false_positive_checks_required entries the collector
|
|
298
|
+
// ran against every flagged file: [0] sticky-bit carriers and [1]
|
|
299
|
+
// 0-byte/socket/FIFO carriers were both stat-inspected and excluded,
|
|
300
|
+
// so a surviving hit satisfies both. Without this attestation the
|
|
301
|
+
// runner downgrades a real world-writable hit to inconclusive.
|
|
302
|
+
if (wwHit) {
|
|
303
|
+
signal_overrides["world-writable-in-trusted-path__fp_checks"] = { "0": true, "1": true };
|
|
304
|
+
}
|
|
270
305
|
}
|
|
271
306
|
// orphan-privileged-process: only emit when /proc was walkable
|
|
272
307
|
// AND the scan had enough exe-link visibility to reach a verdict.
|
package/lib/collectors/sbom.js
CHANGED
|
@@ -354,6 +354,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
354
354
|
const j = JSON.parse(fs.readFileSync(npmLockfile.path, "utf8"));
|
|
355
355
|
let withIntegrity = 0;
|
|
356
356
|
let withoutIntegrity = 0;
|
|
357
|
+
// Track whether any integrity-less entry is a local-path / workspace /
|
|
358
|
+
// git ref. lockfile-no-integrity FP[0] demotes those — they legitimately
|
|
359
|
+
// have no registry integrity hash. A remote-registry tarball without
|
|
360
|
+
// integrity is the genuine finding.
|
|
361
|
+
let withoutIntegrityLocalOnly = true;
|
|
362
|
+
const LOCAL_REF_RE = /^(?:file:|link:|workspace:|git\+ssh:|git\+https:|git:|github:|portal:)/i;
|
|
357
363
|
const walk = (obj) => {
|
|
358
364
|
if (!obj || typeof obj !== "object") return;
|
|
359
365
|
// Only remote-tarball entries (those with a `resolved` URL) are
|
|
@@ -362,8 +368,12 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
362
368
|
// `integrity`, so keying off `version` would false-positive on
|
|
363
369
|
// every clean lockfile. Mirror library-author.js's guard.
|
|
364
370
|
if (obj.resolved != null) {
|
|
365
|
-
if (obj.integrity != null)
|
|
366
|
-
|
|
371
|
+
if (obj.integrity != null) {
|
|
372
|
+
withIntegrity++;
|
|
373
|
+
} else {
|
|
374
|
+
withoutIntegrity++;
|
|
375
|
+
if (!LOCAL_REF_RE.test(String(obj.resolved))) withoutIntegrityLocalOnly = false;
|
|
376
|
+
}
|
|
367
377
|
}
|
|
368
378
|
for (const v of Object.values(obj)) if (v && typeof v === "object") walk(v);
|
|
369
379
|
};
|
|
@@ -373,6 +383,15 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
373
383
|
// class, not full coverage.
|
|
374
384
|
if (withoutIntegrity > 0) {
|
|
375
385
|
signal_overrides["lockfile-no-integrity"] = "hit";
|
|
386
|
+
// __fp_checks attestation. [0]: at least one integrity-less entry is a
|
|
387
|
+
// remote-registry tarball (not exclusively local-path/workspace/git
|
|
388
|
+
// refs). [1]: the lockfile is the canonical root package-lock.json the
|
|
389
|
+
// build consumes, not a stale copy under archive/ pre-migration/.
|
|
390
|
+
const att = {};
|
|
391
|
+
if (!withoutIntegrityLocalOnly) att["0"] = true;
|
|
392
|
+
const rel = (npmLockfile.path || "").replace(/\\/g, "/");
|
|
393
|
+
if (!/\/(?:archive|pre-migration|old|backup|legacy)\//i.test(rel)) att["1"] = true;
|
|
394
|
+
if (Object.keys(att).length) signal_overrides["lockfile-no-integrity__fp_checks"] = att;
|
|
376
395
|
} else if (withIntegrity > 0) {
|
|
377
396
|
signal_overrides["lockfile-no-integrity"] = "miss";
|
|
378
397
|
}
|
|
@@ -112,6 +112,98 @@ const AWS_EXAMPLE_ACCESS_KEY_IDS = new Set([
|
|
|
112
112
|
"AKIAIOSFODNN7EXAMPLE",
|
|
113
113
|
]);
|
|
114
114
|
|
|
115
|
+
// AWS-published sample secret-access-key (paired with AKIAIOSFODNN7EXAMPLE
|
|
116
|
+
// throughout the AWS docs). aws-secret-access-key false_positive_checks_required[1]
|
|
117
|
+
// demotes this exact value.
|
|
118
|
+
const AWS_EXAMPLE_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
|
|
119
|
+
|
|
120
|
+
// Placeholder/fixture substrings the API-key indicators' FP[0] checks demote.
|
|
121
|
+
// A literal `PLACEHOLDER` / `EXAMPLE` / `XXXX` run / a `dummy`/`test` infix
|
|
122
|
+
// marks documentation material rather than a live credential.
|
|
123
|
+
const PLACEHOLDER_RE = /placeholder|example|redacted|dummy|x{4,}|0{6,}|1234567890/i;
|
|
124
|
+
|
|
125
|
+
// Path segments the per-indicator FP path checks treat as documentation /
|
|
126
|
+
// fixture material in addition to TEST_PATH_SEGMENTS (which already covers
|
|
127
|
+
// /examples/, /fixtures/, /test/ etc.). The secret indicators' FP prose adds
|
|
128
|
+
// /docs/ and quickstart/snippet paths.
|
|
129
|
+
const DOC_PATH_SEGMENTS = [
|
|
130
|
+
"/docs/", "/doc/", "/sdk-quickstart/", "/quickstart/", "/docs-snippet/",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
function isDocOrTestPath(rel) {
|
|
134
|
+
if (isTestPath(rel)) return true;
|
|
135
|
+
const norm = "/" + rel.replace(/\\/g, "/").toLowerCase() + "/";
|
|
136
|
+
return DOC_PATH_SEGMENTS.some((seg) => norm.includes(seg));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Per-indicator deterministic false_positive_checks_required evaluation.
|
|
140
|
+
// Returns the set of FP-check indices (as strings) the collector can attest
|
|
141
|
+
// for a single hit — i.e. the checks it actually ran and that the hit
|
|
142
|
+
// survives. Indices requiring network reachability or operator judgement are
|
|
143
|
+
// deliberately omitted so the runner honestly downgrades to inconclusive.
|
|
144
|
+
// `value` is the matched credential, `file` the relative path, `window` a
|
|
145
|
+
// few-line context slice around the match.
|
|
146
|
+
function fpIndicesSatisfied(indicatorId, value, file, window) {
|
|
147
|
+
const sat = new Set();
|
|
148
|
+
const notDocPath = !isDocOrTestPath(file);
|
|
149
|
+
switch (indicatorId) {
|
|
150
|
+
case "aws-secret-access-key": {
|
|
151
|
+
// [0] co-occurrence with an AKIA*/ASIA*/AGPA*/AIDA* id in a 10-line window
|
|
152
|
+
if (/\b(?:AKIA|ASIA|AGPA|AIDA)[0-9A-Z]{12,}\b/.test(window)) sat.add("0");
|
|
153
|
+
// [1] not the AWS-published sample secret
|
|
154
|
+
if (value !== AWS_EXAMPLE_SECRET_KEY) sat.add("1");
|
|
155
|
+
// [2] not under examples/ docs/ fixtures/ / a test snapshot
|
|
156
|
+
if (notDocPath) sat.add("2");
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "slack-bot-or-user-token": {
|
|
160
|
+
// [0] not a placeholder / published doc fixture
|
|
161
|
+
if (!PLACEHOLDER_RE.test(value)) sat.add("0");
|
|
162
|
+
// [1] conforms to a current Slack token shape: at least three
|
|
163
|
+
// dash-separated segments after the xox? prefix
|
|
164
|
+
if (value.split("-").length >= 4) sat.add("1");
|
|
165
|
+
// [2] not under examples/ docs/ fixtures/
|
|
166
|
+
if (notDocPath) sat.add("2");
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case "stripe-secret-key": {
|
|
170
|
+
// [0] not a sk_test_ published sample (deterministic only for the
|
|
171
|
+
// test prefix; live keys are handled by [2])
|
|
172
|
+
if (!(value.startsWith("sk_test_") && PLACEHOLDER_RE.test(value))) sat.add("0");
|
|
173
|
+
// [1] not under examples/ fixtures/ docs/ / a quickstart template
|
|
174
|
+
if (notDocPath) sat.add("1");
|
|
175
|
+
// [2] the live-validity probe applies only to sk_live_*; a sk_test_
|
|
176
|
+
// key carries no live financial exposure, so the check is moot and
|
|
177
|
+
// the collector can attest it deterministically. sk_live_* still
|
|
178
|
+
// needs operator-authorised network validation — left unattested.
|
|
179
|
+
if (value.startsWith("sk_test_") || value.startsWith("rk_test_")) sat.add("2");
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "openai-api-key": {
|
|
183
|
+
// [0] not a placeholder / sk-test- / sk-dummy- fixture
|
|
184
|
+
if (!PLACEHOLDER_RE.test(value) && !/^sk-(?:test|dummy)-/i.test(value)) sat.add("0");
|
|
185
|
+
// [1] post-prefix length meets the entropy floor (>= 48 chars)
|
|
186
|
+
if (value.replace(/^sk-(?:proj-|svcacct-|admin-)?/, "").length >= 48) sat.add("1");
|
|
187
|
+
// [2] vendor disambiguation — sk-ant-* is Anthropic, not OpenAI
|
|
188
|
+
if (!/^sk-ant-/i.test(value)) sat.add("2");
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case "anthropic-api-key": {
|
|
192
|
+
// [0] not a placeholder / sk-ant-test- fixture
|
|
193
|
+
if (!PLACEHOLDER_RE.test(value) && !/^sk-ant-test-/i.test(value)) sat.add("0");
|
|
194
|
+
// [1] not under examples/ fixtures/ sdk-quickstart/ docs-snippet/
|
|
195
|
+
if (notDocPath) sat.add("1");
|
|
196
|
+
// [2] post-prefix length meets the entropy floor (>= 80 chars after
|
|
197
|
+
// sk-ant-(api03|admin01)-)
|
|
198
|
+
if (value.replace(/^sk-ant-(?:api03|admin01)-/, "").length >= 80) sat.add("2");
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
return sat;
|
|
205
|
+
}
|
|
206
|
+
|
|
115
207
|
const INDICATOR_PATTERNS = [
|
|
116
208
|
{ id: "aws-access-key-id", re: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
117
209
|
{ id: "aws-secret-access-key", re: /\baws_secret_access_key\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi },
|
|
@@ -209,6 +301,13 @@ function scanContent(full, rel) {
|
|
|
209
301
|
// Demote AWS-published example access-key IDs (e.g. the docs' canonical
|
|
210
302
|
// AKIAIOSFODNN7EXAMPLE). A README quoting the AWS docs must not hit.
|
|
211
303
|
if (p.id === "aws-access-key-id" && AWS_EXAMPLE_ACCESS_KEY_IDS.has(m[0])) continue;
|
|
304
|
+
// The captured credential is group 1 when the pattern brackets it
|
|
305
|
+
// (aws_secret_access_key=<value>); otherwise the whole match.
|
|
306
|
+
const value = m[1] != null ? m[1] : m[0];
|
|
307
|
+
// ±600-byte context window (a deterministic proxy for "nearby lines")
|
|
308
|
+
// used by the co-occurrence FP check.
|
|
309
|
+
const winStart = Math.max(0, m.index - 600);
|
|
310
|
+
const window = buf.slice(winStart, m.index + 600);
|
|
212
311
|
hits.push({
|
|
213
312
|
indicator_id: p.id,
|
|
214
313
|
file: rel,
|
|
@@ -217,6 +316,9 @@ function scanContent(full, rel) {
|
|
|
217
316
|
// (SARIF startLine) instead of a bare file-level location.
|
|
218
317
|
line: lineFromOffset(buf, m.index),
|
|
219
318
|
redacted_match: redactMatch(m[0]),
|
|
319
|
+
// The false_positive_checks_required indices this hit deterministically
|
|
320
|
+
// survives — attested so the runner doesn't downgrade hit → inconclusive.
|
|
321
|
+
fp_satisfied: fpIndicesSatisfied(p.id, value, rel, window),
|
|
220
322
|
});
|
|
221
323
|
if (++count >= 5) break; // cap per-indicator-per-file
|
|
222
324
|
}
|
|
@@ -310,6 +412,29 @@ function collect({ cwd = process.cwd(), env = process.env, args = {} } = {}) {
|
|
|
310
412
|
for (const p of INDICATOR_PATTERNS) {
|
|
311
413
|
signal_overrides[p.id] = prodHitsByIndicator[p.id] && prodHitsByIndicator[p.id].length > 0 ? "hit" : "miss";
|
|
312
414
|
}
|
|
415
|
+
// Per-indicator __fp_checks attestation. For each FP-gated indicator that
|
|
416
|
+
// fired, attest the false_positive_checks_required indices the collector
|
|
417
|
+
// deterministically ran AND that EVERY surviving hit satisfies (the
|
|
418
|
+
// intersection — an index is only universally true if no hit fails it).
|
|
419
|
+
// Network / operator-judgement indices are never in the set, so the runner
|
|
420
|
+
// still downgrades indicators that carry one. Without this, a real secret
|
|
421
|
+
// surfaced by `collect` is downgraded to inconclusive after `run`.
|
|
422
|
+
for (const p of INDICATOR_PATTERNS) {
|
|
423
|
+
if (signal_overrides[p.id] !== "hit") continue;
|
|
424
|
+
const hits = prodHitsByIndicator[p.id] || [];
|
|
425
|
+
if (!hits.length || !hits[0].fp_satisfied) continue;
|
|
426
|
+
let common = null;
|
|
427
|
+
for (const h of hits) {
|
|
428
|
+
const s = h.fp_satisfied || new Set();
|
|
429
|
+
if (common === null) { common = new Set(s); continue; }
|
|
430
|
+
for (const idx of [...common]) if (!s.has(idx)) common.delete(idx);
|
|
431
|
+
}
|
|
432
|
+
if (common && common.size) {
|
|
433
|
+
const att = {};
|
|
434
|
+
for (const idx of common) att[idx] = true;
|
|
435
|
+
signal_overrides[`${p.id}__fp_checks`] = att;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
313
438
|
// ssh-private-key-block is also flipped by file presence (a private
|
|
314
439
|
// key file with the matching magic bytes counts even without a
|
|
315
440
|
// content scan match — e.g. binary-only key formats). Re-flip when
|
|
@@ -63,7 +63,6 @@ const path = require('path');
|
|
|
63
63
|
const fs = require('fs');
|
|
64
64
|
|
|
65
65
|
const CVE_ID_RE = /^CVE-((?:19|20)\d{2})-\d{4,7}$/;
|
|
66
|
-
const TODAY = new Date().toISOString().slice(0, 10);
|
|
67
66
|
|
|
68
67
|
/**
|
|
69
68
|
* Extract the year from a CVE-YYYY-NNN identifier. Returns null for non-CVE
|
|
@@ -274,7 +273,11 @@ function findRegressionCandidates(diffs, catalog, opts) {
|
|
|
274
273
|
candidates,
|
|
275
274
|
historical_id_threshold_year: thresholdYear,
|
|
276
275
|
evaluated_diffs: evaluated,
|
|
277
|
-
|
|
276
|
+
// Stamp from the same clock that derives the threshold year, so both
|
|
277
|
+
// date-derived report fields share one instant. Falls back to call-time
|
|
278
|
+
// `new Date()` (via the `now` default) when no clock is injected — never
|
|
279
|
+
// a module-load constant, which would go stale in a long-lived process.
|
|
280
|
+
generated_at: now.toISOString().slice(0, 10),
|
|
278
281
|
control_ref: 'NEW-CTRL-074',
|
|
279
282
|
};
|
|
280
283
|
}
|
package/lib/playbook-runner.js
CHANGED
|
@@ -3425,9 +3425,22 @@ function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
|
3425
3425
|
if (runOpts.session_id) {
|
|
3426
3426
|
sessionId = runOpts.session_id;
|
|
3427
3427
|
} else if (runOpts.bundleDeterministic) {
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
.
|
|
3428
|
+
let submissionDigest;
|
|
3429
|
+
try {
|
|
3430
|
+
submissionDigest = crypto.createHash('sha256')
|
|
3431
|
+
.update(canonicalStringify(extractSubmissionForHash(agentSubmission)))
|
|
3432
|
+
.digest('hex');
|
|
3433
|
+
} catch (e) {
|
|
3434
|
+
// canonicalStringify deliberately throws (EVIDENCE_TOO_DEEP) on
|
|
3435
|
+
// pathological nesting. The mutex lockfile and the _activeRuns entry
|
|
3436
|
+
// are already held here, but the protecting try/finally below has
|
|
3437
|
+
// not opened yet — release both before rethrowing, or the leaked
|
|
3438
|
+
// lockfile blocks every subsequent run of this playbook for as long
|
|
3439
|
+
// as this PID lives.
|
|
3440
|
+
_activeRuns.delete(playbookId);
|
|
3441
|
+
releaseLock(lockPath);
|
|
3442
|
+
throw e;
|
|
3443
|
+
}
|
|
3431
3444
|
sessionId = crypto.createHash('sha256')
|
|
3432
3445
|
.update(`${playbookId}\0${submissionDigest}\0${getEngineVersion()}`)
|
|
3433
3446
|
.digest('hex')
|
package/lib/refresh-external.js
CHANGED
|
@@ -109,13 +109,17 @@ function parseArgs(argv) {
|
|
|
109
109
|
// older than 7d or one that was prefetched without a signing keypair.
|
|
110
110
|
// EXCEPTD_FORCE_STALE=1 mirrors for non-interactive automation.
|
|
111
111
|
else if (a === "--force-stale") out.forceStale = true;
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
112
|
+
// --prefetch / --no-network are prefetch-cache operations. Capture them so
|
|
113
|
+
// main() can delegate to lib/prefetch.js (the same routing bin/exceptd.js
|
|
114
|
+
// performs) when this script is invoked directly — otherwise the help
|
|
115
|
+
// text's "report-only, no cache write" promise for --no-network is a lie
|
|
116
|
+
// on the direct path, which would fall through to the live refresh loop.
|
|
117
|
+
else if (a === "--no-network") { out.noNetwork = true; }
|
|
118
|
+
else if (a === "--prefetch") { out.prefetch = true; }
|
|
119
|
+
// Remaining bin-translated aliases are tolerated as no-ops at this layer
|
|
120
|
+
// so the unknown-flag guard below doesn't false-reject them.
|
|
117
121
|
else if (
|
|
118
|
-
a === "--
|
|
122
|
+
a === "--indexes-only" ||
|
|
119
123
|
a === "--network" || a === "--curate" || a === "--force-stale-acked"
|
|
120
124
|
) { /* accepted, no-op at this layer */ }
|
|
121
125
|
// Any remaining --flag is an unrecognized typo. Record it; refuse after
|
|
@@ -148,8 +152,10 @@ Modes:
|
|
|
148
152
|
place. Same trust anchor as \`npm update -g\`, only the
|
|
149
153
|
data slice changes — useful when you want fresher
|
|
150
154
|
intel without re-resolving CLI/lib code.
|
|
151
|
-
--prefetch
|
|
152
|
-
|
|
155
|
+
--prefetch populate the cache for offline use. Equivalent to
|
|
156
|
+
\`exceptd prefetch\`.
|
|
157
|
+
--no-network report-only: list what would be fetched, WITHOUT writing
|
|
158
|
+
the cache (the dry-run opposite of --prefetch).
|
|
153
159
|
--from-cache [<p>] read from prefetch cache (default .cache/upstream).
|
|
154
160
|
Combine with --apply to upsert against cached data
|
|
155
161
|
entirely offline. Cache must be pre-populated via --prefetch.
|
|
@@ -1099,15 +1105,16 @@ function loadCtx(opts) {
|
|
|
1099
1105
|
const abs = path.resolve(opts.fromCache);
|
|
1100
1106
|
ctx.cacheDir = abs;
|
|
1101
1107
|
if (!fs.existsSync(abs)) {
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1108
|
+
// Operators following the air-gap workflow hit this with an unhelpful
|
|
1109
|
+
// "path does not exist" stack trace. The cache is populated by
|
|
1110
|
+
// `exceptd refresh --prefetch` (which routes to prefetch) — NOT by
|
|
1111
|
+
// `--no-network`, which is the report-only dry run that writes nothing.
|
|
1112
|
+
// Tell them exactly that, and emit a structured JSON error to stderr
|
|
1113
|
+
// instead of a fatal stack trace.
|
|
1107
1114
|
const err = new Error(
|
|
1108
1115
|
`refresh: --from-cache path does not exist: ${abs}\n` +
|
|
1109
|
-
`Hint: the cache is populated by running \`exceptd refresh --
|
|
1110
|
-
`on a connected host first. Air-gap workflow: (1) on connected host: \`exceptd refresh --
|
|
1116
|
+
`Hint: the cache is populated by running \`exceptd refresh --prefetch\` ` +
|
|
1117
|
+
`on a connected host first. Air-gap workflow: (1) on connected host: \`exceptd refresh --prefetch\`, ` +
|
|
1111
1118
|
`(2) copy .cache/upstream/ across the boundary, (3) on offline host: \`exceptd refresh --from-cache --apply\`.`
|
|
1112
1119
|
);
|
|
1113
1120
|
err._exceptd_hint = true;
|
|
@@ -1526,6 +1533,24 @@ async function main() {
|
|
|
1526
1533
|
return;
|
|
1527
1534
|
}
|
|
1528
1535
|
|
|
1536
|
+
// `--prefetch` / `--no-network` are prefetch-cache operations. The operator
|
|
1537
|
+
// path (bin/exceptd.js) routes them to lib/prefetch.js; when this script is
|
|
1538
|
+
// invoked directly, delegate the SAME way so behavior matches the help text:
|
|
1539
|
+
// --prefetch populates the cache, --no-network is a report-only dry run that
|
|
1540
|
+
// writes nothing. Without this, the direct path fell through to the live
|
|
1541
|
+
// refresh loop and could egress + write refresh-report.json despite
|
|
1542
|
+
// --no-network.
|
|
1543
|
+
if (opts.prefetch || opts.noNetwork) {
|
|
1544
|
+
const { spawnSync } = require("child_process");
|
|
1545
|
+
const pfArgs = [require.resolve("./prefetch.js")];
|
|
1546
|
+
if (opts.noNetwork) pfArgs.push("--no-network");
|
|
1547
|
+
if (opts.source) pfArgs.push("--source", opts.source);
|
|
1548
|
+
if (opts.quiet) pfArgs.push("--quiet");
|
|
1549
|
+
const r = spawnSync(process.execPath, pfArgs, { stdio: "inherit" });
|
|
1550
|
+
process.exitCode = r.status == null ? 1 : r.status;
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1529
1554
|
// v0.12.0: `--advisory <id>` short-circuits the normal source loop and
|
|
1530
1555
|
// seeds a single CVE catalog entry from GHSA. Exits non-zero ("draft
|
|
1531
1556
|
// written, please review") so CI pipelines surface the needed editorial
|