feat: generic compliance detailed view (#7990)

This commit is contained in:
Alejandro Bailo
2025-06-11 09:40:53 +02:00
committed by GitHub
parent 1bd0d774e5
commit 4876d8435c
5 changed files with 286 additions and 10 deletions
+1 -7
View File
@@ -71,13 +71,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
return "success";
};
const isPressable =
id.includes("ens") ||
id.includes("iso") ||
id.includes("cis_") ||
id.includes("kisa") ||
id.includes("threat") ||
id.includes("pillar");
const isPressable = !id.includes("mitre");
const navigateToDetail = () => {
// We will unlock this while developing the rest of complainces.
@@ -0,0 +1,67 @@
import { Requirement } from "@/types/compliance";
import {
ComplianceBadge,
ComplianceBadgeContainer,
ComplianceDetailContainer,
ComplianceDetailSection,
ComplianceDetailText,
} from "./shared-components";
export const GenericCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<ComplianceDetailContainer>
{requirement.description && (
<ComplianceDetailSection title="Description">
<ComplianceDetailText>{requirement.description}</ComplianceDetailText>
</ComplianceDetailSection>
)}
<ComplianceBadgeContainer>
{requirement.item_id && (
<ComplianceBadge
label="Item ID"
value={requirement.item_id as string}
color="indigo"
/>
)}
{requirement.service && (
<ComplianceBadge
label="Service"
value={requirement.service as string}
color="blue"
/>
)}
{requirement.type && (
<ComplianceBadge
label="Type"
value={requirement.type as string}
color="orange"
/>
)}
</ComplianceBadgeContainer>
{requirement.subsection && (
<ComplianceDetailSection title="SubSection">
<ComplianceDetailText>
{requirement.subsection as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
{requirement.subgroup && (
<ComplianceDetailSection title="SubGroup">
<ComplianceDetailText>
{requirement.subgroup as string}
</ComplianceDetailText>
</ComplianceDetailSection>
)}
</ComplianceDetailContainer>
);
};
+13 -2
View File
@@ -3,6 +3,7 @@ import React from "react";
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details";
import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details";
import { GenericCustomDetails } from "@/components/compliance/compliance-custom-details/generic-details";
import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details";
import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details";
import { ThreatCustomDetails } from "@/components/compliance/compliance-custom-details/threat-details";
@@ -28,6 +29,10 @@ import {
mapComplianceData as mapENSComplianceData,
toAccordionItems as toENSAccordionItems,
} from "./ens";
import {
mapComplianceData as mapGenericComplianceData,
toAccordionItems as toGenericAccordionItems,
} from "./generic";
import {
mapComplianceData as mapISOComplianceData,
toAccordionItems as toISOAccordionItems,
@@ -145,8 +150,14 @@ const complianceMappers: Record<string, ComplianceMapper> = {
},
};
// Default mapper (fallback to ENS for backward compatibility)
const defaultMapper: ComplianceMapper = complianceMappers.ENS;
// Default mapper (fallback to generic for maximum compatibility)
const defaultMapper: ComplianceMapper = {
mapComplianceData: mapGenericComplianceData,
toAccordionItems: toGenericAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(GenericCustomDetails, { requirement }),
};
/**
* Get the appropriate compliance mapper based on the framework name
+193
View File
@@ -0,0 +1,193 @@
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,
GenericAttributesMetadata,
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 GenericAttributesMetadata[];
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 sectionName = attrs.Section; // Level 1: Section -> Category
const requirementName = attributeItem.attributes.name || id; // Level 2: name -> Control
const description = attributeItem.attributes.description;
const status = requirementData.attributes.status || "";
const checks = attributeItem.attributes.attributes.check_ids || [];
if (!sectionName) {
continue;
}
// 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 (Section)
let category = framework.categories.find((c) => c.name === sectionName);
if (!category) {
category = {
name: sectionName,
pass: 0,
fail: 0,
manual: 0,
controls: [],
};
framework.categories.push(category);
}
// Create a control for this requirement (each requirement is its own control in this generic approach)
const control = {
label: requirementName,
pass: 0,
fail: 0,
manual: 0,
requirements: [] as Requirement[],
};
// 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,
item_id: attrs.ItemId,
subsection: attrs.SubSection,
subgroup: attrs.SubGroup || undefined,
service: attrs.Service || undefined,
type: attrs.Type || undefined,
};
control.requirements.push(requirement);
// Update control counters
if (requirement.status === "MANUAL") {
control.manual++;
} else if (requirement.status === "PASS") {
control.pass++;
} else if (requirement.status === "FAIL") {
control.fail++;
}
category.controls.push(control);
}
// Calculate counters for categories and frameworks
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) => {
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) => {
const requirement = control.requirements[0]; // Each control has one requirement
const itemKey = `${framework.name}-${category.name}-control-${i}`;
return {
key: itemKey,
title: (
<ComplianceAccordionRequirementTitle
type=""
name={control.label}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
items: [],
};
}),
};
}),
);
};
+12 -1
View File
@@ -133,10 +133,20 @@ export interface KISAAttributesMetadata {
NonComplianceCases: string[];
}
export interface GenericAttributesMetadata {
ItemId: string;
Section: string;
SubSection: string;
SubGroup: string | null;
Service: string | null;
Type: string | null;
}
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
attributes: {
name?: string;
framework: string;
version: string;
description: string;
@@ -147,7 +157,8 @@ export interface AttributesItemData {
| CISAttributesMetadata[]
| AWSWellArchitectedAttributesMetadata[]
| ThreatAttributesMetadata[]
| KISAAttributesMetadata[];
| KISAAttributesMetadata[]
| GenericAttributesMetadata[];
check_ids: string[];
};
};