From 526aefb1ea217fbd02d90c684fcb195aa333f70a Mon Sep 17 00:00:00 2001 From: ogt Date: Sat, 11 Jul 2026 00:37:03 +0800 Subject: [PATCH] feat(iwooos): add wazuh executor handoff runway --- apps/api/src/api/v1/iwooos.py | 79 +++- .../iwooos_security_tool_closure_readback.py | 111 +++-- ...wooos_wazuh_controlled_executor_handoff.py | 388 ++++++++++++++++++ ...t_iwooos_security_tool_closure_readback.py | 66 +-- ...wooos_wazuh_controlled_executor_handoff.py | 179 ++++++++ apps/web/messages/en.json | 14 +- apps/web/messages/zh-TW.json | 14 +- apps/web/src/app/[locale]/iwooos/page.tsx | 97 ++++- apps/web/src/lib/api-client.ts | 9 + 9 files changed, 890 insertions(+), 67 deletions(-) create mode 100644 apps/api/src/services/iwooos_wazuh_controlled_executor_handoff.py create mode 100644 apps/api/tests/test_iwooos_wazuh_controlled_executor_handoff.py diff --git a/apps/api/src/api/v1/iwooos.py b/apps/api/src/api/v1/iwooos.py index 8b28d9104..d0d752ac9 100644 --- a/apps/api/src/api/v1/iwooos.py +++ b/apps/api/src/api/v1/iwooos.py @@ -43,6 +43,12 @@ from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( validate_iwooos_wazuh_allowlisted_check_mode_dry_run_packet as validate_wazuh_allowlisted_check_mode_dry_run_packet_payload, ) +from src.services.iwooos_wazuh_controlled_executor_handoff import ( + load_latest_iwooos_wazuh_controlled_executor_handoff, +) +from src.services.iwooos_wazuh_controlled_executor_handoff import ( + preview_iwooos_wazuh_controlled_executor_handoff as preview_wazuh_controlled_executor_handoff_payload, +) from src.services.iwooos_wazuh_fim_vulnerability_alert_verifier import ( load_latest_iwooos_wazuh_fim_vulnerability_alert_verifier, ) @@ -161,9 +167,9 @@ def _alertmanager_metric_fallback_readback(*, source_error: str) -> dict[str, An "source_status": "source_unavailable", "source_error": source_error, "fallback_source": "alert_chain_last_success_metric", - "fallback_age_seconds": round(age_seconds, 3) - if last_success_timestamp > 0 - else None, + "fallback_age_seconds": ( + round(age_seconds, 3) if last_success_timestamp > 0 else None + ), } return { @@ -755,6 +761,73 @@ async def validate_iwooos_wazuh_incident_response_closure_packet( ) from exc +@router.get( + "/api/v1/iwooos/wazuh-controlled-executor-handoff-readback", + response_model=dict[str, Any], + summary="取得 Wazuh controlled executor handoff bridge 讀回", + description=( + "彙整 Wazuh incident response same-run closure contract 與 AI controlled executor " + "handoff runway,形成 P0-03 進入受控 executor 前的公開安全 handoff preview。" + "此端點不保存 payload、不 dispatch executor、不查 live Wazuh API、不讀主機、不讀或回傳" + "機密明文、不啟用主動回應、不改 Nginx / Docker / K8s / firewall。" + ), +) +async def get_iwooos_wazuh_controlled_executor_handoff_readback() -> dict[str, Any]: + """回傳 Wazuh controlled executor handoff bridge 公開安全讀回。""" + try: + alert_receipt_readback = await _alertmanager_receipt_readback() + payload = await asyncio.to_thread( + load_latest_iwooos_wazuh_controlled_executor_handoff, + alert_receipt_readback=alert_receipt_readback, + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS Wazuh controlled executor handoff 讀回無效:{exc}", + ) from exc + + +@router.post( + "/api/v1/iwooos/wazuh-controlled-executor-handoff/preview", + response_model=dict[str, Any], + summary="預覽 Wazuh incident packet 的 controlled executor handoff", + description=( + "針對單次 redacted Wazuh incident response same-run packet 進行 no-persist handoff " + "preview,先重用 incident response closure validator,再回傳受控 executor 候選路由、" + "必要輸入、blocked actions 與下一個 verifier gate。此端點不保存 payload、不 dispatch executor、" + "不查 live Wazuh API、不讀主機、不讀或回傳機密明文、不啟用主動回應、不改 Nginx / Docker / K8s / firewall。" + ), +) +async def preview_iwooos_wazuh_controlled_executor_handoff( + incident_response_packet: dict[str, Any], +) -> dict[str, Any]: + """回傳單次 Wazuh incident packet 的受控 executor handoff preview。""" + try: + alert_receipt_readback = await _alertmanager_receipt_readback() + payload = await asyncio.to_thread( + preview_wazuh_controlled_executor_handoff_payload, + incident_response_packet, + alert_receipt_readback=alert_receipt_readback, + ) + return redact_public_lan_topology(payload) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"IwoooS Wazuh controlled executor handoff preview 無效:{exc}", + ) from exc + + @router.get( "/api/v1/iwooos/wazuh-runtime-gate-owner-review-readback", response_model=dict[str, Any], diff --git a/apps/api/src/services/iwooos_security_tool_closure_readback.py b/apps/api/src/services/iwooos_security_tool_closure_readback.py index c7f6d211b..dc502540b 100644 --- a/apps/api/src/services/iwooos_security_tool_closure_readback.py +++ b/apps/api/src/services/iwooos_security_tool_closure_readback.py @@ -19,6 +19,9 @@ from src.services.iwooos_security_operating_system import ( from src.services.iwooos_wazuh_allowlisted_check_mode_dry_run import ( load_latest_iwooos_wazuh_allowlisted_check_mode_dry_run, ) +from src.services.iwooos_wazuh_controlled_executor_handoff import ( + load_latest_iwooos_wazuh_controlled_executor_handoff, +) from src.services.iwooos_wazuh_fim_vulnerability_alert_verifier import ( load_latest_iwooos_wazuh_fim_vulnerability_alert_verifier, ) @@ -66,6 +69,11 @@ def load_latest_iwooos_security_tool_closure_readback( alert_receipt_readback=alert_receipt_readback ) ) + wazuh_controlled_executor_handoff = ( + load_latest_iwooos_wazuh_controlled_executor_handoff( + alert_receipt_readback=alert_receipt_readback + ) + ) source_payloads = [ coverage, @@ -77,6 +85,7 @@ def load_latest_iwooos_security_tool_closure_readback( dry_run, wazuh_verifier, wazuh_incident_response_closure, + wazuh_controlled_executor_handoff, ] _require_runtime_boundaries(source_payloads) @@ -89,8 +98,9 @@ def load_latest_iwooos_security_tool_closure_readback( "owner_review": _summary(owner_review), "dry_run": _summary(dry_run), "wazuh_verifier": _summary(wazuh_verifier), - "wazuh_incident_response_closure": _summary( - wazuh_incident_response_closure + "wazuh_incident_response_closure": _summary(wazuh_incident_response_closure), + "wazuh_controlled_executor_handoff": _summary( + wazuh_controlled_executor_handoff ), } tracks = _tool_tracks(source_summaries) @@ -145,6 +155,7 @@ def load_latest_iwooos_security_tool_closure_readback( *dry_run.get("source_refs", []), *wazuh_verifier.get("source_refs", []), *wazuh_incident_response_closure.get("source_refs", []), + *wazuh_controlled_executor_handoff.get("source_refs", []), ], } @@ -289,7 +300,9 @@ def _tool_tracks(source_summaries: dict[str, dict[str, Any]]) -> list[dict[str, required_signal_count=4, next_executable_action="stage_rule_dry_run_event_redaction_and_wazuh_correlation", required_verifier="runtime_detection_event_post_verifier", - source_refs=["docs/security/monitoring-alerting-observability-inventory.snapshot.json"], + source_refs=[ + "docs/security/monitoring-alerting-observability-inventory.snapshot.json" + ], ), _track( track_id="web_api_dast", @@ -311,7 +324,9 @@ def _tool_tracks(source_summaries: dict[str, dict[str, Any]]) -> list[dict[str, required_signal_count=4, next_executable_action="normalize_wazuh_alertmanager_app_events_into_review_cards", required_verifier="sigma_ocsf_event_mapping_regression_verifier", - source_refs=["docs/security/iwooos-security-operating-system.snapshot.json"], + source_refs=[ + "docs/security/iwooos-security-operating-system.snapshot.json" + ], ), _track( track_id="ai_agent_controlled_apply", @@ -351,7 +366,11 @@ def _track( ) -> dict[str, Any]: ready_count = max(min(ready_signal_count, required_signal_count), 0) missing_count = max(required_signal_count - ready_count, 0) - source_readiness_percent = int(round((ready_count / required_signal_count) * 100)) if required_signal_count else 0 + source_readiness_percent = ( + int(round((ready_count / required_signal_count) * 100)) + if required_signal_count + else 0 + ) source_ready = missing_count == 0 if runtime_closed: runtime_closure_state = "runtime_closed" @@ -404,6 +423,9 @@ def _summary_rollup( wazuh_incident_response_closure = source_summaries[ "wazuh_incident_response_closure" ] + wazuh_controlled_executor_handoff = source_summaries[ + "wazuh_controlled_executor_handoff" + ] expected_hosts = max( _int(managed_hosts.get("expected_host_scope_count")), @@ -425,20 +447,18 @@ def _summary_rollup( 1 for track in tracks if track.get("runtime_closed") is True ) source_readiness_percent = ( - int(round((total_ready / total_required) * 100)) - if total_required - else 0 + int(round((total_ready / total_required) * 100)) if total_required else 0 ) runtime_closure_percent = ( - int(round((runtime_closed_track_count / len(tracks)) * 100)) - if tracks - else 0 + int(round((runtime_closed_track_count / len(tracks)) * 100)) if tracks else 0 ) return { "source_readback_count": len(source_summaries), "tool_track_count": len(tracks), - "p0_tool_track_count": sum(1 for track in tracks if track.get("priority") == "P0"), + "p0_tool_track_count": sum( + 1 for track in tracks if track.get("priority") == "P0" + ), "source_ready_tool_track_count": source_ready_track_count, "closed_tool_track_count": runtime_closed_track_count, "partial_tool_track_count": sum( @@ -459,7 +479,9 @@ def _summary_rollup( "tool_closure_percent": runtime_closure_percent, "wazuh_expected_host_scope_count": expected_hosts, "wazuh_manager_registry_accepted_count": accepted_hosts, - "wazuh_registry_complete_count": 1 if expected_hosts > 0 and accepted_hosts >= expected_hosts else 0, + "wazuh_registry_complete_count": ( + 1 if expected_hosts > 0 and accepted_hosts >= expected_hosts else 0 + ), "wazuh_fim_vulnerability_alert_closed_count": _int( wazuh_verifier.get("wazuh_fim_vulnerability_alert_closed_count") ), @@ -469,9 +491,7 @@ def _summary_rollup( ) ), "wazuh_fim_vulnerability_alert_runtime_closed_count": _int( - wazuh_verifier.get( - "wazuh_fim_vulnerability_alert_runtime_closed_count" - ) + wazuh_verifier.get("wazuh_fim_vulnerability_alert_runtime_closed_count") ), "wazuh_fim_vulnerability_alert_verifier_ready_count": _int( wazuh_verifier.get("wazuh_fim_vulnerability_alert_verifier_ready_count") @@ -500,6 +520,30 @@ def _summary_rollup( "wazuh_incident_response_runtime_closed_count" ) ), + "wazuh_controlled_executor_handoff_preview_ready_count": _int( + wazuh_controlled_executor_handoff.get( + "controlled_executor_handoff_preview_ready_count" + ) + ), + "wazuh_controlled_executor_route_ready_count": _int( + wazuh_controlled_executor_handoff.get( + "controlled_executor_route_ready_count" + ) + ), + "wazuh_controlled_executor_post_verifier_binding_count": _int( + wazuh_controlled_executor_handoff.get("post_action_verifier_binding_count") + ), + "wazuh_controlled_executor_learning_writeback_contract_count": _int( + wazuh_controlled_executor_handoff.get("learning_writeback_contract_count") + ), + "wazuh_controlled_executor_dispatch_enabled_count": _int( + wazuh_controlled_executor_handoff.get( + "bounded_executor_dispatch_enabled_count" + ) + ), + "wazuh_controlled_executor_dispatch_count": _int( + wazuh_controlled_executor_handoff.get("controlled_executor_dispatch_count") + ), "alert_receipt_observed_count": _int( security_os.get("alert_receipt_observed_count") ), @@ -543,6 +587,9 @@ def _next_executable_queue( wazuh_incident_response_closure = source_summaries[ "wazuh_incident_response_closure" ] + wazuh_controlled_executor_handoff = source_summaries[ + "wazuh_controlled_executor_handoff" + ] controlled_apply = source_summaries["controlled_apply"] dry_run = source_summaries["dry_run"] expected_hosts = max( @@ -562,11 +609,7 @@ def _next_executable_queue( > 0 ) runtime_closed = ( - _int( - wazuh_verifier.get( - "wazuh_fim_vulnerability_alert_runtime_closed_count" - ) - ) + _int(wazuh_verifier.get("wazuh_fim_vulnerability_alert_runtime_closed_count")) > 0 ) same_run_candidate_ready = ( @@ -618,9 +661,7 @@ def _next_executable_queue( wazuh_incident_response_closure.get("same_run_required_stage_count") ), "same_run_source_stage_ready_count": _int( - wazuh_incident_response_closure.get( - "same_run_source_stage_ready_count" - ) + wazuh_incident_response_closure.get("same_run_source_stage_ready_count") ), "same_run_closure_candidate_ready_count": _int( wazuh_incident_response_closure.get( @@ -631,6 +672,17 @@ def _next_executable_queue( "/api/v1/iwooos/wazuh-incident-response-closure/" "validate-incident-response-packet" ), + "controlled_executor_handoff_readback_endpoint": ( + "/api/v1/iwooos/wazuh-controlled-executor-handoff-readback" + ), + "controlled_executor_handoff_preview_endpoint": ( + "/api/v1/iwooos/wazuh-controlled-executor-handoff/preview" + ), + "controlled_executor_handoff_preview_ready_count": _int( + wazuh_controlled_executor_handoff.get( + "controlled_executor_handoff_preview_ready_count" + ) + ), "next_gate_after_packet_acceptance": ( "route_to_iwooos_controlled_executor_for_bounded_execution_and_post_verifier_writeback" ), @@ -687,12 +739,12 @@ def _require_runtime_boundaries(payloads: list[dict[str, Any]]) -> None: summary = _summary(payload) for key in _REQUIRED_ZERO_SUMMARY_KEYS: if key in summary and _int(summary.get(key)) != 0: - raise ValueError(f"{payload.get('schema_version')}: summary.{key} must remain 0") + raise ValueError( + f"{payload.get('schema_version')}: summary.{key} must remain 0" + ) boundaries = payload.get("boundaries") if isinstance(boundaries, dict): - for key in ( - "secret_value_collection_allowed", - ): + for key in ("secret_value_collection_allowed",): if key in boundaries and boundaries.get(key) is not False: raise ValueError( f"{payload.get('schema_version')}: boundaries.{key} must remain false" @@ -711,6 +763,9 @@ def _boundary_markers(summary: dict[str, Any]) -> list[str]: f"iwooos_security_tool_closure_alert_receipt_accepted_count={summary['alert_receipt_accepted_count']}", "iwooos_security_tool_closure_controlled_apply_policy_active=true", f"iwooos_security_tool_closure_controlled_apply_candidate_ready_count={summary['controlled_apply_candidate_ready_count']}", + f"iwooos_wazuh_controlled_executor_handoff_preview_ready_count={summary['wazuh_controlled_executor_handoff_preview_ready_count']}", + f"iwooos_wazuh_controlled_executor_dispatch_enabled_count={summary['wazuh_controlled_executor_dispatch_enabled_count']}", + f"iwooos_wazuh_controlled_executor_dispatch_count={summary['wazuh_controlled_executor_dispatch_count']}", "owner_review_required_for_low_medium_high=false", "runtime_execution_performed=false", "secret_value_collection_allowed=false", diff --git a/apps/api/src/services/iwooos_wazuh_controlled_executor_handoff.py b/apps/api/src/services/iwooos_wazuh_controlled_executor_handoff.py new file mode 100644 index 000000000..bc0e9ebea --- /dev/null +++ b/apps/api/src/services/iwooos_wazuh_controlled_executor_handoff.py @@ -0,0 +1,388 @@ +""" +IwoooS Wazuh controlled executor handoff readback. + +This service bridges the P0-03 Wazuh incident response packet validator to the +existing AI controlled-executor runway. It only produces a public-safe handoff +preview: no payload persistence, live Wazuh query, active response, host write, +secret read, or executor dispatch is performed here. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from src.services.ai_agent_controlled_executor_handoff import ( + load_latest_ai_agent_controlled_executor_handoff, +) +from src.services.iwooos_wazuh_incident_response_closure import ( + load_latest_iwooos_wazuh_incident_response_closure, +) +from src.services.iwooos_wazuh_incident_response_closure import ( + validate_iwooos_wazuh_incident_response_closure_packet, +) + +_SCHEMA_VERSION = "iwooos_wazuh_controlled_executor_handoff_readback_v1" +_PREVIEW_SCHEMA_VERSION = "iwooos_wazuh_controlled_executor_handoff_preview_result_v1" +_ROUTE_ID = "iwooos_wazuh_incident_response_controlled_executor" +_VALIDATION_ENDPOINT = ( + "/api/v1/iwooos/wazuh-incident-response-closure/" + "validate-incident-response-packet" +) +_PREVIEW_ENDPOINT = "/api/v1/iwooos/wazuh-controlled-executor-handoff/preview" +_READBACK_ENDPOINT = "/api/v1/iwooos/wazuh-controlled-executor-handoff-readback" + +_RUNWAY_STAGES = ( + "incident_packet_review", + "controlled_executor_handoff_preview", + "bounded_executor_dispatch", + "independent_post_verifier", + "km_rag_mcp_playbook_writeback", +) + + +def load_latest_iwooos_wazuh_controlled_executor_handoff( + security_dir: Path | None = None, + alert_receipt_readback: dict[str, Any] | None = None, + executor_handoff_readback: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Return the no-write handoff bridge between Wazuh P0-03 and executor runway.""" + incident_contract = load_latest_iwooos_wazuh_incident_response_closure( + security_dir=security_dir, + alert_receipt_readback=alert_receipt_readback, + ) + executor_handoff = ( + executor_handoff_readback or load_latest_ai_agent_controlled_executor_handoff() + ) + incident_summary = _summary(incident_contract) + executor_rollups = _rollups(executor_handoff) + executor_truth = _truth(executor_handoff) + + handoff_preview_ready = ( + _int(incident_summary.get("closure_packet_validator_available_count")) > 0 + and _int(executor_rollups.get("ready_for_controlled_executor_count")) > 0 + and _int(executor_rollups.get("post_action_verifier_binding_count")) > 0 + and _int(executor_rollups.get("learning_writeback_contract_count")) > 0 + and executor_truth.get("controlled_executor_dispatch_enabled") is False + ) + runtime_closed = ( + _int(incident_summary.get("wazuh_incident_response_runtime_closed_count")) > 0 + ) + + summary = { + "incident_response_contract_ready_count": 1, + "incident_response_packet_validator_available_count": _int( + incident_summary.get("closure_packet_validator_available_count") + ), + "same_run_source_stage_ready_count": _int( + incident_summary.get("same_run_source_stage_ready_count") + ), + "same_run_required_stage_count": _int( + incident_summary.get("same_run_required_stage_count") + ), + "same_run_closure_candidate_ready_count": _int( + incident_summary.get("same_run_closure_candidate_ready_count") + ), + "controlled_executor_handoff_preview_ready_count": ( + 1 if handoff_preview_ready else 0 + ), + "controlled_executor_route_ready_count": _int( + executor_rollups.get("executor_route_count") + ), + "ready_for_controlled_executor_packet_count": _int( + executor_rollups.get("ready_for_controlled_executor_count") + ), + "post_action_verifier_binding_count": _int( + executor_rollups.get("post_action_verifier_binding_count") + ), + "learning_writeback_contract_count": _int( + executor_rollups.get("learning_writeback_contract_count") + ), + "critical_break_glass_count": _int( + executor_rollups.get("critical_break_glass_count") + ), + "bounded_executor_dispatch_enabled_count": 0, + "controlled_executor_dispatch_count": 0, + "runtime_execution_performed_count": 1 if runtime_closed else 0, + "wazuh_incident_response_runtime_closed_count": 1 if runtime_closed else 0, + "runtime_gate_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + } + + return { + "schema_version": _SCHEMA_VERSION, + "status": ( + "wazuh_controlled_executor_handoff_preview_ready_runtime_open" + if handoff_preview_ready and not runtime_closed + else "wazuh_controlled_executor_handoff_preview_unavailable" + ), + "mode": "handoff_preview_only_no_dispatch_no_live_apply", + "summary": summary, + "runway": _runway(summary), + "controlled_executor_candidate": _candidate(summary), + "incident_response_packet_validation_endpoint": _VALIDATION_ENDPOINT, + "controlled_executor_handoff_preview_endpoint": _PREVIEW_ENDPOINT, + "controlled_executor_handoff_readback_endpoint": _READBACK_ENDPOINT, + "next_executable_action": ( + "preview_redacted_wazuh_incident_packet_for_controlled_executor_handoff" + ), + "next_gate_after_preview": ( + "bounded_executor_dispatch_requires_runtime_executor_receipt_and_independent_post_verifier" + ), + "boundary_markers": _boundary_markers(summary), + "boundaries": _boundaries(), + "no_false_green_rules": [ + "handoff preview readiness is not executor dispatch", + "runtime closure remains zero until bounded execution and post-verifier receipts are read back in production", + "this endpoint never enables Wazuh active response or host writes", + "critical break-glass packets remain excluded from automated dispatch", + ], + "source_refs": [ + *incident_contract.get("source_refs", []), + "apps/api/src/services/ai_agent_controlled_executor_handoff.py", + "apps/api/src/services/iwooos_wazuh_controlled_executor_handoff.py", + ], + } + + +def preview_iwooos_wazuh_controlled_executor_handoff( + packet: dict[str, Any], + security_dir: Path | None = None, + alert_receipt_readback: dict[str, Any] | None = None, +) -> dict[str, Any]: + """Validate a redacted packet and return a no-dispatch handoff preview.""" + readback = load_latest_iwooos_wazuh_controlled_executor_handoff( + security_dir=security_dir, + alert_receipt_readback=alert_receipt_readback, + ) + validation = validate_iwooos_wazuh_incident_response_closure_packet( + packet, + security_dir=security_dir, + alert_receipt_readback=alert_receipt_readback, + ) + accepted = ( + validation.get("accepted_for_incident_response_closure_review_only") is True + ) + runtime_rejected = validation.get("runtime_action_rejected") is True + quarantined = validation.get("quarantined") is True + preview_ready = ( + accepted + and _int( + readback["summary"].get("controlled_executor_handoff_preview_ready_count") + ) + > 0 + ) + + summary = { + "incident_response_packet_received_count": 1, + "incident_response_packet_review_ready_count": 1 if accepted else 0, + "controlled_executor_handoff_preview_ready_count": 1 if preview_ready else 0, + "same_run_required_stage_count": _int( + validation["summary"].get("same_run_required_stage_count") + ), + "same_run_packet_stage_ready_count": _int( + validation["summary"].get("same_run_packet_stage_ready_count") + ), + "bounded_executor_dispatch_enabled_count": 0, + "controlled_executor_dispatch_count": 0, + "runtime_execution_performed_count": 0, + "wazuh_incident_response_runtime_closed_count": 0, + "runtime_gate_count": 0, + "wazuh_api_live_query_authorized_count": 0, + "wazuh_active_response_authorized_count": 0, + "host_write_authorized_count": 0, + "secret_value_collection_allowed_count": 0, + } + + return { + "schema_version": _PREVIEW_SCHEMA_VERSION, + "contract_schema_version": readback["schema_version"], + "packet_validation_schema_version": validation["schema_version"], + "status": ( + "ready_for_iwooos_wazuh_controlled_executor_handoff_preview_only" + if preview_ready + else str(validation.get("status") or "handoff_preview_blocked") + ), + "mode": "preview_only_no_persist_no_dispatch_no_live_apply", + "accepted_for_controlled_executor_handoff_preview_only": preview_ready, + "quarantined": quarantined, + "runtime_action_rejected": runtime_rejected, + "summary": summary, + "validation_status": validation.get("status"), + "validation_findings": validation.get("validation_findings", []), + "controlled_executor_candidate": _candidate(summary), + "packet_field_bindings": ( + _packet_field_bindings(packet) if preview_ready else [] + ), + "next_gate": ( + "bounded_executor_dispatch_requires_runtime_executor_receipt_and_independent_post_verifier" + if preview_ready + else "fix_wazuh_incident_response_packet_before_handoff_preview" + ), + "boundary_markers": _boundary_markers(summary), + "boundaries": _boundaries(), + } + + +def _summary(payload: dict[str, Any]) -> dict[str, Any]: + summary = payload.get("summary") + return summary if isinstance(summary, dict) else {} + + +def _rollups(payload: dict[str, Any]) -> dict[str, Any]: + rollups = payload.get("rollups") + return rollups if isinstance(rollups, dict) else {} + + +def _truth(payload: dict[str, Any]) -> dict[str, Any]: + truth = payload.get("handoff_truth") + return truth if isinstance(truth, dict) else {} + + +def _int(value: Any) -> int: + return value if isinstance(value, int) else 0 + + +def _runway(summary: dict[str, Any]) -> list[dict[str, Any]]: + return [ + { + "stage_id": "incident_packet_review", + "label": "Incident packet", + "ready_count": _int( + summary.get("incident_response_packet_validator_available_count") + ), + "required_count": 1, + "status": "ready", + }, + { + "stage_id": "controlled_executor_handoff_preview", + "label": "Executor handoff preview", + "ready_count": _int( + summary.get("controlled_executor_handoff_preview_ready_count") + ), + "required_count": 1, + "status": ( + "ready" + if _int(summary.get("controlled_executor_handoff_preview_ready_count")) + else "waiting" + ), + }, + { + "stage_id": "bounded_executor_dispatch", + "label": "Bounded dispatch", + "ready_count": 0, + "required_count": 1, + "status": "runtime_receipt_required", + }, + { + "stage_id": "independent_post_verifier", + "label": "Post verifier", + "ready_count": 0, + "required_count": 1, + "status": "runtime_receipt_required", + }, + { + "stage_id": "km_rag_mcp_playbook_writeback", + "label": "Learning writeback", + "ready_count": 0, + "required_count": 1, + "status": "runtime_receipt_required", + }, + ] + + +def _candidate(summary: dict[str, Any]) -> dict[str, Any]: + return { + "route_id": _ROUTE_ID, + "risk_tier": "high", + "handoff_status": ( + "ready_for_controlled_executor_handoff_preview" + if _int(summary.get("controlled_executor_handoff_preview_ready_count")) + else "waiting_for_validated_incident_packet" + ), + "controlled_executor_handoff_allowed": True, + "bounded_executor_dispatch_enabled": False, + "runtime_write_performed": False, + "side_effect_count": 0, + "required_inputs": [ + "redacted_wazuh_event_receipt_ref", + "redacted_forensics_evidence_ref", + "incident_case_ref", + "candidate_controlled_response_ref", + "source_truth_diff_ref", + "risk_policy_decision_ref", + "check_mode_dry_run_ref", + "rollback_plan_ref", + "post_verifier_ref", + "km_rag_mcp_playbook_writeback_ref", + ], + "blocked_actions": [ + "live_wazuh_query", + "wazuh_active_response", + "host_write", + "firewall_change", + "secret_value_read", + "executor_dispatch_by_readback_endpoint", + ], + } + + +def _packet_field_bindings(packet: dict[str, Any]) -> list[dict[str, str]]: + allowed_fields = ( + "trace_id", + "run_id", + "wazuh_event_receipt_ref", + "forensics_evidence_ref", + "incident_case_ref", + "candidate_controlled_response_ref", + "check_mode_dry_run_ref", + "rollback_plan_ref", + "post_verifier_ref", + "km_rag_mcp_playbook_writeback_ref", + ) + return [ + {"field": field, "binding_status": "present"} + for field in allowed_fields + if packet.get(field) + ] + + +def _boundary_markers(summary: dict[str, Any]) -> list[str]: + return [ + "iwooos_wazuh_controlled_executor_handoff_preview_available=true", + ( + "iwooos_wazuh_controlled_executor_handoff_preview_ready_count=" + f"{_int(summary.get('controlled_executor_handoff_preview_ready_count'))}" + ), + "iwooos_wazuh_controlled_executor_dispatch_enabled_count=0", + "iwooos_wazuh_controlled_executor_dispatch_count=0", + "iwooos_wazuh_controlled_executor_runtime_closed_count=0", + "wazuh_api_live_query_authorized=false", + "wazuh_active_response_authorized=false", + "host_write_authorized=false", + "secret_value_collection_allowed=false", + "not_authorization=true", + ] + + +def _boundaries() -> dict[str, Any]: + return { + "payload_persisted": False, + "live_host_query_performed": False, + "live_wazuh_query_performed": False, + "scanner_execution_performed": False, + "bounded_executor_dispatch_performed": False, + "runtime_execution_performed": False, + "runtime_execution_authorized": False, + "runtime_gate_open": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "firewall_change_authorized": False, + "secret_value_collection_allowed": False, + "readback_endpoint_is_executor": False, + "not_authorization": True, + } diff --git a/apps/api/tests/test_iwooos_security_tool_closure_readback.py b/apps/api/tests/test_iwooos_security_tool_closure_readback.py index 378b83853..990aca422 100644 --- a/apps/api/tests/test_iwooos_security_tool_closure_readback.py +++ b/apps/api/tests/test_iwooos_security_tool_closure_readback.py @@ -24,7 +24,7 @@ def test_iwooos_security_tool_closure_readback_rolls_up_tool_tracks() -> None: assert payload["schema_version"] == "iwooos_security_tool_closure_readback_v2" assert payload["status"] == "tool_source_readiness_available_runtime_closure_open" - assert payload["summary"]["source_readback_count"] == 9 + assert payload["summary"]["source_readback_count"] == 10 assert payload["summary"]["tool_track_count"] == 8 assert payload["summary"]["p0_tool_track_count"] == 3 assert payload["summary"]["wazuh_expected_host_scope_count"] == 6 @@ -35,26 +35,23 @@ def test_iwooos_security_tool_closure_readback_rolls_up_tool_tracks() -> None: assert payload["summary"]["tool_runtime_closure_percent"] == 0 assert payload["summary"]["tool_closure_percent"] == 0 assert payload["summary"]["tool_source_readiness_percent"] > 0 - assert ( - payload["summary"]["wazuh_fim_vulnerability_alert_verifier_ready_count"] - == 0 - ) + assert payload["summary"]["wazuh_fim_vulnerability_alert_verifier_ready_count"] == 0 assert ( payload["summary"]["wazuh_fim_vulnerability_alert_verifier_ready_signal_count"] == 5 ) assert ( - payload["summary"][ - "wazuh_incident_response_closure_validator_available_count" - ] + payload["summary"]["wazuh_incident_response_closure_validator_available_count"] == 1 ) assert ( - payload["summary"][ - "wazuh_incident_response_same_run_required_stage_count" - ] - == 9 + payload["summary"]["wazuh_incident_response_same_run_required_stage_count"] == 9 ) + assert ( + payload["summary"]["wazuh_controlled_executor_handoff_preview_ready_count"] == 1 + ) + assert payload["summary"]["wazuh_controlled_executor_dispatch_enabled_count"] == 0 + assert payload["summary"]["wazuh_controlled_executor_dispatch_count"] == 0 assert payload["summary"]["supply_chain_scan_closed_count"] == 0 assert payload["summary"]["runtime_detection_closed_count"] == 0 assert payload["summary"]["web_api_dast_closed_count"] == 0 @@ -83,10 +80,7 @@ def test_iwooos_security_tool_closure_readback_uses_controlled_apply_policy() -> assert payload["boundaries"]["medium_risk_controlled_apply_allowed"] is True assert payload["boundaries"]["high_risk_controlled_apply_allowed"] is True assert payload["boundaries"]["critical_break_glass_required"] is True - assert ( - payload["boundaries"]["owner_review_required_for_low_medium_high"] - is False - ) + assert payload["boundaries"]["owner_review_required_for_low_medium_high"] is False assert payload["boundaries"]["secret_value_collection_allowed"] is False assert payload["boundaries"]["readback_endpoint_is_executor"] is False assert all( @@ -97,14 +91,18 @@ def test_iwooos_security_tool_closure_readback_uses_controlled_apply_policy() -> track["runtime_execution_performed"] is False for track in payload["tool_tracks"] ) - assert all(track["owner_review_required"] is False for track in payload["tool_tracks"]) + assert all( + track["owner_review_required"] is False for track in payload["tool_tracks"] + ) assert all( track["secret_value_collection_allowed"] is False for track in payload["tool_tracks"] ) -def test_iwooos_security_tool_closure_readback_marks_partial_vs_missing_tracks() -> None: +def test_iwooos_security_tool_closure_readback_marks_partial_vs_missing_tracks() -> ( + None +): payload = load_latest_iwooos_security_tool_closure_readback() tracks = {track["track_id"]: track for track in payload["tool_tracks"]} @@ -135,12 +133,21 @@ def test_iwooos_security_tool_closure_queue_has_executable_receipt_contract() -> assert queue["P0-03"]["incident_response_packet_validation_endpoint"].endswith( "/validate-incident-response-packet" ) + assert queue["P0-03"]["controlled_executor_handoff_readback_endpoint"].endswith( + "/wazuh-controlled-executor-handoff-readback" + ) + assert queue["P0-03"]["controlled_executor_handoff_preview_endpoint"].endswith( + "/wazuh-controlled-executor-handoff/preview" + ) + assert queue["P0-03"]["controlled_executor_handoff_preview_ready_count"] == 1 assert queue["P0-03"]["required_verifier"] == ( "wazuh_registry_fim_vulnerability_alert_receipt_post_verifier" ) assert all(item["next_executable_action"] for item in queue.values()) assert all(item["required_signal_count"] > 0 for item in queue.values()) - assert all(item["controlled_apply_policy_allowed"] is True for item in queue.values()) + assert all( + item["controlled_apply_policy_allowed"] is True for item in queue.values() + ) def test_iwooos_security_tool_closure_readback_accepts_alert_receipt_signal() -> None: @@ -157,20 +164,12 @@ def test_iwooos_security_tool_closure_readback_accepts_alert_receipt_signal() -> assert payload["summary"]["alert_receipt_accepted_count"] == 1 assert payload["summary"]["alert_receipt_observed_count"] == 1 assert payload["summary"]["wazuh_fim_vulnerability_alert_closed_count"] == 0 + assert payload["summary"]["wazuh_fim_vulnerability_alert_verifier_ready_count"] == 1 assert ( - payload["summary"]["wazuh_fim_vulnerability_alert_verifier_ready_count"] + payload["summary"]["wazuh_fim_vulnerability_alert_source_verifier_ready_count"] == 1 ) - assert ( - payload["summary"][ - "wazuh_fim_vulnerability_alert_source_verifier_ready_count" - ] - == 1 - ) - assert ( - payload["summary"]["wazuh_fim_vulnerability_alert_runtime_closed_count"] - == 0 - ) + assert payload["summary"]["wazuh_fim_vulnerability_alert_runtime_closed_count"] == 0 assert tracks["wazuh_detection_response"]["ready_signal_count"] == 6 assert tracks["wazuh_detection_response"]["missing_signal_count"] == 0 assert tracks["wazuh_detection_response"]["closure_state"] == ( @@ -211,16 +210,17 @@ def test_iwooos_security_tool_closure_readback_api_is_public_safe(monkeypatch) - assert response.status_code == 200 data = response.json() assert data["schema_version"] == "iwooos_security_tool_closure_readback_v2" - assert data["summary"]["source_readback_count"] == 9 + assert data["summary"]["source_readback_count"] == 10 assert data["summary"]["tool_track_count"] == 8 assert data["summary"]["alert_receipt_accepted_count"] == 1 assert data["summary"]["wazuh_fim_vulnerability_alert_closed_count"] == 0 assert data["summary"]["tool_runtime_closure_percent"] == 0 assert data["summary"]["controlled_apply_policy_active_count"] == 1 + assert data["summary"]["wazuh_controlled_executor_handoff_preview_ready_count"] == 1 + assert data["summary"]["wazuh_controlled_executor_dispatch_count"] == 0 assert data["boundaries"]["controlled_apply_policy_active"] is True assert any( - item["track_id"] == "supply_chain_sbom_scan" - for item in data["tool_tracks"] + item["track_id"] == "supply_chain_sbom_scan" for item in data["tool_tracks"] ) assert "192.168.0." not in response.text assert "工作視窗" not in response.text diff --git a/apps/api/tests/test_iwooos_wazuh_controlled_executor_handoff.py b/apps/api/tests/test_iwooos_wazuh_controlled_executor_handoff.py new file mode 100644 index 000000000..43229391c --- /dev/null +++ b/apps/api/tests/test_iwooos_wazuh_controlled_executor_handoff.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import os + +os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test") + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.iwooos import router +from src.services.iwooos_wazuh_controlled_executor_handoff import ( + load_latest_iwooos_wazuh_controlled_executor_handoff, +) +from src.services.iwooos_wazuh_controlled_executor_handoff import ( + preview_iwooos_wazuh_controlled_executor_handoff, +) + + +def _client() -> TestClient: + app = FastAPI() + app.include_router(router) + return TestClient(app) + + +def _alert_receipt() -> dict[str, object]: + return { + "events": [{"provider_event_id": "alertmanager:received:wazuh-handoff"}], + "total": 1, + "limit": 5, + "source_status": "ready", + } + + +def _stage_receipts() -> list[dict[str, str]]: + return [ + {"stage_id": "sensor_source_receipt", "receipt_ref": "ref-sensor"}, + {"stage_id": "normalized_asset_identity", "receipt_ref": "ref-asset"}, + {"stage_id": "source_truth_diff", "receipt_ref": "ref-diff"}, + {"stage_id": "ai_decision_candidate_action", "receipt_ref": "ref-ai"}, + {"stage_id": "risk_policy_decision", "receipt_ref": "ref-policy"}, + {"stage_id": "check_mode_dry_run_receipt", "receipt_ref": "ref-dry-run"}, + {"stage_id": "bounded_execution_receipt", "receipt_ref": "ref-bounded"}, + {"stage_id": "independent_post_verifier", "receipt_ref": "ref-post"}, + { + "stage_id": "km_rag_mcp_playbook_write_ack", + "receipt_ref": "ref-km-rag-mcp-playbook", + }, + ] + + +def _valid_packet() -> dict[str, object]: + return { + "trace_id": "iwooos-wazuh-p0-03-trace-001", + "run_id": "iwooos-wazuh-p0-03-run-001", + "asset_scope_aliases": ["managed_core_node_a", "managed_core_node_b"], + "wazuh_event_receipt_ref": "ref-wazuh-event", + "forensics_evidence_ref": "ref-forensics", + "incident_case_ref": "ref-incident-case", + "ai_decision_ref": "ref-ai-decision", + "candidate_controlled_response_ref": "ref-controlled-response-candidate", + "source_truth_diff_ref": "ref-source-truth-diff", + "risk_policy_decision_ref": "ref-risk-policy", + "check_mode_dry_run_ref": "ref-check-mode-dry-run", + "bounded_execution_receipt_ref": "ref-bounded-execution", + "rollback_plan_ref": "ref-rollback-plan", + "post_verifier_ref": "ref-independent-post-verifier", + "km_rag_mcp_playbook_writeback_ref": "ref-km-rag-mcp-playbook", + "same_run_stage_receipts": _stage_receipts(), + "runtime_boundary_ack": ( + "runtime_gate_remains_closed_until_executor_post_verifier" + ), + "host_write_boundary_ack": "no_host_write_performed_by_validator", + "secret_boundary_ack": "no_secret_value_collected_or_submitted", + "live_wazuh_query_boundary_ack": ("no_live_wazuh_query_performed_by_validator"), + } + + +def test_wazuh_controlled_executor_handoff_readback_exposes_bridge() -> None: + payload = load_latest_iwooos_wazuh_controlled_executor_handoff( + alert_receipt_readback=_alert_receipt() + ) + + assert ( + payload["schema_version"] + == "iwooos_wazuh_controlled_executor_handoff_readback_v1" + ) + assert ( + payload["status"] + == "wazuh_controlled_executor_handoff_preview_ready_runtime_open" + ) + assert payload["summary"]["same_run_source_stage_ready_count"] == 5 + assert payload["summary"]["same_run_required_stage_count"] == 9 + assert payload["summary"]["controlled_executor_handoff_preview_ready_count"] == 1 + assert payload["summary"]["ready_for_controlled_executor_packet_count"] > 0 + assert payload["summary"]["post_action_verifier_binding_count"] > 0 + assert payload["summary"]["learning_writeback_contract_count"] > 0 + assert payload["summary"]["bounded_executor_dispatch_enabled_count"] == 0 + assert payload["summary"]["controlled_executor_dispatch_count"] == 0 + assert payload["summary"]["wazuh_incident_response_runtime_closed_count"] == 0 + assert payload["boundaries"]["bounded_executor_dispatch_performed"] is False + assert payload["boundaries"]["wazuh_active_response_authorized"] is False + assert payload["boundaries"]["host_write_authorized"] is False + + +def test_wazuh_controlled_executor_handoff_preview_accepts_redacted_packet() -> None: + payload = preview_iwooos_wazuh_controlled_executor_handoff( + _valid_packet(), + alert_receipt_readback=_alert_receipt(), + ) + + assert ( + payload["schema_version"] + == "iwooos_wazuh_controlled_executor_handoff_preview_result_v1" + ) + assert ( + payload["status"] + == "ready_for_iwooos_wazuh_controlled_executor_handoff_preview_only" + ) + assert payload["accepted_for_controlled_executor_handoff_preview_only"] is True + assert payload["summary"]["incident_response_packet_review_ready_count"] == 1 + assert payload["summary"]["controlled_executor_handoff_preview_ready_count"] == 1 + assert payload["summary"]["same_run_packet_stage_ready_count"] == 9 + assert payload["summary"]["bounded_executor_dispatch_enabled_count"] == 0 + assert payload["summary"]["wazuh_incident_response_runtime_closed_count"] == 0 + assert payload["controlled_executor_candidate"]["risk_tier"] == "high" + assert ( + payload["controlled_executor_candidate"]["handoff_status"] + == "ready_for_controlled_executor_handoff_preview" + ) + assert payload["controlled_executor_candidate"]["side_effect_count"] == 0 + assert "host_write" in payload["controlled_executor_candidate"]["blocked_actions"] + assert payload["boundaries"]["runtime_execution_authorized"] is False + + +def test_wazuh_controlled_executor_handoff_preview_rejects_runtime_action() -> None: + packet = _valid_packet() + packet["requested_actions"] = ["wazuh_active_response"] + + payload = preview_iwooos_wazuh_controlled_executor_handoff( + packet, + alert_receipt_readback=_alert_receipt(), + ) + + assert payload["status"] == "reject_runtime_action_request" + assert payload["runtime_action_rejected"] is True + assert payload["summary"]["controlled_executor_handoff_preview_ready_count"] == 0 + assert payload["summary"]["wazuh_active_response_authorized_count"] == 0 + + +def test_wazuh_controlled_executor_handoff_api_is_public_safe(monkeypatch) -> None: + async def fake_recent_events(**_: object) -> dict[str, object]: + return _alert_receipt() + + monkeypatch.setattr( + "src.api.v1.iwooos.list_recent_channel_events", + fake_recent_events, + ) + + client = _client() + readback = client.get("/api/v1/iwooos/wazuh-controlled-executor-handoff-readback") + preview = client.post( + "/api/v1/iwooos/wazuh-controlled-executor-handoff/preview", + json=_valid_packet(), + ) + + assert readback.status_code == 200 + assert preview.status_code == 200 + assert ( + readback.json()["summary"]["controlled_executor_handoff_preview_ready_count"] + == 1 + ) + assert ( + preview.json()["accepted_for_controlled_executor_handoff_preview_only"] is True + ) + assert preview.json()["summary"]["controlled_executor_dispatch_count"] == 0 + assert "192.168.0." not in readback.text + assert "192.168.0." not in preview.text + assert "WAZUH_API_PASSWORD" not in preview.text + assert "批准!繼續" not in preview.text diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 715aa0251..4326cc7b5 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -13986,7 +13986,19 @@ "missing": "missing", "action": "next", "verifier": "verifier", - "sameRun": "same-run" + "sameRun": "same-run", + "handoff": "handoff", + "dispatch": "dispatch" + }, + "runway": { + "source": "Source", + "packet": "Packet", + "handoff": "Handoff", + "runtime": "Runtime", + "ready": "ready", + "waiting": "waiting", + "closed": "closed", + "blocked": "blocked" }, "guards": { "runtimeActions": "runtime action", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 0d3888457..f093df8b9 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -13986,7 +13986,19 @@ "missing": "缺口", "action": "下一步", "verifier": "驗證器", - "sameRun": "same-run" + "sameRun": "same-run", + "handoff": "handoff", + "dispatch": "dispatch" + }, + "runway": { + "source": "來源", + "packet": "Packet", + "handoff": "Handoff", + "runtime": "Runtime", + "ready": "ready", + "waiting": "waiting", + "closed": "closed", + "blocked": "blocked" }, "guards": { "runtimeActions": "runtime action", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index 17e09dfb1..ad4b5123b 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -531,7 +531,7 @@ type IwoooSSecurityToolClosureSummary = IwoooSSecurityToolClosureReadbackRespons type IwoooSSecurityToolClosureQueueEntry = IwoooSSecurityToolClosureReadbackResponse['next_executable_queue'][number] const SECURITY_TOOL_CLOSURE_BOOTSTRAP_SUMMARY: IwoooSSecurityToolClosureSummary = { - source_readback_count: 9, + source_readback_count: 10, tool_track_count: 8, p0_tool_track_count: 3, source_ready_tool_track_count: 2, @@ -571,6 +571,12 @@ const SECURITY_TOOL_CLOSURE_BOOTSTRAP_SUMMARY: IwoooSSecurityToolClosureSummary wazuh_incident_response_same_run_required_stage_count: 9, wazuh_incident_response_same_run_candidate_ready_count: 0, wazuh_incident_response_runtime_closed_count: 0, + wazuh_controlled_executor_handoff_preview_ready_count: 1, + wazuh_controlled_executor_route_ready_count: 5, + wazuh_controlled_executor_post_verifier_binding_count: 5, + wazuh_controlled_executor_learning_writeback_contract_count: 3, + wazuh_controlled_executor_dispatch_enabled_count: 0, + wazuh_controlled_executor_dispatch_count: 0, } const SECURITY_TOOL_CLOSURE_BOOTSTRAP_TRACKS: IwoooSSecurityToolClosureTrack[] = [ @@ -788,6 +794,11 @@ const SECURITY_TOOL_CLOSURE_BOOTSTRAP_QUEUE: IwoooSSecurityToolClosureQueueEntry same_run_closure_candidate_ready_count: 0, incident_response_packet_validation_endpoint: '/api/v1/iwooos/wazuh-incident-response-closure/validate-incident-response-packet', + controlled_executor_handoff_readback_endpoint: + '/api/v1/iwooos/wazuh-controlled-executor-handoff-readback', + controlled_executor_handoff_preview_endpoint: + '/api/v1/iwooos/wazuh-controlled-executor-handoff/preview', + controlled_executor_handoff_preview_ready_count: 1, next_gate_after_packet_acceptance: 'route_to_iwooos_controlled_executor_for_bounded_execution_and_post_verifier_writeback', controlled_apply_policy_allowed: true, @@ -828,6 +839,9 @@ const SECURITY_TOOL_CLOSURE_BOOTSTRAP_BOUNDARY_MARKERS = [ 'iwooos_wazuh_incident_response_same_run_stage_ready_count=5', 'iwooos_wazuh_incident_response_same_run_required_stage_count=9', 'iwooos_wazuh_incident_response_runtime_closed_count=0', + 'iwooos_wazuh_controlled_executor_handoff_preview_ready_count=1', + 'iwooos_wazuh_controlled_executor_dispatch_enabled_count=0', + 'iwooos_wazuh_controlled_executor_dispatch_count=0', 'owner_review_required_for_low_medium_high=false', 'runtime_execution_performed=false', 'secret_value_collection_allowed=false', @@ -16661,6 +16675,42 @@ function IwoooSSecurityToolClosureBoard() { primaryQueue.same_run_required_stage_count ?? summary.wazuh_incident_response_same_run_required_stage_count ?? 0 const primarySameRunReady = primaryQueue.same_run_source_stage_ready_count ?? summary.wazuh_incident_response_same_run_stage_ready_count ?? 0 + const primaryHandoffReady = + primaryQueue.controlled_executor_handoff_preview_ready_count ?? + summary.wazuh_controlled_executor_handoff_preview_ready_count ?? + 0 + const runtimeClosed = summary.wazuh_incident_response_runtime_closed_count ?? 0 + const dispatchEnabled = summary.wazuh_controlled_executor_dispatch_enabled_count ?? 0 + const runwayItems = [ + { + key: 'source', + icon: SearchCheck, + value: primarySameRunRequired > 0 ? `${primarySameRunReady}/${primarySameRunRequired}` : '--', + tone: primarySameRunReady > 0 ? 'warn' as const : 'locked' as const, + state: primarySameRunReady >= primarySameRunRequired && primarySameRunRequired > 0 ? 'ready' : 'waiting', + }, + { + key: 'packet', + icon: FileCheck2, + value: `${summary.wazuh_incident_response_closure_validator_available_count ?? 0}/1`, + tone: (summary.wazuh_incident_response_closure_validator_available_count ?? 0) > 0 ? 'warn' as const : 'locked' as const, + state: (summary.wazuh_incident_response_closure_validator_available_count ?? 0) > 0 ? 'ready' : 'waiting', + }, + { + key: 'handoff', + icon: Route, + value: `${primaryHandoffReady}/1`, + tone: primaryHandoffReady > 0 ? 'warn' as const : 'locked' as const, + state: primaryHandoffReady > 0 ? 'ready' : 'waiting', + }, + { + key: 'runtime', + icon: ShieldCheck, + value: `${runtimeClosed}/1`, + tone: runtimeClosed > 0 ? 'steady' as const : 'locked' as const, + state: runtimeClosed > 0 ? 'closed' : dispatchEnabled > 0 ? 'waiting' : 'blocked', + }, + ] const metrics: IwoooSSecurityToolClosureMetric[] = [ { key: 'closure', @@ -16838,10 +16888,55 @@ function IwoooSSecurityToolClosureBoard() { {t('compact.sameRun')}: {primarySameRunReady}/{primarySameRunRequired} ) : null} + 0 ? '#315f43' : '#776b5e', padding: '4px 7px', fontSize: 10, fontWeight: 900, ...textWrap }}> + {t('compact.handoff')}: {primaryHandoffReady}/1 + +
+ {runwayItems.map(item => { + const Icon = item.icon + return ( +
+
+ + {t(`runway.${item.key}` as never)} + + +
+
+ + {item.value} + + + {t(`runway.${item.state}` as never)} + +
+
+ ) + })} +
+
{metrics.map(item => { const Icon = item.icon diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 802402304..6d68311fc 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -816,6 +816,9 @@ export interface IwoooSSecurityToolClosureQueueItem { same_run_source_stage_ready_count?: number same_run_closure_candidate_ready_count?: number incident_response_packet_validation_endpoint?: string + controlled_executor_handoff_readback_endpoint?: string + controlled_executor_handoff_preview_endpoint?: string + controlled_executor_handoff_preview_ready_count?: number next_gate_after_packet_acceptance?: string controlled_apply_policy_allowed: boolean } @@ -865,6 +868,12 @@ export interface IwoooSSecurityToolClosureReadbackResponse { wazuh_incident_response_same_run_required_stage_count?: number wazuh_incident_response_same_run_candidate_ready_count?: number wazuh_incident_response_runtime_closed_count?: number + wazuh_controlled_executor_handoff_preview_ready_count?: number + wazuh_controlled_executor_route_ready_count?: number + wazuh_controlled_executor_post_verifier_binding_count?: number + wazuh_controlled_executor_learning_writeback_contract_count?: number + wazuh_controlled_executor_dispatch_enabled_count?: number + wazuh_controlled_executor_dispatch_count?: number } tool_tracks: IwoooSSecurityToolClosureTrack[] next_executable_queue: IwoooSSecurityToolClosureQueueItem[]