Merge branch 'master' of github.com:prowler-cloud/prowler into showdown-demo

This commit is contained in:
Pepe Fagoaga
2025-06-05 11:07:08 +02:00
22 changed files with 147 additions and 283 deletions
+4
View File
@@ -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).
---
+7 -7
View File
@@ -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,
-40
View File
@@ -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"
+6 -6
View File
@@ -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": {
+1 -36
View File
@@ -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)
+18
View File
@@ -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
):
+15 -7
View File
@@ -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)
+1
View File
@@ -128,6 +128,7 @@ DJANGO_GUID = {
}
DATABASE_ROUTERS = ["api.db_router.MainRouter"]
POSTGRES_EXTRA_DB_BACKEND_BASE = "database_backend"
# Password validation
+17 -1
View File
@@ -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,
)
+15
View File
@@ -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)
+1 -1
View File
@@ -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)]
@@ -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}
/>
<Suspense
@@ -175,11 +175,6 @@ export default async function ComplianceDetail({
region={regionFilter}
filter={cisProfileFilter}
logoPath={logoPath}
uniqueRegions={uniqueRegions}
isRegionFiltered={
!!regionFilter &&
regionFilter.split(",").length < uniqueRegions.length
}
/>
</Suspense>
</ContentLayout>
@@ -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 ({
<ChartsWrapper logoPath={logoPath}>
<PieChart pass={0} fail={0} manual={0} />
<BarChart sections={[]} />
<HeatmapChart
regions={[]}
categories={[]}
isRegionFiltered={
!!region && region.split(",").length < uniqueRegions.length
}
filteredRegionName={region}
/>
<HeatmapChart categories={[]} />
</ChartsWrapper>
<ClientAccordionWrapper items={[]} defaultExpandedKeys={[]} />
</div>
@@ -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}
/>
<BarChart sections={topFailedSections} />
<HeatmapChart
regions={regionHeatmapData}
categories={categoryHeatmapData}
isRegionFiltered={isRegionFiltered}
filteredRegionName={region}
/>
<HeatmapChart categories={categoryHeatmapData} />
</ChartsWrapper>
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
@@ -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<FindingProps[]>([]);
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<string | null>(null);
@@ -116,8 +116,8 @@ export const ClientAccordionContent = ({
return (
<div className="w-full">
{renderDetails()}
<p className="text-sm text-gray-500">
This requirement has no checks; therefore, there are no findings.
<p className="mt-2 text-sm font-medium text-gray-800">
This requirement has no checks; therefore, there are no findings.
</p>
</div>
);
@@ -164,7 +164,11 @@ export const ClientAccordionContent = ({
);
}
return <div>There are no findings for this regions</div>;
return (
<div className="text-sm font-medium text-gray-800">
There are no findings for this regions
</div>
);
};
return (
@@ -172,7 +176,7 @@ export const ClientAccordionContent = ({
{renderDetails()}
{checks.length > 0 && (
<div className="mb-6 mt-2">
<div className="mb-2 mt-2">
<Accordion
items={accordionChecksItems}
variant="light"
@@ -12,8 +12,8 @@ export const ComplianceAccordionRequirementTitle = ({
}: ComplianceAccordionRequirementTitleProps) => {
return (
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-3/4 items-center gap-1">
<span className="whitespace-nowrap text-sm">{name}</span>
<div className="flex w-5/6 items-center gap-1">
<span>{name}</span>
</div>
<StatusFindingBadge status={status} />
</div>
@@ -24,7 +24,7 @@ export const ComplianceAccordionTitle = ({
<div className="flex flex-col items-start justify-between gap-1 md:flex-row md:items-center md:gap-2">
<div className="overflow-hidden md:min-w-0 md:flex-1">
<span
className="block w-full overflow-hidden truncate text-ellipsis text-sm"
className="block max-w-[600px] overflow-hidden truncate text-ellipsis text-sm"
title={label}
>
{label.charAt(0).toUpperCase() + label.slice(1)}
+8 -7
View File
@@ -71,14 +71,15 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
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<ComplianceCardProps> = ({
fullWidth
isHoverable
shadow="sm"
isPressable
isPressable={isPressable}
onPress={navigateToDetail}
>
<CardBody className="flex flex-row items-center justify-between space-x-4 dark:bg-prowler-blue-800">
@@ -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<CategoryData | null>(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 (
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[400px]">
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
{isRegionFiltered
? "Categories Failure Rate"
: "Failure Rate by Region"}
Sections Failure Rate
</h3>
<div className="flex h-[320px] w-full items-center justify-center">
<p className="text-sm text-gray-500">{noDataMessage}</p>
<p className="text-sm text-gray-500">No category data available</p>
</div>
</div>
);
}
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 = ({
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
<div>
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
{isRegionFiltered
? "Categories Failure Rate"
: "Failure Rate by Region"}
Sections Failure Rate
</h3>
</div>
<div className="h-full w-full p-4">
{/* 3x3 Grid */}
<div className="grid h-full w-full grid-cols-3 gap-1">
<div className="h-full w-full p-2">
<div
className={cn(
"grid h-full w-full gap-1",
heatmapData.length < 3 ? "grid-cols-1" : "grid-cols-3",
)}
style={{
gridTemplateRows:
heatmapData.length < 3
? `repeat(${heatmapData.length}, ${heatmapData.length}fr)`
: `repeat(${Math.min(Math.ceil(heatmapData.length / 3), 3)}, 1fr)`,
}}
>
{heatmapData.map((item) => (
<div
key={item.name}
className="flex items-center justify-center rounded border"
className="flex items-center justify-center rounded border p-1"
style={{
backgroundColor: getHeatmapColor(item.failurePercentage),
borderColor: theme === "dark" ? "#374151" : "#e5e7eb",
@@ -111,16 +101,15 @@ export const HeatmapChart = ({
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<div className="text-center">
<div className="w-full px-1 text-center">
<div
className="text-xs font-semibold"
className="truncate text-xs font-semibold"
style={{
color: theme === "dark" ? "#ffffff" : "#000000",
}}
title={capitalizeFirstLetter(item.name)}
>
{isRegionFiltered
? capitalizeFirstLetter(item.name)
: item.name}
{capitalizeFirstLetter(item.name)}
</div>
<div
className="text-xs"
@@ -148,9 +137,7 @@ export const HeatmapChart = ({
}}
>
<div className="mb-1 font-semibold">
{isRegionFiltered
? capitalizeFirstLetter(hoveredItem.name)
: hoveredItem.name}
{capitalizeFirstLetter(hoveredItem.name)}
</div>
<div>Failure Rate: {hoveredItem.failurePercentage}%</div>
<div>
@@ -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 && <FilterControls search />}
<Spacer y={8} />
<DataCompliance scans={scans} />
{showProviders && <DataCompliance scans={scans} />}
{allFilters.length > 0 && (
<>
<Spacer y={8} />
{showProviders && <Spacer y={8} />}
<DataTableFilterCustom filters={allFilters} defaultOpen={true} />
</>
)}
+1 -1
View File
@@ -138,7 +138,7 @@ export const Accordion = ({
indicator={<ChevronDown className="text-gray-500" />}
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",
-80
View File
@@ -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<RegionData[]> => {
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[] => {
-11
View File
@@ -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[];
}