@blamejs/exceptd-skills 0.13.51 → 0.13.53

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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.53 — 2026-05-21
4
+
5
+ Polish round across CLI UX, container false-positives, collector skip-disclosure, and README narrative refresh.
6
+
7
+ ### Bugs
8
+
9
+ - **`containers` collector demotes the `dockerfile-runs-as-root` indicator on metadata-only Dockerfiles.** A Dockerfile that contains only `FROM <image>` (no `RUN`/`COPY`/`ADD`/`CMD`/`ENTRYPOINT`/`EXPOSE`/`VOLUME`/`WORKDIR`/`USER`/`HEALTHCHECK`/`SHELL` directive) is not a runtime image — it's a base-image probe used by `docker build` to extract a version label or similar. The runs-as-root predicate is meaningless on those; demote.
10
+ - **`scan` / `dispatch` / `currency` aliases relabelled in the README as legacy passthroughs.** These verbs dispatch to the v0.10.x orchestrator script and emit the legacy `{timestamp, host, findings}` shape — NOT the canonical verb's structured envelope. The previous README claim ("alias for `discover --scan-only`" / etc.) implied output-shape equivalence; corrected.
11
+
12
+ ### Features
13
+
14
+ - **`exceptd ask` alternates list now tags collector-backed playbooks with `[collector]`** (matching the discover output convention). Operators see at a glance which routed-to suggestions have a `collect | run` pipe path vs. which require AI-driven evidence.
15
+ - **`exceptd collect` emits a stderr skip-disclosure line when the collector's preconditions fail** (e.g. `[collect crypto] precondition not satisfied: linux-platform — empty submission emitted (collector skipped on this host)`). Previously the empty `signal_overrides` on a gated collector looked indistinguishable from "ran but found nothing".
16
+ - **Errors render as human text when stderr is a TTY** (interactive operator), and continue to emit the structured JSON envelope when stderr is piped (CI parsers, smart-agent retry, tests). Operators with stderr=tty see `error: <msg>\n hint: ...\n suggested: ...` lines; the JSON-by-default contract on piped stderr is preserved. Explicit `--json` / `--pretty` / `--json-stdout-only` forces JSON even on a TTY.
17
+ - **README narrative refresh** covering the v0.13.34+ evidence-collection layer: `exceptd collect <playbook>` verb, the discover-collect-run pipe pattern, `--attest-ownership` for cicd-pipeline-compromise, the 13/23 collector coverage, and the pointer to `exceptd doctor --collectors` for the live list.
18
+
19
+ ## 0.13.52 — 2026-05-21
20
+
21
+ README jurisdiction-count normalization; collect-verb + collector-pipe docs; new predeploy gate verifying AGENTS.md collector enumeration matches `lib/collectors/`.
22
+
23
+ ### Bugs
24
+
25
+ - **README jurisdiction count normalized to 35 across all three locations.** The preamble previously claimed "38 jurisdictions", the `jurisdiction-clocks.json` description claimed "29 jurisdictions", and the `global-frameworks.json` description claimed "35 jurisdictions" — three different numbers in the same file. The actual count in `data/global-frameworks.json` (top-level keys excluding `_meta` + the two `_*_summary` entries) is 35; all three README mentions now match.
26
+
27
+ ### Features
28
+
29
+ - **`exceptd collect <playbook>` documented in the CLI command reference.** The verb has shipped since v0.13.34 and now backs 13 of 23 playbooks, but the README only documented `discover` / `brief` / `run` / `ci` / `attest` / `doctor` / `ask` / `refresh` / `lint`. The reference now describes `collect` + the canonical `discover → collect → run --evidence -` pipe + the `--attest-ownership` flag (cicd-pipeline-compromise specific) + the pointer to `exceptd doctor --collectors` for the live list.
30
+ - **New predeploy gate: `scripts/check-agents-md-collectors.js`.** Verifies that AGENTS.md's "<N> reference collectors ship today (...)" paragraph stays in sync with the actual contents of `lib/collectors/`. Checks the spelled-out count word (Eleven / Twelve / ...) matches the on-disk count AND every collector path in the parenthesized list exists on disk AND every on-disk collector appears in the list. Predeploy gate count: 17 → 18.
31
+
3
32
  ## 0.13.51 — 2026-05-21
4
33
 
5
34
  `doctor` signing-check renders by severity; `crypto-codebase` weak-hash predicate demotes content-integrity files; `collect` no-arg hint points at operator-facing verbs.
package/README.md CHANGED
@@ -30,7 +30,7 @@ This platform surfaces what is actually happening right now. Every skill explici
30
30
 
31
31
  ## Status
32
32
 
