@blamejs/exceptd-skills 0.14.1 → 0.14.3

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.3 — 2026-05-27
4
+
5
+ The resolved-citation cache is now integrity-checked. Each cached record carries a content digest (covering its `resolved_at` timestamp) that is verified on read, and freshness is gated on that timestamp rather than the file's modification time. A cache file edited in place — flipping a rejected CVE to "published" — is rejected as tampered instead of trusted, and a touched file can no longer resurrect a stale verdict. This closes a path where a writable cache could launder a rejected or fabricated citation into a passing verdict (which feeds `collect citation-hygiene --resolve` and, in turn, attestations).
6
+
7
+ `exceptd cve` and `exceptd rfc` reject unknown flags instead of silently ignoring them — a mistyped `--json` no longer emits human text into a pipe that asked for JSON.
8
+
9
+ Malformed evidence is rejected at the boundary. A JSON `null`, array, or scalar piped to `run --evidence -` now returns "evidence must be a JSON object" instead of being silently accepted as an empty run or surfacing as an internal error.
10
+
11
+ `collect citation-hygiene --resolve` now flags a cited RFC number that resolves to nothing, matching how it already flags a fabricated CVE. `ci --max-rwep` rejects a non-numeric or negative cap instead of silently coercing it to 0 (which had quietly degenerated the gate to "block everything"). `run --format` notes on stderr when it overrides `--json`. `cve`, `rfc`, `collect`, `watch`, and `report` are now listed in `exceptd help`.
12
+
13
+ ## 0.14.2 — 2026-05-27
14
+
15
+ `exceptd collect citation-hygiene --resolve` now resolves the cited CVEs the offline catalog can't confirm — once each, through the shared resolver cache — and flips their verdicts instead of parking them as inconclusive for an agent to chase: a rejected or disputed identifier becomes a hit, a well-formed identifier NVD doesn't know becomes fabricated, and a confirmed one clears. Honors `--air-gap` (catalog and cache only, no network).
16
+
17
+ The RFC index now includes obsoleted and historic RFCs (8888 entries, up from 7476). `exceptd rfc <number>` resolves a superseded RFC entirely offline — RFC 2616, for example, returns "Hypertext Transfer Protocol -- HTTP/1.1, obsoleted by RFC 7230–7235" — so confirming whether a cited RFC is still current no longer requires an IETF datatracker lookup.
18
+
3
19
  ## 0.14.1 — 2026-05-27
4
20
 
5
21
  Two citation resolvers — `exceptd cve <id>` and `exceptd rfc <number>` — answer "is this CVE/RFC citation valid?" so an agent gets the answer from exceptd instead of researching each identifier against NVD or the IETF datatracker by hand. A fan-out of agents auditing a codebase previously re-researched the same citations independently; these resolvers do it once and cache the result for the rest.
package/bin/exceptd.js CHANGED
@@ -450,8 +450,19 @@ Canonical verbs
450
450
  verify-attestation <sid> Alias for \`attest verify\`.
451
451
  run-all Alias for \`run --all\`.
452
452
 
453
+ cve <CVE-ID> Resolve a CVE citation: published | rejected | disputed
454
+ | fabricated | nonexistent (catalog → cache → one NVD
455
+ lookup). --air-gap/--no-network offline-only; exit 2 on
456
+ a citation that won't stand up.
457
+ rfc <number> Resolve an RFC number → title + status from the local
458
+ index (offline). --check "<title>" flags a mismatch.
459
+ collect <playbook> Run a playbook's companion collector; emits submission
460
+ JSON to pipe into \`run --evidence -\`. --resolve
461
+ (citation-hygiene) resolves uncatalogued citations.
453
462
  skill <name> Show context for a specific skill.
454
463
  framework-gap <fw> <ref> Programmatic gap analysis (one framework, one CVE/scenario).
464
+ watch [--alerts] Forward-watch aggregator across skills.
465
+ report [executive] Structured posture report.
455
466
  path Absolute path to the installed package.
456
467
  version Package version.
457
468
 
@@ -945,6 +956,19 @@ function readJsonFile(filePath) {
945
956
  }
946
957
  }
947
958
 
959
+ // Evidence must be a JSON object. `null`, an array, or a scalar parse as valid
960
+ // JSON but are not a submission — without this guard `null` NPE'd deep in the
961
+ // runner ("internal error") and `[]` / a wrong-typed field were silently
962
+ // accepted and run as if empty, so an operator believed a malformed submission
963
+ // was evaluated. Reject at the read boundary with an actionable message.
964
+ function asEvidenceObject(parsed) {
965
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
966
+ const got = parsed === null ? "null" : Array.isArray(parsed) ? "array" : typeof parsed;
967
+ throw new Error(`evidence must be a JSON object (e.g. {"artifacts": {...}, "signal_overrides": {...}}); got ${got}. Run \`exceptd brief <playbook>\` for the expected shape.`);
968
+ }
969
+ return parsed;
970
+ }
971
+
948
972
  function readEvidence(evidenceFlag) {
949
973
  if (!evidenceFlag) return {};
950
974
  // v0.12.12: file-path branch enforces a max size to defend against an
@@ -988,7 +1012,7 @@ function readEvidence(evidenceFlag) {
988
1012
  );
989
1013
  return {};
990
1014
  }
