@blamejs/exceptd-skills 0.12.10 → 0.12.13
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 +131 -0
- package/README.md +3 -1
- package/bin/exceptd.js +152 -39
- package/data/_indexes/_meta.json +10 -9
- package/data/_indexes/activity-feed.json +11 -3
- package/data/_indexes/catalog-summaries.json +24 -2
- package/data/_indexes/frequency.json +2 -0
- package/data/attack-techniques.json +96 -0
- package/data/cve-catalog.json +9 -9
- package/data/cwe-catalog.json +4 -3
- package/data/framework-control-gaps.json +52 -0
- package/data/playbooks/library-author.json +3 -3
- package/lib/cve-curation.js +491 -46
- package/lib/lint-skills.js +212 -15
- package/lib/playbook-runner.js +485 -108
- package/lib/prefetch.js +121 -8
- package/lib/refresh-external.js +257 -81
- package/lib/refresh-network.js +15 -1
- package/lib/schemas/manifest.schema.json +16 -0
- package/lib/scoring.js +68 -5
- package/lib/sign.js +112 -3
- package/lib/source-ghsa.js +7 -1
- package/lib/source-osv.js +228 -57
- package/lib/validate-cve-catalog.js +171 -3
- package/lib/validate-playbooks.js +469 -0
- package/lib/verify.js +241 -16
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/scheduler.js +50 -7
- package/package.json +1 -1
- package/sbom.cdx.json +8 -8
- package/scripts/predeploy.js +31 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,136 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.13 — 2026-05-14
|
|
4
|
+
|
|
5
|
+
**Patch: e2e scenarios updated for the v0.12.12 jurisdiction-clock semantics.**
|
|
6
|
+
|
|
7
|
+
Two e2e scenarios (`02-tanstack-worm-payload`, `09-secrets-aws-key`) assert that `phases.close.jurisdiction_clocks_count >= 1` against a `detected` classification. In v0.12.12 the clock-starts contract was tightened: `clock_starts: detect_confirmed` no longer auto-stamps when classification turns `detected`; the operator must pass `--ack` for the clock to start. Both scenarios now pass `--ack` so the contract is exercised end-to-end. No code changes; v0.12.13 ships solely to land the scenario update and a corresponding npm publish — the v0.12.12 tag exists on git but never reached the npm registry because the validate gate failed against the pre-update scenarios.
|
|
8
|
+
|
|
9
|
+
Test count: 585/585. Predeploy gates: 16/16. Skills: 38/38 signed and verified.
|
|
10
|
+
|
|
11
|
+
## 0.12.12 — 2026-05-13
|
|
12
|
+
|
|
13
|
+
**Patch: deep multi-surface hardening — engine semantics, concurrency, signing round-trip, output bundles, validators, scheduler, curation. 73 distinct fixes across 10 surface classes.**
|
|
14
|
+
|
|
15
|
+
### Engine semantics
|
|
16
|
+
|
|
17
|
+
`lib/playbook-runner.js` corrects several long-standing classification and clock bugs:
|
|
18
|
+
|
|
19
|
+
- **False-positive checks now gate classification.** When an indicator's `signal_overrides` says `hit` but the indicator's `false_positive_checks_required[]` haven't been attested, the verdict downgrades to `inconclusive` and `fp_checks_unsatisfied[]` is surfaced on the indicator. Operators attest FP checks with `signal_overrides: { '<id>__fp_checks': { '<check>': true } }`. Before: submitting a hit without attesting FP checks would auto-stamp `classification: detected`.
|
|
20
|
+
- **Dead branch on empty submission**: the indicator-default arm previously emitted `inconclusive` for both `anyCaptured` and the empty case. Empty submissions with no captured artifacts now correctly produce `classification: not_detected` with theater verdict `clear`.
|
|
21
|
+
- **`evalCondition` regex no longer crashes the run.** A malformed indicator condition (operator-authored regex) used to throw out of `analyze()`. Now wrapped in try/catch; the failure surfaces as `analyze.runtime_errors[]` with the source condition + exception message.
|
|
22
|
+
- **`--strict-preconditions` is now load-bearing.** The flag escalates `precondition_unverified` / `precondition_warn` / `precondition_skip` outcomes to halt, with `escalated_from` provenance. The CLI exit body now carries `strict_preconditions_violated[]` so consumers grep'ing the JSON see the contract reason without inspecting stderr.
|
|
23
|
+
- **`on_fail: skip_phase` is actually honored.** A precondition that fails `on_fail: skip_phase` now emits a placeholder detect phase `{skipped: true, classification: 'skipped', reason: <id>}` and runs analyze with empty signals. Previously the runner ignored the directive and proceeded into detect as if the precondition had passed.
|
|
24
|
+
- **`clock_starts: detect_confirmed` is bound to operator awareness.** Jurisdiction notification clocks (NIS2 24h, DORA 4h, GDPR 72h, etc.) no longer auto-stamp when classification turns `detected`; the operator must pass `--ack` for the clock to start. Without `--ack`, the notification entry carries `clock_pending_ack: true`. Matches the legal contract — the clock starts from operator awareness, not from the runner's decision.
|
|
25
|
+
- **`analyze.active_exploitation` is now the worst across matched CVEs**, not the first. Two matched CVEs where #1 is `suspected` and #2 is `confirmed` correctly report `confirmed`.
|
|
26
|
+
- **`signal_overrides` collisions are surfaced** rather than silently last-wins. Two observations targeting the same indicator id now record the discarded values in `analyze.signal_origins_with_collisions[]`.
|
|
27
|
+
- **Per-run playbook cache**: the runner reads the playbook once per `run()` invocation instead of re-loading it inside each of the seven phase calls.
|
|
28
|
+
|
|
29
|
+
### Scoring
|
|
30
|
+
|
|
31
|
+
`lib/scoring.js` exports a new `validateFactors(factors)` returning structured warnings for missing fields, out-of-range `blast_radius`, or non-enum `active_exploitation`. `scoreCustom(factors, {collectWarnings: true})` returns the score plus `_scoring_warnings[]` for downstream consumers; the bare-number return is preserved for backwards compatibility.
|
|
32
|
+
|
|
33
|
+
### Concurrency
|
|
34
|
+
|
|
35
|
+
Catalog read-modify-write was racy under concurrent `refresh --advisory --apply` invocations — five sites in `lib/refresh-external.js` and two in `lib/prefetch.js`. Now serialized via `withCatalogLock` / `withIndexLock` (lockfile-gated, atomic tmp+rename writes; 30s stale-lock reaper for crash recovery). Concurrent applies to distinct CVEs now both survive in the final catalog rather than 1/20 trials losing an entry to interleaved writes. Same pattern applied to the prefetch `_index.json`.
|
|
36
|
+
|
|
37
|
+
`persistAttestation` (in `bin/exceptd.js`) no longer has a TOCTOU window between `existsSync` and `writeFileSync` — atomic create via `flag: 'wx'` (`O_EXCL`) guarantees that two concurrent runs sharing a session-id produce one winner and one explicit `EEXIST` rather than silent last-write-wins.
|
|
38
|
+
|
|
39
|
+
`lib/refresh-external.js` post-pool `process.exit()` calls replaced with `process.exitCode = N; return;` so buffered stdout drains before the event loop ends (same v0.11.10 class).
|
|
40
|
+
|
|
41
|
+
### Signing round-trip
|
|
42
|
+
|
|
43
|
+
`lib/sign.js` + `lib/verify.js` now normalize content (strip UTF-8 BOM, convert CRLF → LF) before computing or verifying signatures. A skill body cloned with `core.autocrlf=true` on Windows but signed on Linux CI no longer fails verification on the consumer side. Byte-level proof: all four variants of `hello\nworld\n` (LF, CRLF, BOM+LF, BOM+CRLF) normalize to the identical signature.
|
|
44
|
+
|
|
45
|
+
Manifest schema validation lands in `lib/schemas/manifest.schema.json` + `loadManifestValidated()`. A tampered manifest with `path: "../../../etc/passwd"` is rejected at load time before any skill resolution. Per-skill paths must match `^skills/[A-Za-z0-9._/-]+/skill\.md$`.
|
|
46
|
+
|
|
47
|
+
`lib/lint-skills.js` rejects duplicate frontmatter keys (last-wins parsing previously masked identity spoofing) and walks `skills/` for orphan `skill.md` files not referenced in the manifest.
|
|
48
|
+
|
|
49
|
+
The fingerprint banner now prints AFTER the verdict line in both `sign-all` and `verify`, so a quick read of `gh run watch` output isn't ambiguous about pass/fail.
|
|
50
|
+
|
|
51
|
+
### Path traversal hardening
|
|
52
|
+
|
|
53
|
+
- `--session-id` now enforces `^[A-Za-z0-9._-]{1,64}$` (alphanumeric, dot, underscore, hyphen; up to 64 chars). Path separators and `..` are rejected at input.
|
|
54
|
+
- `--attestation-root` rejects `..`-bearing relative paths and resolves to an absolute path before propagation.
|
|
55
|
+
- `--evidence-dir` validates each `<id>.json` entry, refuses traversal-escaping resolved paths.
|
|
56
|
+
- `--evidence` enforces a 32 MB file-size limit to defend against adversarial JSON bombs.
|
|
57
|
+
- `persistAttestation` validates the session-id + filename and confirms the resolved directory stays under the attestation root.
|
|
58
|
+
- `parseTar` in `lib/refresh-network.js` skips entries with `..` segments or absolute paths — defense-in-depth against a compromised registry CDN shipping path-traversal tarballs.
|
|
59
|
+
|
|
60
|
+
### Output bundles (CSAF 2.0 / SARIF 2.1.0 / OpenVEX 0.2.0)
|
|
61
|
+
|
|
62
|
+
`buildEvidenceBundle()` in `lib/playbook-runner.js` produces bundles that pass canonical-schema validation against each spec:
|
|
63
|
+
|
|
64
|
+
- **CSAF**: `csaf_security_advisory` documents now include a populated `product_tree.full_product_names[]`; every `vulnerabilities[]` entry references a declared product via `product_status` (`known_affected` / `fixed` / `under_investigation`). NVD / Red Hat / ENISA CSAF dashboards previously rejected exceptd CSAF output for missing product_tree.
|
|
65
|
+
- **SARIF**: indicator-hit results now populate `physicalLocation.artifactLocation.uri` from the playbook's look-phase artifact source paths so GitHub Code Scanning surfaces them. Null property-bag keys are pruned. Framework-gap results carry `kind: "informational"` per spec §3.27.9.
|
|
66
|
+
- **OpenVEX**: every statement carries `products` (B1). Status semantics rebuilt — indicator hits become `affected` with an `action_statement` from the validate phase's selected remediation; misses become `not_affected` with `vulnerable_code_not_present` justification; inconclusive stays `under_investigation` (no action_statement). Framework-gap statements are removed from the VEX feed entirely (they're control-design observations, not vulnerabilities — they remain in CSAF and SARIF). Vulnerability `@id` values now follow RFC 8141 (`urn:cve:<id>`, `urn:exceptd:indicator:<playbook>:<id>`), replacing the unregistered `exceptd:` scheme.
|
|
67
|
+
|
|
68
|
+
### Validators
|
|
69
|
+
|
|
70
|
+
`lib/validate-playbooks.js` is a new validator that checks all 13 shipped playbooks against `lib/schemas/playbook.schema.json` plus cross-catalog references (`atlas_refs`, `cve_refs`, `cwe_refs`, `d3fend_refs`, `attack_refs`), internal consistency (duplicate indicator ids, RWEP threshold ordering, obligation_ref resolution), and feeds_into / mutex / skill_chain resolution. Wired as predeploy gate 16 (informational in v0.12.12; flips to enforcing in v0.13.0). 75-entry `data/attack-techniques.json` lands to support `attack_refs` resolution across skills and playbooks.
|
|
71
|
+
|
|
72
|
+
`lib/validate-cve-catalog.js` adds warning-class checks for the Hard Rule #14 iocs-when-poc-and-exploit-url contract, `atlas_refs` + `cwe_refs` cross-catalog resolution, duplicate-name detection, impossible-date guards, and strict CVSS-version prefix recognition. All new findings emit as warnings in v0.12.12 to preserve patch-class compatibility; v0.13.0 will flip them to errors.
|
|
73
|
+
|
|
74
|
+
`lib/lint-skills.js` extends section detection to require an anchored `^## <Section>` heading with ≥20 words of body text (warning-class), resolves `attack_refs` against `data/attack-techniques.json`, and flags missing "Defensive Countermeasure Mapping" sections on skills whose `last_threat_review >= 2026-05-11`.
|
|
75
|
+
|
|
76
|
+
### Curation `--apply`
|
|
77
|
+
|
|
78
|
+
`lib/cve-curation.js` gains the missing apply path. `curate(cveId, {apply: true, answers})` validates each answer against a per-field whitelist, applies, derives `rwep_score` from `rwep_factors` when an explicit score isn't supplied, computes `residual_warnings[]` against the required-schema set, and promotes the draft (strips `_auto_imported` + `_draft` + `_draft_reason`) when zero warnings remain. CLI surface: `exceptd refresh --curate <id> --answers <file>` or the explicit `--apply` alias. The questionnaire now always asks for `cvss_score`, `cvss_vector`, patch fields, `affected_versions`, and `cisa_kev` when those are unpopulated — without these, the apply path can't produce a schema-passing entry. Severity rendering for `cvss_score: null` returns `unrated` (was misleading `low`). Catalog reads honor absolute paths on Windows. OSV-imported drafts now show `"OSV: <id>"` in `auto_imported_from` (was always `"unknown"`).
|
|
79
|
+
|
|
80
|
+
### Scheduler
|
|
81
|
+
|
|
82
|
+
`orchestrator/scheduler.js` `MONTHLY_CVE_VALIDATION` (2.59 billion ms) and `ANNUAL_AUDIT` (31.5 billion ms) exceeded Node's INT32 setTimeout limit (2.15 billion ms), which silently clamps to 1 ms — producing a 1000 fires/sec stdout flood on idle `exceptd watch`. New `scheduleEvery(intervalMs, handler)` primitive uses a bounded `setInterval` (capped at 24 h) with wall-clock elapsed comparison. Idle watch goes from 1000 lines/sec to 0.
|
|
83
|
+
|
|
84
|
+
### Predeploy
|
|
85
|
+
|
|
86
|
+
`scripts/predeploy.js` now reports per-gate timing (`(NNN ms)` next to each pass / fail / informational line + the summary table). New 16th gate `Validate playbooks` runs informationally in v0.12.12.
|
|
87
|
+
|
|
88
|
+
### Repository
|
|
89
|
+
|
|
90
|
+
- `.github/workflows/ci.yml` gains a `validate-playbooks` job (`continue-on-error: true` in v0.12.12).
|
|
91
|
+
- `manifest-snapshot.json` + `sbom.cdx.json` + `data/_indexes/` refreshed.
|
|
92
|
+
- `data/attack-techniques.json` new — 75 ATT&CK technique entries with v17 metadata, supporting `attack_refs` resolution across the catalog.
|
|
93
|
+
|
|
94
|
+
Test count: 492 → 573 (+81 across engine, sign/verify, refresh-external, prefetch, scheduler, cve-curation, bundle-correctness, validate-playbooks, and operator-bugs test files). Predeploy gates: 16/16. Skills: 38/38 signed and verified.
|
|
95
|
+
|
|
96
|
+
## 0.12.11 — 2026-05-13
|
|
97
|
+
|
|
98
|
+
**Patch: OSV source hardening, indicator regex widening, CWE/framework-gap reconciliation. v0.12.10 audit closeout.**
|
|
99
|
+
|
|
100
|
+
### OSV source hardening
|
|
101
|
+
|
|
102
|
+
`lib/source-osv.js` matures from greenfield to GHSA-parity:
|
|
103
|
+
|
|
104
|
+
- **Structured fixture-I/O error envelope.** Missing or malformed `EXCEPTD_OSV_FIXTURE` paths no longer crash with a Node stack trace; the source returns `{ok:false, error, source:"offline"}` matching the GHSA convention. Operators piping the CLI through `jq` or scripting around exit codes get a structured failure they can branch on.
|
|
105
|
+
- **Case-fold ids before lookup.** `fetchAdvisoryById("mal-2026-3083")` (lowercase) now resolves correctly. OSV.dev's `/v1/vulns/{id}` is case-sensitive — the source uppercases the id at entry before any branch on fixture lookup or network call.
|
|
106
|
+
- **Highest-CVSS-version wins + compute from vector.** `extractCvss` previously overwrote the chosen vector on every loop iteration ("last wins" not "highest-version wins") and returned `null` `score` when the OSV record carried only a vector string with no embedded numeric tail. Both fixed: explicit version-comparison via the `CVSS:N.M` prefix, and a new `cvss3BaseScore(vector)` helper that computes the CVSS 3.1 base score per FIRST §7.1 (handles Scope:U + Scope:C). MAL-* records that previously normalized to `cvss_score: null` / `active_exploitation: "unknown"` now carry computed scores.
|
|
107
|
+
- **GHSA-404 → OSV fallback for CVE-*.** `seedSingleAdvisory` previously routed `CVE-*` unconditionally through `source-ghsa`. When GHSA returned 404 for a CVE that had only PYSEC / RUSTSEC / SNYK / MAL coverage, the operator saw `GHSA returned HTTP 404` even though OSV had the record. Now: on GHSA-404 for a CVE-* id, retry via `source-osv.fetchAdvisoryById(id)`; surface the combined error when both 404.
|
|
108
|
+
- **`epss_note` on non-CVE drafts.** Non-CVE catalog keys (MAL-*, SNYK-*, RUSTSEC-*, etc.) now carry a populated `epss_note` documenting the FIRST EPSS API limitation — drafts no longer look incomplete to downstream consumers grepping for the field.
|
|
109
|
+
- **`verification_sources` deduped.** The canonical `osv.dev/vulnerability/<id>` URL was previously both prepended unconditionally AND pulled from `rec.references[]`. Deduped via `new Set` before return.
|
|
110
|
+
- **`buildDiff` error categorization.** Returns `unreachable_count` + `normalize_error_count` separately so an operator can distinguish "OSV unreachable" from "10 ids returned but none normalized cleanly."
|
|
111
|
+
- **`GHSA-` dropped from `OSV_ID_PREFIXES`.** The export previously listed GHSA-* even though the dispatcher unconditionally routes GHSA-* through `source-ghsa`. `isOsvId("GHSA-...")` now returns false. A top-of-file comment documents the routing decision (GHSA has richer field coverage for that namespace).
|
|
112
|
+
- **`OSV_HOST_OVERRIDE` env var for offline HTTP testing.** New stubbing surface — lets `tests/source-osv.test.js` spin up a local HTTP server to exercise HTTP 500 / 429 / timeout / parse-error paths previously uncovered. 429 surfaces as `rate-limited`; timeout error message clarified.
|
|
113
|
+
- **`seedSingleAdvisory` exported** for in-process testing.
|
|
114
|
+
|
|
115
|
+
### Indicator regex widening
|
|
116
|
+
|
|
117
|
+
`gha-workflow-script-injection-sink` (added v0.12.10) previously anchored on `run:\s*\|` (block-scalar pipe only). Single-line `run: echo "${{ github.event.comment.body }}"` bypassed the regex despite being the same vulnerability class. Widened to `run:[\s\S]*?...` which admits both block-scalar AND single-line forms. The indicator's `confidence` drops from `deterministic` → `high` and `deterministic` flag flips to `false` to reflect the reasoning step still required for the false-positive demotion (sandboxed `pull_request` + `contents: read` permissions). `tests/gha-workflow-script-injection-sink.test.js` lands as a new end-to-end regex test with 8 fixture YAML cases covering both the catch and the FP-demotion classes. All 5 of this repo's own `.github/workflows/*.yml` files remain clean against the widened regex.
|
|
118
|
+
|
|
119
|
+
### CWE reverse-references
|
|
120
|
+
|
|
121
|
+
The v0.12.10 catalog additions cited existing CWEs (CWE-89, CWE-77, CWE-94) without updating their reverse-reference `evidence_cves` arrays. Bidirectional linkage restored: CWE-89 now lists CVE-2026-42208 (LiteLLM SQLi), CWE-77 lists MAL-2026-3083 (elementary-data secondary classification), CWE-94 adds MAL-2026-3083 alongside the existing CVE-2025-53773 and CVE-2026-30615.
|
|
122
|
+
|
|
123
|
+
### Framework-control-gaps key reconciliation
|
|
124
|
+
|
|
125
|
+
Eight `framework_control_gaps` keys used by the v0.12.10 catalog additions did not resolve in `data/framework-control-gaps.json`. Six reconciled to canonical existing forms: `SLSA-L3` → `SLSA-v1.0-Build-L3`; `OWASP-LLM01` → `OWASP-LLM-Top-10-2025-LLM01`; `NIST-800-218-PO.4` → `NIST-800-218-SSDF`; `NIS2-Art21-2d` / `-2g` → `NIS2-Art21-patch-management`; `NIS2-Art21-2e` → `NIS2-Art21-incident-handling`. Two genuinely-distinct citations gained new entries in the framework-gaps catalog: `EU-CRA-Art13` (essential cybersecurity requirements + technical documentation; the elementary-data class of supply-chain compromise where the maintainer is a victim) and `NIST-800-53-SI-10` (information input validation; the trust-boundary-vs-inside-boundary distinction that argument-injection / SQL-injection / prompt-injection exploit). All `framework_control_gaps` references in the catalog now resolve to a real entry.
|
|
126
|
+
|
|
127
|
+
### Repository
|
|
128
|
+
|
|
129
|
+
- `lib/source-ghsa.js` "unrecognized id format" error message widened to enumerate the OSV-native prefixes operators can pass via `--advisory` (was previously CVE/GHSA only).
|
|
130
|
+
- `README.md` documents the OSV source: install command, `--advisory MAL-...` form, `EXCEPTD_OSV_FIXTURE` env var, the fresh-disclosure workflow expanded to mention OSV's coverage breadth.
|
|
131
|
+
|
|
132
|
+
Test count: 462 → 492 (+30: 18 OSV source-hardening tests + 10 indicator regex tests + 2 catalog drift assertions). Predeploy gates: 15/15. Skills: 38/38 signed and verified.
|
|
133
|
+
|
|
3
134
|
## 0.12.10 — 2026-05-13
|
|
4
135
|
|
|
5
136
|
**Patch: OSV.dev wired as an upstream source, three new catalog entries, one new library-author indicator.**
|
package/README.md
CHANGED
|
@@ -135,6 +135,7 @@ You want to refresh CVE/RFC data, run currency checks, or generate reports. Inst
|
|
|
135
135
|
npx @blamejs/exceptd-skills doctor # health check
|
|
136
136
|
npx @blamejs/exceptd-skills refresh --apply --swarm # pull KEV/NVD/EPSS/RFC/GHSA + apply
|
|
137
137
|
npx @blamejs/exceptd-skills refresh --advisory CVE-2026-45321 # seed one CVE draft from GHSA
|
|
138
|
+
npx @blamejs/exceptd-skills refresh --advisory MAL-2026-3083 # seed via OSV (MAL-/SNYK-/RUSTSEC-/USN-/PYSEC-/GO-/MGASA-/UVI-)
|
|
138
139
|
npx @blamejs/exceptd-skills refresh --curate CVE-2026-45321 # surface editorial questions for a draft
|
|
139
140
|
npx @blamejs/exceptd-skills refresh --network # swap data/ from latest signed npm tarball
|
|
140
141
|
```
|
|
@@ -148,7 +149,7 @@ exceptd help
|
|
|
148
149
|
|
|
149
150
|
Air-gapped operation: run `exceptd refresh --prefetch` on a connected host, copy the resulting `.cache/upstream/` to the airgap, run `exceptd refresh --from-cache <path> --apply` over there. The vendored upstream snapshots replace every network call.
|
|
150
151
|
|
|
151
|
-
Fresh-disclosure workflow (v0.12.0): the nightly auto-PR job pulls KEV / NVD / EPSS / IETF / **GHSA** (added in v0.12.0). KEV typically takes days; NVD ~10 days; GHSA fires within hours of disclosure and covers npm + PyPI + Maven + Go + NuGet +
|
|
152
|
+
Fresh-disclosure workflow (v0.12.0): the nightly auto-PR job pulls KEV / NVD / EPSS / IETF / **GHSA** (added in v0.12.0) / **OSV** (added in v0.12.10). KEV typically takes days; NVD ~10 days; GHSA fires within hours of disclosure and covers npm + PyPI + Maven + Go + NuGet + …; OSV aggregates the OSSF Malicious Packages dataset (`MAL-*` keys) + Snyk + RustSec + Mageia + Ubuntu USN + Go Vuln DB + PYSEC + UVI on top of GHSA — useful for malicious-package compromises that don't have CVEs yet (`exceptd refresh --advisory MAL-2026-3083`). New IDs land as drafts (`_auto_imported: true`, `_draft: true`) that the catalog validator treats as warnings, not errors — operators get the fresh entry immediately, editorial review (framework gaps, IoCs, ATLAS/ATT&CK refs) follows via `exceptd refresh --curate <ID>`. For "I want this advisory today, not tomorrow": `exceptd refresh --advisory <CVE-or-GHSA-or-MAL-or-SNYK-or-RUSTSEC-ID> --apply`.
|
|
152
153
|
|
|
153
154
|
Optional env vars for higher rate budgets:
|
|
154
155
|
|
|
@@ -157,6 +158,7 @@ Optional env vars for higher rate budgets:
|
|
|
157
158
|
| `NVD_API_KEY` | Lifts NVD 2.0 from 5 → 50 requests per 30s window. Free key at <https://nvd.nist.gov/developers/request-an-api-key>. |
|
|
158
159
|
| `GITHUB_TOKEN` | Lifts GitHub Releases + GHSA from 60 → 5000 requests per hour. |
|
|
159
160
|
| `EXCEPTD_GHSA_FIXTURE` | Path to a JSON fixture matching the api.github.com/advisories shape. For offline tests + air-gap workflows. |
|
|
161
|
+
| `EXCEPTD_OSV_FIXTURE` | Path to a JSON fixture matching the OSV schema (https://ossf.github.io/osv-schema/). For offline tests + air-gap workflows against the OSV source (added v0.12.10). |
|
|
160
162
|
| `EXCEPTD_REGISTRY_FIXTURE` | Path to a JSON fixture matching the npm registry response. Used by `doctor --registry-check` + `run --upstream-check` + `refresh --network` for offline testing. |
|
|
161
163
|
|
|
162
164
|
### 3. Maintainer (extend / sign / publish)
|
package/bin/exceptd.js
CHANGED
|
@@ -554,6 +554,17 @@ function readEvidence(evidenceFlag) {
|
|
|
554
554
|
if (!buf.trim()) return {};
|
|
555
555
|
return JSON.parse(buf);
|
|
556
556
|
}
|
|
557
|
+
// v0.12.12: read enforces a max size to defend against an operator
|
|
558
|
+
// accidentally passing a multi-gigabyte file (binary, log, or
|
|
559
|
+
// adversarial JSON bomb). 32 MB is well beyond any legitimate
|
|
560
|
+
// submission and still drains in a single read on modern hardware.
|
|
561
|
+
const MAX_EVIDENCE_BYTES = 32 * 1024 * 1024;
|
|
562
|
+
let stat;
|
|
563
|
+
try { stat = fs.statSync(evidenceFlag); }
|
|
564
|
+
catch (e) { throw new Error(`evidence path not readable: ${e.message}`); }
|
|
565
|
+
if (stat.size > MAX_EVIDENCE_BYTES) {
|
|
566
|
+
throw new Error(`evidence file too large: ${stat.size} bytes > ${MAX_EVIDENCE_BYTES} byte limit. Reduce the submission or split into multiple playbook runs.`);
|
|
567
|
+
}
|
|
557
568
|
return JSON.parse(fs.readFileSync(evidenceFlag, "utf8"));
|
|
558
569
|
}
|
|
559
570
|
|
|
@@ -607,8 +618,39 @@ function dispatchPlaybook(cmd, argv) {
|
|
|
607
618
|
airGap: !!args["air-gap"],
|
|
608
619
|
forceStale: !!args["force-stale"],
|
|
609
620
|
};
|
|
610
|
-
if (args["session-id"])
|
|
611
|
-
|
|
621
|
+
if (args["session-id"]) {
|
|
622
|
+
// v0.12.12: --session-id is a filesystem path component (resolves to
|
|
623
|
+
// .exceptd/attestations/<id>/attestation.json). Operator-supplied input
|
|
624
|
+
// with `..` or path separators escapes the attestation root. Validate
|
|
625
|
+
// strict allowlist before propagating.
|
|
626
|
+
const sid = args["session-id"];
|
|
627
|
+
if (typeof sid !== "string" || !/^[A-Za-z0-9._-]{1,64}$/.test(sid)) {
|
|
628
|
+
return emitError(
|
|
629
|
+
"run: --session-id must match /^[A-Za-z0-9._-]{1,64}$/ (alphanumeric, dot, underscore, hyphen; up to 64 chars). Path separators and '..' are rejected.",
|
|
630
|
+
{ provided: typeof sid === "string" ? sid.slice(0, 80) : typeof sid },
|
|
631
|
+
pretty
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
runOpts.session_id = sid;
|
|
635
|
+
}
|
|
636
|
+
if (args["attestation-root"]) {
|
|
637
|
+
// v0.12.12: --attestation-root must resolve to an absolute path the
|
|
638
|
+
// operator owns. Reject `..`-bearing relatives at input so a misconfigured
|
|
639
|
+
// env doesn't write outside the intended root. Final resolution still
|
|
640
|
+
// happens in resolveAttestationRoot — this is the input-validation layer.
|
|
641
|
+
const ar = args["attestation-root"];
|
|
642
|
+
if (typeof ar !== "string" || ar.length === 0) {
|
|
643
|
+
return emitError("run: --attestation-root must be a non-empty string.", { provided: typeof ar }, pretty);
|
|
644
|
+
}
|
|
645
|
+
if (ar.split(/[\\/]/).some(seg => seg === "..")) {
|
|
646
|
+
return emitError(
|
|
647
|
+
"run: --attestation-root must not contain '..' path segments. Pass an absolute path under your home directory or an explicit project-relative path without traversal.",
|
|
648
|
+
{ provided: ar.slice(0, 200) },
|
|
649
|
+
pretty
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
runOpts.attestationRoot = path.resolve(ar);
|
|
653
|
+
}
|
|
612
654
|
if (args["session-key"]) {
|
|
613
655
|
// Bug #33: validate that --session-key is hex. Previously any string was
|
|
614
656
|
// silently accepted; HMAC signing then either failed silently or produced
|
|
@@ -1678,6 +1720,14 @@ function cmdRun(runner, args, runOpts, pretty) {
|
|
|
1678
1720
|
i.kind === "precondition_unverified" || i.kind === "precondition_warn"
|
|
1679
1721
|
);
|
|
1680
1722
|
if (warnIssues.length > 0) {
|
|
1723
|
+
// v0.12.12: surface the contract violation in the emitted body so
|
|
1724
|
+
// downstream consumers grepping the JSON see WHY the exit is non-zero.
|
|
1725
|
+
// result.ok stays true (the playbook executed) but the explicit flag
|
|
1726
|
+
// makes the strict-preconditions contract observable, not just inferable
|
|
1727
|
+
// from exit code + stderr line.
|
|
1728
|
+
result.strict_preconditions_violated = warnIssues.map(i => ({
|
|
1729
|
+
id: i.id, kind: i.kind, message: i.message || null, on_fail: i.on_fail || null,
|
|
1730
|
+
}));
|
|
1681
1731
|
process.stderr.write(`[exceptd run] --strict-preconditions: ${warnIssues.length} unverified/warn precondition(s) — exit 1.\n`);
|
|
1682
1732
|
emit(result, pretty);
|
|
1683
1733
|
// v0.11.11: exitCode + return so emit()'s stdout flushes (process.exit
|
|
@@ -1922,13 +1972,28 @@ function cmdRunMulti(runner, ids, args, runOpts, pretty, meta) {
|
|
|
1922
1972
|
// contract in one pass.
|
|
1923
1973
|
if (args["evidence-dir"]) {
|
|
1924
1974
|
const dir = args["evidence-dir"];
|
|
1975
|
+
if (typeof dir !== "string" || dir.length === 0) {
|
|
1976
|
+
return emitError("run: --evidence-dir must be a non-empty string.", null, pretty);
|
|
1977
|
+
}
|
|
1925
1978
|
if (!fs.existsSync(dir)) {
|
|
1926
1979
|
return emitError(`run: --evidence-dir ${dir} does not exist.`, null, pretty);
|
|
1927
1980
|
}
|
|
1981
|
+
const resolvedDir = path.resolve(dir);
|
|
1982
|
+
// v0.12.12: only `<playbook-id>.json` entries are honored. Reject
|
|
1983
|
+
// anything where the filename strip leaves traversal segments — npm
|
|
1984
|
+
// refuses to write such filenames so the realistic risk is an operator
|
|
1985
|
+
// symlink/junction inside the dir, but the filter is cheap.
|
|
1928
1986
|
for (const f of fs.readdirSync(dir).filter(x => x.endsWith(".json"))) {
|
|
1929
1987
|
const pbId = f.replace(/\.json$/, "");
|
|
1988
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(pbId)) {
|
|
1989
|
+
return emitError(`run: --evidence-dir entry ${JSON.stringify(f)} has unsafe playbook-id segment.`, null, pretty);
|
|
1990
|
+
}
|
|
1991
|
+
const entryPath = path.resolve(path.join(resolvedDir, f));
|
|
1992
|
+
if (!entryPath.startsWith(resolvedDir + path.sep)) {
|
|
1993
|
+
return emitError(`run: --evidence-dir entry ${f} resolves outside the directory; refusing.`, null, pretty);
|
|
1994
|
+
}
|
|
1930
1995
|
try {
|
|
1931
|
-
bundle[pbId] = JSON.parse(fs.readFileSync(
|
|
1996
|
+
bundle[pbId] = JSON.parse(fs.readFileSync(entryPath, "utf8"));
|
|
1932
1997
|
} catch (e) {
|
|
1933
1998
|
return emitError(`run: failed to parse --evidence-dir entry ${f}: ${e.message}`, null, pretty);
|
|
1934
1999
|
}
|
|
@@ -2128,47 +2193,86 @@ function deriveRunTag() {
|
|
|
2128
2193
|
function persistAttestation(args) {
|
|
2129
2194
|
const { sessionId, playbookId, directiveId, evidenceHash, operator,
|
|
2130
2195
|
operatorConsent, submission, runOpts, forceOverwrite, filename } = args;
|
|
2196
|
+
// v0.12.12: session-id is supposed to be sanitized at input. Defense in
|
|
2197
|
+
// depth: reject anything that path-traverses out of the attestation root.
|
|
2198
|
+
if (!/^[A-Za-z0-9._-]{1,64}$/.test(sessionId || "")) {
|
|
2199
|
+
return {
|
|
2200
|
+
ok: false,
|
|
2201
|
+
error: `Refusing to persist attestation with unsafe session-id: ${JSON.stringify(sessionId).slice(0, 80)}. Must match /^[A-Za-z0-9._-]{1,64}$/.`,
|
|
2202
|
+
existingPath: null,
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
if (!/^[A-Za-z0-9._-]{1,64}\.json$/.test(filename || "")) {
|
|
2206
|
+
return {
|
|
2207
|
+
ok: false,
|
|
2208
|
+
error: `Refusing to persist attestation with unsafe filename: ${JSON.stringify(filename).slice(0, 80)}.`,
|
|
2209
|
+
existingPath: null,
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2131
2212
|
const root = resolveAttestationRoot(runOpts);
|
|
2132
2213
|
const dir = path.join(root, sessionId);
|
|
2133
2214
|
const filePath = path.join(dir, filename);
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
if (
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
existingPath: path.relative(process.cwd(), filePath),
|
|
2143
|
-
};
|
|
2144
|
-
}
|
|
2215
|
+
// Final-resolution check: dir must remain inside root after normalization.
|
|
2216
|
+
const normRoot = path.resolve(root) + path.sep;
|
|
2217
|
+
if (!(path.resolve(dir) + path.sep).startsWith(normRoot)) {
|
|
2218
|
+
return {
|
|
2219
|
+
ok: false,
|
|
2220
|
+
error: `Refusing to persist attestation outside root. session_id=${sessionId} root=${root}`,
|
|
2221
|
+
existingPath: null,
|
|
2222
|
+
};
|
|
2145
2223
|
}
|
|
2146
2224
|
|
|
2147
2225
|
try {
|
|
2148
2226
|
fs.mkdirSync(dir, { recursive: true });
|
|
2149
|
-
const
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2227
|
+
const writeAttestation = (priorEvidenceHash, priorCapturedAt, flag) => {
|
|
2228
|
+
const attestation = {
|
|
2229
|
+
session_id: sessionId,
|
|
2230
|
+
playbook_id: playbookId,
|
|
2231
|
+
directive_id: directiveId,
|
|
2232
|
+
evidence_hash: evidenceHash,
|
|
2233
|
+
operator: operator || null,
|
|
2234
|
+
operator_consent: operatorConsent || null,
|
|
2235
|
+
submission,
|
|
2236
|
+
run_opts: { airGap: runOpts.airGap, forceStale: runOpts.forceStale, mode: runOpts.mode },
|
|
2237
|
+
captured_at: new Date().toISOString(),
|
|
2238
|
+
// When overwriting (with --force-overwrite), link to the prior content
|
|
2239
|
+
// by evidence_hash + capture timestamp. session_id is the same (that's
|
|
2240
|
+
// why we collided), so it's the hash + timestamp that distinguish.
|
|
2241
|
+
prior_evidence_hash: priorEvidenceHash,
|
|
2242
|
+
prior_captured_at: priorCapturedAt,
|
|
2243
|
+
};
|
|
2244
|
+
// Atomic-create via O_EXCL ('wx' flag) eliminates the TOCTOU window
|
|
2245
|
+
// between existsSync and writeFileSync. Two concurrent run-with-same-
|
|
2246
|
+
// session-id invocations now produce one winner + one EEXIST loser,
|
|
2247
|
+
// not silent last-write-wins.
|
|
2248
|
+
fs.writeFileSync(filePath, JSON.stringify(attestation, null, 2), { flag });
|
|
2249
|
+
maybeSignAttestation(filePath);
|
|
2171
2250
|
};
|
|
2251
|
+
|
|
2252
|
+
try {
|
|
2253
|
+
writeAttestation(null, null, "wx");
|
|
2254
|
+
return { ok: true, prior_session_id: null, overwrote_at: null };
|
|
2255
|
+
} catch (eExcl) {
|
|
2256
|
+
if (eExcl.code !== "EEXIST") throw eExcl;
|
|
2257
|
+
// Slot already taken — read prior to chain audit trail, then decide.
|
|
2258
|
+
let prior = null;
|
|
2259
|
+
try { prior = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { /* malformed prior — proceed */ }
|
|
2260
|
+
if (!forceOverwrite) {
|
|
2261
|
+
return {
|
|
2262
|
+
ok: false,
|
|
2263
|
+
error: `Attestation already exists at ${path.relative(process.cwd(), filePath)}. Session-id collision (${sessionId}) — refusing to overwrite to preserve audit trail.`,
|
|
2264
|
+
existingPath: path.relative(process.cwd(), filePath),
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
writeAttestation(prior ? (prior.evidence_hash || null) : null,
|
|
2268
|
+
prior ? (prior.captured_at || null) : null,
|
|
2269
|
+
"w");
|
|
2270
|
+
return {
|
|
2271
|
+
ok: true,
|
|
2272
|
+
prior_session_id: prior ? sessionId : null,
|
|
2273
|
+
overwrote_at: prior ? prior.captured_at : null,
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2172
2276
|
} catch (e) {
|
|
2173
2277
|
return { ok: false, error: `Failed to write attestation: ${e.message}`, existingPath: null };
|
|
2174
2278
|
}
|
|
@@ -3367,8 +3471,14 @@ function cmdAiRun(runner, args, runOpts, pretty) {
|
|
|
3367
3471
|
directPhase = runner.direct(playbookId, directiveId);
|
|
3368
3472
|
lookPhase = runner.look(playbookId, directiveId, runOpts);
|
|
3369
3473
|
} catch (e) {
|
|
3370
|
-
|
|
3371
|
-
|
|
3474
|
+
// v0.12.12 (T8): process.exit(1) immediately after a stdout write can
|
|
3475
|
+
// truncate buffered output under piped consumers (same class as v0.11.10
|
|
3476
|
+
// #100). Use exitCode+return so the JSONL error frame drains. Also write
|
|
3477
|
+
// the framed error event so the stdout-only JSONL contract holds — host
|
|
3478
|
+
// AIs reading this stream must see structured frames, never bare text.
|
|
3479
|
+
process.stdout.write(JSON.stringify({ event: "error", reason: e.message, phase: "info", playbook_id: playbookId, directive_id: directiveId }) + "\n");
|
|
3480
|
+
process.exitCode = 1;
|
|
3481
|
+
return;
|
|
3372
3482
|
}
|
|
3373
3483
|
|
|
3374
3484
|
const governEvent = {
|
|
@@ -3444,8 +3554,11 @@ function cmdAiRun(runner, args, runOpts, pretty) {
|
|
|
3444
3554
|
return emitError(`ai-run: runner threw: ${e.message}`, { playbook: playbookId }, pretty);
|
|
3445
3555
|
}
|
|
3446
3556
|
if (!result || result.ok === false) {
|
|
3557
|
+
// v0.12.12: same exit-after-write anti-pattern as the pre-stream
|
|
3558
|
+
// load path. Use exitCode + return so stderr drains.
|
|
3447
3559
|
process.stderr.write((pretty ? JSON.stringify(result || {}, null, 2) : JSON.stringify(result || {})) + "\n");
|
|
3448
|
-
process.
|
|
3560
|
+
process.exitCode = 1;
|
|
3561
|
+
return;
|
|
3449
3562
|
}
|
|
3450
3563
|
// v0.11.8 (#101): unify ai-run --no-stream shape with `run`. Pre-0.11.8
|
|
3451
3564
|
// ai-run flattened phases to top-level (`govern`, `direct`, `look`, ...),
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-14T14:28:45.659Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
|
-
"source_count":
|
|
5
|
+
"source_count": 50,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "a3c012232fd18e4a2186bf3243fb969bb411e6815d170f568e42867ce7c6c308",
|
|
8
8
|
"data/atlas-ttps.json": "f3f75ff2778a0a2c7d953a21386bc4f265cb2685ce41242eee45f9e9f2a6add6",
|
|
9
|
-
"data/
|
|
10
|
-
"data/
|
|
9
|
+
"data/attack-techniques.json": "b6dde8f2d8bbe809cbd017d1490b16c01cc54034d695bc8535613b699e3b45c6",
|
|
10
|
+
"data/cve-catalog.json": "197f5313d93f0a7225d5ff275e21cbd067b3970a6f2fdc6da35f81c847e8bdee",
|
|
11
|
+
"data/cwe-catalog.json": "19ce1fad3ed0b0687ec9a328b2d6cd1b544eea7f19140234ec1a8467de1f908d",
|
|
11
12
|
"data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
|
|
12
13
|
"data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
|
|
13
14
|
"data/exploit-availability.json": "7dad52f459c324c40aa4df7cd9157f6a19f670fdfb9d8f687d777c9d99798668",
|
|
14
|
-
"data/framework-control-gaps.json": "
|
|
15
|
+
"data/framework-control-gaps.json": "9240ea4a825090fe2716947f2f6f9171c065a133ef003e04d2fbc4f01fc55bdf",
|
|
15
16
|
"data/global-frameworks.json": "84fd19061f052e4ccf66308a7b8d3fd38e00325e97e9e5e19e4d9b302c128957",
|
|
16
17
|
"data/rfc-references.json": "583360bae01e324d752bd28a7d344b4276478381426428d683fc82b0ac19d64a",
|
|
17
18
|
"data/zeroday-lessons.json": "d670e73dfd5237ceb71a56326676d90c05387b9547f8ed6f3a60a153854b444b",
|
|
@@ -55,7 +56,7 @@
|
|
|
55
56
|
"skills/age-gates-child-safety/skill.md": "c741d7dca9da0abb09bdebb8a02e803ce4ae9fb9a6904fb8df3ec19cae83917d"
|
|
56
57
|
},
|
|
57
58
|
"skill_count": 38,
|
|
58
|
-
"catalog_count":
|
|
59
|
+
"catalog_count": 11,
|
|
59
60
|
"index_stats": {
|
|
60
61
|
"xref_entries": {
|
|
61
62
|
"cwe_refs": 34,
|
|
@@ -80,8 +81,8 @@
|
|
|
80
81
|
"theater_fingerprints": 7,
|
|
81
82
|
"currency_action_required": 0,
|
|
82
83
|
"frequency_fields": 7,
|
|
83
|
-
"activity_feed_events":
|
|
84
|
-
"catalog_summaries":
|
|
84
|
+
"activity_feed_events": 50,
|
|
85
|
+
"catalog_summaries": 11,
|
|
85
86
|
"stale_content_findings": 0
|
|
86
87
|
},
|
|
87
88
|
"invalidation_note": "If any source file in source_hashes has a different SHA-256 than recorded here, the indexes are stale. Re-run `npm run build-indexes`."
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"_meta": {
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
4
|
"note": "Per-artifact 'last changed' feed sorted descending by date. Skill events from manifest.last_threat_review; catalog events from data/<catalog>.json _meta.last_updated.",
|
|
5
|
-
"event_count":
|
|
5
|
+
"event_count": 50
|
|
6
6
|
},
|
|
7
7
|
"events": [
|
|
8
8
|
{
|
|
@@ -13,6 +13,14 @@
|
|
|
13
13
|
"schema_version": "1.0.0",
|
|
14
14
|
"entry_count": 15
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
"date": "2026-05-13",
|
|
18
|
+
"type": "catalog_update",
|
|
19
|
+
"artifact": "data/attack-techniques.json",
|
|
20
|
+
"path": "data/attack-techniques.json",
|
|
21
|
+
"schema_version": "1.0.0",
|
|
22
|
+
"entry_count": 75
|
|
23
|
+
},
|
|
16
24
|
{
|
|
17
25
|
"date": "2026-05-13",
|
|
18
26
|
"type": "catalog_update",
|
|
@@ -349,14 +357,14 @@
|
|
|
349
357
|
"artifact": "data/framework-control-gaps.json",
|
|
350
358
|
"path": "data/framework-control-gaps.json",
|
|
351
359
|
"schema_version": "1.0.0",
|
|
352
|
-
"entry_count":
|
|
360
|
+
"entry_count": 61
|
|
353
361
|
},
|
|
354
362
|
{
|
|
355
363
|
"date": "2026-05-01",
|
|
356
364
|
"type": "manifest_review",
|
|
357
365
|
"artifact": "manifest.json",
|
|
358
366
|
"path": "manifest.json",
|
|
359
|
-
"note": "manifest threat_review_date — 38 skills,
|
|
367
|
+
"note": "manifest threat_review_date — 38 skills, 11 catalogs"
|
|
360
368
|
}
|
|
361
369
|
]
|
|
362
370
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"_meta": {
|
|
3
3
|
"schema_version": "1.0.0",
|
|
4
4
|
"note": "Per-catalog compact summary so AI consumers can discover available data without loading every _meta block. Purpose strings are curated in scripts/builders/catalog-summaries.js.",
|
|
5
|
-
"catalog_count":
|
|
5
|
+
"catalog_count": 11
|
|
6
6
|
},
|
|
7
7
|
"catalogs": {
|
|
8
8
|
"atlas-ttps.json": {
|
|
@@ -27,6 +27,28 @@
|
|
|
27
27
|
"AML.T0018"
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
|
+
"attack-techniques.json": {
|
|
31
|
+
"path": "data/attack-techniques.json",
|
|
32
|
+
"purpose": null,
|
|
33
|
+
"schema_version": "1.0.0",
|
|
34
|
+
"last_updated": "2026-05-13",
|
|
35
|
+
"tlp": "CLEAR",
|
|
36
|
+
"source_confidence_default": "A1",
|
|
37
|
+
"freshness_policy": {
|
|
38
|
+
"default_review_cadence_days": 90,
|
|
39
|
+
"stale_after_days": 180,
|
|
40
|
+
"rebuild_after_days": 365,
|
|
41
|
+
"note": "Catalog must be rebuilt against the upstream ATT&CK release whenever MITRE publishes a new version. AGENTS.md hard rule #8 requires the bump to be intentional, not silent."
|
|
42
|
+
},
|
|
43
|
+
"entry_count": 75,
|
|
44
|
+
"sample_keys": [
|
|
45
|
+
"T0001",
|
|
46
|
+
"T0017",
|
|
47
|
+
"T0051",
|
|
48
|
+
"T0096",
|
|
49
|
+
"T0853"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
30
52
|
"cve-catalog.json": {
|
|
31
53
|
"path": "data/cve-catalog.json",
|
|
32
54
|
"purpose": "Per-CVE record (CVSS, EPSS, CISA KEV, RWEP, AI-discovery, vendor advisories, framework gaps, ATLAS/ATT&CK mappings). Cross-validated against NVD + CISA KEV + FIRST EPSS via validate-cves.",
|
|
@@ -150,7 +172,7 @@
|
|
|
150
172
|
"rebuild_after_days": 365,
|
|
151
173
|
"note": "Per-entry last_verified governs decay. Skills depending on this catalog must check entry freshness before high-stakes use."
|
|
152
174
|
},
|
|
153
|
-
"entry_count":
|
|
175
|
+
"entry_count": 61,
|
|
154
176
|
"sample_keys": [
|
|
155
177
|
"NIST-800-53-SI-2",
|
|
156
178
|
"NIST-800-53-SC-8",
|