diff --git a/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py b/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py index d206b8041..11d193001 100644 --- a/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py @@ -26,6 +26,12 @@ SCHEMA_VERSION = "iwooos_wazuh_controlled_executor_runtime_readback_v1" CATALOG_ID = "ansible:wazuh-manager-posture-readback" INGRESS_CATALOG_ID = "ansible:wazuh-alertmanager-integration" WORK_ITEM_ID = "P0-03-WAZUH-MANAGER-POSTURE" +_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING = ( + "wazuh_privileged_convergence_capability_missing" +) +_PUBLIC_SAFE_CHECK_FAILURE_MARKERS = frozenset( + {_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING} +) _LIVE_RUNTIME_SQL = """ WITH latest_candidate AS ( @@ -60,6 +66,18 @@ _LIVE_RUNTIME_SQL = """ check_mode.output ->> 'timed_out', check_mode.dry_run_result ->> 'timed_out' ) AS check_timed_out, + CASE + WHEN concat_ws( + ' ', + check_mode.error, + check_mode.output ->> 'stdout_tail', + check_mode.output ->> 'stderr_tail', + check_mode.dry_run_result ->> 'stdout_tail', + check_mode.dry_run_result ->> 'stderr_tail' + ) LIKE '%wazuh_privileged_convergence_capability_missing%' + THEN 'wazuh_privileged_convergence_capability_missing' + ELSE NULL + END AS check_failure_marker, check_mode.duration_ms AS check_duration_ms, check_mode.created_at AS check_created_at FROM automation_operation_log check_mode @@ -485,6 +503,14 @@ def _next_action( else "internal_worker_enqueue_wazuh_manager_posture_candidate" ) if check_failed: + if ( + check_failure_class + == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING + ): + return ( + "queue_critical_break_glass_privileged_convergence_capability_" + "then_bounded_retry" + ) return ( "repair_and_enqueue_bounded_retry:" + (check_failure_class or "ansible_check_mode_failed") @@ -507,6 +533,9 @@ def _next_action( def _check_failure_class(data: Mapping[str, Any]) -> str | None: if data.get("check_status") != "failed": return None + failure_marker = str(data.get("check_failure_marker") or "").strip() + if failure_marker in _PUBLIC_SAFE_CHECK_FAILURE_MARKERS: + return failure_marker if str(data.get("check_timed_out") or "").lower() == "true": return "ansible_check_mode_timeout" returncode = str(data.get("check_returncode") or "").strip() diff --git a/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py b/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py index 0310f7f9c..41ba7d303 100644 --- a/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py @@ -2,6 +2,7 @@ from __future__ import annotations # ruff: noqa: E402, I001 +import json import os from contextlib import asynccontextmanager from datetime import UTC, datetime @@ -197,6 +198,60 @@ def test_wazuh_runtime_readback_classifies_failed_check_for_bounded_retry() -> N assert "ansible_target_unreachable" in payload["active_blockers"] +def test_wazuh_runtime_readback_projects_allowlisted_privilege_blocker() -> None: + row = { + "candidate_op_id": "00000000-0000-0000-0000-000000000312", + "automation_run_id": "00000000-0000-0000-0000-000000000312", + "trace_id": "00000000-0000-0000-0000-000000000312", + "run_id": "00000000-0000-0000-0000-000000000312", + "work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "catalog_id": "ansible:wazuh-alertmanager-integration", + "check_status": "failed", + "check_returncode": "2", + "check_timed_out": "false", + "check_failure_marker": ( + "wazuh_privileged_convergence_capability_missing" + ), + "stage_ids": [], + } + + payload = build_iwooos_wazuh_controlled_executor_runtime_readback( + row, + executor_enabled=True, + ) + + assert payload["check_failure_class"] == ( + "wazuh_privileged_convergence_capability_missing" + ) + assert payload["next_safe_action"] == ( + "queue_critical_break_glass_privileged_convergence_capability_" + "then_bounded_retry" + ) + assert "wazuh_privileged_convergence_capability_missing" in ( + payload["active_blockers"] + ) + assert payload["boundaries"]["host_write_performed"] is False + assert payload["boundaries"]["secret_value_collection_allowed"] is False + + +def test_wazuh_runtime_readback_rejects_untrusted_failure_marker() -> None: + row = { + "candidate_op_id": "00000000-0000-0000-0000-000000000313", + "check_status": "failed", + "check_returncode": "2", + "check_failure_marker": "untrusted_raw_failure_text", + "stage_ids": [], + } + + payload = build_iwooos_wazuh_controlled_executor_runtime_readback( + row, + executor_enabled=True, + ) + + assert payload["check_failure_class"] == "ansible_check_mode_returncode_2" + assert "untrusted_raw_failure_text" not in str(payload) + + def test_wazuh_posture_catalog_and_playbook_are_bounded_no_write() -> None: catalog = get_ansible_catalog_item("ansible:wazuh-manager-posture-readback") assert catalog is not None @@ -870,6 +925,10 @@ def test_iwooos_runtime_workspace_defaults_to_live_closure_without_legacy_text_w ).read_text(encoding="utf-8") assert "useState('runtime')" in source + assert "wazuh_privileged_convergence_capability_missing" in source + assert "privilegedCapabilityMissing" in source + assert "ansible:wazuh-alertmanager-integration" in source + assert "labelKey: runtimeSummary?.selected_catalog_id" in source runtime_panel = source.split( "", 1 )[1].split("", 1)[0] @@ -895,3 +954,22 @@ def test_iwooos_runtime_workspace_defaults_to_live_closure_without_legacy_text_w ) assert all(board not in runtime_panel for board in legacy_boards) assert all(board in ledger_panel for board in legacy_boards) + + runtime_failure_keys = None + for locale in ("zh-TW", "en"): + messages = json.loads( + (repo_root / f"apps/web/messages/{locale}.json").read_text( + encoding="utf-8" + ) + ) + locale_keys = set( + messages["iwooos"]["securityToolClosure"]["runtimeFailure"] + ) + assert "privilegedCapabilityMissing" in locale_keys + if runtime_failure_keys is None: + runtime_failure_keys = locale_keys + else: + assert locale_keys == runtime_failure_keys + + runway = messages["iwooos"]["securityToolClosure"]["runway"] + assert "ingress" in runway diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 8cc647cce..5e1a5e8a5 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14226,6 +14226,7 @@ "source": "Source", "packet": "Packet", "handoff": "Handoff", + "ingress": "Alertmanager integration", "runtime": "Manager posture", "ready": "ready", "waiting": "waiting", @@ -14235,6 +14236,7 @@ }, "runtimeFailure": { "title": "Wazuh execution chain", + "privilegedCapabilityMissing": "The target lacks controlled privilege capability; writes remain stopped until a critical break-glass capability package is completed, then bounded retry may resume", "targetUnreachable": "Target trust failed; waiting for this release's bounded retry", "timeout": "Check mode timed out; waiting for this release's bounded retry", "checkFailed": "Check mode failed; waiting for this release's bounded retry", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index a86239adf..25332eb76 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14226,6 +14226,7 @@ "source": "來源", "packet": "Packet", "handoff": "Handoff", + "ingress": "Alertmanager 整合", "runtime": "Manager posture", "ready": "ready", "waiting": "waiting", @@ -14235,6 +14236,7 @@ }, "runtimeFailure": { "title": "Wazuh 執行鏈", + "privilegedCapabilityMissing": "主機端缺少受控提權能力;已停止寫入,需由 critical break-glass 能力包補齊後再進行受控重試", "targetUnreachable": "目標連線信任未通過,等待本版受控重試", "timeout": "check-mode 逾時,等待本版受控重試", "checkFailed": "check-mode 未通過,等待本版受控重試", diff --git a/apps/web/src/app/[locale]/iwooos/page.tsx b/apps/web/src/app/[locale]/iwooos/page.tsx index d209347c5..e51399d38 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -16266,7 +16266,7 @@ function IwoooSManagerCockpit() { const registryComplete = managerRegistryAcceptedCount >= expectedHostScopeCount && expectedHostScopeCount > 0 const runtimeLocked = runtimeGateCount === 0 && hostWriteAuthorizedCount === 0 && activeResponseAuthorizedCount === 0 const statusKey = registryComplete && runtimeLocked ? 'registryAcceptedRuntimeClosed' : 'evidenceStillMissing' - const runtimeStatus = runtime?.status ?? 'blocked_waiting_owner_evidence_and_runtime_gates' + const runtimeStatus = runtime?.status ?? 'degraded_waiting_controlled_evidence_and_runtime_receipts' const metrics: IwoooSManagerCockpitMetric[] = [ { key: 'visibility', @@ -16700,18 +16700,21 @@ function IwoooSSecurityToolClosureBoard() { const dispatchEnabled = runtimeSummary?.executor_policy_enabled_count ?? summary.wazuh_controlled_executor_dispatch_enabled_count ?? 0 const dispatchCandidate = runtimeSummary?.dispatch_candidate_count ?? 0 const checkModeFailed = runtimeSummary?.check_mode_failed_count ?? 0 - const runtimeFailureKey = runtimeData?.check_failure_class === 'ansible_target_unreachable' - ? 'targetUnreachable' - : runtimeData?.check_failure_class === 'ansible_check_mode_timeout' - ? 'timeout' - : runtimeData?.check_failure_class?.startsWith('ansible_check_mode_returncode_') - ? 'checkFailed' - : runtimeData?.check_failure_class - ? 'unclassified' - : null + const runtimeFailureKey = runtimeData?.check_failure_class === 'wazuh_privileged_convergence_capability_missing' + ? 'privilegedCapabilityMissing' + : runtimeData?.check_failure_class === 'ansible_target_unreachable' + ? 'targetUnreachable' + : runtimeData?.check_failure_class === 'ansible_check_mode_timeout' + ? 'timeout' + : runtimeData?.check_failure_class?.startsWith('ansible_check_mode_returncode_') + ? 'checkFailed' + : runtimeData?.check_failure_class + ? 'unclassified' + : null const runwayItems = [ { key: 'source', + labelKey: 'source', icon: SearchCheck, value: primarySameRunRequired > 0 ? `${primarySameRunReady}/${primarySameRunRequired}` : '--', tone: primarySameRunReady > 0 ? 'warn' as const : 'locked' as const, @@ -16719,6 +16722,7 @@ function IwoooSSecurityToolClosureBoard() { }, { key: 'packet', + labelKey: '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, @@ -16726,6 +16730,7 @@ function IwoooSSecurityToolClosureBoard() { }, { key: 'handoff', + labelKey: 'handoff', icon: Route, value: `${primaryHandoffReady}/1`, tone: primaryHandoffReady > 0 ? 'warn' as const : 'locked' as const, @@ -16733,6 +16738,9 @@ function IwoooSSecurityToolClosureBoard() { }, { key: 'runtime', + labelKey: runtimeSummary?.selected_catalog_id === 'ansible:wazuh-alertmanager-integration' + ? 'ingress' + : 'runtime', icon: checkModeFailed > 0 ? ShieldAlert : ShieldCheck, value: `${runtimePresent}/${runtimeRequired}`, tone: runtimeClosed > 0 ? 'steady' as const : runtimePresent > 0 || checkModeFailed > 0 ? 'warn' as const : 'locked' as const, @@ -16979,7 +16987,7 @@ function IwoooSSecurityToolClosureBoard() { >
- {t(`runway.${item.key}` as never)} + {t(`runway.${item.labelKey}` as never)}
diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index 1f208c61d..e10b9ce71 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -896,12 +896,14 @@ export interface IwoooSWazuhControlledExecutorRuntimeReadbackResponse { same_run_correlation_required: boolean } summary: { + selected_catalog_id: string executor_policy_enabled_count: number dispatch_candidate_count: number check_mode_passed_count: number check_mode_failed_count: number runtime_execution_performed_count: number bounded_posture_execution_passed_count: number + bounded_ingress_convergence_passed_count: number independent_post_verifier_passed_count: number auto_repair_execution_receipt_count: number km_writeback_count: number