feat: ISO compliance detail view (#7897)

Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
Co-authored-by: Pablo Lara <larabjj@gmail.com>
This commit is contained in:
Alejandro Bailo
2025-06-03 09:20:52 +02:00
committed by GitHub
co-authored by Víctor Fernández Poyatos Pablo Lara
parent e34c18757d
commit 3e8c86d880
14 changed files with 506 additions and 136 deletions
@@ -16,14 +16,11 @@ import { FailedSectionsChart } from "@/components/compliance/failed-sections-cha
import { FailedSectionsChartSkeleton } from "@/components/compliance/failed-sections-chart-skeleton";
import { RequirementsChart } from "@/components/compliance/requirements-chart";
import { RequirementsChartSkeleton } from "@/components/compliance/requirements-chart-skeleton";
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
import { ContentLayout } from "@/components/ui";
import { mapComplianceData, toAccordionItems } from "@/lib/compliance/ens";
import { getComplianceMapper } from "@/lib/compliance/commons";
import { ScanProps } from "@/types";
import {
FailedSection,
MappedComplianceData,
RequirementsTotals,
} from "@/types/compliance";
import { Framework, RequirementsTotals } from "@/types/compliance";
interface ComplianceDetailSearchParams {
complianceId: string;
@@ -32,7 +29,7 @@ interface ComplianceDetailSearchParams {
"filter[region__in]"?: string;
}
const Logo = ({ logoPath }: { logoPath: string }) => {
const ComplianceLogo = ({ logoPath }: { logoPath: string }) => {
return (
<div className="relative ml-auto hidden h-[200px] w-[200px] flex-shrink-0 md:block">
<Image
@@ -51,14 +48,14 @@ const ChartsWrapper = ({
logoPath,
}: {
children: React.ReactNode;
logoPath: string;
logoPath?: string;
}) => {
return (
<div className="mb-8 flex w-full">
<div className="flex flex-col items-center gap-16 lg:flex-row">
{children}
</div>
{logoPath && <Logo logoPath={logoPath} />}
{logoPath && <ComplianceLogo logoPath={logoPath} />}
</div>
);
};
@@ -73,8 +70,7 @@ export default async function ComplianceDetail({
const { compliancetitle } = params;
const { complianceId, version, scanId } = searchParams;
const regionFilter = searchParams["filter[region__in]"];
const logoPath = `/${compliancetitle.toLowerCase()}.png`;
const logoPath = getComplianceIcon(compliancetitle);
// Create a key that includes region filter for Suspense
const searchParamsKey = JSON.stringify(searchParams || {});
@@ -164,7 +160,7 @@ const getComplianceData = async (
complianceId: string,
scanId: string,
region?: string,
): Promise<MappedComplianceData> => {
): Promise<Framework[]> => {
const [attributesData, requirementsData] = await Promise.all([
getComplianceAttributes(complianceId),
getComplianceRequirements({
@@ -174,44 +170,16 @@ const getComplianceData = async (
}),
]);
const mappedData = mapComplianceData(attributesData, requirementsData);
// Determine framework from the first attribute item
const framework = attributesData?.data?.[0]?.attributes?.framework;
// Get the appropriate mapper for this framework
const mapper = getComplianceMapper(framework);
const mappedData = mapper.mapComplianceData(attributesData, requirementsData);
return mappedData;
};
const getTopFailedSections = (
mappedData: MappedComplianceData,
): FailedSection[] => {
const failedSectionMap = new Map();
mappedData.forEach((framework) => {
framework.categories.forEach((category) => {
category.controls.forEach((control) => {
control.requirements.forEach((requirement) => {
if (requirement.status === "FAIL") {
const sectionName = category.name;
if (!failedSectionMap.has(sectionName)) {
failedSectionMap.set(sectionName, { total: 0, types: {} });
}
const sectionData = failedSectionMap.get(sectionName);
sectionData.total += 1;
const type = requirement.type;
sectionData.types[type] = (sectionData.types[type] || 0) + 1;
}
});
});
});
});
// Convert in descending order and slice top 5
return Array.from(failedSectionMap.entries())
.map(([name, data]) => ({ name, ...data }))
.sort((a, b) => b.total - a.total)
.slice(0, 5); // Top 5
};
const SSRComplianceContent = async ({
complianceId,
scanId,
@@ -221,7 +189,7 @@ const SSRComplianceContent = async ({
complianceId: string;
scanId: string;
region?: string;
logoPath: string;
logoPath?: string;
}) => {
if (!scanId) {
return (
@@ -244,9 +212,19 @@ const SSRComplianceContent = async ({
}),
{ pass: 0, fail: 0, manual: 0 },
);
const topFailedSections = getTopFailedSections(data);
const accordionItems = toAccordionItems(data, scanId);
const defaultKeys = accordionItems.slice(0, 2).map((item) => item.key);
// Get the framework to determine which mapper to use
const attributesData = await getComplianceAttributes(complianceId);
const framework = attributesData?.data?.[0]?.attributes?.framework;
// Get the appropriate mapper for this framework
const mapper = getComplianceMapper(framework);
const accordionItems = mapper.toAccordionItems(data, scanId);
const topFailedSections = mapper.getTopFailedSections(data);
// Todo: rethink as every compliance has a different number of items
// const defaultKeys = accordionItems.slice(0, 2).map((item) => item.key);
const defaultKeys = [""];
return (
<div className="space-y-8">