@blamejs/exceptd-skills 0.10.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,5 +1,59 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1 — 2026-05-12
4
+
5
+ **Patch: operator-reported bugs from v0.10.0 first contact + scope-aware `run` default.**
6
+
7
+ ### New: `_meta.scope` + scope-aware multi-playbook `run`
8
+
9
+ Pre-0.10.1, `exceptd run` required a single explicit `<playbook>`. Operators had to know which of the 11 playbooks fit their context. Now:
10
+
11
+ - `exceptd run` (no args) auto-detects cwd: `.git/` → code playbooks; `/proc` + `/etc/os-release` → system playbooks. Always includes `cross-cutting`.
12
+ - `exceptd run --scope <type>` runs all playbooks matching `system | code | service | cross-cutting | all`.
13
+ - `exceptd run --all` runs every playbook.
14
+ - `exceptd run <playbook>` (explicit) keeps its existing behavior.
15
+
16
+ Each shipped playbook now carries `_meta.scope`:
17
+ - **system**: kernel · hardening · runtime · sbom · cred-stores
18
+ - **code**: secrets · containers
19
+ - **service**: mcp · ai-api · crypto
20
+ - **cross-cutting**: framework
21
+
22
+ Multi-playbook runs share one `session_id`; per-playbook attestations land under `.exceptd/attestations/<session_id>/<playbook_id>.json`. Aggregate output reports `summary.{succeeded, blocked, detected, inconclusive}`.
23
+
24
+ `exceptd plan` now groups output by scope by default with a `scope_summary` count. `--flat` returns the old flat list. `--scope <type>` filters.
25
+
26
+ ### Bug fixes from operator first-contact
27
+
28
+ 1. **Per-verb `--help` printed missing-arg errors.** `exceptd run --help` returned `{"ok":false,"error":"run: missing <playbookId> positional argument."}` instead of usage. Now every playbook verb (`plan`/`govern`/`direct`/`look`/`run`/`ingest`/`reattest`) honors `--help`/`-h` before positional validation and emits per-verb usage with flag descriptions, invocation modes, and `precondition_checks` submission shape.
29
+
30
+ 2. **Preconditions were invisible to the host AI.** Neither `govern` nor `look` surfaced `_meta.preconditions`, so the AI couldn't see what facts to declare in its submission. `run` would then halt with `precondition_unverified` and the AI was blind. Fix: `look` response now includes `preconditions: [{id, check, on_fail, description}]` plus a `precondition_submission_shape` field giving the literal JSON shape (`{ "precondition_checks": { "<id>": true } }`) and an example. AGENTS.md updated.
31
+
32
+ 3. **`precondition_checks` submission shape was undocumented in errors.** Preflight halt now returns a `remediation` field with the exact submission hint per failed precondition.
33
+
34
+ 4. **`matched_cves` violated AGENTS.md Hard Rule #1.** Pre-0.10.1 output emitted `[{cve_id, rwep, cisa_kev, active_exploitation, ai_discovered}]` only — missing CVSS score/vector, KEV due date, PoC availability, AI-assisted-weaponization flag, patch availability, live-patch availability, EPSS, affected_versions, ATLAS/ATT&CK refs. The framework's own hard rule (every CVE reference must carry CVSS + KEV + PoC + AI-discovery + active-exploitation + patch/live-patch availability — theoretical-only is refused) was violated by the runner itself. Fix: `analyze.matched_cves[]` entries now carry all 15 required + optional Hard Rule #1 fields populated from the catalog. Null only when the catalog lacks the value, never when the runner forgot to forward.
35
+
36
+ 5. **`detect.classification` ignored `signals.detection_classification`.** Agent could submit `{"detection_classification":"clean"}` with all-miss `signal_overrides` and still get `inconclusive`. Fix: agent override honored when set to `detected | inconclusive | not_detected | clean` (alias). Engine-computed classification used as fallback.
37
+
38
+ 6. **`compliance_theater_check.verdict` stuck at `pending_agent_run` when classification was clear.** When the framework playbook ran with clean `detect.classification = not_detected`, the theater verdict still came back as pending instead of `clear`. Fix: when agent didn't submit `theater_verdict`, engine derives one from classification (`not_detected` → `clear`; otherwise `pending_agent_run`). Aliases `clean` / `no_theater` map to `clear`.
39
+
40
+ 7. **No directive discoverability.** `exceptd plan` showed directive counts but not IDs/titles. Fix: `exceptd plan --directives` expands each playbook entry with `directives: [{id, title, applies_to}]`.
41
+
42
+ 8. **No attestation inventory command.** Operators accumulated attestations under `.exceptd/attestations/` with no inventory verb; discovery required shell-globbing. Fix: new `exceptd list-attestations [--playbook <id>]` enumerates every prior session, sorted newest-first, with truncated evidence_hash + capture timestamp + file path.
43
+
44
+ ### Deferred from operator report
45
+
46
+ These were noted in the same report and are scoped to v0.10.2 / v0.11:
47
+
48
+ - `framework-gap <framework> <cve-id>` named-framework filter doesn't match by gap-id prefix (carried over from v0.9.x).
49
+ - Crypto-codebase / library-internal playbook variant (new attack class for library authors).
50
+ - Framework-author operator persona (audit what you ship, not what you run).
51
+ - `reattest --latest <playbook>` / `--since <date>` (no need to know session-id).
52
+ - `run --diff-from-latest` for cron-driven baselines.
53
+ - `run --ci` exit-code-based gating for `.github/workflows/`.
54
+ - VEX consumption in sbom (`run sbom --vex vex.cdx.json` drops `known_not_affected` from analyze output).
55
+ - feeds_into threshold matrix documentation.
56
+
3
57
  ## 0.10.0 — 2026-05-11
