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
parent e34c18757d
commit 3e8c86d880
14 changed files with 506 additions and 136 deletions
+2
View File
@@ -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:
@@ -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>
+1 -1
View File
@@ -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}
+4 -4
View File
@@ -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
+95
View File
@@ -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;
};
+8 -6
View File
@@ -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}
/>
+210
View File
@@ -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
View File
@@ -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[];
};
};