@blamejs/exceptd-skills 0.12.11 → 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 +93 -0
- package/bin/exceptd.js +152 -39
- package/data/_indexes/_meta.json +7 -6
- package/data/_indexes/activity-feed.json +10 -2
- package/data/_indexes/catalog-summaries.json +23 -1
- package/data/attack-techniques.json +96 -0
- 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 +221 -73
- 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/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,98 @@
|
|
|
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
|
+
|
|
3
96
|
## 0.12.11 — 2026-05-13
|
|
4
97
|
|
|
5
98
|
**Patch: OSV source hardening, indicator regex widening, CWE/framework-gap reconciliation. v0.12.10 audit closeout.**
|
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,11 +1,12 @@
|
|
|
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/attack-techniques.json": "b6dde8f2d8bbe809cbd017d1490b16c01cc54034d695bc8535613b699e3b45c6",
|
|
9
10
|
"data/cve-catalog.json": "197f5313d93f0a7225d5ff275e21cbd067b3970a6f2fdc6da35f81c847e8bdee",
|
|
10
11
|
"data/cwe-catalog.json": "19ce1fad3ed0b0687ec9a328b2d6cd1b544eea7f19140234ec1a8467de1f908d",
|
|
11
12
|
"data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
|
|
@@ -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",
|
|
@@ -356,7 +364,7 @@
|
|
|
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.",
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"schema_version": "1.0.0",
|
|
4
|
+
"last_updated": "2026-05-13",
|
|
5
|
+
"attack_version": "v17",
|
|
6
|
+
"attack_version_date": "2025-06-25",
|
|
7
|
+
"source": "https://attack.mitre.org — MITRE ATT&CK Enterprise + ICS. Only techniques currently referenced by shipped exceptd skills and playbooks. The full ATT&CK matrix (~700 techniques) is intentionally not duplicated here; this is a resolution catalog for cross-reference validation, not a substitute for attack.mitre.org. See `npm run refresh-attack-techniques` (v0.13.0+) for the full corpus.",
|
|
8
|
+
"tlp": "CLEAR",
|
|
9
|
+
"source_confidence": {
|
|
10
|
+
"scheme": "Admiralty (A-F + 1-6)",
|
|
11
|
+
"default": "A1",
|
|
12
|
+
"note": "A1 (completely reliable, confirmed) — MITRE ATT&CK is a public reference catalog. Per-entry overrides are not currently used; if an entry's mapping is uncertain it is left out of the catalog rather than carried with reduced confidence."
|
|
13
|
+
},
|
|
14
|
+
"freshness_policy": {
|
|
15
|
+
"default_review_cadence_days": 90,
|
|
16
|
+
"stale_after_days": 180,
|
|
17
|
+
"rebuild_after_days": 365,
|
|
18
|
+
"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."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"T0001": { "name": "Authority Spoof", "version": "v17" },
|
|
22
|
+
"T0017": { "name": "Spearphishing Attachment (ICS)", "version": "v17" },
|
|
23
|
+
"T0051": { "name": "Position Tampering", "version": "v17" },
|
|
24
|
+
"T0096": { "name": "Remote System Discovery (ICS)", "version": "v17" },
|
|
25
|
+
"T0853": { "name": "Scripting", "version": "v17" },
|
|
26
|
+
"T0855": { "name": "Unauthorized Command Message", "version": "v17" },
|
|
27
|
+
"T0867": { "name": "Lateral Tool Transfer", "version": "v17" },
|
|
28
|
+
"T0883": { "name": "Internet Accessible Device", "version": "v17" },
|
|
29
|
+
"T1021": { "name": "Remote Services", "version": "v17" },
|
|
30
|
+
"T1027": { "name": "Obfuscated Files or Information", "version": "v17" },
|
|
31
|
+
"T1040": { "name": "Network Sniffing", "version": "v17" },
|
|
32
|
+
"T1041": { "name": "Exfiltration Over C2 Channel", "version": "v17" },
|
|
33
|
+
"T1053.003": { "name": "Scheduled Task/Job: Cron", "version": "v17" },
|
|
34
|
+
"T1055": { "name": "Process Injection", "version": "v17" },
|
|
35
|
+
"T1059": { "name": "Command and Scripting Interpreter", "version": "v17" },
|
|
36
|
+
"T1068": { "name": "Exploitation for Privilege Escalation", "version": "v17" },
|
|
37
|
+
"T1071": { "name": "Application Layer Protocol", "version": "v17" },
|
|
38
|
+
"T1078": { "name": "Valid Accounts", "version": "v17" },
|
|
39
|
+
"T1078.002": { "name": "Valid Accounts: Domain Accounts", "version": "v17" },
|
|
40
|
+
"T1078.003": { "name": "Valid Accounts: Local Accounts", "version": "v17" },
|
|
41
|
+
"T1078.004": { "name": "Valid Accounts: Cloud Accounts", "version": "v17" },
|
|
42
|
+
"T1098": { "name": "Account Manipulation", "version": "v17" },
|
|
43
|
+
"T1102": { "name": "Web Service", "version": "v17" },
|
|
44
|
+
"T1110": { "name": "Brute Force", "version": "v17" },
|
|
45
|
+
"T1110.001": { "name": "Brute Force: Password Guessing", "version": "v17" },
|
|
46
|
+
"T1133": { "name": "External Remote Services", "version": "v17" },
|
|
47
|
+
"T1136.001": { "name": "Create Account: Local Account", "version": "v17" },
|
|
48
|
+
"T1190": { "name": "Exploit Public-Facing Application", "version": "v17" },
|
|
49
|
+
"T1195": { "name": "Supply Chain Compromise", "version": "v17" },
|
|
50
|
+
"T1195.001": { "name": "Supply Chain Compromise: Software Dependencies and Development Tools", "version": "v17" },
|
|
51
|
+
"T1195.002": { "name": "Supply Chain Compromise: Software Supply Chain", "version": "v17" },
|
|
52
|
+
"T1199": { "name": "Trusted Relationship", "version": "v17" },
|
|
53
|
+
"T1203": { "name": "Exploitation for Client Execution", "version": "v17" },
|
|
54
|
+
"T1212": { "name": "Exploitation for Credential Access", "version": "v17" },
|
|
55
|
+
"T1213": { "name": "Data from Information Repositories", "version": "v17" },
|
|
56
|
+
"T1485": { "name": "Data Destruction", "version": "v17" },
|
|
57
|
+
"T1486": { "name": "Data Encrypted for Impact", "version": "v17" },
|
|
58
|
+
"T1505": { "name": "Server Software Component", "version": "v17" },
|
|
59
|
+
"T1518": { "name": "Software Discovery", "version": "v17" },
|
|
60
|
+
"T1525": { "name": "Implant Internal Image", "version": "v17" },
|
|
61
|
+
"T1528": { "name": "Steal Application Access Token", "version": "v17" },
|
|
62
|
+
"T1530": { "name": "Data from Cloud Storage", "version": "v17" },
|
|
63
|
+
"T1543": { "name": "Create or Modify System Process", "version": "v17" },
|
|
64
|
+
"T1546": { "name": "Event Triggered Execution", "version": "v17" },
|
|
65
|
+
"T1547": { "name": "Boot or Logon Autostart Execution", "version": "v17" },
|
|
66
|
+
"T1548.001": { "name": "Abuse Elevation Control Mechanism: Setuid and Setgid", "version": "v17" },
|
|
67
|
+
"T1548.003": { "name": "Abuse Elevation Control Mechanism: Sudo and Sudo Caching", "version": "v17" },
|
|
68
|
+
"T1552": { "name": "Unsecured Credentials", "version": "v17" },
|
|
69
|
+
"T1552.001": { "name": "Unsecured Credentials: Credentials In Files", "version": "v17" },
|
|
70
|
+
"T1552.004": { "name": "Unsecured Credentials: Private Keys", "version": "v17" },
|
|
71
|
+
"T1552.005": { "name": "Unsecured Credentials: Cloud Instance Metadata API", "version": "v17" },
|
|
72
|
+
"T1552.007": { "name": "Unsecured Credentials: Container API", "version": "v17" },
|
|
73
|
+
"T1554": { "name": "Compromise Host Software Binary", "version": "v17" },
|
|
74
|
+
"T1555": { "name": "Credentials from Password Stores", "version": "v17" },
|
|
75
|
+
"T1556": { "name": "Modify Authentication Process", "version": "v17" },
|
|
76
|
+
"T1557": { "name": "Adversary-in-the-Middle", "version": "v17" },
|
|
77
|
+
"T1562.001": { "name": "Impair Defenses: Disable or Modify Tools", "version": "v17" },
|
|
78
|
+
"T1562.006": { "name": "Impair Defenses: Indicator Blocking", "version": "v17" },
|
|
79
|
+
"T1565": { "name": "Data Manipulation", "version": "v17" },
|
|
80
|
+
"T1566": { "name": "Phishing", "version": "v17" },
|
|
81
|
+
"T1566.001": { "name": "Phishing: Spearphishing Attachment", "version": "v17" },
|
|
82
|
+
"T1566.002": { "name": "Phishing: Spearphishing Link", "version": "v17" },
|
|
83
|
+
"T1566.003": { "name": "Phishing: Spearphishing via Service", "version": "v17" },
|
|
84
|
+
"T1567": { "name": "Exfiltration Over Web Service", "version": "v17" },
|
|
85
|
+
"T1568": { "name": "Dynamic Resolution", "version": "v17" },
|
|
86
|
+
"T1570": { "name": "Lateral Tool Transfer", "version": "v17" },
|
|
87
|
+
"T1573": { "name": "Encrypted Channel", "version": "v17" },
|
|
88
|
+
"T1574": { "name": "Hijack Execution Flow", "version": "v17" },
|
|
89
|
+
"T1574.005": { "name": "Hijack Execution Flow: Executable Installer File Permissions Weakness", "version": "v17" },
|
|
90
|
+
"T1595": { "name": "Active Scanning", "version": "v17" },
|
|
91
|
+
"T1600": { "name": "Weaken Encryption", "version": "v17" },
|
|
92
|
+
"T1606.001": { "name": "Forge Web Credentials: Web Cookies", "version": "v17" },
|
|
93
|
+
"T1610": { "name": "Deploy Container", "version": "v17" },
|
|
94
|
+
"T1611": { "name": "Escape to Host", "version": "v17" },
|
|
95
|
+
"T1613": { "name": "Container and Resource Discovery", "version": "v17" }
|
|
96
|
+
}
|