diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 06ef78af7..a44318155 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -2620,6 +2620,7 @@ jobs: CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} DATABASE_URL: ${{ secrets.DATABASE_URL }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + HOST112_ANSIBLE_BECOME_PASSWORD: ${{ secrets.HOST112_ANSIBLE_BECOME_PASSWORD }} JWT_ALGORITHM: ${{ secrets.JWT_ALGORITHM }} JWT_SECRET: ${{ secrets.JWT_SECRET }} LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }} @@ -2683,6 +2684,7 @@ jobs: TG_CHAT_ID_B64="$(secret_b64_env SRE_GROUP_CHAT_ID)" NVIDIA_API_KEY_B64="$(secret_b64_env NVIDIA_API_KEY)" GEMINI_API_KEY_B64="$(secret_b64_env GEMINI_API_KEY)" + HOST112_ANSIBLE_BECOME_PASSWORD_B64="$(secret_b64_env HOST112_ANSIBLE_BECOME_PASSWORD)" LANGFUSE_PUBLIC_KEY_B64="$(secret_b64_env LANGFUSE_PUBLIC_KEY)" LANGFUSE_SECRET_KEY_B64="$(secret_b64_env LANGFUSE_SECRET_KEY)" TG_USER_WHITELIST_B64="$(secret_b64_env OPENCLAW_TG_USER_WHITELIST)" @@ -2750,6 +2752,18 @@ jobs: echo "⚠️ GEMINI_API_KEY 未設定,跳過" fi + # Wazuh host 112: only project the opaque become credential from the + # Gitea secret boundary. Never print, decode, or read it back here. + if [ -n "${HOST112_ANSIBLE_BECOME_PASSWORD_B64}" ]; then + \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ + {"op":"add","path":"/data/HOST112_ANSIBLE_BECOME_PASSWORD","value":"${HOST112_ANSIBLE_BECOME_PASSWORD_B64}"} + ]' \ + && echo "host112_become_secret_reference_injected=1" \ + || { echo "host112_become_secret_reference_injected=0"; exit 1; } + else + echo "host112_become_secret_reference_injected=0" + fi + # 2026-04-01 Claude Code: Langfuse LLMOps keys (補齊 CD 注入,之前只有手動設定) if [ -n "${LANGFUSE_PUBLIC_KEY_B64}" ] && [ -n "${LANGFUSE_SECRET_KEY_B64}" ]; then \$KUBECTL patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[ @@ -3507,6 +3521,18 @@ jobs: wait_for_rollout awoooi-ansible-executor-broker || exit 1 echo "✅ 部署完成" + # Presence-only verifier: it never reads or returns the credential. + # A missing reference keeps Wazuh convergence open without blocking + # unrelated catalogs or misclassifying the high-risk action itself as + # critical break-glass. + if $KUBECTL exec deployment/awoooi-ansible-executor-broker \ + -n awoooi-prod -- /bin/sh -c \ + 'test -s /run/secrets/host112-become/password && test -r /run/secrets/host112-become/password'; then + echo "host112_become_secret_projection_ready=1" + else + echo "host112_become_secret_projection_ready=0" + fi + # AIA-P0-011: production must prove that the public API cannot mutate # workloads or read host executor credentials. Source YAML alone is # insufficient because stale SSA-owned list fields can survive sync. diff --git a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py index 1b41027dc..96579996d 100644 --- a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py +++ b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py @@ -66,6 +66,10 @@ _WAZUH_POSTURE_WORK_ITEM_ID = "P0-03-WAZUH-MANAGER-POSTURE" _WAZUH_POSTURE_PUBLIC_ASSET_ALIAS = "security_manager_primary" _WAZUH_INGRESS_CATALOG_ID = "ansible:wazuh-alertmanager-integration" _WAZUH_INGRESS_WORK_ITEM_ID = "P0-03-WAZUH-ALERT-INGRESS" +_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE = ( + "ansible_controlled_capability_repair_queued" +) +_EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE = "remediation_executed" _WAZUH_SOURCE_RECURRENCE_DRIFT_WORK_ITEM_ID = ( "DRIFT-WAZUH-SOURCE-RECURRENCE" ) @@ -81,6 +85,34 @@ _WAZUH_SCHEDULED_RECURRENCE_PREFIX = "scheduled-wazuh-source:" _WAZUH_SCHEDULED_IDENTITY_PREFIX = "automation_operation_log:" +def _controlled_capability_packet_is_active( + row: Mapping[str, Any] | None, + *, + observed_now: datetime, +) -> bool: + if not row: + return False + if ( + str(row.get("latest_controlled_capability_packet_status") or "") + != "pending" + or str(row.get("latest_controlled_capability_queue_status") or "") + != "queued" + ): + return False + expires_at = str( + row.get("latest_controlled_capability_packet_expires_at") or "" + ).strip() + if not expires_at: + return False + try: + parsed_expiry = datetime.fromisoformat(expires_at.replace("Z", "+00:00")) + except ValueError: + return False + if parsed_expiry.tzinfo is None: + parsed_expiry = parsed_expiry.replace(tzinfo=UTC) + return parsed_expiry.astimezone(UTC) > observed_now.astimezone(UTC) + + def _validated_wazuh_source_recurrence( receipt: Mapping[str, Any] | None, *, @@ -1139,6 +1171,13 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( check_mode.status AS latest_check_status, apply.status AS latest_apply_status, learning.status AS latest_learning_status, + capability_packet.status + AS latest_controlled_capability_packet_status, + capability_packet.output ->> 'queue_status' + AS latest_controlled_capability_queue_status, + capability_packet.input + #>> '{gate_lifecycle,expires_at}' + AS latest_controlled_capability_packet_expires_at, EXISTS ( SELECT 1 FROM incident_evidence evidence @@ -1186,6 +1225,23 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( ORDER BY learning_receipt.created_at DESC LIMIT 1 ) learning ON TRUE + LEFT JOIN LATERAL ( + SELECT packet.status, packet.input, packet.output + FROM automation_operation_log packet + WHERE packet.parent_op_id = check_mode.op_id + AND ( + packet.operation_type = + :capability_packet_operation_type + OR ( + packet.operation_type = + :capability_packet_fallback_operation_type + AND packet.input ->> 'semantic_operation_type' = + :capability_packet_operation_type + ) + ) + ORDER BY packet.created_at DESC + LIMIT 1 + ) capability_packet ON TRUE WHERE candidate.operation_type = 'ansible_candidate_matched' AND candidate.input -> 'executor_candidates' @> CAST(:catalog_match AS jsonb) AND candidate.input ->> 'project_id' = :project_id @@ -1207,6 +1263,12 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( "catalog_match": json.dumps( [{"catalog_id": _catalog_id}] ), + "capability_packet_operation_type": ( + _WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE + ), + "capability_packet_fallback_operation_type": ( + _EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE + ), "freshness_hours": freshness_hours, "project_id": project_id, "work_item_id": _work_item_id, @@ -1230,6 +1292,33 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( ) if existing_row else "" retry_attempt_ref: str | None = None retry_reason = _wazuh_posture_retry_reason(existing_row) + capability_packet_pending = _controlled_capability_packet_is_active( + existing_row, + observed_now=observed_now, + ) + if ( + _catalog_id == _WAZUH_INGRESS_CATALOG_ID + and retry_reason == "check_mode_failed" + and capability_packet_pending + ): + return { + "status": "controlled_privilege_capability_repair_already_queued", + "queued": 0, + "existing": 1, + "evidence_ready": 1 if existing_evidence_ready else 0, + "automation_run_id": str( + existing_row.get("automation_run_id") + or existing_row.get("op_id") + or "" + ), + "work_item_id": _work_item_id, + "controlled_apply_allowed": True, + "retry_reason": retry_reason, + "retry_of_failed_check_mode": True, + "active_blockers": [ + "wazuh_controlled_privilege_capability_repair_pending" + ], + } if ( existing_row and retry_reason is None 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 2e262bbf6..baf2fae01 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -88,8 +88,17 @@ _WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID = ( _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING = ( "wazuh_privileged_convergence_capability_missing" ) -_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE = ( - "ansible_break_glass_capability_work_item_queued" +_WAZUH_PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING = ( + "wazuh_privileged_convergence_secret_reference_missing" +) +_WAZUH_PRIVILEGED_CONVERGENCE_FAILURE_MARKERS = frozenset( + { + _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING, + _WAZUH_PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING, + } +) +_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE = ( + "ansible_controlled_capability_repair_queued" ) _EXECUTION_CAPABILITY_OPERATION_TYPES = frozenset( { @@ -6385,9 +6394,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, + "controlled_capability_packet_scanned": 0, + "controlled_capability_packet_written": 0, + "controlled_capability_packet_error": None, "skipped": 0, "error": None, } @@ -6700,21 +6709,21 @@ 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( + capability_packet = ( + await backfill_missing_wazuh_controlled_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["controlled_capability_packet_scanned"] = int( + capability_packet.get("scanned") or 0 ) - stats["critical_break_glass_packet_written"] = int( - break_glass_packet.get("written") or 0 + stats["controlled_capability_packet_written"] = int( + capability_packet.get("written") or 0 ) - stats["critical_break_glass_packet_error"] = ( - str(break_glass_packet.get("error") or "")[:500] or None + stats["controlled_capability_packet_error"] = ( + str(capability_packet.get("error") or "")[:500] or None ) except Exception as exc: stats["error"] = f"{type(exc).__name__}: {exc}"[:500] @@ -8921,29 +8930,33 @@ async def _insert_skipped_candidate( ) -def _wazuh_privileged_convergence_capability_is_missing( +def _wazuh_privileged_convergence_failure_class( claim: AnsibleCheckModeClaim, result: AnsibleRunResult, -) -> bool: +) -> str | None: 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) - ) + return None + combined_output = " ".join((result.stdout or "", result.stderr or "")) + for failure_marker in _WAZUH_PRIVILEGED_CONVERGENCE_FAILURE_MARKERS: + if failure_marker in combined_output: + return failure_marker + return None -def _build_wazuh_break_glass_capability_packet( +def _build_wazuh_controlled_capability_packet( *, check_op_id: str, source_candidate_op_id: str, incident_id: str, catalog_id: str, source_input: Mapping[str, Any], + failure_class: str, + observed_now: datetime | None = None, ) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]: + packet_created_at = observed_now or datetime.now(UTC) automation_run_id = str( source_input.get("automation_run_id") or source_candidate_op_id ).strip()[:180] @@ -8961,11 +8974,15 @@ def _build_wazuh_break_glass_capability_packet( source_recurrence = {} packet_uuid = uuid5( NAMESPACE_URL, - f"awoooi:wazuh-break-glass:{automation_run_id}:{check_op_id}", + f"awoooi:wazuh-controlled-capability:{automation_run_id}:{check_op_id}", + ) + capability_packet_id = f"IWZ-CC-{packet_uuid.hex[:16].upper()}" + secret_reference_missing = ( + failure_class + == _WAZUH_PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING ) - capability_packet_id = f"IWZ-BG-{packet_uuid.hex[:16].upper()}" packet_input = { - "schema_version": "wazuh_privileged_convergence_capability_packet_v1", + "schema_version": "wazuh_controlled_privileged_capability_repair_v2", "capability_packet_id": capability_packet_id, "automation_run_id": automation_run_id, "trace_id": trace_id, @@ -8986,32 +9003,58 @@ def _build_wazuh_break_glass_capability_packet( "source_candidate_op_id": str(source_candidate_op_id), "source_check_mode_op_id": str(check_op_id), "catalog_id": str(catalog_id), - "semantic_operation_type": ( - _WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE - ), + "semantic_operation_type": _WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE, "capability_type": "bounded_privileged_convergence", - "risk_level": "critical", - "policy_route": "critical_break_glass_queue", - "queue_owner": "ai_critical_capability_controller", + "risk_level": "high", + "policy_route": "high_risk_controlled_apply_capability_repair", + "queue_owner": "ansible_capability_repair_agent", + "controlled_apply_allowed": True, + "windows99_dispatch_required": True, "same_run_correlation_required": True, - "execution_allowed": False, + "execution_allowed": True, + "capability_verified": False, + "capability_granted": False, + "critical_secret_reference_required": secret_reference_missing, + "secret_reference_status": ( + "missing" if secret_reference_missing else "verification_required" + ), "host_write_performed": False, "secret_value_exposed": False, "secret_value_logged": False, "secret_value_read_back": False, + "gate_lifecycle": { + "policy_version": "global_product_governance_v2", + "risk_class": "high", + "scope": "service:wazuh-manager", + "owner_lane": "ansible_capability_repair_agent", + "created_at": packet_created_at.isoformat(), + "expires_at": ( + packet_created_at + timedelta(minutes=30) + ).isoformat(), + "exit_condition": ( + "windows99_no_secret_privilege_preflight_and_same_run_" + "independent_post_verifier_pass" + ), + "replacement_action": ( + "expire_then_reconcile_capability_reference_without_host_write" + ), + "rollback_ref": "same_run_no_write_terminal_or_capability_revocation", + "post_verifier": "wazuh_alert_ingress_controlled_post_verifier", + }, } packet_output = { "queue_status": "queued", - "failure_class": _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING, + "failure_class": failure_class, "required_receipts": [ + "windows99_dispatch", "capability_reference_presence", "no_secret_privilege_preflight", - "bounded_check_mode_retry", + "bounded_same_run_check_mode_retry", "independent_post_verifier", "capability_revocation", ], "next_controlled_action": ( - "verify_bounded_privileged_capability_then_" + "windows99_verify_controlled_privilege_capability_then_" "requeue_same_run_check_mode" ), "telegram_notification_emitted": False, @@ -9029,7 +9072,7 @@ def _build_wazuh_break_glass_capability_packet( return packet_input, packet_output, dry_run_result -async def _insert_wazuh_break_glass_capability_packet( +async def _insert_wazuh_controlled_capability_packet( db: Any, *, check_op_id: str, @@ -9037,14 +9080,18 @@ async def _insert_wazuh_break_glass_capability_packet( incident_id: str, catalog_id: str, source_input: Mapping[str, Any], + failure_class: str, + observed_now: datetime | None = None, ) -> bool: packet_input, packet_output, dry_run_result = ( - _build_wazuh_break_glass_capability_packet( + _build_wazuh_controlled_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, + failure_class=failure_class, + observed_now=observed_now, ) ) inserted = await db.execute( @@ -9060,9 +9107,9 @@ async def _insert_wazuh_break_glass_capability_packet( FROM pg_constraint WHERE conname = 'automation_operation_log_type_valid' AND pg_get_constraintdef(oid) LIKE - '%{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}%' + '%{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}%' ) - THEN '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + THEN '{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}' ELSE '{_EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE}' END, 'ansible_check_mode_worker', @@ -9073,20 +9120,21 @@ async def _insert_wazuh_break_glass_capability_packet( 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' + 'ansible', 'wazuh', 'controlled_apply', 'high_risk', + 'capability_repair', 'windows99_dispatch', + '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}' + '{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}' OR ( existing.operation_type = '{_EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE}' AND existing.input ->> 'semantic_operation_type' = - '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + '{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}' ) ) AND existing.input ->> 'capability_packet_id' @@ -9169,24 +9217,29 @@ 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( + wazuh_failure_class = _wazuh_privileged_convergence_failure_class( + claim, + result, + ) + if wazuh_failure_class: + await _insert_wazuh_controlled_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, + failure_class=wazuh_failure_class, ) -async def backfill_missing_wazuh_break_glass_capability_packets_once( +async def backfill_missing_wazuh_controlled_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.""" + """Persist controlled high-risk repair packets for failed Wazuh checks.""" stats: dict[str, Any] = {"scanned": 0, "written": 0, "error": None} try: @@ -9223,7 +9276,20 @@ async def backfill_missing_wazuh_break_glass_capability_packets_once( candidate.input ->> 'source_receipt_ref' AS source_receipt_ref, candidate.input -> 'source_recurrence' - AS source_recurrence + AS source_recurrence, + check_mode.created_at AS check_created_at, + 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 ('%' || :secret_failure_marker || '%') + THEN :secret_failure_marker + ELSE :capability_failure_marker + END AS failure_class FROM automation_operation_log check_mode JOIN automation_operation_log candidate ON candidate.op_id = check_mode.parent_op_id @@ -9244,18 +9310,21 @@ async def backfill_missing_wazuh_break_glass_capability_packets_once( check_mode.output ->> 'stderr_tail', check_mode.dry_run_result ->> 'stdout_tail', check_mode.dry_run_result ->> 'stderr_tail' - ) LIKE ('%' || :failure_marker || '%') + ) LIKE ANY (ARRAY[ + '%' || :capability_failure_marker || '%', + '%' || :secret_failure_marker || '%' + ]) AND NOT EXISTS ( SELECT 1 FROM automation_operation_log packet WHERE ( packet.operation_type = - '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + '{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}' OR ( packet.operation_type = '{_EXECUTION_CAPABILITY_FALLBACK_OPERATION_TYPE}' AND packet.input ->> 'semantic_operation_type' = - '{_WAZUH_BREAK_GLASS_CAPABILITY_PACKET_OPERATION_TYPE}' + '{_WAZUH_CONTROLLED_CAPABILITY_PACKET_OPERATION_TYPE}' ) ) AND packet.parent_op_id = check_mode.op_id @@ -9273,9 +9342,12 @@ async def backfill_missing_wazuh_break_glass_capability_packets_once( ) }] ), - "failure_marker": ( + "capability_failure_marker": ( _WAZUH_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING ), + "secret_failure_marker": ( + _WAZUH_PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING + ), }, ) rows = [dict(row) for row in result.mappings().all()] @@ -9292,7 +9364,11 @@ async def backfill_missing_wazuh_break_glass_capability_packets_once( "source_receipt_ref": row.get("source_receipt_ref"), "source_recurrence": row.get("source_recurrence"), } - if await _insert_wazuh_break_glass_capability_packet( + failure_class = str(row.get("failure_class") or "").strip() + if failure_class not in _WAZUH_PRIVILEGED_CONVERGENCE_FAILURE_MARKERS: + stats["error"] = "unsupported_wazuh_failure_class" + continue + if await _insert_wazuh_controlled_capability_packet( db, check_op_id=str(row.get("check_op_id") or ""), source_candidate_op_id=str( @@ -9301,12 +9377,18 @@ async def backfill_missing_wazuh_break_glass_capability_packets_once( incident_id=str(row.get("incident_id") or ""), catalog_id=_WAZUH_ALERTMANAGER_INTEGRATION_CATALOG_ID, source_input=source_input, + failure_class=failure_class, + observed_now=( + row.get("check_created_at") + if isinstance(row.get("check_created_at"), datetime) + else None + ), ): stats["written"] += 1 except Exception as exc: stats["error"] = f"{type(exc).__name__}: {exc}"[:500] logger.warning( - "wazuh_break_glass_capability_packet_backfill_failed", + "wazuh_controlled_capability_packet_backfill_failed", project_id=project_id, **stats, ) @@ -9796,15 +9878,15 @@ 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 + "controlled_capability_packet_scanned": int( + receipt_stats.get("controlled_capability_packet_scanned") or 0 ), - "critical_break_glass_packet_written": int( - receipt_stats.get("critical_break_glass_packet_written") or 0 + "controlled_capability_packet_written": int( + receipt_stats.get("controlled_capability_packet_written") or 0 ), - "critical_break_glass_packet_error": ( + "controlled_capability_packet_error": ( str( - receipt_stats.get("critical_break_glass_packet_error") or "" + receipt_stats.get("controlled_capability_packet_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 3a764aa66..a27cee135 100644 --- a/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py @@ -22,7 +22,7 @@ from src.services.ai_automation_runtime_contract import ( ) from src.services.awooop_ansible_audit_service import get_ansible_catalog_item -SCHEMA_VERSION = "iwooos_wazuh_controlled_executor_runtime_readback_v1" +SCHEMA_VERSION = "iwooos_wazuh_controlled_executor_runtime_readback_v2" CATALOG_ID = "ansible:wazuh-manager-posture-readback" INGRESS_CATALOG_ID = "ansible:wazuh-alertmanager-integration" WORK_ITEM_ID = "P0-03-WAZUH-MANAGER-POSTURE" @@ -35,8 +35,14 @@ _WAZUH_SCHEDULED_RECURRENCE_SOURCE = "iwooos_asset_sensor_receipt" _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING = ( "wazuh_privileged_convergence_capability_missing" ) +_PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING = ( + "wazuh_privileged_convergence_secret_reference_missing" +) _PUBLIC_SAFE_CHECK_FAILURE_MARKERS = frozenset( - {_PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING} + { + _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING, + _PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING, + } ) _LIVE_RUNTIME_SQL = """ @@ -90,6 +96,15 @@ _LIVE_RUNTIME_SQL = """ 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_secret_reference_missing%' + THEN 'wazuh_privileged_convergence_secret_reference_missing' WHEN concat_ws( ' ', check_mode.error, @@ -131,10 +146,10 @@ _LIVE_RUNTIME_SQL = """ WHERE apply.operation_type = 'ansible_apply_executed' ORDER BY apply.created_at DESC LIMIT 1 - ), latest_break_glass_packet AS ( + ), latest_controlled_capability_packet AS ( SELECT - packet.op_id::text AS break_glass_packet_receipt_id, - packet.status AS break_glass_packet_status, + packet.op_id::text AS controlled_capability_packet_receipt_id, + packet.status AS controlled_capability_packet_status, packet.input ->> 'capability_packet_id' AS capability_packet_id, packet.input ->> 'automation_run_id' AS capability_automation_run_id, @@ -149,6 +164,20 @@ _LIVE_RUNTIME_SQL = """ packet.input ->> 'capability_type' AS capability_type, packet.input ->> 'risk_level' AS capability_risk_level, packet.input ->> 'policy_route' AS capability_policy_route, + coalesce( + (packet.input ->> 'controlled_apply_allowed')::boolean, + false + ) AS capability_controlled_apply_allowed, + coalesce( + (packet.input ->> 'windows99_dispatch_required')::boolean, + false + ) AS capability_windows99_dispatch_required, + coalesce( + (packet.input ->> 'critical_secret_reference_required')::boolean, + false + ) AS capability_critical_secret_reference_required, + packet.input #>> '{gate_lifecycle,expires_at}' + AS capability_expires_at, packet.output ->> 'queue_status' AS capability_queue_status, coalesce( (packet.input ->> 'execution_allowed')::boolean, @@ -162,17 +191,17 @@ _LIVE_RUNTIME_SQL = """ (packet.dry_run_result ->> 'host_write_performed')::boolean, false ) AS capability_packet_host_write_performed, - packet.created_at AS break_glass_packet_created_at + packet.created_at AS controlled_capability_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' + 'ansible_controlled_capability_repair_queued' OR ( packet.operation_type = 'remediation_executed' AND packet.input ->> 'semantic_operation_type' = - 'ansible_break_glass_capability_work_item_queued' + 'ansible_controlled_capability_repair_queued' ) ) ORDER BY packet.created_at DESC @@ -286,7 +315,7 @@ _LIVE_RUNTIME_SQL = """ SELECT candidate.*, check_mode.*, - break_glass_packet.*, + controlled_capability_packet.*, apply.*, runtime_stages.stage_ids, runtime_stages.runtime_stage_identity_verified, @@ -297,7 +326,8 @@ _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_controlled_capability_packet + controlled_capability_packet ON TRUE LEFT JOIN latest_apply apply ON TRUE LEFT JOIN runtime_stages ON TRUE LEFT JOIN latest_verifier verifier ON TRUE @@ -485,12 +515,12 @@ 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") + controlled_capability_packet_receipt_present = bool( + data.get("controlled_capability_packet_receipt_id") ) capability_packet_id = str(data.get("capability_packet_id") or "") - break_glass_packet_identity_verified = bool( - break_glass_packet_receipt_present + controlled_capability_packet_identity_verified = bool( + controlled_capability_packet_receipt_present and candidate_identity_verified and check_identity_verified and str(data.get("capability_automation_run_id") or "") @@ -507,17 +537,33 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( and data.get("capability_source_recurrence") == candidate_source_recurrence ) - break_glass_packet_present = all( + capability_expires_at = _utc_receipt_datetime( + data.get("capability_expires_at") + ) + controlled_capability_packet_active = bool( + capability_expires_at + and capability_expires_at > datetime.now(UTC) + ) + controlled_capability_packet_present = all( ( - break_glass_packet_identity_verified, - data.get("break_glass_packet_status") == "pending", - capability_packet_id.startswith("IWZ-BG-"), + controlled_capability_packet_identity_verified, + controlled_capability_packet_active, + data.get("controlled_capability_packet_status") == "pending", + capability_packet_id.startswith("IWZ-CC-"), 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_risk_level") == "high", + data.get("capability_policy_route") + == "high_risk_controlled_apply_capability_repair", data.get("capability_queue_status") == "queued", - data.get("capability_execution_allowed") is False, + data.get("capability_controlled_apply_allowed") is True, + data.get("capability_windows99_dispatch_required") is True, + bool(data.get("capability_critical_secret_reference_required")) + == ( + check_failure_class + == _PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING + ), + data.get("capability_execution_allowed") is True, data.get("capability_granted") is False, data.get("capability_packet_host_write_performed") is False, ) @@ -601,14 +647,19 @@ 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: + if check_failure_class in _PUBLIC_SAFE_CHECK_FAILURE_MARKERS: active_blockers.append( - "critical_break_glass_capability_packet_pending" - if break_glass_packet_present + "controlled_capability_repair_packet_pending" + if controlled_capability_packet_present else ( - "critical_break_glass_capability_packet_identity_mismatch" - if break_glass_packet_receipt_present - else "critical_break_glass_capability_packet_missing" + "controlled_capability_repair_packet_expired" + if ( + controlled_capability_packet_identity_verified + and not controlled_capability_packet_active + ) + else "controlled_capability_repair_packet_identity_mismatch" + if controlled_capability_packet_receipt_present + else "controlled_capability_repair_packet_missing" ) ) if apply_failed: @@ -652,8 +703,8 @@ 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 + "controlled_capability_repair_packet_count": ( + 1 if controlled_capability_packet_present else 0 ), "runtime_execution_performed_count": 1 if apply_terminal else 0, "bounded_posture_execution_passed_count": ( @@ -699,27 +750,32 @@ 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": ( + "controlled_capability_repair_packet": ( { "receipt_id": str( - data.get("break_glass_packet_receipt_id") or "" - ), - "capability_packet_id": str( - capability_packet_id + data.get("controlled_capability_packet_receipt_id") or "" ), + "capability_packet_id": 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, + "risk_level": "high", + "policy_route": "high_risk_controlled_apply_capability_repair", + "controlled_apply_allowed": True, + "windows99_dispatch_required": True, + "critical_secret_reference_required": bool( + data.get("capability_critical_secret_reference_required") + is True + ), + "expires_at": capability_expires_at.isoformat(), + "execution_allowed": True, "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 + if controlled_capability_packet_present else None ), "next_safe_action": _next_action( @@ -728,7 +784,9 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( check_passed=check_passed, check_failed=check_failed, check_failure_class=check_failure_class, - break_glass_packet_present=break_glass_packet_present, + controlled_capability_packet_present=( + controlled_capability_packet_present + ), apply_terminal=apply_terminal, apply_passed=apply_passed, verifier_passed=verifier_passed, @@ -754,21 +812,26 @@ def build_iwooos_wazuh_controlled_executor_runtime_readback( "secret_value_collection_allowed": False, "critical_break_glass_required": ( check_failure_class - == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING + == _PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING ), - "critical_break_glass_capability_packet_persisted": ( - break_glass_packet_present + "controlled_capability_repair_packet_persisted": ( + controlled_capability_packet_present ), - "critical_break_glass_capability_packet_identity_verified": ( - break_glass_packet_identity_verified + "controlled_capability_repair_packet_identity_verified": ( + controlled_capability_packet_identity_verified ), - "critical_break_glass_capability_granted": False, + "controlled_capability_repair_packet_active": ( + controlled_capability_packet_active + ), + "controlled_capability_verified": False, "github_api_used": False, }, "source_refs": [ "infra/ansible/playbooks/wazuh-manager-posture-readback.yml", "infra/ansible/playbooks/wazuh-alertmanager-integration.yml", "infra/ansible/files/wazuh/custom-awoooi-alertmanager.py", + "k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml", + ".gitea/workflows/cd.yaml", "apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py", "apps/api/src/services/awooop_ansible_check_mode_service.py", "apps/api/src/services/iwooos_wazuh_controlled_executor_runtime.py", @@ -812,7 +875,7 @@ def _next_action( check_passed: bool, check_failed: bool, check_failure_class: str | None, - break_glass_packet_present: bool, + controlled_capability_packet_present: bool, apply_terminal: bool, apply_passed: bool, verifier_passed: bool, @@ -835,18 +898,22 @@ def _next_action( else "internal_worker_enqueue_wazuh_manager_posture_candidate" ) if check_failed: - if ( - check_failure_class - == _PRIVILEGED_CONVERGENCE_CAPABILITY_MISSING - ): - if break_glass_packet_present: + if check_failure_class in _PUBLIC_SAFE_CHECK_FAILURE_MARKERS: + if controlled_capability_packet_present: return ( - "critical_break_glass_capability_packet_queued_waiting_" - "bounded_capability_verifier_then_retry" + "windows99_verify_controlled_privilege_capability_then_" + "requeue_same_run" + ) + if ( + check_failure_class + == _PRIVILEGED_CONVERGENCE_SECRET_REFERENCE_MISSING + ): + return ( + "queue_high_risk_capability_repair_with_critical_secret_" + "reference_boundary" ) return ( - "queue_critical_break_glass_privileged_convergence_capability_" - "then_bounded_retry" + "queue_high_risk_controlled_privilege_capability_repair_then_retry" ) return ( "repair_and_enqueue_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 6e53bc7ae..280c4433f 100644 --- a/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py @@ -22,9 +22,9 @@ 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, + _build_wazuh_controlled_capability_packet, + _wazuh_privileged_convergence_failure_class, + backfill_missing_wazuh_controlled_capability_packets_once, finalize_check_mode_claim, ) from src.services.awooop_ansible_post_verifier import postconditions_for_catalog @@ -48,7 +48,7 @@ def _current_wazuh_source_recurrence(observed_at: str) -> dict[str, object]: def test_wazuh_runtime_sql_reads_schema_safe_packet_fallback() -> None: - assert "ansible_break_glass_capability_work_item_queued" in _LIVE_RUNTIME_SQL + assert "ansible_controlled_capability_repair_queued" in _LIVE_RUNTIME_SQL assert "remediation_executed" in _LIVE_RUNTIME_SQL assert "semantic_operation_type" in _LIVE_RUNTIME_SQL @@ -146,7 +146,7 @@ def test_wazuh_runtime_readback_closes_only_with_complete_same_run_receipts() -> assert ( payload["schema_version"] - == "iwooos_wazuh_controlled_executor_runtime_readback_v1" + == "iwooos_wazuh_controlled_executor_runtime_readback_v2" ) assert payload["status"] == "wazuh_manager_posture_same_run_runtime_closed" assert payload["summary"]["runtime_execution_performed_count"] == 1 @@ -373,8 +373,7 @@ def test_wazuh_runtime_readback_projects_allowlisted_privilege_blocker() -> None "wazuh_privileged_convergence_capability_missing" ) assert payload["next_safe_action"] == ( - "queue_critical_break_glass_privileged_convergence_capability_" - "then_bounded_retry" + "queue_high_risk_controlled_privilege_capability_repair_then_retry" ) assert "wazuh_privileged_convergence_capability_missing" in ( payload["active_blockers"] @@ -382,15 +381,61 @@ 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" + "controlled_capability_repair_packet_count" ] == 0 - assert payload["critical_break_glass_capability_packet"] is None - assert "critical_break_glass_capability_packet_missing" in ( + assert payload["controlled_capability_repair_packet"] is None + assert "controlled_capability_repair_packet_missing" in ( payload["active_blockers"] ) -def test_wazuh_runtime_readback_projects_durable_break_glass_packet() -> None: +def test_wazuh_runtime_readback_keeps_only_secret_reference_as_critical() -> None: + run_id = "00000000-0000-0000-0000-000000000342" + recurrence = _current_wazuh_source_recurrence( + "2026-07-17T10:00:00+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:2026071710", + "candidate_source_recurrence": recurrence, + "candidate_source_recurrence_verified": True, + "catalog_id": "ansible:wazuh-alertmanager-integration", + "check_op_id": "00000000-0000-0000-0000-000000000343", + "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_secret_reference_missing" + ), + "stage_ids": [], + } + + payload = build_iwooos_wazuh_controlled_executor_runtime_readback( + row, + executor_enabled=True, + ) + + assert payload["boundaries"]["critical_break_glass_required"] is True + assert payload["boundaries"]["secret_value_collection_allowed"] is False + assert payload["next_safe_action"] == ( + "queue_high_risk_capability_repair_with_critical_secret_" + "reference_boundary" + ) + + +def test_wazuh_runtime_readback_projects_controlled_capability_packet() -> None: run_id = "00000000-0000-0000-0000-000000000314" recurrence = _current_wazuh_source_recurrence( "2026-07-15T05:59:58+08:00" @@ -420,11 +465,11 @@ def test_wazuh_runtime_readback_projects_durable_break_glass_packet() -> None: "check_failure_marker": ( "wazuh_privileged_convergence_capability_missing" ), - "break_glass_packet_receipt_id": ( + "controlled_capability_packet_receipt_id": ( "00000000-0000-0000-0000-000000000315" ), - "break_glass_packet_status": "pending", - "capability_packet_id": "IWZ-BG-0011223344556677", + "controlled_capability_packet_status": "pending", + "capability_packet_id": "IWZ-CC-0011223344556677", "capability_automation_run_id": run_id, "capability_trace_id": run_id, "capability_run_id": run_id, @@ -436,10 +481,16 @@ def test_wazuh_runtime_readback_projects_durable_break_glass_packet() -> None: ), "capability_source_recurrence": recurrence, "capability_type": "bounded_privileged_convergence", - "capability_risk_level": "critical", - "capability_policy_route": "critical_break_glass_queue", + "capability_risk_level": "high", + "capability_policy_route": ( + "high_risk_controlled_apply_capability_repair" + ), "capability_queue_status": "queued", - "capability_execution_allowed": False, + "capability_controlled_apply_allowed": True, + "capability_windows99_dispatch_required": True, + "capability_critical_secret_reference_required": False, + "capability_expires_at": "2030-07-17T02:30:00+00:00", + "capability_execution_allowed": True, "capability_granted": False, "capability_packet_host_write_performed": False, "stage_ids": [], @@ -451,25 +502,39 @@ def test_wazuh_runtime_readback_projects_durable_break_glass_packet() -> None: ) assert payload["summary"][ - "critical_break_glass_capability_packet_count" + "controlled_capability_repair_packet_count" ] == 1 assert payload["next_safe_action"] == ( - "critical_break_glass_capability_packet_queued_waiting_" - "bounded_capability_verifier_then_retry" + "windows99_verify_controlled_privilege_capability_then_" + "requeue_same_run" ) - packet = payload["critical_break_glass_capability_packet"] + packet = payload["controlled_capability_repair_packet"] assert packet["queue_status"] == "queued" - assert packet["execution_allowed"] is False + assert packet["execution_allowed"] is True + assert packet["controlled_apply_allowed"] is True + assert packet["windows99_dispatch_required"] is True 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 ( + assert "controlled_capability_repair_packet_pending" in ( payload["active_blockers"] ) assert payload["boundaries"][ - "critical_break_glass_capability_packet_persisted" + "controlled_capability_repair_packet_persisted" ] is True + row["capability_expires_at"] = "2020-07-17T02:30:00+00:00" + expired = build_iwooos_wazuh_controlled_executor_runtime_readback( + row, + executor_enabled=True, + ) + assert expired["summary"][ + "controlled_capability_repair_packet_count" + ] == 0 + assert "controlled_capability_repair_packet_expired" in ( + expired["active_blockers"] + ) + def test_wazuh_runtime_readback_rejects_untrusted_failure_marker() -> None: row = { @@ -491,7 +556,7 @@ 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: +def test_wazuh_controlled_capability_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", @@ -512,12 +577,14 @@ def test_wazuh_break_glass_packet_is_deterministic_and_public_safe() -> None: "unexpected_material": "must-not-be-projected", "command_output": "must-not-be-projected", }, + "failure_class": "wazuh_privileged_convergence_capability_missing", + "observed_now": datetime(2026, 7, 17, 2, 0, tzinfo=UTC), } packet_input, packet_output, dry_run = ( - _build_wazuh_break_glass_capability_packet(**kwargs) + _build_wazuh_controlled_capability_packet(**kwargs) ) - repeated_input, _, _ = _build_wazuh_break_glass_capability_packet( + repeated_input, _, _ = _build_wazuh_controlled_capability_packet( **kwargs ) serialized = json.dumps( @@ -527,8 +594,14 @@ def test_wazuh_break_glass_packet_is_deterministic_and_public_safe() -> None: 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["risk_level"] == "high" + assert packet_input["execution_allowed"] is True + assert packet_input["controlled_apply_allowed"] is True + assert packet_input["windows99_dispatch_required"] is True + assert packet_input["critical_secret_reference_required"] is False + assert packet_input["gate_lifecycle"]["expires_at"] == ( + "2026-07-17T02:30:00+00:00" + ) assert packet_input["host_write_performed"] is False assert packet_input["project_id"] == "awoooi" assert packet_input["source_recurrence"]["verified"] is True @@ -601,25 +674,29 @@ async def test_failed_wazuh_check_atomically_queues_deduplicated_packet( duration_ms=20, ) - assert _wazuh_privileged_convergence_capability_is_missing(claim, result) + assert _wazuh_privileged_convergence_failure_class(claim, result) == ( + "wazuh_privileged_convergence_capability_missing" + ) 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 "ansible_controlled_capability_repair_queued" in insert_sql assert "remediation_executed" in insert_sql assert "pg_get_constraintdef" 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["semantic_operation_type"] == ( - "ansible_break_glass_capability_work_item_queued" + "ansible_controlled_capability_repair_queued" ) + assert packet_input["risk_level"] == "high" + assert packet_input["execution_allowed"] is True assert packet_input["secret_value_exposed"] is False @pytest.mark.asyncio -async def test_wazuh_break_glass_packet_backfill_closes_existing_gap( +async def test_wazuh_controlled_capability_packet_backfill_closes_existing_gap( monkeypatch: pytest.MonkeyPatch, ) -> None: from src.services import awooop_ansible_check_mode_service as service @@ -639,6 +716,7 @@ async def test_wazuh_break_glass_packet_backfill_closes_existing_gap( "source_recurrence": _current_wazuh_source_recurrence( "2026-07-15T05:59:58+08:00" ), + "failure_class": "wazuh_privileged_convergence_capability_missing", }] class _Mappings: @@ -669,7 +747,7 @@ async def test_wazuh_break_glass_packet_backfill_closes_existing_gap( monkeypatch.setattr(service, "get_db_context", fake_db_context) - result = await backfill_missing_wazuh_break_glass_capability_packets_once() + result = await backfill_missing_wazuh_controlled_capability_packets_once() assert result == {"scanned": 1, "written": 1, "error": None} diff --git a/apps/api/tests/test_wazuh_alertmanager_integration.py b/apps/api/tests/test_wazuh_alertmanager_integration.py index 66ba65e67..d66c91614 100644 --- a/apps/api/tests/test_wazuh_alertmanager_integration.py +++ b/apps/api/tests/test_wazuh_alertmanager_integration.py @@ -52,6 +52,7 @@ BROKER_DEPLOYMENT = ( ROOT / "k8s" / "awoooi-prod" / "08-deployment-ansible-executor-broker.yaml" ) WORKER_DEPLOYMENT = ROOT / "k8s" / "awoooi-prod" / "08-deployment-worker.yaml" +CD_WORKFLOW = ROOT / ".gitea" / "workflows" / "cd.yaml" CATALOG_ID = "ansible:wazuh-alertmanager-integration" @@ -179,9 +180,19 @@ def test_wazuh_integration_catalog_and_rollback_contract_are_bounded() -> None: assert self_test_task["ansible.builtin.command"]["argv"][0] == ( "{{ ansible_playbook_python }}" ) + secret_reference_assert = next( + task + for task in playbook[0]["tasks"] + if task.get("ansible.builtin.assert", {}).get("fail_msg") + == "wazuh_privileged_convergence_secret_reference_missing" + ) + assert secret_reference_assert.get("no_log") is not True assert "backup: true" in text assert "remote_src: true" in text assert "wazuh_privileged_convergence_capability_missing" in text + assert "wazuh_privileged_convergence_secret_reference_missing" in text + assert "ANSIBLE_BECOME_PASSWORD_FILE" in text + assert "get_checksum: false" in text assert "/var/lib/awoooi-wazuh-alert-ingress/receipt.env" in text assert "raw_payload_persisted=0" in text assert "active_response_enabled=0" in text @@ -255,6 +266,27 @@ def test_wazuh_become_password_projection_is_path_only_and_optional( assert "ANSIBLE_BECOME_PASSWORD_FILE" not in unrelated_spec.env +def test_cd_projects_wazuh_privilege_reference_without_readback() -> None: + workflow = CD_WORKFLOW.read_text(encoding="utf-8") + + assert ( + "HOST112_ANSIBLE_BECOME_PASSWORD: " + "${{ secrets.HOST112_ANSIBLE_BECOME_PASSWORD }}" + ) in workflow + assert ( + 'HOST112_ANSIBLE_BECOME_PASSWORD_B64="$(secret_b64_env ' + 'HOST112_ANSIBLE_BECOME_PASSWORD)"' + ) in workflow + assert '"path":"/data/HOST112_ANSIBLE_BECOME_PASSWORD"' in workflow + assert "host112_become_secret_reference_injected=1" in workflow + assert "host112_become_secret_projection_ready=1" in workflow + assert ( + "test -s /run/secrets/host112-become/password && " + "test -r /run/secrets/host112-become/password" + ) in workflow + assert "kubectl get secret awoooi-secrets -o json" not in workflow + + def test_wazuh_ingress_convergence_uses_only_the_mutating_catalog() -> None: route = resolve_typed_alert_target( alertname="WazuhAlertmanagerIntegrationConvergence", @@ -511,6 +543,115 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat ] +@pytest.mark.asyncio +async def test_wazuh_ingress_scheduler_does_not_repeat_failed_check_while_capability_repair_is_open( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from src.jobs import awooop_ansible_candidate_backfill_job as job + + run_id = "00000000-0000-0000-0000-000000000572" + + class _Mappings: + def first(self): + return { + "op_id": run_id, + "automation_run_id": run_id, + "source_receipt_ref": "wazuh-alert:2026071500", + "latest_check_status": "failed", + "latest_apply_status": None, + "latest_learning_status": None, + "latest_controlled_capability_packet_status": "pending", + "latest_controlled_capability_queue_status": "queued", + "latest_controlled_capability_packet_expires_at": ( + "2026-07-15T06:29:59+08:00" + ), + "predecision_evidence_ready": True, + } + + class _Result: + def mappings(self): + return _Mappings() + + class _Db: + async def execute(self, statement, params): + query = str(statement) + assert ":capability_packet_operation_type" in query + assert params["capability_packet_operation_type"] == ( + "ansible_controlled_capability_repair_queued" + ) + return _Result() + + @asynccontextmanager + async def fake_db_context(_project_id: str): + yield _Db() + + recorder = AsyncMock(return_value=True) + collector = AsyncMock(return_value=MagicMock(snapshot_id="must-not-run")) + verifier = AsyncMock(return_value=True) + binder = AsyncMock(return_value=True) + monkeypatch.setattr(job, "get_db_context", fake_db_context) + monkeypatch.setattr( + job.settings, + "ENABLE_IWOOOS_WAZUH_MANAGER_POSTURE_EXECUTOR", + True, + ) + monkeypatch.setattr( + job, + "now_taipei", + MagicMock(return_value=datetime.fromisoformat("2026-07-15T05:59:59+08:00")), + ) + + result = await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once( + recorder=recorder, + evidence_collector=collector, + evidence_verifier=verifier, + incident_binder=binder, + source_recurrence=_current_wazuh_source_recurrence( + "2026-07-15T05:59:58+08:00" + ), + ) + + assert result["status"] == ( + "controlled_privilege_capability_repair_already_queued" + ) + assert result["queued"] == 0 + assert result["retry_of_failed_check_mode"] is True + assert result["active_blockers"] == [ + "wazuh_controlled_privilege_capability_repair_pending" + ] + recorder.assert_not_awaited() + collector.assert_not_awaited() + verifier.assert_not_awaited() + binder.assert_not_awaited() + + +def test_wazuh_controlled_capability_packet_cooldown_expires() -> None: + from src.jobs import awooop_ansible_candidate_backfill_job as job + + observed_now = datetime.fromisoformat("2026-07-15T05:59:59+08:00") + row = { + "latest_controlled_capability_packet_status": "pending", + "latest_controlled_capability_queue_status": "queued", + "latest_controlled_capability_packet_expires_at": ( + "2026-07-15T06:29:59+08:00" + ), + } + + assert job._controlled_capability_packet_is_active( + row, + observed_now=observed_now, + ) is True + assert job._controlled_capability_packet_is_active( + { + **row, + "latest_controlled_capability_packet_expires_at": ( + "2026-07-15T05:59:58+08:00" + ), + }, + observed_now=observed_now, + ) is False + + @pytest.mark.asyncio async def test_wazuh_ingress_scheduler_fails_closed_without_current_recurrence( monkeypatch: pytest.MonkeyPatch, diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index b3924aeb8..f63c23ec0 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -14236,7 +14236,7 @@ "runway": { "source": "Source", "packet": "Packet", - "capabilityPacket": "Critical capability", + "capabilityPacket": "Controlled capability", "handoff": "Handoff", "ingress": "Alertmanager integration", "runtime": "Manager posture", @@ -14249,8 +14249,10 @@ }, "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", + "privilegedCapabilityMissing": "Controlled privilege is unverified. AI will run a high-risk repair through Windows99 and retry the same run.", + "privilegedCapabilityPacketQueued": "The controlled repair is queued; Windows99 will verify it and retry the same run.", + "privilegedSecretReferenceMissing": "Host 112's opaque credential reference is missing; its value is never read back or logged.", + "privilegedSecretReferencePacketQueued": "Repair is queued for the credential reference and verifier, without a human terminal.", "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 1b279750d..b7304c3ae 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -14236,7 +14236,7 @@ "runway": { "source": "來源", "packet": "Packet", - "capabilityPacket": "Critical 能力包", + "capabilityPacket": "受控能力包", "handoff": "Handoff", "ingress": "Alertmanager 整合", "runtime": "Manager posture", @@ -14249,8 +14249,10 @@ }, "runtimeFailure": { "title": "Wazuh 執行鏈", - "privilegedCapabilityMissing": "主機端缺少受控提權能力;已停止寫入,需由 critical break-glass 能力包補齊後再進行受控重試", - "privilegedCapabilityPacketQueued": "Critical break-glass 能力包已建立 durable queue receipt;主機寫入仍為 0,待能力驗證後才會進行受控重試", + "privilegedCapabilityMissing": "受控提權能力未驗證。AI 將由 Windows99 執行 high-risk 修復並重試同一 run。", + "privilegedCapabilityPacketQueued": "受控能力修復已排隊;Windows99 驗證後自動重試同一 run。", + "privilegedSecretReferenceMissing": "主機 112 的不透明憑證引用未投影;敏感值不讀回、不記錄。", + "privilegedSecretReferencePacketQueued": "修復包已排隊;等待憑證引用與 verifier,不轉成人工終局。", "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 8c58e8fc4..90736e693 100644 --- a/apps/web/src/app/[locale]/iwooos/page.tsx +++ b/apps/web/src/app/[locale]/iwooos/page.tsx @@ -16700,20 +16700,24 @@ 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' - ? criticalCapabilityPacket > 0 - ? 'privilegedCapabilityPacketQueued' - : '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 controlledCapabilityPacket = runtimeSummary?.controlled_capability_repair_packet_count ?? 0 + const runtimeFailureKey = runtimeData?.check_failure_class === 'wazuh_privileged_convergence_secret_reference_missing' + ? controlledCapabilityPacket > 0 + ? 'privilegedSecretReferencePacketQueued' + : 'privilegedSecretReferenceMissing' + : runtimeData?.check_failure_class === 'wazuh_privileged_convergence_capability_missing' + ? controlledCapabilityPacket > 0 + ? 'privilegedCapabilityPacketQueued' + : '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', @@ -16725,16 +16729,16 @@ function IwoooSSecurityToolClosureBoard() { }, { key: 'packet', - labelKey: runtimeFailureKey?.startsWith('privilegedCapability') ? 'capabilityPacket' : 'packet', + labelKey: runtimeFailureKey?.startsWith('privileged') ? 'capabilityPacket' : 'packet', icon: FileCheck2, - value: runtimeFailureKey?.startsWith('privilegedCapability') - ? `${criticalCapabilityPacket}/1` + value: runtimeFailureKey?.startsWith('privileged') + ? `${controlledCapabilityPacket}/1` : `${summary.wazuh_incident_response_closure_validator_available_count ?? 0}/1`, - tone: runtimeFailureKey?.startsWith('privilegedCapability') - ? criticalCapabilityPacket > 0 ? 'warn' as const : 'locked' as const + tone: runtimeFailureKey?.startsWith('privileged') + ? controlledCapabilityPacket > 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' + state: runtimeFailureKey?.startsWith('privileged') + ? controlledCapabilityPacket > 0 ? 'queued' : 'waiting' : (summary.wazuh_incident_response_closure_validator_available_count ?? 0) > 0 ? 'ready' : 'waiting', }, { diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index ec37f800d..6bce36e8f 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -885,7 +885,7 @@ export interface IwoooSSecurityToolClosureReadbackResponse { } export interface IwoooSWazuhControlledExecutorRuntimeReadbackResponse { - schema_version: 'iwooos_wazuh_controlled_executor_runtime_readback_v1' + schema_version: 'iwooos_wazuh_controlled_executor_runtime_readback_v2' status: string mode: string work_item_id: string @@ -901,7 +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 + controlled_capability_repair_packet_count: number runtime_execution_performed_count: number bounded_posture_execution_passed_count: number bounded_ingress_convergence_passed_count: number @@ -929,15 +929,19 @@ export interface IwoooSWazuhControlledExecutorRuntimeReadbackResponse { }> missing_stage_ids: string[] check_failure_class: string | null - critical_break_glass_capability_packet: { + controlled_capability_repair_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 + risk_level: 'high' + policy_route: 'high_risk_controlled_apply_capability_repair' + controlled_apply_allowed: true + windows99_dispatch_required: true + critical_secret_reference_required: boolean + expires_at: string + execution_allowed: true capability_granted: false host_write_performed: false secret_value_exposed: false diff --git a/infra/ansible/playbooks/wazuh-alertmanager-integration.yml b/infra/ansible/playbooks/wazuh-alertmanager-integration.yml index 746f9b016..94c83b674 100644 --- a/infra/ansible/playbooks/wazuh-alertmanager-integration.yml +++ b/infra/ansible/playbooks/wazuh-alertmanager-integration.yml @@ -37,6 +37,40 @@ check_mode: false changed_when: false + - name: Resolve the opaque controller become-secret reference + ansible.builtin.set_fact: + wazuh_become_password_file_ref: >- + {{ lookup('ansible.builtin.env', 'ANSIBLE_BECOME_PASSWORD_FILE') | trim }} + delegate_to: localhost + become: false + check_mode: false + no_log: true + + - name: Inspect the controller become-secret projection without reading it + ansible.builtin.stat: + path: "{{ wazuh_become_password_file_ref }}" + get_attributes: false + get_checksum: false + get_mime: false + delegate_to: localhost + become: false + register: wazuh_become_password_file + when: wazuh_become_password_file_ref | length > 0 + check_mode: false + changed_when: false + no_log: true + + - name: Fail closed when the opaque become-secret reference is absent + ansible.builtin.assert: + that: + - wazuh_become_password_file_ref | length > 0 + - (wazuh_become_password_file.stat | default({})).get('exists', false) + - (wazuh_become_password_file.stat | default({})).get('isreg', false) + - ((wazuh_become_password_file.stat | default({})).get('size', 0) | int) > 0 + fail_msg: wazuh_privileged_convergence_secret_reference_missing + delegate_to: localhost + become: false + - name: Probe the exact privileged convergence capability ansible.builtin.command: argv: