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) => (
+
+ ))}
+
+
+ )}
+
+ {relatedRegulations && relatedRegulations.length > 0 && (
+
+
+ Related Regulations
+
+
+ {relatedRegulations.map((regulation: string, index: number) => (
+
+ ))}
+
+
+ )}
+
+ {auditEvidence && auditEvidence.length > 0 && (
+
+
+ Audit Evidence
+
+
+ {auditEvidence.map((evidence: string, index: number) => (
+
+ ))}
+
+
+ )}
+
+ {nonComplianceCases && nonComplianceCases.length > 0 && (
+
+
+ Non-Compliance Cases
+
+
+ {nonComplianceCases.map((caseItem: string, index: number) => (
+
+ ))}
+
+
+ )}
+
+ );
+};
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[];
};
};