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 3d376f3e2..f51580162 100644 --- a/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py +++ b/apps/api/src/jobs/awooop_ansible_candidate_backfill_job.py @@ -527,6 +527,7 @@ async def enqueue_iwooos_wazuh_manager_posture_candidate_once( proposal_data={ "source": "iwooos_wazuh_manager_posture_scheduler", "risk_level": "low", + "execution_priority": 0, "action": "run_bounded_no_write_wazuh_manager_posture_readback", }, decision_path=_BACKFILL_DECISION_PATH, diff --git a/apps/api/src/services/awooop_ansible_audit_service.py b/apps/api/src/services/awooop_ansible_audit_service.py index 7346f7c29..daaf1d8f3 100644 --- a/apps/api/src/services/awooop_ansible_audit_service.py +++ b/apps/api/src/services/awooop_ansible_audit_service.py @@ -586,6 +586,10 @@ def build_ansible_decision_audit_payload( and str((signal.get("labels") or {}).get("namespace") or "").strip() }) proposal_risk_level = str(proposal_data.get("risk_level") or "").strip().lower() + execution_priority = proposal_data.get("execution_priority", 100) + if not isinstance(execution_priority, int) or isinstance(execution_priority, bool): + execution_priority = 100 + execution_priority = max(0, min(execution_priority, 100)) idempotency_key = f"ansible-candidate:{project_id}:{incident_id}" input_payload = { "incident_id": incident_id, @@ -602,6 +606,7 @@ def build_ansible_decision_audit_payload( "executor": "ansible", "execution_backend": "ansible", "single_writer_executor": "awoooi-ansible-executor-broker", + "execution_priority": execution_priority, "decision_path": decision_path, "idempotency_key": idempotency_key, "router_source_sha": os.getenv("AWOOOI_BUILD_COMMIT_SHA", "") 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 9460c8f89..ff318a906 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -3185,7 +3185,19 @@ async def claim_pending_check_modes( 'ansible_execution_skipped' ) ) - ORDER BY candidate.created_at ASC + ORDER BY + CASE + WHEN candidate.input ->> 'work_item_id' LIKE 'P0-%' + THEN 0 + WHEN candidate.input ->> 'execution_priority' + ~ '^[0-9]{1,3}$' + THEN LEAST( + (candidate.input ->> 'execution_priority')::int, + 100 + ) + ELSE 100 + END ASC, + candidate.created_at ASC LIMIT :limit FOR UPDATE SKIP LOCKED """), diff --git a/apps/api/src/services/awooop_ansible_post_verifier.py b/apps/api/src/services/awooop_ansible_post_verifier.py index 2d74a0d10..13bb5a944 100644 --- a/apps/api/src/services/awooop_ansible_post_verifier.py +++ b/apps/api/src/services/awooop_ansible_post_verifier.py @@ -46,10 +46,10 @@ ProbeRunner = Callable[..., Awaitable[ReadOnlyProbeResult]] _POSTCONDITION_REGISTRY: dict[str, tuple[AssetPostcondition, ...]] = { "ansible:wazuh-manager-posture-readback": ( AssetPostcondition( - "host_112_wazuh_manager_control_binary", + "host_112_wazuh_manager_unit_loaded", "security", "host_112", - "test -f /var/ossec/bin/wazuh-control", + "systemctl show --property=LoadState --value wazuh-manager.service | grep -Fxq loaded", ), AssetPostcondition( "host_112_wazuh_manager_runtime", diff --git a/apps/api/tests/test_awooop_truth_chain_service.py b/apps/api/tests/test_awooop_truth_chain_service.py index 960992f6a..eb3637e69 100644 --- a/apps/api/tests/test_awooop_truth_chain_service.py +++ b/apps/api/tests/test_awooop_truth_chain_service.py @@ -1346,6 +1346,7 @@ def test_ansible_decision_audit_payload_is_dry_run_only() -> None: assert payload["input"]["check_mode"] is True assert payload["input"]["apply_enabled"] is False assert payload["input"]["approval_required"] is True + assert payload["input"]["execution_priority"] == 100 assert payload["input"]["executor_candidates"] assert payload["output"]["decision_effect"] == "audit_only" assert payload["dry_run_result"]["check_mode_executed"] is False @@ -1373,6 +1374,7 @@ def test_ansible_decision_audit_payload_marks_repair_candidate_queue_claimable() proposal_data={ "source": "repair_candidate_controlled_queue", "risk_level": "medium", + "execution_priority": 0, "action": "systemctl restart node-exporter-188", }, decision_path="repair_candidate_controlled_queue", @@ -1389,6 +1391,7 @@ def test_ansible_decision_audit_payload_marks_repair_candidate_queue_claimable() assert payload["input"]["executor_candidates"] assert payload["dry_run_result"]["check_mode_executed"] is False assert payload["input"]["project_id"] == "awoooi" + assert payload["input"]["execution_priority"] == 0 def test_ansible_decision_audit_payload_exposes_check_mode_safety_flags( @@ -2701,6 +2704,16 @@ def test_ansible_claim_query_limits_recent_candidate_backlog() -> None: assert "AWOOOP_ANSIBLE_CHECK_MODE_CANDIDATE_MAX_AGE_HOURS" in source +def test_ansible_claim_query_honors_bounded_execution_priority_before_fifo() -> None: + source = inspect.getsource(claim_pending_check_modes) + + assert "candidate.input ->> 'work_item_id' LIKE 'P0-%'" in source + assert "candidate.input ->> 'execution_priority'" in source + assert "LEAST(" in source + assert "100" in source + assert source.index("execution_priority") < source.index("candidate.created_at ASC") + + def test_ansible_transport_blocker_detects_repair_forced_command_denial() -> None: blockers = detect_ansible_transport_blockers( "fatal: host unreachable REPAIR_DENIED:invalid_command", 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 e97fa0cba..9094f0a11 100644 --- a/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py +++ b/apps/api/tests/test_iwooos_wazuh_controlled_executor_runtime.py @@ -7,6 +7,7 @@ from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest +import yaml from fastapi import FastAPI from fastapi.testclient import TestClient @@ -158,14 +159,14 @@ def test_wazuh_posture_catalog_and_playbook_are_bounded_no_write() -> None: "ansible:wazuh-manager-posture-readback" ) assert {condition.condition_id for condition in postconditions} == { - "host_112_wazuh_manager_control_binary", + "host_112_wazuh_manager_unit_loaded", "host_112_wazuh_manager_runtime", } assert {condition.inventory_host for condition in postconditions} == { "host_112" } assert {condition.probe for condition in postconditions} == { - "test -f /var/ossec/bin/wazuh-control", + "systemctl show --property=LoadState --value wazuh-manager.service | grep -Fxq loaded", "systemctl is-active --quiet wazuh-manager.service", } @@ -187,6 +188,25 @@ def test_wazuh_posture_catalog_and_playbook_are_bounded_no_write() -> None: ): assert forbidden not in source + inventory = yaml.safe_load( + (repo_root / "infra/ansible/inventory/hosts.yml").read_text() + ) + host_112 = inventory["all"]["children"]["security"]["hosts"]["host_112"] + assert host_112["ansible_user"] == "kali" + assert "ansible_skip_tags" not in host_112 + + service_registry = yaml.safe_load( + (repo_root / "ops/monitoring/service-registry.yaml").read_text() + ) + wazuh_manager = next( + node + for node in service_registry["nodes"] + if node.get("name") == "wazuh-manager" + ) + assert wazuh_manager["ip"] == "192.168.0.112" + assert wazuh_manager["port"] == 55000 + assert wazuh_manager["criticality"] == "P0" + @pytest.mark.asyncio async def test_wazuh_posture_scheduler_enqueues_after_mcp_and_log_evidence( @@ -247,6 +267,7 @@ async def test_wazuh_posture_scheduler_enqueues_after_mcp_and_log_evidence( assert recorded["incident"]["run_id"] == result["automation_run_id"] assert recorded["incident"]["work_item_id"] == ("P0-03-WAZUH-MANAGER-POSTURE") assert recorded["incident"]["asset_scope_aliases"] == ["security_manager_primary"] + assert recorded["proposal_data"]["execution_priority"] == 0 assert recorded["incident"]["incident_id"] == "IWZ-POSTURE-2026071100" assert ( recorded["incident"]["source_receipt_ref"] diff --git a/infra/ansible/inventory/hosts.yml b/infra/ansible/inventory/hosts.yml index 3b27b3131..0e8e65a78 100644 --- a/infra/ansible/inventory/hosts.yml +++ b/infra/ansible/inventory/hosts.yml @@ -43,6 +43,5 @@ all: hosts: host_112: ansible_host: 192.168.0.112 - ansible_user: wooo + ansible_user: kali ansible_ssh_private_key_file: "~/.ssh/id_ed25519" - ansible_skip_tags: "all" # 預留,本次不執行 diff --git a/infra/ansible/playbooks/wazuh-manager-posture-readback.yml b/infra/ansible/playbooks/wazuh-manager-posture-readback.yml index 6c01af070..c8c27253a 100644 --- a/infra/ansible/playbooks/wazuh-manager-posture-readback.yml +++ b/infra/ansible/playbooks/wazuh-manager-posture-readback.yml @@ -7,20 +7,19 @@ any_errors_fatal: true tasks: - - name: Verify the Wazuh manager control binary is present - ansible.builtin.stat: - path: /var/ossec/bin/wazuh-control - register: wazuh_control_binary - changed_when: false - - - name: Require a local Wazuh manager installation - ansible.builtin.assert: - that: - - wazuh_control_binary.stat.exists - - wazuh_control_binary.stat.isreg - fail_msg: wazuh_manager_control_binary_missing - success_msg: wazuh_manager_control_binary_present + - name: Verify the Wazuh manager unit is loaded + ansible.builtin.command: + argv: + - /usr/bin/systemctl + - show + - --property=LoadState + - --value + - wazuh-manager.service + register: wazuh_manager_load_state + check_mode: false changed_when: false + failed_when: wazuh_manager_load_state.stdout | trim != 'loaded' + no_log: true - name: Verify the Wazuh manager service is active ansible.builtin.command: diff --git a/k8s/awoooi-prod/06-deployment-api.yaml b/k8s/awoooi-prod/06-deployment-api.yaml index c7a203bbd..1657bcc05 100644 --- a/k8s/awoooi-prod/06-deployment-api.yaml +++ b/k8s/awoooi-prod/06-deployment-api.yaml @@ -154,12 +154,12 @@ spec: - name: AWOOOI_BUILD_COMMIT_SHA # 2026-06-29 Codex: CD rewrites this to the deployed image tag so # production deploy readback does not rely on a stale static snapshot. - value: "69e55718123c99d4aa7275b1150daf3fe8a92bd7" + value: "ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c" - name: AWOOOI_DESIRED_API_IMAGE_TAG # 2026-06-30 Codex: CD rewrites this alongside AWOOOI_BUILD_COMMIT_SHA. # Production readback compares runtime image truth against this # GitOps desired tag instead of doing a slow Gitea raw fetch. - value: "69e55718123c99d4aa7275b1150daf3fe8a92bd7" + value: "ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c" - name: DATABASE_POOL_SIZE # 2026-07-01 Codex: production role `awoooi` currently has a low # connection limit. Keep API pool conservative until DB role diff --git a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml index 804465e10..22ada4f95 100644 --- a/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml +++ b/k8s/awoooi-prod/08-deployment-ansible-executor-broker.yaml @@ -66,7 +66,7 @@ spec: - name: DATABASE_NULL_POOL value: "true" - name: AWOOOI_BUILD_COMMIT_SHA - value: "69e55718123c99d4aa7275b1150daf3fe8a92bd7" + value: "ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c" - name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER value: "false" - name: ENABLE_AWOOOP_ANSIBLE_CHECK_MODE_WORKER diff --git a/k8s/awoooi-prod/08-deployment-worker.yaml b/k8s/awoooi-prod/08-deployment-worker.yaml index 1c2cdb7d9..060fb73ca 100644 --- a/k8s/awoooi-prod/08-deployment-worker.yaml +++ b/k8s/awoooi-prod/08-deployment-worker.yaml @@ -168,7 +168,7 @@ spec: - name: DATABASE_NULL_POOL value: "true" - name: AWOOOI_BUILD_COMMIT_SHA - value: "69e55718123c99d4aa7275b1150daf3fe8a92bd7" + value: "ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c" - name: ENABLE_AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_WORKER value: "true" - name: AWOOOP_ANSIBLE_CANDIDATE_BACKFILL_INTERVAL_SECONDS diff --git a/k8s/awoooi-prod/kustomization.yaml b/k8s/awoooi-prod/kustomization.yaml index b4aa714a2..db02e7c53 100644 --- a/k8s/awoooi-prod/kustomization.yaml +++ b/k8s/awoooi-prod/kustomization.yaml @@ -42,7 +42,7 @@ resources: images: - name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/api - newTag: 69e55718123c99d4aa7275b1150daf3fe8a92bd7 + newTag: ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c - name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER newName: 192.168.0.110:5000/awoooi/web - newTag: 69e55718123c99d4aa7275b1150daf3fe8a92bd7 + newTag: ffe1218d8c1ea3e0969892a6a63fb891d3f8bb7c diff --git a/ops/monitoring/service-registry.yaml b/ops/monitoring/service-registry.yaml index e566ab8c8..10bafca81 100644 --- a/ops/monitoring/service-registry.yaml +++ b/ops/monitoring/service-registry.yaml @@ -498,6 +498,19 @@ nodes: - service_down owner: security-team + - name: wazuh-manager + ip: 192.168.0.112 + port: 55000 + role: security-manager + monitoring: + blackbox_tcp: true + prometheus_scrape: false + alerts: + - node_down + - service_down + criticality: P0 + owner: security-team + # Phase O-1.3 2026-04-02: MinIO 備份儲存 (Phase O 補完) - name: minio ip: 192.168.0.188