@blamejs/exceptd-skills 0.10.2 → 0.11.0
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 +114 -0
- package/bin/exceptd.js +1874 -143
- package/data/_indexes/_meta.json +2 -2
- package/lib/playbook-runner.js +222 -9
- package/lib/prefetch.js +9 -1
- package/manifest-snapshot.json +1 -1
- package/manifest.json +39 -39
- package/orchestrator/index.js +98 -8
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
- package/sources/README.md +170 -0
- package/sources/validators/atlas-validator.js +158 -0
- package/sources/validators/cve-validator.js +277 -0
- package/sources/validators/index.js +86 -0
- package/sources/validators/rfc-validator.js +165 -0
- package/sources/validators/version-pin-validator.js +144 -0
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-12T14:47:57.871Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 49,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "f4243591af627c02011ae29d97eb702764e495522188a4d8fcab7b58950d941c",
|
|
8
8
|
"data/atlas-ttps.json": "1500b5830dab070c4252496964a8c0948e1052a656e2c7c6e1efaf0350645e13",
|
|
9
9
|
"data/cve-catalog.json": "a81d3e4b491b27ccc084596b063a6108ff10c9eb01d7776922fc393980b534fe",
|
|
10
10
|
"data/cwe-catalog.json": "c3367d469b4b3d31e4c56397dd7a8305a0be338ecd85afa27804c0c9ce12157b",
|
package/lib/playbook-runner.js
CHANGED
|
@@ -586,13 +586,23 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
|
|
589
|
-
// evidence_package
|
|
589
|
+
// evidence_package — playbook declares one primary bundle_format; the
|
|
590
|
+
// operator may request additional formats via agentSignals._bundle_formats
|
|
591
|
+
// (e.g. SARIF for GitHub Code Scanning + OpenVEX for supply-chain tooling
|
|
592
|
+
// alongside the CSAF default).
|
|
593
|
+
const primaryFormat = c.evidence_package?.bundle_format || 'csaf-2.0';
|
|
594
|
+
const extraFormats = Array.isArray(agentSignals._bundle_formats)
|
|
595
|
+
? agentSignals._bundle_formats.filter(f => f !== primaryFormat)
|
|
596
|
+
: [];
|
|
590
597
|
const evidencePackage = c.evidence_package ? {
|
|
591
|
-
bundle_format:
|
|
598
|
+
bundle_format: primaryFormat,
|
|
592
599
|
contents: c.evidence_package.contents || [],
|
|
593
600
|
destination: c.evidence_package.destination || 'local_only',
|
|
594
601
|
signed: c.evidence_package.signed !== false,
|
|
595
|
-
bundle_body: buildEvidenceBundle(
|
|
602
|
+
bundle_body: buildEvidenceBundle(primaryFormat, playbook, analyzeResult, validateResult, agentSignals),
|
|
603
|
+
bundles_by_format: extraFormats.length ? Object.fromEntries(
|
|
604
|
+
[primaryFormat, ...extraFormats].map(f => [f, buildEvidenceBundle(f, playbook, analyzeResult, validateResult, agentSignals)])
|
|
605
|
+
) : null,
|
|
596
606
|
} : null;
|
|
597
607
|
|
|
598
608
|
if (evidencePackage && evidencePackage.signed && runOpts.session_key) {
|
|
@@ -687,29 +697,219 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals)
|
|
|
687
697
|
},
|
|
688
698
|
vulnerabilities: analyze.matched_cves.map(c => ({
|
|
689
699
|
cve: c.cve_id,
|
|
690
|
-
scores: [{ products: [], cvss_v3: { base_score: 0 } }],
|
|
700
|
+
scores: [{ products: [], cvss_v3: { base_score: c.cvss_score || 0 } }],
|
|
691
701
|
threats: c.active_exploitation === 'confirmed' ? [{ category: 'exploit_status', details: 'Active exploitation confirmed (CISA KEV).' }] : [],
|
|
692
702
|
remediations: [{ category: 'vendor_fix', details: validate.selected_remediation?.description || 'See selected remediation path.' }]
|
|
693
703
|
})),
|
|
694
704
|
exceptd_extension: {
|
|
695
705
|
rwep: analyze.rwep,
|
|
696
706
|
blast_radius_score: analyze.blast_radius_score,
|
|
697
|
-
compliance_theater: analyze.
|
|
707
|
+
compliance_theater: analyze.compliance_theater_check,
|
|
698
708
|
framework_gap_mapping: analyze.framework_gap_mapping,
|
|
699
709
|
evidence_requirements: validate.evidence_requirements,
|
|
700
710
|
residual_risk_statement: validate.residual_risk_statement
|
|
701
711
|
}
|
|
702
712
|
};
|
|
703
713
|
}
|
|
704
|
-
|
|
705
|
-
|
|
714
|
+
|
|
715
|
+
// SARIF 2.1.0 — GitHub Code Scanning / VS Code SARIF Viewer / Azure DevOps
|
|
716
|
+
// and most static analysis tooling. One run per playbook directive, one
|
|
717
|
+
// result per matched CVE. Each result references a rule (cve_id) and ties
|
|
718
|
+
// back to the directive as the "tool" producer.
|
|
719
|
+
if (format === 'sarif' || format === 'sarif-2.1.0') {
|
|
720
|
+
return {
|
|
721
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
722
|
+
version: '2.1.0',
|
|
723
|
+
runs: [{
|
|
724
|
+
tool: {
|
|
725
|
+
driver: {
|
|
726
|
+
name: 'exceptd',
|
|
727
|
+
version: playbook._meta.version,
|
|
728
|
+
informationUri: 'https://exceptd.com',
|
|
729
|
+
rules: analyze.matched_cves.map(c => ({
|
|
730
|
+
id: c.cve_id,
|
|
731
|
+
shortDescription: { text: c.cve_id },
|
|
732
|
+
fullDescription: { text: `RWEP ${c.rwep} · KEV=${c.cisa_kev} · active_exploitation=${c.active_exploitation} · PoC=${c.poc_available}` },
|
|
733
|
+
defaultConfiguration: { level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note' },
|
|
734
|
+
helpUri: `https://nvd.nist.gov/vuln/detail/${c.cve_id}`,
|
|
735
|
+
}))
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
results: analyze.matched_cves.map(c => ({
|
|
739
|
+
ruleId: c.cve_id,
|
|
740
|
+
level: c.rwep >= 90 ? 'error' : c.rwep >= 70 ? 'warning' : 'note',
|
|
741
|
+
message: { text: `${c.cve_id}: RWEP ${c.rwep}, blast_radius ${analyze.blast_radius_score}. ${validate.selected_remediation?.description || ''}` },
|
|
742
|
+
properties: {
|
|
743
|
+
rwep: c.rwep,
|
|
744
|
+
cisa_kev: c.cisa_kev,
|
|
745
|
+
cisa_kev_due_date: c.cisa_kev_due_date,
|
|
746
|
+
active_exploitation: c.active_exploitation,
|
|
747
|
+
ai_discovered: c.ai_discovered,
|
|
748
|
+
blast_radius_score: analyze.blast_radius_score,
|
|
749
|
+
framework_gaps: analyze.framework_gap_mapping?.length || 0,
|
|
750
|
+
}
|
|
751
|
+
}))
|
|
752
|
+
}]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// OpenVEX 0.2.0 — supply-chain VEX statements. Each matched CVE becomes a
|
|
757
|
+
// statement with status derived from confidence + RWEP. Downstream tools
|
|
758
|
+
// (sigstore, in-toto, GUAC) consume this directly.
|
|
759
|
+
if (format === 'openvex' || format === 'openvex-0.2.0') {
|
|
760
|
+
const issued = new Date().toISOString();
|
|
761
|
+
return {
|
|
762
|
+
'@context': 'https://openvex.dev/ns/v0.2.0',
|
|
763
|
+
'@id': `https://exceptd.com/vex/${playbook._meta.id}/${Date.now()}`,
|
|
764
|
+
author: 'exceptd',
|
|
765
|
+
timestamp: issued,
|
|
766
|
+
version: 1,
|
|
767
|
+
statements: analyze.matched_cves.map(c => ({
|
|
768
|
+
vulnerability: { '@id': c.cve_id, name: c.cve_id },
|
|
769
|
+
status: c.active_exploitation === 'confirmed' ? 'under_investigation' : (c.live_patch_available ? 'fixed' : 'affected'),
|
|
770
|
+
timestamp: issued,
|
|
771
|
+
action_statement: validate.selected_remediation?.description || null,
|
|
772
|
+
impact_statement: `RWEP ${c.rwep}. Blast radius ${analyze.blast_radius_score}/5.`
|
|
773
|
+
}))
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// v0.11.0 redesign #39: --format summary emits a 5-line digest for CI gates
|
|
778
|
+
// and human triage. Drops everything except verdict + RWEP + blast +
|
|
779
|
+
// feeds_into + jurisdiction clock count.
|
|
780
|
+
if (format === 'summary') {
|
|
781
|
+
return {
|
|
782
|
+
format: 'summary',
|
|
783
|
+
summary: {
|
|
784
|
+
playbook: playbook._meta.id,
|
|
785
|
+
verdict: analyze.compliance_theater_check?.verdict || 'pending',
|
|
786
|
+
matched_cves: analyze.matched_cves.length,
|
|
787
|
+
rwep_adjusted: analyze.rwep?.adjusted || 0,
|
|
788
|
+
rwep_threshold_escalate: analyze.rwep?.threshold?.escalate || null,
|
|
789
|
+
blast_radius_score: analyze.blast_radius_score || 0,
|
|
790
|
+
feeds_into: null, // populated by close()
|
|
791
|
+
jurisdiction_clocks_active: null, // populated by close()
|
|
792
|
+
remediation_recommended: validate.selected_remediation?.id || null,
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (format === 'markdown') {
|
|
798
|
+
const lines = [
|
|
799
|
+
`# exceptd finding: ${playbook.domain.name}`,
|
|
800
|
+
`**Playbook:** ${playbook._meta.id} v${playbook._meta.version}`,
|
|
801
|
+
`**Matched CVEs:** ${analyze.matched_cves.length}`,
|
|
802
|
+
`**Top RWEP:** ${analyze.rwep?.adjusted || 0}`,
|
|
803
|
+
`**Blast radius:** ${analyze.blast_radius_score || 'unknown'}/5`,
|
|
804
|
+
`**Theater verdict:** ${analyze.compliance_theater_check?.verdict || 'n/a'}`,
|
|
805
|
+
`\n## Matched CVEs`,
|
|
806
|
+
...analyze.matched_cves.map(c => `- **${c.cve_id}** RWEP ${c.rwep} · KEV=${c.cisa_kev} · ${c.active_exploitation}`),
|
|
807
|
+
`\n## Selected remediation`,
|
|
808
|
+
validate.selected_remediation ? `${validate.selected_remediation.id} (priority ${validate.selected_remediation.priority}): ${validate.selected_remediation.description}` : 'No remediation path selected.',
|
|
809
|
+
`\n## Residual risk`,
|
|
810
|
+
validate.residual_risk_statement ? `${validate.residual_risk_statement.risk}\n\n_Acceptance level: ${validate.residual_risk_statement.acceptance_level}_` : 'None recorded.',
|
|
811
|
+
];
|
|
812
|
+
return { format: 'markdown', body: lines.join('\n') };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return { format, note: 'Unknown format — supported: csaf-2.0, sarif, openvex, markdown.', analyze, validate };
|
|
706
816
|
}
|
|
707
817
|
|
|
708
818
|
// --- orchestrate: full run in one call ---
|
|
709
819
|
|
|
820
|
+
/**
|
|
821
|
+
* v0.11.0 flat submission shape → v0.10.x nested shape. The flat shape is:
|
|
822
|
+
*
|
|
823
|
+
* {
|
|
824
|
+
* observations: {
|
|
825
|
+
* <artifact-id>: { captured, value, indicator?, result? } | "<precondition-value>",
|
|
826
|
+
* },
|
|
827
|
+
* verdict: { theater, classification, blast_radius }
|
|
828
|
+
* }
|
|
829
|
+
*
|
|
830
|
+
* Already-nested submissions pass through unchanged.
|
|
831
|
+
*/
|
|
832
|
+
function normalizeSubmission(submission, playbook) {
|
|
833
|
+
if (!submission || typeof submission !== "object") return submission || {};
|
|
834
|
+
if (submission.artifacts || submission.signal_overrides || submission.signals) return submission;
|
|
835
|
+
if (!submission.observations && !submission.verdict) return submission;
|
|
836
|
+
|
|
837
|
+
const out = { artifacts: {}, signal_overrides: {}, signals: {}, precondition_checks: {} };
|
|
838
|
+
const knownPreconditions = new Set((playbook?._meta?.preconditions || []).map(p => p.id));
|
|
839
|
+
const knownArtifacts = new Set((playbook?.phases?.look?.artifacts || []).map(a => a.id));
|
|
840
|
+
|
|
841
|
+
for (const [key, val] of Object.entries(submission.observations || {})) {
|
|
842
|
+
if (knownPreconditions.has(key)) {
|
|
843
|
+
out.precondition_checks[key] = val === "ok" || val === true || val === "true";
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (typeof val === "object" && val !== null) {
|
|
847
|
+
const aid = knownArtifacts.has(key) ? key : (val.artifact || key);
|
|
848
|
+
out.artifacts[aid] = { value: val.value, captured: val.captured !== false };
|
|
849
|
+
if (val.indicator && val.result) out.signal_overrides[val.indicator] = val.result;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const v = submission.verdict || {};
|
|
854
|
+
if (v.theater) out.signals.theater_verdict = v.theater === "actual_security" ? "clear" : v.theater;
|
|
855
|
+
if (v.classification) out.signals.detection_classification = v.classification;
|
|
856
|
+
if (v.blast_radius !== undefined) out.signals.blast_radius_score = v.blast_radius;
|
|
857
|
+
|
|
858
|
+
// Carry over precondition_checks if the operator supplied them at the top
|
|
859
|
+
// level even in the flat shape.
|
|
860
|
+
if (submission.precondition_checks) Object.assign(out.precondition_checks, submission.precondition_checks);
|
|
861
|
+
|
|
862
|
+
return out;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Smart precondition auto-detect (redesign #9). Some preconditions are
|
|
867
|
+
* mechanically answerable by the runner itself — host platform, cwd
|
|
868
|
+
* readability, command-on-PATH. The AI shouldn't have to declare these;
|
|
869
|
+
* we resolve them ourselves and only escalate to AI declaration when the
|
|
870
|
+
* check requires intent (e.g. "operator authorized this scan").
|
|
871
|
+
*/
|
|
872
|
+
function autoDetectPreconditions(submission, playbook) {
|
|
873
|
+
const fs = require('fs');
|
|
874
|
+
const out = { ...(submission || {}) };
|
|
875
|
+
out.precondition_checks = { ...(submission?.precondition_checks || {}) };
|
|
876
|
+
for (const pc of (playbook?._meta?.preconditions || [])) {
|
|
877
|
+
if (out.precondition_checks[pc.id] !== undefined) continue; // operator already supplied
|
|
878
|
+
const check = (pc.check || '').toLowerCase();
|
|
879
|
+
if (check.includes("host.platform == 'linux'") || check.includes("host.platform == \"linux\"")) {
|
|
880
|
+
out.precondition_checks[pc.id] = process.platform === 'linux';
|
|
881
|
+
} else if (check.includes("host.platform == 'darwin'") || check.includes("host.platform == \"darwin\"")) {
|
|
882
|
+
out.precondition_checks[pc.id] = process.platform === 'darwin';
|
|
883
|
+
} else if (check.includes("cwd_readable")) {
|
|
884
|
+
try { fs.readdirSync(process.cwd()); out.precondition_checks[pc.id] = true; }
|
|
885
|
+
catch { out.precondition_checks[pc.id] = false; }
|
|
886
|
+
} else if (check.includes("agent_has_filesystem_read")) {
|
|
887
|
+
out.precondition_checks[pc.id] = true; // Node has fs by definition
|
|
888
|
+
} else if (check.match(/agent_has_command\(['"]([^'"]+)['"]\)/)) {
|
|
889
|
+
const cmdName = check.match(/agent_has_command\(['"]([^'"]+)['"]\)/)[1];
|
|
890
|
+
const { spawnSync } = require('child_process');
|
|
891
|
+
const probe = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmdName], { stdio: 'ignore' });
|
|
892
|
+
out.precondition_checks[pc.id] = probe.status === 0;
|
|
893
|
+
}
|
|
894
|
+
// Intent-requiring checks (e.g. "operator_authorized == true") are NOT
|
|
895
|
+
// auto-resolved — the AI / operator still declares them. We leave them
|
|
896
|
+
// undefined and the preflight gate handles missing values per on_fail.
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
|
|
710
901
|
function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
711
902
|
const playbook = loadPlaybook(playbookId);
|
|
712
|
-
|
|
903
|
+
|
|
904
|
+
// v0.11.0: accept flat submission shape (observations + verdict). Normalize
|
|
905
|
+
// to the engine's internal nested shape before preflight/detect. Smart
|
|
906
|
+
// precondition auto-detect (redesign #9) fires here when the cwd is readable
|
|
907
|
+
// / the host platform matches — the runner can answer those itself rather
|
|
908
|
+
// than blocking on AI declaration.
|
|
909
|
+
agentSubmission = normalizeSubmission(agentSubmission, playbook);
|
|
910
|
+
agentSubmission = autoDetectPreconditions(agentSubmission, playbook);
|
|
911
|
+
|
|
912
|
+
const pre = preflight(playbook, { ...runOpts, precondition_checks: { ...(agentSubmission.precondition_checks || {}), ...(runOpts.precondition_checks || {}) } });
|
|
713
913
|
if (!pre.ok) {
|
|
714
914
|
return { ok: false, phase: 'preflight', blocked_by: pre.blocked_by, reason: pre.reason, issues: pre.issues };
|
|
715
915
|
}
|
|
@@ -906,13 +1106,24 @@ function plan(opts = {}) {
|
|
|
906
1106
|
session_id: opts.session_id || crypto.randomBytes(8).toString('hex'),
|
|
907
1107
|
playbooks: ids.map(id => {
|
|
908
1108
|
const pb = loadPlaybook(id);
|
|
1109
|
+
const baseDirect = pb.phases?.direct || {};
|
|
909
1110
|
return {
|
|
910
1111
|
id,
|
|
911
1112
|
domain: pb.domain,
|
|
912
1113
|
scope: pb._meta.scope || null,
|
|
913
1114
|
threat_currency_score: pb._meta.threat_currency_score,
|
|
914
1115
|
air_gap_mode: !!pb._meta.air_gap_mode,
|
|
915
|
-
directives: pb.directives.map(d =>
|
|
1116
|
+
directives: pb.directives.map(d => {
|
|
1117
|
+
const overrideDirect = d.phase_overrides?.direct || {};
|
|
1118
|
+
const threatContext = overrideDirect.threat_context || baseDirect.threat_context || null;
|
|
1119
|
+
// Bug #46: include description by default (not just under --directives).
|
|
1120
|
+
// Operators picking a directive need operator-facing prose.
|
|
1121
|
+
const desc = d.description
|
|
1122
|
+
|| (threatContext ? (threatContext.split(/(?<=[.!?])\s+/)[0] || "").slice(0, 240) : null)
|
|
1123
|
+
|| pb.domain?.name
|
|
1124
|
+
|| null;
|
|
1125
|
+
return { id: d.id, title: d.title, description: desc, applies_to: d.applies_to };
|
|
1126
|
+
})
|
|
916
1127
|
};
|
|
917
1128
|
})
|
|
918
1129
|
};
|
|
@@ -932,6 +1143,8 @@ module.exports = {
|
|
|
932
1143
|
close,
|
|
933
1144
|
run,
|
|
934
1145
|
vexFilterFromDoc,
|
|
1146
|
+
normalizeSubmission,
|
|
1147
|
+
autoDetectPreconditions,
|
|
935
1148
|
// internal helpers exposed for tests
|
|
936
1149
|
_resolvedPhase: resolvedPhase,
|
|
937
1150
|
_deepMerge: deepMerge,
|
package/lib/prefetch.js
CHANGED
|
@@ -260,6 +260,10 @@ async function prefetch(options = {}) {
|
|
|
260
260
|
result.by_source[item.source].skipped_fresh++;
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
|
+
// Unconditional one-line summary (--quiet preserved on per-entry chatter
|
|
264
|
+
// but operator still needs confirmation the dry-run completed).
|
|
265
|
+
const stale = plan.length - result.skipped_fresh;
|
|
266
|
+
console.log(`prefetch summary: 0 fetched, ${result.skipped_fresh} fresh, ${stale} would-fetch (dry-run)`);
|
|
263
267
|
return result;
|
|
264
268
|
}
|
|
265
269
|
|
|
@@ -306,7 +310,11 @@ async function prefetch(options = {}) {
|
|
|
306
310
|
idx.generated_at = new Date().toISOString();
|
|
307
311
|
saveIndex(opts.cacheDir, idx);
|
|
308
312
|
|
|
309
|
-
|
|
313
|
+
// Final summary is unconditional — --quiet suppresses per-entry chatter
|
|
314
|
+
// (the noisy part) but the operator still needs one line confirming success.
|
|
315
|
+
// Without this, --quiet + --no-network was zero output even on dry-run
|
|
316
|
+
// success, leaving operators unsure if the command had run at all.
|
|
317
|
+
console.log(`prefetch summary: ${result.fetched} fetched, ${result.skipped_fresh} fresh, ${result.errors} error(s)${opts.noNetwork ? " (dry-run)" : ""}`);
|
|
310
318
|
return result;
|
|
311
319
|
}
|
|
312
320
|
|
package/manifest-snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
|
|
3
|
-
"_generated_at": "2026-05-
|
|
3
|
+
"_generated_at": "2026-05-12T14:46:11.640Z",
|
|
4
4
|
"atlas_version": "5.1.0",
|
|
5
5
|
"skill_count": 38,
|
|
6
6
|
"skills": [
|