991
- return JSON.parse(text);
1015
+ return asEvidenceObject(JSON.parse(text));
992
1016
  }
993
1017
  let stat;
994
1018
  try { stat = fs.statSync(evidenceFlag); }
@@ -999,7 +1023,7 @@ function readEvidence(evidenceFlag) {
999
1023
  // Route through readJsonFile() for UTF-8-BOM / UTF-16 tolerance.
1000
1024
  // Windows-tool-emitted JSON commonly carries these markers; the raw "utf8"
1001
1025
  // decode in readFileSync chokes on the leading 0xFEFF.
1002
- return readJsonFile(evidenceFlag);
1026
+ return asEvidenceObject(readJsonFile(evidenceFlag));
1003
1027
  }
1004
1028
 
1005
1029
  function loadRunner() {
@@ -2337,7 +2361,7 @@ Flags (selected — see \`exceptd run --help\` for the full list):
2337
2361
  * its evidence JSON before going through phases 4-7. Returns a categorized
2338
2362
  * list: ok / missing_required / unknown_keys / type_mismatch / suggestions.
2339
2363
  */
2340
- function cmdCollect(runner, args, runOpts, pretty) {
2364
+ async function cmdCollect(runner, args, runOpts, pretty) {
2341
2365
  const playbookId = args._[0];
2342
2366
  if (!playbookId) {
2343
2367
  return emitError(
@@ -2439,6 +2463,30 @@ function cmdCollect(runner, args, runOpts, pretty) {
2439
2463
  try { pbMetaAirGap = !!(runner.loadPlaybook(playbookId)?._meta?.air_gap_mode); }
2440
2464
  catch { /* playbook load shouldn't fail here — collector exists — but be defensive */ }
2441
2465
  const collectAirGap = !!(runOpts.airGap || process.env.EXCEPTD_AIR_GAP === "1" || pbMetaAirGap);
2466
+
2467
+ // --resolve: resolve the citations the offline catalog couldn't confirm,
2468
+ // flipping their parked signals instead of leaving them inconclusive for the
2469
+ // operator to research. Opt-in, collector-specific (only citation-hygiene
2470
+ // exposes applyResolution). Honors the collect air-gap disposition.
2471
+ if (args.resolve) {
2472
+ if (typeof mod.applyResolution !== "function") {
2473
+ return emitError(
2474
+ `collect: --resolve is not supported by the "${playbookId}" collector (no resolution step).`,
2475
+ { verb: "collect", playbook_id: playbookId },
2476
+ pretty,
2477
+ );
2478
+ }
2479
+ try {
2480
+ submission = await mod.applyResolution(submission, { airGap: collectAirGap });
2481
+ } catch (e) {
2482
+ return emitError(
2483
+ `collect: --resolve failed for "${playbookId}": ${e.message}`,
2484
+ { verb: "collect", playbook_id: playbookId, exit_code: 2 },
2485
+ pretty,
2486
+ );
2487
+ }
2488
+ }
2489
+
2442
2490
  // Spread `submission` first, then explicit fields, so a submission key
2443
2491
  // named `air_gap_mode` (currently always undefined but defensive against
2444
2492
  // future collector contracts) can't clobber the envelope marker.
@@ -3552,6 +3600,14 @@ function cmdRun(runner, args, runOpts, pretty) {
3552
3600
  pretty,
3553
3601
  );
3554
3602
  }
3603
+ // --format wins over --json (one stdout document). Note it rather than
3604
+ // silently discarding --json — a script that pipes for JSON and later adds
3605
+ // --format markdown for a human would otherwise get non-JSON with no signal.
3606
+ if ((args.json || global.__exceptdWantJson) && requested !== "json") {
3607
+ process.stderr.write(
3608
+ `[exceptd] note: --format "${requested}" overrides --json; stdout is the ${requested} document, not the JSON envelope.\n`
3609
+ );
3610
+ }
3555
3611
  // Only one document can be written to stdout. When several --format values
3556
3612
  // are given, emit the first and tell the operator where the rest live so
3557
3613
  // the extras aren't silently dropped.
@@ -7807,6 +7863,16 @@ function cmdAsk(runner, args, runOpts, pretty) {
7807
7863
  function cmdCi(runner, args, runOpts, pretty) {
7808
7864
  const scope = args.scope;
7809
7865
  const maxRwep = args["max-rwep"] !== undefined ? Number(args["max-rwep"]) : null;
7866
+ // Reject a non-numeric / negative cap rather than silently coercing it.
7867
+ // `--max-rwep abc` previously became Number→NaN→0, degenerating the gate to
7868
+ // "block everything at RWEP 0" with no error — a silently-broken CI gate.
7869
+ if (maxRwep !== null && (!Number.isFinite(maxRwep) || maxRwep < 0)) {
7870
+ return emitError(
7871
+ `ci: --max-rwep must be a non-negative number; got ${JSON.stringify(String(args["max-rwep"]))}.`,
7872
+ { verb: "ci", provided: args["max-rwep"] },
7873
+ pretty,
7874
+ );
7875
+ }
7810
7876
  const blockOnClock = !!args["block-on-jurisdiction-clock"];
7811
7877
 
7812
7878
  // v0.11.9 (#115): --required <playbook,playbook,...> takes precedence over
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-27T12:31:54.921Z",
3
+ "generated_at": "2026-05-27T14:56:20.154Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "cc72d668222e9cd18eaef7928e173ad17434ff4b411235bbba2b13fca722db9e",
7
+ "manifest.json": "cfe6dbf90ca102b12a5d538e2744df007f7c8df306dc2ccdeac76f408c328d91",
8
8
  "data/atlas-ttps.json": "d24bc02859d40ccf1615db75cca68c077585904e41e0d8f6de448121e9b1abb0",
9
9
  "data/attack-techniques.json": "fa193f0d2d248176a8beddb641e9fe56ba4faa9e15dc253ff876dbf0c5d58a77",
10
10
  "data/cve-catalog.json": "3d451dda7ac0c7d57a4075ae4bafd3148c6184b35dc1bc59d8b81d1f2641e430",
@@ -14,7 +14,7 @@
14
14
  "data/exploit-availability.json": "ec2656f0d9a893610e27b43eb6035fe9b18e057c9f6dfaac7e7d4959bbcbb795",
15
15
  "data/framework-control-gaps.json": "46094ad9b9584f454daddd28f4c5d27faf0ea0510daafd38fb60bf9d30f6a305",
16
16
  "data/global-frameworks.json": "9ba563a85f7f8d6c3c957de64945e20925a89d0ed6ea6fc561cf093811acf558",
17
- "data/rfc-references.json": "66ef2e1f444a2cf0c2700a754f0a66030bb8a91d9e68394b9537ea1fe8b904fe",
17
+ "data/rfc-references.json": "b21d03b948c41bc8a854e2f057948ecf844bd8c105848aeb141d1eadf8192c31",
18
18
  "data/zeroday-lessons.json": "258252b9bff1fc11b05b76e10b659c1195971884bd44c92af5fefe17a5ca9512",
19
19
  "skills/kernel-lpe-triage/skill.md": "08b3e9815ba481c57c80f5fc0ccbf5bb7cbb41f570c235ba6ff9596b8c07354d",
20
20
  "skills/ai-attack-surface/skill.md": "c4c1eb22a38ca7a959b5725222bab8fbd4f4044a548a93f3e288e6f698334b72",
@@ -5,6 +5,14 @@
5
5
  "event_count": 54
6
6
  },
7
7
  "events": [
8
+ {
9
+ "date": "2026-05-27",
10
+ "type": "catalog_update",
11
+ "artifact": "data/rfc-references.json",
12
+ "path": "data/rfc-references.json",
13
+ "schema_version": "1.0.0",
14
+ "entry_count": 8888
15
+ },
8
16
  {
9
17
  "date": "2026-05-22",
10
18
  "type": "skill_review",
@@ -100,14 +108,6 @@
100
108
  "schema_version": "1.0.0",
101
109
  "entry_count": 468
102
110
  },
103
- {
104
- "date": "2026-05-19",
105
- "type": "catalog_update",
106
- "artifact": "data/rfc-references.json",
107
- "path": "data/rfc-references.json",
108
- "schema_version": "1.0.0",
109
- "entry_count": 7476
110
- },
111
111
  {
112
112
  "date": "2026-05-18",
113
113
  "type": "skill_review",
@@ -207,7 +207,7 @@
207
207
  "path": "data/rfc-references.json",
208
208
  "purpose": "IETF RFCs + active Internet-Drafts cited by skills (TLS, IPsec, PQ crypto migration, HTTP/3, CT). Cross-validated against IETF Datatracker via validate-rfcs.",
209
209
  "schema_version": "1.0.0",
210
- "last_updated": "2026-05-19",
210
+ "last_updated": "2026-05-27",
211
211
  "tlp": "CLEAR",
212
212
  "source_confidence_default": "A1",
213
213
  "freshness_policy": {
@@ -216,7 +216,7 @@
216
216
  "rebuild_after_days": 365,
217
217
  "note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
218
218
  },
219
- "entry_count": 7476,
219
+ "entry_count": 8888,
220
220
  "sample_keys": [
221
221
  "RFC-4301",
222
222
  "RFC-4303",