diff --git a/api/src/backend/api/filters.py b/api/src/backend/api/filters.py index d8b803d475..cd52e33c7d 100644 --- a/api/src/backend/api/filters.py +++ b/api/src/backend/api/filters.py @@ -15,6 +15,7 @@ from django_filters.rest_framework import ( from rest_framework_json_api.django_filters.backends import DjangoFilterBackend from rest_framework_json_api.serializers import ValidationError +from api.constants import SEVERITY_ORDER from api.db_utils import ( FindingDeltaEnumField, InvitationStateEnumField, @@ -803,11 +804,17 @@ class FindingGroupFilter(CommonFindingFilters): check_id = CharFilter(field_name="check_id", lookup_expr="exact") check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") + check_title__icontains = CharFilter( + field_name="check_metadata__CheckTitle", lookup_expr="icontains" + ) + scan = UUIDFilter(field_name="scan_id", lookup_expr="exact") + scan__in = UUIDInFilter(field_name="scan_id", lookup_expr="in") class Meta: model = Finding fields = { "check_id": ["exact", "in", "icontains"], + "scan": ["exact", "in"], } def filter_queryset(self, queryset): @@ -895,11 +902,17 @@ class LatestFindingGroupFilter(CommonFindingFilters): check_id = CharFilter(field_name="check_id", lookup_expr="exact") check_id__in = CharInFilter(field_name="check_id", lookup_expr="in") check_id__icontains = CharFilter(field_name="check_id", lookup_expr="icontains") + check_title__icontains = CharFilter( + field_name="check_metadata__CheckTitle", lookup_expr="icontains" + ) + scan = UUIDFilter(field_name="scan_id", lookup_expr="exact") + scan__in = UUIDInFilter(field_name="scan_id", lookup_expr="in") class Meta: model = Finding fields = { "check_id": ["exact", "in", "icontains"], + "scan": ["exact", "in"], } @@ -1048,6 +1061,91 @@ class LatestFindingGroupSummaryFilter(FilterSet): } +class FindingGroupAggregatedComputedFilter(FilterSet): + """Filter aggregated finding-group rows by computed status/severity.""" + + STATUS_CHOICES = ( + ("FAIL", "Fail"), + ("PASS", "Pass"), + ("MUTED", "Muted"), + ) + + status = ChoiceFilter(method="filter_status", choices=STATUS_CHOICES) + status__in = CharInFilter(method="filter_status_in", lookup_expr="in") + severity = ChoiceFilter(method="filter_severity", choices=SeverityChoices) + severity__in = CharInFilter(method="filter_severity_in", lookup_expr="in") + + def filter_status(self, queryset, name, value): + return queryset.filter(aggregated_status=value) + + def filter_status_in(self, queryset, name, value): + values = value + if isinstance(value, str): + values = [part.strip() for part in value.split(",") if part.strip()] + + allowed = {choice[0] for choice in self.STATUS_CHOICES} + invalid = [ + status_value for status_value in values if status_value not in allowed + ] + if invalid: + raise ValidationError( + [ + { + "detail": f"invalid status filter: {invalid[0]}", + "status": "400", + "source": {"pointer": "/data"}, + "code": "invalid", + } + ] + ) + + if not values: + return queryset + + return queryset.filter(aggregated_status__in=values) + + def filter_severity(self, queryset, name, value): + severity_order = SEVERITY_ORDER.get(value) + if severity_order is None: + raise ValidationError( + [ + { + "detail": f"invalid severity filter: {value}", + "status": "400", + "source": {"pointer": "/data"}, + "code": "invalid", + } + ] + ) + return queryset.filter(severity_order=severity_order) + + def filter_severity_in(self, queryset, name, value): + values = value + if isinstance(value, str): + values = [part.strip() for part in value.split(",") if part.strip()] + + orders = [] + for severity_value in values: + severity_order = SEVERITY_ORDER.get(severity_value) + if severity_order is None: + raise ValidationError( + [ + { + "detail": f"invalid severity filter: {severity_value}", + "status": "400", + "source": {"pointer": "/data"}, + "code": "invalid", + } + ] + ) + orders.append(severity_order) + + if not orders: + return queryset + + return queryset.filter(severity_order__in=orders) + + class ProviderSecretFilter(FilterSet): inserted_at = DateFilter( field_name="inserted_at", diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index b2e82d2ea1..ec78028e8f 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -1055,9 +1055,9 @@ class TestProviderViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) def test_providers_retrieve(self, authenticated_client, providers_fixture): provider1, *_ = providers_fixture @@ -4919,13 +4919,13 @@ class TestAttackPathsScanViewSet: content_type=API_JSON_CONTENT_TYPE, ) if i < 10: - assert ( - response.status_code == status.HTTP_200_OK - ), f"Request {i + 1} should succeed with 200 OK, got {response.status_code}" + assert response.status_code == status.HTTP_200_OK, ( + f"Request {i + 1} should succeed with 200 OK, got {response.status_code}" + ) else: - assert ( - response.status_code == status.HTTP_429_TOO_MANY_REQUESTS - ), f"Request {i + 1} should be throttled" + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS, ( + f"Request {i + 1} should be throttled" + ) # -- Timeout simulation ------------------------------------------------------- @@ -5128,9 +5128,9 @@ class TestResourceViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "filter_name, filter_value, expected_count", @@ -5679,9 +5679,9 @@ class TestResourceViewSet: (e for e in errors if e["source"]["parameter"] == expected_invalid_param), None, ) - assert ( - error is not None - ), f"Expected error for parameter '{expected_invalid_param}'" + assert error is not None, ( + f"Expected error for parameter '{expected_invalid_param}'" + ) assert error["code"] == "invalid" assert error["status"] == "400" # Must be string per JSON:API spec assert expected_invalid_param in error["detail"] @@ -6213,16 +6213,16 @@ class TestResourceViewSet: # Test with completely malformed token client.credentials(HTTP_AUTHORIZATION="Bearer not.a.valid.jwt.token") response = client.get(reverse("resource-events", kwargs={"pk": resource.id})) - assert ( - response.status_code == status.HTTP_401_UNAUTHORIZED - ), f"Expected 401 for malformed token but got {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"Expected 401 for malformed token but got {response.status_code}" + ) # Test with empty bearer token client.credentials(HTTP_AUTHORIZATION="Bearer ") response = client.get(reverse("resource-events", kwargs={"pk": resource.id})) - assert ( - response.status_code == status.HTTP_401_UNAUTHORIZED - ), f"Expected 401 for empty bearer token but got {response.status_code}" + assert response.status_code == status.HTTP_401_UNAUTHORIZED, ( + f"Expected 401 for empty bearer token but got {response.status_code}" + ) @pytest.mark.django_db @@ -6283,9 +6283,9 @@ class TestFindingViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "filter_name, filter_value, expected_count", @@ -6824,9 +6824,9 @@ class TestJWTFields: reverse("token-obtain"), data, format="json" ) - assert ( - response.status_code == status.HTTP_200_OK - ), f"Unexpected status code: {response.status_code}" + assert response.status_code == status.HTTP_200_OK, ( + f"Unexpected status code: {response.status_code}" + ) access_token = response.data["attributes"]["access"] payload = jwt.decode(access_token, options={"verify_signature": False}) @@ -6840,23 +6840,23 @@ class TestJWTFields: # Verify expected fields for field in expected_fields: assert field in payload, f"The field '{field}' is not in the JWT" - assert ( - payload[field] == expected_fields[field] - ), f"The value of '{field}' does not match" + assert payload[field] == expected_fields[field], ( + f"The value of '{field}' does not match" + ) # Verify time fields are integers for time_field in ["exp", "iat", "nbf"]: assert time_field in payload, f"The field '{time_field}' is not in the JWT" - assert isinstance( - payload[time_field], int - ), f"The field '{time_field}' is not an integer" + assert isinstance(payload[time_field], int), ( + f"The field '{time_field}' is not an integer" + ) # Verify identification fields are non-empty strings for id_field in ["jti", "sub", "tenant_id"]: assert id_field in payload, f"The field '{id_field}' is not in the JWT" - assert ( - isinstance(payload[id_field], str) and payload[id_field] - ), f"The field '{id_field}' is not a valid string" + assert isinstance(payload[id_field], str) and payload[id_field], ( + f"The field '{id_field}' is not a valid string" + ) @pytest.mark.django_db @@ -10795,9 +10795,9 @@ class TestIntegrationViewSet: included_data = response.json()["included"] for expected_type in expected_resources: - assert any( - d.get("type") == expected_type for d in included_data - ), f"Expected type '{expected_type}' not found in included data" + assert any(d.get("type") == expected_type for d in included_data), ( + f"Expected type '{expected_type}' not found in included data" + ) @pytest.mark.parametrize( "integration_type, configuration, credentials", @@ -12234,9 +12234,9 @@ class TestLighthouseConfigViewSet: ) # Check that API key is masked with asterisks only masked_api_key = data["attributes"]["api_key"] - assert all( - c == "*" for c in masked_api_key - ), "API key should contain only asterisks" + assert all(c == "*" for c in masked_api_key), ( + "API key should contain only asterisks" + ) @pytest.mark.parametrize( "field_name, invalid_value", @@ -15245,6 +15245,85 @@ class TestFindingGroupViewSet: # rds_encryption has all muted findings assert data[0]["attributes"]["status"] == "MUTED" + def test_finding_groups_status_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test finding groups can be filtered by aggregated status.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, "filter[status]": "FAIL"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["status"] == "FAIL" for item in data) + + def test_finding_groups_status_in_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test finding groups support status__in filter on aggregated status.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, "filter[status__in]": "FAIL,PASS"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["status"] in {"FAIL", "PASS"} for item in data) + + def test_finding_groups_severity_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test finding groups can be filtered by aggregated severity.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, "filter[severity]": "critical"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["severity"] == "critical" for item in data) + + @pytest.mark.parametrize( + "filter_name,filter_value", + [ + ("region", "__region_does_not_exist__"), + ("service", "__service_does_not_exist__"), + ("category", "__category_does_not_exist__"), + ("resource_groups", "__group_does_not_exist__"), + ("resource_type", "__type_does_not_exist__"), + ("scan", "00000000-0000-7000-8000-000000000001"), + ], + ) + def test_finding_groups_finding_level_filters_are_applied( + self, + authenticated_client, + finding_groups_fixture, + filter_name, + filter_value, + ): + """Test finding-level filters are applied in /finding-groups aggregation.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, f"filter[{filter_name}]": filter_value}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 0 + + def test_finding_groups_delta_filter_is_applied( + self, authenticated_client, finding_groups_fixture + ): + """Test delta filter is applied in /finding-groups aggregation.""" + response = authenticated_client.get( + reverse("finding-group-list"), + {"filter[inserted_at]": TODAY, "filter[delta]": "new"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["new_count"] > 0 for item in data) + def test_finding_groups_provider_aggregation( self, authenticated_client, finding_groups_fixture ): @@ -15704,12 +15783,12 @@ class TestFindingGroupViewSet: assert response_p1.status_code == status.HTTP_200_OK p1_check_ids = {item["id"] for item in response_p1.json()["data"]} # Provider1 has scan1 with 4 checks - assert ( - len(p1_check_ids) == 4 - ), f"Provider1 should have 4 checks, got {len(p1_check_ids)}" - assert ( - "cloudtrail_enabled" not in p1_check_ids - ), "cloudtrail_enabled should NOT be in provider1" + assert len(p1_check_ids) == 4, ( + f"Provider1 should have 4 checks, got {len(p1_check_ids)}" + ) + assert "cloudtrail_enabled" not in p1_check_ids, ( + "cloudtrail_enabled should NOT be in provider1" + ) # Get finding groups for provider2 only response_p2 = authenticated_client.get( @@ -15719,12 +15798,12 @@ class TestFindingGroupViewSet: assert response_p2.status_code == status.HTTP_200_OK p2_check_ids = {item["id"] for item in response_p2.json()["data"]} # Provider2 has scan2 with 1 check - assert ( - len(p2_check_ids) == 1 - ), f"Provider2 should have 1 check, got {len(p2_check_ids)}" - assert ( - "cloudtrail_enabled" in p2_check_ids - ), "cloudtrail_enabled should be in provider2" + assert len(p2_check_ids) == 1, ( + f"Provider2 should have 1 check, got {len(p2_check_ids)}" + ) + assert "cloudtrail_enabled" in p2_check_ids, ( + "cloudtrail_enabled should be in provider2" + ) # Test provider_type filter actually filters data def test_finding_groups_provider_type_filter_actually_filters( @@ -15747,9 +15826,9 @@ class TestFindingGroupViewSet: {"filter[inserted_at]": TODAY, "filter[provider_type]": "gcp"}, ) assert response_gcp.status_code == status.HTTP_200_OK - assert ( - len(response_gcp.json()["data"]) == 0 - ), "GCP filter should return 0 results" + assert len(response_gcp.json()["data"]) == 0, ( + "GCP filter should return 0 results" + ) def test_finding_groups_pagination( self, authenticated_client, finding_groups_fixture @@ -15853,6 +15932,116 @@ class TestFindingGroupViewSet: assert len(data) == 1 assert data[0]["id"] == "cloudtrail_enabled" + def test_finding_groups_latest_status_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test /latest supports status filter on aggregated status.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {"filter[status]": "FAIL"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["status"] == "FAIL" for item in data) + + def test_finding_groups_latest_status_in_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test /latest supports status__in filter on aggregated status.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {"filter[status__in]": "FAIL,PASS"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["status"] in {"FAIL", "PASS"} for item in data) + + def test_finding_groups_latest_severity_filter( + self, authenticated_client, finding_groups_fixture + ): + """Test /latest supports severity filter on aggregated severity.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {"filter[severity]": "critical"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["severity"] == "critical" for item in data) + + @pytest.mark.parametrize( + "filter_name,filter_value", + [ + ("region", "__region_does_not_exist__"), + ("service", "__service_does_not_exist__"), + ("category", "__category_does_not_exist__"), + ("resource_groups", "__group_does_not_exist__"), + ("resource_type", "__type_does_not_exist__"), + ("scan", "00000000-0000-7000-8000-000000000001"), + ], + ) + def test_finding_groups_latest_finding_level_filters_are_applied( + self, + authenticated_client, + finding_groups_fixture, + filter_name, + filter_value, + ): + """Test finding-level filters are applied in /finding-groups/latest aggregation.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {f"filter[{filter_name}]": filter_value}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 0 + + def test_finding_groups_check_title_filter_applies_with_delta( + self, authenticated_client, finding_groups_fixture + ): + """Test check_title filter is honored when finding-level path is used.""" + response = authenticated_client.get( + reverse("finding-group-list"), + { + "filter[inserted_at]": TODAY, + "filter[delta]": "new", + "filter[check_title.icontains]": "__missing_check_title__", + }, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 0 + + def test_finding_groups_latest_check_title_filter_applies_with_delta( + self, authenticated_client, finding_groups_fixture + ): + """Test /latest check_title filter is honored on finding-level path.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + { + "filter[delta]": "new", + "filter[check_title.icontains]": "__missing_check_title__", + }, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) == 0 + + def test_finding_groups_latest_delta_filter_is_applied( + self, authenticated_client, finding_groups_fixture + ): + """Test delta filter is applied in /finding-groups/latest aggregation.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {"filter[delta]": "new"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + assert all(item["attributes"]["new_count"] > 0 for item in data) + def test_finding_groups_latest_aggregates_latest_per_provider( self, authenticated_client, providers_fixture ): @@ -15934,6 +16123,19 @@ class TestFindingGroupViewSet: check_ids = [item["id"] for item in data] assert check_ids == sorted(check_ids) + def test_finding_groups_latest_sort_by_check_title( + self, authenticated_client, finding_groups_fixture + ): + """Test /latest supports sorting by check_title.""" + response = authenticated_client.get( + reverse("finding-group-latest"), + {"sort": "check_title"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + check_titles = [item["attributes"]["check_title"] for item in data] + assert check_titles == sorted(check_titles) + def test_finding_groups_latest_ignores_date_filters( self, authenticated_client, finding_groups_fixture ): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 85c3062965..20d8fa065b 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -31,6 +31,7 @@ from django.contrib.postgres.search import SearchQuery from django.db import transaction from django.db.models import ( Case, + CharField, Count, DecimalField, ExpressionWrapper, @@ -124,6 +125,7 @@ from api.filters import ( CustomDjangoFilterBackend, DailySeveritySummaryFilter, FindingFilter, + FindingGroupAggregatedComputedFilter, FindingGroupFilter, FindingGroupSummaryFilter, IntegrationFilter, @@ -6872,6 +6874,39 @@ class FindingGroupViewSet(BaseRLSViewSet): "resource_type__icontains": "type__icontains", } + def _get_finding_level_filter_keys(self, latest: bool = False) -> set[str]: + """Derive filters that require finding-level prefiltering.""" + summary_filterset = ( + LatestFindingGroupSummaryFilter if latest else FindingGroupSummaryFilter + ) + finding_filterset = LatestFindingGroupFilter if latest else FindingGroupFilter + + summary_supported = set(summary_filterset.base_filters.keys()) | { + "status", + "status__in", + "severity", + "severity__in", + } + finding_supported = set(finding_filterset.base_filters.keys()) + return finding_supported - summary_supported + + def _requires_finding_level_aggregation( + self, params: QueryDict, latest: bool = False + ) -> bool: + finding_level_keys = self._get_finding_level_filter_keys(latest=latest) + return any(key in finding_level_keys for key in params.keys()) + + def _get_matching_check_ids_from_findings( + self, normalized_params: QueryDict, latest: bool = False + ): + """Return check_ids matching advanced finding-level filters.""" + finding_queryset = self._get_finding_queryset() + filterset_class = LatestFindingGroupFilter if latest else FindingGroupFilter + filterset = filterset_class(normalized_params, queryset=finding_queryset) + if not filterset.is_valid(): + raise ValidationError(filterset.errors) + return filterset.qs.values("check_id").distinct() + def _split_resource_filters(self, params: QueryDict) -> tuple[QueryDict, QueryDict]: resource_keys = set(self.RESOURCE_FILTER_MAP) finding_params = QueryDict(mutable=True) @@ -6998,6 +7033,7 @@ class FindingGroupViewSet(BaseRLSViewSet): """Validate and map JSON:API sort fields for aggregated finding groups.""" sort_field_map = { "check_id": "check_id", + "check_title": "check_title", "severity": "severity_order", "fail_count": "fail_count", "pass_count": "pass_count", @@ -7035,6 +7071,43 @@ class FindingGroupViewSet(BaseRLSViewSet): return ordering + def _apply_aggregated_computed_filters( + self, queryset, normalized_params: QueryDict + ): + """Apply computed filters (status/severity) on aggregated finding-group rows.""" + status_params = QueryDict(mutable=True) + if normalized_params.get("status"): + status_params.setlist("status", [normalized_params.get("status")]) + if normalized_params.getlist("status__in"): + status_params.setlist("status__in", normalized_params.getlist("status__in")) + if normalized_params.get("severity"): + status_params.setlist("severity", [normalized_params.get("severity")]) + if normalized_params.getlist("severity__in"): + status_params.setlist( + "severity__in", normalized_params.getlist("severity__in") + ) + + if not status_params: + return queryset + + if status_params.get("status") or status_params.getlist("status__in"): + queryset = queryset.annotate( + aggregated_status=Case( + When(fail_count__gt=0, then=Value("FAIL")), + When(pass_count__gt=0, then=Value("PASS")), + default=Value("MUTED"), + output_field=CharField(), + ) + ) + + filterset = FindingGroupAggregatedComputedFilter( + status_params, queryset=queryset + ) + if not filterset.is_valid(): + raise ValidationError(filterset.errors) + + return filterset.qs + def _build_resource_mapping_queryset( self, filtered_queryset, resource_ids=None, tenant_id: str | None = None ): @@ -7166,6 +7239,17 @@ class FindingGroupViewSet(BaseRLSViewSet): # Re-aggregate daily summaries across the date range aggregated_queryset = self._aggregate_daily_summaries(filtered_queryset) + aggregated_queryset = self._apply_aggregated_computed_filters( + aggregated_queryset, normalized_params + ) + + if self._requires_finding_level_aggregation(normalized_params, latest=False): + matching_check_ids = self._get_matching_check_ids_from_findings( + normalized_params, latest=False + ) + aggregated_queryset = aggregated_queryset.filter( + check_id__in=Subquery(matching_check_ids) + ) # Apply ordering (respect JSON:API sort param or use default) sort_param = request.query_params.get("sort") @@ -7239,6 +7323,17 @@ class FindingGroupViewSet(BaseRLSViewSet): # Re-aggregate daily summaries aggregated_queryset = self._aggregate_daily_summaries(latest_per_check) + aggregated_queryset = self._apply_aggregated_computed_filters( + aggregated_queryset, normalized_params + ) + + if self._requires_finding_level_aggregation(normalized_params, latest=True): + matching_check_ids = self._get_matching_check_ids_from_findings( + normalized_params, latest=True + ) + aggregated_queryset = aggregated_queryset.filter( + check_id__in=Subquery(matching_check_ids) + ) # Apply ordering sort_param = request.query_params.get("sort")