From 1c21c7acc892889a1710ca4e426ed551fabe0c77 Mon Sep 17 00:00:00 2001 From: ogt Date: Wed, 15 Jul 2026 15:12:15 +0800 Subject: [PATCH] fix(security): mount canonical registry in worker --- .../awooop_ansible_candidate_backfill_job.py | 42 ++++++++-- .../test_wazuh_alertmanager_integration.py | 84 +++++++++++++++++++ k8s/awoooi-prod/08-deployment-worker.yaml | 9 ++ 3 files changed, 127 insertions(+), 8 deletions(-) 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 0745852e2..dc835a6d1 100644 --- a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py +++ b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py @@ -842,14 +842,34 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( ], } + proposal_data = { + "source": _scheduler_source, + "risk_level": _risk_level, + "execution_priority": 0, + "action": _proposal_action, + } + if build_ansible_decision_audit_payload( + incident=incident, + proposal_data=proposal_data, + decision_path=_BACKFILL_DECISION_PATH, + not_used_reason=_reason, + ) is None: + return { + "status": "blocked_canonical_asset_or_ansible_catalog_unresolved", + "queued": 0, + "existing": 1 if existing_row else 0, + "evidence_ready": 1, + "automation_run_id": automation_run_id, + "work_item_id": _work_item_id, + "retry_reason": retry_reason, + "active_blockers": [ + "canonical_asset_or_ansible_catalog_unresolved" + ], + } + queued = await recorder( incident=incident, - proposal_data={ - "source": _scheduler_source, - "risk_level": _risk_level, - "execution_priority": 0, - "action": _proposal_action, - }, + proposal_data=proposal_data, decision_path=_BACKFILL_DECISION_PATH, not_used_reason=_reason, automation_run_id=automation_run_id, @@ -863,9 +883,11 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( else "queued_for_controlled_executor" if queued else "candidate_race_deduplicated" + if existing_row + else "candidate_write_not_acknowledged" ), "queued": 1 if queued else 0, - "existing": 0 if queued else 1, + "existing": 1 if existing_row else 0, "evidence_ready": 1 if evidence_ready else 0, "automation_run_id": automation_run_id, "work_item_id": _work_item_id, @@ -878,7 +900,11 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( "controlled_apply_failed", "post_apply_learning_incomplete", }, - "active_blockers": [] if queued else ["candidate_write_not_acknowledged"], + "active_blockers": ( + [] + if queued or existing_row + else ["candidate_write_not_acknowledged"] + ), } except Exception as exc: logger.warning( diff --git a/apps/api/tests/test_wazuh_alertmanager_integration.py b/apps/api/tests/test_wazuh_alertmanager_integration.py index f8c235b45..c1cef7f58 100644 --- a/apps/api/tests/test_wazuh_alertmanager_integration.py +++ b/apps/api/tests/test_wazuh_alertmanager_integration.py @@ -48,6 +48,7 @@ PLAYBOOK = ( BROKER_DEPLOYMENT = ( ROOT / "k8s" / "awoooi-prod" / "08-deployment-ansible-executor-broker.yaml" ) +WORKER_DEPLOYMENT = ROOT / "k8s" / "awoooi-prod" / "08-deployment-worker.yaml" CATALOG_ID = "ansible:wazuh-alertmanager-integration" @@ -325,6 +326,89 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat "scheduled-wazuh-alert-ingress:2026071500" ) + write_not_acknowledged = ( + await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once( + recorder=AsyncMock(return_value=False), + evidence_collector=AsyncMock( + return_value=MagicMock(snapshot_id="wazuh-ingress") + ), + evidence_verifier=AsyncMock(return_value=True), + ) + ) + assert write_not_acknowledged["status"] == "candidate_write_not_acknowledged" + assert write_not_acknowledged["existing"] == 0 + assert write_not_acknowledged["active_blockers"] == [ + "candidate_write_not_acknowledged" + ] + + +@pytest.mark.asyncio +async def test_wazuh_ingress_scheduler_reports_unresolved_payload_before_recording( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from src.jobs import awooop_ansible_candidate_backfill_job as job + + class _Mappings: + def first(self): + return None + + class _Result: + def mappings(self): + return _Mappings() + + class _Db: + async def execute(self, _statement, _params): + return _Result() + + @asynccontextmanager + async def fake_db_context(_project_id: str): + yield _Db() + + recorder = AsyncMock(return_value=False) + monkeypatch.setattr(job, "get_db_context", fake_db_context) + monkeypatch.setattr( + job.settings, + "ENABLE_IWOOOS_WAZUH_MANAGER_POSTURE_EXECUTOR", + True, + ) + monkeypatch.setattr( + job, + "build_ansible_decision_audit_payload", + MagicMock(return_value=None), + ) + + result = await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once( + recorder=recorder, + evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-ingress")), + evidence_verifier=AsyncMock(return_value=True), + ) + + assert result["status"] == "blocked_canonical_asset_or_ansible_catalog_unresolved" + assert result["existing"] == 0 + assert result["active_blockers"] == [ + "canonical_asset_or_ansible_catalog_unresolved" + ] + recorder.assert_not_awaited() + + +def test_wazuh_ingress_scheduler_worker_mounts_canonical_service_registry() -> None: + deployment = yaml.safe_load(WORKER_DEPLOYMENT.read_text(encoding="utf-8")) + pod_spec = deployment["spec"]["template"]["spec"] + container = pod_spec["containers"][0] + + mount = next( + item for item in container["volumeMounts"] if item["name"] == "service-registry" + ) + volume = next(item for item in pod_spec["volumes"] if item["name"] == "service-registry") + + assert mount == { + "name": "service-registry", + "mountPath": "/app/ops/config/service-registry.yaml", + "subPath": "service-registry.yaml", + "readOnly": True, + } + assert volume["configMap"]["name"] == "service-registry" + def test_wazuh_security_event_uses_secops_lifecycle() -> None: assert classify_alert_early( diff --git a/k8s/awoooi-prod/08-deployment-worker.yaml b/k8s/awoooi-prod/08-deployment-worker.yaml index 29cb5bb34..167ea6767 100644 --- a/k8s/awoooi-prod/08-deployment-worker.yaml +++ b/k8s/awoooi-prod/08-deployment-worker.yaml @@ -220,6 +220,11 @@ spec: value: "180" - name: AWOOOP_ANSIBLE_CHECK_MODE_STARTUP_SLEEP_SECONDS value: "30" + volumeMounts: + - name: service-registry + mountPath: /app/ops/config/service-registry.yaml + subPath: service-registry.yaml + readOnly: true resources: requests: cpu: "100m" @@ -272,3 +277,7 @@ spec: matchLabels: app: awoooi-worker topologyKey: kubernetes.io/hostname + volumes: + - name: service-registry + configMap: + name: service-registry