Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## [1.22.1] (Prowler v5.21.1)

### 🐞 Fixed

- Threat score aggregation query to eliminate unnecessary JOINs and `COUNT(DISTINCT)` overhead [(#10394)](https://github.com/prowler-cloud/prowler/pull/10394)

---

## [1.22.0] (Prowler v5.21.0)

### 🚀 Added
Expand Down
11 changes: 6 additions & 5 deletions api/src/backend/tasks/jobs/threatscore_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from api.db_router import READ_REPLICA_ALIAS
from api.db_utils import rls_transaction
from api.models import Finding, StatusChoices
from api.models import Finding, Scan, StatusChoices
from prowler.lib.outputs.finding import Finding as FindingOutput

logger = get_task_logger(__name__)
Expand Down Expand Up @@ -35,25 +35,26 @@ def _aggregate_requirement_statistics_from_database(
}
"""
requirement_statistics_by_check_id = {}
# TODO: take into account that now the relation is 1 finding == 1 resource, review this when the logic changes
# TODO: review when finding-resource relation changes from 1:1
with rls_transaction(tenant_id, using=READ_REPLICA_ALIAS):
# Pre-check: skip if the scan's provider is deleted (avoids JOINs in the main query)
if Scan.all_objects.filter(id=scan_id, provider__is_deleted=True).exists():
return requirement_statistics_by_check_id

aggregated_statistics_queryset = (
Finding.all_objects.filter(
tenant_id=tenant_id,
scan_id=scan_id,
muted=False,
resources__provider__is_deleted=False,
)
.values("check_id")
.annotate(
total_findings=Count(
"id",
distinct=True,
filter=Q(status__in=[StatusChoices.PASS, StatusChoices.FAIL]),
),
passed_findings=Count(
"id",
distinct=True,
filter=Q(status=StatusChoices.PASS),
),
)
Expand Down
26 changes: 9 additions & 17 deletions api/src/backend/tasks/tests/test_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,35 +169,27 @@ def test_mixed_statuses(self, tenants_fixture, scans_fixture):
assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1

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

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

# Finding WITHOUT resource → should be EXCLUDED
Finding.objects.create(
tenant_id=tenant.id,
scan=scan,
uid="finding-2",
check_id="check_1",
status=StatusChoices.FAIL,
severity=Severity.high,
impact=Severity.high,
check_metadata={},
raw_result={},
)
# Soft-delete the provider
provider = scan.provider
provider.is_deleted = True
provider.save(update_fields=["is_deleted"])

result = _aggregate_requirement_statistics_from_database(
str(tenant.id), str(scan.id)
)

assert result["check_1"]["passed"] == 1
assert result["check_1"]["total"] == 1
assert result == {}

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