@blamejs/exceptd-skills 0.13.20 → 0.13.22
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 +47 -0
- package/bin/exceptd.js +140 -1
- package/data/_indexes/_meta.json +3 -3
- package/data/attack-techniques.json +2 -3
- package/lib/gap-detectors.js +555 -0
- package/lib/playbook-runner.js +77 -1
- package/manifest.json +44 -44
- package/package.json +4 -3
- package/sbom.cdx.json +53 -23
- package/scripts/audit-catalog-gaps.js +63 -11
- package/scripts/check-catalog-gap-budget.js +133 -0
- package/scripts/predeploy.js +14 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.22 — 2026-05-19
|
|
4
|
+
|
|
5
|
+
`ci` is now usable at the terminal without piping through `jq`.
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
- **Human-readable `ci` output by default.** Pre-0.13.22 the default `ci` output was 1000+ lines of indented JSON on every run. Now the default is a one-screen digest: verdict line, per-playbook table (id / verdict / rwep / evidence-completeness / top-finding), session-level warnings, scope-selection rules, framework gap rollup, and fail reasons. Pass `--json` or `--pretty` to get the structured body for automation.
|
|
10
|
+
- **Per-result hoisted summary fields.** Every `run()` result now carries `verdict`, `rwep_score`, `top_finding`, `summary_line`, and `evidence_completeness` (one of `complete` / `partial` / `missing` / `unknown` / `not-evaluated`) at the top level. Machine-readable consumers no longer walk `phases.analyze.rwep.adjusted` and `phases.detect.classification` separately to extract the headline numbers.
|
|
11
|
+
- **`indicators_evaluated` + `indicators_known` per result.** Surface how many of the playbook's known indicators were actually exercised by the operator's evidence, so a result that returns `verdict=inconclusive` with `indicators_evaluated=0` is distinguishable from one that evaluated every indicator and found no hits.
|
|
12
|
+
- **Session-level warning de-duplication.** `ci` runs that span N playbooks no longer emit the same `bundle_publisher_unclaimed` warning N times. The summary now carries `runtime_warnings` and `runtime_warnings_count` with one entry per unique (kind, reason) across the session.
|
|
13
|
+
- **Scope-inclusion transparency.** When `ci --scope <type>` is used, the summary now lists `scope_request` plus `scope_inclusion_rules` explaining that cross-cutting playbooks are always added and (for `--scope code`) that `sbom` is auto-included on repos with a lockfile.
|
|
14
|
+
|
|
15
|
+
### Bugs
|
|
16
|
+
|
|
17
|
+
- **Blocked results now carry `playbook_id`.** Previously, a playbook that halted at preflight returned `{ ok:false, blocked_by, reason }` with no playbook identifier — operators iterating `results[]` for failure rows had to correlate by array index. Now every result, blocked or not, carries `playbook_id` at the top level.
|
|
18
|
+
|
|
19
|
+
## 0.13.21 — 2026-05-19
|
|
20
|
+
|
|
21
|
+
Seven new catalog-gap detection classes wired into the predeploy gate. The v0.13.19 detector covered missing-context / dangling-ref / draft-debt; the v0.13.20 audit confirmed that left genuine gap classes unsurfaced. v0.13.21 adds the seven cross-cutting classes the prior detector missed and wires them into a budget gate that runs alongside the existing tests + predeploy gates.
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
**Seven new detection classes in `lib/gap-detectors.js`:**
|
|
26
|
+
|
|
27
|
+
- **content-quality** — fields present but content weak. Catches: vector text < 50 chars (likely a stub), placeholder-language sentinels (TBD / TKTK / "pending operator curation" / "[]"), KEV-listed entries with empty vendor_advisories, name-repeated-as-description.
|
|
28
|
+
- **temporal-staleness** — time-based decay. Catches: source_verified > 180d old, last_updated > 365d, CISA-KEV due-date passed without remediation status, epss_date > 90d.
|
|
29
|
+
- **logical-consistency** — internal-state contradictions that pass schema validation but don't make sense. Catches: `cisa_kev:true + cisa_kev_date:null`, `live_patch_available:true + live_patch_tools:[]`, `ai_discovered:true + attribution_note < 30 chars`, `active_exploitation:"confirmed" + verification_sources.length < 2`, `rwep_score declared + rwep_factors empty`.
|
|
30
|
+
- **cross-ref-completeness** — bidirectional reference checks. The v0.13.19 dangling-ref class only verified the forward direction (CVE→CWE resolves); v0.13.21 also verifies the back-reference is present (CWE.evidence_cves includes the citing CVE). Same logic for ATT&CK.cve_refs and framework-control-gaps.evidence_cves.
|
|
31
|
+
- **schema-evolution** — required-since-version checks. Fields the schema requires today were optional on entries added in older releases. Surfaces pre-existing entries the operator should backfill (e.g. pre-v0.12.36 CVEs lacking the `ai_discovered` boolean).
|
|
32
|
+
- **operator-action-sla** — un-curated auto-imports older than the SLA window. Defaults: 60d for `_auto_imported`, 90d for `_draft`.
|
|
33
|
+
- **unused-orphan** — auto-imported catalog entries that no skill / playbook / CVE references. Operator-curated entries are exempt (intentional content); `forward_looking:true` entries are exempt (intentional forward-look content).
|
|
34
|
+
|
|
35
|
+
**`scripts/check-catalog-gap-budget.js` + predeploy gate.** New predeploy gate runs the seven extended detectors and asserts every class is within its documented budget. Mirrors the budget enforced by `tests/shipped-catalog-integrity.test.js` so a regression surfaces in BOTH the gate-summary table AND the test output. Predeploy summary now reports 16 gates (was 15).
|
|
36
|
+
|
|
37
|
+
**`tests/gap-detectors.test.js`** — 22 per-detector tests pin each of the seven classes against synthetic catalog inputs. Each pin asserts the detector fires on the shape it's designed to catch AND does NOT fire on the inverse shape (no false positives).
|
|
38
|
+
|
|
39
|
+
### Bugs
|
|
40
|
+
|
|
41
|
+
**T1574 ATT&CK back-ref synced.** v0.13.20's CTFMON-mapping fix added T1574 to `BUG-2026-NIGHTMARE-ECLIPSE-GREENPLASMA.attack_refs[]`, but the reverse-refs pass didn't run before sign + sbom + commit, so `attack-techniques.T1574.cve_refs[]` didn't pick up the back-ref. The v0.13.21 cross-ref-completeness detector surfaced this on first run — fixed via `npm run refresh-reverse-refs`.
|
|
42
|
+
|
|
43
|
+
### Internal
|
|
44
|
+
|
|
45
|
+
- `scripts/audit-catalog-gaps.js` CLI extended: `--class <name>` accepts the seven new class names (`content-quality`, `temporal-staleness`, `logical-consistency`, `cross-ref-completeness`, `schema-evolution`, `operator-action-sla`, `unused-orphan`) for scoped audits. JSON + pretty output include an `extended_findings` section grouped by class, with `totals.extended` counts.
|
|
46
|
+
- `tests/shipped-catalog-integrity.test.js` includes a new budget pin for the seven extended classes — a future PR that worsens any class beyond budget fires the test.
|
|
47
|
+
- npm alias: `npm run audit-catalog-gap-budget` runs the budget gate standalone (operator-facing convenience).
|
|
48
|
+
- Current shipped-catalog snapshot: content-quality=10, temporal-staleness=255, logical-consistency=0, cross-ref-completeness=0, schema-evolution=0, operator-action-sla=0, unused-orphan=1342. The non-zero classes are operator-curation work items surfaced honestly by the new detectors.
|
|
49
|
+
|
|
3
50
|
## 0.13.20 — 2026-05-19
|
|
4
51
|
|
|
5
52
|
Root-cause refactor addressing every audit class surfaced by the v0.13.17–v0.13.19 self-audit (no-MVP violations, regex-where-logic-is-required, symptom patches, coincidence-pinning tests, uncaught bugs). The audit found I had been patching symptoms instead of fixing root causes; v0.13.20 fixes the actual issues and lets the audit tell the truth about catalog state.
|
package/bin/exceptd.js
CHANGED
|
@@ -6814,6 +6814,46 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
6814
6814
|
clock_started_reasons: clockStartedReasons,
|
|
6815
6815
|
};
|
|
6816
6816
|
|
|
6817
|
+
// v0.13.22 B5: Each `run()` call independently surfaces session-level
|
|
6818
|
+
// runtime conditions (e.g. bundle_publisher_unclaimed) into its own
|
|
6819
|
+
// phases.analyze.runtime_errors. On a ci run that spans 9 playbooks, the
|
|
6820
|
+
// operator saw the same warning 9 times. Dedupe across results by
|
|
6821
|
+
// (kind, reason) so a session-level condition surfaces once at the ci
|
|
6822
|
+
// summary level — operators read one row, not N copies.
|
|
6823
|
+
const warningSeen = new Set();
|
|
6824
|
+
const runtimeWarningsDedup = [];
|
|
6825
|
+
for (const r of results) {
|
|
6826
|
+
const errs = r?.phases?.analyze?.runtime_errors || [];
|
|
6827
|
+
for (const e of errs) {
|
|
6828
|
+
const key = `${e.kind || ""}::${e.reason || ""}`;
|
|
6829
|
+
if (warningSeen.has(key)) continue;
|
|
6830
|
+
warningSeen.add(key);
|
|
6831
|
+
runtimeWarningsDedup.push({
|
|
6832
|
+
kind: e.kind || null,
|
|
6833
|
+
reason: e.reason || null,
|
|
6834
|
+
remediation: e.remediation || null,
|
|
6835
|
+
});
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
summary.runtime_warnings = runtimeWarningsDedup;
|
|
6839
|
+
summary.runtime_warnings_count = runtimeWarningsDedup.length;
|
|
6840
|
+
|
|
6841
|
+
// v0.13.22 B8 (transparency): document why each playbook was selected.
|
|
6842
|
+
// --scope <s> always adds cross-cutting; --scope code on a repo with a
|
|
6843
|
+
// lockfile also adds sbom. The selection rule was buried in code; surface
|
|
6844
|
+
// it in the summary so operators reading the JSON / pretty trailer can
|
|
6845
|
+
// see what was scoped vs. auto-included.
|
|
6846
|
+
if (scope) {
|
|
6847
|
+
summary.scope_request = scope;
|
|
6848
|
+
summary.scope_inclusion_rules = [
|
|
6849
|
+
`--scope ${scope} selected playbooks with _meta.scope === "${scope}"`,
|
|
6850
|
+
`cross-cutting playbooks are always added (apply to every scope by design)`,
|
|
6851
|
+
];
|
|
6852
|
+
if (scope === "code") {
|
|
6853
|
+
summary.scope_inclusion_rules.push("--scope code also adds sbom when the cwd is a git repo with a lockfile");
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
|
|
6817
6857
|
// v0.11.4 (#72): ci --format <fmt> previously emitted the full bundle
|
|
6818
6858
|
// regardless of flag. Now honors the same shortcuts as `run --format`:
|
|
6819
6859
|
// summary → one-line JSON of session + verdict + counts
|
|
@@ -6856,7 +6896,106 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
6856
6896
|
);
|
|
6857
6897
|
return;
|
|
6858
6898
|
} else {
|
|
6859
|
-
|
|
6899
|
+
// v0.13.22 B3+B4+B6: human renderer for `ci` default output. Pre-0.13.22
|
|
6900
|
+
// the only output was indented JSON (or compact JSON when not TTY) —
|
|
6901
|
+
// operators running `exceptd ci` at the terminal saw 1000+ lines of JSON
|
|
6902
|
+
// for a 9-playbook scan and had to grep for the verdict by hand.
|
|
6903
|
+
//
|
|
6904
|
+
// Renderer shape (one screen for a typical 9-playbook scope):
|
|
6905
|
+
// - one verdict line (PASS/FAIL/BLOCKED + counts)
|
|
6906
|
+
// - per-playbook row: id | verdict | rwep | evidence | top_finding
|
|
6907
|
+
// - deduped session-level runtime warnings (B5)
|
|
6908
|
+
// - scope inclusion rules (B8 transparency) when --scope was used
|
|
6909
|
+
// - footer pointing at --json / --format for the structured body
|
|
6910
|
+
emit({ verb: "ci", session_id: sessionId, playbooks_run: ids, summary, results }, pretty, (obj) => {
|
|
6911
|
+
const s = obj.summary;
|
|
6912
|
+
const lines = [];
|
|
6913
|
+
lines.push(`ci: ${obj.playbooks_run.length} playbook(s) session-id: ${obj.session_id}`);
|
|
6914
|
+
const verdictIcon = s.verdict === "PASS"
|
|
6915
|
+
? "[ok]"
|
|
6916
|
+
: s.verdict === "BLOCKED"
|
|
6917
|
+
? "[!! BLOCKED]"
|
|
6918
|
+
: s.verdict === "CLOCK_STARTED"
|
|
6919
|
+
? "[!! CLOCK]"
|
|
6920
|
+
: s.verdict === "NO_EVIDENCE"
|
|
6921
|
+
? "[i NO_EVIDENCE]"
|
|
6922
|
+
: "[!! FAIL]";
|
|
6923
|
+
lines.push(`\n${verdictIcon} verdict=${s.verdict} detected=${s.detected} inconclusive=${s.inconclusive} clean=${s.not_detected} blocked=${s.blocked} max_rwep=${s.max_rwep_observed}`);
|
|
6924
|
+
|
|
6925
|
+
// Per-playbook table.
|
|
6926
|
+
const rows = (obj.results || []).map(r => {
|
|
6927
|
+
if (r && r.ok === false) {
|
|
6928
|
+
return {
|
|
6929
|
+
id: r.playbook_id || "?",
|
|
6930
|
+
verdict: "blocked",
|
|
6931
|
+
rwep: "-",
|
|
6932
|
+
evidence: r.evidence_completeness || "not-evaluated",
|
|
6933
|
+
top: r.blocked_by || r.reason || r.error || "",
|
|
6934
|
+
};
|
|
6935
|
+
}
|
|
6936
|
+
const cls = r?.phases?.detect?.classification || r?.verdict || "?";
|
|
6937
|
+
return {
|
|
6938
|
+
id: r.playbook_id || "?",
|
|
6939
|
+
verdict: cls,
|
|
6940
|
+
rwep: (r?.rwep_score != null) ? String(r.rwep_score) : "-",
|
|
6941
|
+
evidence: r?.evidence_completeness || "unknown",
|
|
6942
|
+
top: r?.top_finding || "",
|
|
6943
|
+
};
|
|
6944
|
+
});
|
|
6945
|
+
const wId = Math.max(8, ...rows.map(r => r.id.length));
|
|
6946
|
+
const wV = Math.max(8, ...rows.map(r => r.verdict.length));
|
|
6947
|
+
const wR = Math.max(4, ...rows.map(r => r.rwep.length));
|
|
6948
|
+
const wE = Math.max(8, ...rows.map(r => r.evidence.length));
|
|
6949
|
+
const pad = (s, w) => (s + " ".repeat(w)).slice(0, w);
|
|
6950
|
+
lines.push("");
|
|
6951
|
+
lines.push(` ${pad("playbook", wId)} ${pad("verdict", wV)} ${pad("rwep", wR)} ${pad("evidence", wE)} finding`);
|
|
6952
|
+
lines.push(` ${"-".repeat(wId)} ${"-".repeat(wV)} ${"-".repeat(wR)} ${"-".repeat(wE)} -------`);
|
|
6953
|
+
for (const row of rows) {
|
|
6954
|
+
const finding = row.top.length > 80 ? row.top.slice(0, 77) + "..." : row.top;
|
|
6955
|
+
lines.push(` ${pad(row.id, wId)} ${pad(row.verdict, wV)} ${pad(row.rwep, wR)} ${pad(row.evidence, wE)} ${finding}`);
|
|
6956
|
+
}
|
|
6957
|
+
|
|
6958
|
+
// Session-level deduped runtime warnings (B5).
|
|
6959
|
+
if (s.runtime_warnings && s.runtime_warnings.length) {
|
|
6960
|
+
lines.push(`\nSession warnings (${s.runtime_warnings_count}):`);
|
|
6961
|
+
for (const w of s.runtime_warnings) {
|
|
6962
|
+
const reason = (w.reason || "").length > 200 ? (w.reason || "").slice(0, 197) + "..." : (w.reason || "");
|
|
6963
|
+
lines.push(` [${w.kind || "warning"}] ${reason}`);
|
|
6964
|
+
if (w.remediation) lines.push(` → ${w.remediation}`);
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
|
|
6968
|
+
// Scope inclusion (B8 transparency).
|
|
6969
|
+
if (s.scope_inclusion_rules && s.scope_inclusion_rules.length) {
|
|
6970
|
+
lines.push(`\nScope selection (${s.scope_request}):`);
|
|
6971
|
+
for (const rule of s.scope_inclusion_rules) lines.push(` - ${rule}`);
|
|
6972
|
+
}
|
|
6973
|
+
|
|
6974
|
+
// Jurisdiction clocks.
|
|
6975
|
+
if (s.jurisdiction_clocks_started > 0) {
|
|
6976
|
+
lines.push(`\nJurisdiction clocks started: ${s.jurisdiction_clocks_started}`);
|
|
6977
|
+
for (const n of (s.jurisdiction_clock_rollup || []).slice(0, 5)) {
|
|
6978
|
+
lines.push(` ${n.jurisdiction || "?"}/${n.regulation || "?"} → deadline ${n.deadline || "?"}`);
|
|
6979
|
+
}
|
|
6980
|
+
}
|
|
6981
|
+
|
|
6982
|
+
// Framework gap rollup.
|
|
6983
|
+
if (s.framework_gap_count > 0) {
|
|
6984
|
+
lines.push(`\nFramework gaps (${s.framework_gap_count}):`);
|
|
6985
|
+
for (const g of (s.framework_gap_rollup || []).slice(0, 5)) {
|
|
6986
|
+
lines.push(` ${g.framework || "?"} :: ${g.claimed_control || "?"} (${g.playbooks.length} playbook(s))`);
|
|
6987
|
+
}
|
|
6988
|
+
}
|
|
6989
|
+
|
|
6990
|
+
// Fail reasons.
|
|
6991
|
+
if (s.fail_reasons && s.fail_reasons.length) {
|
|
6992
|
+
lines.push(`\nFail reasons:`);
|
|
6993
|
+
for (const r of s.fail_reasons) lines.push(` - ${r}`);
|
|
6994
|
+
}
|
|
6995
|
+
|
|
6996
|
+
lines.push(`\nFull structured result: --json (or --pretty for indented JSON).`);
|
|
6997
|
+
return lines.join("\n");
|
|
6998
|
+
});
|
|
6860
6999
|
}
|
|
6861
7000
|
// v0.11.14 (#134): exit-code matrix with BLOCKED before FAIL.
|
|
6862
7001
|
// Pre-0.11.14 the `if (fail)` check fired first for blocked runs (because
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-19T22:22:02.165Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 54,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "068eb6c464d8999f41906bd3ea080b6b579d6dc15c3c1b05c1caf94adb72cd27",
|
|
8
8
|
"data/atlas-ttps.json": "d296c1d3e71807c9279b731f047e57796e85137f186586743a8cdad214b408f9",
|
|
9
|
-
"data/attack-techniques.json": "
|
|
9
|
+
"data/attack-techniques.json": "49b6010b317edd219def135171ea8f3b1bbf1e00e9c5a08bf7237215ff54e2c3",
|
|
10
10
|
"data/cve-catalog.json": "a09c83af3f9679a7ea73935726a1ff9de2cab94b4ab6321fc017fc147747d7c3",
|
|
11
11
|
"data/cwe-catalog.json": "c56e74b8c9290583b1d6fdd21b54bd65a254c58890c5f683379788ca7b080e9d",
|
|
12
12
|
"data/d3fend-catalog.json": "4271102f8c38999444bcd981c1cf5feb4ad09f8c0b1d9b79df3f1a82f4fb50f0",
|
|
@@ -1833,6 +1833,7 @@
|
|
|
1833
1833
|
"name": "Hijack Execution Flow",
|
|
1834
1834
|
"version": "v19",
|
|
1835
1835
|
"cve_refs": [
|
|
1836
|
+
"BUG-2026-NIGHTMARE-ECLIPSE-GREENPLASMA",
|
|
1836
1837
|
"CVE-2026-45321"
|
|
1837
1838
|
],
|
|
1838
1839
|
"description_full": "Adversaries may execute their own malicious payloads by hijacking the way operating systems run programs. Hijacking execution flow can be for the purposes of persistence, since this hijacked execution may reoccur over time. Adversaries may also use these mechanisms to elevate privileges or evade defenses, such as application control or other restrictions on execution. There are many ways an adversary may hijack the flow of execution, including by manipulating how the operating system locates programs to be executed. How the operating system locates libraries to be used by a program can also be intercepted. Locations where the operating system looks for programs/resources, such as file directories and in the case of Windows the Registry, could also be poisoned to include malicious payloads.",
|
|
@@ -5474,9 +5475,7 @@
|
|
|
5474
5475
|
"last_verified": "2026-05-19",
|
|
5475
5476
|
"_auto_imported": true,
|
|
5476
5477
|
"_intake_method": "v0.13.18-orphan-fill",
|
|
5477
|
-
"cve_refs": [
|
|
5478
|
-
"BUG-2026-NIGHTMARE-ECLIPSE-GREENPLASMA"
|
|
5479
|
-
],
|
|
5478
|
+
"cve_refs": [],
|
|
5480
5479
|
"description_full": "Adversaries may leverage the COR_PROFILER environment variable to hijack the execution flow of programs that load the .NET CLR. The COR_PROFILER is a .NET Framework feature which allows developers to specify an unmanaged (or external of .NET) profiling DLL to be loaded into each .NET process that loads the Common Language Runtime (CLR). These profilers are designed to monitor, troubleshoot, and debug managed code executed by the .NET CLR.(Citation: Microsoft Profiling Mar 2017)(Citation: Microsoft COR_PROFILER Feb 2013) The COR_PROFILER environment variable can be set at various scopes (system, user, or process) resulting in different levels of influence. System and user-wide environment variable scopes are specified in the Registry, where a [Component Object Model](https://attack.mitre.org/techniques/T1559/001) (COM) object can be registered as a profiler DLL. A process scope COR_PROFILER can also be created in-memory without modifying the Registry. Starting with .NET Framework 4, the profiling DLL does not need to be registered as long as the location of the DLL is specified in the COR_PROFILER_PATH environment variable.(Citation: Microsoft COR_PROFILER Feb 2013) Adversaries may abuse COR_PROFILER to establish persistence that executes a malicious DLL in the context of all .NET processes every time the CLR is invoked. The COR_PROFILER can also be used to elevate privileges (ex: [Bypass User Account Control](https://attack.mitre.org/techniques/T1548/002)) if the victim .NET process executes at a higher permission level, as well as to hook and impair defenses provided by .NET processes.(Citation: RedCanary Mockingbird May 2020)(Citation: Red Canary COR_PROFILER May 2020)(Citation: Almond COR_PROFILER Apr 2019)(Citation: GitHub OmerYa Invisi-Shell)(Citation: subTee .NET Profilers May 2017)",
|
|
5481
5480
|
"platforms": [
|
|
5482
5481
|
"Windows"
|