From 2eaa37921d5e0efee5af111e5e5d0e062b368bfb Mon Sep 17 00:00:00 2001 From: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:29:34 +0200 Subject: [PATCH] feat: KISA detailed view (#7965) --- ui/CHANGELOG.md | 1 + .../compliance/[compliancetitle]/page.tsx | 16 +- .../client-accordion-content.tsx | 4 +- ui/components/compliance/compliance-card.tsx | 1 + .../compliance-charts/bar-chart.tsx | 61 +++-- .../compliance-charts/heatmap-chart.tsx | 16 +- .../compliance-custom-details/iso-details.tsx | 2 +- .../kisa-details.tsx | 93 ++++++++ .../skeletons/bar-chart-skeleton.tsx | 2 +- ui/lib/compliance/commons.ts | 12 + ui/lib/compliance/kisa.tsx | 214 ++++++++++++++++++ ui/types/compliance.ts | 13 +- 12 files changed, 400 insertions(+), 35 deletions(-) create mode 100644 ui/components/compliance/compliance-custom-details/kisa-details.tsx create mode 100644 ui/lib/compliance/kisa.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index cc5ccc7b3d..15fd774391 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Add compliance detail view: ISO [(#7897)](https://github.com/prowler-cloud/prowler/pull/7897) - Add compliance detail view: CIS [(#7913)](https://github.com/prowler-cloud/prowler/pull/7913) - Add compliance detail view: AWS Well-Architected Framework [(#7925)](https://github.com/prowler-cloud/prowler/pull/7925) +- Add compliance detail view: KISA [(#7965)](https://github.com/prowler-cloud/prowler/pull/7965) - Improve `Scan ID` filter by adding more context and enhancing the UI/UX. [(#7949)](https://github.com/prowler-cloud/prowler/pull/7949) ### 🔄 Changed diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 98a83d403b..f75c344039 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -1,6 +1,6 @@ import { Spacer } from "@nextui-org/react"; import Image from "next/image"; -import { Suspense } from "react"; +import React, { Suspense } from "react"; import { getComplianceAttributes, @@ -63,8 +63,18 @@ const ChartsWrapper = ({ logoPath?: string; }) => { return ( -
- {children} +
+ {children && + React.Children.toArray(children).map( + (child: React.ReactNode, index: number) => ( +
+ {child} +
+ ), + )}
); }; diff --git a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx index ae13888bdd..bbcba71f26 100644 --- a/ui/components/compliance/compliance-accordion/client-accordion-content.tsx +++ b/ui/components/compliance/compliance-accordion/client-accordion-content.tsx @@ -116,7 +116,7 @@ export const ClientAccordionContent = ({ return (
{renderDetails()} -

+

⚠️ This requirement has no checks; therefore, there are no findings.

@@ -165,7 +165,7 @@ export const ClientAccordionContent = ({ } return ( -
+
⚠️ There are no findings for this regions
); diff --git a/ui/components/compliance/compliance-card.tsx b/ui/components/compliance/compliance-card.tsx index 720c5acd77..0445e8690a 100644 --- a/ui/components/compliance/compliance-card.tsx +++ b/ui/components/compliance/compliance-card.tsx @@ -75,6 +75,7 @@ export const ComplianceCard: React.FC = ({ id.includes("ens") || id.includes("iso") || id.includes("cis_") || + id.includes("kisa") || id.includes("pillar"); const navigateToDetail = () => { diff --git a/ui/components/compliance/compliance-charts/bar-chart.tsx b/ui/components/compliance/compliance-charts/bar-chart.tsx index 5701075247..4e5fe95995 100644 --- a/ui/components/compliance/compliance-charts/bar-chart.tsx +++ b/ui/components/compliance/compliance-charts/bar-chart.tsx @@ -14,12 +14,36 @@ import { import { translateType } from "@/lib/compliance/ens"; import { FailedSection } from "@/types/compliance"; +const CustomYAxisTick = (props: any) => { + const { x, y, payload, theme } = props; + const text = payload.value; + const maxLength = 50; + + const truncatedText = + text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; + + return ( + + + {truncatedText} + + + ); +}; + interface FailedSectionsListProps { sections: FailedSection[]; } const title = ( -

+

Failed Sections (Top 5)

); @@ -74,26 +98,28 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => { // Check if there are no failed sections if (!sections || sections.length === 0) { return ( -
+
{title}
-

There are no failed sections

+

+ There are no failed sections +

); } return ( -
-
{title}
+
+ {title}
{ } axisLine={false} tickLine={false} /> @@ -153,8 +170,13 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => { }} > {props.payload.map((entry: any, index: number) => ( -
- {translateType(entry.dataKey)}: {entry.value} +
+

{data.name}

+

+ + {translateType(entry.dataKey)}: {entry.value} + +

))}
@@ -180,6 +202,7 @@ export const BarChart = ({ sections }: FailedSectionsListProps) => { width: "100%", paddingTop: "16px", marginBottom: "16px", + marginLeft: "56px", }} iconType="circle" layout="horizontal" diff --git a/ui/components/compliance/compliance-charts/heatmap-chart.tsx b/ui/components/compliance/compliance-charts/heatmap-chart.tsx index bf5474595f..3a78faf346 100644 --- a/ui/components/compliance/compliance-charts/heatmap-chart.tsx +++ b/ui/components/compliance/compliance-charts/heatmap-chart.tsx @@ -30,6 +30,12 @@ const capitalizeFirstLetter = (text: string): string => { ); }; +const title = ( +

+ Sections Failure Rate +

+); + export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { const { theme } = useTheme(); const [hoveredItem, setHoveredItem] = useState(null); @@ -45,9 +51,7 @@ export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { if (!categories.length || heatmapData.length === 0) { return (
-

- Sections Failure Rate -

+ {title}

No category data available

@@ -70,11 +74,7 @@ export const HeatmapChart = ({ categories = [] }: HeatmapChartProps) => { return (
-
-

- Sections Failure Rate -

-
+ {title}
{ return (
-
+
{requirement.description}
diff --git a/ui/components/compliance/compliance-custom-details/kisa-details.tsx b/ui/components/compliance/compliance-custom-details/kisa-details.tsx new file mode 100644 index 0000000000..8f0041be7f --- /dev/null +++ b/ui/components/compliance/compliance-custom-details/kisa-details.tsx @@ -0,0 +1,93 @@ +import { Requirement } from "@/types/compliance"; + +export const KISACustomDetails = ({ + requirement, +}: { + requirement: Requirement; +}) => { + const auditChecklist = requirement.audit_checklist as string[] | undefined; + const relatedRegulations = requirement.related_regulations as + | string[] + | undefined; + const auditEvidence = requirement.audit_evidence as string[] | undefined; + const nonComplianceCases = requirement.non_compliance_cases as + | string[] + | undefined; + + return ( +
+ {requirement.description && ( +
+

+ Description +

+

{requirement.description}

+
+ )} + + {auditChecklist && auditChecklist.length > 0 && ( +
+

+ Audit Checklist +

+
+ {auditChecklist.map((item: string, index: number) => ( +
+ +

{item}

+
+ ))} +
+
+ )} + + {relatedRegulations && relatedRegulations.length > 0 && ( +
+

+ Related Regulations +

+
+ {relatedRegulations.map((regulation: string, index: number) => ( +
+ +

{regulation}

+
+ ))} +
+
+ )} + + {auditEvidence && auditEvidence.length > 0 && ( +
+

+ Audit Evidence +

+
+ {auditEvidence.map((evidence: string, index: number) => ( +
+ +

{evidence}

+
+ ))} +
+
+ )} + + {nonComplianceCases && nonComplianceCases.length > 0 && ( +
+

+ Non-Compliance Cases +

+
+ {nonComplianceCases.map((caseItem: string, index: number) => ( +
+ +

{caseItem}

+
+ ))} +
+
+ )} +
+ ); +}; diff --git a/ui/components/compliance/skeletons/bar-chart-skeleton.tsx b/ui/components/compliance/skeletons/bar-chart-skeleton.tsx index 05f26ae938..2338818068 100644 --- a/ui/components/compliance/skeletons/bar-chart-skeleton.tsx +++ b/ui/components/compliance/skeletons/bar-chart-skeleton.tsx @@ -4,7 +4,7 @@ import { Skeleton } from "@nextui-org/react"; export const BarChartSkeleton = () => { return ( -
+
{/* Title skeleton */}
diff --git a/ui/lib/compliance/commons.ts b/ui/lib/compliance/commons.ts index c62933b635..49fb6bf932 100644 --- a/ui/lib/compliance/commons.ts +++ b/ui/lib/compliance/commons.ts @@ -4,6 +4,7 @@ import { AWSWellArchitectedCustomDetails } from "@/components/compliance/complia import { CISCustomDetails } from "@/components/compliance/compliance-custom-details/cis-details"; import { ENSCustomDetails } from "@/components/compliance/compliance-custom-details/ens-details"; import { ISOCustomDetails } from "@/components/compliance/compliance-custom-details/iso-details"; +import { KISACustomDetails } from "@/components/compliance/compliance-custom-details/kisa-details"; import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; import { AttributesData, @@ -30,6 +31,10 @@ import { mapComplianceData as mapISOComplianceData, toAccordionItems as toISOAccordionItems, } from "./iso"; +import { + mapComplianceData as mapKISAComplianceData, + toAccordionItems as toKISAAccordionItems, +} from "./kisa"; export interface ComplianceMapper { mapComplianceData: ( @@ -119,6 +124,13 @@ const complianceMappers: Record = { getDetailsComponent: (requirement: Requirement) => React.createElement(AWSWellArchitectedCustomDetails, { requirement }), }, + "KISA-ISMS-P": { + mapComplianceData: mapKISAComplianceData, + toAccordionItems: toKISAAccordionItems, + getTopFailedSections, + getDetailsComponent: (requirement: Requirement) => + React.createElement(KISACustomDetails, { requirement }), + }, }; // Default mapper (fallback to ENS for backward compatibility) diff --git a/ui/lib/compliance/kisa.tsx b/ui/lib/compliance/kisa.tsx new file mode 100644 index 0000000000..70545ead92 --- /dev/null +++ b/ui/lib/compliance/kisa.tsx @@ -0,0 +1,214 @@ +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, + KISAAttributesMetadata, + 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 KISAAttributesMetadata[]; + 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.Domain; // Level 1: Domain + const controlLabel = attrs.Subdomain; // Level 2: Subdomain + const sectionName = attrs.Section; // Level 3: Section + const description = attributeItem.attributes.description; + const status = requirementData.attributes.status || ""; + const checks = attributeItem.attributes.attributes.check_ids || []; + const requirementName = id; + + // 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 (Domain) + 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 (Subdomain) + 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 (Section) + 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, + section: sectionName, + audit_checklist: attrs.AuditChecklist, + related_regulations: attrs.RelatedRegulations, + audit_evidence: attrs.AuditEvidence, + non_compliance_cases: attrs.NonComplianceCases, + }; + + 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: ( + + ), + content: "", + items: category.controls.map((control, i: number) => { + return { + key: `${framework.name}-${category.name}-control-${i}`, + title: ( + + ), + content: "", + items: control.requirements.map((requirement, j: number) => { + const itemKey = `${framework.name}-${category.name}-control-${i}-req-${j}`; + + return { + key: itemKey, + title: ( + + ), + content: ( + + ), + items: [], + }; + }), + isDisabled: + control.pass === 0 && control.fail === 0 && control.manual === 0, + }; + }), + }; + }), + ); +}; diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 994c90d200..f6551ba07e 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -113,6 +113,16 @@ export interface AWSWellArchitectedAttributesMetadata { ImplementationGuidanceUrl: string; } +export interface KISAAttributesMetadata { + Domain: string; + Subdomain: string; + Section: string; + AuditChecklist: string[]; + RelatedRegulations: string[]; + AuditEvidence: string[]; + NonComplianceCases: string[]; +} + export interface AttributesItemData { type: "compliance-requirements-attributes"; id: string; @@ -125,7 +135,8 @@ export interface AttributesItemData { | ENSAttributesMetadata[] | ISO27001AttributesMetadata[] | CISAttributesMetadata[] - | AWSWellArchitectedAttributesMetadata[]; + | AWSWellArchitectedAttributesMetadata[] + | KISAAttributesMetadata[]; check_ids: string[]; }; };