From f41fb6a186add53d70d6059a94a5dd7cfd873074 Mon Sep 17 00:00:00 2001 From: Josema Camacho Date: Wed, 10 Jun 2026 14:11:57 +0200 Subject: [PATCH] perf(api): resolve compliance-overviews attributes provider via O(1) index --- api/CHANGELOG.md | 1 + api/src/backend/api/compliance.py | 24 +++++++++++++++++++++++ api/src/backend/api/tests/test_views.py | 26 +++++++++++++++++++++++++ api/src/backend/api/v1/views.py | 11 ++--------- 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 2033dcc420..d19373e43a 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the **Prowler API** are documented in this file. - Workers now shut down gracefully on deploy or restart, finishing or re-queueing in-flight tasks instead of being force-killed and leaving them stuck [(#11416)](https://github.com/prowler-cloud/prowler/pull/11416) - Resource `name` is now stored and refreshed on every scan, so resources no longer keep an empty name [(#11476)](https://github.com/prowler-cloud/prowler/pull/11476) +- `GET /api/v1/compliance-overviews/attributes` resolves the owning provider via a prebuilt `compliance_id → provider` index instead of scanning every provider on each request [(#11525)](https://github.com/prowler-cloud/prowler/pull/11525) ### 🔐 Security diff --git a/api/src/backend/api/compliance.py b/api/src/backend/api/compliance.py index 678aff8d57..fbb29eca73 100644 --- a/api/src/backend/api/compliance.py +++ b/api/src/backend/api/compliance.py @@ -7,6 +7,7 @@ from prowler.lib.check.compliance_models import ( from prowler.lib.check.models import CheckMetadata AVAILABLE_COMPLIANCE_FRAMEWORKS = {} +COMPLIANCE_FRAMEWORK_PROVIDER = {} class LazyComplianceTemplate(Mapping): @@ -117,6 +118,29 @@ def get_compliance_frameworks(provider_type: Provider.ProviderChoices) -> list[s return AVAILABLE_COMPLIANCE_FRAMEWORKS[provider_type] +def get_provider_for_compliance(compliance_id: str) -> str | None: + """Resolve which provider owns `compliance_id`. + + Built once, lazily, from the per-provider framework lists in + ProviderChoices order so the first provider that supports a universal + framework (e.g. ``dora``, ``csa_ccm_4.0``) wins, matching the previous + per-request linear scan. + + Args: + compliance_id (str): Framework identifier (e.g., "cis_1.4_aws", "dora"). + + Returns: + str | None: The owning provider type, or None if no framework matches. + """ + global COMPLIANCE_FRAMEWORK_PROVIDER + if not COMPLIANCE_FRAMEWORK_PROVIDER: + for provider_type in Provider.ProviderChoices.values: + for framework_id in get_compliance_frameworks(provider_type): + COMPLIANCE_FRAMEWORK_PROVIDER.setdefault(framework_id, provider_type) + + return COMPLIANCE_FRAMEWORK_PROVIDER.get(compliance_id) + + def get_prowler_provider_checks(provider_type: Provider.ProviderChoices): """ Retrieve all check IDs for the specified provider type. diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index d213e8c855..1bc8ec5cd1 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -9570,6 +9570,32 @@ class TestComplianceOverviewViewSet: assert "Category" in first_attr assert "AWSService" in first_attr + def test_compliance_overview_attributes_universal_framework( + self, authenticated_client + ): + # Universal frameworks (no per-provider file) must still resolve through + # the reverse index, which maps them to the first supporting provider. + response = authenticated_client.get( + reverse("complianceoverview-attributes"), + {"filter[compliance_id]": "dora"}, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert len(data) > 0 + for item in data: + assert "id" in item + assert "metadata" in item["attributes"]["attributes"] + assert "check_ids" in item["attributes"]["attributes"] + + def test_compliance_overview_attributes_unknown_compliance_id( + self, authenticated_client + ): + response = authenticated_client.get( + reverse("complianceoverview-attributes"), + {"filter[compliance_id]": "does_not_exist"}, + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + def test_compliance_overview_attributes_missing_compliance_id( self, authenticated_client ): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 9c91c3201f..3c86395c23 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -116,6 +116,7 @@ from api.base_views import BaseRLSViewSet, BaseTenantViewset, BaseUserViewset from api.compliance import ( PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, get_compliance_frameworks, + get_provider_for_compliance, get_prowler_provider_compliance, ) from api.constants import SEVERITY_ORDER @@ -5072,15 +5073,7 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): ] ) - provider_type = None - - # If we couldn't determine from database, try each provider type - if not provider_type: - for pt in Provider.ProviderChoices.values: - if compliance_id in get_compliance_frameworks(pt): - provider_type = pt - break - + provider_type = get_provider_for_compliance(compliance_id) if not provider_type: raise NotFound(detail=f"Compliance framework '{compliance_id}' not found.")