From 89aee2157081d196d5eb067419200141d35cc57d Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 13:11:47 +0800 Subject: [PATCH] fix(security): route Wazuh to canonical executor --- .../controlled_alert_target_router.py | 7 +- ...3s_controlled_automation_work_items_api.py | 32 +++++++ .../api/tests/test_sre_typed_domain_router.py | 86 +++++++++++++++++++ .../15-service-registry-configmap.yaml | 16 +++- ops/config/service-registry.yaml | 14 +++ 5 files changed, 153 insertions(+), 2 deletions(-) diff --git a/apps/api/src/services/controlled_alert_target_router.py b/apps/api/src/services/controlled_alert_target_router.py index 265dd5f90..7ae678a4d 100644 --- a/apps/api/src/services/controlled_alert_target_router.py +++ b/apps/api/src/services/controlled_alert_target_router.py @@ -389,7 +389,12 @@ def resolve_typed_alert_target( _text_token(target_resource) == "sentry-self-hosted-snuba-profiling-functions-consumer-1" ) - if not exact_sentry_consumer: + exact_registry_catalog_target = bool( + domain == "host_systemd" + and _text_token(target_resource) + == _text_token(str(identity.get("service_name") or "")) + ) + if not (exact_sentry_consumer or exact_registry_catalog_target): catalog_ids = [] stateful_level = str(identity.get("stateful_level") or "") risk = ( diff --git a/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py b/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py index f66d3ed77..4961741e8 100644 --- a/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py +++ b/apps/api/tests/test_sre_k3s_controlled_automation_work_items_api.py @@ -181,3 +181,35 @@ def test_typed_target_preview_is_no_write_and_fail_closed() -> None: assert agent99_payload["route"]["agent99_bridge"][ "dispatch_allowed" ] is True + + +def test_wazuh_typed_target_preview_is_bounded_and_public_safe() -> None: + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.post( + "/api/v1/agents/controlled-alert-target-preview", + json={ + "alertname": "WazuhSecurityEvent", + "target_resource": "wazuh-manager", + "namespace": "awoooi", + "labels": {"service": "wazuh-manager", "tool": "wazuh"}, + "alert_category": "security", + }, + ) + + assert response.status_code == 200 + payload = response.json() + route = payload["route"] + assert payload["preview_only"] is True + assert payload["dispatch_performed"] is False + assert payload["runtime_closure_verified"] is False + assert route["resolution_status"] == "resolved" + assert route["canonical_asset_id"] == "service:wazuh-manager" + assert route["host"] == "host:kali-readonly" + assert route["allowed_catalog_ids"] == [ + "ansible:wazuh-manager-posture-readback" + ] + assert route["allowed_inventory_hosts"] == ["host_112"] + assert route["cross_domain_fallback_allowed"] is False diff --git a/apps/api/tests/test_sre_typed_domain_router.py b/apps/api/tests/test_sre_typed_domain_router.py index 4234c6361..01ff13681 100644 --- a/apps/api/tests/test_sre_typed_domain_router.py +++ b/apps/api/tests/test_sre_typed_domain_router.py @@ -47,6 +47,7 @@ PLAYBOOK = ( ) SENTRY_CONTAINER = "sentry-self-hosted-snuba-profiling-functions-consumer-1" SENTRY_CATALOG = "ansible:110-sentry-profiling-consumer-recovery" +WAZUH_CATALOG = "ansible:wazuh-manager-posture-readback" def _sentry_incident() -> dict: @@ -110,6 +111,49 @@ def test_sentry_container_resolves_to_exact_host110_domain_route() -> None: assert route["reasoning_contract"]["paid_call_allowed"] is False +def test_wazuh_manager_exact_service_resolves_to_host112_posture_lane() -> None: + registry = ServiceRegistryClient(REGISTRY) + route = resolve_typed_alert_target( + alertname="WazuhSecurityEvent", + target_resource="wazuh-manager", + namespace="awoooi", + labels={"service": "wazuh-manager", "tool": "wazuh"}, + alert_category="security", + registry=registry, + ) + + identity = registry.resolve_identity("wazuh-manager") + assert identity["resolution_status"] == "resolved" + assert identity["canonical_id"] == "service:wazuh-manager" + assert identity["allowed_catalog_ids"] == [WAZUH_CATALOG] + assert route["resolution_status"] == "resolved" + assert route["target_kind"] == "host_systemd" + assert route["canonical_asset_id"] == "service:wazuh-manager" + assert route["executor"] == "host_ansible_executor" + assert route["verifier"] == "iwooos_wazuh_controlled_executor_runtime" + assert route["allowed_catalog_ids"] == [WAZUH_CATALOG] + assert route["allowed_inventory_hosts"] == ["host_112"] + assert route["controlled_apply_allowed"] is True + assert route["critical_break_glass_required"] is False + assert route["cross_domain_fallback_allowed"] is False + + +def test_wazuh_systemd_alias_cannot_inherit_execution_catalog() -> None: + route = resolve_typed_alert_target( + alertname="SystemdUnitUnavailable", + target_resource="wazuh-manager.service", + namespace="awoooi", + labels={}, + alert_category="security", + registry=ServiceRegistryClient(REGISTRY), + ) + + assert route["resolution_status"] == "resolved" + assert route["canonical_asset_id"] == "service:wazuh-manager" + assert route["allowed_catalog_ids"] == [] + assert route["cross_domain_fallback_allowed"] is False + + def test_sentry_incident_cannot_fall_to_host188_or_full_convergence() -> None: hints = _catalog_hints(_sentry_incident(), None) @@ -363,6 +407,48 @@ def test_sentry_candidate_reaches_bounded_check_mode_with_exact_scope() -> None: assert claim["controlled_apply_allowed"] is True +def test_wazuh_incident_reaches_bounded_host112_check_mode() -> None: + class Incident: + incident_id = "INC-WAZUH-TYPED-ROUTE" + project_id = "awoooi" + alertname = "WazuhSecurityEvent" + alert_category = "security" + notification_type = "TYPE-2" + severity = None + affected_services = ["wazuh-manager"] + signals = [ + { + "alert_name": "WazuhSecurityEvent", + "labels": { + "service": "wazuh-manager", + "tool": "wazuh", + "namespace": "awoooi", + }, + } + ] + + payload = build_ansible_decision_audit_payload( + incident=Incident(), + proposal_data={"source": "test", "risk_level": "medium"}, + decision_path="repair_candidate_controlled_queue", + not_used_reason="test", + ) + assert payload is not None + + claim = build_ansible_check_mode_claim_input( + source_candidate_op_id="00000000-0000-0000-0000-000000112112", + candidate_input=payload["input"], + ) + + assert claim["catalog_id"] == WAZUH_CATALOG + assert claim["inventory_hosts"] == ["host_112"] + assert claim["typed_domain"] == "host_systemd" + assert claim["canonical_asset_id"] == "service:wazuh-manager" + assert claim["target_scope_validated"] is True + assert claim["cross_domain_fallback_allowed"] is False + assert claim["controlled_apply_allowed"] is True + + def test_sentry_playbook_and_post_verifier_are_exact_container_bounded() -> None: payload = yaml.safe_load(PLAYBOOK.read_text(encoding="utf-8")) source = PLAYBOOK.read_text(encoding="utf-8") diff --git a/k8s/awoooi-prod/15-service-registry-configmap.yaml b/k8s/awoooi-prod/15-service-registry-configmap.yaml index df9b37c7f..221f1203e 100644 --- a/k8s/awoooi-prod/15-service-registry-configmap.yaml +++ b/k8s/awoooi-prod/15-service-registry-configmap.yaml @@ -9,7 +9,7 @@ metadata: app: awoooi component: service-registry annotations: - awoooi.wooo.work/source-sha256: "1662532ccf9c574e9853291a6f2881377c1a5b2519c4123ae05caf45be904121" + awoooi.wooo.work/source-sha256: "5548d19a6060900d51810cb3b1c3e1dc5f2fbb2b7c77df20227499cdb68599fc" data: service-registry.yaml: | # ops/config/service-registry.yaml @@ -165,6 +165,20 @@ data: containers: ["alertmanager"] # ─── AUTO:無狀態,允許自動修復 ────────────────────────────────────── + - name: wazuh-manager + canonical_id: "service:wazuh-manager" + display_name: "Wazuh Manager (host_112)" + host: "192.168.0.112" + stateful_level: AUTO + asset_domain: host_systemd + reason: "Only the bounded no-write posture catalog is enabled; active response and arbitrary service mutation remain unavailable" + executor: host_ansible_executor + verifier: iwooos_wazuh_controlled_executor_runtime + aliases: ["wazuh-manager.service"] + containers: [] + allowed_catalog_ids: + - ansible:wazuh-manager-posture-readback + - name: nginx display_name: "Nginx (反向代理)" host: "192.168.0.110" diff --git a/ops/config/service-registry.yaml b/ops/config/service-registry.yaml index a40397fed..d38263b38 100644 --- a/ops/config/service-registry.yaml +++ b/ops/config/service-registry.yaml @@ -151,6 +151,20 @@ services: containers: ["alertmanager"] # ─── AUTO:無狀態,允許自動修復 ────────────────────────────────────── + - name: wazuh-manager + canonical_id: "service:wazuh-manager" + display_name: "Wazuh Manager (host_112)" + host: "192.168.0.112" + stateful_level: AUTO + asset_domain: host_systemd + reason: "Only the bounded no-write posture catalog is enabled; active response and arbitrary service mutation remain unavailable" + executor: host_ansible_executor + verifier: iwooos_wazuh_controlled_executor_runtime + aliases: ["wazuh-manager.service"] + containers: [] + allowed_catalog_ids: + - ansible:wazuh-manager-posture-readback + - name: nginx display_name: "Nginx (反向代理)" host: "192.168.0.110"