feat(compliance): Add endpoint to retrieve compliance overviews metadata (#7333)

This commit is contained in:
Víctor Fernández Poyatos
2025-03-24 10:34:43 +01:00
committed by Pepe Fagoaga
parent bd839e1398
commit a4f5566589
7 changed files with 235 additions and 3 deletions
+1
View File
@@ -9,6 +9,7 @@ All notable changes to the **Prowler API** are documented in this file.
- Support for developing new integrations [(#7167)](https://github.com/prowler-cloud/prowler/pull/7167).
- HTTP Security Headers [(#7289)](https://github.com/prowler-cloud/prowler/pull/7289).
- New endpoint to get the compliance overviews metadata [(#7333)](https://github.com/prowler-cloud/prowler/pull/7333).
---
+104
View File
@@ -233,6 +233,42 @@ paths:
schema:
$ref: '#/components/schemas/ComplianceOverviewFullResponse'
description: ''
/api/v1/compliance-overviews/metadata:
get:
operationId: compliance_overviews_metadata_retrieve
description: Fetch unique metadata values from a set of compliance overviews.
This is useful for dynamic filtering.
summary: Retrieve metadata values from compliance overviews
parameters:
- in: query
name: fields[compliance-overviews-metadata]
schema:
type: array
items:
type: string
enum:
- regions
description: endpoint return only specific fields in the response on a per-type
basis by including a fields[TYPE] query parameter.
explode: false
- in: query
name: filter[scan_id]
schema:
type: string
format: uuid
description: Related scan ID.
required: true
tags:
- Compliance Overview
security:
- jwtAuth: []
responses:
'200':
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ComplianceOverviewMetadataResponse'
description: ''
/api/v1/findings:
get:
operationId: findings_list
@@ -3674,6 +3710,7 @@ paths:
- name
- manage_users
- manage_account
- manage_integrations
- manage_providers
- manage_scans
- permission_state
@@ -3792,6 +3829,8 @@ paths:
- -manage_users
- manage_account
- -manage_account
- manage_integrations
- -manage_integrations
- manage_providers
- -manage_providers
- manage_scans
@@ -3867,6 +3906,7 @@ paths:
- name
- manage_users
- manage_account
- manage_integrations
- manage_providers
- manage_scans
- permission_state
@@ -4236,6 +4276,17 @@ paths:
description: |-
* `scheduled` - Scheduled
* `manual` - Manual
- in: query
name: include
schema:
type: array
items:
type: string
enum:
- provider
description: include query parameter to allow the client to customize which
related resources should be returned.
explode: false
- name: page[number]
required: false
in: query
@@ -4350,6 +4401,17 @@ paths:
format: uuid
description: A UUID string identifying this scan.
required: true
- in: query
name: include
schema:
type: array
items:
type: string
enum:
- provider
description: include query parameter to allow the client to customize which
related resources should be returned.
explode: false
tags:
- Scan
security:
@@ -6105,6 +6167,40 @@ components:
$ref: '#/components/schemas/ComplianceOverviewFull'
required:
- data
ComplianceOverviewMetadata:
type: object
required:
- type
- id
additionalProperties: false
properties:
type:
allOf:
- $ref: '#/components/schemas/ComplianceOverviewMetadataTypeEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
id: {}
attributes:
type: object
properties:
regions:
type: array
items:
type: string
required:
- regions
ComplianceOverviewMetadataResponse:
type: object
properties:
data:
$ref: '#/components/schemas/ComplianceOverviewMetadata'
required:
- data
ComplianceOverviewMetadataTypeEnum:
type: string
enum:
- compliance-overviews-metadata
Finding:
type: object
required:
@@ -8398,6 +8494,8 @@ components:
type: boolean
manage_account:
type: boolean
manage_integrations:
type: boolean
manage_providers:
type: boolean
manage_scans:
@@ -10050,6 +10148,8 @@ components:
type: boolean
manage_account:
type: boolean
manage_integrations:
type: boolean
manage_providers:
type: boolean
manage_scans:
@@ -10179,6 +10279,8 @@ components:
type: boolean
manage_account:
type: boolean
manage_integrations:
type: boolean
manage_providers:
type: boolean
manage_scans:
@@ -10313,6 +10415,8 @@ components:
type: boolean
manage_account:
type: boolean
manage_integrations:
type: boolean
manage_providers:
type: boolean
manage_scans:
+28
View File
@@ -14,6 +14,7 @@ from django.urls import reverse
from rest_framework import status
from api.models import (
ComplianceOverview,
Integration,
Invitation,
Membership,
@@ -4508,6 +4509,33 @@ class TestComplianceOverviewViewSet:
assert len(response.json()["data"]) == 1
assert response.json()["data"][0]["id"] == str(compliance_overview1.id)
def test_compliance_overview_metadata(
self, authenticated_client, compliance_overviews_fixture
):
response = authenticated_client.get(
reverse("complianceoverview-metadata"),
{"filter[scan_id]": str(compliance_overviews_fixture[0].scan_id)},
)
data = response.json()
expected_regions = set(
ComplianceOverview.objects.all()
.values_list("region", flat=True)
.distinct("region")
)
assert response.status_code == status.HTTP_200_OK
assert data["data"]["type"] == "compliance-overviews-metadata"
assert data["data"]["id"] is None
assert set(data["data"]["attributes"]["regions"]) == expected_regions
def test_compliance_overview_metadata_missing_scan_id(self, authenticated_client):
# Attempt to list compliance overviews without providing filter[scan_id]
response = authenticated_client.get(reverse("complianceoverview-metadata"))
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json()["errors"][0]["source"]["pointer"] == "filter[scan_id]"
assert response.json()["errors"][0]["code"] == "required"
@pytest.mark.django_db
class TestOverviewViewSet:
+11
View File
@@ -851,6 +851,10 @@ class ScanSerializer(RLSSerializer):
"url",
]
included_serializers = {
"provider": "api.v1.serializers.ProviderIncludeSerializer",
}
class ScanIncludeSerializer(RLSSerializer):
trigger = serializers.ChoiceField(
@@ -1897,6 +1901,13 @@ class ComplianceOverviewFullSerializer(ComplianceOverviewSerializer):
return obj.requirements
class ComplianceOverviewMetadataSerializer(serializers.Serializer):
regions = serializers.ListField(child=serializers.CharField(), allow_empty=True)
class Meta:
resource_name = "compliance-overviews-metadata"
# Overviews
+47
View File
@@ -102,6 +102,7 @@ from api.rls import Tenant
from api.utils import CustomOAuth2Client, validate_invitation
from api.v1.serializers import (
ComplianceOverviewFullSerializer,
ComplianceOverviewMetadataSerializer,
ComplianceOverviewSerializer,
FindingDynamicFilterSerializer,
FindingMetadataSerializer,
@@ -2057,6 +2058,21 @@ class RoleProviderGroupRelationshipView(RelationshipView, BaseRLSViewSet):
description="Fetch detailed information about a specific compliance overview by its ID, including detailed "
"requirement information and check's status.",
),
metadata=extend_schema(
tags=["Compliance Overview"],
summary="Retrieve metadata values from compliance overviews",
description="Fetch unique metadata values from a set of compliance overviews. This is useful for dynamic "
"filtering.",
parameters=[
OpenApiParameter(
name="filter[scan_id]",
required=True,
type=OpenApiTypes.UUID,
location=OpenApiParameter.QUERY,
description="Related scan ID.",
),
],
),
)
@method_decorator(CACHE_DECORATOR, name="list")
@method_decorator(CACHE_DECORATOR, name="retrieve")
@@ -2118,6 +2134,8 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
def get_serializer_class(self):
if self.action == "retrieve":
return ComplianceOverviewFullSerializer
elif self.action == "metadata":
return ComplianceOverviewMetadataSerializer
return super().get_serializer_class()
def list(self, request, *args, **kwargs):
@@ -2134,6 +2152,35 @@ class ComplianceOverviewViewSet(BaseRLSViewSet):
)
return super().list(request, *args, **kwargs)
@action(detail=False, methods=["get"], url_name="metadata")
def metadata(self, request):
scan_id = request.query_params.get("filter[scan_id]")
if not scan_id:
raise ValidationError(
[
{
"detail": "This query parameter is required.",
"status": 400,
"source": {"pointer": "filter[scan_id]"},
"code": "required",
}
]
)
tenant_id = self.request.tenant_id
regions = list(
ComplianceOverview.objects.filter(tenant_id=tenant_id, scan_id=scan_id)
.values_list("region", flat=True)
.order_by("region")
.distinct()
)
result = {"regions": regions}
serializer = self.get_serializer(data=result)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(tags=["Overview"])
@extend_schema_view(
+41
View File
@@ -43,3 +43,44 @@ export const getCompliancesOverview = async ({
return undefined;
}
};
export const getComplianceOverviewMetadataInfo = async ({
query = "",
sort = "",
filters = {},
}) => {
const session = await auth();
const url = new URL(`${apiBaseUrl}/compliance-overviews/metadata`);
if (query) url.searchParams.append("filter[search]", query);
if (sort) url.searchParams.append("sort", sort);
Object.entries(filters).forEach(([key, value]) => {
// Define filters to exclude
if (key !== "filter[search]") {
url.searchParams.append(key, String(value));
}
});
try {
const response = await fetch(url.toString(), {
headers: {
Accept: "application/vnd.api+json",
Authorization: `Bearer ${session?.accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch compliance overview metadata info: ${response.statusText}`);
}
const parsedData = parseStringify(await response.json());
return parsedData;
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error fetching compliance overview metadata info:", error);
return undefined;
}
};
+3 -3
View File
@@ -4,7 +4,7 @@ import { Spacer } from "@nextui-org/react";
import { Suspense } from "react";
import { getCompliancesOverview } from "@/actions/compliances";
import { getMetadataInfo } from "@/actions/findings";
import { getComplianceOverviewMetadataInfo } from "@/actions/compliances";
import { getProvider } from "@/actions/providers";
import { getScans } from "@/actions/scans";
import {
@@ -70,10 +70,10 @@ export default async function Compliance({
searchParams.scanId || expandedScansData[0]?.id || null;
const query = (filters["filter[search]"] as string) || "";
const metadataInfoData = await getMetadataInfo({
const metadataInfoData = await getComplianceOverviewMetadataInfo({
query,
filters: {
"filter[scan]": selectedScanId,
"filter[scan_id]": selectedScanId,
},
});