@blamejs/exceptd-skills 0.13.4 → 0.13.5

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,35 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.5 — 2026-05-18
4
+
5
+ Three new playbooks, two cross-cutting CLI behaviours, and a deterministic schema gate on `active_exploitation` vocabulary.
6
+
7
+ ### Features
8
+
9
+ **Three new playbooks bring the canonical set to 23.**
10
+
11
+ - **`post-quantum-migration`** — the operational migration programme (distinct from `crypto`, which is handshake-level). Covers per-asset cryptographic register, vendor-SLA tracking, regulator-deadline orchestration (CNSA 2.0, OMB M-23-02, NIS2 Art.21(2)(h), DORA Art.9, EU CRA Annex I, BSI TR-02102, ACSC ISM-1546), and HNDL exposure-window analysis. Nine indicators including `no-cryptographic-asset-register`, `hsm-firmware-no-pqc` (Thales/Entrust/CloudHSM migration blocker), `long-retention-classical-only-asset`, and `embedded-tls-stack-classical-only`. 13 framework-gap mappings (NIST SC-12/SC-13, ISO A.8.24/A.8.25, PCI 3.6/4.2.1, NIS2/DORA/EU CRA, UK CAF, AU ISM, MAS TRM, JP NISC, HIPAA). Feeds into `crypto` + `framework` + `sbom`.
12
+
13
+ - **`ai-discovered-cve-triage`** — operator-side response to AI-discovered CVE arrival. Anchors on CVE-2026-31431 (Copy Fail, Theori+Xint), CVE-2026-46300 (Fragnesia, Zellic AI-agentic), CVE-2026-42945 (NGINX Rift, depthfirst — first publicly-attributed AI-discovered nginx CVE), the GTIG 41% AI-zero-day statistic, and Hard Rule #7. Seven indicators including `ai-discovery-attribution-band-c-unverified` (don't apply +15 ai_factor on unverified claims), `ai-discovery-feed-coverage-incomplete` (operator pipeline misses Theori/depthfirst/Zellic/GTIG/Project Zero AI sources), and `asset-unpatched-past-rwep-sla` (RWEP-derived SLA: 4h ≥ 90, 24h 75–89, 72h 60–74). Feeds into `framework` + `kernel` + `sbom` + `runtime`.
14
+
15
+ - **`supply-chain-recovery`** — post-compromise recovery workflow (distinct from `sbom`, which is pre-incident hygiene). Anchors on Shai-Hulud (Sep 2025 / Nov 2025 / May 2026 waves), MAL-2026-SHAI-HULUD-OSS (TeamPCP open-sourced 2026-05-12), MAL-2026-TANSTACK-MINI (42 `@tanstack/*` packages), MAL-2026-NODE-IPC-STEALER, and CVE-2026-45321. Encodes NEW-CTRL-050 (exhaustive maintainer-credential rotation), NEW-CTRL-051 (install-window audit), NEW-CTRL-052 (AI-assistant config exfil as first-class — `~/.cursor`, `~/.codeium`, `~/.claude`). Eight indicators including `ai-assistant-config-mutated` (Shai-Hulud startup-hook persistence), `outbound-exfil-during-window`, `operator-published-package-republish` (downstream notification mandatory), `long-lived-token-in-compromised-ci-log`. Feeds into `cred-stores` + `idp-incident-response` + `sbom` + `mcp` + `framework`.
16
+
17
+ **`exceptd watchlist --org-scan --output-format markdown`.** Adds GitHub-flavored markdown table output for PR / issue / advisory body consumption. Accepted values: `json` | `markdown` | `human` (default). The legacy `--json` shorthand remains accepted (equivalent to `--output-format json`). Invalid values exit non-zero with the accepted-set in the error envelope.
18
+
19
+ **`exceptd doctor --ai-config --fix` now repairs Windows ACLs.** The POSIX path applied `chmod 0600`; on Windows the audit reported the gap as "manual review." The Windows path now invokes `icacls /inheritance:r /grant:r` to restrict to the current user + SYSTEM + Administrators. The audit check itself (without `--fix`) parses `icacls <path>` and reports any extra principals.
20
+
21
+ **Skill chain: MCP findings inside a CI runner escalate to `cicd-pipeline-compromise`.** New deterministic indicator `mcp-server-invoked-from-ci-pipeline` keys on `GITHUB_ACTIONS` / `GITLAB_CI` / `BUILDKITE` / `JENKINS_URL` / `CIRCLECI` / `RUNNER_OS` env vars and known runner workdirs (`/_work/`, `/builds/`, `/var/jenkins_home/workspace/`, `/var/lib/buildkite-agent/builds/`). When paired with any other high-confidence MCP signal, the finding feeds into `cicd-pipeline-compromise` for OIDC / signing-key / publish-channel scope handling. Without this arc, MCP findings in CI received local-dev close-out only, under-counting publish-channel blast radius.
22
+
23
+ ### Bugs
24
+
25
+ **`cve-catalog.schema.json` `active_exploitation` enum now matches `_meta.active_exploitation_vocabulary`.** The schema enumerated four values (`confirmed` / `suspected` / `none` / `unknown`); the meta vocabulary listed five (adding `theoretical`). Catalog entries written against the meta vocabulary that used `theoretical` were silently rejected at validation time. Schema now lists five values; `validate-cve-catalog.js` adds a cross-check that fails the gate if the two surfaces ever drift again.
26
+
27
+ ### Internal
28
+
29
+ - Test count baseline updated for the +3 playbook delta and the new `--output-format` test cases.
30
+ - `validate-cve-catalog.js` schema-vs-meta enum cross-check is a hard predeploy gate.
31
+ - All 23 playbooks pass `validate-playbooks` and `lint-skills` warning-free.
32
+
3
33
  ## 0.13.4 — 2026-05-18
