mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 04:51:51 +00:00
fix(api): align finding-group latest aggregation (#10419)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user