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 847d0a89e..d759722ec 100644 --- a/apps/api/src/services/awooop_ansible_check_mode_service.py +++ b/apps/api/src/services/awooop_ansible_check_mode_service.py @@ -1102,6 +1102,34 @@ def _telegram_shadow_completion_allowed(claim: AnsibleCheckModeClaim) -> bool: ) +def _backfill_telegram_provider_delivery(claim: AnsibleCheckModeClaim) -> str: + """Allow only the current deploy-scoped Wazuh receipt retry to egress.""" + + build_sha = os.getenv("AWOOOI_BUILD_COMMIT_SHA", "").strip().lower() + source_receipt_ref = str( + claim.input_payload.get("source_receipt_ref") or "" + ).strip() + current_receipt_retry = bool( + len(build_sha) == 40 + and all(character in "0123456789abcdef" for character in build_sha) + and source_receipt_ref.startswith( + "scheduled-wazuh-manager-posture:" + ) + and source_receipt_ref.upper().endswith( + f"-{build_sha[:12].upper()}-RECEIPT" + ) + ) + if ( + claim.catalog_id == _WAZUH_MANAGER_POSTURE_CATALOG_ID + and _claim_is_no_write_observation(claim) + and claim.input_payload.get("canonical_asset_id") + == "service:wazuh-manager" + and current_receipt_retry + ): + return "state_transition" + return "shadow_only" + + def _controlled_apply_allowed(candidate: dict[str, Any]) -> tuple[bool, str | None]: risk = str(candidate.get("risk_level") or "").strip().lower() if settings.ENABLE_AWOOOP_ANSIBLE_CONTROLLED_APPLY is not True: @@ -8164,7 +8192,7 @@ async def backfill_missing_auto_repair_execution_receipts_once( apply_op_id=str(row.get("op_id") or ""), writeback=writeback, project_id=project_id, - provider_delivery="shadow_only", + provider_delivery=_backfill_telegram_provider_delivery(claim), ) if closure.get("telegram_receipt_acknowledged") is True: stats["telegram_receipt_acknowledged"] += 1 diff --git a/apps/api/tests/test_ansible_incident_ledger_churn_guard.py b/apps/api/tests/test_ansible_incident_ledger_churn_guard.py index eab60c8f6..54468f6bc 100644 --- a/apps/api/tests/test_ansible_incident_ledger_churn_guard.py +++ b/apps/api/tests/test_ansible_incident_ledger_churn_guard.py @@ -337,10 +337,40 @@ async def test_post_apply_pending_receipt_does_not_write_learning_or_failure( @pytest.mark.asyncio +@pytest.mark.parametrize( + ("source_receipt_ref", "expected_provider_delivery"), + [ + ( + "scheduled-wazuh-manager-posture:2026071506", + "shadow_only", + ), + ( + ( + "scheduled-wazuh-manager-posture:2026071506-" + "ABCDEF123456-RECEIPT" + ), + "state_transition", + ), + ( + ( + "scheduled-wazuh-manager-posture:2026071506-" + "DEADBEEFCAFE-RECEIPT" + ), + "shadow_only", + ), + ], +) async def test_backfill_repairs_orphan_incident_and_reuses_verifier( monkeypatch: pytest.MonkeyPatch, + source_receipt_ref: str, + expected_provider_delivery: str, ) -> None: claim = _wazuh_claim() + claim.input_payload["source_receipt_ref"] = source_receipt_ref + monkeypatch.setenv( + "AWOOOI_BUILD_COMMIT_SHA", + "abcdef1234567890abcdef1234567890abcdef12", + ) apply_op_id = "00000000-0000-0000-0000-000000000203" receipt = _durable_verifier_receipt(claim, apply_op_id=apply_op_id) row = { @@ -424,15 +454,16 @@ async def test_backfill_repairs_orphan_incident_and_reuses_verifier( "_record_runtime_stage_receipts", AsyncMock(return_value=True), ) + closure = AsyncMock( + return_value={ + "closed": True, + "telegram_receipt_acknowledged": True, + } + ) monkeypatch.setattr( service, "_reconcile_verified_apply_closure_projections", - AsyncMock( - return_value={ - "closed": True, - "telegram_receipt_acknowledged": True, - } - ), + closure, ) result = await service.backfill_missing_auto_repair_execution_receipts_once(limit=1) @@ -443,6 +474,9 @@ async def test_backfill_repairs_orphan_incident_and_reuses_verifier( assert result["incident_closure_written"] == 1 incident_ledger.assert_awaited_once() assert writeback.await_args.kwargs["durable_verifier_receipt"] == receipt + assert closure.await_args.kwargs["provider_delivery"] == ( + expected_provider_delivery + ) @pytest.mark.asyncio