From 1387a307487dace8abfa60e7ca5ebbc40d732e4a Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 18:24:36 +0800 Subject: [PATCH] fix(wazuh): persist critical capability packets --- .../awooop_ansible_check_mode_service.py | 343 ++++++++++++++++++ ...wooos_wazuh_controlled_executor_runtime.py | 132 +++++++ ...wooos_wazuh_controlled_executor_runtime.py | 274 ++++++++++++++ apps/web/messages/en.json | 3 + apps/web/messages/zh-TW.json | 3 + apps/web/src/app/[locale]/iwooos/page.tsx | 19 +- apps/web/src/lib/api-client.ts | 16 + 7 files changed, 785 insertions(+), 5 deletions(-) diff --git a/apps/api/src/services/awooop_ansible_check_mode_service.py b/apps/api/src/services/awooop_ansible_check_mode_service.py index 01395ee46..7e5ffa387 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -77,6 +77,15 @@ _EXECUTION_CAPABILITY_SAFETY_MARGIN_SECONDS = 5 _WORKING_MEMORY_PROJECTION_WINDOW_HOURS = 8 * 24 _WORKING_MEMORY_PROJECTION_CURSOR: dict[str, tuple[datetime, str]] = {} _EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE = "remediation_executed" +_WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID = ( + "ansible:wazuh-alertmanager-integration" +) +_WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING = ( + "wazuh_privileged_convergence_capability_missing" +) +_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE = ( + "ansible_break_glass_capability_work_item_queued" +) _EXECUTION_CAPABILITY_OPERATION_TYPES = frozenset( { "ansible_executor_capability_issued", @@ -6135,6 +6144,9 @@ async def backfill_missing_auto_repair_execution_receipts_once( "retry_terminal_projection_lifecycle_written": 0, "retry_terminal_projection_verified": 0, "retry_terminal_projection_runtime_apply_executed": False, + "critical_break_glass_packet_scanned": 0, + "critical_break_glass_packet_written": 0, + "critical_break_glass_packet_error": None, "skipped": 0, "error": None, } @@ -6399,6 +6411,22 @@ async def backfill_missing_auto_repair_execution_receipts_once( stats["telegram_receipt_acknowledged"] += 1 if closure.get("closed") is True: stats["incident_closure_written"] += 1 + break_glass_packet = ( + await backfill_missing_wazuh_break_glass_capability_packets_once( + project_id=project_id, + window_hours=window_hours, + limit=max(1, min(limit, 5)), + ) + ) + stats["critical_break_glass_packet_scanned"] = int( + break_glass_packet.get("scanned") or 0 + ) + stats["critical_break_glass_packet_written"] = int( + break_glass_packet.get("written") or 0 + ) + stats["critical_break_glass_packet_error"] = ( + str(break_glass_packet.get("error") or "")[:500] or None + ) except Exception as exc: stats["error"] = f"{type(exc).__name__}: {exc}"[:500] logger.warning("ansible_auto_repair_execution_receipt_backfill_failed", **stats) @@ -8603,6 +8631,175 @@ async def _insert_skipped_candidate( ) +def _wazuh_privileged_convergence_capability_is_missing( + claim: AnsibleCheckModeClaim, + result: AnsibleRunResult, +) -> bool: + if ( + claim.catalog_id != _WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID + or result.returncode == 0 + ): + return False + return any( + _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING in value + for value in (result.stdout, result.stderr) + ) + + +def _build_wazuh_break_glass_capability_packet( + *, + check_op_id: str, + source_candidate_op_id: str, + incident_id: str, + catalog_id: str, + source_input: Mapping[str, Any], +) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: + automation_run_id = str( + source_input.get("automation_run_id") or source_candidate_op_id + ).strip()[:180] + trace_id = str( + source_input.get("trace_id") or automation_run_id + ).strip()[:180] + run_id = str( + source_input.get("run_id") or automation_run_id + ).strip()[:180] + work_item_id = str( + source_input.get("work_item_id") or "P0-03-WAZUH-ALERT-INGRESS" + ).strip()[:180] + source_recurrence = source_input.get("source_recurrence") + if not isinstance(source_recurrence, Mapping): + source_recurrence = {} + packet_uuid = uuid5( + NAMESPACE_URL, + f"awoooi:wazuh-break-glass:{automation_run_id}:{check_op_id}", + ) + capability_packet_id = f"IWZ-BG-{packet_uuid.hex[:16].upper()}" + packet_input = { + "schema_version": "wazuh_privileged_convergence_capability_packet_v1", + "capability_packet_id": capability_packet_id, + "automation_run_id": automation_run_id, + "trace_id": trace_id, + "run_id": run_id, + "work_item_id": work_item_id, + "project_id": str(source_input.get("project_id") or "")[:180], + "run_namespace": str( + source_input.get("run_namespace") or "" + )[:180], + "proposal_source": str( + source_input.get("proposal_source") or "" + )[:180], + "source_receipt_ref": str( + source_input.get("source_receipt_ref") or "" + )[:180], + "source_recurrence": dict(source_recurrence), + "incident_id": str(incident_id or "").strip()[:180], + "source_candidate_op_id": str(source_candidate_op_id), + "source_check_mode_op_id": str(check_op_id), + "catalog_id": str(catalog_id), + "capability_type": "bounded_privileged_convergence", + "risk_level": "critical", + "policy_route": "critical_break_glass_queue", + "queue_owner": "ai_critical_capability_controller", + "same_run_correlation_required": True, + "execution_allowed": False, + "host_write_performed": False, + "secret_value_exposed": False, + "secret_value_logged": False, + "secret_value_read_back": False, + } + packet_output = { + "queue_status": "queued", + "failure_class": _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING, + "required_receipts": [ + "capability_reference_presence", + "no_secret_privilege_preflight", + "bounded_check_mode_retry", + "independent_post_verifier", + "capability_revocation", + ], + "next_controlled_action": ( + "verify_bounded_privileged_capability_then_" + "requeue_same_run_check_mode" + ), + "telegram_notification_emitted": False, + } + dry_run_result = { + "capability_packet_persisted": True, + "capability_granted": False, + "check_mode_replayed": False, + "apply_executed": False, + "host_write_performed": False, + "secret_value_exposed": False, + "secret_value_logged": False, + "secret_value_read_back": False, + } + return packet_input, packet_output, dry_run_result + + +async def _insert_wazuh_break_glass_capability_packet( + db: Any, + *, + check_op_id: str, + source_candidate_op_id: str, + incident_id: str, + catalog_id: str, + source_input: Mapping[str, Any], +) -> bool: + packet_input, packet_output, dry_run_result = ( + _build_wazuh_break_glass_capability_packet( + check_op_id=check_op_id, + source_candidate_op_id=source_candidate_op_id, + incident_id=incident_id, + catalog_id=catalog_id, + source_input=source_input, + ) + ) + inserted = await db.execute( + text(f""" + INSERT INTO automation_operation_log ( + operation_type, actor, status, incident_id, + input, output, dry_run_result, parent_op_id, tags + ) + SELECT + '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}', + 'ansible_check_mode_worker', + 'pending', + :incident_db_id, + CAST(:input AS jsonb), + CAST(:output AS jsonb), + CAST(:dry_run_result AS jsonb), + CAST(:parent_op_id AS uuid), + ARRAY[ + 'ansible', 'wazuh', 'critical_break_glass', + 'capability_packet', 'no_host_write', 'no_secret' + ]::varchar[] + WHERE NOT EXISTS ( + SELECT 1 + FROM automation_operation_log existing + WHERE existing.operation_type = + '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + AND existing.input ->> 'capability_packet_id' + = :capability_packet_id + ) + RETURNING op_id::text + """), + { + "incident_db_id": _automation_operation_log_incident_id( + incident_id + ), + "input": json.dumps(packet_input, ensure_ascii=False), + "output": json.dumps(packet_output, ensure_ascii=False), + "dry_run_result": json.dumps( + dry_run_result, + ensure_ascii=False, + ), + "parent_op_id": check_op_id, + "capability_packet_id": packet_input["capability_packet_id"], + }, + ) + return inserted.scalar_one_or_none() is not None + + async def finalize_check_mode_claim( claim: AnsibleCheckModeClaim, result: AnsibleRunResult, @@ -8661,6 +8858,140 @@ async def finalize_check_mode_claim( "op_id": claim.op_id, }, ) + if _wazuh_privileged_convergence_capability_is_missing(claim, result): + await _insert_wazuh_break_glass_capability_packet( + db, + check_op_id=claim.op_id, + source_candidate_op_id=claim.source_candidate_op_id, + incident_id=claim.incident_id, + catalog_id=claim.catalog_id, + source_input=claim.input_payload, + ) + + +async def backfill_missing_wazuh_break_glass_capability_packets_once( + *, + project_id: str = "awoooi", + window_hours: int = 24, + limit: int = 5, +) -> dict[str, Any]: + """Persist safe critical-capability packets for earlier failed Wazuh checks.""" + + stats: dict[str, Any] = {"scanned": 0, "written": 0, "error": None} + try: + async with get_db_context(project_id) as db: + await db.execute(text("SET LOCAL statement_timeout = '3000ms'")) + result = await db.execute( + text(f""" + SELECT + check_mode.op_id::text AS check_op_id, + candidate.op_id::text AS source_candidate_op_id, + coalesce( + check_mode.input ->> 'incident_id', + candidate.input ->> 'incident_id', + check_mode.incident_id::text + ) AS incident_id, + coalesce( + candidate.input ->> 'automation_run_id', + candidate.op_id::text + ) AS automation_run_id, + coalesce( + candidate.input ->> 'trace_id', + candidate.input ->> 'automation_run_id', + candidate.op_id::text + ) AS trace_id, + coalesce( + candidate.input ->> 'run_id', + candidate.input ->> 'automation_run_id', + candidate.op_id::text + ) AS run_id, + candidate.input ->> 'work_item_id' AS work_item_id, + candidate.input ->> 'project_id' AS project_id, + candidate.input ->> 'run_namespace' AS run_namespace, + candidate.input ->> 'proposal_source' AS proposal_source, + candidate.input ->> 'source_receipt_ref' + AS source_receipt_ref, + candidate.input -> 'source_recurrence' + AS source_recurrence + FROM automation_operation_log check_mode + JOIN automation_operation_log candidate + ON candidate.op_id = check_mode.parent_op_id + WHERE check_mode.operation_type = + 'ansible_check_mode_executed' + AND check_mode.status = 'failed' + AND check_mode.created_at >= NOW() - ( + :window_hours * INTERVAL '1 hour' + ) + AND candidate.operation_type = + 'ansible_candidate_matched' + AND candidate.input -> 'executor_candidates' + @> CAST(:catalog_match AS jsonb) + AND 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 ('%' || :failure_marker || '%') + AND NOT EXISTS ( + SELECT 1 + FROM automation_operation_log packet + WHERE packet.operation_type = + '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + AND packet.parent_op_id = check_mode.op_id + ) + ORDER BY check_mode.created_at DESC + LIMIT :limit + """), + { + "window_hours": max(1, int(window_hours)), + "limit": max(1, min(int(limit), 20)), + "catalog_match": json.dumps( + [{ + "catalog_id": ( + _WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID + ) + }] + ), + "failure_marker": ( + _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING + ), + }, + ) + rows = [dict(row) for row in result.mappings().all()] + stats["scanned"] = len(rows) + for row in rows: + source_input = { + "automation_run_id": row.get("automation_run_id"), + "trace_id": row.get("trace_id"), + "run_id": row.get("run_id"), + "work_item_id": row.get("work_item_id"), + "project_id": row.get("project_id"), + "run_namespace": row.get("run_namespace"), + "proposal_source": row.get("proposal_source"), + "source_receipt_ref": row.get("source_receipt_ref"), + "source_recurrence": row.get("source_recurrence"), + } + if await _insert_wazuh_break_glass_capability_packet( + db, + check_op_id=str(row.get("check_op_id") or ""), + source_candidate_op_id=str( + row.get("source_candidate_op_id") or "" + ), + incident_id=str(row.get("incident_id") or ""), + catalog_id=_WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID, + source_input=source_input, + ): + stats["written"] += 1 + except Exception as exc: + stats["error"] = f"{type(exc).__name__}: {exc}"[:500] + logger.warning( + "wazuh_break_glass_capability_packet_backfill_failed", + project_id=project_id, + **stats, + ) + return stats async def run_claimed_check_mode( @@ -9144,6 +9475,18 @@ async def run_pending_check_modes_once( receipt_stats.get("retry_terminal_projection_verified") or 0 ), "retry_terminal_projection_runtime_apply_executed": False, + "critical_break_glass_packet_scanned": int( + receipt_stats.get("critical_break_glass_packet_scanned") or 0 + ), + "critical_break_glass_packet_written": int( + receipt_stats.get("critical_break_glass_packet_written") or 0 + ), + "critical_break_glass_packet_error": ( + str( + receipt_stats.get("critical_break_glass_packet_error") or "" + )[:500] + or None + ), "repair_receipt_backfill_error": ( str(receipt_stats.get("error") or "")[:500] or None ), 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 f402375d1..8bf2c1d33 100644 --- a/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py @@ -130,6 +130,45 @@ _LIVE_RUNTIME_SQL = """ WHERE apply.operation_type = 'ansible_apply_executed' ORDER BY apply.created_at DESC LIMIT 1 + ), latest_break_glass_packet AS ( + SELECT + packet.op_id::text AS break_glass_packet_receipt_id, + packet.status AS break_glass_packet_status, + packet.input ->> 'capability_packet_id' AS capability_packet_id, + packet.input ->> 'automation_run_id' + AS capability_automation_run_id, + packet.input ->> 'trace_id' AS capability_trace_id, + packet.input ->> 'run_id' AS capability_run_id, + packet.input ->> 'work_item_id' AS capability_work_item_id, + packet.input ->> 'project_id' AS capability_project_id, + packet.input ->> 'run_namespace' AS capability_run_namespace, + packet.input ->> 'proposal_source' AS capability_proposal_source, + packet.input -> 'source_recurrence' + AS capability_source_recurrence, + packet.input ->> 'capability_type' AS capability_type, + packet.input ->> 'risk_level' AS capability_risk_level, + packet.input ->> 'policy_route' AS capability_policy_route, + packet.output ->> 'queue_status' AS capability_queue_status, + coalesce( + (packet.input ->> 'execution_allowed')::boolean, + false + ) AS capability_execution_allowed, + coalesce( + (packet.dry_run_result ->> 'capability_granted')::boolean, + false + ) AS capability_granted, + coalesce( + (packet.dry_run_result ->> 'host_write_performed')::boolean, + false + ) AS capability_packet_host_write_performed, + packet.created_at AS break_glass_packet_created_at + FROM automation_operation_log packet + JOIN latest_check check_mode + ON packet.parent_op_id::text = check_mode.check_op_id + WHERE packet.operation_type = + 'ansible_break_glass_capability_work_item_queued' + ORDER BY packet.created_at DESC + LIMIT 1 ), runtime_stages AS ( SELECT coalesce( @@ -239,6 +278,7 @@ _LIVE_RUNTIME_SQL = """ SELECT candidate.*, check_mode.*, + break_glass_packet.*, apply.*, runtime_stages.stage_ids, runtime_stages.runtime_stage_identity_verified, @@ -249,6 +289,7 @@ _LIVE_RUNTIME_SQL = """ telegram.* FROM latest_candidate candidate LEFT JOIN latest_check check_mode ON TRUE + LEFT JOIN latest_break_glass_packet break_glass_packet ON TRUE LEFT JOIN latest_apply apply ON TRUE LEFT JOIN runtime_stages ON TRUE LEFT JOIN latest_verifier verifier ON TRUE @@ -416,6 +457,43 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( ) check_failed = data.get("check_status") == "failed" check_failure_class = _check_failure_class(data) + break_glass_packet_receipt_present = bool( + data.get("break_glass_packet_receipt_id") + ) + capability_packet_id = str(data.get("capability_packet_id") or "") + break_glass_packet_identity_verified = bool( + break_glass_packet_receipt_present + and candidate_identity_verified + and check_identity_verified + and str(data.get("capability_automation_run_id") or "") + == candidate_op_id + and str(data.get("capability_trace_id") or "") == candidate_op_id + and str(data.get("capability_run_id") or "") == candidate_op_id + and str(data.get("capability_work_item_id") or "") + == expected_work_item_id + and str(data.get("capability_project_id") or "").strip() + and str(data.get("capability_run_namespace") or "") + == expected_run_namespace + and str(data.get("capability_proposal_source") or "") + == expected_proposal_source + and data.get("capability_source_recurrence") + == candidate_source_recurrence + ) + break_glass_packet_present = all( + ( + break_glass_packet_identity_verified, + data.get("break_glass_packet_status") == "pending", + capability_packet_id.startswith("IWZ-BG-"), + len(capability_packet_id) == 23, + data.get("capability_type") == "bounded_privileged_convergence", + data.get("capability_risk_level") == "critical", + data.get("capability_policy_route") == "critical_break_glass_queue", + data.get("capability_queue_status") == "queued", + data.get("capability_execution_allowed") is False, + data.get("capability_granted") is False, + data.get("capability_packet_host_write_performed") is False, + ) + ) apply_terminal = data.get("apply_status") in {"success", "failed"} apply_passed = apply_terminal and str( data.get("apply_returncode") or "" @@ -495,6 +573,16 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( active_blockers.append("wazuh_controlled_check_mode_failed") if check_failure_class: active_blockers.append(check_failure_class) + if check_failure_class == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING: + active_blockers.append( + "critical_break_glass_capability_packet_pending" + if break_glass_packet_present + else ( + "critical_break_glass_capability_packet_identity_mismatch" + if break_glass_packet_receipt_present + else "critical_break_glass_capability_packet_missing" + ) + ) if apply_failed: active_blockers.append("wazuh_controlled_apply_failed") if apply_passed and not verifier_passed: @@ -536,6 +624,9 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( "dispatch_candidate_count": 1 if candidate_present else 0, "check_mode_passed_count": 1 if check_passed else 0, "check_mode_failed_count": 1 if check_failed else 0, + "critical_break_glass_capability_packet_count": ( + 1 if break_glass_packet_present else 0 + ), "runtime_execution_performed_count": 1 if apply_terminal else 0, "bounded_posture_execution_passed_count": ( 1 if apply_passed and not ingress_mode else 0 @@ -580,12 +671,36 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( ], "missing_stage_ids": missing_stage_ids, "check_failure_class": check_failure_class, + "critical_break_glass_capability_packet": ( + { + "receipt_id": str( + data.get("break_glass_packet_receipt_id") or "" + ), + "capability_packet_id": str( + capability_packet_id + ), + "status": "pending", + "queue_status": "queued", + "capability_type": "bounded_privileged_convergence", + "risk_level": "critical", + "policy_route": "critical_break_glass_queue", + "execution_allowed": False, + "capability_granted": False, + "host_write_performed": False, + "secret_value_exposed": False, + "secret_value_logged": False, + "secret_value_read_back": False, + } + if break_glass_packet_present + else None + ), "next_safe_action": _next_action( ingress_mode=ingress_mode, candidate_present=candidate_present, check_passed=check_passed, check_failed=check_failed, check_failure_class=check_failure_class, + break_glass_packet_present=break_glass_packet_present, apply_terminal=apply_terminal, apply_passed=apply_passed, verifier_passed=verifier_passed, @@ -609,6 +724,17 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( "command_output_returned": False, "inventory_identity_returned": False, "secret_value_collection_allowed": False, + "critical_break_glass_required": ( + check_failure_class + == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING + ), + "critical_break_glass_capability_packet_persisted": ( + break_glass_packet_present + ), + "critical_break_glass_capability_packet_identity_verified": ( + break_glass_packet_identity_verified + ), + "critical_break_glass_capability_granted": False, "github_api_used": False, }, "source_refs": [ @@ -658,6 +784,7 @@ def _next_action( check_passed: bool, check_failed: bool, check_failure_class: str | None, + break_glass_packet_present: bool, apply_terminal: bool, apply_passed: bool, verifier_passed: bool, @@ -684,6 +811,11 @@ def _next_action( check_failure_class == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING ): + if break_glass_packet_present: + return ( + "critical_break_glass_capability_packet_queued_waiting_" + "bounded_capability_verifier_then_retry" + ) return ( "queue_critical_break_glass_privileged_convergence_capability_" "then_bounded_retry" 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 1de0782e6..4a59f896f 100644 --- a/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py @@ -19,6 +19,14 @@ os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/ from src.api.v1 import iwooos from src.api.v1.iwooos import router from src.services.awooop_ansible_audit_service import get_ansible_catalog_item +from src.services.awooop_ansible_check_mode_service import ( + AnsibleCheckModeClaim, + AnsibleRunResult, + _build_wazuh_break_glass_capability_packet, + _wazuh_privileged_convergence_capability_is_missing, + backfill_missing_wazuh_break_glass_capability_packets_once, + finalize_check_mode_claim, +) from src.services.awooop_ansible_post_verifier import postconditions_for_catalog from src.services.iwooos_wazuh_controlled_executor_runtime import ( build_iwooos_wazuh_controlled_executor_runtime_readback, @@ -323,6 +331,94 @@ def test_wazuh_runtime_readback_projects_allowlisted_privilege_blocker() -> None ) assert payload["boundaries"]["host_write_performed"] is False assert payload["boundaries"]["secret_value_collection_allowed"] is False + assert payload["summary"][ + "critical_break_glass_capability_packet_count" + ] == 0 + assert payload["critical_break_glass_capability_packet"] is None + assert "critical_break_glass_capability_packet_missing" in ( + payload["active_blockers"] + ) + + +def test_wazuh_runtime_readback_projects_durable_break_glass_packet() -> None: + run_id = "00000000-0000-0000-0000-000000000314" + recurrence = _current_wazuh_source_recurrence( + "2026-07-15T05:59:58+08:00" + ) + row = { + "candidate_op_id": run_id, + "automation_run_id": run_id, + "trace_id": run_id, + "run_id": run_id, + "candidate_project_id": "awoooi", + "work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "proposal_source": "iwooos_wazuh_alert_ingress_scheduler", + "run_namespace": "awoooi:iwooos:wazuh-alert-ingress", + "source_receipt_ref": "wazuh-alert:2026071500", + "candidate_source_recurrence": recurrence, + "candidate_source_recurrence_verified": True, + "catalog_id": "ansible:wazuh-alertmanager-integration", + "check_op_id": "00000000-0000-0000-0000-000000000313", + "check_status": "failed", + "check_returncode": "2", + "check_timed_out": "false", + "check_automation_run_id": run_id, + "check_trace_id": run_id, + "check_run_id": run_id, + "check_work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "check_source_recurrence": recurrence, + "check_failure_marker": ( + "wazuh_privileged_convergence_capability_missing" + ), + "break_glass_packet_receipt_id": ( + "00000000-0000-0000-0000-000000000315" + ), + "break_glass_packet_status": "pending", + "capability_packet_id": "IWZ-BG-0011223344556677", + "capability_automation_run_id": run_id, + "capability_trace_id": run_id, + "capability_run_id": run_id, + "capability_work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "capability_project_id": "awoooi", + "capability_run_namespace": "awoooi:iwooos:wazuh-alert-ingress", + "capability_proposal_source": ( + "iwooos_wazuh_alert_ingress_scheduler" + ), + "capability_source_recurrence": recurrence, + "capability_type": "bounded_privileged_convergence", + "capability_risk_level": "critical", + "capability_policy_route": "critical_break_glass_queue", + "capability_queue_status": "queued", + "capability_execution_allowed": False, + "capability_granted": False, + "capability_packet_host_write_performed": False, + "stage_ids": [], + } + + payload = build_iwooos_wazuh_controlled_executor_runtime_readback( + row, + executor_enabled=True, + ) + + assert payload["summary"][ + "critical_break_glass_capability_packet_count" + ] == 1 + assert payload["next_safe_action"] == ( + "critical_break_glass_capability_packet_queued_waiting_" + "bounded_capability_verifier_then_retry" + ) + packet = payload["critical_break_glass_capability_packet"] + assert packet["queue_status"] == "queued" + assert packet["execution_allowed"] is False + assert packet["capability_granted"] is False + assert packet["host_write_performed"] is False + assert packet["secret_value_exposed"] is False + assert "critical_break_glass_capability_packet_pending" in ( + payload["active_blockers"] + ) + assert payload["boundaries"][ + "critical_break_glass_capability_packet_persisted" + ] is True def test_wazuh_runtime_readback_rejects_untrusted_failure_marker() -> None: @@ -345,6 +441,184 @@ def test_wazuh_runtime_readback_rejects_untrusted_failure_marker() -> None: assert payload["boundaries"]["host_write_performed"] is False +def test_wazuh_break_glass_packet_is_deterministic_and_public_safe() -> None: + kwargs = { + "check_op_id": "00000000-0000-0000-0000-000000000316", + "source_candidate_op_id": "00000000-0000-0000-0000-000000000317", + "incident_id": "IWZ-I-2026071512-TEST", + "catalog_id": "ansible:wazuh-alertmanager-integration", + "source_input": { + "automation_run_id": "00000000-0000-0000-0000-000000000317", + "trace_id": "trace-317", + "run_id": "run-317", + "work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "project_id": "awoooi", + "run_namespace": "awoooi:iwooos:wazuh-alert-ingress", + "proposal_source": "iwooos_wazuh_alert_ingress_scheduler", + "source_receipt_ref": "wazuh-alert:2026071500", + "source_recurrence": _current_wazuh_source_recurrence( + "2026-07-15T05:59:58+08:00" + ), + "unexpected_material": "must-not-be-projected", + "command_output": "must-not-be-projected", + }, + } + + packet_input, packet_output, dry_run = ( + _build_wazuh_break_glass_capability_packet(**kwargs) + ) + repeated_input, _, _ = _build_wazuh_break_glass_capability_packet( + **kwargs + ) + serialized = json.dumps( + {"input": packet_input, "output": packet_output, "dry": dry_run} + ) + + assert packet_input["capability_packet_id"] == repeated_input[ + "capability_packet_id" + ] + assert packet_input["risk_level"] == "critical" + assert packet_input["execution_allowed"] is False + assert packet_input["host_write_performed"] is False + assert packet_input["project_id"] == "awoooi" + assert packet_input["source_recurrence"]["verified"] is True + assert packet_output["queue_status"] == "queued" + assert dry_run["capability_granted"] is False + assert "must-not-be-projected" not in serialized + assert "command_output" not in serialized + + +@pytest.mark.asyncio +async def test_failed_wazuh_check_atomically_queues_deduplicated_packet( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from src.services import awooop_ansible_check_mode_service as service + + calls: list[tuple[str, dict[str, object]]] = [] + + class _Result: + def __init__(self, inserted: bool = False): + self.inserted = inserted + + def scalar_one_or_none(self): + if self.inserted: + return "00000000-0000-0000-0000-000000000318" + return None + + class _Db: + async def execute(self, statement, params): + sql = str(statement) + calls.append((sql, params)) + return _Result(inserted="RETURNING op_id::text" in sql) + + @asynccontextmanager + async def fake_db_context(project_id: str): + assert project_id == "awoooi" + yield _Db() + + monkeypatch.setattr(service, "get_db_context", fake_db_context) + claim = AnsibleCheckModeClaim( + op_id="00000000-0000-0000-0000-000000000319", + source_candidate_op_id="00000000-0000-0000-0000-000000000320", + incident_id="IWZ-I-2026071512-TEST", + catalog_id="ansible:wazuh-alertmanager-integration", + playbook_path=( + "infra/ansible/playbooks/wazuh-alertmanager-integration.yml" + ), + apply_playbook_path=( + "infra/ansible/playbooks/wazuh-alertmanager-integration.yml" + ), + inventory_hosts=("host_112",), + risk_level="medium", + input_payload={ + "automation_run_id": "00000000-0000-0000-0000-000000000320", + "trace_id": "trace-320", + "run_id": "run-320", + "work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "project_id": "awoooi", + "run_namespace": "awoooi:iwooos:wazuh-alert-ingress", + "proposal_source": "iwooos_wazuh_alert_ingress_scheduler", + "source_receipt_ref": "wazuh-alert:2026071500", + "source_recurrence": _current_wazuh_source_recurrence( + "2026-07-15T05:59:58+08:00" + ), + }, + ) + result = AnsibleRunResult( + returncode=2, + stdout="wazuh_privileged_convergence_capability_missing", + stderr="", + duration_ms=20, + ) + + assert _wazuh_privileged_convergence_capability_is_missing(claim, result) + await finalize_check_mode_claim(claim, result) + + assert len(calls) == 2 + insert_sql, insert_params = calls[1] + assert "ansible_break_glass_capability_work_item_queued" in insert_sql + assert "WHERE NOT EXISTS" in insert_sql + packet_input = json.loads(str(insert_params["input"])) + assert packet_input["work_item_id"] == "P0-03-WAZUH-ALERT-INGRESS" + assert packet_input["secret_value_exposed"] is False + + +@pytest.mark.asyncio +async def test_wazuh_break_glass_packet_backfill_closes_existing_gap( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from src.services import awooop_ansible_check_mode_service as service + + rows = [{ + "check_op_id": "00000000-0000-0000-0000-000000000321", + "source_candidate_op_id": "00000000-0000-0000-0000-000000000322", + "incident_id": "IWZ-I-2026071512-TEST", + "automation_run_id": "00000000-0000-0000-0000-000000000322", + "trace_id": "trace-322", + "run_id": "run-322", + "work_item_id": "P0-03-WAZUH-ALERT-INGRESS", + "project_id": "awoooi", + "run_namespace": "awoooi:iwooos:wazuh-alert-ingress", + "proposal_source": "iwooos_wazuh_alert_ingress_scheduler", + "source_receipt_ref": "wazuh-alert:2026071500", + "source_recurrence": _current_wazuh_source_recurrence( + "2026-07-15T05:59:58+08:00" + ), + }] + + class _Mappings: + def all(self): + return rows + + class _Result: + def __init__(self, *, selected: bool = False): + self.selected = selected + + def mappings(self): + return _Mappings() if self.selected else MagicMock() + + def scalar_one_or_none(self): + if not self.selected: + return "00000000-0000-0000-0000-000000000323" + return None + + class _Db: + async def execute(self, statement, _params=None): + sql = str(statement) + return _Result(selected="SELECT" in sql and "JOIN" in sql) + + @asynccontextmanager + async def fake_db_context(project_id: str): + assert project_id == "awoooi" + yield _Db() + + monkeypatch.setattr(service, "get_db_context", fake_db_context) + + result = await backfill_missing_wazuh_break_glass_capability_packets_once() + + assert result == {"scanned": 1, "written": 1, "error": None} + + 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 diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5e1a5e8a5..54ba3fd77 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14225,10 +14225,12 @@ "runway": { "source": "Source", "packet": "Packet", + "capabilityPacket": "Critical capability", "handoff": "Handoff", "ingress": "Alertmanager integration", "runtime": "Manager posture", "ready": "ready", + "queued": "queued", "waiting": "waiting", "failed": "check failed", "closed": "closed", @@ -14237,6 +14239,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", + "privilegedCapabilityPacketQueued": "A durable critical break-glass capability packet is queued; host writes remain at zero until capability verification permits a bounded retry", "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 25332eb76..c24f293ab 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14225,10 +14225,12 @@ "runway": { "source": "來源", "packet": "Packet", + "capabilityPacket": "Critical 能力包", "handoff": "Handoff", "ingress": "Alertmanager 整合", "runtime": "Manager posture", "ready": "ready", + "queued": "queued", "waiting": "waiting", "failed": "check failed", "closed": "closed", @@ -14237,6 +14239,7 @@ "runtimeFailure": { "title": "Wazuh 執行鏈", "privilegedCapabilityMissing": "主機端缺少受控提權能力;已停止寫入,需由 critical break-glass 能力包補齊後再進行受控重試", + "privilegedCapabilityPacketQueued": "Critical break-glass 能力包已建立 durable queue receipt;主機寫入仍為 0,待能力驗證後才會進行受控重試", "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 e51399d38..8c58e8fc4 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -16700,8 +16700,11 @@ 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 criticalCapabilityPacket = runtimeSummary?.critical_break_glass_capability_packet_count ?? 0 const runtimeFailureKey = runtimeData?.check_failure_class === 'wazuh_privileged_convergence_capability_missing' - ? 'privilegedCapabilityMissing' + ? criticalCapabilityPacket > 0 + ? 'privilegedCapabilityPacketQueued' + : 'privilegedCapabilityMissing' : runtimeData?.check_failure_class === 'ansible_target_unreachable' ? 'targetUnreachable' : runtimeData?.check_failure_class === 'ansible_check_mode_timeout' @@ -16722,11 +16725,17 @@ function IwoooSSecurityToolClosureBoard() { }, { key: 'packet', - labelKey: 'packet', + labelKey: runtimeFailureKey?.startsWith('privilegedCapability') ? 'capabilityPacket' : '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', + value: runtimeFailureKey?.startsWith('privilegedCapability') + ? `${criticalCapabilityPacket}/1` + : `${summary.wazuh_incident_response_closure_validator_available_count ?? 0}/1`, + tone: runtimeFailureKey?.startsWith('privilegedCapability') + ? criticalCapabilityPacket > 0 ? 'warn' as const : 'locked' as const + : (summary.wazuh_incident_response_closure_validator_available_count ?? 0) > 0 ? 'warn' as const : 'locked' as const, + state: runtimeFailureKey?.startsWith('privilegedCapability') + ? criticalCapabilityPacket > 0 ? 'queued' : 'waiting' + : (summary.wazuh_incident_response_closure_validator_available_count ?? 0) > 0 ? 'ready' : 'waiting', }, { key: 'handoff', diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index e10b9ce71..ec37f800d 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -901,6 +901,7 @@ export interface IwoooSWazuhControlledExecutorRuntimeReadbackResponse { dispatch_candidate_count: number check_mode_passed_count: number check_mode_failed_count: number + critical_break_glass_capability_packet_count: number runtime_execution_performed_count: number bounded_posture_execution_passed_count: number bounded_ingress_convergence_passed_count: number @@ -928,6 +929,21 @@ export interface IwoooSWazuhControlledExecutorRuntimeReadbackResponse { }> missing_stage_ids: string[] check_failure_class: string | null + critical_break_glass_capability_packet: { + receipt_id: string + capability_packet_id: string + status: string + queue_status: 'queued' + capability_type: 'bounded_privileged_convergence' + risk_level: 'critical' + policy_route: 'critical_break_glass_queue' + execution_allowed: false + capability_granted: false + host_write_performed: false + secret_value_exposed: false + secret_value_logged: false + secret_value_read_back: false + } | null next_safe_action: string active_blockers: string[] latest_receipt_at: string | null