33
- Pre-1.0. Latest release lives on [GitHub Releases](https://github.com/blamejs/exceptd-skills/releases) and on npm as [`@blamejs/exceptd-skills`](https://www.npmjs.com/package/@blamejs/exceptd-skills) with signed npm provenance attestation and Ed25519-signed skill bodies. The package ships 42 skills across kernel LPE, MCP supply chain, AI-as-C2, prompt injection, post-quantum crypto, SBOM integrity, identity-incident response, and 35 other AI/security domains, plus 10 intelligence catalogs (CVE / ATLAS / ATT&CK / CWE / D3FEND / DLP / RFC / framework gaps / global frameworks / zero-day lessons) covering 38 jurisdictions — the CVE catalog grew from 68 to 312 entries in v0.13.17 via a CISA KEV bulk-intake of `dateAdded >= 2024-01-01` actively-exploited vulnerabilities. 23 investigation playbooks (kernel, MCP, AI-API, framework, SBOM, runtime, hardening, secrets, cred-stores, containers, crypto, plus `webhook-callback-abuse`, `cicd-pipeline-compromise`, `identity-sso-compromise`, `llm-tool-use-exfil`, `post-quantum-migration`, `ai-discovered-cve-triage`, `supply-chain-recovery`, and more), a CLI for discovery and seven-phase investigation runs (`govern → direct → look → detect → analyze → validate → close`), and a nightly auto-refresh job that pulls KEV / NVD / EPSS / GHSA / OSV / IETF deltas plus 15 primary-source advisory + research-blog + tech-press feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, ZDI, kernel.org, oss-security, JFrog, CISA, Microsoft Security Blog, Sysdig, Trail of Bits, Embrace the Red, BleepingComputer security, The Hacker News, and a GitHub public-events tracker for the Nightmare-Eclipse researcher handle that anchors NEW-CTRL-073) into auto-PRs for editorial review. v0.13.17 also ships `lib/cve-regression-watcher.js` (NEW-CTRL-074) — a complementary detection method that surfaces poller-diff historical-CVE references as candidate silent-regression cases, the class anchored by MiniPlasma (a 2026 PoC drop that re-broke CVE-2020-17103 without any new ID being assigned).
33
+ Pre-1.0. Latest release lives on [GitHub Releases](https://github.com/blamejs/exceptd-skills/releases) and on npm as [`@blamejs/exceptd-skills`](https://www.npmjs.com/package/@blamejs/exceptd-skills) with signed npm provenance attestation and Ed25519-signed skill bodies. The package ships 42 skills across kernel LPE, MCP supply chain, AI-as-C2, prompt injection, post-quantum crypto, SBOM integrity, identity-incident response, and 35 other AI/security domains, plus 10 intelligence catalogs (CVE / ATLAS / ATT&CK / CWE / D3FEND / DLP / RFC / framework gaps / global frameworks / zero-day lessons) covering 35 jurisdictions — the CVE catalog grew from 68 to 312 entries in v0.13.17 via a CISA KEV bulk-intake of `dateAdded >= 2024-01-01` actively-exploited vulnerabilities. 23 investigation playbooks (kernel, MCP, AI-API, framework, SBOM, runtime, hardening, secrets, cred-stores, containers, crypto, plus `webhook-callback-abuse`, `cicd-pipeline-compromise`, `identity-sso-compromise`, `llm-tool-use-exfil`, `post-quantum-migration`, `ai-discovered-cve-triage`, `supply-chain-recovery`, and more), a CLI for discovery and seven-phase investigation runs (`govern → direct → look → detect → analyze → validate → close`), and a nightly auto-refresh job that pulls KEV / NVD / EPSS / GHSA / OSV / IETF deltas plus 15 primary-source advisory + research-blog + tech-press feeds (Qualys TRU, Red Hat RHSA, Ubuntu USN, ZDI, kernel.org, oss-security, JFrog, CISA, Microsoft Security Blog, Sysdig, Trail of Bits, Embrace the Red, BleepingComputer security, The Hacker News, and a GitHub public-events tracker for the Nightmare-Eclipse researcher handle that anchors NEW-CTRL-073) into auto-PRs for editorial review. v0.13.17 also ships `lib/cve-regression-watcher.js` (NEW-CTRL-074) — a complementary detection method that surfaces poller-diff historical-CVE references as candidate silent-regression cases, the class anchored by MiniPlasma (a 2026 PoC drop that re-broke CVE-2020-17103 without any new ID being assigned).
34
34
 
35
35
  ---
36
36
 
@@ -162,6 +162,8 @@ GitHub repo-pattern monitoring: `exceptd watchlist --org-scan --org <login>` pro
162
162
 
163
163
  AI-assistant config-file audit: `exceptd doctor --ai-config` walks `~/.claude`, `~/.cursor`, `~/.codeium`, `~/.aider`, and `~/.continue`, flagging sensitive files (`settings.json`, `mcp.json`, `*.mcp_config.json`, `api_key*`, `*.token`, `*.credentials`) not at mode 0600 on POSIX. On Windows the mode bits aren't load-bearing; each finding is surfaced with an info-level "manual ACL review" note. Catches the AI-config-credential-exfil class that the Shai-Hulud framework targets. Opt-in — does not run as part of the default no-flag `doctor` pass.
164
164
 
165
+ Evidence-collection layer: `exceptd collect <playbook>` invokes a companion script under `lib/collectors/<playbook>.js` that walks cwd, applies the catalogued regex set, stats permissions, and emits the submission JSON in the same shape `exceptd run --evidence -` accepts. 13 of 23 playbooks have collectors today (`ai-api`, `cicd-pipeline-compromise`, `containers`, `cred-stores`, `crypto`, `crypto-codebase`, `hardening`, `kernel`, `library-author`, `mcp`, `runtime`, `sbom`, `secrets`); the remaining 10 are policy-skipped per AGENTS.md (judgement-shaped incident / governance / pure-analyze playbooks where AI-driven evidence collection is the design). Canonical operator pipe: `exceptd collect <pb> | exceptd run <pb> --evidence -`. `exceptd doctor --collectors` enumerates the layer; `exceptd discover` tags applicable playbooks with `[collector]` when one ships. `cicd-pipeline-compromise` requires `--attest-ownership` on the collect call (the playbook's `operator-owns-ci-fleet` precondition is opt-in to prevent unauthorized CI assessments).
166
+
165
167
  Daily scheduled threat intake: a `routine: exceptd-threat-intake` (claude.ai remote agent) runs daily at 14:00 UTC. Sequence: `npm install` → `refresh --check-advisories` → `watchlist --alerts` → `refresh --apply` → `refresh --advisory <CVE-ID>` for up to 5 new CVE IDs from the primary-source feeds → re-sign + rebuild-indexes if the catalog mutated → commit on `intake/<YYYY-MM-DD>` branch with the full diff in the report. Closes the cadence gap that previously left fresh disclosures dependent on operator-triggered intake. Operator-managed at <https://claude.ai/code/routines>.
166
168
 
167
169
  Optional env vars for higher rate budgets:
@@ -257,6 +259,25 @@ exceptd ai-run <playbook> JSONL streaming variant of run. AI emits
257
259
  file handoff.
258
260
  --no-stream Single-shot mode (emit one combined JSON).
259
261
 
262
+ exceptd collect <playbook> Walk cwd + invoke the companion collector
263
+ under lib/collectors/<playbook>.js. Emits
264
+ a submission JSON ready to pipe into
265
+ `exceptd run <playbook> --evidence -`.
266
+ 13/23 playbooks have collectors; the rest
267
+ are AI-driven by design (incident /
268
+ governance / pure-analyze — see
269
+ AGENTS.md).
270
+ --cwd <path> Collect against a different repo / host.
271
+ --pretty Indented JSON.
272
+ --attest-ownership cicd-pipeline-compromise only — opt-in to
273
+ the operator-owns-ci-fleet precondition
274
+ so the runner doesn't halt at preflight.
275
+
276
+ # Canonical operator flow on a freshly-cloned repo:
277
+ exceptd discover # which playbooks apply here?
278
+ exceptd collect <pb> | exceptd run <pb> --evidence - # full pipe to verdict
279
+ exceptd doctor --collectors # list every collector + which are skipped
280
+
260
281
  exceptd attest <subverb> [<sid>] Auditor-facing operations.
261
282
  attest list Inventory all sessions across both
262
283
  ~/.exceptd and cwd-legacy roots.
@@ -397,20 +418,27 @@ Five verbs removed in v0.13.0 after deprecation since v0.11.0. Invoking any of t
397
418
  | `look <pb>` | `brief <pb> --phase look` |
398
419
  | `ingest` | `run` |
399
420
 
400
- The remaining v0.10.x verbs are aliases — still functional, no banner, no removal scheduled:
421
+ The remaining v0.10.x verbs are still functional, no banner, no removal scheduled. Two shapes:
422
+
423
+ **Canonical-equivalent aliases** — same output shape as the canonical verb; safe to use interchangeably:
424
+
425
+ | Alias | Canonical | Output shape |
426
+ |---|---|---|
427
+ | `verify` | `doctor --signatures` | matches canonical |
428
+ | `validate-cves` | `doctor --cves` | matches canonical |
429
+ | `validate-rfcs` | `doctor --rfcs` | matches canonical |
430
+ | `list-attestations` | `attest list` | matches canonical |
431
+ | `reattest <sid>` | `attest diff <sid>` | matches canonical |
432
+ | `prefetch` | `refresh --no-network` | matches canonical |
433
+ | `build-indexes` | `refresh --indexes-only` | matches canonical |
434
+
435
+ **Legacy passthrough verbs** — dispatch to the v0.10.x orchestrator script. The output shape is **NOT** identical to the canonical verb — it's the legacy `{timestamp, host, findings}` envelope. Use the canonical verb when you want the v0.11+ structured envelope contract; the passthrough is kept only for scripts that depend on the legacy output:
401
436
 
402
- | Alias | Canonical |
437
+ | Passthrough | Canonical (different output shape) |
403
438
  |---|---|
404
439
  | `scan` | `discover --scan-only` |
405
440
  | `dispatch` | `discover` |
406
441
  | `currency` | `doctor --currency` |
407
- | `verify` | `doctor --signatures` |
408
- | `validate-cves` | `doctor --cves` |
409
- | `validate-rfcs` | `doctor --rfcs` |
410
- | `reattest <sid>` | `attest diff <sid>` |
411
- | `list-attestations` | `attest list` |
412
- | `prefetch` | `refresh --no-network` |
413
- | `build-indexes` | `refresh --indexes-only` |
414
442
 
415
443
  ### Result envelope contract
416
444
 
@@ -483,7 +511,7 @@ If your tool has a conventional auto-load filename not listed here and you'd lik
483
511
  - **`recipes.json`** — 8 curated skill sequences for common use cases (AI red team prep, PCI audit defense, federal IR, DORA TLPT, K-12 EdTech review, ransomware tabletop, new-CVE triage, OSS dep triage).
484
512
  - **`chains.json`** — pre-hydrated cross-walks per CVE and per CWE: which skills cite this, which framework gaps it surfaces, which D3FEND countermeasures back it.
485
513
  - **`token-budget.json`** — approximate token cost per skill + per section for context budgeting.
486
- - **`jurisdiction-clocks.json`** — normalized jurisdiction × obligation × hours matrix (breach notification, patch SLA) across 29 jurisdictions.
514
+ - **`jurisdiction-clocks.json`** — normalized jurisdiction × obligation × hours matrix (breach notification, patch SLA) across 35 jurisdictions.
487
515
  - **`did-ladders.json`** — canonical defense-in-depth ladders per attack class (prompt injection, kernel LPE, AI-as-C2, ransomware, supply chain, BOLA, model exfiltration, BEC).
488
516
  - **`theater-fingerprints.json`** — structured records for the 7 compliance theater patterns: claim, audit evidence, reality, fast detection test, controls implicated.
489
517
  - **`_meta.json`** — sha256 of every source file. The `validate-indexes` predeploy gate fails if any source changed after the last build; `build-indexes --changed` reads this to know what to rebuild.
package/bin/exceptd.js CHANGED
@@ -796,20 +796,34 @@ function emit(obj, pretty, humanRenderer) {
796
796
  }
797
797
 
798
798
  function emitError(msg, extra, pretty) {
799
- // v0.12.14: the v0.11.13 emit() fix used exitCode + return
800
- // to defend stdout-buffered writes from truncation under piped consumers.
801
- // emitError() (stderr) kept process.exit(1), which has the same truncation
802
- // class — CLAUDE.md's "fix the class, not the instance." Now: write to
803
- // stderr, set exitCode = 1, return. Every caller already uses
804
- // `return emitError(...)` so the return-value propagation is clean.
799
+ // Stderr + exitCode + return (defends stdout drain under piped
800
+ // consumers same class as emit()'s v0.11.13 fix).
805
801
  //
806
- // Errors emit as JSON envelope on stderr regardless of mode that
807
- // contract has been load-bearing since v0.11.x for piped consumers
808
- // (CI parsers, smart-agent retry logic). Operators wanting human
809
- // text on stderr can pipe `2>&1 | jq -r .error || cat` as a
810
- // workaround.
802
+ // Output shape branches on whether stderr is attached to a TTY:
803
+ // - piped (CI parsers, smart-agent retry, tests using
804
+ // `tryJson(r.stderr)`): JSON envelope the load-bearing
805
+ // contract for programmatic consumers.
806
+ // - interactive (operator at a terminal): human-readable
807
+ // "error: <msg>" + indented helper lines. Operators wanting
808
+ // the structured envelope explicitly can pass --json.
809
+ // Explicit --json / --pretty / --json-stdout-only also force JSON
810
+ // regardless of TTY (e.g. an operator redirecting to a file).
811
811
  const body = Object.assign({ ok: false, error: msg }, extra || {});
812
- const s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
812
+ const wantJson = !!global.__exceptdWantJson || !!process.env.EXCEPTD_RAW_JSON;
813
+ const stderrIsTty = process.stderr.isTTY === true;
814
+ let s;
815
+ if (wantJson || !stderrIsTty) {
816
+ s = pretty ? JSON.stringify(body, null, 2) : JSON.stringify(body);
817
+ } else {
818
+ const lines = [`error: ${msg}`];
819
+ if (extra && typeof extra === "object") {
820
+ const helperFields = ["hint", "suggested", "did_you_mean", "remediation", "submission_hint"];
821
+ for (const key of helperFields) {
822
+ if (extra[key] != null) lines.push(` ${key}: ${extra[key]}`);
823
+ }
824
+ }
825
+ s = lines.join("\n");
826
+ }
813
827
  process.stderr.write(s + "\n");
814
828
  process.exitCode = EXIT_CODES.GENERIC_FAILURE;
815
829
  }
@@ -2264,10 +2278,23 @@ function cmdCollect(runner, args, runOpts, pretty) {
2264
2278
  return emitError(
2265
2279
  `collect: collector for "${playbookId}" threw an unhandled exception: ${e.message}. File a bug — collectors must catch their own errors and surface them via collector_errors[].`,
2266
2280
  { verb: "collect", playbook_id: playbookId, stack: e.stack || null, exit_code: 2 },
2267
- pretty
2281
+ pretty,
2268
2282
  );
2269
2283
  }
2270
2284
 
2285
+ // Skip-disclosure on stderr when a precondition gate returned
2286
+ // false (typically a platform gate — `linux-platform: false` on
2287
+ // win32 / macOS). Without this, the empty signal_overrides on a
2288
+ // gated collector looks indistinguishable from "the collector
2289
+ // ran but found nothing". Operators get one stderr line per
2290
+ // platform-skipped run.
2291
+ const failedPre = Object.entries(submission.precondition_checks || {})
2292
+ .filter(([, v]) => v === false)
2293
+ .map(([k]) => k);
2294
+ if (failedPre.length > 0 && (submission.signal_overrides && Object.keys(submission.signal_overrides).length === 0)) {
2295
+ process.stderr.write(`[collect ${playbookId}] precondition not satisfied: ${failedPre.join(", ")} — empty submission emitted (collector skipped on this host)\n`);
2296
+ }
2297
+
2271
2298
  // Emit the submission JSON to stdout. The operator pipes this into
2272
2299
  // `exceptd run <playbook> --evidence -` to drive a real verdict.
2273
2300
  // Human-rendered version is concise so an interactive operator can
@@ -7143,6 +7170,16 @@ function cmdAsk(runner, args, runOpts, pretty) {
7143
7170
  return;
7144
7171
  }
7145
7172
 
7173
+ // Enrich each match with whether a companion collector exists for
7174
+ // the playbook (same lookup discover uses). Operators see at a
7175
+ // glance which alternates have a collect|run pipe path vs. which
7176
+ // require AI-driven evidence.
7177
+ const collectorsDir = path.join(PKG_ROOT, "lib", "collectors");
7178
+ for (const t of top) {
7179
+ const collectorPath = path.join(collectorsDir, t.id + ".js");
7180
+ t.collector_available = fs.existsSync(collectorPath);
7181
+ }
7182
+
7146
7183
  const result = {
7147
7184
  verb: "ask",
7148
7185
  question,
@@ -7152,7 +7189,9 @@ function cmdAsk(runner, args, runOpts, pretty) {
7152
7189
  full_match_list: top,
7153
7190
  };
7154
7191
  if (args.json || args.pretty) return emit(result, pretty);
7155
- process.stdout.write(`ask: ${question}\n top match: ${top[0].id} (score ${top[0].score})\n next: ${result.next_step}\n alternates: ${top.slice(1).map(t => t.id).join(", ") || "(none)"}\n`);
7192
+ const topGlyph = top[0].collector_available ? " [collector]" : "";
7193
+ const altLine = top.slice(1).map(t => t.id + (t.collector_available ? " [collector]" : "")).join(", ") || "(none)";
7194
+ process.stdout.write(`ask: ${question}\n top match: ${top[0].id}${topGlyph} (score ${top[0].score})\n next: ${result.next_step}\n alternates: ${altLine}\n`);
7156
7195
  }
7157
7196
 
7158
7197
  /**
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-21T21:52:47.561Z",
3
+ "generated_at": "2026-05-21T23:13:39.676Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "8b7d17485a799593f74506f92ad8f129155ac52d40f508e90b4adf15cb11916d",
7
+ "manifest.json": "9eb25cdba45631f79eaf5ee628532b284912a2318d85c7a97d2acbaf2db31e08",
8
8
  "data/atlas-ttps.json": "d296c1d3e71807c9279b731f047e57796e85137f186586743a8cdad214b408f9",
9
9
  "data/attack-techniques.json": "49b6010b317edd219def135171ea8f3b1bbf1e00e9c5a08bf7237215ff54e2c3",
10
10
  "data/cve-catalog.json": "a09c83af3f9679a7ea73935726a1ff9de2cab94b4ab6321fc017fc147747d7c3",
@@ -114,6 +114,31 @@ function scanDockerfile(content, rel) {
114
114
  "dockerfile-curl-pipe-bash": [],
115
115
  };
116
116
 
117
+ // Metadata-only Dockerfile heuristic — when the file has a FROM
118
+ // line but no RUN / COPY / ADD / CMD / ENTRYPOINT / EXPOSE / VOLUME
119
+ // / WORKDIR / USER directives, it's not a runtime image. Examples:
120
+ // the go-version-scraping Dockerfile (cosign style) or a base-image-
121
+ // probe used only by `docker build` to extract a version label.
122
+ // The runs-as-root predicate is meaningless on those — demote.
123
+ const isMetadataOnly = (() => {
124
+ let sawFrom = false;
125
+ let sawBuildOrRuntime = false;
126
+ for (const raw of lines) {
127
+ const t = raw.trim();
128
+ if (!t || t.startsWith("#")) continue;
129
+ if (/^FROM\b/i.test(t)) { sawFrom = true; continue; }
130
+ if (/^(RUN|COPY|ADD|CMD|ENTRYPOINT|EXPOSE|VOLUME|WORKDIR|USER|HEALTHCHECK|ONBUILD|STOPSIGNAL|SHELL|ARG|ENV|LABEL)\b/i.test(t)) {
131
+ // ARG / ENV / LABEL alone aren't enough to make this a runtime
132
+ // image — only the directives that define execution shape do.
133
+ if (/^(RUN|COPY|ADD|CMD|ENTRYPOINT|EXPOSE|VOLUME|WORKDIR|USER|HEALTHCHECK|ONBUILD|SHELL)\b/i.test(t)) {
134
+ sawBuildOrRuntime = true;
135
+ break;
136
+ }
137
+ }
138
+ }
139
+ return sawFrom && !sawBuildOrRuntime;
140
+ })();
141
+
117
142
  let sawNonRootUser = false;
118
143
  let sawAnyUser = false;
119
144
  for (let i = 0; i < lines.length; i++) {
@@ -167,8 +192,9 @@ function scanDockerfile(content, rel) {
167
192
 
168
193
  // runs-as-root indicator: file fires if NO non-root USER directive
169
194
  // was seen anywhere. (sawAnyUser=false also counts — image defaults
170
- // to root.)
171
- if (!sawNonRootUser) {
195
+ // to root.) Demoted on metadata-only Dockerfiles (FROM-only, no
196
+ // execution-shape directives) — those aren't runtime images.
197
+ if (!sawNonRootUser && !isMetadataOnly) {
172
198
  hits["dockerfile-runs-as-root"].push({ file: rel, line: 0, snippet: sawAnyUser ? "USER directive sets root/0" : "no USER directive (defaults to root)" });
173
199
  }
174
200
 
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.13.51",
3
+ "version": "0.13.53",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -53,7 +53,7 @@
53
53
  ],
54
54
  "last_threat_review": "2026-05-01",
55
55
  "signature": "lXhZgoIrrVloO3XaTvo/43AxZn4mwErstd7DR0O/oVhD3AOGODM4HqrageYEou9WKOdMEGP5mJNTjJsXdP5NDA==",
56
- "signed_at": "2026-05-21T21:27:15.938Z",
56
+ "signed_at": "2026-05-21T23:11:05.491Z",
57
57
  "cwe_refs": [
58
58
  "CWE-125",
59
59
  "CWE-362",
@@ -117,7 +117,7 @@
117
117
  ],
118
118
  "last_threat_review": "2026-05-01",
119
119
  "signature": "vSVqu4wBm+d68ujZmM6Rto/HzViCkE0gPUcv/MYE/bjFiqamf/s0On4kTOo1KIveV9cOwYNxiItaGEWlVkRFDg==",
120
- "signed_at": "2026-05-21T21:27:15.940Z",
120
+ "signed_at": "2026-05-21T23:11:05.493Z",
121
121
  "cwe_refs": [
122
122
  "CWE-1039",
123
123
  "CWE-1426",
@@ -180,7 +180,7 @@
180
180
  ],
181
181
  "last_threat_review": "2026-05-01",
182
182
  "signature": "RIgXKvolQjgJdnlrDnVOd90IOY1B7VHHZD/YJQRzouL+wUeOLclPrdK/EgEuFyiu7lR4bi+Pl6aGB9G9tOxYCQ==",
183
- "signed_at": "2026-05-21T21:27:15.940Z",
183
+ "signed_at": "2026-05-21T23:11:05.493Z",
184
184
  "cwe_refs": [
185
185
  "CWE-22",
186
186
  "CWE-345",
@@ -226,7 +226,7 @@
226
226
  "framework_gaps": [],
227
227
  "last_threat_review": "2026-05-01",
228
228
  "signature": "RYOxeq/o3uTwTWq4H7RcdH2Aclg9UyCERfUH9Frwkzncsowg7LgxpaEDc3swTCv73HMEGbU8wVbXguZ4JxHUCQ==",
229
- "signed_at": "2026-05-21T21:27:15.940Z"
229
+ "signed_at": "2026-05-21T23:11:05.493Z"
230
230
  },
231
231
  {
232
232
  "name": "compliance-theater",
@@ -257,7 +257,7 @@
257
257
  ],
258
258
  "last_threat_review": "2026-05-01",
259
259
  "signature": "DneJCPKCPcoe6nQ82XptqSqNfSRdt1orKaO+o7K36YCciDrzwJb+1BuBLusPDtpcdDaGY0y0e+AqiTYJklhBAQ==",
260
- "signed_at": "2026-05-21T21:27:15.941Z"
260
+ "signed_at": "2026-05-21T23:11:05.494Z"
261
261
  },
262
262
  {
263
263
  "name": "exploit-scoring",
@@ -286,7 +286,7 @@
286
286
  ],
287
287
  "last_threat_review": "2026-05-01",
288
288
  "signature": "NA1hoQycvQhSUoG5rwlXX0mOVmGxoXRVezkELGEA2nZOdGis4gXkHT3O6Sfw7zxE4JuMrsCb65TEeOWk9WEPDg==",
289
- "signed_at": "2026-05-21T21:27:15.941Z"
289
+ "signed_at": "2026-05-21T23:11:05.494Z"
290
290
  },
291
291
  {
292
292
  "name": "rag-pipeline-security",
@@ -323,7 +323,7 @@
323
323
  ],
324
324
  "last_threat_review": "2026-05-01",
325
325
  "signature": "XgrzcA2brPhXrSTxrcLnJec0OpgGYJBoSTUlJ10UdePHffxqb9LTVGnfbmEk1ykQifXREZexui2bG7X/+eFfCQ==",
326
- "signed_at": "2026-05-21T21:27:15.942Z",
326
+ "signed_at": "2026-05-21T23:11:05.495Z",
327
327
  "cwe_refs": [
328
328
  "CWE-1395",
329
329
  "CWE-1426"
@@ -380,7 +380,7 @@
380
380
  ],
381
381
  "last_threat_review": "2026-05-01",
382
382
  "signature": "9+hZlZOqZdeACUmamQk66L5levZhhwnFXuYRhdT6Mce99eQaKT7wNfWq12hXQztkRcVRKaFH+a01zwJQwsRQCA==",
383
- "signed_at": "2026-05-21T21:27:15.942Z",
383
+ "signed_at": "2026-05-21T23:11:05.495Z",
384
384
  "d3fend_refs": [
385
385
  "D3-CA",
386
386
  "D3-CSPP",
@@ -415,7 +415,7 @@
415
415
  "framework_gaps": [],
416
416
  "last_threat_review": "2026-05-01",
417
417
  "signature": "ciqhVloMWWXEigPZvvwoV2c54tEqsDqsoc+sS/mNTFFJk2H+tz2+XUrgfEPRuYw0FeyNB6/+27pL2NpKHzUqAg==",
418
- "signed_at": "2026-05-21T21:27:15.942Z",
418
+ "signed_at": "2026-05-21T23:11:05.496Z",
419
419
  "cwe_refs": [
420
420
  "CWE-1188"
421
421
  ]
@@ -443,7 +443,7 @@
443
443
  "framework_gaps": [],
444
444
  "last_threat_review": "2026-05-01",
445
445
  "signature": "xiHAhhdufm9hCKU8PLiPE0MX65ej2F4OZwtlWLGLCiie9/km+Kiqbt192LcMvr94v83C98pb9wIaqFsFWft6AQ==",
446
- "signed_at": "2026-05-21T21:27:15.943Z"
446
+ "signed_at": "2026-05-21T23:11:05.496Z"
447
447
  },
448
448
  {
449
449
  "name": "global-grc",
@@ -475,7 +475,7 @@
475
475
  "framework_gaps": [],
476
476
  "last_threat_review": "2026-05-01",
477
477
  "signature": "oYsSk35N2Uzq7MRofACykylcVwkgPhI4luWZ14vmQT+gUKLyZiKVOUJbe1+7lGl6BYPRN0sUDQ0f7S5Eu5w2Ag==",
478
- "signed_at": "2026-05-21T21:27:15.943Z"
478
+ "signed_at": "2026-05-21T23:11:05.496Z"
479
479
  },
480
480
  {
481
481
  "name": "zeroday-gap-learn",
@@ -502,7 +502,7 @@
502
502
  "framework_gaps": [],
503
503
  "last_threat_review": "2026-05-01",
504
504
  "signature": "igRqYyU1unRFH40BsPyAR62SPrk8QZv8dPGb8S9O9EvLCNOZAzm3t+HdT/NKqzWHwrpomOzkkkyLfYI/0qTUDA==",
505
- "signed_at": "2026-05-21T21:27:15.943Z"
505
+ "signed_at": "2026-05-21T23:11:05.497Z"
506
506
  },
507
507
  {
508
508
  "name": "pqc-first",
@@ -554,7 +554,7 @@
554
554
  ],
555
555
  "last_threat_review": "2026-05-01",
556
556
  "signature": "vhc3wuQEro/86s1ro2b/KakUXg8QVnySYTBqA7ebzv9oeR2HYO5bvGEJp3oOHWtL37JDqcCAHYadSN/qxIyCCA==",
557
- "signed_at": "2026-05-21T21:27:15.944Z",
557
+ "signed_at": "2026-05-21T23:11:05.497Z",
558
558
  "cwe_refs": [
559
559
  "CWE-327"
560
560
  ],
@@ -601,7 +601,7 @@
601
601
  ],
602
602
  "last_threat_review": "2026-05-01",
603
603
  "signature": "MS35nWm8djfJGn4OOoT0JKJ2aO+Dkbb6wOOWJYvNZlRKT3UGA59o2gxg1JOnD20hb/RwxtkmCujhl2tuYSR+AQ==",
604
- "signed_at": "2026-05-21T21:27:15.944Z"
604
+ "signed_at": "2026-05-21T23:11:05.497Z"
605
605
  },
606
606
  {
607
607
  "name": "security-maturity-tiers",
@@ -638,7 +638,7 @@
638
638
  ],
639
639
  "last_threat_review": "2026-05-01",
640
640
  "signature": "8Px1s2lDj10/Q6erwEQlXgUHM1+OTruUR8qAHPX7Oo3k/l69N6P9sm0PsafS9wDFtj9l5C/OiLiFgzMlMt6vBw==",
641
- "signed_at": "2026-05-21T21:27:15.944Z",
641
+ "signed_at": "2026-05-21T23:11:05.497Z",
642
642
  "cwe_refs": [
643
643
  "CWE-1188"
644
644
  ]
@@ -673,7 +673,7 @@
673
673
  "framework_gaps": [],
674
674
  "last_threat_review": "2026-05-11",
675
675
  "signature": "WAu5fRirzSOcnnZsTx2d/JJZwa/LPpXCi+31qATTGLmoNuhyy81k3ooPe9kCM3E0CLMtvTePg9DagYqBninZDQ==",
676
- "signed_at": "2026-05-21T21:27:15.945Z"
676
+ "signed_at": "2026-05-21T23:11:05.498Z"
677
677
  },
678
678
  {
679
679
  "name": "attack-surface-pentest",
@@ -744,7 +744,7 @@
744
744
  "PTES revision incorporating AI-surface enumeration"
745
745
  ],
746
746
  "signature": "7eEwCXFd9pDKUw7yCUbRJSjfzozE44dwwwemCQUPm8JBPztLltibD9bL/RszSbYyCrYJmVb5Drncz2cGe62gCw==",
747
- "signed_at": "2026-05-21T21:27:15.945Z"
747
+ "signed_at": "2026-05-21T23:11:05.498Z"
748
748
  },
749
749
  {
750
750
  "name": "fuzz-testing-strategy",
@@ -804,7 +804,7 @@
804
804
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
805
805
  ],
806
806
  "signature": "Z7ypCUnXx8JpLtgxxB6RHNi39w74AmrGY1N4ofAGCXhkuM2EaFVm1AU0dvl9UQ1bVLfHKEDGqMO/TwlIY7RABg==",
807
- "signed_at": "2026-05-21T21:27:15.945Z"
807
+ "signed_at": "2026-05-21T23:11:05.498Z"
808
808
  },
809
809
  {
810
810
  "name": "dlp-gap-analysis",
@@ -879,7 +879,7 @@
879
879
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
880
880
  ],
881
881
  "signature": "fgxG344JGYBWWWwFXZ1IzGipWKP7EyBhrsvsbsb0CCGXfv/MvNHVNI6G0zQddCsWX1JeQbhZT3Vk8v1uJKDTDA==",
882
- "signed_at": "2026-05-21T21:27:15.946Z"
882
+ "signed_at": "2026-05-21T23:11:05.499Z"
883
883
  },
884
884
  {
885
885
  "name": "supply-chain-integrity",
@@ -956,7 +956,7 @@
956
956
  "OpenSSF model-signing — emerging Sigstore-based signing standard for ML model weights; track for production adoption"
957
957
  ],
958
958
  "signature": "pcLrM98A3vUSZRjwNAk0aZ9umvOwB41XCLLsCOy/IebB2F/06oIrGUKkMHtHwm4pTVPShMMcKdZQQ3jz30FnCg==",
959
- "signed_at": "2026-05-21T21:27:15.946Z"
959
+ "signed_at": "2026-05-21T23:11:05.499Z"
960
960
  },
961
961
  {
962
962
  "name": "defensive-countermeasure-mapping",
@@ -1013,7 +1013,7 @@
1013
1013
  ],
1014
1014
  "last_threat_review": "2026-05-11",
1015
1015
  "signature": "gqF8eU3VBrZhO2WnlcqKa7wm1d2mmWtvpbmx0kNCgHojNV+qEt+Ij84RO6bZvaUqhfYPWizWL79Fa4DL0curAQ==",
1016
- "signed_at": "2026-05-21T21:27:15.946Z"
1016
+ "signed_at": "2026-05-21T23:11:05.499Z"
1017
1017
  },
1018
1018
  {
1019
1019
  "name": "identity-assurance",
@@ -1080,7 +1080,7 @@
1080
1080
  "d3fend_refs": [],
1081
1081
  "last_threat_review": "2026-05-11",
1082
1082
  "signature": "Wv5hGMeHjlaQK1zwicVCA7AvdKgJBgvcjdpGM9Ywahh9tagAKhbkOjybowDQZzu7OZ3bDkbh6pBYc1Sdwr6NAA==",
1083
- "signed_at": "2026-05-21T21:27:15.947Z"
1083
+ "signed_at": "2026-05-21T23:11:05.500Z"
1084
1084
  },
1085
1085
  {
1086
1086
  "name": "ot-ics-security",
@@ -1136,7 +1136,7 @@
1136
1136
  "d3fend_refs": [],
1137
1137
  "last_threat_review": "2026-05-11",
1138
1138
  "signature": "8t5qKHd3yWi57dvG36YQkLN/X9bQWqtEiYjay4IfSmqhJpM/xXPaQVKNGz3wscrO8OLKUZ0OaX7Mj5kzpgBKBQ==",
1139
- "signed_at": "2026-05-21T21:27:15.947Z"
1139
+ "signed_at": "2026-05-21T23:11:05.500Z"
1140
1140
  },
1141
1141
  {
1142
1142
  "name": "coordinated-vuln-disclosure",
@@ -1188,7 +1188,7 @@
1188
1188
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1189
1189
  ],
1190
1190
  "signature": "GDGt4UPqBa04PjlpSmpyihGzd3OgfBN7jaAK5tfwp+LRSs3ygKOdbeivUCCHNagTY1hE6hG2Ou40ADfBFuXeAg==",
1191
- "signed_at": "2026-05-21T21:27:15.947Z"
1191
+ "signed_at": "2026-05-21T23:11:05.500Z"
1192
1192
  },
1193
1193
  {
1194
1194
  "name": "threat-modeling-methodology",
@@ -1238,7 +1238,7 @@
1238
1238
  "PASTA v2 updates incorporating AI/ML application threats"
1239
1239
  ],
1240
1240
  "signature": "rFBpOQEJUPpl+v88Lw/WqVJRhTl80vy0VbPAbzQj3Q0suJRRrJg368I9uKu5LXIBKFDvKxnGIcIzbGg9NUtaCA==",
1241
- "signed_at": "2026-05-21T21:27:15.948Z"
1241
+ "signed_at": "2026-05-21T23:11:05.501Z"
1242
1242
  },
1243
1243
  {
1244
1244
  "name": "webapp-security",
@@ -1312,7 +1312,7 @@
1312
1312
  "d3fend_refs": [],
1313
1313
  "last_threat_review": "2026-05-11",
1314
1314
  "signature": "ux85YI4t2mVHOyt744Yin1HHy+z11JIFygjKfFfQOBBl5QVV3A267jeIy7utix85irMcpZm/T3yx/ooqiK2tBA==",
1315
- "signed_at": "2026-05-21T21:27:15.948Z"
1315
+ "signed_at": "2026-05-21T23:11:05.501Z"
1316
1316
  },
1317
1317
  {
1318
1318
  "name": "ai-risk-management",
@@ -1362,7 +1362,7 @@
1362
1362
  "d3fend_refs": [],
1363
1363
  "last_threat_review": "2026-05-11",
1364
1364
  "signature": "IIXnkZ5ZNqFwOto5KfytADTLLZLoyXNZACD1ORZ40P1HUAQxe6u2uyXFzzsfuob4Uy06jNkRGr2FFgCphUH1Cw==",
1365
- "signed_at": "2026-05-21T21:27:15.948Z"
1365
+ "signed_at": "2026-05-21T23:11:05.501Z"
1366
1366
  },
1367
1367
  {
1368
1368
  "name": "sector-healthcare",
@@ -1422,7 +1422,7 @@
1422
1422
  "d3fend_refs": [],
1423
1423
  "last_threat_review": "2026-05-11",
1424
1424
  "signature": "AhF9KF8ZBlDteciV+F8IBSmFVYCvQOn44GmD4rZjgLoPxfIv/QE1/vSkK32zyqDKtHWkLSXExbkkPkxA/V6dDw==",
1425
- "signed_at": "2026-05-21T21:27:15.949Z"
1425
+ "signed_at": "2026-05-21T23:11:05.502Z"
1426
1426
  },
1427
1427
  {
1428
1428
  "name": "sector-financial",
@@ -1503,7 +1503,7 @@
1503
1503
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1504
1504
  ],
1505
1505
  "signature": "HQgZvb4ReziEz5rNFr8i/O8/rJEZR+iHRROT7m/D2QUqhrcNISPkYXENsUZlG8xapzy/Ik92ehkseyj4hdmhCQ==",
1506
- "signed_at": "2026-05-21T21:27:15.949Z"
1506
+ "signed_at": "2026-05-21T23:11:05.502Z"
1507
1507
  },
1508
1508
  {
1509
1509
  "name": "sector-federal-government",
@@ -1572,7 +1572,7 @@
1572
1572
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1573
1573
  ],
1574
1574
  "signature": "linxmsXZiOYtcs71sSWgGCrvb8xQfmxmtTY5PRvZJ0/8FgJulo0tQtejzexYG775s7XhjAmGsDP238BQTQ8ADA==",
1575
- "signed_at": "2026-05-21T21:27:15.950Z"
1575
+ "signed_at": "2026-05-21T23:11:05.503Z"
1576
1576
  },
1577
1577
  {
1578
1578
  "name": "sector-energy",
@@ -1637,7 +1637,7 @@
1637
1637
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1638
1638
  ],
1639
1639
  "signature": "JjBfc0ovta560Clk0x3QGRM5osFJDwcvpy3rT7QEGdCIL827jzE8QCow1C8deXq+4JhY2sA/d7/8IsxikdlkCg==",
1640
- "signed_at": "2026-05-21T21:27:15.950Z"
1640
+ "signed_at": "2026-05-21T23:11:05.503Z"
1641
1641
  },
1642
1642
  {
1643
1643
  "name": "sector-telecom",
@@ -1723,7 +1723,7 @@
1723
1723
  "O-RAN SFG / WG11 security specifications"
1724
1724
  ],
1725
1725
  "signature": "JWVxKFoKrbX4d+Tko1d4OBdwyg25MfFFKn4CT6E/CzH+YwnU3T6Y76uBQIKg3+gIGTvPduqyvQwQQ5FxKDuPBw==",
1726
- "signed_at": "2026-05-21T21:27:15.950Z"
1726
+ "signed_at": "2026-05-21T23:11:05.503Z"
1727
1727
  },
1728
1728
  {
1729
1729
  "name": "api-security",
@@ -1792,7 +1792,7 @@
1792
1792
  "d3fend_refs": [],
1793
1793
  "last_threat_review": "2026-05-11",
1794
1794
  "signature": "BmCRCestWqr55+fCynEhtAl5NWLT+xLTkpwS0Icp3SaoZOw/ce3Y6TtqjHRSKn4CBJq7YDiLRWxmhO3MStvOAA==",
1795
- "signed_at": "2026-05-21T21:27:15.951Z"
1795
+ "signed_at": "2026-05-21T23:11:05.503Z"
1796
1796
  },
1797
1797
  {
1798
1798
  "name": "cloud-security",
@@ -1873,7 +1873,7 @@
1873
1873
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1874
1874
  ],
1875
1875
  "signature": "/DV3pmZwrRySrk1OCbyI+0BQESacjupJfUX3eC2NGtXuYOBro0vndIP+z27heFxumnjU3a9sfla7/U9X+pqnDw==",
1876
- "signed_at": "2026-05-21T21:27:15.951Z"
1876
+ "signed_at": "2026-05-21T23:11:05.504Z"
1877
1877
  },
1878
1878
  {
1879
1879
  "name": "container-runtime-security",
@@ -1935,7 +1935,7 @@
1935
1935
  "d3fend_refs": [],
1936
1936
  "last_threat_review": "2026-05-11",
1937
1937
  "signature": "E2UGSf9ATyYgzBr8uM/0ubOUmDqo1jVA7f9mVxv6LHfWGCNuQNXDyuNou9VAmUCeeXEeUYIi3AFjXkJqpOkxDA==",
1938
- "signed_at": "2026-05-21T21:27:15.951Z"
1938
+ "signed_at": "2026-05-21T23:11:05.504Z"
1939
1939
  },
1940
1940
  {
1941
1941
  "name": "mlops-security",
@@ -2006,7 +2006,7 @@
2006
2006
  "MITRE ATLAS v5.6.0 (released February 2026) shipped the AML.T0010 sub-technique expansion this forecast tracked plus new techniques (\"Publish Poisoned AI Agent Tool\", \"Escape to Host\"); inventory now 16 tactics, 84 techniques, 56 sub-techniques. Forward watch: ATLAS v5.5 / v6.0 — track next-cadence updates to agentic-AI TTPs and MLOps-pipeline-specific techniques"
2007
2007
  ],
2008
2008
  "signature": "BGNE6ZQWBA1LmsUFe8tU0L67iGDSrFqiuqaZD2f1KqfcyqqzQfMs9PWNHFzxxaJmXeKlm87eU8lgELF0bX+RBA==",
2009
- "signed_at": "2026-05-21T21:27:15.952Z"
2009
+ "signed_at": "2026-05-21T23:11:05.504Z"
2010
2010
  },
2011
2011
  {
2012
2012
  "name": "incident-response-playbook",
@@ -2068,7 +2068,7 @@
2068
2068
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
2069
2069
  ],
2070
2070
  "signature": "FkZQerh3VHVJAwIcCktDyMRh5KE2+Em/i0ek8zEz7JG/PXtQx8ujHWTh3VjZbOLhPNtdB2qxgXOIAYIofaVOAQ==",
2071
- "signed_at": "2026-05-21T21:27:15.952Z"
2071
+ "signed_at": "2026-05-21T23:11:05.505Z"
2072
2072
  },
2073
2073
  {
2074
2074
  "name": "ransomware-response",
@@ -2148,7 +2148,7 @@
2148
2148
  ],
2149
2149
  "last_threat_review": "2026-05-15",
2150
2150
  "signature": "n3UToNuN3A1HgLvcuqmIx8vrZY71+r/79waK92jG+rSX4uYOzkmxMUpROrE5K9bDwMezNBHdjWv8Uul6zugyDQ==",
2151
- "signed_at": "2026-05-21T21:27:15.953Z"
2151
+ "signed_at": "2026-05-21T23:11:05.505Z"
2152
2152
  },
2153
2153
  {
2154
2154
  "name": "email-security-anti-phishing",
@@ -2201,7 +2201,7 @@
2201
2201
  "d3fend_refs": [],
2202
2202
  "last_threat_review": "2026-05-11",
2203
2203
  "signature": "rK+WnuS+9tqEABmwc0jO/PEmxcLjG1/tmUb897HsClQeKzf+TQOlwBE+OsbtuKxpjYNwur62Xxs3TxObkwm8Cw==",
2204
- "signed_at": "2026-05-21T21:27:15.953Z"
2204
+ "signed_at": "2026-05-21T23:11:05.505Z"
2205
2205
  },
2206
2206
  {
2207
2207
  "name": "age-gates-child-safety",
@@ -2269,7 +2269,7 @@
2269
2269
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2270
2270
  ],
2271
2271
  "signature": "+OO0RhQ303RJV7kaH38IuZpLeQbapep6Ds4Re/WEZu0FHBwKSlwvF7jbtP7KQ57xldJYn/xZm2jaszyOacMfDg==",
2272
- "signed_at": "2026-05-21T21:27:15.953Z"
2272
+ "signed_at": "2026-05-21T23:11:05.506Z"
2273
2273
  },
2274
2274
  {
2275
2275
  "name": "cloud-iam-incident",
@@ -2349,7 +2349,7 @@
2349
2349
  ],
2350
2350
  "last_threat_review": "2026-05-15",
2351
2351
  "signature": "e/kij7GtKaytROyIj7V5RH+FC9WtmVFzrmG2kIlNDNn29ep/CRNlIQKwXLpzo/81AIf634pmdr1qy/+vwIuUDA==",
2352
- "signed_at": "2026-05-21T21:27:15.954Z"
2352
+ "signed_at": "2026-05-21T23:11:05.506Z"
2353
2353
  },
2354
2354
  {
2355
2355
  "name": "idp-incident-response",
@@ -2430,11 +2430,11 @@
2430
2430
  ],
2431
2431
  "last_threat_review": "2026-05-15",
2432
2432
  "signature": "ew9Kglc9fAZzbn0ZIfGP7WSK/j4eV2VhSvpy+s5bEfNEVYIMa2kZjnGBapgUsyGDLes9H9K2ovjQyX17+GKiBw==",
2433
- "signed_at": "2026-05-21T21:27:15.954Z"
2433
+ "signed_at": "2026-05-21T23:11:05.507Z"
2434
2434
  }
2435
2435
  ],
2436
2436
  "manifest_signature": {
2437
2437
  "algorithm": "Ed25519",
2438
- "signature_base64": "8vEIMaf7hdCLMYxi4u0T4fyLB98qLcEXuuvqCp3DkSkNpQadJnTW/Ltb34oFKnS0QIsWHEGiWzZqZMpc5o3tBQ=="
2438
+ "signature_base64": "AdXaVihJbe3X7lti1RIiG9eN9JsaM91s2Roe1ReUvJDuaNC30GvX9jdtMTT2rsR+KLHd5Jb3uWsRgB23ykYEAA=="
2439
2439
  }
2440
2440
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.13.51",
3
+ "version": "0.13.53",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs (312 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 7476 RFCs), 34 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:7b988e1d-2c04-4011-b873-b729271dd830",
4
+ "serialNumber": "urn:uuid:65dee8f3-fd04-4d6b-8d77-673f2df0c55f",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2091-09-16T22:43:41.000Z",
7
+ "timestamp": "2080-02-28T08:04:03.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.13.51"
12
+ "version": "0.13.53"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.51",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.13.53",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.13.51",
19
+ "version": "0.13.53",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 10 catalogs (312 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 7476 RFCs), 34 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.51",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.13.53",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "a5eab2f190d93f11fd0fa5713f7fdaad2798742857a0e08e17d0113efb5a538d"
32
+ "content": "cf2301346da5c3f7a089b5cb53e2f333865382ad9f00f5d09cff65e01d9d19a1"
33
33
  }
34
34
  ],
35
35
  "externalReferences": [
36
36
  {
37
37
  "type": "distribution",
38
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.51"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.13.53"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "9990b9daf55ba5b8eb1fb583e7af94a999e0c7acabaa979981448eaa972c421f"
119
+ "content": "ca93a16f6fd19f0f6112334b9df390d0e1b20052e4b224faf52ac795b92d419a"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "6664a26adf0d7a9da2b5345603b18150f3acb7b8660b40c580701c68cc9ad24ea0b672213ed78a03aabefeacf97abecd6be34613c8b82bf13591c0bf88ce5ebb"
123
+ "content": "8a3de54a276b1a69337ffbd26f9a83be0fa8d57fe34cf4636902c358f71303b9cc196194c73144756dabb381380df2c3d15c6066d11bc6d3ed54e8015e0f23a8"
124
124
  }
125
125
  ]
126
126
  },
@@ -176,11 +176,11 @@
176
176
  "hashes": [
177
177
  {
178
178
  "alg": "SHA-256",
179
- "content": "31af6cab873ca74dab6f9f05b32054b2926911575f5ee88554474798f3aa0792"
179
+ "content": "dec21940a1c2ceb4f43ce70a0a150b9b9e00581d43c9fa23d2a03f5f4d018f7d"
180
180
  },
181
181
  {
182
182
  "alg": "SHA3-512",
183
- "content": "6510ae240e80bcc21dd75ae4a1202ad88e9a8e29fd45b5b6d1d240a5f552689eb968c13399a6b3e77600ce4776f48f63876b787bd99703d9afe44e73c91567c4"
183
+ "content": "0bd13c9424ededac057a1ee4bc2606e80d46eeeffea3ca3c1971dbac555b03dcf70bc0d44df914028a4abbd3a5ffafc0a3939d7a6cd6950cee40a56a5c99695b"
184
184
  }
185
185
  ]
186
186
  },
@@ -281,11 +281,11 @@
281
281
  "hashes": [
282
282
  {
283
283
  "alg": "SHA-256",
284
- "content": "ad2a1e3dc6b41ecf201be7c560773b6566850a2f7a122a460cce60ea8da8f8eb"
284
+ "content": "c92c84e424929844ec9adbead08eb5e247cb6138a89860dc36b66380587e378e"
285
285
  },
286
286
  {
287
287
  "alg": "SHA3-512",
288
- "content": "62df00811005e77315cd850ce8486bff63b57964279d4fa11696f354abb45c668d8258e0073278ef439dc05f5930558002f283f79137c52323b7837d9fdd2715"
288
+ "content": "030e671e1e8663bc1e64e0f0d350039bec4ec0f0322b80ee38b17c5f354c493442d2a10ea1874dcf773987a917f9ecf2020d1422399f466800d3d15244713521"
289
289
  }
290
290
  ]
291
291
  },
@@ -911,11 +911,11 @@
911
911
  "hashes": [
912
912
  {
913
913
  "alg": "SHA-256",
914
- "content": "2a7cea0a16f1b6b3c6594d4b6f4ed4fafdb1dcbd1c9a24f66030fe066b7c2515"
914
+ "content": "a274c03349d64dd08d9e8b5451cbe03e28ca6ab83f52b1bab7ab131f889dd4d2"
915
915
  },
916
916
  {
917
917
  "alg": "SHA3-512",
918
- "content": "e61f1d2a6b25c7997ab2fd9a20b8b32a1840e27ae42e5e0b1a5ac3aa51c2ac06fa3c643b610911958c165bfea7e5d8d30d95930167ddc3c8305f2904f8b69041"
918
+ "content": "b92163edbb677305ed8f870deb4e69b8ec9a7f51caff692355ec612ba497e0bb3a739b5cd1d0663f02302fd9625306750dd8fe22fd999c44775571989c496af8"
919
919
  }
920
920
  ]
921
921
  },
@@ -1661,11 +1661,11 @@
1661
1661
  "hashes": [
1662
1662
  {
1663
1663
  "alg": "SHA-256",
1664
- "content": "8b7d17485a799593f74506f92ad8f129155ac52d40f508e90b4adf15cb11916d"
1664
+ "content": "9eb25cdba45631f79eaf5ee628532b284912a2318d85c7a97d2acbaf2db31e08"
1665
1665
  },
1666
1666
  {
1667
1667
  "alg": "SHA3-512",
1668
- "content": "44a2515c3e8ac6dfc8e23b906c5be4dbd79d598932f2454b82a61202911994d52a2956787ed0abce086092a78f2a1d20df801ceedf6aea5ca749839c0b8873c6"
1668
+ "content": "4cf5527f79a6d936b5edc31e3abfa40d53840fe2aa8aa14142d76d2cee5326d95fd2eab8a7dfd1206eb9a12a1ad1a17a886430182b40467617956ab52c38ad49"
1669
1669
  }
1670
1670
  ]
1671
1671
  },
@@ -2059,6 +2059,21 @@
2059
2059
  }
2060
2060
  ]
2061
2061
  },
2062
+ {
2063
+ "bom-ref": "file:scripts/check-agents-md-collectors.js",
2064
+ "type": "file",
2065
+ "name": "scripts/check-agents-md-collectors.js",
2066
+ "hashes": [
2067
+ {
2068
+ "alg": "SHA-256",
2069
+ "content": "a79fcefca5c48f5fb21b8b397861e317ef77a615cf00a867619ba5ed52bb3604"
2070
+ },
2071
+ {
2072
+ "alg": "SHA3-512",
2073
+ "content": "54a59929bebe5fc10e11e7cb8d89b08a2ea119a0cbeb228f8a1eacfdb771698582ae9243b87cf6163b6e119930fcc54b63c2aa3c858c805934148363c925bfe2"
2074
+ }
2075
+ ]
2076
+ },
2062
2077
  {
2063
2078
  "bom-ref": "file:scripts/check-catalog-gap-budget.js",
2064
2079
  "type": "file",
@@ -2186,11 +2201,11 @@
2186
2201
  "hashes": [
2187
2202
  {
2188
2203
  "alg": "SHA-256",
2189
- "content": "b1edfc22514b633f9932382d5d0fa091fa28b83535b4f0dac764ea2f4aebc85d"
2204
+ "content": "81967862730d2fcfd6b42442031d584cf9718939305d40f7b5ae2ab590f7c85c"
2190
2205
  },
2191
2206
  {
2192
2207
  "alg": "SHA3-512",
2193
- "content": "f2b2d68424127b87e0f6a5a7cd2e64e4910d26af9ef4279f0aac49c81fdd9a47617013cde0165066a7678db1bee3a3a4c75882228befb94cad5219125dbb8fd9"
2208
+ "content": "f8d0179cf645f05b3838ebf8ac024689a21bdd7b5a22a824ac47db866ce1246d3fb256dcf71eaa2c8e177dccd9447478dc655edc01f043f7532ac403c6b181fa"
2194
2209
  }
2195
2210
  ]
2196
2211
  },
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ /**
5
+ * scripts/check-agents-md-collectors.js
6
+ *
7
+ * Predeploy gate. Verifies that AGENTS.md's "<N> reference collectors
8
+ * ship today" paragraph stays in sync with the actual contents of
9
+ * lib/collectors/. Drift is silent today - AGENTS.md gets bumped by
10
+ * hand each release; a missed bump produces inaccurate count + stale
11
+ * enumeration that downstream AI consumers parse.
12
+ *
13
+ * Checks:
14
+ * 1. The numeric count word in the paragraph (Eleven / Twelve /
15
+ * Thirteen / ...) matches the actual count of
16
+ * lib/collectors/*.js modules.
17
+ * 2. Every collector named in the parenthesized list exists at
18
+ * lib/collectors/<name>.js.
19
+ * 3. Every lib/collectors/<name>.js module appears in the
20
+ * parenthesized list.
21
+ *
22
+ * Exit codes: 0 ok, 1 drift, 2 parse error.
23
+ */
24
+
25
+ const fs = require("node:fs");
26
+ const path = require("node:path");
27
+
28
+ const ROOT = path.join(__dirname, "..");
29
+ const AGENTS = path.join(ROOT, "AGENTS.md");
30
+ const COLLECTOR_DIR = path.join(ROOT, "lib", "collectors");
31
+
32
+ const WORD_TO_NUMBER = {
33
+ one: 1, two: 2, three: 3, four: 4, five: 5, six: 6, seven: 7,
34
+ eight: 8, nine: 9, ten: 10, eleven: 11, twelve: 12, thirteen: 13,
35
+ fourteen: 14, fifteen: 15, sixteen: 16, seventeen: 17, eighteen: 18,
36
+ nineteen: 19, twenty: 20,
37
+ };
38
+
39
+ function fail(msg) {
40
+ console.error(`[check-agents-md-collectors] FAIL - ${msg}`);
41
+ process.exitCode = 1;
42
+ }
43
+
44
+ function ok(msg) {
45
+ console.log(`[check-agents-md-collectors] ok - ${msg}`);
46
+ }
47
+
48
+ function main() {
49
+ let agents;
50
+ try { agents = fs.readFileSync(AGENTS, "utf8"); }
51
+ catch (e) {
52
+ console.error(`[check-agents-md-collectors] cannot read AGENTS.md: ${e.message}`);
53
+ process.exitCode = 2;
54
+ return;
55
+ }
56
+
57
+ let collectorFiles;
58
+ try {
59
+ collectorFiles = fs.readdirSync(COLLECTOR_DIR)
60
+ .filter(f => f.endsWith(".js"))
61
+ .map(f => `lib/collectors/${f}`)
62
+ .sort();
63
+ } catch (e) {
64
+ console.error(`[check-agents-md-collectors] cannot read ${COLLECTOR_DIR}: ${e.message}`);
65
+ process.exitCode = 2;
66
+ return;
67
+ }
68
+ const onDiskCount = collectorFiles.length;
69
+
70
+ const para = agents.match(/(\b[A-Z][a-z]+)\s+reference collectors ship today\s*\(([^)]+)\)/);
71
+ if (!para) {
72
+ fail("could not locate the 'N reference collectors ship today (...)' paragraph in AGENTS.md");
73
+ return;
74
+ }
75
+ const word = para[1].toLowerCase();
76
+ const listed = para[2];
77
+ const claimedCount = WORD_TO_NUMBER[word];
78
+ if (!claimedCount) {
79
+ fail(`unrecognized count word '${para[1]}' - extend WORD_TO_NUMBER in scripts/check-agents-md-collectors.js`);
80
+ return;
81
+ }
82
+ if (claimedCount !== onDiskCount) {
83
+ fail(`claimed count ${para[1]} (${claimedCount}) != on-disk count ${onDiskCount}`);
84
+ return;
85
+ }
86
+
87
+ const claimedPaths = [];
88
+ const pathRe = /`lib\/collectors\/([a-z0-9-]+\.js)`/g;
89
+ let m;
90
+ while ((m = pathRe.exec(listed)) !== null) {
91
+ claimedPaths.push(`lib/collectors/${m[1]}`);
92
+ }
93
+ claimedPaths.sort();
94
+
95
+ const onDiskSet = new Set(collectorFiles);
96
+ const claimedSet = new Set(claimedPaths);
97
+
98
+ const missingFromAgents = collectorFiles.filter(f => !claimedSet.has(f));
99
+ const extraInAgents = claimedPaths.filter(f => !onDiskSet.has(f));
100
+
101
+ if (missingFromAgents.length > 0) {
102
+ fail(`on-disk but not in AGENTS.md list: ${missingFromAgents.join(", ")}`);
103
+ }
104
+ if (extraInAgents.length > 0) {
105
+ fail(`in AGENTS.md list but not on disk: ${extraInAgents.join(", ")}`);
106
+ }
107
+
108
+ if (process.exitCode !== 1) {
109
+ ok(`${onDiskCount}/${onDiskCount} collectors enumerated correctly in AGENTS.md`);
110
+ }
111
+ }
112
+
113
+ main();
@@ -221,6 +221,17 @@ const GATES = [
221
221
  args: [path.join(ROOT, "scripts", "check-version-tags.js")],
222
222
  ciJobName: "Data integrity (catalog + manifest snapshot)",
223
223
  },
224
+ {
225
+ // AGENTS.md collector enumeration drift gate. Catches the case
226
+ // where lib/collectors/ gets a new module but AGENTS.md's
227
+ // "<N> reference collectors ship today (...)" paragraph isn't
228
+ // bumped (or vice versa). The paragraph is the canonical source
229
+ // for AI-agent consumers; drift produces stale enumeration.
230
+ name: "AGENTS.md collector enumeration drift",
231
+ command: process.execPath,
232
+ args: [path.join(ROOT, "scripts", "check-agents-md-collectors.js")],
233
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
234
+ },
224
235
  ];
225
236
 
226
237
  function runGate(gate) {