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
+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,
};
}),
};
}),
);
};