@blamejs/exceptd-skills 0.18.13 → 0.18.15
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 +20 -0
- package/bin/exceptd.js +26 -0
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/ai-api.json +2 -2
- package/data/playbooks/ai-discovered-cve-triage.json +1 -1
- package/data/playbooks/audit-log-integrity.json +3 -3
- package/data/playbooks/cicd-pipeline-compromise.json +1 -1
- package/data/playbooks/citation-hygiene.json +4 -4
- package/data/playbooks/containers.json +2 -2
- package/data/playbooks/cred-stores.json +1 -1
- package/data/playbooks/crypto-codebase.json +3 -3
- package/data/playbooks/crypto.json +1 -1
- package/data/playbooks/decompression-dos.json +3 -3
- package/data/playbooks/framework.json +1 -1
- package/data/playbooks/hardening.json +1 -1
- package/data/playbooks/identity-sso-compromise.json +1 -1
- package/data/playbooks/idp-incident.json +1 -1
- package/data/playbooks/kernel.json +2 -2
- package/data/playbooks/library-author.json +1 -1
- package/data/playbooks/llm-tool-use-exfil.json +1 -1
- package/data/playbooks/log-injection-telemetry.json +3 -3
- package/data/playbooks/mail-server-hardening.json +3 -3
- package/data/playbooks/mcp.json +3 -3
- package/data/playbooks/multitenancy-isolation.json +3 -3
- package/data/playbooks/network-trust.json +3 -3
- package/data/playbooks/post-quantum-migration.json +1 -1
- package/data/playbooks/privacy-consent-ops.json +3 -3
- package/data/playbooks/runtime.json +2 -2
- package/data/playbooks/sbom.json +1 -1
- package/data/playbooks/secrets.json +1 -1
- package/data/playbooks/self-update-integrity.json +3 -3
- package/data/playbooks/supply-chain-recovery.json +2 -2
- package/data/playbooks/vc-wallet-trust.json +5 -5
- package/data/playbooks/webhook-callback-abuse.json +1 -1
- package/lib/playbook-runner.js +86 -5
- package/lib/validate-playbooks.js +132 -0
- package/manifest.json +53 -53
- package/package.json +1 -1
- package/sbom.cdx.json +80 -80
|
@@ -528,11 +528,11 @@
|
|
|
528
528
|
],
|
|
529
529
|
"escalation_criteria": [
|
|
530
530
|
{
|
|
531
|
-
"condition": "
|
|
531
|
+
"condition": "issuer-key-not-pinned-to-trust-anchor == true OR presentation-no-nonce-audience-binding == true",
|
|
532
532
|
"action": "raise_severity"
|
|
533
533
|
},
|
|
534
534
|
{
|
|
535
|
-
"condition": "
|
|
535
|
+
"condition": "credential-revocation-status-not-checked == true",
|
|
536
536
|
"action": "trigger_playbook"
|
|
537
537
|
}
|
|
538
538
|
]
|
|
@@ -543,7 +543,7 @@
|
|
|
543
543
|
"id": "pin-issuer-trust-anchors",
|
|
544
544
|
"description": "Replace open issuerKeyResolver acceptance with an issuer allowlist / x5c-or-federation chain validated to a configured trust anchor; reject issuer keys that do not chain to it.",
|
|
545
545
|
"preconditions": [
|
|
546
|
-
"
|
|
546
|
+
"verifier_controls_trusted_issuers == true"
|
|
547
547
|
],
|
|
548
548
|
"priority": 1,
|
|
549
549
|
"for_signals": [
|
|
@@ -557,7 +557,7 @@
|
|
|
557
557
|
"id": "enforce-revocation-on-accept",
|
|
558
558
|
"description": "Resolve the status_list on the accept path and reject on a set status bit; treat a status fetch failure as fail-closed (not \"valid\").",
|
|
559
559
|
"preconditions": [
|
|
560
|
-
"
|
|
560
|
+
"credentials_carry_status_mechanism == true"
|
|
561
561
|
],
|
|
562
562
|
"priority": 1,
|
|
563
563
|
"for_signals": [
|
|
@@ -568,7 +568,7 @@
|
|
|
568
568
|
"id": "bind-presentations-to-nonce-audience",
|
|
569
569
|
"description": "Issue a fresh per-request nonce, require a Key-Binding JWT echoing that nonce + the verifier audience, and verify device-auth for mdoc; reject unbound presentations.",
|
|
570
570
|
"preconditions": [
|
|
571
|
-
"
|
|
571
|
+
"verifier_controls_request_challenge == true"
|
|
572
572
|
],
|
|
573
573
|
"priority": 1,
|
|
574
574
|
"for_signals": [
|
|
@@ -694,7 +694,7 @@
|
|
|
694
694
|
"id": "policy-exception",
|
|
695
695
|
"description": "Generate an auditor-ready policy exception when faster paths are blocked (legacy receiver cannot be patched within window, provider does not offer state-parameter enforcement, etc.) with compensating controls and time-bound risk acceptance.",
|
|
696
696
|
"preconditions": [
|
|
697
|
-
"
|
|
697
|
+
"prior_remediations_blocked == true",
|
|
698
698
|
"ciso_acceptance_obtainable == true"
|
|
699
699
|
],
|
|
700
700
|
"priority": 4,
|
package/lib/playbook-runner.js
CHANGED
|
@@ -953,6 +953,31 @@ function detect(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
|
953
953
|
|
|
954
954
|
// --- phase 5: analyze ---
|
|
955
955
|
|
|
956
|
+
/**
|
|
957
|
+
* Mirror the FIRED detect indicators (verdict === 'hit') into a flat
|
|
958
|
+
* `{ <indicator-id>: true }` map for the escalation / feeds_into / precondition
|
|
959
|
+
* eval contexts. Indicator hits arrive from the collector / AI evidence path as
|
|
960
|
+
* `signal_overrides` (which detect() reads); the condition contexts spread
|
|
961
|
+
* `agentSubmission.signals`, NOT signal_overrides — so an escalation written as
|
|
962
|
+
* `<indicator-id> == true` (the catalog's canonical form) stayed false even when
|
|
963
|
+
* that indicator DETECTED, unless the operator redundantly re-submitted the id
|
|
964
|
+
* under `signals`. The standard `exceptd collect <playbook>` path never does
|
|
965
|
+
* that, so the entire indicator-gated escalation/chain layer was dead on the
|
|
966
|
+
* real evidence path. Mirroring fired indicators here binds the condition layer
|
|
967
|
+
* to the detect layer. Only verdict 'hit' (FP-checks satisfied) is mirrored — an
|
|
968
|
+
* 'inconclusive' indicator is unconfirmed and must not fire an escalation. The
|
|
969
|
+
* map is spread with the LOWEST precedence (before agentSignals, which is before
|
|
970
|
+
* the engine roots) so an operator can still override a specific id and the
|
|
971
|
+
* engine-computed values always win.
|
|
972
|
+
*/
|
|
973
|
+
function firedIndicatorSignals(detectIndicators) {
|
|
974
|
+
const out = {};
|
|
975
|
+
for (const ind of (detectIndicators || [])) {
|
|
976
|
+
if (ind && ind.verdict === 'hit' && typeof ind.id === 'string') out[ind.id] = true;
|
|
977
|
+
}
|
|
978
|
+
return out;
|
|
979
|
+
}
|
|
980
|
+
|
|
956
981
|
/**
|
|
957
982
|
* RWEP composition + blast-radius scoring + theater check + framework gap
|
|
958
983
|
* mapping + escalation evaluation. Inputs are the detect result + any
|
|
@@ -1490,7 +1515,7 @@ function analyze(playbookId, directiveId, detectResult, agentSignals = {}, runOp
|
|
|
1490
1515
|
findingShape = {};
|
|
1491
1516
|
}
|
|
1492
1517
|
for (const ec of an.escalation_criteria || []) {
|
|
1493
|
-
if (evalCondition(ec.condition, { ...agentSignals, ...evalCtxRoot, rwep: adjustedRwep, blast_radius_score: blastRadiusScore, theater_verdict: theaterVerdict, compliance_theater_check: result.compliance_theater_check, jurisdiction_obligations: (playbook.phases && playbook.phases.govern && playbook.phases.govern.jurisdiction_obligations) || [], analyze: result, matched_cve: result.matched_cves || [],
|
|
1518
|
+
if (evalCondition(ec.condition, { ...firedIndicatorSignals(detectResult.indicators), ...agentSignals, ...evalCtxRoot, rwep: adjustedRwep, blast_radius_score: blastRadiusScore, theater_verdict: theaterVerdict, compliance_theater_check: result.compliance_theater_check, jurisdiction_obligations: (playbook.phases && playbook.phases.govern && playbook.phases.govern.jurisdiction_obligations) || [], analyze: result, matched_cve: result.matched_cves || [],
|
|
1494
1519
|
// finding.* is two-sourced: the engine computes the CVE/severity-derived
|
|
1495
1520
|
// keys (severity, rwep_adjusted, matched_cve_*, blast_radius_score,
|
|
1496
1521
|
// active_exploitation, framework/control_id_first) via analyzeFindingShape,
|
|
@@ -1579,9 +1604,38 @@ function capSummary(s, max = 240) {
|
|
|
1579
1604
|
|
|
1580
1605
|
function validate(playbookId, directiveId, analyzeResult, agentSignals = {}, runOpts = {}) {
|
|
1581
1606
|
const playbook = runOpts._playbookCache || loadPlaybook(playbookId);
|
|
1582
|
-
//
|
|
1583
|
-
//
|
|
1584
|
-
|
|
1607
|
+
// Remediation-path preconditions are evaluated through the SAME evalCondition
|
|
1608
|
+
// the analyze/close phases use, so the precondition context must expose the
|
|
1609
|
+
// SAME engine-computed roots those phases thread in — otherwise a precondition
|
|
1610
|
+
// that gates on `rwep`, `finding.severity`, `matched_cve.*`,
|
|
1611
|
+
// `blast_radius_score`, or `theater_verdict` resolves null and can NEVER be
|
|
1612
|
+
// satisfied, so its remediation path is only ever reachable as the priority
|
|
1613
|
+
// fallback (kernel's `any matched_cve.vector matches 'userns|bpf|ptrace|kptr'`
|
|
1614
|
+
// hardening-compensation path was permanently unsatisfiable for this reason).
|
|
1615
|
+
// agentSignals spread FIRST so the engine-computed values WIN on collision (a
|
|
1616
|
+
// poisoning signals.rwep can't override the engine value) — mirrors the
|
|
1617
|
+
// escalation (analyze) and feeds_into (close) contexts. finding.* merges the
|
|
1618
|
+
// host-AI-asserted descriptive keys UNDER the engine shape with the same
|
|
1619
|
+
// !Array.isArray guard used there.
|
|
1620
|
+
const govern = (playbook.phases && playbook.phases.govern) || {};
|
|
1621
|
+
let findingShape = {};
|
|
1622
|
+
try { findingShape = analyzeFindingShape(analyzeResult || {}); } catch { findingShape = {}; }
|
|
1623
|
+
const evalCtx = {
|
|
1624
|
+
// Fired detect indicators mirrored in (lowest precedence) so a precondition
|
|
1625
|
+
// referencing an indicator id resolves on the collector path. See
|
|
1626
|
+
// firedIndicatorSignals.
|
|
1627
|
+
...firedIndicatorSignals(analyzeResult && analyzeResult._detect_indicators),
|
|
1628
|
+
...agentSignals,
|
|
1629
|
+
rwep: analyzeResult && analyzeResult.rwep ? analyzeResult.rwep.adjusted : undefined,
|
|
1630
|
+
blast_radius_score: analyzeResult ? analyzeResult.blast_radius_score : undefined,
|
|
1631
|
+
theater_verdict: analyzeResult && analyzeResult.compliance_theater_check ? analyzeResult.compliance_theater_check.verdict : undefined,
|
|
1632
|
+
compliance_theater_check: analyzeResult ? analyzeResult.compliance_theater_check : undefined,
|
|
1633
|
+
jurisdiction_obligations: govern.jurisdiction_obligations || [],
|
|
1634
|
+
matched_cve: (analyzeResult && analyzeResult.matched_cves) || [],
|
|
1635
|
+
analyze: analyzeResult,
|
|
1636
|
+
finding: { ...(agentSignals.finding && typeof agentSignals.finding === 'object' && !Array.isArray(agentSignals.finding) ? agentSignals.finding : {}), ...findingShape },
|
|
1637
|
+
...(runOpts._runErrors ? { _runErrors: runOpts._runErrors } : {}),
|
|
1638
|
+
};
|
|
1585
1639
|
const v = resolvedPhase(playbook, directiveId, 'validate');
|
|
1586
1640
|
|
|
1587
1641
|
// Pick the highest-priority remediation_path whose preconditions are all
|
|
@@ -2058,6 +2112,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
2058
2112
|
// feeds_into chaining — full analyze result is exposed so conditions can
|
|
2059
2113
|
// reference `analyze.compliance_theater_check.verdict` etc.
|
|
2060
2114
|
const feedsCtx = {
|
|
2115
|
+
// Fired detect indicators (verdict 'hit') mirrored in so a feeds_into
|
|
2116
|
+
// condition written as `<indicator-id> == true` resolves on the standard
|
|
2117
|
+
// collector path (signal_overrides), not only when the id is re-submitted
|
|
2118
|
+
// under signals. Lowest precedence — operator signals and engine values win.
|
|
2119
|
+
...firedIndicatorSignals(analyzeResult && analyzeResult._detect_indicators),
|
|
2061
2120
|
// Operator-submitted signals come FIRST so the engine-computed reserved
|
|
2062
2121
|
// keys below always win — otherwise a submitted signals.rwep / finding /
|
|
2063
2122
|
// analyze would override the engine value the feeds_into condition tests
|
|
@@ -4035,10 +4094,32 @@ function extractSubmissionForHash(sub) {
|
|
|
4035
4094
|
// canonicalStringify can serialize them.
|
|
4036
4095
|
const signals = {};
|
|
4037
4096
|
for (const [k, v] of Object.entries(sub.signals)) {
|
|
4097
|
+
// Underscore-prefixed signal keys are runner-internal / output-rendering
|
|
4098
|
+
// directives, NOT operator evidence: `_bundle_formats` (the CLI sets it
|
|
4099
|
+
// from `--format sarif|openvex|csaf`) only chooses which bundle close()
|
|
4100
|
+
// renders — the analyzed posture is identical. Hashing it broke the
|
|
4101
|
+
// "same evidence → same evidence_hash" contract: two runs over identical
|
|
4102
|
+
// evidence that differ only in `--format` produced different digests and
|
|
4103
|
+
// a self-contradicting attest diff. Exclude every `_`-prefixed signal key
|
|
4104
|
+
// (matches the file-wide "underscore = internal" convention). vex_filter /
|
|
4105
|
+
// vex_fixed are deliberately NOT excluded — they DROP CVEs from
|
|
4106
|
+
// matched_cves and change the finding, so they are posture-affecting
|
|
4107
|
+
// evidence; excluding them would make reattest blind to a VEX-driven
|
|
4108
|
+
// posture change (a real drift hidden behind an unchanged hash).
|
|
4109
|
+
if (k.startsWith('_')) continue;
|
|
4038
4110
|
if (v instanceof Set) signals[k] = Array.from(v).sort();
|
|
4039
4111
|
else signals[k] = v;
|
|
4040
4112
|
}
|
|
4041
|
-
|
|
4113
|
+
// Omit an empty signals bag rather than recording `signals: {}`. A
|
|
4114
|
+
// submission whose ONLY signal was a `_`-prefixed render directive
|
|
4115
|
+
// (`{ _bundle_formats: [...] }`, injected by `ci`/`run --format`) reduces to
|
|
4116
|
+
// {} after the filter above; the baseline no-format submission carries no
|
|
4117
|
+
// `signals` key at all. Recording `{}` vs omitting it would hash identical
|
|
4118
|
+
// evidence as `{signals:{}}` vs no-signals and reintroduce the exact
|
|
4119
|
+
// --format-driven evidence_hash / session-id drift this exclusion exists to
|
|
4120
|
+
// prevent. Only attach signals when at least one operator-meaningful key
|
|
4121
|
+
// survived.
|
|
4122
|
+
if (Object.keys(signals).length > 0) pick.signals = signals;
|
|
4042
4123
|
}
|
|
4043
4124
|
if (sub.precondition_checks && typeof sub.precondition_checks === 'object') {
|
|
4044
4125
|
pick.precondition_checks = sub.precondition_checks;
|
|
@@ -79,6 +79,70 @@ const fs = require('node:fs');
|
|
|
79
79
|
const path = require('node:path');
|
|
80
80
|
const process = require('node:process');
|
|
81
81
|
const { safeExit } = require('./exit-codes');
|
|
82
|
+
// _evalCondition is the SAME parser the runner uses at analyze/validate/close
|
|
83
|
+
// time. Exercising every escalation / feeds_into / remediation-precondition
|
|
84
|
+
// condition through the real evaluator lets the validator reject a condition the
|
|
85
|
+
// engine cannot parse (condition_unparsed) — a prose / bare-token /
|
|
86
|
+
// unimplemented-syntax condition that would silently return false for every
|
|
87
|
+
// input and disable the escalation it gates. No cycle: playbook-runner does not
|
|
88
|
+
// require this validator. Loaded defensively: the engine carries a dependency
|
|
89
|
+
// closure (scoring / cross-ref-api / id-validation) that a stripped test mirror
|
|
90
|
+
// staging only the validator + schema does not copy, so a hard top-level require
|
|
91
|
+
// would crash the validator there. In the real repo and the shipped tarball the
|
|
92
|
+
// engine is always present, so the parse-gate always runs; when it is genuinely
|
|
93
|
+
// unresolvable the gate surfaces a WARNING (never a silent error-skip — see
|
|
94
|
+
// checkCrossRefs) so the degradation is observable rather than a false pass.
|
|
95
|
+
let _evalCondition = null;
|
|
96
|
+
try { ({ _evalCondition } = require('./playbook-runner')); } catch { /* warned at the gate */ }
|
|
97
|
+
|
|
98
|
+
// Decompose a condition into its leaf atoms the SAME way evalCondition does —
|
|
99
|
+
// strip wrapping parens, split on top-level OR (lowest precedence), then on
|
|
100
|
+
// top-level AND, quote- and depth-aware. This exists because evalCondition
|
|
101
|
+
// evaluates AND via `.every` / OR via `.some`, which SHORT-CIRCUIT: a dead
|
|
102
|
+
// (unparseable) sub-clause behind a leading clause that resolves false (AND) or
|
|
103
|
+
// true (OR) in the empty validation context is never reached, so running the
|
|
104
|
+
// whole condition once never surfaces its condition_unparsed. Atomizing and
|
|
105
|
+
// parse-checking each leaf independently catches a dead sub-clause regardless of
|
|
106
|
+
// short-circuit (e.g. `blast_radius_score >= 3 AND any deterministic indicator
|
|
107
|
+
// fires` — the first clause is false with no signals, so the dead prose second
|
|
108
|
+
// clause never parsed). Mirrors evalCondition's splitAtTopLevel / stripOuterParens.
|
|
109
|
+
function _splitTopLevel(expr, sep) {
|
|
110
|
+
const parts = []; const needle = ' ' + sep + ' ';
|
|
111
|
+
let depth = 0, buf = '', i = 0, quote = null;
|
|
112
|
+
while (i < expr.length) {
|
|
113
|
+
const ch = expr[i];
|
|
114
|
+
if (quote) { if (ch === '\\' && i + 1 < expr.length) { buf += ch + expr[i + 1]; i += 2; continue; } if (ch === quote) quote = null; buf += ch; i++; continue; }
|
|
115
|
+
if (ch === "'" || ch === '"') { quote = ch; buf += ch; i++; continue; }
|
|
116
|
+
if (ch === '(') { depth++; buf += ch; i++; continue; }
|
|
117
|
+
if (ch === ')') { depth--; buf += ch; i++; continue; }
|
|
118
|
+
if (depth === 0 && expr.startsWith(needle, i)) { parts.push(buf.trim()); buf = ''; i += needle.length; continue; }
|
|
119
|
+
buf += ch; i++;
|
|
120
|
+
}
|
|
121
|
+
parts.push(buf.trim());
|
|
122
|
+
return parts;
|
|
123
|
+
}
|
|
124
|
+
function _stripWrappingParens(e) {
|
|
125
|
+
e = e.trim();
|
|
126
|
+
while (e.startsWith('(') && e.endsWith(')')) {
|
|
127
|
+
let d = 0, wraps = true;
|
|
128
|
+
for (let i = 0; i < e.length; i++) {
|
|
129
|
+
if (e[i] === '(') d++;
|
|
130
|
+
else if (e[i] === ')') { d--; if (d === 0 && i < e.length - 1) { wraps = false; break; } }
|
|
131
|
+
}
|
|
132
|
+
if (wraps) e = e.slice(1, -1).trim(); else break;
|
|
133
|
+
}
|
|
134
|
+
return e;
|
|
135
|
+
}
|
|
136
|
+
function atomizeCondition(cond) {
|
|
137
|
+
const atoms = [];
|
|
138
|
+
(function rec(e) {
|
|
139
|
+
e = _stripWrappingParens(e);
|
|
140
|
+
const ors = _splitTopLevel(e, 'OR'); if (ors.length > 1) { ors.forEach(rec); return; }
|
|
141
|
+
const ands = _splitTopLevel(e, 'AND'); if (ands.length > 1) { ands.forEach(rec); return; }
|
|
142
|
+
atoms.push(e);
|
|
143
|
+
})(cond);
|
|
144
|
+
return atoms;
|
|
145
|
+
}
|
|
82
146
|
|
|
83
147
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
84
148
|
const SCHEMA_PATH = path.join(REPO_ROOT, 'lib', 'schemas', 'playbook.schema.json');
|
|
@@ -550,6 +614,74 @@ function checkCrossRefs(playbook, ctx, playbookIds) {
|
|
|
550
614
|
}
|
|
551
615
|
}
|
|
552
616
|
|
|
617
|
+
// Condition PARSEABILITY gate. The path-root check above only catches a
|
|
618
|
+
// dotted path rooted at an unavailable phase; the broader and far more common
|
|
619
|
+
// dead-condition shape is a condition the evaluator cannot parse AT ALL —
|
|
620
|
+
// free-text prose ("A single compromised identity can rewrite the trail"), a
|
|
621
|
+
// bare-truthiness token without `== true`, an unimplemented keyword
|
|
622
|
+
// (`<tok> fired`, `not in`, `is`), or an apostrophe-bearing token that fails
|
|
623
|
+
// the LHS pattern. Every such condition falls through evalCondition to a
|
|
624
|
+
// silent `false` for every input, so the raise_severity / trigger_playbook /
|
|
625
|
+
// remediation-precondition it gates never fires — and nothing flagged it,
|
|
626
|
+
// because the validator never ran the actual evaluator. Run each condition in
|
|
627
|
+
// the THREE locations the runner threads through evalCondition (analyze
|
|
628
|
+
// escalation_criteria, _meta.feeds_into, validate remediation_paths
|
|
629
|
+
// preconditions) and hard-fail on condition_unparsed. Scoped to exactly those
|
|
630
|
+
// three: scoring_rubric entries are descriptive blast-level prose (never
|
|
631
|
+
// evaluated — the score comes from agentSignals.blast_radius_score) and
|
|
632
|
+
// regression_trigger conditions are consumed by computeRegressionNextRun's own
|
|
633
|
+
// cadence/event parser, so neither is in scope here. Gate ONLY on
|
|
634
|
+
// condition_unparsed — a genuine PARSE failure is input-independent;
|
|
635
|
+
// condition_path_unresolved / condition_type_mismatch are runtime data
|
|
636
|
+
// diagnostics that would false-positive against this empty validation context
|
|
637
|
+
// (a dotted path legitimately resolves null when no run has populated it).
|
|
638
|
+
if (!_evalCondition) {
|
|
639
|
+
// Engine module not resolvable from this validator location (a stripped
|
|
640
|
+
// test mirror). Surface it as a WARNING so the skip is observable — in the
|
|
641
|
+
// real repo + shipped tarball the engine always loads and this never fires.
|
|
642
|
+
warn(
|
|
643
|
+
'escalation/feeds_into/precondition parse-gate skipped: lib/playbook-runner.js (the condition evaluator) was not resolvable from this validator location, so condition parseability was not checked',
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const conditionParses = (cond) => {
|
|
647
|
+
if (typeof cond !== 'string' || !cond.trim()) return true;
|
|
648
|
+
if (!_evalCondition) return true; // engine unavailable — warned above, never a silent error-skip
|
|
649
|
+
// Check EACH atomic leaf, not the whole condition once: evalCondition's
|
|
650
|
+
// AND/OR short-circuit hides a dead sub-clause behind a leading clause that
|
|
651
|
+
// resolves false (AND) / true (OR) in the empty validation context.
|
|
652
|
+
for (const atom of atomizeCondition(cond)) {
|
|
653
|
+
const runErrors = [];
|
|
654
|
+
try { _evalCondition(atom, { _runErrors: runErrors }, { _runErrors: runErrors }); }
|
|
655
|
+
catch { continue; } // an evaluator throw is a separate concern, not "unparseable"
|
|
656
|
+
if (runErrors.some((e) => e && e.kind === 'condition_unparsed')) return false;
|
|
657
|
+
}
|
|
658
|
+
return true;
|
|
659
|
+
};
|
|
660
|
+
for (const [i, ec] of (((phases.analyze || {}).escalation_criteria) || []).entries()) {
|
|
661
|
+
if (ec && typeof ec === 'object' && !conditionParses(ec.condition)) {
|
|
662
|
+
err(
|
|
663
|
+
`phases.analyze.escalation_criteria[${i}].condition: not parseable by the runner's evalCondition (${JSON.stringify(String(ec.condition).slice(0, 80))}) — it returns false for every input, so action "${ec.action || '?'}" never fires. Rewrite into the mini-language (<indicator-id> == true [AND/OR …], engine roots rwep/blast_radius_score/matched_cve/finding.*, membership IN [...], quantifier any/all, matches '…').`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const [i, f] of (meta.feeds_into || []).entries()) {
|
|
668
|
+
if (f && typeof f === 'object' && !conditionParses(f.condition)) {
|
|
669
|
+
err(
|
|
670
|
+
`_meta.feeds_into[${i}].condition: not parseable by the runner's evalCondition (${JSON.stringify(String(f.condition).slice(0, 80))}) — the chain to "${f.playbook_id || '?'}" never fires. Rewrite into the mini-language.`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
for (const [i, rp] of (validatePhase.remediation_paths || []).entries()) {
|
|
675
|
+
if (!rp || typeof rp !== 'object' || !Array.isArray(rp.preconditions)) continue;
|
|
676
|
+
for (const [j, pc] of rp.preconditions.entries()) {
|
|
677
|
+
if (!conditionParses(pc)) {
|
|
678
|
+
err(
|
|
679
|
+
`phases.validate.remediation_paths[${i}] (${rp.id || 'unknown'}).preconditions[${j}]: not parseable by the runner's evalCondition (${JSON.stringify(String(pc).slice(0, 80))}) — it is never satisfiable, so the path can only be selected as the priority fallback. Rewrite into the mini-language (bare token → "<token> == true", prose gate → an operator-signalable "<snake_token> == true").`,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
553
685
|
// Air-gap completeness. When _meta.air_gap_mode is true the runner refuses
|
|
554
686
|
// to touch the network, so every artifact whose source is a network call
|
|
555
687
|
// (https://, http://, gh api, gh release, curl, wget, fetch) MUST carry a
|