fix(wazuh): require current same-run recurrence

This commit is contained in:
ogt
2026-07-15 16:40:46 +08:00
parent 6dc148ad2f
commit 9c5b85f9bb
6 changed files with 751 additions and 24 deletions

View File

@@ -24,25 +24,61 @@ from src.services.iwooos_wazuh_controlled_executor_runtime import (
)
def _current_wazuh_source_recurrence(observed_at: str) -> dict[str, object]:
return {
"schema_version": "alert_source_recurrence_receipt_v1",
"verified": True,
"fingerprint": "wazuh-posture-source-fingerprint",
"occurrence_id": "wazuh-posture-source-occurrence",
"observed_at": observed_at,
"identity_anchor": "awooop_conversation_event:wazuh-posture",
"canonical_asset_id": "service:wazuh-manager",
"source": "awooop_conversation_event",
}
def _complete_row() -> dict[str, object]:
run_id = "00000000-0000-0000-0000-000000000301"
recurrence = _current_wazuh_source_recurrence(
"2026-07-15T05:59:58+08:00"
)
return {
"candidate_op_id": run_id,
"candidate_status": "dry_run",
"automation_run_id": run_id,
"trace_id": run_id,
"run_id": run_id,
"candidate_project_id": "awoooi",
"work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"proposal_source": "iwooos_wazuh_manager_posture_scheduler",
"run_namespace": "awoooi:iwooos:wazuh-manager-posture",
"source_receipt_ref": "scheduled-wazuh-manager-posture:2026071500",
"candidate_source_recurrence": recurrence,
"candidate_source_recurrence_verified": True,
"asset_scope_alias_count": 1,
"check_op_id": "00000000-0000-0000-0000-000000000302",
"check_status": "success",
"check_returncode": "0",
"check_automation_run_id": run_id,
"check_trace_id": run_id,
"check_run_id": run_id,
"check_work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"check_source_recurrence": recurrence,
"apply_op_id": "00000000-0000-0000-0000-000000000303",
"apply_status": "success",
"apply_returncode": "0",
"apply_automation_run_id": run_id,
"apply_trace_id": run_id,
"apply_run_id": run_id,
"apply_work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"apply_source_recurrence": recurrence,
"verifier_id": "verifier-303",
"verification_result": "success",
"verifier_run_id": run_id,
"verifier_trace_id": run_id,
"verifier_identity_run_id": run_id,
"verifier_work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"verifier_source_recurrence": recurrence,
"km_entry_id": "km-303",
"km_status": "review",
"playbook_trust_acknowledged": True,
@@ -62,6 +98,7 @@ def _complete_row() -> dict[str, object]:
"playbook_trust",
"timeline_projection",
],
"runtime_stage_identity_verified": True,
}
@@ -96,6 +133,11 @@ def test_wazuh_runtime_readback_prefers_ingress_convergence_semantics() -> None:
row = _complete_row()
row["catalog_id"] = "ansible:wazuh-alertmanager-integration"
row["work_item_id"] = "P0-03-WAZUH-ALERT-INGRESS"
row["proposal_source"] = "iwooos_wazuh_alert_ingress_scheduler"
row["run_namespace"] = "awoooi:iwooos:wazuh-alert-ingress"
row["check_work_item_id"] = "P0-03-WAZUH-ALERT-INGRESS"
row["apply_work_item_id"] = "P0-03-WAZUH-ALERT-INGRESS"
row["verifier_work_item_id"] = "P0-03-WAZUH-ALERT-INGRESS"
payload = build_iwooos_wazuh_controlled_executor_runtime_readback(
row,
@@ -140,6 +182,23 @@ def test_wazuh_runtime_readback_keeps_partial_execution_open() -> None:
assert "same_run_mcp_context_missing" in payload["active_blockers"]
def test_wazuh_runtime_readback_rejects_mismatched_identity_chain() -> None:
row = _complete_row()
row["verifier_work_item_id"] = "other-lane"
payload = build_iwooos_wazuh_controlled_executor_runtime_readback(
row,
executor_enabled=True,
)
assert payload["summary"]["runtime_closed_count"] == 0
assert payload["trace"]["identity_chain_verified"] is False
assert "same_run_identity_chain_not_verified" in payload["active_blockers"]
assert payload["next_safe_action"] == (
"repair_same_run_identity_chain_before_closure"
)
def test_wazuh_runtime_readback_separates_probe_success_from_writeback_failure() -> None:
row = _complete_row()
row["apply_status"] = "failed"
@@ -172,10 +231,26 @@ def test_wazuh_runtime_readback_classifies_failed_check_for_bounded_retry() -> N
"automation_run_id": "00000000-0000-0000-0000-000000000311",
"trace_id": "00000000-0000-0000-0000-000000000311",
"run_id": "00000000-0000-0000-0000-000000000311",
"candidate_project_id": "awoooi",
"work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"proposal_source": "iwooos_wazuh_manager_posture_scheduler",
"run_namespace": "awoooi:iwooos:wazuh-manager-posture",
"source_receipt_ref": "scheduled-wazuh-manager-posture:2026071500",
"candidate_source_recurrence": _current_wazuh_source_recurrence(
"2026-07-15T05:59:58+08:00"
),
"candidate_source_recurrence_verified": True,
"check_op_id": "00000000-0000-0000-0000-000000000312",
"check_status": "failed",
"check_returncode": "4",
"check_timed_out": "false",
"check_automation_run_id": "00000000-0000-0000-0000-000000000311",
"check_trace_id": "00000000-0000-0000-0000-000000000311",
"check_run_id": "00000000-0000-0000-0000-000000000311",
"check_work_item_id": "P0-03-WAZUH-MANAGER-POSTURE",
"check_source_recurrence": _current_wazuh_source_recurrence(
"2026-07-15T05:59:58+08:00"
),
"stage_ids": [],
}
@@ -327,6 +402,9 @@ async def test_wazuh_posture_scheduler_enqueues_after_mcp_and_log_evidence(
recorder=recorder,
evidence_collector=collector,
evidence_verifier=verifier,
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor"
@@ -405,6 +483,9 @@ async def test_wazuh_posture_scheduler_retries_failed_check_once_per_deploy(
recorder=recorder,
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-2")),
evidence_verifier=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor_retry"
@@ -481,6 +562,9 @@ async def test_wazuh_posture_scheduler_queues_one_context_retry_per_deploy(
recorder=recorder,
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-3")),
evidence_verifier=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor_context_retry"
@@ -502,6 +586,9 @@ async def test_wazuh_posture_scheduler_queues_one_context_retry_per_deploy(
duplicate = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=duplicate_collector,
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert duplicate["status"] == "context_retry_already_attempted_for_deploy"
@@ -576,6 +663,9 @@ async def test_wazuh_posture_scheduler_retries_incomplete_learning_once_per_depl
recorder=recorder,
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-4")),
evidence_verifier=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor_retry"
@@ -598,6 +688,9 @@ async def test_wazuh_posture_scheduler_retries_incomplete_learning_once_per_depl
duplicate = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=duplicate_collector,
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-11T05:59:58+08:00"
),
)
assert duplicate["status"] == (
@@ -656,6 +749,9 @@ async def test_wazuh_posture_scheduler_does_not_duplicate_active_candidate(
result = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
source_recurrence=_current_wazuh_source_recurrence(
datetime.now(UTC).isoformat()
),
)
assert result["status"] == "fresh_candidate_already_present"
@@ -708,6 +804,9 @@ async def test_wazuh_posture_scheduler_does_not_retry_pending_candidate_without_
result = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=collector,
source_recurrence=_current_wazuh_source_recurrence(
datetime.now(UTC).isoformat()
),
)
assert result["status"] == (
@@ -774,6 +873,9 @@ async def test_wazuh_posture_scheduler_limits_failed_retry_to_once_per_deploy(
result = await job.enqueue_iwooos_wazuh_manager_posture_candidate_once(
recorder=recorder,
evidence_collector=collector,
source_recurrence=_current_wazuh_source_recurrence(
datetime.now(UTC).isoformat()
),
)
assert result["status"] == (
@@ -827,6 +929,9 @@ async def test_wazuh_posture_scheduler_fails_closed_with_degraded_context(
recorder=recorder,
evidence_collector=AsyncMock(return_value=None),
evidence_verifier=AsyncMock(return_value=False),
source_recurrence=_current_wazuh_source_recurrence(
datetime.now(UTC).isoformat()
),
)
assert result["status"] == "blocked_predecision_evidence_not_verified"

View File

@@ -17,11 +17,14 @@ os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/
from src.services.awooop_ansible_audit_service import (
_catalog_hints,
build_ansible_decision_audit_payload,
get_ansible_catalog_item,
preflight_ansible_candidate_generation,
)
from src.services.awooop_ansible_post_verifier import postconditions_for_catalog
from src.services.awooop_ansible_check_mode_service import (
build_ansible_apply_command,
build_ansible_check_mode_claim_input,
build_ansible_check_mode_command,
)
from src.core.config import settings
@@ -52,6 +55,21 @@ WORKER_DEPLOYMENT = ROOT / "k8s" / "awoooi-prod" / "08-deployment-worker.yaml"
CATALOG_ID = "ansible:wazuh-alertmanager-integration"
def _current_wazuh_source_recurrence(
observed_at: str | None = None,
) -> dict[str, object]:
return {
"schema_version": "alert_source_recurrence_receipt_v1",
"verified": True,
"fingerprint": "wazuh-source-fingerprint-5710",
"occurrence_id": "wazuh-source-occurrence-5710",
"observed_at": observed_at or datetime.now(UTC).isoformat(),
"identity_anchor": "awooop_conversation_event:wazuh-5710",
"canonical_asset_id": "service:wazuh-manager",
"source": "awooop_conversation_event",
}
def _load_bridge_module():
spec = importlib.util.spec_from_file_location("wazuh_awoooi_bridge", SCRIPT)
assert spec is not None and spec.loader is not None
@@ -284,10 +302,37 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat
return _Mappings()
class _Db:
async def execute(self, _statement, params):
async def execute(self, statement, params):
assert json.loads(params["catalog_match"]) == [
{"catalog_id": CATALOG_ID}
]
assert params["project_id"] == "awoooi"
assert params["work_item_id"] == "P0-03-WAZUH-ALERT-INGRESS"
assert params["proposal_source"] == (
"iwooos_wazuh_alert_ingress_scheduler"
)
assert params["run_namespace"] == (
"awoooi:iwooos:wazuh-alert-ingress"
)
assert params["source_fingerprint"] == (
"wazuh-source-fingerprint-5710"
)
assert params["source_occurrence_id"] == (
"wazuh-source-occurrence-5710"
)
query = str(statement)
assert "candidate.input ->> 'project_id' = :project_id" in query
assert "candidate.input ->> 'work_item_id' = :work_item_id" in query
assert "candidate.input ->> 'proposal_source' = :proposal_source" in query
assert "candidate.input ->> 'run_namespace' = :run_namespace" in query
assert (
"candidate.input #>> '{source_recurrence,fingerprint}' = :source_fingerprint"
in query
)
assert (
"candidate.input #>> '{source_recurrence,occurrence_id}' = :source_occurrence_id"
in query
)
return _Result()
@asynccontextmanager
@@ -318,6 +363,9 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-ingress")),
evidence_verifier=AsyncMock(return_value=True),
incident_binder=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-15T05:59:58+08:00"
),
)
assert result["status"] == "queued_for_controlled_executor"
@@ -333,6 +381,49 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat
assert recorded["incident"]["source_receipt_ref"] == (
"scheduled-wazuh-alert-ingress:2026071500"
)
candidate = build_ansible_decision_audit_payload(
incident=recorded["incident"],
proposal_data=recorded["proposal_data"],
decision_path="repair_candidate_controlled_queue",
not_used_reason="test",
)
assert candidate is not None
candidate["input"]["automation_run_id"] = result["automation_run_id"]
claim = build_ansible_check_mode_claim_input(
source_candidate_op_id=result["automation_run_id"],
candidate_input=candidate["input"],
observed_now=datetime.fromisoformat("2026-07-15T05:59:59+08:00"),
)
assert claim["project_id"] == "awoooi"
assert claim["trace_id"] == claim["run_id"] == result["automation_run_id"]
assert claim["work_item_id"] == "P0-03-WAZUH-ALERT-INGRESS"
assert claim["run_namespace"] == "awoooi:iwooos:wazuh-alert-ingress"
assert claim["source_recurrence"]["verified"] is True
mismatched_candidate = {**candidate["input"], "work_item_id": "other-lane"}
with pytest.raises(
ValueError,
match="scheduled_candidate_identity_chain_not_verified",
):
build_ansible_check_mode_claim_input(
source_candidate_op_id=result["automation_run_id"],
candidate_input=mismatched_candidate,
observed_now=datetime.fromisoformat("2026-07-15T05:59:59+08:00"),
)
stale_candidate = {
**candidate["input"],
"source_recurrence": _current_wazuh_source_recurrence(
"2026-07-15T05:30:00+08:00"
),
}
with pytest.raises(
ValueError,
match="scheduled_candidate_identity_chain_not_verified",
):
build_ansible_check_mode_claim_input(
source_candidate_op_id=result["automation_run_id"],
candidate_input=stale_candidate,
observed_now=datetime.fromisoformat("2026-07-15T05:59:59+08:00"),
)
write_not_acknowledged = (
await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once(
@@ -342,6 +433,9 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat
),
evidence_verifier=AsyncMock(return_value=True),
incident_binder=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(
"2026-07-15T05:59:58+08:00"
),
)
)
assert write_not_acknowledged["status"] == "candidate_write_not_acknowledged"
@@ -351,6 +445,96 @@ async def test_wazuh_ingress_scheduler_records_one_high_risk_controlled_candidat
]
@pytest.mark.asyncio
async def test_wazuh_ingress_scheduler_fails_closed_without_current_recurrence(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from src.jobs import awooop_ansible_candidate_backfill_job as job
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.settings,
"ENABLE_IWOOOS_WAZUH_MANAGER_POSTURE_EXECUTOR",
True,
)
result = await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once(
recorder=recorder,
evidence_collector=collector,
evidence_verifier=verifier,
incident_binder=binder,
)
assert result == {
"status": "blocked_current_source_recurrence_not_verified",
"queued": 0,
"existing": 0,
"evidence_ready": 0,
"work_item_id": "P0-03-WAZUH-ALERT-INGRESS",
"drift_work_item_id": "DRIFT-WAZUH-SOURCE-RECURRENCE",
"controlled_apply_allowed": False,
"active_blockers": ["current_source_recurrence_not_verified"],
}
recorder.assert_not_awaited()
collector.assert_not_awaited()
verifier.assert_not_awaited()
binder.assert_not_awaited()
unrelated_recurrence = {
**_current_wazuh_source_recurrence(),
"canonical_asset_id": "service:sentry",
}
unrelated = (
await job.enqueue_iwooos_wazuh_alertmanager_integration_candidate_once(
recorder=recorder,
evidence_collector=collector,
evidence_verifier=verifier,
incident_binder=binder,
source_recurrence=unrelated_recurrence,
)
)
assert unrelated["status"] == (
"blocked_current_source_recurrence_not_verified"
)
assert unrelated["controlled_apply_allowed"] is False
@pytest.mark.asyncio
async def test_wazuh_scheduler_audit_preflight_requires_source_recurrence() -> None:
run_id = "00000000-0000-0000-0000-000000000571"
incident = {
"incident_id": "IWZ-INGRESS-2026071500",
"project_id": "awoooi",
"severity": "high",
"alertname": "WazuhAlertmanagerIntegrationConvergence",
"alert_category": "security",
"affected_services": ["wazuh-manager", "siem"],
"trace_id": run_id,
"run_id": run_id,
"work_item_id": "P0-03-WAZUH-ALERT-INGRESS",
"run_namespace": "awoooi:iwooos:wazuh-alert-ingress",
"signals": [{"labels": {"service": "wazuh-manager"}}],
}
result = await preflight_ansible_candidate_generation(
incident=incident,
proposal_data={
"source": "iwooos_wazuh_alert_ingress_scheduler",
"risk_level": "high",
"action": "converge_wazuh_alertmanager_integration",
},
decision_path="repair_candidate_controlled_queue",
not_used_reason="test",
project_id="awoooi",
)
assert result["status"] == "current_source_recurrence_not_verified"
assert result["should_collect_evidence"] is False
@pytest.mark.asyncio
async def test_wazuh_ingress_scheduler_reports_unresolved_payload_before_recording(
monkeypatch: pytest.MonkeyPatch,
@@ -391,6 +575,7 @@ async def test_wazuh_ingress_scheduler_reports_unresolved_payload_before_recordi
evidence_collector=AsyncMock(return_value=MagicMock(snapshot_id="wazuh-ingress")),
evidence_verifier=AsyncMock(return_value=True),
incident_binder=AsyncMock(return_value=True),
source_recurrence=_current_wazuh_source_recurrence(),
)
assert result["status"] == "blocked_canonical_asset_or_ansible_catalog_unresolved"
@@ -437,6 +622,7 @@ async def test_wazuh_ingress_scheduler_blocks_orphan_candidate(
evidence_collector=collector,
evidence_verifier=AsyncMock(return_value=True),
incident_binder=AsyncMock(return_value=False),
source_recurrence=_current_wazuh_source_recurrence(),
)
assert result["status"] == "blocked_scheduled_incident_write_not_acknowledged"