diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index dd3446550a..524e83f46b 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -19,8 +19,12 @@ All notable changes to the **Prowler API** are documented in this file. ## [v1.8.3] (Prowler v5.7.3) +### Added +- Database backend to handle already closed connections [(#7935)](https://github.com/prowler-cloud/prowler/pull/7935). + ### Fixed - Fixed transaction persistence with RLS operations [(#7916)](https://github.com/prowler-cloud/prowler/pull/7916). +- Reverted the change `get_with_retry` to use the original `get` method for retrieving tasks [(#7932)](https://github.com/prowler-cloud/prowler/pull/7932). --- diff --git a/api/src/backend/api/compliance.py b/api/src/backend/api/compliance.py index 96b5e313f3..46e7211962 100644 --- a/api/src/backend/api/compliance.py +++ b/api/src/backend/api/compliance.py @@ -190,6 +190,8 @@ def generate_compliance_overview_template(prowler_compliance: dict): total_checks = len(requirement.Checks) checks_dict = {check: None for check in requirement.Checks} + req_status_val = "MANUAL" if total_checks == 0 else "PASS" + # Build requirement dictionary requirement_dict = { "name": requirement.Name or requirement.Id, @@ -204,20 +206,18 @@ def generate_compliance_overview_template(prowler_compliance: dict): "manual": 0, "total": total_checks, }, - "status": "PASS", + "status": req_status_val, } - # Update requirements status - if total_checks == 0: + # Update requirements status counts for the framework + if req_status_val == "MANUAL": requirements_status["manual"] += 1 + elif req_status_val == "PASS": + requirements_status["passed"] += 1 # Add requirement to compliance requirements compliance_requirements[requirement.Id] = requirement_dict - # Calculate pending requirements - pending_requirements = total_requirements - requirements_status["manual"] - requirements_status["passed"] = pending_requirements - # Build compliance dictionary compliance_dict = { "framework": compliance_data.Framework, diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index d82fe45fe7..1bb87f7b55 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1,11 +1,9 @@ import json import logging import re -import time from uuid import UUID, uuid4 from config.custom_logging import BackendLogger -from config.env import env from cryptography.fernet import Fernet, InvalidToken from django.conf import settings from django.contrib.auth.models import AbstractBaseUser @@ -358,42 +356,6 @@ class ProviderGroupMembership(RowLevelSecurityProtectedModel): resource_name = "provider_groups-provider" -class TaskManager(models.Manager): - def get_with_retry( - self, - id: str, - max_retries: int = None, - delay_seconds: float = None, - ): - """ - Retry fetching a Task by ID in case it hasn't been created yet. - - Args: - id (str): The Celery task ID (expected to match Task model PK). - max_retries (int, optional): Number of retry attempts. Defaults to env TASK_RETRY_ATTEMPTS or 5. - delay_seconds (float, optional): Delay between retries in seconds. Defaults to env TASK_RETRY_DELAY_SECONDS or 0.1. - - Returns: - Task: The retrieved Task instance. - - Raises: - Task.DoesNotExist: If the task is not found after all retries. - """ - max_retries = max_retries or env.int("TASK_RETRY_ATTEMPTS", default=5) - delay_seconds = delay_seconds or env.float( - "TASK_RETRY_DELAY_SECONDS", default=0.1 - ) - - for _attempt in range(max_retries): - try: - return self.get(id=id) - except self.model.DoesNotExist: - time.sleep(delay_seconds) - raise self.model.DoesNotExist( - f"Task with ID {id} not found after {max_retries} retries." - ) - - class Task(RowLevelSecurityProtectedModel): id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) @@ -406,8 +368,6 @@ class Task(RowLevelSecurityProtectedModel): blank=True, ) - objects = TaskManager() - class Meta(RowLevelSecurityProtectedModel.Meta): db_table = "tasks" diff --git a/api/src/backend/api/tests/test_compliance.py b/api/src/backend/api/tests/test_compliance.py index 6b405536b3..bf3208a4d1 100644 --- a/api/src/backend/api/tests/test_compliance.py +++ b/api/src/backend/api/tests/test_compliance.py @@ -1,12 +1,12 @@ -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from api.compliance import ( + generate_compliance_overview_template, + generate_scan_compliance, get_prowler_provider_checks, get_prowler_provider_compliance, - load_prowler_compliance, load_prowler_checks, - generate_scan_compliance, - generate_compliance_overview_template, + load_prowler_compliance, ) from api.models import Provider @@ -69,7 +69,7 @@ class TestCompliance: load_prowler_compliance() - from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, PROWLER_CHECKS + from api.compliance import PROWLER_CHECKS, PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE assert PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE == { "template_key": "template_value" @@ -268,7 +268,7 @@ class TestCompliance: "manual": 0, "total": 0, }, - "status": "PASS", + "status": "MANUAL", }, }, "requirements_status": { diff --git a/api/src/backend/api/tests/test_models.py b/api/src/backend/api/tests/test_models.py index de2ec7c59e..c2beeb3583 100644 --- a/api/src/backend/api/tests/test_models.py +++ b/api/src/backend/api/tests/test_models.py @@ -1,9 +1,6 @@ -import uuid -from unittest import mock - import pytest -from api.models import Resource, ResourceTag, Task +from api.models import Resource, ResourceTag @pytest.mark.django_db @@ -123,35 +120,3 @@ class TestResourceModel: # compliance={}, # ) # assert Finding.objects.filter(uid=long_uid).exists() - - -@pytest.mark.django_db -class TestTaskManager: - def test_get_with_retry_success(self): - task_id = uuid.uuid4() - call_counter = {"count": 0} - - def side_effect(*args, **kwargs): - if call_counter["count"] < 2: - call_counter["count"] += 1 - raise Task.DoesNotExist() - return Task(id=task_id) - - with mock.patch.object(Task.objects, "get", side_effect=side_effect): - task = Task.objects.get_with_retry( - task_id, max_retries=5, delay_seconds=0.01 - ) - - assert task.id == task_id - assert call_counter["count"] == 2 - - def test_get_with_retry_fail(self): - non_existent_id = uuid.uuid4() - - with mock.patch.object(Task.objects, "get", side_effect=Task.DoesNotExist): - with pytest.raises(Task.DoesNotExist) as excinfo: - Task.objects.get_with_retry( - non_existent_id, max_retries=3, delay_seconds=0.01 - ) - - assert str(non_existent_id) in str(excinfo.value) diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 5764d265e7..36f4e3acc2 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -4837,6 +4837,24 @@ class TestComplianceOverviewViewSet: assert "description" in attributes assert "status" in attributes + def test_compliance_overview_requirements_manual( + self, authenticated_client, compliance_requirements_overviews_fixture + ): + scan_id = str(compliance_requirements_overviews_fixture[0].scan.id) + # Compliance with a manual requirement + compliance_id = "aws_account_security_onboarding_aws" + + response = authenticated_client.get( + reverse("complianceoverview-requirements"), + { + "filter[scan_id]": scan_id, + "filter[compliance_id]": compliance_id, + }, + ) + assert response.status_code == status.HTTP_200_OK + data = response.json()["data"] + assert data[-1]["attributes"]["status"] == "MANUAL" + def test_compliance_overview_requirements_missing_scan_id( self, authenticated_client ): diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 5e6ce25a11..0313e5f31d 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1097,7 +1097,7 @@ class ProviderViewSet(BaseRLSViewSet): task = check_provider_connection_task.delay( provider_id=pk, tenant_id=self.request.tenant_id ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) serializer = TaskSerializer(prowler_task) return Response( data=serializer.data, @@ -1120,7 +1120,7 @@ class ProviderViewSet(BaseRLSViewSet): task = delete_provider_task.delay( provider_id=pk, tenant_id=self.request.tenant_id ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) serializer = TaskSerializer(prowler_task) return Response( data=serializer.data, @@ -1500,7 +1500,7 @@ class ScanViewSet(BaseRLSViewSet): }, ) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) scan.task_id = task.id scan.save(update_fields=["task_id"]) @@ -2738,7 +2738,10 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): "requirement_id", "framework", "version", "description" ) .distinct() - .annotate(total_instances=Count("id")) + .annotate( + total_instances=Count("id"), + manual_count=Count("id", filter=Q(requirement_status="MANUAL")), + ) ) passed_instances = ( @@ -2756,8 +2759,13 @@ class ComplianceOverviewViewSet(BaseRLSViewSet, TaskManagementMixin): requirement_id = requirement["requirement_id"] total_instances = requirement["total_instances"] passed_count = passed_counts.get(requirement_id, 0) - - requirement_status = "PASS" if passed_count == total_instances else "FAIL" + is_manual = requirement["manual_count"] == total_instances + if is_manual: + requirement_status = "MANUAL" + elif passed_count == total_instances: + requirement_status = "PASS" + else: + requirement_status = "FAIL" requirements_summary.append( { @@ -3128,7 +3136,7 @@ class ScheduleViewSet(BaseRLSViewSet): with transaction.atomic(): task = schedule_provider_scan(provider_instance) - prowler_task = Task.objects.get_with_retry(id=task.id) + prowler_task = Task.objects.get(id=task.id) self.response_serializer_class = TaskSerializer output_serializer = self.get_serializer(prowler_task) diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 5de1d9ca71..db4a23e464 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -128,6 +128,7 @@ DJANGO_GUID = { } DATABASE_ROUTERS = ["api.db_router.MainRouter"] +POSTGRES_EXTRA_DB_BACKEND_BASE = "database_backend" # Password validation diff --git a/api/src/backend/conftest.py b/api/src/backend/conftest.py index e2a2746610..1d53e87ddb 100644 --- a/api/src/backend/conftest.py +++ b/api/src/backend/conftest.py @@ -847,8 +847,23 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture): total_checks=2, ) - # Create a different compliance framework for testing requirement_overview5 = ComplianceRequirementOverview.objects.create( + tenant=tenant, + scan=scan1, + compliance_id="aws_account_security_onboarding_aws", + framework="AWS-Account-Security-Onboarding", + version="1.0", + description="Description for AWS Account Security Onboarding (MANUAL)", + region="eu-west-2", + requirement_id="requirement3", + requirement_status=StatusChoices.MANUAL, + passed_checks=0, + failed_checks=0, + total_checks=0, + ) + + # Create a different compliance framework for testing + requirement_overview6 = ComplianceRequirementOverview.objects.create( tenant=tenant, scan=scan1, compliance_id="cis_1.4_aws", @@ -869,6 +884,7 @@ def compliance_requirements_overviews_fixture(scans_fixture, tenants_fixture): requirement_overview3, requirement_overview4, requirement_overview5, + requirement_overview6, ) diff --git a/api/src/backend/database_backend/__init__.py b/api/src/backend/database_backend/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/src/backend/database_backend/base.py b/api/src/backend/database_backend/base.py new file mode 100644 index 0000000000..69fee7df4b --- /dev/null +++ b/api/src/backend/database_backend/base.py @@ -0,0 +1,15 @@ +import django.db +from django.db.backends.postgresql.base import ( + DatabaseWrapper as BuiltinPostgresDatabaseWrapper, +) +from psycopg2 import InterfaceError + + +class DatabaseWrapper(BuiltinPostgresDatabaseWrapper): + def create_cursor(self, name=None): + try: + return super().create_cursor(name=name) + except InterfaceError: + django.db.close_old_connections() + django.db.connection.connect() + return super().create_cursor(name=name) diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index ae0e1cb084..6f637a3503 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixes - Fix sync between filter buttons and URL when filters change. [(#7928)](https://github.com/prowler-cloud/prowler/pull/7928) +- Improve heatmap perfomance. [(#7934)](https://github.com/prowler-cloud/prowler/pull/7934) ### 🚀 Added @@ -31,7 +32,6 @@ All notable changes to the **Prowler UI** are documented in this file. ### 🐞 Fixes - Download report behaviour updated to show feedback based on API response. [(#7758)](https://github.com/prowler-cloud/prowler/pull/7758) -- Compliace detail page, now available for ENS. [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853) - Missing KISA and ProwlerThreat icons added to the compliance page. [(#7860)(https://github.com/prowler-cloud/prowler/pull/7860)] - Retrieve more than 10 scans in /compliance page. [(#7865)](https://github.com/prowler-cloud/prowler/pull/7865) - Improve CustomDropdownFilter component. [(#7868)(https://github.com/prowler-cloud/prowler/pull/7868)] diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 189a33531a..98a83d403b 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -24,7 +24,6 @@ import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance" import { ContentLayout } from "@/components/ui"; import { calculateCategoryHeatmapData, - calculateRegionHeatmapData, getComplianceMapper, } from "@/lib/compliance/commons"; import { ScanProps } from "@/types"; @@ -154,6 +153,7 @@ export default async function ComplianceDetail({ uniqueRegions={uniqueRegions} showSearch={false} framework={compliancetitle} + showProviders={false} /> @@ -192,16 +187,12 @@ const SSRComplianceContent = async ({ region, filter, logoPath, - uniqueRegions, - isRegionFiltered, }: { complianceId: string; scanId: string; region?: string; filter?: string; logoPath?: string; - uniqueRegions: string[]; - isRegionFiltered: boolean; }) => { if (!scanId) { return ( @@ -209,14 +200,7 @@ const SSRComplianceContent = async ({ - + @@ -236,20 +220,15 @@ const SSRComplianceContent = async ({ // Determine framework from the first attribute item const framework = attributesData?.data?.[0]?.attributes?.framework; const mapper = getComplianceMapper(framework); + + // Use the same data for both compliance view and heatmap const data = mapper.mapComplianceData( attributesData, requirementsData, filter, ); - // Calculate region heatmap data using already obtained data - const regionHeatmapData = await calculateRegionHeatmapData( - complianceId, - scanId, - uniqueRegions, - attributesData, - mapper, - ); + // Calculate category heatmap data const categoryHeatmapData = calculateCategoryHeatmapData(data); const totalRequirements: RequirementsTotals = data.reduce( @@ -277,12 +256,7 @@ const SSRComplianceContent = async ({ manual={totalRequirements.manual} /> - + diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx index d22c4b2fe4..ae13888bdd 100644 --- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx +++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx @@ -12,7 +12,7 @@ import { Accordion } from "@/components/ui/accordion/Accordion"; import { DataTable } from "@/components/ui/table"; import { createDict } from "@/lib"; import { getComplianceMapper } from "@/lib/compliance/commons"; -import { ComplianceId, Requirement } from "@/types/compliance"; +import { Requirement } from "@/types/compliance"; import { FindingProps, FindingsResponse } from "@/types/components"; interface ClientAccordionContentProps { @@ -32,7 +32,7 @@ export const ClientAccordionContent = ({ const [expandedFindings, setExpandedFindings] = useState([]); const searchParams = useSearchParams(); const pageNumber = searchParams.get("page") || "1"; - const complianceId = searchParams.get("complianceId") as ComplianceId; + const complianceId = searchParams.get("complianceId"); const defaultSort = "severity,status,-inserted_at"; const sort = searchParams.get("sort") || defaultSort; const loadedPageRef = useRef(null); @@ -116,8 +116,8 @@ export const ClientAccordionContent = ({ return (
{renderDetails()} -

