@blamejs/exceptd-skills 0.12.13 → 0.12.16
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 +217 -0
- package/bin/exceptd.js +522 -27
- package/data/_indexes/_meta.json +45 -45
- package/data/_indexes/activity-feed.json +4 -4
- package/data/_indexes/catalog-summaries.json +29 -29
- package/data/_indexes/chains.json +3238 -3210
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/jurisdiction-map.json +5 -3
- package/data/_indexes/section-offsets.json +712 -685
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +355 -340
- package/data/atlas-ttps.json +144 -129
- package/data/attack-techniques.json +319 -76
- package/data/cve-catalog.json +516 -476
- package/data/cwe-catalog.json +1081 -759
- package/data/exploit-availability.json +63 -15
- package/data/framework-control-gaps.json +867 -843
- package/data/playbooks/ai-api.json +3 -1
- package/data/playbooks/containers.json +11 -3
- package/data/playbooks/cred-stores.json +3 -1
- package/data/playbooks/crypto-codebase.json +11 -11
- package/data/playbooks/crypto.json +1 -1
- package/data/playbooks/hardening.json +3 -1
- package/data/playbooks/kernel.json +3 -1
- package/data/playbooks/library-author.json +21 -10
- package/data/playbooks/mcp.json +1 -1
- package/data/playbooks/runtime.json +3 -1
- package/data/playbooks/sbom.json +2 -2
- package/data/playbooks/secrets.json +3 -1
- package/data/rfc-references.json +276 -276
- package/keys/EXPECTED_FINGERPRINT +1 -0
- package/lib/auto-discovery.js +57 -35
- package/lib/cross-ref-api.js +39 -6
- package/lib/cve-curation.js +33 -14
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +742 -78
- package/lib/prefetch.js +30 -8
- package/lib/refresh-external.js +40 -22
- package/lib/refresh-network.js +233 -17
- package/lib/scoring.js +191 -18
- package/lib/source-ghsa.js +219 -37
- package/lib/source-osv.js +381 -122
- package/lib/validate-catalog-meta.js +64 -9
- package/lib/validate-cve-catalog.js +56 -18
- package/lib/validate-indexes.js +88 -37
- package/lib/validate-playbooks.js +46 -0
- package/lib/verify.js +72 -0
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -0
- package/manifest.json +73 -73
- package/orchestrator/dispatcher.js +21 -1
- package/orchestrator/event-bus.js +52 -8
- package/orchestrator/index.js +279 -20
- package/orchestrator/pipeline.js +63 -2
- package/orchestrator/scanner.js +32 -10
- package/orchestrator/scheduler.js +150 -17
- package/package.json +3 -1
- package/sbom.cdx.json +7 -7
- package/scripts/check-manifest-snapshot.js +32 -0
- package/scripts/check-sbom-currency.js +65 -3
- package/scripts/check-test-coverage.js +142 -19
- package/scripts/predeploy.js +83 -39
- package/scripts/refresh-manifest-snapshot.js +55 -4
- package/scripts/validate-vendor-online.js +169 -0
- package/scripts/verify-shipped-tarball.js +141 -9
- package/skills/ai-attack-surface/skill.md +18 -10
- package/skills/ai-c2-detection/skill.md +7 -2
- package/skills/ai-risk-management/skill.md +5 -4
- package/skills/api-security/skill.md +3 -3
- package/skills/attack-surface-pentest/skill.md +5 -5
- package/skills/cloud-security/skill.md +1 -1
- package/skills/compliance-theater/skill.md +8 -8
- package/skills/container-runtime-security/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +5 -1
- package/skills/email-security-anti-phishing/skill.md +1 -1
- package/skills/exploit-scoring/skill.md +18 -18
- package/skills/framework-gap-analysis/skill.md +6 -6
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +21 -2
- package/skills/mcp-agent-trust/skill.md +17 -10
- package/skills/mlops-security/skill.md +2 -1
- package/skills/ot-ics-security/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +3 -3
- package/skills/pqc-first/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +7 -3
- package/skills/researcher/skill.md +20 -3
- package/skills/sector-energy/skill.md +1 -1
- package/skills/sector-federal-government/skill.md +1 -1
- package/skills/sector-financial/skill.md +3 -3
- package/skills/sector-healthcare/skill.md +2 -2
- package/skills/security-maturity-tiers/skill.md +7 -7
- package/skills/skill-update-loop/skill.md +19 -3
- package/skills/supply-chain-integrity/skill.md +1 -1
- package/skills/threat-model-currency/skill.md +11 -11
- package/skills/threat-modeling-methodology/skill.md +3 -3
- package/skills/webapp-security/skill.md +1 -1
- package/skills/zeroday-gap-learn/skill.md +51 -7
- package/vendor/blamejs/_PROVENANCE.json +4 -1
- package/vendor/blamejs/worker-pool.js +38 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,222 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.16 — 2026-05-14
|
|
4
|
+
|
|
5
|
+
**Patch: highest-impact P1 security findings from the v0.12.15 audit pile.**
|
|
6
|
+
|
|
7
|
+
### Sign/verify trust chain (audit I)
|
|
8
|
+
|
|
9
|
+
- **CRLF/BOM bypass on the shipped-tarball verify gate closed.** `scripts/verify-shipped-tarball.js` previously read raw on-disk bytes and called `crypto.verify` directly — bypassing the CRLF/BOM normalization that `lib/sign.js` + `lib/verify.js` apply on both sides of the byte-stability contract. The gate's whole purpose is to catch the v0.11.x signature regression class; without the same normalization, it would itself report 0/38 on any tree where line-ending normalization touched the source between sign and pack (a Windows contributor with `core.autocrlf=true`, or any tool like Prettier in the CI pipeline). The `normalizeSkillBytes` helper is now mirrored in this fourth normalize() implementation.
|
|
10
|
+
- **`keys/EXPECTED_FINGERPRINT` pin now consulted at every public-key load site.** Previously only `lib/verify.js` + `scripts/verify-shipped-tarball.js` checked the pin. `lib/refresh-network.js` and `bin/exceptd.js attest verify` both loaded `keys/public.pem` and trusted it without the cross-check. A coordinated attacker who tampered with `keys/public.pem` on the operator's host (e.g. via a prior compromised refresh) passed every check because the local↔tarball fingerprints matched each other. Now the pin is the external trust anchor at all four load sites. Honors `KEYS_ROTATED=1` env to allow legitimate rotation without re-bootstrap; missing pin file degrades to warn-and-continue.
|
|
11
|
+
|
|
12
|
+
### CI workflow security (audit N)
|
|
13
|
+
|
|
14
|
+
- **`atlas-currency.yml` script-injection sink closed (CWE-1395).** `${{ steps.currency.outputs.report }}` was interpolated directly into a github-script template literal; the `report` value is unescaped output of `node orchestrator/index.js currency`. A skill author who landed a string containing a backtick followed by `${process.exit(0)}` (or worse, an exfil to a webhook with `${process.env.GITHUB_TOKEN}`) got arbitrary JS execution inside the github-script runtime with the workflow's token. Now routed via `env.REPORT_TEXT` and read inside the script body as `process.env.REPORT_TEXT`.
|
|
15
|
+
- **`refresh.yml` shell-injection from `workflow_dispatch` input closed (CWE-78).** `${{ inputs.source }}` was interpolated directly into a bash `run:` block. An operator passing `kev; rm -rf /; #` got shell injection inside the runner. Now routed via `env.SOURCE_INPUT` and validated against `^[a-z,]+$` (the documented `kev,epss,nvd,rfc,pins` allowlist shape) before passing to the CLI.
|
|
16
|
+
- `actions/checkout` SHA comments aligned across `ci.yml`/`release.yml`/`scorecard.yml` (no SHA change; comment-only).
|
|
17
|
+
- `secret-scan` job declares explicit `permissions: contents: read` (survives a future repo visibility flip).
|
|
18
|
+
- `gitleaks` resolver now has a hardcoded fallback version + non-fatal failure path so a GitHub API HTML-error response doesn't block every CI run.
|
|
19
|
+
- New `tests/workflows-security.test.js` enforces: no `${{ steps.*.outputs.* }}` inside github-script template literals; no `${{ inputs.* }}` inside bash `run:` blocks; every third-party action is SHA-pinned; every workflow declares `permissions:`.
|
|
20
|
+
|
|
21
|
+
### CLI hardening (audit L)
|
|
22
|
+
|
|
23
|
+
- **`--block-on-jurisdiction-clock` now honored on `cmdRun`.** Previously the flag was registered + documented but only `cmdCi` consumed it; `run --block-on-jurisdiction-clock` exited 0 even when an NIS2 24h clock had started. Now both verbs exit 5 (`CLOCK_STARTED`) when any notification action has a non-null `clock_started_at` and an unacked operator consent.
|
|
24
|
+
- **`cmdIngest` auto-detects piped stdin.** Mirrors the `cmdRun` shape — `echo '{...}' | exceptd ingest` now works without an explicit `--evidence -`.
|
|
25
|
+
- **`--vex` validates document shape before applying.** Previously any malformed JSON (SARIF, SBOM, CSAF advisory by mistake) resulted in a silent empty filter; now CycloneDX (`vulnerabilities[]` or `bomFormat: 'CycloneDX'`) or OpenVEX (`statements[]` + `@context` on openvex.dev) shape required before the filter is consumed.
|
|
26
|
+
- **`cmdReattest` verifies the `.sig` sidecar** before consuming the prior attestation. A tampered attestation is no longer silently consumed for the drift verdict. `--force-replay` available for legitimate ack-of-divergence.
|
|
27
|
+
- **`--operator <name>` validated**: rejects ASCII control chars + newlines; caps length at 256; rejects all-whitespace. Closes the "multi-line operator forgery" surface in CSAF / attest export rendering.
|
|
28
|
+
- **`--diff-from-latest` result surfaced in human renderer**: operators running with `--diff-from-latest` and no `--json` now see a `> drift vs prior: <status>` line.
|
|
29
|
+
- **Cross-playbook jurisdiction clock rollup** in `cmdRunMulti` / `cmdCi`: deduped by `(jurisdiction, regulation, obligation, window_hours)`, `triggered_by_playbooks[]` lists contributors. Operators running 13 playbooks no longer draft 8 separate NIS2 24h notifications.
|
|
30
|
+
- `--block-on-jurisdiction-clock` exit code split from `FAIL` (exit 2) → `CLOCK_STARTED` (exit 5). CI gates can distinguish "detected" from "clock fired".
|
|
31
|
+
- `cmdReattest --since` validated as parseable ISO-8601.
|
|
32
|
+
|
|
33
|
+
### Scoring math hardening (audit J)
|
|
34
|
+
|
|
35
|
+
- `scoreCustom` now treats `active_exploitation: 'unknown'` as `0.25 × weight` (was 0) — aligning with `playbook-runner._activeExploitationLadder` semantics so catalog-side and runtime-side scoring agree.
|
|
36
|
+
- New `deriveRwepFromFactors(factors)` helper exported; detects whether `rwep_factors` is in Shape A (boolean inputs to `scoreCustom`) or Shape B (numeric weighted contributions) and produces a consistent score. Documents the dual-semantics so the rename can land cleanly in v0.13.0.
|
|
37
|
+
- `validateFactors` NaN/Infinity diagnostics now use `Number.isFinite` with dedicated messages (was misleading "expected number, got number (null)").
|
|
38
|
+
- `validateFactors` flags unknown factor keys ("unknown factor: X (ignored)").
|
|
39
|
+
- `scoreCustom(factors, {collectWarnings: true})` returns `_rwep_raw_unclamped` so operators see deduction magnitude even when the floor clamp absorbs negative weights.
|
|
40
|
+
- `compare()` "broadly aligned" band tightened from ±20 to ±10. The Copy Fail RWEP-vs-CVSS divergence (delta 12) now correctly surfaces as "significantly higher than CVSS equivalent."
|
|
41
|
+
- `Math.floor(20/2)` arithmetic replaced with `RWEP_WEIGHTS.active_exploitation * 0.5` (no behavior change today; closes a future odd-weight asymmetry).
|
|
42
|
+
|
|
43
|
+
### Curation + auto-discovery + prefetch (audit M)
|
|
44
|
+
|
|
45
|
+
- **Hidden second scoring path in `lib/cve-curation.js` closed.** The apply path previously derived `rwep_score` via `Object.values(rwep_factors).reduce(sum, 0)` — bypassing `scoring.js` entirely. Replaced with `deriveRwepFromFactors()`.
|
|
46
|
+
- **Auto-discovery RWEP divergence closed.** `lib/auto-discovery.js` previously stored `rwep_factors` with null values for poc_available/ai_*/reboot_required while calling `scoreCustom` with `true` defaults; stored factors and stored score were inconsistent and `scoring.validate()` always flagged it. New `buildScoringInputs(kev, nvd)` is the single source of truth.
|
|
47
|
+
- **`lib/prefetch.js` GITHUB_TOKEN now reaches the request.** The auth lookup keyed off source name `"github"` but the registered source is `"pins"` — anonymous rate-limit applied even when `GITHUB_TOKEN` was set. Fixed.
|
|
48
|
+
- **`lib/prefetch.js` docs corrected**: header comment + `printHelp()` no longer reference non-existent source names `ietf` and `github`.
|
|
49
|
+
- **`readCached` no longer returns stale data as fresh** when `fetched_at` is missing/corrupt (the `NaN > maxAgeMs === false` short-circuit was treating undefined-age entries as eternally-fresh).
|
|
50
|
+
|
|
51
|
+
### Playbook quality (audit K)
|
|
52
|
+
|
|
53
|
+
- **Mutex reciprocity validator** in `lib/validate-playbooks.js`: walks every `_meta.mutex` entry, emits WARNING per asymmetric edge. Reciprocity backfilled across 7 mutex relationships (secrets↔library-author, kernel↔hardening, containers↔library-author, etc.).
|
|
54
|
+
- **`containers → sbom` feeds_into edge** added (container-image-layer SBOM matching against KEV-listed CVEs is a primary v0.12.x use case but wasn't declared).
|
|
55
|
+
- **Domain CVE refs backfilled** where threat_context cited CVEs without referencing them: `runtime.cve_refs += CVE-2026-31431`, `ai-api.cve_refs += CVE-2026-30615`. `containers` threat_context's stale `CVE-2024-21626` (not in catalog) stripped.
|
|
56
|
+
- **ATLAS refs backfilled**: `cred-stores.atlas_refs += AML.T0055` (Unsecured Credentials), `containers.atlas_refs += AML.T0010` (ML Supply Chain).
|
|
57
|
+
- **Artifact type enum drift normalized**: 19 occurrences across crypto-codebase / crypto / library-author / mcp / sbom of `"file_path"` and `"log_pattern"` rewritten to the schema enum (`"file"` / `"log"`).
|
|
58
|
+
- **Indicator type enum drift normalized**: 3 occurrences in `library-author` of `"api_response"` rewritten to `"api_call_sequence"`.
|
|
59
|
+
- **FP-check backfill** on library-author indicators (publish-workflow-action-refs-mutable + tag-protection-absent) — gold-standard pattern from `gha-workflow-script-injection-sink` extended to two more high-confidence indicators.
|
|
60
|
+
|
|
61
|
+
### Repository
|
|
62
|
+
|
|
63
|
+
- `data/cve-catalog.json` synthetic test-pollution entry (`CVE-9999-99999`) removed (left by a test run that used the real catalog path).
|
|
64
|
+
- 29 new RWEP vector regression tests in `tests/scoring-vectors.test.js`.
|
|
65
|
+
- 8 new workflow-security regression tests in `tests/workflows-security.test.js`.
|
|
66
|
+
- `validate-playbooks.js` now reports 12/13 PASS + 1 WARN (was 8 PASS + 5 WARN before normalization).
|
|
67
|
+
|
|
68
|
+
Test count: 701 → 738 (+37: 29 scoring vectors + 8 workflow-security). Predeploy gates: 14/14. Skills: 38/38 signed and verified.
|
|
69
|
+
|
|
70
|
+
## 0.12.15 — 2026-05-14
|
|
71
|
+
|
|
72
|
+
**Patch: e2e RWEP factor-scaling fix + audit-surfaced silent-disable regressions. v0.12.14 publish payload.**
|
|
73
|
+
|
|
74
|
+
The v0.12.14 tag exists on git but never reached npm — the `validate` job's `npm run test:e2e` gate failed 9/20 scenarios because the v0.12.14 RWEP factor-scaling change (F5 from the engine agent) had no fallback for class-of-vulnerability playbooks that detect without per-CVE evidence correlation. `_factorScale` returned 0 when no `factorCve` was available; every fired indicator's `weight_applied` was forced to 0; catalog-shape playbooks (secrets, library-author, crypto-codebase, framework, cred-stores, containers, runtime, crypto, ai-api) emitted `adjusted: 0` for every detection.
|
|
75
|
+
|
|
76
|
+
v0.12.15 ships the v0.12.14 deep-audit fix payload plus:
|
|
77
|
+
|
|
78
|
+
### Engine: class-of-vulnerability RWEP fallback
|
|
79
|
+
|
|
80
|
+
`lib/playbook-runner.js` factor-scaling now has a three-tier fallback:
|
|
81
|
+
|
|
82
|
+
1. **Evidence-correlated CVE** (`factorCve from matchedCves[0]`): scale by the matched CVE's catalog attributes (v0.12.14 F5 semantics — `cisa_kev` weight only when the matched CVE actually has `cisa_kev: true`, etc.).
|
|
83
|
+
2. **Domain-CVE fallback** (`factorCve from playbook.domain.cve_refs[]`): when no evidence correlation but the playbook declares its threat class via `domain.cve_refs[]`, use the highest-RWEP catalog entry from those refs.
|
|
84
|
+
3. **Class fallback** (no domain CVE either): apply the declared weight as-is (`factor_scale = 1`), mirroring pre-v0.12.14 behaviour. Class-of-vulnerability playbooks that detect without CVE anchoring (e.g. `secrets`, `library-author`) get a sensible default while still honoring an operator-supplied `blast_radius_score` when present.
|
|
85
|
+
|
|
86
|
+
The breakdown emits `factor_cve_source: 'evidence' | 'domain' | 'class'` so operators see which tier the run used.
|
|
87
|
+
|
|
88
|
+
### Silent-disable regressions closed
|
|
89
|
+
|
|
90
|
+
Three v0.12.12+v0.12.14 fixes that audit M discovered were silently dead:
|
|
91
|
+
|
|
92
|
+
- **`lib/cve-curation.js loadCveEntrySchema()`** always returned `null` because the function looked for `root.patternProperties["^CVE-\\d{4}-\\d+$"]` or an object `root.additionalProperties`, but the actual `lib/schemas/cve-catalog.schema.json` has neither — its top level IS the entry shape. The v0.12.12 codex P1 #1 fix that gates promotion on strict-schema validation was silently disabled; schema-violating entries promoted out of draft anyway. Fixed to use the root schema directly.
|
|
93
|
+
- **`lib/cve-curation.js loadJson("data/attack-ttps.json")`** referenced a path that doesn't exist (canonical is `data/attack-techniques.json`). `loadJsonRaw` swallowed the ENOENT and cached `null`, so the ATT&CK candidate-ranking branch in the curation questionnaire always returned zero proposals. Fixed.
|
|
94
|
+
- **`lib/auto-discovery.js _auto_imported`** wrote object-shape provenance (`{source, imported_at, curation_needed}`) but `lib/validate-cve-catalog.js` checks `entry._auto_imported === true` (strict identity). KEV-discovered drafts were treated as production-grade entries instead of warning-tier drafts, hard-failing the strict catalog gate. Fixed to write the boolean `true` with provenance moved to a sibling `_auto_imported_meta` field. Also: `source_verified: false` (boolean) violated the schema's `YYYY-MM-DD | null` shape — fixed to `null`. Template literal bug on the RFC errata URL hint also fixed (was printing literal `${number}` to operators).
|
|
95
|
+
|
|
96
|
+
### Scoring math hardening (audit J)
|
|
97
|
+
|
|
98
|
+
- `scoreCustom` now rejects `NaN` / `Infinity` / stringified-number `blast_radius` cleanly via `Number.isFinite(Number(blast_radius))`. The prior `typeof === 'number'` check accepted `NaN` (which IS `typeof === 'number'`) and propagated it through `Math.min/max` to the final return — defeating the `[0, 100]` clamp contract.
|
|
99
|
+
- `scoreCustom` now accepts either `reboot_required` or the catalog's `patch_required_reboot` field name. The catalog stores `patch_required_reboot`; `scoreCustom` expected `reboot_required`. `validate()` aliased at the call site, but a direct caller passing the catalog entry silently lost the reboot factor.
|
|
100
|
+
- Defense-in-depth: the final clamp now rejects non-finite scores explicitly (`Number.isFinite(score) ? clamp : 0`).
|
|
101
|
+
|
|
102
|
+
### CLI fuzz fixes (audit L)
|
|
103
|
+
|
|
104
|
+
- `--scope <invalid>` now produces a structured error instead of silently producing zero results. The prior shape: `run --scope nonsense` returned `count: 0` + `ok: true` + exit 0; `ci --scope nonsense` silently ran only the cross-cutting set (`framework`) with `verdict: PASS`. Both validated as operator-intent loss patterns. Accepted scope set: `system | code | service | cross-cutting | all`.
|
|
105
|
+
|
|
106
|
+
### Test alignment
|
|
107
|
+
|
|
108
|
+
- `tests/e2e-scenarios/11-library-author-static-token/expect.json` `json_path_min.adjusted` floor adjusted from `20` to `18` to reflect the post-v0.12.14 RWEP semantics. The scenario evidence supplies `verdict.blast_radius: 4` which legitimately scales blast-radius-factor weights by 0.8; the computed value is `19` (was implicitly `>= 20` only because pre-v0.12.14 every fired indicator applied full weight regardless of CVE attributes or agent-supplied blast score).
|
|
109
|
+
- `tests/auto-discovery.test.js` updated to assert the new `_auto_imported: true` + `_auto_imported_meta: {...}` shape.
|
|
110
|
+
|
|
111
|
+
Test count: 701 (700 pass + 1 skipped POSIX-only SIGTERM test). Predeploy gates: 14/14. Skills: 38/38 signed and verified. v0.12.14 deep-audit fix payload is included.
|
|
112
|
+
|
|
113
|
+
## 0.12.14 — 2026-05-14
|
|
114
|
+
|
|
115
|
+
**Patch: ~210-finding deep-audit fix batch across trust chain, engine, refresh sources, orchestrator/watch, predeploy gates, catalogs, and skill content.**
|
|
116
|
+
|
|
117
|
+
### Trust chain (lib/refresh-network.js)
|
|
118
|
+
|
|
119
|
+
The `exceptd refresh --network` path was effectively unsigned-code-delivery. The signature loop iterated `sk.id` (not exposed on manifest entries) and a fixed payload path `skills/<id>/SKILL.md` (uppercase, while the manifest's path is `skills/<name>/skill.md` lowercase). Result: `0/38 signatures verified` across every operator pulling the network refresh. The `failures.length === 0` short-circuit then allowed `ok: true` to ship.
|
|
120
|
+
|
|
121
|
+
Now: manifest entries iterated by `name` + `path` + `signature`, mirroring `lib/verify.js`. CRLF + BOM normalization applied before verify — Windows-`core.autocrlf=true` contributors produce signatures that round-trip stably through the network refresh. Manifest paths validated with the same regex-and-resolve check the source-tree verifier uses. The swap also enforces that every `skills/*/skill.md` entry shipped in the tarball is declared in the manifest — a tarball-vs-manifest divergence now refuses the swap.
|
|
122
|
+
|
|
123
|
+
Integrity: SHA-512 SRI from `dist.integrity` is verified first (collision-resistant beyond SHA-1 reach), then SHA-1 `dist.shasum` for compatibility. `dist.signatures[]` count is now surfaced. A 200 MB tarball size cap (overridable via `EXCEPTD_TARBALL_SIZE_CAP_BYTES`) is enforced during download.
|
|
124
|
+
|
|
125
|
+
Atomic swap rewritten with two-phase semantics: backup-all-targets THEN install-all-targets, with reverse-walk rollback on mid-swap failure. Backup-dir suffix uses `${process.pid}-${randomBytes(4)}` so concurrent invocations don't collide on the millisecond clock.
|
|
126
|
+
|
|
127
|
+
### Engine semantics (lib/playbook-runner.js)
|
|
128
|
+
|
|
129
|
+
- `evidence_hash` now incorporates a canonicalized SHA-256 over the operator's submission (observations, signal_overrides, signals — sorted keys recursively). Previously it hashed only `(playbook, directive, matched_cves, rwep, classification)`, so two materially different submissions producing the same classification were indistinguishable; `reattest` couldn't detect drift. A `submission_digest` sibling field is also surfaced for downstream consumers.
|
|
130
|
+
- `run()` generates `session_id` once and threads it through close() and into CSAF tracking.id + OpenVEX @id + product PURLs. Previously close() and the bundle emitters each minted independent ids, so an attestation file at `.exceptd/attestations/<run-id>/attestation.json` couldn't be correlated to the bundle URN inside it.
|
|
131
|
+
- Indicator-level `cve_ref` is now load-bearing: when an indicator hits and declares a `cve_ref`, the catalog entry is pulled into `analyze.matched_cves[]` with `correlated_via: 'indicator_cve_ref:<id>'`. Previously the field was dead data — `library-author`'s `gha-workflow-script-injection-sink` had a `cve_ref: "MAL-2026-3083"` that never reached matched_cves.
|
|
132
|
+
- `analyzeFindingShape` now emits a derived `severity` from `rwep_adjusted` (critical >= 80, high >= 50, medium >= 20, low). Nine shipped playbooks reference `finding.severity` in `feeds_into` / `escalation_criteria`; those conditions were dead until now.
|
|
133
|
+
- RWEP `rwep_factor` semantics implemented. Previously the runner applied every weight whenever the named indicator hit — every kernel-LPE hit jumped to RWEP 100 regardless of whether the matched CVE was KEV-listed or had `active_exploitation: confirmed`. Each factor now scales by the first matched CVE's corresponding catalog attribute (`cisa_kev`, `active_exploitation` enum, `poc_available`, `ai_factor`, `patch_available`, etc.). Breakdown surfaces `weight_declared` + `factor_scale` + `weight_applied`.
|
|
134
|
+
- `blast_radius_score`: no signal → `null` (was: first rubric entry's score, which encoded "best case"); supplied → validated in `[0, 5]`; out-of-range → null + `blast_radius_signal: 'rejected'` + runtime_error.
|
|
135
|
+
- Corrupt `data/cve-catalog.json` no longer crashes the runner uncaught at require-time. `lib/cross-ref-api.js` catches JSON parse failures, records them in a `_loadErrors[]` array, and returns a degraded empty catalog. `run()` surfaces `{ok:false, blocked_by:'catalog_corrupt', error: ...}` instead of throwing.
|
|
136
|
+
- Unknown `directiveId` now returns `{ok:false, blocked_by:'directive_not_found', valid_directives:[...]}` instead of throwing inside analyze().
|
|
137
|
+
- VEX `fixed` / CycloneDX `resolved` no longer conflated with `not_affected`. Fixed CVEs are retained in `matched_cves` with a `vex_status: 'fixed'` annotation and excluded from driving RWEP base — operators tracking residual-risk for partially-deployed patches see them; the score doesn't double-count.
|
|
138
|
+
- `analyze.active_exploitation` reduces worst-of-N across matched CVEs (was first-match).
|
|
139
|
+
- `interpolate()` surfaces unresolved `${var}` placeholders as `<MISSING:var>` and emits `missing_interpolation_vars[]` on each notification record. Previously the literal `${var}` reached operator-facing regulator notification drafts.
|
|
140
|
+
- `signal_overrides` non-object input (string, array, number) rejected; previously a string `"HELLO"` spread character-by-character producing phantom indicator overrides.
|
|
141
|
+
- Unknown bundle format no longer leaks `analyze` + `validate` internals via a fallback `{format, note, analyze, validate}` — returns supported-formats list instead.
|
|
142
|
+
- `theater_verdict` validated against allowlist (`clear`, `present`, `theater`, `pending_agent_run`, `unknown`); off-allowlist values rejected with runtime_error.
|
|
143
|
+
- `jurisdiction_obligations` sorted by `window_hours` ascending so shortest-deadline obligations (DORA 4h) surface first.
|
|
144
|
+
- Non-day regression intervals (`wk`, `mo`, `yr`, `on_event`) now honored; previously only `\d+d` matched and 49 shipped triggers with `on_event` were silently dropped. `regression_event_triggers[]` + `regression_unparseable_triggers[]` surfaced.
|
|
145
|
+
- `precondition_check_source` provenance annotation: `'submission' | 'runOpts' | 'merged'` so operators reading attestations see whose precondition declarations the run actually used.
|
|
146
|
+
- `lockDir()` moved from `process.cwd()` to `os.tmpdir() + 'exceptd-locks-<platform>'` (overridable via `EXCEPTD_LOCK_DIR`) so cross-cwd invocations share lock state.
|
|
147
|
+
|
|
148
|
+
### Refresh upstream sources (lib/source-osv.js, lib/source-ghsa.js, lib/refresh-external.js)
|
|
149
|
+
|
|
150
|
+
GHSA + OSV `applyDiff` now route through `withCatalogLock` — previously they mutated `ctx.cveCatalog` in memory but never persisted. Bulk `--source ghsa|osv --apply` reported `applied: N updates` while the catalog file gained zero entries; under `--swarm`, KEV's lock-and-re-read overwrote the unflushed in-memory mutations. Lost-update bug closed.
|
|
151
|
+
|
|
152
|
+
`normalizeAdvisory` now defensively coerces non-string `published_at` / `published` / `modified` to null; iterates `vulnerabilities` / `affected` / `references` only when arrays; coerces GHSA `cvss.score` numerically; validates dates against ISO-8601 prefix + year-in-[1990, currentYear+1]. Garbage upstream values fall to null rather than throwing out of the import.
|
|
153
|
+
|
|
154
|
+
GHSA fixture envelope now rejects null / number / string roots; OSV `OSV_HOST_OVERRIDE` validates host + port. `isOsvId` + `fetchAdvisoryById` + `normalizeAdvisory` + `buildDiff` trim whitespace from operator-supplied identifiers. `pickCatalogKey` upper-cases non-CVE identifiers so mixed-case upstream doesn't produce duplicate catalog entries. CVSS v4-over-v3 fallback: when v4 wins version-order but `cvss4BaseScore` returns null, fall back to v3 score. GHSA `buildDiff` summary now discloses `ghsa_only_skipped` count.
|
|
155
|
+
|
|
156
|
+
### CLI (bin/exceptd.js)
|
|
157
|
+
|
|
158
|
+
- **Path traversal on attest read paths closed** (audit A P1-1). `attest show / export / verify / diff` and `reattest` now validate session-id against the same `^[A-Za-z0-9._-]{1,64}$` regex used on writes. Live reproducer `exceptd attest show '../../..'` (which dumped `~/.claude.json` and other home-dir JSON) no longer reads outside the attestation root.
|
|
159
|
+
- **`emitError` v0.11.10 anti-pattern closed**. The `process.exit(1)` after stderr-write in `emitError` (and three sibling sites in `cmdRun` / `cmdCi`) replaced with `process.exitCode = 1; return;` — stderr drains under piped CI consumers.
|
|
160
|
+
- **`ai-run` now persists attestations** in both `--no-stream` and streaming modes. Previously the returned `session_id` couldn't be resolved by `attest show / verify / diff` or `reattest` because the persistence call was missing.
|
|
161
|
+
- **`attest list --playbook` honors multi-flag** (was: array-vs-scalar comparison silently returned `count: 0`). `--since` validated as parseable ISO-8601.
|
|
162
|
+
- `--evidence-dir` per-entry path-traversal guard hardened.
|
|
163
|
+
|
|
164
|
+
### Orchestrator + watch (orchestrator/)
|
|
165
|
+
|
|
166
|
+
- `bus.eventLog` is now a ring buffer (default cap 1000 entries; `EXCEPTD_EVENT_LOG_MAX_SIZE` env override). Previously unbounded: ~400 B/event monotonic growth — 462 MB at 1M events.
|
|
167
|
+
- `exceptd watch` now handles SIGTERM, SIGHUP, SIGBREAK in addition to SIGINT — container/k8s/systemd shutdown drains scheduler timers and releases the lockfile.
|
|
168
|
+
- Lockfile at `~/.exceptd/watch.lock` prevents two concurrent watch processes against the same store. Stale-lock check uses PID-liveness probe (`process.kill(pid, 0)`) plus 60s mtime fallback.
|
|
169
|
+
- Monthly + annual scheduler bootstrap now fires when overdue (was: only fired after 30/365 days of continuous uptime; weekly-restart watch processes never saw them). Last-fired state persisted at `~/.exceptd/scheduler-last-fired.json`.
|
|
170
|
+
- Scheduler bootstrap `runWeeklyCurrencyCheck()` call wrapped in try/catch matching the per-tick wrapper.
|
|
171
|
+
- `require('orchestrator/index')` no longer triggers full CLI execution — `main()` gated behind `if (require.main === module)`. Duplicate `case 'watch':` removed.
|
|
172
|
+
- `scanner.probeTls()` now honors `EXCEPTD_AIR_GAP=1` and uses `EXCEPTD_TLS_PROBE_TARGET` (default `registry.npmjs.org:443`) instead of hardcoded `google.com:443`.
|
|
173
|
+
- `scan --json` no longer emits `_deprecation` field (CLAUDE.md no-internal-narrative rule).
|
|
174
|
+
- `dispatch()` rejects non-array inputs (was: iterated a string char-by-char). `routeQuery('')` returns `[]` (was: matched all 38 skills via empty-substring short-circuit).
|
|
175
|
+
- `pipeline.buildHandoff` bounds-checks `stageIndex`; `currencyCheck` caches `manifest.json` reads with 60s TTL.
|
|
176
|
+
- Worker-pool `scriptPath` validator rejects Windows UNC + extended-path prefixes (`\\?\`, `\\.\`, `\\server`).
|
|
177
|
+
- New `--log-file <path>` on watch, `--concurrency N` on validate-cves, 50 MB cache-file cap on validateAllCvesPreferCache.
|
|
178
|
+
|
|
179
|
+
### Predeploy gates
|
|
180
|
+
|
|
181
|
+
- New `keys/EXPECTED_FINGERPRINT` pin (closes audit G F4): silent key rotation now fails the gate unless `KEYS_ROTATED=1` is explicitly set.
|
|
182
|
+
- New `manifest-snapshot.sha256` pin (audit G F23): manifest-snapshot integrity is now check-able instead of trusted blindly.
|
|
183
|
+
- `scripts/check-sbom-currency.js` now cross-checks `sbom.components[]` names + versions against `manifest.skills` and `vendor/blamejs/_PROVENANCE.json`. A renamed/version-bumped skill that didn't regenerate SBOM now fails the gate (was: count-only comparison).
|
|
184
|
+
- `scripts/check-test-coverage.js` (diff-coverage gate) tightened: identifier must appear inside an actual `test(`/`it(`/`describe(`/`assert(` call body in the same test file that has the matching `require()` — not just anywhere in the corpus. Default routing for unclassified files changed from `other → allowlisted` to `manual-review` so schema files / data catalogs / package.json drift surface in CI output.
|
|
185
|
+
- `scripts/verify-shipped-tarball.js` now re-`require()`s the extracted tarball's `lib/refresh-network.js` and re-parses the tarball with the shipped parser — `npm pack --offline` flag added. A regression in the parser that previously would have been invisible (gate only used the source-tree parser) now produces a structured divergence error.
|
|
186
|
+
- `lib/validate-cve-catalog.js` extends cross-ref resolution to walk `attack_refs`, `atlas_refs`, `d3fend_refs`, `framework_control_gaps` keys in addition to `cwe_refs`. New `--strict` flag mirrors `validate-playbooks.js` for v0.13.0 preview. All new findings emit as warnings to preserve patch-class.
|
|
187
|
+
- `lib/validate-indexes.js` refuses empty `source_hashes` table; rejects symlinked source entries (defense-in-depth).
|
|
188
|
+
- `lib/validate-catalog-meta.js` now applies the declared `freshness_policy.stale_after_days` (was: declared but never enforced). Warning by default; `--strict` promotes to error.
|
|
189
|
+
- Informational gates' WARN counts surface in the summary as `passed (N warnings)`.
|
|
190
|
+
- Two no-op offline gates (validate-cves / validate-rfcs with forced `--no-fail`) removed; total gates now 14 (was 16).
|
|
191
|
+
- New `scripts/validate-vendor-online.js` (opt-in) fetches each vendored file from upstream and verifies SHA-256 against `_PROVENANCE.json` pinned commit.
|
|
192
|
+
|
|
193
|
+
### Catalog data corrections
|
|
194
|
+
|
|
195
|
+
Nine CVE→catalog cross-ref breaks closed: missing CWE-669 + CWE-123 added; missing ATT&CK sub-techniques T1059.001/006/007 + T1078.001 added; CVE framework_control_gaps keys reconciled to the suffixed canonical names per v0.12.11 (`NIS2-Art21-patch-management`, `SOC2-CC6-logical-access`, `SOC2-CC9-vendor-management`); `ALL-MAJOR-FRAMEWORKS` stub removed; new `DORA-Art28` (ICT third-party risk monitoring) entry added.
|
|
196
|
+
|
|
197
|
+
15 ATLAS entries gained `last_verified` so freshness-decay logic can fire per-entry. `attack-techniques._meta.attack_version` changed from `"v17"` to `"17"` to match `manifest.json.attack_version`. `T0867`/`T1570` "Lateral Tool Transfer" duplicate disambiguated via `domain: ICS` vs `domain: Enterprise`.
|
|
198
|
+
|
|
199
|
+
`cwe-catalog.skills_referencing` contamination cleaned up: 16 entries that mixed skill dir names with playbook IDs split into `skills_referencing` + `playbooks_referencing`. CWE→CVE back-references symmetrized: CVE-2026-43500 ↔ CWE-787 and CVE-2025-53773 ↔ CWE-77.
|
|
200
|
+
|
|
201
|
+
`exploit-availability.json` extended with the 4 newest CVEs (CVE-2026-45321, MAL-2026-3083, CVE-2026-42208, CVE-2026-39884).
|
|
202
|
+
|
|
203
|
+
### Skill content corrections (operator-facing factual drift)
|
|
204
|
+
|
|
205
|
+
- **ATLAS TTP names corrected across 14 skills.** AML.T0054 was systematically mislabeled "Craft Adversarial Data — NLP" (it's "LLM Jailbreak"). AML.T0017 mislabeled "Develop Capabilities" (it's "Discover ML Model Ontology"). AML.T0016 mislabeled "Acquire Public ML Artifacts" (it's "Obtain Capabilities: Develop Capabilities"). AML.T0000 (non-existent) replaced with the actual reconnaissance tactic AML.TA0002.
|
|
206
|
+
- **CVE-2026-30615 (Windsurf MCP) re-aligned with catalog correction.** 17 skills cited CVSS 9.8 / "zero-interaction RCE"; catalog v0.12.9 correction documents CVSS 8.0 / AV:L / local-vector RCE requiring attacker-controlled HTML. Skill bodies and the exploit-scoring pedagogical example reframed accordingly.
|
|
207
|
+
- **CVE-2025-53773 (GitHub Copilot) re-aligned** across 11 skills: cited 9.6 / RWEP 42 (or 91), catalog says 7.8 / RWEP 30.
|
|
208
|
+
- **CVE-2026-31431 KEV date corrected** across 5 skills: cited 2026-03-15, catalog says 2026-05-01. Compliance-theater pedagogical "30 days exposed" narrative recomputed to "13 days exposed" against today's date.
|
|
209
|
+
- **ATT&CK v17 pin propagated** to incident-response-playbook, pqc-first, skill-update-loop (was citing v15.1 / v15 / v16). Spurious "AGENTS.md rule #12" reference corrected to "rule #8".
|
|
210
|
+
- **Four newest catalog CVEs cited in appropriate skills**: MAL-2026-3083 (mlops-security, zeroday-gap-learn), CVE-2026-42208 (ai-attack-surface, ai-c2-detection, rag-pipeline-security, dlp-gap-analysis), CVE-2026-39884 (mcp-agent-trust), CVE-2026-45321 (zeroday-gap-learn, ai-attack-surface, supply-chain-integrity).
|
|
211
|
+
- **Defensive Countermeasure Mapping section added** to kernel-lpe-triage, researcher, skill-update-loop (previously missing despite `last_threat_review >= 2026-05-11`).
|
|
212
|
+
|
|
213
|
+
### Repository
|
|
214
|
+
|
|
215
|
+
- `package.json files` allowlist extended with `keys/EXPECTED_FINGERPRINT` and `manifest-snapshot.sha256` so the new pin checks ship to operators.
|
|
216
|
+
- `vendor/blamejs/_PROVENANCE.json` `exceptd_deltas` documents the worker-pool UNC-path Windows rejection.
|
|
217
|
+
|
|
218
|
+
Test count: 586 → 693 (+107: refresh-network rewrite tests, engine non-engine fixes, orchestrator audit tests, source-osv + source-ghsa hardening, predeploy gate additions, validate-cve-catalog cross-ref tests). Predeploy gates: 14/14 (was 16; two no-op offline gates removed). Skills: 38/38 signed and verified.
|
|
219
|
+
|
|
3
220
|
## 0.12.13 — 2026-05-14
|
|
4
221
|
|
|
5
222
|
**Patch: e2e scenarios updated for the v0.12.12 jurisdiction-clock semantics.**
|