feat: add status filter to /overviews endpoint (#8186)

Co-authored-by: Adrián Jesús Peña Rodríguez <adrianjpr@gmail.com>
This commit is contained in:
Pablo Lara
2025-09-04 18:46:14 +02:00
committed by GitHub
parent daa2536005
commit 12ada66978
9 changed files with 152 additions and 17 deletions
+1
View File
@@ -6,6 +6,7 @@ All notable changes to the **Prowler API** are documented in this file.
### Added
- Integration with JIRA, enabling sending findings to a JIRA project [(#8622)](https://github.com/prowler-cloud/prowler/pull/8622), [(#8637)](https://github.com/prowler-cloud/prowler/pull/8637)
- `GET /overviews/findings_severity` now supports `filter[status]` and `filter[status__in]` to aggregate by specific statuses (`FAIL`, `PASS`)[(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
---
+68 -1
View File
@@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta, timezone
from dateutil.parser import parse
from django.conf import settings
from django.db.models import Q
from django.db.models import F, Q
from django_filters.rest_framework import (
BaseInFilter,
BooleanFilter,
@@ -28,6 +28,7 @@ from api.models import (
Integration,
Invitation,
Membership,
OverviewStatusChoices,
PermissionChoices,
Processor,
Provider,
@@ -750,6 +751,72 @@ class ScanSummaryFilter(FilterSet):
}
class ScanSummarySeverityFilter(ScanSummaryFilter):
"""Filter for findings_severity ScanSummary endpoint - includes status filters"""
# Custom status filters - only for severity grouping endpoint
status = ChoiceFilter(method="filter_status", choices=OverviewStatusChoices.choices)
status__in = CharInFilter(method="filter_status_in", lookup_expr="in")
def filter_status(self, queryset, name, value):
# Validate the status value
if value not in [choice[0] for choice in OverviewStatusChoices.choices]:
raise ValidationError(f"Invalid status value: {value}")
# Apply the filter by annotating the queryset with the status field
if value == OverviewStatusChoices.FAIL:
return queryset.annotate(status_count=F("fail"))
elif value == OverviewStatusChoices.PASS:
return queryset.annotate(status_count=F("_pass"))
else:
return queryset.annotate(status_count=F("total"))
def filter_status_in(self, queryset, name, value):
# Validate the status values
valid_statuses = [choice[0] for choice in OverviewStatusChoices.choices]
for status_val in value:
if status_val not in valid_statuses:
raise ValidationError(f"Invalid status value: {status_val}")
# If all statuses or no valid statuses, use total
if (
set(value)
>= {
OverviewStatusChoices.FAIL,
OverviewStatusChoices.PASS,
}
or not value
):
return queryset.annotate(status_count=F("total"))
# Build the sum expression based on status values
sum_expression = None
for status in value:
if status == OverviewStatusChoices.FAIL:
field_expr = F("fail")
elif status == OverviewStatusChoices.PASS:
field_expr = F("_pass")
else:
continue
if sum_expression is None:
sum_expression = field_expr
else:
sum_expression = sum_expression + field_expr
if sum_expression is None:
return queryset.annotate(status_count=F("total"))
return queryset.annotate(status_count=sum_expression)
class Meta:
model = ScanSummary
fields = {
"inserted_at": ["date", "gte", "lte"],
"region": ["exact", "icontains", "in"],
}
class ServiceOverviewFilter(ScanSummaryFilter):
def is_valid(self):
# Check if at least one of the inserted_at filters is present
+9
View File
@@ -74,6 +74,15 @@ class StatusChoices(models.TextChoices):
MANUAL = "MANUAL", _("Manual")
class OverviewStatusChoices(models.TextChoices):
"""
Status filters allowed in overview/severity endpoints.
"""
FAIL = "FAIL", _("Fail")
PASS = "PASS", _("Pass")
class StateChoices(models.TextChoices):
AVAILABLE = "available", _("Available")
SCHEDULED = "scheduled", _("Scheduled")
+19
View File
@@ -3506,6 +3506,25 @@ paths:
description: A search term.
schema:
type: string
- in: query
name: filter[status]
schema:
type: string
enum:
- FAIL
- PASS
description: |-
* `FAIL` - Fail
* `PASS` - Pass
- in: query
name: filter[status__in]
schema:
type: array
items:
type: string
description: Multiple values may be separated by commas.
explode: false
style: form
- name: sort
required: false
in: query
+20 -6
View File
@@ -89,6 +89,7 @@ from api.filters import (
RoleFilter,
ScanFilter,
ScanSummaryFilter,
ScanSummarySeverityFilter,
ServiceOverviewFilter,
TaskFilter,
TenantFilter,
@@ -3546,8 +3547,10 @@ class OverviewViewSet(BaseRLSViewSet):
def get_filterset_class(self):
if self.action == "providers":
return None
elif self.action in ["findings", "findings_severity"]:
elif self.action == "findings":
return ScanSummaryFilter
elif self.action == "findings_severity":
return ScanSummarySeverityFilter
elif self.action == "services":
return ServiceOverviewFilter
return None
@@ -3669,7 +3672,12 @@ class OverviewViewSet(BaseRLSViewSet):
@action(detail=False, methods=["get"], url_name="findings_severity")
def findings_severity(self, request):
tenant_id = self.request.tenant_id
queryset = self.get_queryset()
# Load only required fields
queryset = self.get_queryset().only(
"tenant_id", "scan_id", "severity", "fail", "_pass", "total"
)
filtered_queryset = self.filter_queryset(queryset)
provider_filter = (
{"provider__in": self.allowed_providers}
@@ -3689,16 +3697,22 @@ class OverviewViewSet(BaseRLSViewSet):
tenant_id=tenant_id, scan_id__in=latest_scan_ids
)
# The filter will have added a status_count annotation if any status filter was used
if "status_count" in filtered_queryset.query.annotations:
sum_expression = Sum("status_count")
else:
sum_expression = Sum("total")
severity_counts = (
filtered_queryset.values("severity")
.annotate(count=Sum("total"))
.annotate(count=sum_expression)
.order_by("severity")
)
severity_data = {sev[0]: 0 for sev in SeverityChoices}
for item in severity_counts:
severity_data[item["severity"]] = item["count"]
severity_data.update(
{item["severity"]: item["count"] for item in severity_counts}
)
serializer = self.get_serializer(severity_data)
return Response(serializer.data, status=status.HTTP_200_OK)
+4
View File
@@ -8,6 +8,10 @@ All notable changes to the **Prowler UI** are documented in this file.
- Jira integration[(#8640)](https://github.com/prowler-cloud/prowler/pull/8640)
### 🔄 Changed
- Overview chart "Findings by Severity" now shows only failing findings (defaults to `status=FAIL`) and chart links open the Findings page pre-filtered to fails per severity [(#8186)](https://github.com/prowler-cloud/prowler/pull/8186)
## [1.11.1] (Prowler v5.11.1)
### 🐞 Added
+14 -3
View File
@@ -56,7 +56,13 @@ export const getFindingsByStatus = async ({
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && key !== "filter[muted]") {
// The overviews/findings endpoint does not support status or muted filters
// (allowed filters include date, region, provider fields). Exclude unsupported ones.
if (
key !== "filter[search]" &&
key !== "filter[muted]" &&
key !== "filter[status]"
) {
url.searchParams.append(key, String(value));
}
});
@@ -89,9 +95,14 @@ export const getFindingsBySeverity = async ({
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
// Handle multiple filters, but exclude muted filter as overviews endpoint doesn't support it
// Handle multiple filters, but exclude unsupported filters
// The overviews/findings_severity endpoint does not support status or muted filters
Object.entries(filters).forEach(([key, value]) => {
if (key !== "filter[search]" && key !== "filter[muted]") {
if (
key !== "filter[search]" &&
key !== "filter[muted]" &&
key !== "filter[status]"
) {
url.searchParams.append(key, String(value));
}
});
+12 -2
View File
@@ -107,6 +107,10 @@ const SSRFindingsBySeverity = async ({
}: {
searchParams: SearchParamsProps | undefined | null;
}) => {
const defaultFilters = {
"filter[status]": "FAIL",
} as const;
const filters = searchParams
? Object.fromEntries(
Object.entries(searchParams).filter(([key]) =>
@@ -115,11 +119,17 @@ const SSRFindingsBySeverity = async ({
)
: {};
const findingsBySeverity = await getFindingsBySeverity({ filters });
const combinedFilters = { ...defaultFilters, ...filters };
const findingsBySeverity = await getFindingsBySeverity({
filters: combinedFilters,
});
return (
<>
<h3 className="mb-4 text-sm font-bold uppercase">Findings by Severity</h3>
<h3 className="mb-4 text-sm font-bold uppercase">
Failed Findings by Severity
</h3>
<FindingsBySeverityChart findingsBySeverity={findingsBySeverity} />
</>
);
@@ -24,27 +24,27 @@ const chartConfig = {
critical: {
label: "Critical",
color: "hsl(var(--chart-critical))",
link: "/findings?filter%5Bseverity__in%5D=critical",
link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=critical",
},
high: {
label: "High",
color: "hsl(var(--chart-fail))",
link: "/findings?filter%5Bseverity__in%5D=high",
link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=high",
},
medium: {
label: "Medium",
color: "hsl(var(--chart-medium))",
link: "/findings?filter%5Bseverity__in%5D=medium",
link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=medium",
},
low: {
label: "Low",
color: "hsl(var(--chart-low))",
link: "/findings?filter%5Bseverity__in%5D=low",
link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=low",
},
informational: {
label: "Informational",
color: "hsl(var(--chart-informational))",
link: "/findings?filter%5Bseverity__in%5D=informational",
link: "/findings?filter%5Bstatus__in%5D=FAIL&filter%5Bseverity__in%5D=informational",
},
} satisfies ChartConfig;