From 4876d8435c267a89dfda851a2f31f746f7fe04ca Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:40:53 +0200 Subject: [PATCH] feat: generic compliance detailed view (#7990) --- ui/components/compliance/compliance-card.tsx | 8 +- .../generic-details.tsx | 67 ++++++ ui/lib/compliance/commons.ts | 15 +- ui/lib/compliance/generic.tsx | 193 ++++++++++++++++++ ui/types/compliance.ts | 13 +- 5 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 ui/components/compliance/compliance-custom-details/generic-details.tsx create mode 100644 ui/lib/compliance/generic.tsx diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 1c8b772475..ec50cde4c0 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -71,13 +71,7 @@ export const ComplianceCard: React.FC = ({ 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. diff --git a/ui/components/compliance/compliance-custom-details/generic-details.tsx b/ui/components/compliance/compliance-custom-details/generic-details.tsx new file mode 100644 index 0000000000..caa3fec05d --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/generic-details.tsx @@ -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 ( + + {requirement.description && ( + + {requirement.description} + + )} + + + {requirement.item_id && ( + + )} + + {requirement.service && ( + + )} + + {requirement.type && ( + + )} + + + {requirement.subsection && ( + + + {requirement.subsection as string} + + + )} + + {requirement.subgroup && ( + + + {requirement.subgroup as string} + + + )} + + ); +}; diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index 0ede59f1c4..286d6e6e78 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -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 = { }, }; -// 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 diff --git a/ui/lib/compliance/generic.tsx b/ui/lib/compliance/generic.tsx new file mode 100644 index 0000000000..b46188f693 --- /dev/null +++ b/ui/lib/compliance/generic.tsx @@ -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(); + 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: ( + + ), + 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: ( + + ), + content: ( + + ), + items: [], + }; + }), + }; + }), + ); +}; diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index c4f028331c..04c39877a8 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -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[]; }; };