Skip to content

Commit 872e6e2

Browse files
authored
perf(api): replace JOINs with pre-check in threat score aggregation query (#10394)
1 parent 2fe92cf commit 872e6e2

File tree

3 files changed

+23
-22
lines changed

3 files changed

+23
-22
lines changed

api/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to the **Prowler API** are documented in this file.
44

5+
## [1.22.1] (Prowler v5.21.1)
6+
7+
### 🐞 Fixed
8+
9+
- Threat score aggregation query to eliminate unnecessary JOINs and `COUNT(DISTINCT)` overhead [(#10394)](https://github.com/prowler-cloud/prowler/pull/10394)
10+
11+
---
12+
513
## [1.22.0] (Prowler v5.21.0)
614

715
### 🚀 Added

api/src/backend/tasks/jobs/threatscore_utils.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from api.db_router import READ_REPLICA_ALIAS
66
from api.db_utils import rls_transaction
7-
from api.models import Finding, StatusChoices
7+
from api.models import Finding, Scan, StatusChoices
88
from prowler.lib.outputs.finding import Finding as FindingOutput
99

1010
logger = get_task_logger(__name__)
@@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database(
3535
}
3636
"""
3737
requirement_statistics_by_check_id = {}
38-
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
38+
# TODO: review when finding-resource relation changes from 1:1
3939
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
40+
# Pre-check: skip if the scan's provider is deleted (avoids JOINs in the main query)
41+
if Scan.all_objects.filter(id=scan_id, provider__is_deleted=True).exists():
42+
return requirement_statistics_by_check_id
43+
4044
aggregated_statistics_queryset = (
4145
Finding.all_objects.filter(
4246
tenant_id=tenant_id,
4347
scan_id=scan_id,
4448
muted=False,
45-
resources__provider__is_deleted=False,
4649
)
4750
.values("check_id")
4851
.annotate(
4952
total_findings=Count(
5053
"id",
51-
distinct=True,
5254
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
5355
),
5456
passed_findings=Count(
5557
"id",
56-
distinct=True,
5758
filter=Q(status=StatusChoices.PASS),
5859
),
5960
)

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -169,35 +169,27 @@ def test_mixed_statuses(self, tenants_fixture, scans_fixture):
169169
assert result["check_1"]["passed"] == 1
170170
assert result["check_1"]["total"] == 1
171171

172-
def test_excludes_findings_without_resources(self, tenants_fixture, scans_fixture):
173-
"""Verify findings without resources are excluded from aggregation."""
172+
def test_skips_aggregation_for_deleted_provider(
173+
self, tenants_fixture, scans_fixture
174+
):
175+
"""Verify aggregation returns empty when the scan's provider is soft-deleted."""
174176
tenant = tenants_fixture[0]
175177
scan = scans_fixture[0]
176178

177-
# Finding WITH resource → should be counted
178179
self._create_finding_with_resource(
179180
tenant, scan, "finding-1", "check_1", StatusChoices.PASS
180181
)
181182

182-
# Finding WITHOUT resource → should be EXCLUDED
183-
Finding.objects.create(
184-
tenant_id=tenant.id,
185-
scan=scan,
186-
uid="finding-2",
187-
check_id="check_1",
188-
status=StatusChoices.FAIL,
189-
severity=Severity.high,
190-
impact=Severity.high,
191-
check_metadata={},
192-
raw_result={},
193-
)
183+
# Soft-delete the provider
184+
provider = scan.provider
185+
provider.is_deleted = True
186+
provider.save(update_fields=["is_deleted"])
194187

195188
result = _aggregate_requirement_statistics_from_database(
196189
str(tenant.id), str(scan.id)
197190
)
198191

199-
assert result["check_1"]["passed"] == 1
200-
assert result["check_1"]["total"] == 1
192+
assert result == {}
201193

202194
def test_multiple_resources_no_double_count(self, tenants_fixture, scans_fixture):
203195
"""Verify a finding with multiple resources is only counted once."""

0 commit comments

Comments
 (0)