mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 04:21:52 +00:00
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:
@@ -10,6 +10,8 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Improved `SnippetChip` component and show resource name in new findings table. [(#7813)](https://github.com/prowler-cloud/prowler/pull/7813)
|
||||
- Possibility to edit the organization name. [(#7829)](https://github.com/prowler-cloud/prowler/pull/7829)
|
||||
- Add GCP credential method (Account Service Key). [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
|
||||
- Add compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853)
|
||||
- Add compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897)
|
||||
|
||||
### 🔄 Changed
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -14,7 +14,8 @@ import { createDict } from "@/lib";
|
||||
import { ComplianceId, Requirement } from "@/types/compliance";
|
||||
import { FindingProps, FindingsResponse } from "@/types/components";
|
||||
|
||||
import { ComplianceCustomDetails } from "../compliance-custom-details/ens-details";
|
||||
import { ENSCustomDetails } from "../compliance-custom-details/ens-details";
|
||||
import { ISOCustomDetails } from "../compliance-custom-details/iso-details";
|
||||
|
||||
interface ClientAccordionContentProps {
|
||||
requirement: Requirement;
|
||||
@@ -150,7 +151,14 @@ export const ClientAccordionContent = ({
|
||||
case "ens_rd2022_aws":
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ComplianceCustomDetails requirement={requirement} />
|
||||
<ENSCustomDetails requirement={requirement} />
|
||||
</div>
|
||||
);
|
||||
case "iso27001_2013_aws":
|
||||
case "iso27001_2022_aws":
|
||||
return (
|
||||
<div className="w-full">
|
||||
<ISOCustomDetails requirement={requirement} />
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
|
||||
-5
@@ -1,5 +1,4 @@
|
||||
import { FindingStatus, StatusFindingBadge } from "@/components/ui/table";
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
|
||||
interface ComplianceAccordionRequirementTitleProps {
|
||||
type: string;
|
||||
@@ -8,16 +7,12 @@ interface ComplianceAccordionRequirementTitleProps {
|
||||
}
|
||||
|
||||
export const ComplianceAccordionRequirementTitle = ({
|
||||
type,
|
||||
name,
|
||||
status,
|
||||
}: 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 font-bold capitalize">
|
||||
{translateType(type)}:
|
||||
</span>
|
||||
<span className="whitespace-nowrap text-sm uppercase">{name}</span>
|
||||
</div>
|
||||
<StatusFindingBadge status={status} />
|
||||
|
||||
@@ -5,6 +5,7 @@ interface ComplianceAccordionTitleProps {
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual?: number;
|
||||
isParentLevel?: boolean;
|
||||
}
|
||||
|
||||
export const ComplianceAccordionTitle = ({
|
||||
@@ -12,6 +13,7 @@ export const ComplianceAccordionTitle = ({
|
||||
pass,
|
||||
fail,
|
||||
manual = 0,
|
||||
isParentLevel = false,
|
||||
}: ComplianceAccordionTitleProps) => {
|
||||
const total = pass + fail + manual;
|
||||
const passPercentage = (pass / total) * 100;
|
||||
@@ -30,7 +32,7 @@ export const ComplianceAccordionTitle = ({
|
||||
</div>
|
||||
<div className="mr-4 flex items-center gap-2">
|
||||
<div className="hidden lg:block">
|
||||
{total > 0 && (
|
||||
{total > 0 && isParentLevel && (
|
||||
<span className="whitespace-nowrap text-xs font-medium text-gray-600">
|
||||
Requirements:
|
||||
</span>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
|
||||
|
||||
const navigateToDetail = () => {
|
||||
// We will unlock this while developing the rest of complainces.
|
||||
if (!id.includes("ens") && !id.includes("cis")) {
|
||||
if (!id.includes("ens") && !id.includes("iso")) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
export const ComplianceCustomDetails = ({
|
||||
export const ENSCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
@@ -11,27 +12,35 @@ export const ComplianceCustomDetails = ({
|
||||
{requirement.description}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Type:</span>
|
||||
<span className="capitalize">
|
||||
{translateType(requirement.type as string)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Level:</span>
|
||||
<span className="capitalize">{requirement.nivel}</span>
|
||||
</div>
|
||||
{requirement.dimensiones && requirement.dimensiones.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Dimensions:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{requirement.dimensiones.map(
|
||||
(dimension: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
|
||||
>
|
||||
{dimension}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
{requirement.dimensiones &&
|
||||
Array.isArray(requirement.dimensiones) &&
|
||||
requirement.dimensiones.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Dimensions:</span>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{requirement.dimensiones.map(
|
||||
(dimension: string, index: number) => (
|
||||
<span
|
||||
key={index}
|
||||
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
|
||||
>
|
||||
{dimension}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Requirement } from "@/types/compliance";
|
||||
|
||||
export const ISOCustomDetails = ({
|
||||
requirement,
|
||||
}: {
|
||||
requirement: Requirement;
|
||||
}) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<div className="mb-2 text-sm text-gray-600">
|
||||
{requirement.description}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 text-sm">
|
||||
{requirement.objetive_name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">Objective:</span>
|
||||
<span>{requirement.objetive_name}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,17 +12,10 @@ import {
|
||||
} from "recharts";
|
||||
|
||||
import { translateType } from "@/lib/compliance/ens";
|
||||
|
||||
type FailedSectionItem = {
|
||||
name: string;
|
||||
total: number;
|
||||
types: {
|
||||
[key: string]: number;
|
||||
};
|
||||
};
|
||||
import { FailedSection } from "@/types/compliance";
|
||||
|
||||
interface FailedSectionsListProps {
|
||||
sections: FailedSectionItem[];
|
||||
sections: FailedSection[];
|
||||
}
|
||||
|
||||
const title = (
|
||||
@@ -43,7 +36,7 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
case "refuerzo":
|
||||
return "#7FB5FF"; // Increased contrast from #B5D7FF
|
||||
default:
|
||||
return "#868994";
|
||||
return "#ff5356";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -56,9 +49,28 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
}));
|
||||
|
||||
const allTypes = Array.from(
|
||||
new Set(sections.flatMap((section) => Object.keys(section.types))),
|
||||
new Set(sections.flatMap((section) => Object.keys(section.types || {}))),
|
||||
);
|
||||
|
||||
// Add empty bars to complete up to 5 bars for better distribution
|
||||
while (chartData.length < 5) {
|
||||
const emptyBar: any = { name: "" };
|
||||
allTypes.forEach((type) => {
|
||||
emptyBar[type] = 0;
|
||||
});
|
||||
chartData.push(emptyBar);
|
||||
}
|
||||
|
||||
// Calculate the maximum value to ensure proper scaling
|
||||
const maxValue = Math.max(
|
||||
...chartData.map((item) =>
|
||||
allTypes.reduce((sum, type) => sum + ((item as any)[type] || 0), 0),
|
||||
),
|
||||
);
|
||||
|
||||
// Set minimum domain to ensure bars are always visible
|
||||
const domainMax = Math.max(maxValue, 1);
|
||||
|
||||
// Check if there are no failed sections
|
||||
if (!sections || sections.length === 0) {
|
||||
return (
|
||||
@@ -73,14 +85,14 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
|
||||
return (
|
||||
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
|
||||
{title}
|
||||
<div className="mt-4">{title}</div>
|
||||
|
||||
<div className="h-[320px] w-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart
|
||||
data={chartData}
|
||||
layout="vertical"
|
||||
margin={{ top: 5, right: 30, left: 40, bottom: 5 }}
|
||||
margin={{ top: 16, right: 30, left: 40, bottom: 5 }}
|
||||
maxBarSize={40}
|
||||
>
|
||||
<XAxis
|
||||
@@ -88,6 +100,9 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
fontSize={12}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
allowDecimals={false}
|
||||
hide={true}
|
||||
domain={[0, domainMax]}
|
||||
tick={{
|
||||
fontSize: 12,
|
||||
fill: theme === "dark" ? "#94a3b8" : "#374151",
|
||||
@@ -105,34 +120,59 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
|
||||
tickLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: theme === "dark" ? "#1e293b" : "white",
|
||||
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
padding: "8px 12px",
|
||||
color: theme === "dark" ? "white" : "black",
|
||||
content={(props) => {
|
||||
if (!props.active || !props.payload || !props.payload.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = props.payload[0].payload;
|
||||
if (!data.name || data.name === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasValues = allTypes.some((type) => data[type] > 0);
|
||||
if (!hasValues) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: theme === "dark" ? "#1e293b" : "white",
|
||||
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
|
||||
borderRadius: "6px",
|
||||
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
|
||||
fontSize: "12px",
|
||||
padding: "8px 12px",
|
||||
color: theme === "dark" ? "white" : "black",
|
||||
}}
|
||||
>
|
||||
{props.payload.map((entry: any, index: number) => (
|
||||
<div key={index} style={{ color: entry.color }}>
|
||||
{translateType(entry.dataKey)}: {entry.value}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
formatter={(value: number, name: string) => [
|
||||
value,
|
||||
translateType(name),
|
||||
]}
|
||||
cursor={false}
|
||||
/>
|
||||
<Legend
|
||||
formatter={(value) => translateType(value)}
|
||||
wrapperStyle={{
|
||||
fontSize: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
}}
|
||||
iconType="circle"
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
height={36}
|
||||
/>
|
||||
{allTypes.length > 1 && (
|
||||
<Legend
|
||||
formatter={(value) => translateType(value)}
|
||||
wrapperStyle={{
|
||||
fontSize: "10px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
width: "100%",
|
||||
paddingTop: "16px",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
iconType="circle"
|
||||
layout="horizontal"
|
||||
verticalAlign="bottom"
|
||||
/>
|
||||
)}
|
||||
{allTypes.map((type, i) => (
|
||||
<Bar
|
||||
key={type}
|
||||
|
||||
@@ -118,7 +118,7 @@ export const Accordion = ({
|
||||
|
||||
return (
|
||||
<NextUIAccordion
|
||||
className={cn("w-full", className)}
|
||||
className={cn("w-full !px-0", className)}
|
||||
variant={variant}
|
||||
selectionMode={selectionMode}
|
||||
selectedKeys={expandedKeys}
|
||||
@@ -137,12 +137,12 @@ export const Accordion = ({
|
||||
isDisabled={item.isDisabled}
|
||||
indicator={<ChevronDown className="text-gray-500" />}
|
||||
classNames={{
|
||||
base: index === 0 || index === 1 ? "my-2" : "my-1",
|
||||
base: index === 0 || index === 1 ? "my-1" : "my-1",
|
||||
title: "text-sm font-medium max-w-full overflow-hidden truncate",
|
||||
subtitle: "text-xs text-gray-500",
|
||||
trigger:
|
||||
"p-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center",
|
||||
content: "p-2",
|
||||
"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",
|
||||
content: "px-0 py-1",
|
||||
}}
|
||||
>
|
||||
<AccordionContent
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import {
|
||||
AttributesData,
|
||||
FailedSection,
|
||||
Framework,
|
||||
RequirementsData,
|
||||
} from "@/types/compliance";
|
||||
|
||||
import {
|
||||
mapComplianceData as mapENSComplianceData,
|
||||
toAccordionItems as toENSAccordionItems,
|
||||
} from "./ens";
|
||||
import {
|
||||
mapComplianceData as mapISOComplianceData,
|
||||
toAccordionItems as toISOAccordionItems,
|
||||
} from "./iso";
|
||||
|
||||
export interface ComplianceMapper {
|
||||
mapComplianceData: (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
) => Framework[];
|
||||
toAccordionItems: (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
) => AccordionItemProps[];
|
||||
getTopFailedSections: (mappedData: Framework[]) => FailedSection[];
|
||||
}
|
||||
|
||||
// Common function for getting top failed sections
|
||||
export const getTopFailedSections = (
|
||||
mappedData: Framework[],
|
||||
): 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 || "Fails";
|
||||
|
||||
sectionData.types[type as string] =
|
||||
(sectionData.types[type as string] || 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
|
||||
};
|
||||
|
||||
// Registry of compliance mappers
|
||||
const complianceMappers: Record<string, ComplianceMapper> = {
|
||||
ENS: {
|
||||
mapComplianceData: mapENSComplianceData,
|
||||
toAccordionItems: toENSAccordionItems,
|
||||
getTopFailedSections,
|
||||
},
|
||||
ISO27001: {
|
||||
mapComplianceData: mapISOComplianceData,
|
||||
toAccordionItems: toISOAccordionItems,
|
||||
getTopFailedSections,
|
||||
},
|
||||
};
|
||||
|
||||
// Default mapper (fallback to ENS for backward compatibility)
|
||||
const defaultMapper: ComplianceMapper = complianceMappers.ENS;
|
||||
|
||||
/**
|
||||
* Get the appropriate compliance mapper based on the framework name
|
||||
* @param framework - The framework name (e.g., "ENS", "ISO27001")
|
||||
* @returns ComplianceMapper object with specific functions for the framework
|
||||
*/
|
||||
export const getComplianceMapper = (framework?: string): ComplianceMapper => {
|
||||
if (!framework) {
|
||||
return defaultMapper;
|
||||
}
|
||||
|
||||
return complianceMappers[framework] || defaultMapper;
|
||||
};
|
||||
@@ -5,8 +5,8 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
ENSAttributesMetadata,
|
||||
Framework,
|
||||
MappedComplianceData,
|
||||
Requirement,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
@@ -31,7 +31,7 @@ export const translateType = (type: string) => {
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): MappedComplianceData => {
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirements = requirementsData?.data || [];
|
||||
|
||||
@@ -46,7 +46,9 @@ export const mapComplianceData = (
|
||||
// Process attributes and merge with requirements data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const attrs = attributeItem.attributes?.attributes?.metadata?.[0];
|
||||
const attrs = attributeItem.attributes?.attributes
|
||||
?.metadata?.[0] as ENSAttributesMetadata;
|
||||
|
||||
if (!attrs) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
@@ -96,7 +98,6 @@ export const mapComplianceData = (
|
||||
if (!control) {
|
||||
control = {
|
||||
label: groupControlLabel,
|
||||
type,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
@@ -166,7 +167,7 @@ export const mapComplianceData = (
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: MappedComplianceData,
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.map((framework) => {
|
||||
@@ -178,6 +179,7 @@ export const toAccordionItems = (
|
||||
pass={framework.pass}
|
||||
fail={framework.fail}
|
||||
manual={framework.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
@@ -212,7 +214,7 @@ export const toAccordionItems = (
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type={requirement.type}
|
||||
type={requirement.type as string}
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { ClientAccordionContent } from "@/components/compliance/compliance-accordion/client-accordion-content";
|
||||
import { ComplianceAccordionRequirementTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-requeriment-title";
|
||||
import { ComplianceAccordionTitle } from "@/components/compliance/compliance-accordion/compliance-accordion-title";
|
||||
import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
|
||||
import { FindingStatus } from "@/components/ui/table/status-finding-badge";
|
||||
import {
|
||||
AttributesData,
|
||||
Framework,
|
||||
ISO27001AttributesMetadata,
|
||||
Requirement,
|
||||
RequirementItemData,
|
||||
RequirementsData,
|
||||
RequirementStatus,
|
||||
} from "@/types/compliance";
|
||||
|
||||
export const mapComplianceData = (
|
||||
attributesData: AttributesData,
|
||||
requirementsData: RequirementsData,
|
||||
): Framework[] => {
|
||||
const attributes = attributesData?.data || [];
|
||||
const requirements = requirementsData?.data || [];
|
||||
|
||||
// Create a map for quick lookup of requirements by id
|
||||
const requirementsMap = new Map<string, RequirementItemData>();
|
||||
requirements.forEach((req: RequirementItemData) => {
|
||||
requirementsMap.set(req.id, req);
|
||||
});
|
||||
|
||||
const frameworks: Framework[] = [];
|
||||
|
||||
// Process attributes and merge with requirements data
|
||||
for (const attributeItem of attributes) {
|
||||
const id = attributeItem.id;
|
||||
const metadataArray = attributeItem.attributes?.attributes
|
||||
?.metadata as unknown as ISO27001AttributesMetadata[];
|
||||
const attrs = metadataArray?.[0];
|
||||
if (!attrs) continue;
|
||||
|
||||
// Get corresponding requirement data
|
||||
const requirementData = requirementsMap.get(id);
|
||||
if (!requirementData) continue;
|
||||
|
||||
const frameworkName = attributeItem.attributes.framework;
|
||||
const categoryName = attrs.Category;
|
||||
const controlLabel = `${attrs.Objetive_ID} - ${attrs.Objetive_Name}`;
|
||||
const description = attributeItem.attributes.description;
|
||||
const status = requirementData.attributes.status || "";
|
||||
const checks = attributeItem.attributes.attributes.check_ids || [];
|
||||
const requirementName = id;
|
||||
const objetiveName = attrs.Objetive_Name;
|
||||
const checkSummary = attrs.Check_Summary;
|
||||
|
||||
// Find or create framework
|
||||
let framework = frameworks.find((f) => f.name === frameworkName);
|
||||
if (!framework) {
|
||||
framework = {
|
||||
name: frameworkName,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
categories: [],
|
||||
};
|
||||
frameworks.push(framework);
|
||||
}
|
||||
|
||||
// Find or create category
|
||||
let category = framework.categories.find((c) => c.name === categoryName);
|
||||
if (!category) {
|
||||
category = {
|
||||
name: categoryName,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
controls: [],
|
||||
};
|
||||
framework.categories.push(category);
|
||||
}
|
||||
|
||||
// Find or create control
|
||||
let control = category.controls.find((c) => c.label === controlLabel);
|
||||
if (!control) {
|
||||
control = {
|
||||
label: controlLabel,
|
||||
pass: 0,
|
||||
fail: 0,
|
||||
manual: 0,
|
||||
requirements: [],
|
||||
};
|
||||
category.controls.push(control);
|
||||
}
|
||||
|
||||
// Create requirement
|
||||
const finalStatus: RequirementStatus = status as RequirementStatus;
|
||||
const requirement: Requirement = {
|
||||
name: requirementName,
|
||||
description: description,
|
||||
status: finalStatus,
|
||||
check_ids: checks,
|
||||
pass: finalStatus === "PASS" ? 1 : 0,
|
||||
fail: finalStatus === "FAIL" ? 1 : 0,
|
||||
manual: finalStatus === "MANUAL" ? 1 : 0,
|
||||
objetive_name: objetiveName,
|
||||
check_summary: checkSummary,
|
||||
};
|
||||
|
||||
control.requirements.push(requirement);
|
||||
}
|
||||
|
||||
// Calculate counters
|
||||
frameworks.forEach((framework) => {
|
||||
framework.pass = 0;
|
||||
framework.fail = 0;
|
||||
framework.manual = 0;
|
||||
|
||||
framework.categories.forEach((category) => {
|
||||
category.pass = 0;
|
||||
category.fail = 0;
|
||||
category.manual = 0;
|
||||
|
||||
category.controls.forEach((control) => {
|
||||
control.pass = 0;
|
||||
control.fail = 0;
|
||||
control.manual = 0;
|
||||
|
||||
control.requirements.forEach((requirement) => {
|
||||
if (requirement.status === "MANUAL") {
|
||||
control.manual++;
|
||||
} else if (requirement.status === "PASS") {
|
||||
control.pass++;
|
||||
} else if (requirement.status === "FAIL") {
|
||||
control.fail++;
|
||||
}
|
||||
});
|
||||
|
||||
category.pass += control.pass;
|
||||
category.fail += control.fail;
|
||||
category.manual += control.manual;
|
||||
});
|
||||
|
||||
framework.pass += category.pass;
|
||||
framework.fail += category.fail;
|
||||
framework.manual += category.manual;
|
||||
});
|
||||
});
|
||||
|
||||
return frameworks;
|
||||
};
|
||||
|
||||
export const toAccordionItems = (
|
||||
data: Framework[],
|
||||
scanId: string | undefined,
|
||||
): AccordionItemProps[] => {
|
||||
return data.flatMap((framework) =>
|
||||
framework.categories.map((category) => {
|
||||
return {
|
||||
key: `${framework.name}-${category.name}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={category.name}
|
||||
pass={category.pass}
|
||||
fail={category.fail}
|
||||
manual={category.manual}
|
||||
isParentLevel={true}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: category.controls.map((control, i: number) => {
|
||||
return {
|
||||
key: `${framework.name}-${category.name}-control-${i}`,
|
||||
title: (
|
||||
<ComplianceAccordionTitle
|
||||
label={control.label}
|
||||
pass={control.pass}
|
||||
fail={control.fail}
|
||||
manual={control.manual}
|
||||
/>
|
||||
),
|
||||
content: "",
|
||||
items: control.requirements.map((requirement, j: number) => {
|
||||
const itemKey = `${framework.name}-${category.name}-control-${i}-req-${j}`;
|
||||
|
||||
return {
|
||||
key: itemKey,
|
||||
title: (
|
||||
<ComplianceAccordionRequirementTitle
|
||||
type=""
|
||||
name={requirement.name}
|
||||
status={requirement.status as FindingStatus}
|
||||
/>
|
||||
),
|
||||
content: (
|
||||
<ClientAccordionContent
|
||||
requirement={requirement}
|
||||
scanId={scanId || ""}
|
||||
/>
|
||||
),
|
||||
items: [],
|
||||
isDisabled:
|
||||
requirement.check_ids.length === 0 &&
|
||||
requirement.manual === 0,
|
||||
};
|
||||
}),
|
||||
isDisabled:
|
||||
control.pass === 0 && control.fail === 0 && control.manual === 0,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
+17
-11
@@ -1,6 +1,9 @@
|
||||
export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings";
|
||||
|
||||
export type ComplianceId = "ens_rd2022_aws";
|
||||
export type ComplianceId =
|
||||
| "ens_rd2022_aws"
|
||||
| "iso27001_2013_aws"
|
||||
| "iso27001_2022_aws";
|
||||
|
||||
export interface CompliancesOverview {
|
||||
data: ComplianceOverviewData[];
|
||||
@@ -23,19 +26,17 @@ export interface Requirement {
|
||||
name: string;
|
||||
description: string;
|
||||
status: RequirementStatus;
|
||||
type: string;
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
check_ids: string[];
|
||||
// ENS
|
||||
nivel?: string;
|
||||
dimensiones?: string[];
|
||||
// This is to allow any key to be added to the requirement object
|
||||
// because each compliance has different keys
|
||||
[key: string]: string | string[] | number | undefined;
|
||||
}
|
||||
|
||||
export interface Control {
|
||||
label: string;
|
||||
type: string;
|
||||
pass: number;
|
||||
fail: number;
|
||||
manual: number;
|
||||
@@ -58,12 +59,10 @@ export interface Framework {
|
||||
categories: Category[];
|
||||
}
|
||||
|
||||
export type MappedComplianceData = Framework[];
|
||||
|
||||
export interface FailedSection {
|
||||
name: string;
|
||||
total: number;
|
||||
types: { [key: string]: number };
|
||||
types?: { [key: string]: number };
|
||||
}
|
||||
|
||||
export interface RequirementsTotals {
|
||||
@@ -73,7 +72,7 @@ export interface RequirementsTotals {
|
||||
}
|
||||
|
||||
// API Responses types:
|
||||
export interface AttributesMetadata {
|
||||
export interface ENSAttributesMetadata {
|
||||
IdGrupoControl: string;
|
||||
Marco: string;
|
||||
Categoria: string;
|
||||
@@ -85,6 +84,13 @@ export interface AttributesMetadata {
|
||||
Dependencias: any[];
|
||||
}
|
||||
|
||||
export interface ISO27001AttributesMetadata {
|
||||
Category: string;
|
||||
Objetive_ID: string;
|
||||
Objetive_Name: string;
|
||||
Check_Summary: string;
|
||||
}
|
||||
|
||||
export interface AttributesItemData {
|
||||
type: "compliance-requirements-attributes";
|
||||
id: string;
|
||||
@@ -93,7 +99,7 @@ export interface AttributesItemData {
|
||||
version: string;
|
||||
description: string;
|
||||
attributes: {
|
||||
metadata: AttributesMetadata[];
|
||||
metadata: ENSAttributesMetadata[] | ISO27001AttributesMetadata[];
|
||||
check_ids: string[];
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user