4
58
 
5
59
  **Minor: seven-phase playbook contract. exceptd becomes a knowledge layer that AI assistants consume, not a parallel scanner.**
package/bin/exceptd.js CHANGED
@@ -101,7 +101,7 @@ const ORCHESTRATOR_PASSTHROUGH = new Set([
101
101
 
102
102
  // Seven-phase playbook verbs handled in-process (no subprocess dispatch).
103
103
  const PLAYBOOK_VERBS = new Set([
104
- "plan", "govern", "direct", "look", "run", "ingest", "reattest",
104
+ "plan", "govern", "direct", "look", "run", "ingest", "reattest", "list-attestations",
105
105
  ]);
106
106
 
107
107
  function readPkgVersion() {
@@ -149,8 +149,9 @@ Analyst:
149
149
 
150
150
  Playbook runner — seven-phase contract
151
151
  (govern → direct → look → detect → analyze → validate → close):
152
- plan [--playbook id]... List playbooks + directives (planning JSON).
153
- [--mode m] [--session-id id] [--pretty]
152
+ plan [--playbook id]... List playbooks + directives, grouped by scope.
153
+ [--scope system|code|service|cross-cutting|all]
154
+ [--flat] [--mode m] [--session-id id] [--pretty]
154
155
  govern <playbook> Phase 1: GRC context (jurisdictions, theater,
155
156
  framework gaps, skill_preload).
156
157
  [--directive id] [--mode m] [--air-gap]
@@ -160,8 +161,14 @@ Playbook runner — seven-phase contract
160
161
  look <playbook> Phase 3: artifact-collection spec the host AI
161
162
  should execute.
162
163
  [--directive id] [--air-gap]
163
- run <playbook> Phases 4-7: detect → analyze → validate → close
164
- from an agent submission JSON.
164
+ run [playbook] Phases 4-7: detect → analyze → validate → close.
165
+ Three invocation modes:
166
+ run <playbook> single playbook (explicit)
167
+ run --scope <type> run all playbooks of that scope
168
+ run --all run every playbook
169
+ run auto-detect from cwd:
170
+ .git/ → code
171
+ /proc + os-release → system
165
172
  [--directive id] [--evidence file|-]
166
173
  [--session-id id] [--session-key hex]
167
174
  [--force-stale] [--air-gap]
@@ -320,8 +327,15 @@ function firstDirectiveId(runner, playbookId) {
320
327
  }
321
328
 
322
329
  function dispatchPlaybook(cmd, argv) {
330
+ // Per-verb --help / -h before any positional-arg validation so users always
331
+ // get usage text instead of an error about missing arguments.
332
+ if (argv.includes("--help") || argv.includes("-h")) {
333
+ printPlaybookVerbHelp(cmd);
334
+ process.exit(0);
335
+ }
336
+
323
337
  const args = parseArgs(argv, {
324
- bool: ["pretty", "air-gap", "force-stale"],
338
+ bool: ["pretty", "air-gap", "force-stale", "all", "flat", "directives"],
325
339
  multi: ["playbook"],
326
340
  });
327
341
  const pretty = !!args.pretty;
@@ -350,24 +364,167 @@ function dispatchPlaybook(cmd, argv) {
350
364
  case "run": return cmdRun(runner, args, runOpts, pretty);
351
365
  case "ingest": return cmdIngest(runner, args, runOpts, pretty);
352
366
  case "reattest": return cmdReattest(runner, args, runOpts, pretty);
367
+ case "list-attestations": return cmdListAttestations(runner, args, runOpts, pretty);
353
368
  }
354
369
  } catch (e) {
355
370
  emitError(e.message, { verb: cmd }, pretty);
356
371
  }
357
372
  }
358
373
 
374
+ function printPlaybookVerbHelp(verb) {
375
+ const cmds = {
376
+ plan: `plan — list playbooks + directives, grouped by scope.
377
+
378
+ Flags:
379
+ --playbook <id> ... Filter to one or more playbook IDs.
380
+ --scope <type> Filter by scope: system | code | service | cross-cutting | all
381
+ --flat Disable grouped-by-scope output; emit flat list.
382
+ --directives Include directive id + title + applies_to per playbook.
383
+ --session-id <id> Reuse a specific session ID for the planning output.
384
+ --mode <m> Investigation mode forwarded into govern.
385
+ --pretty Indented JSON output.`,
386
+ govern: `govern <playbook> — phase 1, load GRC context for a playbook.
387
+
388
+ Args / flags:
389
+ <playbook> Playbook ID. Required positional.
390
+ --directive <id> Specific directive (default: first one).
391
+ --mode <m> Investigation mode forwarded into govern policy.
392
+ --air-gap Honor _meta.air_gap_mode + air_gap_alternative paths.
393
+ --pretty Indented JSON output.
394
+
395
+ Output: jurisdiction_obligations, theater_fingerprints, framework_context, skill_preload.`,
396
+ direct: `direct <playbook> — phase 2, threat context + skill chain + token budget.
397
+
398
+ Args / flags:
399
+ <playbook> Required positional.
400
+ --directive <id> Specific directive (default: first one).
401
+ --pretty Indented JSON output.`,
402
+ look: `look <playbook> — phase 3, artifact-collection spec the host AI executes.
403
+
404
+ Args / flags:
405
+ <playbook> Required positional.
406
+ --directive <id> Specific directive (default: first one).
407
+ --air-gap Honor air_gap_alternative paths.
408
+ --pretty Indented JSON output.
409
+
410
+ Output includes a 'preconditions' array — the host AI MUST verify each
411
+ precondition with its own probes and declare results back in the submission as:
412
+ { "precondition_checks": { "<id>": true | false } }
413
+ The runner refuses the run if a precondition with on_fail=halt is unverified.`,
414
+ run: `run [playbook] — phases 4-7 (detect → analyze → validate → close).
415
+
416
+ Invocation modes:
417
+ run <playbook> Single playbook (explicit).
418
+ run --scope <type> Run all playbooks of that scope.
419
+ run --all Run every playbook.
420
+ run Auto-detect from cwd:
421
+ .git/ → code playbooks
422
+ /proc + os-release → system playbooks
423
+ Always includes cross-cutting playbooks.
424
+
425
+ Flags:
426
+ --directive <id> Specific directive (default: first one per playbook).
427
+ --evidence <file|-> Path to submission JSON or '-' for stdin.
428
+ Single-playbook shape:
429
+ { artifacts, signal_overrides, signals, precondition_checks }
430
+ Multi-playbook shape:
431
+ { "<playbook_id>": { artifacts, ... }, ... }
432
+ --session-id <id> Reuse a specific session ID.
433
+ --session-key <hex> HMAC sign the evidence_package with this key.
434
+ --force-stale Override the threat_currency_score < 50 hard-block.
435
+ --air-gap Honor air_gap_alternative paths.
436
+ --pretty Indented JSON output.
437
+
438
+ Attestation is persisted to .exceptd/attestations/<session_id>/ on every
439
+ successful run (single: attestation.json; multi: <playbook_id>.json).`,
440
+ ingest: `ingest — alias for 'run' matching AGENTS.md terminology.
441
+
442
+ Flags:
443
+ --domain <id> Playbook ID (overrides submission.playbook_id).
444
+ --directive <id> Directive ID (overrides submission.directive_id).
445
+ --evidence <file|-> Submission JSON. May include playbook_id/directive_id.
446
+ --pretty Indented JSON output.`,
447
+ reattest: `reattest <session-id> — replay a prior session and diff the evidence_hash.
448
+
449
+ Args / flags:
450
+ <session-id> Required positional. Looks under .exceptd/attestations/<id>/.
451
+ --pretty Indented JSON output.
452
+
453
+ Reports: unchanged | drifted | resolved from evidence_hash + classification deltas.`,
454
+ };
455
+ process.stdout.write((cmds[verb] || `${verb} — no per-verb help available; see \`exceptd help\` for the full list.`) + "\n");
456
+ }
457
+
359
458
  function cmdPlan(runner, args, runOpts, pretty) {
360
- const playbookIds = args.playbook
459
+ let playbookIds = args.playbook
361
460
  ? (Array.isArray(args.playbook) ? args.playbook : [args.playbook])
362
461
  : null;
462
+ // --scope filters playbook list by _meta.scope.
463
+ if (!playbookIds && args.scope) {
464
+ playbookIds = filterPlaybooksByScope(runner, args.scope);
465
+ }
363
466
  const plan = runner.plan({
364
467
  playbookIds: playbookIds || undefined,
365
468
  mode: runOpts.mode,
366
469
  session_id: runOpts.session_id,
367
470
  });
471
+ // Default UX: group by scope unless --flat or a filter was applied.
472
+ if (!args.flat && !playbookIds) {
473
+ plan.grouped_by_scope = groupPlaybooksByScope(plan.playbooks);
474
+ plan.scope_summary = Object.fromEntries(
475
+ Object.entries(plan.grouped_by_scope).map(([s, list]) => [s, list.length])
476
+ );
477
+ }
478
+ // --directives expands each playbook entry with its directive id + title +
479
+ // applies_to so operators / AIs can pick a specific directive without
480
+ // grepping playbook source.
481
+ if (args.directives) {
482
+ for (const pb of plan.playbooks) {
483
+ const full = runner.loadPlaybook(pb.id);
484
+ pb.directives = full.directives.map(d => ({ id: d.id, title: d.title, applies_to: d.applies_to }));
485
+ }
486
+ }
368
487
  emit(plan, pretty);
369
488
  }
370
489
 
490
+ function filterPlaybooksByScope(runner, scope) {
491
+ const ids = runner.listPlaybooks();
492
+ return ids.filter(id => {
493
+ try {
494
+ const pb = runner.loadPlaybook(id);
495
+ return scope === "all" || pb._meta.scope === scope;
496
+ } catch { return false; }
497
+ });
498
+ }
499
+
500
+ function groupPlaybooksByScope(playbooks) {
501
+ const groups = {};
502
+ for (const pb of playbooks) {
503
+ const scope = pb.scope || pb._meta?.scope || "unscoped";
504
+ (groups[scope] = groups[scope] || []).push(pb.id);
505
+ }
506
+ return groups;
507
+ }
508
+
509
+ /**
510
+ * Auto-detect which scopes apply to the cwd. Returns an array of scope strings.
511
+ * - `code` when the cwd looks like a git repo
512
+ * - `system` when /proc + /etc/os-release exist (Linux host)
513
+ * - `service` always included as advisory — service investigations don't
514
+ * depend on cwd; the operator/AI decides whether to run them
515
+ *
516
+ * Returns at minimum `['cross-cutting']` so framework correlation can always
517
+ * run after other findings land.
518
+ */
519
+ function detectScopes() {
520
+ const detected = [];
521
+ if (fs.existsSync(path.join(process.cwd(), ".git"))) detected.push("code");
522
+ if (fs.existsSync("/proc") && fs.existsSync("/etc/os-release")) detected.push("system");
523
+ // service playbooks need explicit invocation — they have side effects
524
+ // (probing remote endpoints) so we don't auto-include them.
525
+ return detected.length ? detected : ["cross-cutting"];
526
+ }
527
+
371
528
  function cmdGovern(runner, args, runOpts, pretty) {
372
529
  const playbookId = args._[0];
373
530
  if (!playbookId) return emitError("govern: missing <playbookId> positional argument.", null, pretty);
@@ -396,8 +553,32 @@ function cmdLook(runner, args, runOpts, pretty) {
396
553
  }
397
554
 
398
555
  function cmdRun(runner, args, runOpts, pretty) {
399
- const playbookId = args._[0];
400
- if (!playbookId) return emitError("run: missing <playbookId> positional argument.", null, pretty);
556
+ const positional = args._[0];
557
+
558
+ // Multi-playbook dispatch path. Triggered by --all, --scope <type>, or by
559
+ // a bare `exceptd run` (no positional, no flags) which auto-detects scopes
560
+ // from the cwd.
561
+ if (!positional && (args.all || args.scope)) {
562
+ let ids;
563
+ if (args.all) {
564
+ ids = runner.listPlaybooks();
565
+ } else {
566
+ ids = filterPlaybooksByScope(runner, args.scope);
567
+ }
568
+ return cmdRunMulti(runner, ids, args, runOpts, pretty, { trigger: args.all ? "--all" : `--scope ${args.scope}` });
569
+ }
570
+ if (!positional && !args.all && !args.scope) {
571
+ const scopes = detectScopes();
572
+ const ids = scopes.flatMap(s => filterPlaybooksByScope(runner, s));
573
+ const unique = [...new Set(ids)];
574
+ if (unique.length === 0) {
575
+ return emitError("run: no playbook resolved. Pass <playbookId>, --scope <type>, or --all.", null, pretty);
576
+ }
577
+ return cmdRunMulti(runner, unique, args, runOpts, pretty, { trigger: "auto-detect", detected_scopes: scopes });
578
+ }
579
+
580
+ // Single-playbook path (existing behavior).
581
+ const playbookId = positional;
401
582
  const pb = runner.loadPlaybook(playbookId);
402
583
  const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
403
584
  if (!directiveId) return emitError(`run: playbook ${playbookId} has no directives.`, null, pretty);
@@ -446,6 +627,81 @@ function cmdRun(runner, args, runOpts, pretty) {
446
627
  emit(result, pretty);
447
628
  }
448
629
 
630
+ /**
631
+ * Multi-playbook run. Iterates `ids` (already filtered by scope or auto-detect),
632
+ * runs each through runner.run with a shared session_id, persists each
633
+ * attestation under .exceptd/attestations/<session_id>/<playbook_id>.json, and
634
+ * emits a single aggregate bundle. Refuses if no evidence is provided (the
635
+ * host AI MUST submit observations per playbook — the engine can't synthesize them).
636
+ *
637
+ * Evidence shape for multi-run: { <playbook_id>: { artifacts, signal_overrides, signals, precondition_checks } }
638
+ * Falls back to running every playbook with empty evidence (engine returns
639
+ * inconclusive findings + visibility gaps) when no --evidence is given.
640
+ */
641
+ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
642
+ const sessionId = runOpts.session_id || require("crypto").randomBytes(8).toString("hex");
643
+ runOpts.session_id = sessionId;
644
+
645
+ let bundle = {};
646
+ if (args.evidence) {
647
+ try { bundle = readEvidence(args.evidence); } catch (e) {
648
+ return emitError(`run: failed to read evidence bundle: ${e.message}`, { evidence: args.evidence }, pretty);
649
+ }
650
+ }
651
+
652
+ const results = [];
653
+ for (const id of ids) {
654
+ const pb = runner.loadPlaybook(id);
655
+ const directiveId = args.directive || (pb.directives[0] && pb.directives[0].id);
656
+ if (!directiveId) {
657
+ results.push({ playbook_id: id, ok: false, error: "no directives" });
658
+ continue;
659
+ }
660
+ const submission = bundle[id] || {};
661
+ const perRunOpts = { ...runOpts };
662
+ if (submission.precondition_checks) perRunOpts.precondition_checks = submission.precondition_checks;
663
+
664
+ const result = runner.run(id, directiveId, submission, perRunOpts);
665
+
666
+ // Persist per-playbook attestation under the shared session.
667
+ if (result && result.ok) {
668
+ try {
669
+ const dir = path.join(process.cwd(), ".exceptd", "attestations", sessionId);
670
+ fs.mkdirSync(dir, { recursive: true });
671
+ fs.writeFileSync(
672
+ path.join(dir, `${id}.json`),
673
+ JSON.stringify({
674
+ session_id: sessionId,
675
+ playbook_id: id,
676
+ directive_id: directiveId,
677
+ evidence_hash: result.evidence_hash,
678
+ submission,
679
+ run_opts: { airGap: perRunOpts.airGap, forceStale: perRunOpts.forceStale, mode: perRunOpts.mode },
680
+ captured_at: new Date().toISOString(),
681
+ }, null, 2)
682
+ );
683
+ } catch { /* non-fatal */ }
684
+ }
685
+ results.push(result);
686
+ }
687
+
688
+ emit({
689
+ ok: results.every(r => r.ok !== false),
690
+ session_id: sessionId,
691
+ trigger: meta.trigger,
692
+ detected_scopes: meta.detected_scopes || null,
693
+ playbooks_run: ids,
694
+ summary: {
695
+ total: results.length,
696
+ succeeded: results.filter(r => r.ok !== false).length,
697
+ blocked: results.filter(r => r.ok === false).length,
698
+ detected: results.filter(r => r.phases?.detect?.classification === "detected").length,
699
+ inconclusive: results.filter(r => r.phases?.detect?.classification === "inconclusive").length,
700
+ },
701
+ results,
702
+ }, pretty);
703
+ }
704
+
449
705
  function cmdIngest(runner, args, runOpts, pretty) {
450
706
  // `ingest` matches the AGENTS.md ingest contract. The submission JSON may
451
707
  // carry playbook_id + directive_id; --domain/--directive flags override.
@@ -580,6 +836,44 @@ function cmdReattest(runner, args, runOpts, pretty) {
580
836
  }, pretty);
581
837
  }
582
838
 
839
+ function cmdListAttestations(runner, args, runOpts, pretty) {
840
+ const root = path.join(process.cwd(), ".exceptd", "attestations");
841
+ if (!fs.existsSync(root)) {
842
+ return emit({ ok: true, attestations: [], note: `No attestations directory at ${path.relative(process.cwd(), root)}` }, pretty);
843
+ }
844
+ const sessions = fs.readdirSync(root, { withFileTypes: true })
845
+ .filter(d => d.isDirectory())
846
+ .map(d => d.name);
847
+
848
+ const entries = [];
849
+ for (const sid of sessions) {
850
+ const sdir = path.join(root, sid);
851
+ const files = fs.readdirSync(sdir).filter(f => f.endsWith(".json"));
852
+ for (const f of files) {
853
+ try {
854
+ const j = JSON.parse(fs.readFileSync(path.join(sdir, f), "utf8"));
855
+ // Apply --playbook filter if supplied.
856
+ if (args.playbook && j.playbook_id !== args.playbook) continue;
857
+ entries.push({
858
+ session_id: sid,
859
+ playbook_id: j.playbook_id,
860
+ directive_id: j.directive_id,
861
+ evidence_hash: j.evidence_hash ? j.evidence_hash.slice(0, 16) + "..." : null,
862
+ captured_at: j.captured_at || null,
863
+ file: path.relative(process.cwd(), path.join(sdir, f)),
864
+ });
865
+ } catch { /* skip malformed */ }
866
+ }
867
+ }
868
+ entries.sort((a, b) => (b.captured_at || "").localeCompare(a.captured_at || ""));
869
+ emit({
870
+ ok: true,
871
+ attestations: entries,
872
+ count: entries.length,
873
+ filter: { playbook: args.playbook || null },
874
+ }, pretty);
875
+ }
876
+
583
877
  if (require.main === module) main();
584
878
 
585
879
  module.exports = { COMMANDS, PKG_ROOT, PLAYBOOK_VERBS };
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T05:54:24.968Z",
3
+ "generated_at": "2026-05-12T13:36:15.948Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 49,
6
6
  "source_hashes": {
7
- "manifest.json": "d10eff5e3267bca05be76ef3146f0ccd995a4d5a2dd5c958430b251432dfadff",
7
+ "manifest.json": "f71c110a187ffaf19db2bbbb1f15ceb27f88bbb844d81329c21ba6705badac8a",
8
8
  "data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
9
9
  "data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
10
10
  "data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",