fix(api): align finding-group latest aggregation (#10419)

This commit is contained in:
Adrián Peña
2026-03-23 12:43:45 +01:00
committed by GitHub
parent 93b8a7c74c
commit 591f5a8603
3 changed files with 94 additions and 15 deletions
+5
View File
@@ -4,6 +4,11 @@ All notable changes to the **Prowler API** are documented in this file.
## [1.23.0] (Prowler UNRELEASED)
### 🐞 Fixed
- Finding groups latest endpoint now aggregates the latest snapshot per provider before check-level totals, keeping impacted resources aligned across providers [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419)
- Mute rule creation now triggers finding-group summary re-aggregation after historical muting, keeping stats in sync after mute operations [(#10419)](https://github.com/prowler-cloud/prowler/pull/10419)
### 🔐 Security
- Replace stdlib XML parser with `defusedxml` in SAML metadata parsing to prevent XML bomb (billion laughs) DoS attacks [(#10165)](https://github.com/prowler-cloud/prowler/pull/10165)
+59 -4
View File
@@ -45,6 +45,7 @@ from api.models import (
ComplianceRequirementOverview,
DailySeveritySummary,
Finding,
FindingGroupDailySummary,
Integration,
Invitation,
LighthouseProviderConfiguration,
@@ -14689,10 +14690,16 @@ class TestMuteRuleViewSet:
assert len(data) == 2
assert data[0]["id"] == str(mute_rules_fixture[first_index].id)
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
@patch("api.v1.views.chain")
@patch("api.v1.views.aggregate_finding_group_summaries_task.si")
@patch("api.v1.views.mute_historical_findings_task.si")
@patch("api.v1.views.transaction.on_commit", side_effect=lambda fn: fn())
def test_mute_rules_create_valid(
self,
mock_task,
_mock_on_commit,
mock_mute_signature,
mock_aggregate_signature,
mock_chain,
authenticated_client,
findings_fixture,
create_test_user,
@@ -14730,8 +14737,14 @@ class TestMuteRuleViewSet:
assert finding.muted_at is not None
assert finding.muted_reason == "Security exception approved"
# Verify background task was called
mock_task.assert_called_once()
# Verify background task chain was called
mock_mute_signature.assert_called_once()
mock_aggregate_signature.assert_called_once()
mock_chain.assert_called_once_with(
mock_mute_signature.return_value,
mock_aggregate_signature.return_value,
)
mock_chain.return_value.apply_async.assert_called_once()
@patch("tasks.tasks.mute_historical_findings_task.apply_async")
def test_mute_rules_create_converts_finding_ids_to_uids(
@@ -15840,6 +15853,48 @@ class TestFindingGroupViewSet:
assert len(data) == 1
assert data[0]["id"] == "cloudtrail_enabled"
def test_finding_groups_latest_aggregates_latest_per_provider(
self, authenticated_client, providers_fixture
):
"""Test /latest aggregates latest summary from each provider for the same check."""
provider1 = providers_fixture[0]
provider2 = providers_fixture[1]
check_id = "cross_provider_latest_resources_total"
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
FindingGroupDailySummary.objects.create(
tenant_id=provider1.tenant_id,
provider=provider1,
check_id=check_id,
inserted_at=now - timedelta(days=1),
resources_total=20,
resources_fail=20,
fail_count=20,
)
FindingGroupDailySummary.objects.create(
tenant_id=provider2.tenant_id,
provider=provider2,
check_id=check_id,
inserted_at=now,
resources_total=7,
resources_fail=7,
fail_count=7,
)
response = authenticated_client.get(
reverse("finding-group-latest"),
{"filter[check_id]": check_id},
)
assert response.status_code == status.HTTP_200_OK
data = response.json()["data"]
assert len(data) == 1
attrs = data[0]["attributes"]
assert attrs["resources_total"] == 27
assert attrs["resources_fail"] == 27
assert attrs["fail_count"] == 27
def test_finding_groups_latest_provider_type_filter(
self, authenticated_client, finding_groups_fixture
):
+30 -11
View File
@@ -16,6 +16,7 @@ from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.saml.views import FinishACSView, LoginView
from botocore.exceptions import ClientError, NoCredentialsError, ParamValidationError
from celery import chain
from celery.result import AsyncResult
from config.custom_logging import BackendLogger
from config.env import env
@@ -81,6 +82,7 @@ from tasks.beat import schedule_provider_scan
from tasks.jobs.attack_paths import db_utils as attack_paths_db_utils
from tasks.jobs.export import get_s3_client
from tasks.tasks import (
aggregate_finding_group_summaries_task,
backfill_compliance_summaries_task,
backfill_scan_resource_summaries_task,
check_integration_connection_task,
@@ -6725,10 +6727,25 @@ class MuteRuleViewSet(BaseRLSViewSet):
)
# Launch background task for historical muting
with transaction.atomic():
mute_historical_findings_task.apply_async(
kwargs={"tenant_id": tenant_id, "mute_rule_id": str(mute_rule.id)}
)
latest_scan_id = (
Scan.objects.filter(tenant_id=tenant_id, state=StateChoices.COMPLETED)
.order_by("-completed_at", "-inserted_at")
.values_list("id", flat=True)
.first()
)
transaction.on_commit(
lambda: chain(
mute_historical_findings_task.si(
tenant_id=tenant_id,
mute_rule_id=str(mute_rule.id),
),
aggregate_finding_group_summaries_task.si(
tenant_id=tenant_id,
scan_id=str(latest_scan_id),
),
).apply_async()
)
# Return the created mute rule
serializer = self.get_serializer(mute_rule)
@@ -7210,13 +7227,15 @@ class FindingGroupViewSet(BaseRLSViewSet):
raise ValidationError(filterset.errors)
filtered_queryset = filterset.qs
# Keep only rows from the latest inserted_at date per check_id
latest_per_check = filtered_queryset.annotate(
latest_inserted_at=Window(
expression=Max("inserted_at"),
partition_by=[F("check_id")],
)
).filter(inserted_at=F("latest_inserted_at"))
# Keep only the latest row per (check_id, provider), then aggregate by check_id.
latest_per_check_ids = (
filtered_queryset.order_by("check_id", "provider_id", "-inserted_at")
.distinct("check_id", "provider_id")
.values("id")
)
latest_per_check = filtered_queryset.filter(
id__in=Subquery(latest_per_check_ids)
)
# Re-aggregate daily summaries
aggregated_queryset = self._aggregate_daily_summaries(latest_per_check)