mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-22 03:08:23 +00:00
Compare commits
1 Commits
9ae35029dc
...
feat/api-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc17123272 |
@@ -117,7 +117,8 @@ repos:
|
||||
name: safety
|
||||
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
|
||||
# TODO: Botocore needs urllib3 1.X so we need to ignore these vulnerabilities 77744,77745. Remove this once we upgrade to urllib3 2.X
|
||||
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745'
|
||||
# TODO: xmltodict 0.14.2 has vulnerability 79408, upgrade to >=0.15.1 to fix
|
||||
entry: bash -c 'safety check --ignore 70612,66963,74429,76352,76353,77744,77745,79408'
|
||||
language: system
|
||||
|
||||
- id: vulture
|
||||
|
||||
@@ -6,6 +6,7 @@ All notable changes to the **Prowler API** are documented in this file.
|
||||
|
||||
### Added
|
||||
- New endpoint to retrieve an overview of the attack surfaces [(#9309)](https://github.com/prowler-cloud/prowler/pull/9309)
|
||||
- New endpoint `GET /api/v1/overviews/providers/severity` to retrieve severity breakdown of failed findings grouped by provider type
|
||||
|
||||
### Changed
|
||||
- Restore the compliance overview endpoint's mandatory filters [(#9330)](https://github.com/prowler-cloud/prowler/pull/9330)
|
||||
|
||||
@@ -6857,6 +6857,192 @@ class TestOverviewViewSet:
|
||||
assert combined_attributes["medium"] == 4
|
||||
assert combined_attributes["critical"] == 3
|
||||
|
||||
def test_overview_providers_severity(
|
||||
self, authenticated_client, tenants_fixture, providers_fixture
|
||||
):
|
||||
"""Test providers/severity endpoint returns fail counts grouped by provider type and severity."""
|
||||
tenant = tenants_fixture[0]
|
||||
provider_aws1, provider_aws2, provider_gcp, *_ = providers_fixture
|
||||
|
||||
# Create scans for different providers
|
||||
scan_aws1 = Scan.objects.create(
|
||||
name="aws-scan-one",
|
||||
provider=provider_aws1,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
scan_aws2 = Scan.objects.create(
|
||||
name="aws-scan-two",
|
||||
provider=provider_aws2,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
scan_gcp = Scan.objects.create(
|
||||
name="gcp-scan",
|
||||
provider=provider_gcp,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
# AWS provider 1: high=5 fail, medium=3 fail
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_aws1,
|
||||
check_id="aws-check-one",
|
||||
service="s3",
|
||||
severity="high",
|
||||
region="us-east-1",
|
||||
_pass=2,
|
||||
fail=5,
|
||||
muted=1,
|
||||
total=8,
|
||||
)
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_aws1,
|
||||
check_id="aws-check-two",
|
||||
service="ec2",
|
||||
severity="medium",
|
||||
region="us-east-1",
|
||||
_pass=1,
|
||||
fail=3,
|
||||
muted=0,
|
||||
total=4,
|
||||
)
|
||||
|
||||
# AWS provider 2: high=2 fail, critical=1 fail
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_aws2,
|
||||
check_id="aws-check-three",
|
||||
service="iam",
|
||||
severity="high",
|
||||
region="us-west-2",
|
||||
_pass=0,
|
||||
fail=2,
|
||||
muted=0,
|
||||
total=2,
|
||||
)
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_aws2,
|
||||
check_id="aws-check-four",
|
||||
service="iam",
|
||||
severity="critical",
|
||||
region="us-west-2",
|
||||
_pass=0,
|
||||
fail=1,
|
||||
muted=0,
|
||||
total=1,
|
||||
)
|
||||
|
||||
# GCP provider: medium=4 fail
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_gcp,
|
||||
check_id="gcp-check-one",
|
||||
service="compute",
|
||||
severity="medium",
|
||||
region="us-central1",
|
||||
_pass=2,
|
||||
fail=4,
|
||||
muted=0,
|
||||
total=6,
|
||||
)
|
||||
|
||||
# Test without filters - should return all providers aggregated
|
||||
response = authenticated_client.get(reverse("overview-providers-severity"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
|
||||
# Should have 2 provider types: aws and gcp
|
||||
assert len(data) == 2
|
||||
|
||||
# Find AWS and GCP in response
|
||||
aws_data = next((d for d in data if d["id"] == "aws"), None)
|
||||
gcp_data = next((d for d in data if d["id"] == "gcp"), None)
|
||||
|
||||
assert aws_data is not None
|
||||
assert gcp_data is not None
|
||||
|
||||
# AWS: critical=1, high=5+2=7, medium=3
|
||||
assert aws_data["attributes"]["critical"] == 1
|
||||
assert aws_data["attributes"]["high"] == 7
|
||||
assert aws_data["attributes"]["medium"] == 3
|
||||
assert aws_data["attributes"]["low"] == 0
|
||||
assert aws_data["attributes"]["informational"] == 0
|
||||
|
||||
# GCP: medium=4
|
||||
assert gcp_data["attributes"]["critical"] == 0
|
||||
assert gcp_data["attributes"]["high"] == 0
|
||||
assert gcp_data["attributes"]["medium"] == 4
|
||||
assert gcp_data["attributes"]["low"] == 0
|
||||
assert gcp_data["attributes"]["informational"] == 0
|
||||
|
||||
def test_overview_providers_severity_with_provider_filter(
|
||||
self, authenticated_client, tenants_fixture, providers_fixture
|
||||
):
|
||||
"""Test providers/severity endpoint with provider_id filter."""
|
||||
tenant = tenants_fixture[0]
|
||||
provider_aws1, provider_aws2, provider_gcp, *_ = providers_fixture
|
||||
|
||||
scan_aws1 = Scan.objects.create(
|
||||
name="aws-scan-one",
|
||||
provider=provider_aws1,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
scan_gcp = Scan.objects.create(
|
||||
name="gcp-scan",
|
||||
provider=provider_gcp,
|
||||
trigger=Scan.TriggerChoices.MANUAL,
|
||||
state=StateChoices.COMPLETED,
|
||||
tenant=tenant,
|
||||
)
|
||||
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_aws1,
|
||||
check_id="aws-check",
|
||||
service="s3",
|
||||
severity="high",
|
||||
region="us-east-1",
|
||||
_pass=0,
|
||||
fail=10,
|
||||
muted=0,
|
||||
total=10,
|
||||
)
|
||||
ScanSummary.objects.create(
|
||||
tenant=tenant,
|
||||
scan=scan_gcp,
|
||||
check_id="gcp-check",
|
||||
service="compute",
|
||||
severity="medium",
|
||||
region="us-central1",
|
||||
_pass=0,
|
||||
fail=5,
|
||||
muted=0,
|
||||
total=5,
|
||||
)
|
||||
|
||||
# Filter by AWS provider only
|
||||
response = authenticated_client.get(
|
||||
reverse("overview-providers-severity"),
|
||||
{"filter[provider_id__in]": str(provider_aws1.id)},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()["data"]
|
||||
|
||||
# Should only return AWS
|
||||
assert len(data) == 1
|
||||
assert data[0]["id"] == "aws"
|
||||
assert data[0]["attributes"]["high"] == 10
|
||||
assert data[0]["attributes"]["medium"] == 0
|
||||
|
||||
def test_overview_attack_surface_no_data(self, authenticated_client):
|
||||
response = authenticated_client.get(reverse("overview-attack-surface"))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@@ -2204,6 +2204,20 @@ class OverviewSeveritySerializer(BaseSerializerV1):
|
||||
resource_name = "findings-severity-overview"
|
||||
|
||||
|
||||
class OverviewProviderSeveritySerializer(BaseSerializerV1):
|
||||
"""Serializer for severity breakdown of failed findings grouped by provider type."""
|
||||
|
||||
id = serializers.CharField(source="provider_type")
|
||||
critical = serializers.IntegerField()
|
||||
high = serializers.IntegerField()
|
||||
medium = serializers.IntegerField()
|
||||
low = serializers.IntegerField()
|
||||
informational = serializers.IntegerField()
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-severity-overview"
|
||||
|
||||
|
||||
class OverviewServiceSerializer(BaseSerializerV1):
|
||||
id = serializers.CharField(source="service")
|
||||
total = serializers.IntegerField()
|
||||
|
||||
@@ -206,6 +206,7 @@ from api.v1.serializers import (
|
||||
OverviewFindingSerializer,
|
||||
OverviewProviderCountSerializer,
|
||||
OverviewProviderSerializer,
|
||||
OverviewProviderSeveritySerializer,
|
||||
OverviewRegionSerializer,
|
||||
OverviewServiceSerializer,
|
||||
OverviewSeveritySerializer,
|
||||
@@ -3856,6 +3857,16 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin):
|
||||
"This endpoint counts every provider in the tenant, including those without completed scans."
|
||||
),
|
||||
),
|
||||
providers_severity=extend_schema(
|
||||
summary="Get severity breakdown by provider type",
|
||||
description=(
|
||||
"Retrieve the count of failed findings grouped by provider type and severity level. "
|
||||
"The response includes the number of critical, high, medium, low, and informational "
|
||||
"failed findings for each provider type (aws, azure, gcp, etc.). Only failed findings "
|
||||
"from the latest completed scans are considered. Supports provider_id and provider_type filters."
|
||||
),
|
||||
filters=True,
|
||||
),
|
||||
findings=extend_schema(
|
||||
summary="Get aggregated findings data",
|
||||
description=(
|
||||
@@ -3948,6 +3959,8 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
return OverviewProviderSerializer
|
||||
elif self.action == "providers_count":
|
||||
return OverviewProviderCountSerializer
|
||||
elif self.action == "providers_severity":
|
||||
return OverviewProviderSeveritySerializer
|
||||
elif self.action == "findings":
|
||||
return OverviewFindingSerializer
|
||||
elif self.action == "findings_severity":
|
||||
@@ -3965,7 +3978,7 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
def get_filterset_class(self):
|
||||
if self.action == "providers":
|
||||
return None
|
||||
elif self.action in ["findings", "services", "regions"]:
|
||||
elif self.action in ["findings", "services", "regions", "providers_severity"]:
|
||||
return ScanSummaryFilter
|
||||
elif self.action == "findings_severity":
|
||||
return ScanSummarySeverityFilter
|
||||
@@ -4212,6 +4225,38 @@ class OverviewViewSet(BaseRLSViewSet):
|
||||
serializer = self.get_serializer(severity_data)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
url_path="providers/severity",
|
||||
url_name="providers-severity",
|
||||
)
|
||||
def providers_severity(self, request):
|
||||
"""
|
||||
Get severity breakdown of failed findings grouped by provider type.
|
||||
"""
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
|
||||
severity_by_provider = (
|
||||
filtered_queryset.annotate(provider_type=F("scan__provider__provider"))
|
||||
.values("provider_type", "severity")
|
||||
.annotate(count=Sum("fail"))
|
||||
.order_by("provider_type", "severity")
|
||||
)
|
||||
|
||||
provider_data = {}
|
||||
for row in severity_by_provider:
|
||||
provider_type = row["provider_type"]
|
||||
if provider_type not in provider_data:
|
||||
provider_data[provider_type] = {
|
||||
"provider_type": provider_type,
|
||||
**{sev[0].lower(): 0 for sev in SeverityChoices},
|
||||
}
|
||||
provider_data[provider_type][row["severity"].lower()] = row["count"] or 0
|
||||
|
||||
serializer = self.get_serializer(list(provider_data.values()), many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_name="services")
|
||||
def services(self, request):
|
||||
filtered_queryset = self._get_latest_scans_queryset()
|
||||
|
||||
Reference in New Issue
Block a user