@blamejs/exceptd-skills 0.9.4 → 0.10.0
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 +45 -0
- package/CHANGELOG.md +131 -0
- package/README.md +30 -5
- package/bin/exceptd.js +403 -1
- package/data/_indexes/_meta.json +3 -3
- package/data/_indexes/currency.json +138 -138
- package/data/playbooks/ai-api.json +763 -0
- package/data/playbooks/containers.json +766 -0
- package/data/playbooks/cred-stores.json +715 -0
- package/data/playbooks/crypto.json +726 -0
- package/data/playbooks/framework.json +725 -0
- package/data/playbooks/hardening.json +672 -0
- package/data/playbooks/kernel.json +549 -0
- package/data/playbooks/mcp.json +727 -0
- package/data/playbooks/runtime.json +649 -0
- package/data/playbooks/sbom.json +893 -0
- package/data/playbooks/secrets.json +690 -0
- package/lib/cross-ref-api.js +224 -0
- package/lib/playbook-runner.js +826 -0
- package/lib/schemas/playbook.schema.json +652 -0
- package/lib/verify.js +45 -0
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/dispatcher.js +13 -1
- package/orchestrator/index.js +119 -5
- package/orchestrator/pipeline.js +8 -2
- package/orchestrator/scanner.js +191 -4
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/scripts/builders/currency.js +5 -3
package/bin/exceptd.js
CHANGED
|
@@ -24,6 +24,19 @@
|
|
|
24
24
|
* validate-rfcs [args] Cross-check RFC catalog against Datatracker.
|
|
25
25
|
* watchlist [args] Forward-watch aggregator.
|
|
26
26
|
* verify Verify every skill's Ed25519 signature.
|
|
27
|
+
*
|
|
28
|
+
* Seven-phase playbook contract (govern → direct → look → detect →
|
|
29
|
+
* analyze → validate → close):
|
|
30
|
+
*
|
|
31
|
+
* plan List playbooks + directives for session planning.
|
|
32
|
+
* govern <playbook> Phase 1: load GRC context.
|
|
33
|
+
* direct <playbook> Phase 2: scope the investigation.
|
|
34
|
+
* look <playbook> Phase 3: emit artifact-collection spec for agent.
|
|
35
|
+
* run <playbook> Phases 4-7 (detect/analyze/validate/close) from
|
|
36
|
+
* agent submission JSON.
|
|
37
|
+
* ingest Alias for `run` matching AGENTS.md terminology.
|
|
38
|
+
* reattest <session> Re-run a prior session and diff evidence_hash.
|
|
39
|
+
*
|
|
27
40
|
* help, --help, -h This help.
|
|
28
41
|
* version, --version,
|
|
29
42
|
* -v Print the package version.
|
|
@@ -68,11 +81,27 @@ const COMMANDS = {
|
|
|
68
81
|
"validate-cves": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
69
82
|
"validate-rfcs": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
70
83
|
watchlist: () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
84
|
+
"framework-gap": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
85
|
+
"framework-gap-analysis": () => path.join(PKG_ROOT, "orchestrator", "index.js"),
|
|
86
|
+
// Seven-phase playbook verbs — handled in-process via lib/playbook-runner.js.
|
|
87
|
+
plan: null,
|
|
88
|
+
govern: null,
|
|
89
|
+
direct: null,
|
|
90
|
+
look: null,
|
|
91
|
+
run: null,
|
|
92
|
+
ingest: null,
|
|
93
|
+
reattest: null,
|
|
71
94
|
};
|
|
72
95
|
|
|
73
96
|
const ORCHESTRATOR_PASSTHROUGH = new Set([
|
|
74
97
|
"scan", "dispatch", "skill", "currency", "report",
|
|
75
98
|
"validate-cves", "validate-rfcs", "watchlist",
|
|
99
|
+
"framework-gap", "framework-gap-analysis",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
// Seven-phase playbook verbs handled in-process (no subprocess dispatch).
|
|
103
|
+
const PLAYBOOK_VERBS = new Set([
|
|
104
|
+
"plan", "govern", "direct", "look", "run", "ingest", "reattest",
|
|
76
105
|
]);
|
|
77
106
|
|
|
78
107
|
function readPkgVersion() {
|
|
@@ -118,6 +147,31 @@ Analyst:
|
|
|
118
147
|
validate-rfcs [args] Cross-check RFC catalog vs IETF Datatracker.
|
|
119
148
|
watchlist [args] Forward-watch aggregator across skills.
|
|
120
149
|
|
|
150
|
+
Playbook runner — seven-phase contract
|
|
151
|
+
(govern → direct → look → detect → analyze → validate → close):
|
|
152
|
+
plan [--playbook id]... List playbooks + directives (planning JSON).
|
|
153
|
+
[--mode m] [--session-id id] [--pretty]
|
|
154
|
+
govern <playbook> Phase 1: GRC context (jurisdictions, theater,
|
|
155
|
+
framework gaps, skill_preload).
|
|
156
|
+
[--directive id] [--mode m] [--air-gap]
|
|
157
|
+
direct <playbook> Phase 2: scope (threat_context, rwep_threshold,
|
|
158
|
+
skill_chain, token_budget).
|
|
159
|
+
[--directive id]
|
|
160
|
+
look <playbook> Phase 3: artifact-collection spec the host AI
|
|
161
|
+
should execute.
|
|
162
|
+
[--directive id] [--air-gap]
|
|
163
|
+
run <playbook> Phases 4-7: detect → analyze → validate → close
|
|
164
|
+
from an agent submission JSON.
|
|
165
|
+
[--directive id] [--evidence file|-]
|
|
166
|
+
[--session-id id] [--session-key hex]
|
|
167
|
+
[--force-stale] [--air-gap]
|
|
168
|
+
ingest Alias for 'run' matching AGENTS.md terminology.
|
|
169
|
+
[--domain id] [--directive id] [--evidence f|-]
|
|
170
|
+
reattest <session-id> Re-run prior attestation, diff evidence_hash,
|
|
171
|
+
report unchanged | drifted | resolved.
|
|
172
|
+
|
|
173
|
+
Output flags (playbook verbs): default JSON one-line; --pretty for indented.
|
|
174
|
+
|
|
121
175
|
Common:
|
|
122
176
|
help This help.
|
|
123
177
|
version Package version.
|
|
@@ -155,6 +209,13 @@ function main() {
|
|
|
155
209
|
process.exit(0);
|
|
156
210
|
}
|
|
157
211
|
|
|
212
|
+
// Seven-phase playbook verbs run in-process — they emit JSON to stdout
|
|
213
|
+
// rather than dispatch to a script.
|
|
214
|
+
if (PLAYBOOK_VERBS.has(cmd)) {
|
|
215
|
+
dispatchPlaybook(cmd, rest);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
158
219
|
const resolver = COMMANDS[cmd];
|
|
159
220
|
if (typeof resolver !== "function") {
|
|
160
221
|
process.stderr.write(`exceptd: unknown command "${cmd}". Run \`exceptd help\` for the list.\n`);
|
|
@@ -178,6 +239,347 @@ function main() {
|
|
|
178
239
|
process.exit(typeof res.status === "number" ? res.status : 1);
|
|
179
240
|
}
|
|
180
241
|
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Seven-phase playbook dispatch (in-process)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Tiny POSIX-ish argv parser. Recognised forms:
|
|
248
|
+
* --flag → boolean true
|
|
249
|
+
* --key value → string
|
|
250
|
+
* --key=value → string
|
|
251
|
+
* --repeatable v1 --repeatable v2 → array (when listed in `multi`)
|
|
252
|
+
* Bare positional args land in `_`. Unknown flags fall through as booleans /
|
|
253
|
+
* strings using the same rules so the harness stays forgiving for future
|
|
254
|
+
* additions without forcing a schema bump here.
|
|
255
|
+
*/
|
|
256
|
+
function parseArgs(argv, opts) {
|
|
257
|
+
const knownBool = new Set(opts.bool || []);
|
|
258
|
+
const knownMulti = new Set(opts.multi || []);
|
|
259
|
+
const out = { _: [] };
|
|
260
|
+
for (let i = 0; i < argv.length; i++) {
|
|
261
|
+
const a = argv[i];
|
|
262
|
+
if (a.startsWith("--")) {
|
|
263
|
+
const eq = a.indexOf("=");
|
|
264
|
+
const key = (eq === -1 ? a.slice(2) : a.slice(2, eq));
|
|
265
|
+
if (eq !== -1) {
|
|
266
|
+
const val = a.slice(eq + 1);
|
|
267
|
+
if (knownMulti.has(key)) { (out[key] = out[key] || []).push(val); }
|
|
268
|
+
else out[key] = val;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (knownBool.has(key)) { out[key] = true; continue; }
|
|
272
|
+
// Look ahead for a value; if next token is another flag, treat as bool.
|
|
273
|
+
const next = argv[i + 1];
|
|
274
|
+
if (next === undefined || next.startsWith("--")) {
|
|
275
|
+
out[key] = true;
|
|
276
|
+
} else {
|
|
277
|
+
if (knownMulti.has(key)) { (out[key] = out[key] || []).push(next); }
|
|
278
|
+
else out[key] = next;
|
|
279
|
+
i++;
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
out._.push(a);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function emit(obj, pretty) {
|
|
289
|
+
const s = pretty ? JSON.stringify(obj, null, 2) : JSON.stringify(obj);
|
|
290
|
+
process.stdout.write(s + "\n");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function emitError(msg, extra, pretty) {
|
|
294
|
+
const body = Object.assign({ ok: false, error: msg }, extra || {});
|
|
295
|
+
const s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
|
|
296
|
+
process.stderr.write(s + "\n");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readEvidence(evidenceFlag) {
|
|
301
|
+
if (!evidenceFlag) return {};
|
|
302
|
+
if (evidenceFlag === "-") {
|
|
303
|
+
const buf = fs.readFileSync(0, "utf8"); // stdin
|
|
304
|
+
if (!buf.trim()) return {};
|
|
305
|
+
return JSON.parse(buf);
|
|
306
|
+
}
|
|
307
|
+
return JSON.parse(fs.readFileSync(evidenceFlag, "utf8"));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function loadRunner() {
|
|
311
|
+
return require(path.join(PKG_ROOT, "lib", "playbook-runner.js"));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function firstDirectiveId(runner, playbookId) {
|
|
315
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
316
|
+
if (!pb.directives || !pb.directives.length) {
|
|
317
|
+
throw new Error(`Playbook ${playbookId} has no directives.`);
|
|
318
|
+
}
|
|
319
|
+
return pb.directives[0].id;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function dispatchPlaybook(cmd, argv) {
|
|
323
|
+
const args = parseArgs(argv, {
|
|
324
|
+
bool: ["pretty", "air-gap", "force-stale"],
|
|
325
|
+
multi: ["playbook"],
|
|
326
|
+
});
|
|
327
|
+
const pretty = !!args.pretty;
|
|
328
|
+
const runOpts = {
|
|
329
|
+
airGap: !!args["air-gap"],
|
|
330
|
+
forceStale: !!args["force-stale"],
|
|
331
|
+
};
|
|
332
|
+
if (args["session-id"]) runOpts.session_id = args["session-id"];
|
|
333
|
+
if (args["session-key"]) runOpts.session_key = args["session-key"];
|
|
334
|
+
if (args.mode) runOpts.mode = args.mode;
|
|
335
|
+
|
|
336
|
+
let runner;
|
|
337
|
+
try {
|
|
338
|
+
runner = loadRunner();
|
|
339
|
+
} catch (e) {
|
|
340
|
+
emitError(`Failed to load lib/playbook-runner.js: ${e.message}`, null, pretty);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
switch (cmd) {
|
|
346
|
+
case "plan": return cmdPlan(runner, args, runOpts, pretty);
|
|
347
|
+
case "govern": return cmdGovern(runner, args, runOpts, pretty);
|
|
348
|
+
case "direct": return cmdDirect(runner, args, pretty);
|
|
349
|
+
case "look": return cmdLook(runner, args, runOpts, pretty);
|
|
350
|
+
case "run": return cmdRun(runner, args, runOpts, pretty);
|
|
351
|
+
case "ingest": return cmdIngest(runner, args, runOpts, pretty);
|
|
352
|
+
case "reattest": return cmdReattest(runner, args, runOpts, pretty);
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
emitError(e.message, { verb: cmd }, pretty);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function cmdPlan(runner, args, runOpts, pretty) {
|
|
360
|
+
const playbookIds = args.playbook
|
|
361
|
+
? (Array.isArray(args.playbook) ? args.playbook : [args.playbook])
|
|
362
|
+
: null;
|
|
363
|
+
const plan = runner.plan({
|
|
364
|
+
playbookIds: playbookIds || undefined,
|
|
365
|
+
mode: runOpts.mode,
|
|
366
|
+
session_id: runOpts.session_id,
|
|
367
|
+
});
|
|
368
|
+
emit(plan, pretty);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function cmdGovern(runner, args, runOpts, pretty) {
|
|
372
|
+
const playbookId = args._[0];
|
|
373
|
+
if (!playbookId) return emitError("govern: missing <playbookId> positional argument.", null, pretty);
|
|
374
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
375
|
+
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
376
|
+
if (!directiveId) return emitError(`govern: playbook ${playbookId} has no directives.`, null, pretty);
|
|
377
|
+
emit(runner.govern(playbookId, directiveId, runOpts), pretty);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function cmdDirect(runner, args, pretty) {
|
|
381
|
+
const playbookId = args._[0];
|
|
382
|
+
if (!playbookId) return emitError("direct: missing <playbookId> positional argument.", null, pretty);
|
|
383
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
384
|
+
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
385
|
+
if (!directiveId) return emitError(`direct: playbook ${playbookId} has no directives.`, null, pretty);
|
|
386
|
+
emit(runner.direct(playbookId, directiveId), pretty);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function cmdLook(runner, args, runOpts, pretty) {
|
|
390
|
+
const playbookId = args._[0];
|
|
391
|
+
if (!playbookId) return emitError("look: missing <playbookId> positional argument.", null, pretty);
|
|
392
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
393
|
+
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
394
|
+
if (!directiveId) return emitError(`look: playbook ${playbookId} has no directives.`, null, pretty);
|
|
395
|
+
emit(runner.look(playbookId, directiveId, runOpts), pretty);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cmdRun(runner, args, runOpts, pretty) {
|
|
399
|
+
const playbookId = args._[0];
|
|
400
|
+
if (!playbookId) return emitError("run: missing <playbookId> positional argument.", null, pretty);
|
|
401
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
402
|
+
const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
|
|
403
|
+
if (!directiveId) return emitError(`run: playbook ${playbookId} has no directives.`, null, pretty);
|
|
404
|
+
|
|
405
|
+
let submission = {};
|
|
406
|
+
if (args.evidence) {
|
|
407
|
+
try {
|
|
408
|
+
submission = readEvidence(args.evidence);
|
|
409
|
+
} catch (e) {
|
|
410
|
+
return emitError(`run: failed to read evidence: ${e.message}`, { evidence: args.evidence }, pretty);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Lift precondition_checks out of the submission into runOpts so the agent
|
|
415
|
+
// can declare host-platform / tool-availability facts in one JSON blob.
|
|
416
|
+
if (submission.precondition_checks) {
|
|
417
|
+
runOpts.precondition_checks = submission.precondition_checks;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const result = runner.run(playbookId, directiveId, submission, runOpts);
|
|
421
|
+
|
|
422
|
+
// Persist attestation for reattest cycles when the run succeeded.
|
|
423
|
+
if (result && result.ok && result.session_id) {
|
|
424
|
+
try {
|
|
425
|
+
const dir = path.join(process.cwd(), ".exceptd", "attestations", result.session_id);
|
|
426
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
427
|
+
fs.writeFileSync(
|
|
428
|
+
path.join(dir, "attestation.json"),
|
|
429
|
+
JSON.stringify({
|
|
430
|
+
session_id: result.session_id,
|
|
431
|
+
playbook_id: result.playbook_id,
|
|
432
|
+
directive_id: result.directive_id,
|
|
433
|
+
evidence_hash: result.evidence_hash,
|
|
434
|
+
submission,
|
|
435
|
+
run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
|
|
436
|
+
captured_at: new Date().toISOString(),
|
|
437
|
+
}, null, 2)
|
|
438
|
+
);
|
|
439
|
+
} catch { /* non-fatal — attestation persistence is best-effort */ }
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (result && result.ok === false) {
|
|
443
|
+
process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
emit(result, pretty);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function cmdIngest(runner, args, runOpts, pretty) {
|
|
450
|
+
// `ingest` matches the AGENTS.md ingest contract. The submission JSON may
|
|
451
|
+
// carry playbook_id + directive_id; --domain/--directive flags override.
|
|
452
|
+
let submission = {};
|
|
453
|
+
if (args.evidence) {
|
|
454
|
+
try {
|
|
455
|
+
submission = readEvidence(args.evidence);
|
|
456
|
+
} catch (e) {
|
|
457
|
+
return emitError(`ingest: failed to read evidence: ${e.message}`, { evidence: args.evidence }, pretty);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const playbookId = args.domain || submission.playbook_id || submission.domain;
|
|
461
|
+
if (!playbookId) return emitError("ingest: no playbook resolved — pass --domain <id> or include playbook_id in evidence JSON.", null, pretty);
|
|
462
|
+
const pb = runner.loadPlaybook(playbookId);
|
|
463
|
+
const directiveId = args.directive
|
|
464
|
+
|| submission.directive_id
|
|
465
|
+
|| (pb.directives[0] && pb.directives[0].id);
|
|
466
|
+
if (!directiveId) return emitError(`ingest: playbook ${playbookId} has no directives.`, null, pretty);
|
|
467
|
+
|
|
468
|
+
// Strip the routing keys so the runner only sees the contract shape it expects.
|
|
469
|
+
const cleanedSubmission = {
|
|
470
|
+
artifacts: submission.artifacts || {},
|
|
471
|
+
signal_overrides: submission.signal_overrides || {},
|
|
472
|
+
signals: submission.signals || {},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
if (submission.precondition_checks) {
|
|
476
|
+
runOpts.precondition_checks = submission.precondition_checks;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const result = runner.run(playbookId, directiveId, cleanedSubmission, runOpts);
|
|
480
|
+
|
|
481
|
+
if (result && result.ok && result.session_id) {
|
|
482
|
+
try {
|
|
483
|
+
const dir = path.join(process.cwd(), ".exceptd", "attestations", result.session_id);
|
|
484
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
485
|
+
fs.writeFileSync(
|
|
486
|
+
path.join(dir, "attestation.json"),
|
|
487
|
+
JSON.stringify({
|
|
488
|
+
session_id: result.session_id,
|
|
489
|
+
playbook_id: result.playbook_id,
|
|
490
|
+
directive_id: result.directive_id,
|
|
491
|
+
evidence_hash: result.evidence_hash,
|
|
492
|
+
submission: cleanedSubmission,
|
|
493
|
+
run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
|
|
494
|
+
captured_at: new Date().toISOString(),
|
|
495
|
+
}, null, 2)
|
|
496
|
+
);
|
|
497
|
+
} catch { /* non-fatal */ }
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (result && result.ok === false) {
|
|
501
|
+
process.stderr.write((pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)) + "\n");
|
|
502
|
+
process.exit(1);
|
|
503
|
+
}
|
|
504
|
+
emit(result, pretty);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function cmdReattest(runner, args, runOpts, pretty) {
|
|
508
|
+
const sessionId = args._[0];
|
|
509
|
+
if (!sessionId) return emitError("reattest: missing <session-id> positional argument.", null, pretty);
|
|
510
|
+
const dir = path.join(process.cwd(), ".exceptd", "attestations", sessionId);
|
|
511
|
+
const attFile = path.join(dir, "attestation.json");
|
|
512
|
+
if (!fs.existsSync(attFile)) {
|
|
513
|
+
return emitError(`reattest: no attestation found at ${path.relative(process.cwd(), attFile)}`, { session_id: sessionId }, pretty);
|
|
514
|
+
}
|
|
515
|
+
let prior;
|
|
516
|
+
try {
|
|
517
|
+
prior = JSON.parse(fs.readFileSync(attFile, "utf8"));
|
|
518
|
+
} catch (e) {
|
|
519
|
+
return emitError(`reattest: failed to parse prior attestation: ${e.message}`, { session_id: sessionId }, pretty);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Re-run with an empty submission against the same playbook/directive.
|
|
523
|
+
// Preserve only precondition_checks from the prior submission so the runner
|
|
524
|
+
// doesn't halt on host-environment guards (the reattest is about evidence
|
|
525
|
+
// drift, not re-verifying that the host is still Linux etc.).
|
|
526
|
+
const emptySubmission = { artifacts: {}, signal_overrides: {}, signals: {} };
|
|
527
|
+
const replayOpts = Object.assign({}, runOpts, {
|
|
528
|
+
airGap: !!(prior.run_opts && prior.run_opts.airGap) || runOpts.airGap,
|
|
529
|
+
forceStale: true, // bypass currency block on reattest — drift comparison is the point
|
|
530
|
+
});
|
|
531
|
+
if (prior.submission && prior.submission.precondition_checks) {
|
|
532
|
+
replayOpts.precondition_checks = prior.submission.precondition_checks;
|
|
533
|
+
} else {
|
|
534
|
+
// Fallback: synthesise pass-through preconditions from the playbook so the
|
|
535
|
+
// replay isn't blocked when the operator didn't originally pass them.
|
|
536
|
+
try {
|
|
537
|
+
const pb = runner.loadPlaybook(prior.playbook_id);
|
|
538
|
+
const synth = {};
|
|
539
|
+
for (const pc of (pb._meta && pb._meta.preconditions) || []) synth[pc.id] = true;
|
|
540
|
+
replayOpts.precondition_checks = synth;
|
|
541
|
+
} catch { /* ignore */ }
|
|
542
|
+
}
|
|
543
|
+
const replay = runner.run(prior.playbook_id, prior.directive_id, emptySubmission, replayOpts);
|
|
544
|
+
|
|
545
|
+
if (!replay || replay.ok === false) {
|
|
546
|
+
return emitError(`reattest: replay failed: ${replay && replay.reason || "unknown"}`, { replay }, pretty);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const priorHash = prior.evidence_hash;
|
|
550
|
+
const newHash = replay.evidence_hash;
|
|
551
|
+
let status;
|
|
552
|
+
if (priorHash === newHash) {
|
|
553
|
+
status = "unchanged";
|
|
554
|
+
} else {
|
|
555
|
+
// If the original was a detected finding and the replay no longer detects,
|
|
556
|
+
// call it "resolved"; otherwise "drifted".
|
|
557
|
+
const priorClassification = (prior.submission && prior.submission.signals
|
|
558
|
+
&& prior.submission.signals.detection_classification) || null;
|
|
559
|
+
const newClassification = replay.phases && replay.phases.detect && replay.phases.detect.classification;
|
|
560
|
+
if (priorClassification === "detected" && newClassification !== "detected") {
|
|
561
|
+
status = "resolved";
|
|
562
|
+
} else {
|
|
563
|
+
status = "drifted";
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
emit({
|
|
568
|
+
ok: true,
|
|
569
|
+
verb: "reattest",
|
|
570
|
+
session_id: sessionId,
|
|
571
|
+
playbook_id: prior.playbook_id,
|
|
572
|
+
directive_id: prior.directive_id,
|
|
573
|
+
status,
|
|
574
|
+
prior_evidence_hash: priorHash,
|
|
575
|
+
replay_evidence_hash: newHash,
|
|
576
|
+
prior_captured_at: prior.captured_at,
|
|
577
|
+
replayed_at: new Date().toISOString(),
|
|
578
|
+
replay_classification: replay.phases && replay.phases.detect && replay.phases.detect.classification,
|
|
579
|
+
replay_rwep_adjusted: replay.phases && replay.phases.analyze && replay.phases.analyze.rwep && replay.phases.analyze.rwep.adjusted,
|
|
580
|
+
}, pretty);
|
|
581
|
+
}
|
|
582
|
+
|
|
181
583
|
if (require.main === module) main();
|
|
182
584
|
|
|
183
|
-
module.exports = { COMMANDS, PKG_ROOT };
|
|
585
|
+
module.exports = { COMMANDS, PKG_ROOT, PLAYBOOK_VERBS };
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-12T05:54:24.968Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "d10eff5e3267bca05be76ef3146f0ccd995a4d5a2dd5c958430b251432dfadff",
|
|
8
8
|
"data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
|
|
9
9
|
"data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
|
|
10
10
|
"data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"jurisdiction_clocks": 29,
|
|
79
79
|
"did_ladders": 8,
|
|
80
80
|
"theater_fingerprints": 7,
|
|
81
|
-
"currency_action_required":
|
|
81
|
+
"currency_action_required": 0,
|
|
82
82
|
"frequency_fields": 7,
|
|
83
83
|
"activity_feed_events": 49,
|
|
84
84
|
"catalog_summaries": 10,
|