4
34
 
5
35
  Warning-cleanup pass + catalog hygiene + docs surfacing. The post-v0.13.3 state had ~43 skill lint warnings and 20 cosmetic playbook warnings that operators saw on every predeploy run; this release drives both to zero. README and AGENTS catch up with the v0.13.0 → v0.13.3 operator surface.
package/bin/exceptd.js CHANGED
@@ -251,6 +251,63 @@ const RENAMED_VERBS_HINT = {
251
251
  "build-indexes": "refresh --indexes-only",
252
252
  };
253
253
 
254
+ /**
255
+ * v0.13.5: Windows ACL audit helper for `doctor --ai-config`. Replaces
256
+ * the v0.13.3 "manual review" placeholder with a real check.
257
+ *
258
+ * Runs `icacls <path>` and parses the output for any principal beyond
259
+ * the current user. Anything other than the running USERNAME, NT
260
+ * AUTHORITY\SYSTEM, and BUILTIN\Administrators on the ACL counts as
261
+ * "broader than user-only" — typical offenders are inherited entries
262
+ * for BUILTIN\Users, Authenticated Users, or Everyone.
263
+ *
264
+ * Returns { ok: boolean, extraPrincipals: string[], error?: string }.
265
+ * On non-Windows hosts (defensive — only invoked from the win32 branch
266
+ * in cmdDoctor anyway), returns { ok: true, extraPrincipals: [] }.
267
+ */
268
+ function checkWindowsAcl(targetPath) {
269
+ if (process.platform !== 'win32') return { ok: true, extraPrincipals: [] };
270
+ const childProc = require('child_process');
271
+ const user = (process.env.USERNAME || '').toLowerCase();
272
+ // Principals that are EXPECTED on every Windows ACL and don't count
273
+ // as "broader than user-only" — admins legitimately need access for
274
+ // system maintenance; SYSTEM is required for backup/restore.
275
+ const ALLOWED_PRINCIPAL_SUFFIXES = [
276
+ `\\${user}`,
277
+ 'nt authority\\system',
278
+ 'builtin\\administrators',
279
+ 'administrators',
280
+ ];
281
+ let stdout;
282
+ try {
283
+ stdout = childProc.execFileSync('icacls', [targetPath], {
284
+ encoding: 'utf8',
285
+ stdio: ['ignore', 'pipe', 'pipe'],
286
+ timeout: 5000,
287
+ });
288
+ } catch (e) {
289
+ return { ok: false, extraPrincipals: [], error: (e && e.message) || String(e) };
290
+ }
291
+ const extraPrincipals = [];
292
+ // icacls output format: each principal on its own line, prefixed by
293
+ // whitespace and ending with permission bits in parens. Lines for
294
+ // the file itself (target path) and the "Successfully processed"
295
+ // footer are skipped.
296
+ for (const rawLine of stdout.split(/\r?\n/)) {
297
+ const line = rawLine.trim();
298
+ if (!line) continue;
299
+ if (line.toLowerCase().startsWith('successfully processed')) continue;
300
+ // The first line is the path; principal lines start with NT AUTHORITY,
301
+ // BUILTIN, or a domain\user. Match `name:(perms)` shape.
302
+ const m = line.match(/^([^:()]+?):\(/);
303
+ if (!m) continue;
304
+ const principal = m[1].trim().toLowerCase();
305
+ const isAllowed = ALLOWED_PRINCIPAL_SUFFIXES.some((suffix) => principal.endsWith(suffix));
306
+ if (!isAllowed) extraPrincipals.push(m[1].trim());
307
+ }
308
+ return { ok: extraPrincipals.length === 0, extraPrincipals };
309
+ }
310
+
254
311
  function readPkgVersion() {
255
312
  try {
256
313
  return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf8")).version;
@@ -5510,15 +5567,23 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5510
5567
  let st;
5511
5568
  try { st = fs.statSync(childAbs); } catch { continue; }
5512
5569
  if (process.platform === 'win32') {
5513
- // Windows POSIX mode bits don't carry meaningful ACL info.
5514
- // Flag every sensitive file with a manual-review note rather
5515
- // than emit a noisy permission claim that's likely wrong.
5570
+ // v0.13.5: real ACL check via icacls. Replaces the v0.13.3
5571
+ // "manual review" info-level placeholder. Confirms only the
5572
+ // current-user SID has any read entry. Anything else (Users,
5573
+ // Everyone, Authenticated Users, BUILTIN\Users, etc.) on the
5574
+ // ACL counts as "broader than user-only" and surfaces as a
5575
+ // warn finding. The `--fix` path applies `icacls /inheritance:r
5576
+ // /grant:r <USER>:F` to strip inherited entries.
5577
+ const aclCheck = checkWindowsAcl(childAbs);
5578
+ if (aclCheck.ok) continue;
5516
5579
  findings.push({
5517
5580
  path: `${displayRoot}/${childRel}`,
5518
5581
  mode: null,
5519
- severity: 'info',
5520
- issue: 'win32_acl_check_not_implemented',
5521
- hint: 'On Windows the POSIX mode bits are not load-bearing. Use icacls to confirm only the current user has read access. Tracked for v0.14+.',
5582
+ severity: 'warn',
5583
+ issue: 'broader_than_user_only_acl',
5584
+ acl_extra_principals: aclCheck.extraPrincipals,
5585
+ hint: `icacls "${childAbs}" /inheritance:r /grant:r %USERNAME%:F # NEW-CTRL-050: AI-assistant configs holding MCP tokens / API keys must restrict ACL to the workstation user`,
5586
+ fix_command: ['icacls', childAbs, '/inheritance:r', '/grant:r', `${process.env.USERNAME}:F`],
5522
5587
  });
5523
5588
  continue;
5524
5589
  }
@@ -5530,6 +5595,8 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5530
5595
  severity: 'warn',
5531
5596
  issue: 'group_or_other_readable',
5532
5597
  hint: `chmod 600 '${childAbs}' # NEW-CTRL-050: AI-assistant configs holding MCP tokens / API keys must be 0600 to defeat unprivileged exfil`,
5598
+ fix_chmod: 0o600,
5599
+ fix_abs_path: childAbs,
5533
5600
  });
5534
5601
  }