- This requirement has no checks; therefore, there are no findings. +

+ ⚠️ This requirement has no checks; therefore, there are no findings.

); @@ -164,7 +164,11 @@ export const ClientAccordionContent = ({ ); } - return
There are no findings for this regions
; + return ( +
+ ⚠️ There are no findings for this regions +
+ ); }; return ( @@ -172,7 +176,7 @@ export const ClientAccordionContent = ({ {renderDetails()} {checks.length > 0 && ( -
+
{ return (
-
- {name} +
+ {name}
diff --git a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx index 0f3f681b59..c9fa69aa8d 100644 --- a/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx +++ b/ui/components/compliance/compliance-accordion/compliance-accordion-title.tsx @@ -24,7 +24,7 @@ export const ComplianceAccordionTitle = ({
{label.charAt(0).toUpperCase() + label.slice(1)} diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 63f4c0150b..720c5acd77 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -71,14 +71,15 @@ export const ComplianceCard: React.FC = ({ return "success"; }; + const isPressable = + id.includes("ens") || + id.includes("iso") || + id.includes("cis_") || + id.includes("pillar"); + const navigateToDetail = () => { // We will unlock this while developing the rest of complainces. - if ( - !id.includes("ens") && - !id.includes("iso") && - !id.includes("cis_") && - !id.includes("pillar") - ) { + if (!isPressable) { return; } @@ -106,7 +107,7 @@ export const ComplianceCard: React.FC = ({ fullWidth isHoverable shadow="sm" - isPressable + isPressable={isPressable} onPress={navigateToDetail} > diff --git a/ui/components/compliance/compliance-charts/heatmap-chart.tsx b/ui/components/compliance/compliance-charts/heatmap-chart.tsx index d4cb3b3a0a..bf5474595f 100644 --- a/ui/components/compliance/compliance-charts/heatmap-chart.tsx +++ b/ui/components/compliance/compliance-charts/heatmap-chart.tsx @@ -1,15 +1,13 @@ "use client"; +import { cn } from "@nextui-org/react"; import { useTheme } from "next-themes"; import { useState } from "react"; -import { CategoryData, RegionData } from "@/types/compliance"; +import { CategoryData } from "@/types/compliance"; interface HeatmapChartProps { - regions: RegionData[]; categories?: CategoryData[]; - isRegionFiltered?: boolean; // Indicates if a region filter is active - filteredRegionName?: string; // Name of the filtered region } const getHeatmapColor = (percentage: number): string => { @@ -32,48 +30,32 @@ const capitalizeFirstLetter = (text: string): string => { ); }; -export const HeatmapChart = ({ - regions, - categories = [], - isRegionFiltered = false, -}: HeatmapChartProps) => { +export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { const { theme } = useTheme(); - const [hoveredItem, setHoveredItem] = useState< - RegionData | CategoryData | null - >(null); + const [hoveredItem, setHoveredItem] = useState(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); - // Determine what data to show and prepare it - const dataToShow = isRegionFiltered ? categories : regions; - const heatmapData = dataToShow + // Use categories data and prepare it + const heatmapData = categories .filter((item) => item.totalRequirements > 0) .sort((a, b) => b.failurePercentage - a.failurePercentage) .slice(0, 9); // Exactly 9 items for 3x3 grid // Check if there are no items with data - if (!dataToShow || dataToShow.length === 0 || heatmapData.length === 0) { - const noDataMessage = isRegionFiltered - ? "No category data available" - : "No regional data available"; - + if (!categories.length || heatmapData.length === 0) { return (

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-

{noDataMessage}

+

No category data available

); } - const handleMouseEnter = ( - item: RegionData | CategoryData, - event: React.MouseEvent, - ) => { + const handleMouseEnter = (item: CategoryData, event: React.MouseEvent) => { setHoveredItem(item); setMousePosition({ x: event.clientX, y: event.clientY }); }; @@ -90,19 +72,27 @@ export const HeatmapChart = ({

- {isRegionFiltered - ? "Categories Failure Rate" - : "Failure Rate by Region"} + Sections Failure Rate

-
- {/* 3x3 Grid */} -
+
+
{heatmapData.map((item) => (
-
+
- {isRegionFiltered - ? capitalizeFirstLetter(item.name) - : item.name} + {capitalizeFirstLetter(item.name)}
- {isRegionFiltered - ? capitalizeFirstLetter(hoveredItem.name) - : hoveredItem.name} + {capitalizeFirstLetter(hoveredItem.name)}
Failure Rate: {hoveredItem.failurePercentage}%
diff --git a/ui/components/compliance/compliance-header/compliance-header.tsx b/ui/components/compliance/compliance-header/compliance-header.tsx index e6bfe0dd2d..abe54adf11 100644 --- a/ui/components/compliance/compliance-header/compliance-header.tsx +++ b/ui/components/compliance/compliance-header/compliance-header.tsx @@ -14,6 +14,7 @@ interface ComplianceHeaderProps { showSearch?: boolean; showRegionFilter?: boolean; framework?: string; // Framework name to show specific filters + showProviders?: boolean; } export const ComplianceHeader = ({ @@ -22,6 +23,7 @@ export const ComplianceHeader = ({ showSearch = true, showRegionFilter = true, framework, + showProviders = true, }: ComplianceHeaderProps) => { const frameworkFilters = []; @@ -56,10 +58,10 @@ export const ComplianceHeader = ({ <> {showSearch && } - + {showProviders && } {allFilters.length > 0 && ( <> - + {showProviders && } )} diff --git a/ui/components/ui/accordion/Accordion.tsx b/ui/components/ui/accordion/Accordion.tsx index 1bbcbc9ed8..8117d5eea6 100644 --- a/ui/components/ui/accordion/Accordion.tsx +++ b/ui/components/ui/accordion/Accordion.tsx @@ -138,7 +138,7 @@ export const Accordion = ({ indicator={} classNames={{ base: index === 0 || index === 1 ? "my-1" : "my-1", - title: "text-sm font-medium max-w-full overflow-hidden truncate", + title: "text-sm", subtitle: "text-xs text-gray-500", trigger: "py-2 px-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center", diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 7317bb6c0f..c62933b635 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -10,7 +10,6 @@ import { CategoryData, FailedSection, Framework, - RegionData, Requirement, RequirementsData, } from "@/types/compliance"; @@ -138,85 +137,6 @@ export const getComplianceMapper = (framework?: string): ComplianceMapper => { return complianceMappers[framework] || defaultMapper; }; -export const calculateRegionHeatmapData = async ( - complianceId: string, - scanId: string, - uniqueRegions: string[], - attributesData: AttributesData, - mapper: ComplianceMapper, -): Promise => { - if (!complianceId || !scanId || !uniqueRegions?.length) { - return []; - } - - try { - const { getComplianceRequirements } = await import("@/actions/compliances"); - - // Get data for each region in parallel - const regionPromises = uniqueRegions.map(async (region) => { - try { - // Only need to fetch requirements data per region - const regionRequirementsData = await getComplianceRequirements({ - complianceId, - scanId, - region, // Filter by specific region - }); - - // Map the data using the provided mapper - const mappedData = mapper.mapComplianceData( - attributesData, - regionRequirementsData, - ); - - // Calculate totals for this region - const regionTotals = mappedData.reduce( - (acc, framework) => ({ - pass: acc.pass + framework.pass, - fail: acc.fail + framework.fail, - manual: acc.manual + framework.manual, - }), - { pass: 0, fail: 0, manual: 0 }, - ); - - const totalRequirements = - regionTotals.pass + regionTotals.fail + regionTotals.manual; - const failurePercentage = - totalRequirements > 0 - ? Math.round((regionTotals.fail / totalRequirements) * 100) - : 0; - - return { - name: region, - failurePercentage, - totalRequirements, - failedRequirements: regionTotals.fail, - }; - } catch (error) { - console.error(`Error fetching data for region ${region}:`, error); - return { - name: region, - failurePercentage: 0, - totalRequirements: 0, - failedRequirements: 0, - }; - } - }); - - const regionData = await Promise.all(regionPromises); - - // Filter, sort and limit to top 9 regions for 3x3 grid - const filteredData = regionData - .filter((region) => region.totalRequirements > 0) - .sort((a, b) => b.failurePercentage - a.failurePercentage) - .slice(0, 9); - - return filteredData; - } catch (error) { - console.error("Error calculating region heatmap data:", error); - return []; - } -}; - export const calculateCategoryHeatmapData = ( complianceData: Framework[], ): CategoryData[] => { diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 8cfaab0246..994c90d200 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -1,16 +1,5 @@ export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings"; -export type ComplianceId = - | "ens_rd2022_aws" - | "iso27001_2013_aws" - | "iso27001_2022_aws" - | "cis_1.4_aws" - | "cis_1.5_aws" - | "cis_2.0_aws" - | "cis_3.0_aws" - | "cis_4.0_aws" - | "cis_5.0_aws"; - export interface CompliancesOverview { data: ComplianceOverviewData[]; }