@blamejs/exceptd-skills 0.16.3 → 0.16.5
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 +14 -0
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/ai-api.json +5 -0
- package/data/playbooks/ai-discovered-cve-triage.json +5 -0
- package/data/playbooks/cicd-pipeline-compromise.json +5 -0
- package/data/playbooks/citation-hygiene.json +4 -0
- package/data/playbooks/cloud-iam-incident.json +4 -0
- package/data/playbooks/containers.json +7 -0
- package/data/playbooks/cred-stores.json +7 -0
- package/data/playbooks/crypto-codebase.json +8 -0
- package/data/playbooks/crypto.json +5 -0
- package/data/playbooks/framework.json +5 -0
- package/data/playbooks/hardening.json +4 -0
- package/data/playbooks/identity-sso-compromise.json +4 -0
- package/data/playbooks/idp-incident.json +3 -0
- package/data/playbooks/kernel.json +3 -0
- package/data/playbooks/library-author.json +9 -0
- package/data/playbooks/llm-tool-use-exfil.json +4 -0
- package/data/playbooks/mcp.json +4 -0
- package/data/playbooks/post-quantum-migration.json +6 -0
- package/data/playbooks/ransomware.json +1 -0
- package/data/playbooks/runtime.json +4 -0
- package/data/playbooks/sbom.json +6 -0
- package/data/playbooks/secrets.json +2 -0
- package/data/playbooks/supply-chain-recovery.json +6 -0
- package/data/playbooks/webhook-callback-abuse.json +3 -0
- package/lib/playbook-runner.js +68 -16
- package/lib/schemas/playbook.schema.json +5 -0
- package/lib/validate-playbooks.js +18 -0
- package/manifest.json +44 -44
- package/package.json +1 -1
- package/sbom.cdx.json +66 -66
|
@@ -669,6 +669,7 @@
|
|
|
669
669
|
"developer_workflow_can_tolerate_approval_friction == true"
|
|
670
670
|
],
|
|
671
671
|
"priority": 1,
|
|
672
|
+
"for_signals": ["auto-approve-on-high-impact-tool","rubber-stamp-approval-pattern","unprompted-tool-chain"],
|
|
672
673
|
"compensating_controls": [
|
|
673
674
|
"approval_decision_recorded_in_audit_log",
|
|
674
675
|
"approval_pattern_baseline_monitored"
|
|
@@ -683,6 +684,7 @@
|
|
|
683
684
|
"model_supports_trust_zone_pattern == true"
|
|
684
685
|
],
|
|
685
686
|
"priority": 2,
|
|
687
|
+
"for_signals": ["instruction-coercion-in-tool-response","unprompted-tool-chain"],
|
|
686
688
|
"compensating_controls": [
|
|
687
689
|
"trust_zone_test_battery_in_ci",
|
|
688
690
|
"system_prompt_versioned_in_source_control"
|
|
@@ -697,6 +699,7 @@
|
|
|
697
699
|
"agent_egress_traffic_routable_through_inspection == true"
|
|
698
700
|
],
|
|
699
701
|
"priority": 2,
|
|
702
|
+
"for_signals": ["agent-egress-to-non-allowlisted-destination","credential-shadow-in-tool-args"],
|
|
700
703
|
"compensating_controls": [
|
|
701
704
|
"inspection_rules_recorded_in_iac",
|
|
702
705
|
"false_positive_baseline_established"
|
|
@@ -711,6 +714,7 @@
|
|
|
711
714
|
"alternative_content_sources_available_for_removed_externals == true"
|
|
712
715
|
],
|
|
713
716
|
"priority": 2,
|
|
717
|
+
"for_signals": ["rag-source-from-untrusted-origin","instruction-coercion-in-tool-response"],
|
|
714
718
|
"compensating_controls": [
|
|
715
719
|
"rag_source_inventory_recorded",
|
|
716
720
|
"integrity_control_evidence_per_source"
|
package/data/playbooks/mcp.json
CHANGED
|
@@ -876,6 +876,7 @@
|
|
|
876
876
|
"auto_update_available == true"
|
|
877
877
|
],
|
|
878
878
|
"priority": 1,
|
|
879
|
+
"for_signals": ["vulnerable-windsurf-version"],
|
|
879
880
|
"compensating_controls": [],
|
|
880
881
|
"estimated_time_hours": 0.5
|
|
881
882
|
},
|
|
@@ -886,6 +887,7 @@
|
|
|
886
887
|
"operator_authorized_to_modify_mcp_config == true"
|
|
887
888
|
],
|
|
888
889
|
"priority": 2,
|
|
890
|
+
"for_signals": ["unsigned-mcp-manifest","mcp-typosquat-candidate"],
|
|
889
891
|
"compensating_controls": [
|
|
890
892
|
"mcp_egress_network_allowlist",
|
|
891
893
|
"mcp_runs_as_non_root_enforced"
|
|
@@ -899,6 +901,7 @@
|
|
|
899
901
|
"all_authorized_mcp_servers_have_published_signatures == true"
|
|
900
902
|
],
|
|
901
903
|
"priority": 3,
|
|
904
|
+
"for_signals": ["mcp-allowlist-missing","mcp-command-provenance-gap"],
|
|
902
905
|
"compensating_controls": [
|
|
903
906
|
"pre-commit hook validates mcp config against signed allowlist",
|
|
904
907
|
"CI gate rejects PRs that introduce unsigned mcp entries"
|
|
@@ -912,6 +915,7 @@
|
|
|
912
915
|
"package_lockfile_exists == true OR sigstore_rekor_entry_available == true"
|
|
913
916
|
],
|
|
914
917
|
"priority": 4,
|
|
918
|
+
"for_signals": ["mcp-version-without-integrity"],
|
|
915
919
|
"compensating_controls": [
|
|
916
920
|
"scheduled re-pin job on every authorized MCP package release"
|
|
917
921
|
],
|
|
@@ -855,6 +855,7 @@
|
|
|
855
855
|
"operator_assigns_register_owner == true"
|
|
856
856
|
],
|
|
857
857
|
"priority": 1,
|
|
858
|
+
"for_signals": ["no-cryptographic-asset-register","register-incomplete-per-asset-fields","long-retention-classical-only-asset"],
|
|
858
859
|
"compensating_controls": [
|
|
859
860
|
"interim_high_value_asset_prioritisation",
|
|
860
861
|
"vendor_engagement_for_unattested_dependencies"
|
|
@@ -869,6 +870,7 @@
|
|
|
869
870
|
"operator_legal_team_engaged == true"
|
|
870
871
|
],
|
|
871
872
|
"priority": 2,
|
|
873
|
+
"for_signals": ["vendor-no-pqc-commitment","policy-without-vendor-sla"],
|
|
872
874
|
"compensating_controls": [
|
|
873
875
|
"alternative_vendor_shortlist_per_dependency",
|
|
874
876
|
"data_minimisation_for_vendor_handled_data"
|
|
@@ -883,6 +885,7 @@
|
|
|
883
885
|
"hybrid_configured_asset_inventory_present == true"
|
|
884
886
|
],
|
|
885
887
|
"priority": 2,
|
|
888
|
+
"for_signals": ["no-downgrade-detection"],
|
|
886
889
|
"compensating_controls": [
|
|
887
890
|
"alerting_on_downgrade_threshold_per_service",
|
|
888
891
|
"weekly_downgrade_report_to_security_team"
|
|
@@ -897,6 +900,7 @@
|
|
|
897
900
|
"key_migration_window_within_jurisdiction_deadline == true"
|
|
898
901
|
],
|
|
899
902
|
"priority": 3,
|
|
903
|
+
"for_signals": ["hsm-firmware-no-pqc"],
|
|
900
904
|
"compensating_controls": [
|
|
901
905
|
"network_segmentation_for_classical_only_hsm",
|
|
902
906
|
"shortened_retention_for_keys_pending_migration"
|
|
@@ -910,6 +914,7 @@
|
|
|
910
914
|
"operator_jurisdictional_scope_documented == true"
|
|
911
915
|
],
|
|
912
916
|
"priority": 4,
|
|
917
|
+
"for_signals": ["regulator-deadline-missing-or-stale"],
|
|
913
918
|
"compensating_controls": [
|
|
914
919
|
"automated_regulator_publication_monitoring",
|
|
915
920
|
"quarterly_deadline_review_with_legal"
|
|
@@ -923,6 +928,7 @@
|
|
|
923
928
|
"firmware_update_channel_exists == true OR end_of_life_communicable_to_customers == true"
|
|
924
929
|
],
|
|
925
930
|
"priority": 4,
|
|
931
|
+
"for_signals": ["embedded-tls-stack-classical-only"],
|
|
926
932
|
"compensating_controls": [
|
|
927
933
|
"pqc_gateway_for_classical_only_devices",
|
|
928
934
|
"customer_notification_of_hndl_exposure"
|
|
@@ -947,6 +947,7 @@
|
|
|
947
947
|
"forensic_acquisition_tooling_available == true"
|
|
948
948
|
],
|
|
949
949
|
"priority": 1,
|
|
950
|
+
"for_signals": ["mass-file-extension-change-event","shadow-copy-deletion-no-iac-ticket","encrypted-file-extension-growth-rate","cobaltstrike-beacon-signature","large-outbound-transfer-pre-encryption","bloodhound-class-ad-recon","ad-admin-count-modification-event"],
|
|
950
951
|
"compensating_controls": [
|
|
951
952
|
"out-of-band-comms-on-signal-or-wickr",
|
|
952
953
|
"incident-commander-named",
|
|
@@ -685,6 +685,7 @@
|
|
|
685
685
|
"ops_authorization_for_chmod == true"
|
|
686
686
|
],
|
|
687
687
|
"priority": 1,
|
|
688
|
+
"for_signals": ["non-baseline-suid"],
|
|
688
689
|
"compensating_controls": [
|
|
689
690
|
"suid-baseline-tracked",
|
|
690
691
|
"edr-alerts-on-suid-creation"
|
|
@@ -699,6 +700,7 @@
|
|
|
699
700
|
"automation_account_owner_authorization == true"
|
|
700
701
|
],
|
|
701
702
|
"priority": 1,
|
|
703
|
+
"for_signals": ["sudoers-nopasswd-wildcard"],
|
|
702
704
|
"compensating_controls": [
|
|
703
705
|
"sudoers-change-recorded",
|
|
704
706
|
"automation-account-inventory-updated"
|
|
@@ -712,6 +714,7 @@
|
|
|
712
714
|
"deterministic_implant_indicator_fired == true"
|
|
713
715
|
],
|
|
714
716
|
"priority": 1,
|
|
717
|
+
"for_signals": ["duplicate-uid-zero","orphan-privileged-process","cron-or-timer-outside-policy"],
|
|
715
718
|
"compensating_controls": [
|
|
716
719
|
"host-isolated-from-network",
|
|
717
720
|
"memory-snapshot-captured-pre-removal"
|
|
@@ -726,6 +729,7 @@
|
|
|
726
729
|
"service_owner_identified == true"
|
|
727
730
|
],
|
|
728
731
|
"priority": 2,
|
|
732
|
+
"for_signals": ["listening-socket-unknown-bind"],
|
|
729
733
|
"compensating_controls": [
|
|
730
734
|
"host-firewall-rule-added",
|
|
731
735
|
"service-inventory-updated"
|
package/data/playbooks/sbom.json
CHANGED
|
@@ -1261,6 +1261,7 @@
|
|
|
1261
1261
|
"operator_authorized_for_package_upgrade == true"
|
|
1262
1262
|
],
|
|
1263
1263
|
"priority": 1,
|
|
1264
|
+
"for_signals": ["package-matches-catalogued-cve","kev-listed-match","windsurf-vulnerable-version"],
|
|
1264
1265
|
"compensating_controls": [
|
|
1265
1266
|
"restart_affected_services_post_upgrade",
|
|
1266
1267
|
"regression_test_post_upgrade"
|
|
@@ -1274,6 +1275,7 @@
|
|
|
1274
1275
|
"ci_pipeline_modifiable == true"
|
|
1275
1276
|
],
|
|
1276
1277
|
"priority": 2,
|
|
1278
|
+
"for_signals": ["lockfile-no-integrity"],
|
|
1277
1279
|
"compensating_controls": [
|
|
1278
1280
|
"lockfile_review_in_pr_template"
|
|
1279
1281
|
],
|
|
@@ -1298,6 +1300,7 @@
|
|
|
1298
1300
|
"sbom_tooling_supports_transitive == true"
|
|
1299
1301
|
],
|
|
1300
1302
|
"priority": 4,
|
|
1303
|
+
"for_signals": ["transitive-deps-incomplete-sbom"],
|
|
1301
1304
|
"compensating_controls": [
|
|
1302
1305
|
"sbom_completeness_gate_in_ci"
|
|
1303
1306
|
],
|
|
@@ -1310,6 +1313,7 @@
|
|
|
1310
1313
|
"security_team_capacity_for_vex == true"
|
|
1311
1314
|
],
|
|
1312
1315
|
"priority": 5,
|
|
1316
|
+
"for_signals": ["matched-cve-without-vex"],
|
|
1313
1317
|
"compensating_controls": [
|
|
1314
1318
|
"vex_template_in_security_playbook"
|
|
1315
1319
|
],
|
|
@@ -1323,6 +1327,7 @@
|
|
|
1323
1327
|
"ci_or_pre-commit_modifiable == true"
|
|
1324
1328
|
],
|
|
1325
1329
|
"priority": 6,
|
|
1330
|
+
"for_signals": ["ai-code-no-provenance"],
|
|
1326
1331
|
"compensating_controls": [
|
|
1327
1332
|
"pr_review_for_ai_emitted_code",
|
|
1328
1333
|
"ai_code_review_checklist"
|
|
@@ -1336,6 +1341,7 @@
|
|
|
1336
1341
|
"ml_loader_modifiable == true OR ml_inference_pipeline_owned == true"
|
|
1337
1342
|
],
|
|
1338
1343
|
"priority": 7,
|
|
1344
|
+
"for_signals": ["model-weight-unsigned-and-executable-format"],
|
|
1339
1345
|
"compensating_controls": [
|
|
1340
1346
|
"model_inventory_review",
|
|
1341
1347
|
"non-safetensors_models_quarantined"
|
|
@@ -754,6 +754,7 @@
|
|
|
754
754
|
"rotation_ownership_identified == true"
|
|
755
755
|
],
|
|
756
756
|
"priority": 1,
|
|
757
|
+
"for_signals": ["aws-access-key-id","aws-secret-access-key","gcp-service-account-json","github-personal-access-token","github-fine-grained-pat","slack-bot-or-user-token","stripe-secret-key","jwt-token-with-secret-context","ssh-private-key-block","openai-api-key","anthropic-api-key"],
|
|
757
758
|
"compensating_controls": [
|
|
758
759
|
"session-revocation",
|
|
759
760
|
"cloudtrail-or-audit-log-review-for-misuse-window"
|
|
@@ -767,6 +768,7 @@
|
|
|
767
768
|
"file_owner_is_current_user OR has_sudo == true"
|
|
768
769
|
],
|
|
769
770
|
"priority": 2,
|
|
771
|
+
"for_signals": ["world-writable-env-file","ssh-key-bad-perms"],
|
|
770
772
|
"compensating_controls": [
|
|
771
773
|
"perm-change-recorded"
|
|
772
774
|
],
|
|
@@ -908,6 +908,7 @@
|
|
|
908
908
|
"rotation_runbook_documented == true OR ad_hoc_rotation_acceptable_within_4h == true"
|
|
909
909
|
],
|
|
910
910
|
"priority": 1,
|
|
911
|
+
"for_signals": ["credential-store-touched-during-window","long-lived-token-in-compromised-ci-log","outbound-exfil-during-window"],
|
|
911
912
|
"compensating_controls": [
|
|
912
913
|
"rotation_recorded_in_secret_store_audit_log",
|
|
913
914
|
"synthetic_test_validates_new_credential",
|
|
@@ -923,6 +924,7 @@
|
|
|
923
924
|
"host_isolation_authority == true"
|
|
924
925
|
],
|
|
925
926
|
"priority": 1,
|
|
927
|
+
"for_signals": ["compromised-install-on-host"],
|
|
926
928
|
"compensating_controls": [
|
|
927
929
|
"host_isolation_during_audit",
|
|
928
930
|
"fresh_workstation_provisioning_for_affected_developers"
|
|
@@ -937,6 +939,7 @@
|
|
|
937
939
|
"operator_can_re_authenticate_ai_assistant == true"
|
|
938
940
|
],
|
|
939
941
|
"priority": 1,
|
|
942
|
+
"for_signals": ["ai-assistant-config-mutated"],
|
|
940
943
|
"compensating_controls": [
|
|
941
944
|
"ai_assistant_baseline_attestation",
|
|
942
945
|
"weekly_ai_assistant_config_drift_scan"
|
|
@@ -951,6 +954,7 @@
|
|
|
951
954
|
"operator_can_file_advisories == true"
|
|
952
955
|
],
|
|
953
956
|
"priority": 2,
|
|
957
|
+
"for_signals": ["operator-published-package-republish"],
|
|
954
958
|
"compensating_controls": [
|
|
955
959
|
"advisory_filed_within_24h",
|
|
956
960
|
"yank_or_deprecate_within_4h",
|
|
@@ -966,6 +970,7 @@
|
|
|
966
970
|
"slsa_l3_pipeline_operational == true"
|
|
967
971
|
],
|
|
968
972
|
"priority": 3,
|
|
973
|
+
"for_signals": ["no-provenance-revocation-filed"],
|
|
969
974
|
"compensating_controls": [
|
|
970
975
|
"provenance_revocation_filed",
|
|
971
976
|
"clean_release_provenance_attested"
|
|
@@ -979,6 +984,7 @@
|
|
|
979
984
|
"ir_plan_ownership_attested == true"
|
|
980
985
|
],
|
|
981
986
|
"priority": 4,
|
|
987
|
+
"for_signals": ["ir-plan-missing-supply-chain-recovery"],
|
|
982
988
|
"compensating_controls": [
|
|
983
989
|
"ir_team_training_recorded",
|
|
984
990
|
"feed_ingestion_attested"
|
|
@@ -653,6 +653,7 @@
|
|
|
653
653
|
"downstream_consumers_can_be_updated_in_rotation_window == true"
|
|
654
654
|
],
|
|
655
655
|
"priority": 1,
|
|
656
|
+
"for_signals": ["leaked-incoming-webhook-url","webhook-secret-shared-across-apps","long-lived-callback-token-in-ci-log"],
|
|
656
657
|
"compensating_controls": [
|
|
657
658
|
"rotation_recorded_in_secret_store_audit_log",
|
|
658
659
|
"synthetic_event_validates_new_secret"
|
|
@@ -667,6 +668,7 @@
|
|
|
667
668
|
"deploy_window_within_72h == true"
|
|
668
669
|
],
|
|
669
670
|
"priority": 2,
|
|
671
|
+
"for_signals": ["missing-webhook-signature-validation","missing-webhook-replay-window","missing-state-parameter"],
|
|
670
672
|
"compensating_controls": [
|
|
671
673
|
"behaviour_test_added_to_ci",
|
|
672
674
|
"deploy_recorded_in_change_management"
|
|
@@ -681,6 +683,7 @@
|
|
|
681
683
|
"downstream_app_can_tolerate_temporary_callback_outage == true"
|
|
682
684
|
],
|
|
683
685
|
"priority": 3,
|
|
686
|
+
"for_signals": ["wildcard-redirect-uri"],
|
|
684
687
|
"compensating_controls": [
|
|
685
688
|
"redirect_uri_allowlist_recorded_in_secret_store",
|
|
686
689
|
"callback_handler_logs_rejected_origins"
|
package/lib/playbook-runner.js
CHANGED
|
@@ -1421,6 +1421,19 @@ function vexFilterFromDoc(doc) {
|
|
|
1421
1421
|
return out;
|
|
1422
1422
|
}
|
|
1423
1423
|
|
|
1424
|
+
// Cap a summary line at `max` chars on a word boundary, appending an ellipsis
|
|
1425
|
+
// so a truncated line is visibly marked rather than cut mid-token. A raw
|
|
1426
|
+
// .slice() left blocked-preflight summaries ending mid-word ("...and conne")
|
|
1427
|
+
// with no signal that anything was dropped (the full text remains in the JSON
|
|
1428
|
+
// envelope's reason/remediation fields).
|
|
1429
|
+
function capSummary(s, max = 240) {
|
|
1430
|
+
if (typeof s !== 'string' || s.length <= max) return s;
|
|
1431
|
+
const slice = s.slice(0, max - 1);
|
|
1432
|
+
const lastSpace = slice.lastIndexOf(' ');
|
|
1433
|
+
const base = lastSpace > max - 40 ? slice.slice(0, lastSpace) : slice;
|
|
1434
|
+
return base.replace(/[\s—.,;:-]+$/, '') + '…';
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1424
1437
|
// --- phase 6: validate ---
|
|
1425
1438
|
|
|
1426
1439
|
function validate(playbookId, directiveId, analyzeResult, agentSignals = {}, runOpts = {}) {
|
|
@@ -1433,8 +1446,16 @@ function validate(playbookId, directiveId, analyzeResult, agentSignals = {}, run
|
|
|
1433
1446
|
// Pick the highest-priority remediation_path whose preconditions are all
|
|
1434
1447
|
// either satisfied by agentSignals or marked unverified=allow.
|
|
1435
1448
|
const paths = (v.remediation_paths || []).slice().sort((a, b) => a.priority - b.priority);
|
|
1436
|
-
|
|
1449
|
+
// Indicators that actually fired this run — used to prefer a remediation
|
|
1450
|
+
// that addresses the finding (via its for_signals linkage) over the bare
|
|
1451
|
+
// priority-1 default when no path's preconditions are verified.
|
|
1452
|
+
const firedSignalIds = new Set(
|
|
1453
|
+
(analyzeResult._detect_indicators || []).filter(i => i.verdict === 'hit').map(i => i.id)
|
|
1454
|
+
);
|
|
1455
|
+
const addressesFired = (p) =>
|
|
1456
|
+
Array.isArray(p.for_signals) && p.for_signals.some(s => firedSignalIds.has(s));
|
|
1437
1457
|
const considered = [];
|
|
1458
|
+
const satisfiedIds = new Set();
|
|
1438
1459
|
for (const p of paths) {
|
|
1439
1460
|
const pcResult = (p.preconditions || []).map(expr => ({
|
|
1440
1461
|
expr,
|
|
@@ -1442,12 +1463,24 @@ function validate(playbookId, directiveId, analyzeResult, agentSignals = {}, run
|
|
|
1442
1463
|
submitted: agentSignals[expressionKey(expr)] !== undefined
|
|
1443
1464
|
}));
|
|
1444
1465
|
const allSatisfied = pcResult.every(x => x.satisfied);
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
//
|
|
1449
|
-
//
|
|
1450
|
-
|
|
1466
|
+
if (allSatisfied) satisfiedIds.add(p.id);
|
|
1467
|
+
considered.push({ id: p.id, priority: p.priority, all_satisfied: allSatisfied, addresses_fired_signal: addressesFired(p), preconditions: pcResult });
|
|
1468
|
+
}
|
|
1469
|
+
// Precedence (paths is priority-sorted, so `find` returns the highest-priority
|
|
1470
|
+
// match). Relevance to a fired indicator outranks a satisfied-but-unrelated
|
|
1471
|
+
// path: recommending a ready-to-apply remediation that does NOT address the
|
|
1472
|
+
// finding (just because its preconditions happen to hold) was the original
|
|
1473
|
+
// defect — for_signals must beat it.
|
|
1474
|
+
// 1. addresses a fired indicator AND preconditions satisfied (relevant + ready)
|
|
1475
|
+
// 2. addresses a fired indicator (relevant; operator must meet its preconditions)
|
|
1476
|
+
// 3. preconditions satisfied (no fired-signal-linked path — actionable fallback)
|
|
1477
|
+
// 4. priority-1 (nothing else — at least propose the top path)
|
|
1478
|
+
const selected =
|
|
1479
|
+
paths.find(p => addressesFired(p) && satisfiedIds.has(p.id))
|
|
1480
|
+
|| paths.find(addressesFired)
|
|
1481
|
+
|| paths.find(p => satisfiedIds.has(p.id))
|
|
1482
|
+
|| paths[0]
|
|
1483
|
+
|| null;
|
|
1451
1484
|
|
|
1452
1485
|
// selected_remediation selection logic:
|
|
1453
1486
|
// 1. Iterate remediation_paths sorted by priority ASC (lower number =
|
|
@@ -1546,11 +1579,14 @@ function computeRegressionNextRun(triggers) {
|
|
|
1546
1579
|
const parsed = parseInterval(t.interval, now);
|
|
1547
1580
|
if (!parsed) continue;
|
|
1548
1581
|
if (parsed.event) {
|
|
1549
|
-
|
|
1582
|
+
// Shipped playbooks key the trigger string as `condition`; `trigger` /
|
|
1583
|
+
// `event` are accepted for external/fixture submissions. Reading only
|
|
1584
|
+
// the latter two left regression_event_triggers[].trigger always null.
|
|
1585
|
+
eventTriggers.push({ interval: t.interval, trigger: t.trigger || t.event || t.condition || null });
|
|
1550
1586
|
continue;
|
|
1551
1587
|
}
|
|
1552
1588
|
if (parsed.unparseable) {
|
|
1553
|
-
unparseable.push({ interval: parsed.unparseable, trigger: t.trigger || null });
|
|
1589
|
+
unparseable.push({ interval: parsed.unparseable, trigger: t.trigger || t.event || t.condition || null });
|
|
1554
1590
|
continue;
|
|
1555
1591
|
}
|
|
1556
1592
|
if (parsed.date && (!soonest || parsed.date < soonest)) soonest = parsed.date;
|
|
@@ -3271,7 +3307,7 @@ function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
|
3271
3307
|
// playbooks_run[] by array index. `verdict:"blocked"` +
|
|
3272
3308
|
// `summary_line` keep the flat result-envelope shape consistent
|
|
3273
3309
|
// across both branches.
|
|
3274
|
-
const summaryLine = `${playbookId}: blocked at preflight (${pre.blocked_by || 'unknown'}) — ${pre.reason || ''}
|
|
3310
|
+
const summaryLine = capSummary(`${playbookId}: blocked at preflight (${pre.blocked_by || 'unknown'}) — ${pre.reason || ''}`);
|
|
3275
3311
|
return {
|
|
3276
3312
|
ok: false,
|
|
3277
3313
|
playbook_id: playbookId,
|
|
@@ -3487,14 +3523,23 @@ function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
|
3487
3523
|
&& phases.detect.classification !== 'not_detected'
|
|
3488
3524
|
&& phases.detect.classification !== 'inconclusive'
|
|
3489
3525
|
&& phases.detect.classification !== 'pending') {
|
|
3490
|
-
// Only on a real detection verdict: surface the
|
|
3491
|
-
//
|
|
3492
|
-
//
|
|
3493
|
-
//
|
|
3494
|
-
//
|
|
3526
|
+
// Only on a real detection verdict: surface the FIRED indicator that
|
|
3527
|
+
// best explains the result. Gating on the verdict keeps a stray hit on
|
|
3528
|
+
// an inconclusive/not-detected run from advertising a finding. Prefer the
|
|
3529
|
+
// indicator that actually drove the RWEP score — the greatest
|
|
3530
|
+
// weight_applied among fired, weight-bearing rwep_inputs — so the
|
|
3531
|
+
// headline names the finding that produced the number shown beside it.
|
|
3532
|
+
// Fall back to the dominant fired indicator (deterministic/high
|
|
3533
|
+
// confidence), then the first hit, when no weighted signal fired.
|
|
3534
|
+
const breakdown = (phases.analyze.rwep && Array.isArray(phases.analyze.rwep.breakdown))
|
|
3535
|
+
? phases.analyze.rwep.breakdown : [];
|
|
3536
|
+
const driver = breakdown
|
|
3537
|
+
.filter((b) => b.fired && typeof b.weight_applied === 'number' && b.weight_applied > 0)
|
|
3538
|
+
.sort((a, b) => b.weight_applied - a.weight_applied)[0];
|
|
3495
3539
|
const hits = phases.detect.indicators.filter((i) => i.verdict === 'hit');
|
|
3496
3540
|
const dominant = hits.find((i) => i.deterministic === true || i.confidence === 'high') || hits[0];
|
|
3497
|
-
if (
|
|
3541
|
+
if (driver && driver.signal_id) topFinding = driver.signal_id;
|
|
3542
|
+
else if (dominant && dominant.id) topFinding = dominant.id;
|
|
3498
3543
|
}
|
|
3499
3544
|
// Evidence completeness: indicators-evaluated vs indicators-known
|
|
3500
3545
|
// distinguishes "ran fully and found nothing" from "couldn't
|
|
@@ -3534,6 +3579,13 @@ function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
|
|
|
3534
3579
|
evidence_hash: evidenceHash,
|
|
3535
3580
|
submission_digest: submissionDigest,
|
|
3536
3581
|
preflight_issues: pre.issues,
|
|
3582
|
+
// Non-fatal collector notices (e.g. a file skipped for exceeding the
|
|
3583
|
+
// scan size limit) surfaced from the submission so a `collect | run`
|
|
3584
|
+
// consumer can see what the collector could not scan. Advisory only:
|
|
3585
|
+
// never affects verdict, rwep, or evidence_completeness.
|
|
3586
|
+
...(Array.isArray(agentSubmission.collector_errors) && agentSubmission.collector_errors.length
|
|
3587
|
+
? { collector_warnings: agentSubmission.collector_errors }
|
|
3588
|
+
: {}),
|
|
3537
3589
|
// Source provenance for precondition_checks. Shape:
|
|
3538
3590
|
// { '<pc-id>': 'submission' | 'runOpts' | 'merged', ... }
|
|
3539
3591
|
precondition_check_source: pcSource,
|
|
@@ -521,6 +521,11 @@
|
|
|
521
521
|
"examples": [["live_patch_available == true"], ["reboot_window_within_4h == true"], ["compensating_control_deployed == true"]]
|
|
522
522
|
},
|
|
523
523
|
"priority": { "type": "integer", "minimum": 1, "description": "Lower number is preferred. 1 is the recommended path when its preconditions hold." },
|
|
524
|
+
"for_signals": {
|
|
525
|
+
"type": "array",
|
|
526
|
+
"items": { "type": "string" },
|
|
527
|
+
"description": "Optional. detect.indicators ids this remediation addresses. When no path's preconditions are satisfied, selected_remediation prefers the highest-priority path whose for_signals includes a fired indicator, so the recommendation matches the finding instead of always defaulting to priority-1. Omit (or leave empty) for generic/fallback paths."
|
|
528
|
+
},
|
|
524
529
|
"compensating_controls": { "type": "array", "items": { "type": "string" } },
|
|
525
530
|
"estimated_time_hours": { "type": "number" }
|
|
526
531
|
}
|
|
@@ -419,6 +419,24 @@ function checkCrossRefs(playbook, ctx, playbookIds) {
|
|
|
419
419
|
}
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
// validate.remediation_paths[].for_signals[] must reference real indicator
|
|
423
|
+
// ids. A dangling ref silently never matches, so selected_remediation falls
|
|
424
|
+
// back to priority-1 without surfacing the intended finding-specific link —
|
|
425
|
+
// exactly the kind of "looks wired, does nothing" drift this gate exists to
|
|
426
|
+
// catch. Warning severity (promoted to a hard error under --strict, matching
|
|
427
|
+
// the false_positive_profile precedent above).
|
|
428
|
+
const validatePhase = phases.validate || {};
|
|
429
|
+
for (const [i, rp] of (validatePhase.remediation_paths || []).entries()) {
|
|
430
|
+
if (!rp || typeof rp !== 'object' || !Array.isArray(rp.for_signals)) continue;
|
|
431
|
+
for (const sig of rp.for_signals) {
|
|
432
|
+
if (!indIds.has(sig)) {
|
|
433
|
+
warn(
|
|
434
|
+
`phases.validate.remediation_paths[${i}] (${rp.id || 'unknown'}).for_signals: unresolved "${sig}" — no matching phases.detect.indicators[].id`,
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
422
440
|
// rwep_threshold ordering. Hard error — a misordered threshold actively
|
|
423
441
|
// breaks the scoring path.
|
|
424
442
|
const rwep = direct.rwep_threshold || {};
|