@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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-12T13:50:32.319Z",
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": "3614ad87835688365283c95a8b7af1d66f14928c07efef750c206ba2f13aab33",
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",
@@ -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: c.evidence_package.bundle_format || 'csaf-2.0',
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(c.evidence_package.bundle_format || 'csaf-2.0', playbook, analyzeResult, validateResult, agentSignals)
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.compliance_theater,
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
- // Other formats deferred.
705
- return { format, note: 'Non-CSAF formats deferred to GRC integration layer.', analyze, validate };
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
- const pre = preflight(playbook, runOpts);
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 => ({ id: d.id, title: d.title, applies_to: d.applies_to }))
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
- log(`\nprefetch summary: ${result.fetched} fetched, ${result.skipped_fresh} fresh, ${result.errors} error(s)`);
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
 
@@ -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-12T13:48:57.281Z",
3
+ "_generated_at": "2026-05-12T14:46:11.640Z",
4
4
  "atlas_version": "5.1.0",
5
5
  "skill_count": 38,
6
6
  "skills": [