Skip to content

Commit 40b7cb3

Browse files
authored
fix(api): skip scan tasks when provider was deleted (#11185)
1 parent c1cfda5 commit 40b7cb3

3 files changed

Lines changed: 78 additions & 1 deletion

File tree

api/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ All notable changes to the **Prowler API** are documented in this file.
1313
- Replace `poetry` with `uv` (`0.11.14`) as the API package manager; migrate `pyproject.toml` to `[dependency-groups]` and regenerate as `uv.lock` [(#10775)](https://github.com/prowler-cloud/prowler/pull/10775)
1414
- Remove orphaned `gin_resources_search_idx` declaration from `Resource.Meta.indexes` (DB index dropped in `0072_drop_unused_indexes`) [(#11001)](https://github.com/prowler-cloud/prowler/pull/11001)
1515

16+
### 🐞 Fixed
17+
18+
- `perform_scan_task` and `perform_scheduled_scan_task` now short-circuit with a warning and `return None` when the target provider no longer exists, instead of letting `handle_provider_deletion` raise `ProviderDeletedException`. `perform_scheduled_scan_task` also removes any orphan `PeriodicTask` it finds so beat stops re-firing scans for deleted providers. Prevents queued messages for deleted providers from being recorded as `FAILURE` and, in one-shot scan-worker deployments, from burning a fresh container per redelivery [(#11185)](https://github.com/prowler-cloud/prowler/pull/11185)
19+
1620
---
1721

1822
## [1.27.2] (Prowler UNRELEASED)

api/src/backend/tasks/tasks.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969

7070
from api.compliance import get_compliance_frameworks
7171
from api.db_router import READ_REPLICA_ALIAS
72-
from api.db_utils import rls_transaction
72+
from api.db_utils import delete_related_daily_task, rls_transaction
7373
from api.decorators import handle_provider_deletion, set_tenant
7474
from api.models import Finding, Integration, Provider, Scan, ScanSummary, StateChoices
7575
from api.utils import initialize_prowler_provider
@@ -274,6 +274,17 @@ def perform_scan_task(
274274
Returns:
275275
dict: The result of the scan execution, typically including the status and results of the performed checks.
276276
"""
277+
with rls_transaction(tenant_id):
278+
if not Provider.objects.filter(pk=provider_id).exists():
279+
logger.warning(
280+
"scan-perform skipped: provider %s no longer exists "
281+
"(tenant=%s, scan=%s)",
282+
provider_id,
283+
tenant_id,
284+
scan_id,
285+
)
286+
return None
287+
277288
result = perform_prowler_scan(
278289
tenant_id=tenant_id,
279290
scan_id=scan_id,
@@ -310,6 +321,16 @@ def perform_scheduled_scan_task(self, tenant_id: str, provider_id: str):
310321
task_id = self.request.id
311322

312323
with rls_transaction(tenant_id):
324+
if not Provider.objects.filter(pk=provider_id).exists():
325+
logger.warning(
326+
"scheduled scan-perform skipped: provider %s no longer exists "
327+
"(tenant=%s)",
328+
provider_id,
329+
tenant_id,
330+
)
331+
delete_related_daily_task(provider_id)
332+
return None
333+
313334
periodic_task_instance = PeriodicTask.objects.get(
314335
name=f"scan-perform-scheduled-{provider_id}"
315336
)

api/src/backend/tasks/tests/test_tasks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
check_lighthouse_provider_connection_task,
2222
generate_outputs_task,
2323
perform_attack_paths_scan_task,
24+
perform_scan_task,
2425
perform_scheduled_scan_task,
2526
reaggregate_all_finding_group_summaries_task,
2627
refresh_lighthouse_provider_models_task,
@@ -2454,6 +2455,57 @@ def _complete_scan(tenant_id, scan_id, provider_id):
24542455
== 1
24552456
)
24562457

2458+
def test_no_op_when_provider_does_not_exist(self, tenants_fixture):
2459+
"""Return None without raising when the provider was already deleted."""
2460+
tenant = tenants_fixture[0]
2461+
missing_provider_id = str(uuid.uuid4())
2462+
task_id = str(uuid.uuid4())
2463+
self._create_task_result(tenant.id, task_id)
2464+
# Orphan PeriodicTask left behind from a previous lifecycle.
2465+
self._create_periodic_task(missing_provider_id, tenant.id)
2466+
orphan_name = f"scan-perform-scheduled-{missing_provider_id}"
2467+
assert PeriodicTask.objects.filter(name=orphan_name).exists()
2468+
2469+
with (
2470+
patch("tasks.tasks.perform_prowler_scan") as mock_scan,
2471+
patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks,
2472+
self._override_task_request(perform_scheduled_scan_task, id=task_id),
2473+
):
2474+
result = perform_scheduled_scan_task.run(
2475+
tenant_id=str(tenant.id), provider_id=missing_provider_id
2476+
)
2477+
2478+
assert result is None
2479+
mock_scan.assert_not_called()
2480+
mock_complete_tasks.assert_not_called()
2481+
# Orphan PeriodicTask is cleaned up so beat stops re-firing it.
2482+
assert not PeriodicTask.objects.filter(name=orphan_name).exists()
2483+
2484+
2485+
@pytest.mark.django_db
2486+
class TestPerformScanTask:
2487+
"""Unit tests for perform_scan_task."""
2488+
2489+
def test_no_op_when_provider_does_not_exist(self, tenants_fixture):
2490+
"""Return None without raising when the provider was already deleted."""
2491+
tenant = tenants_fixture[0]
2492+
missing_provider_id = str(uuid.uuid4())
2493+
scan_id = str(uuid.uuid4())
2494+
2495+
with (
2496+
patch("tasks.tasks.perform_prowler_scan") as mock_scan,
2497+
patch("tasks.tasks._perform_scan_complete_tasks") as mock_complete_tasks,
2498+
):
2499+
result = perform_scan_task.run(
2500+
tenant_id=str(tenant.id),
2501+
scan_id=scan_id,
2502+
provider_id=missing_provider_id,
2503+
)
2504+
2505+
assert result is None
2506+
mock_scan.assert_not_called()
2507+
mock_complete_tasks.assert_not_called()
2508+
24572509

24582510
@pytest.mark.django_db
24592511
class TestReaggregateAllFindingGroupSummaries:

0 commit comments

Comments
 (0)