5535
5602
  }
@@ -5543,9 +5610,49 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5543
5610
  }
5544
5611
  }
5545
5612
  const errorFindings = findings.filter((f) => f.severity === 'warn');
5613
+
5614
+ // v0.13.5: --fix path. When `doctor --ai-config --fix` is invoked
5615
+ // AND warn-severity findings exist, apply the per-finding fix
5616
+ // command (chmod 600 on POSIX; icacls /inheritance:r /grant:r on
5617
+ // Windows). The fix attempt is recorded per-finding so the report
5618
+ // surfaces which fixes landed vs which failed.
5619
+ let fixesApplied = 0;
5620
+ let fixesFailed = 0;
5621
+ if (args.fix && errorFindings.length > 0) {
5622
+ const childProc = require('child_process');
5623
+ for (const f of errorFindings) {
5624
+ if (f.fix_chmod && f.fix_abs_path) {
5625
+ try {
5626
+ fs.chmodSync(f.fix_abs_path, f.fix_chmod);
5627
+ f.fix_status = 'chmod_applied';
5628
+ fixesApplied++;
5629
+ } catch (e) {
5630
+ f.fix_status = 'chmod_failed';
5631
+ f.fix_error = e.message;
5632
+ fixesFailed++;
5633
+ }
5634
+ continue;
5635
+ }
5636
+ if (f.fix_command) {
5637
+ try {
5638
+ childProc.execFileSync(f.fix_command[0], f.fix_command.slice(1), {
5639
+ stdio: ['ignore', 'ignore', 'pipe'],
5640
+ timeout: 5000,
5641
+ });
5642
+ f.fix_status = 'icacls_applied';
5643
+ fixesApplied++;
5644
+ } catch (e) {
5645
+ f.fix_status = 'icacls_failed';
5646
+ f.fix_error = (e && e.message) || String(e);
5647
+ fixesFailed++;
5648
+ }
5649
+ }
5650
+ }
5651
+ }
5652
+
5546
5653
  checks.ai_config = {
5547
- ok: errorFindings.length === 0,
5548
- severity: errorFindings.length > 0 ? 'warn' : 'info',
5654
+ ok: errorFindings.length === 0 || (args.fix && fixesFailed === 0),
5655
+ severity: errorFindings.length > 0 && fixesFailed > 0 ? 'warn' : (errorFindings.length > 0 && !args.fix ? 'warn' : 'info'),
5549
5656
  scanned_dirs: scannedDirs,
5550
5657
  scanned_files: scannedFiles,
5551
5658
  directories_inspected: AI_CONFIG_DIRS.map((d) => d.display),
@@ -5553,8 +5660,9 @@ function cmdDoctor(runner, args, runOpts, pretty) {
5553
5660
  findings,
5554
5661
  platform: process.platform,
5555
5662
  control_reference: 'NEW-CTRL-050 (MAL-2026-SHAI-HULUD-OSS lesson)',
5663
+ ...(args.fix ? { fix_applied: fixesApplied, fix_failed: fixesFailed } : {}),
5556
5664
  };
5557
- if (errorFindings.length > 0) issues.push('ai_config');
5665
+ if (errorFindings.length > 0 && (!args.fix || fixesFailed > 0)) issues.push('ai_config');
5558
5666
  }
5559
5667
 
5560
5668
  // Walk every check and split: errors (severity error/missing/fail) vs warnings
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-18T04:13:12.063Z",
3
+ "generated_at": "2026-05-18T04:58:08.316Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "0d7cc1e5a718515519e81b973126f0fe316ad8252e4c8e04f54934ea575a9b80",
7
+ "manifest.json": "6bbf4f4d9540c2539d6f2635b39ba42f963e3a2238e9e8b6569d0cc65719b813",
8
8
  "data/atlas-ttps.json": "2b021f47355365d1ba59078dfa582397c7a64c2b4ebea4657ea260a66b76daf6",
9
9
  "data/attack-techniques.json": "76461dbec048c5e072435d57e3a04b780e3992dab9f316b1b52608e0a997e355",
10
10
  "data/cve-catalog.json": "4b8c05074744f9e099c776e0f9c3afd2b978fc52d702bc8805c3b5bfecdbafcb",