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:
authored and GitHub committed 2025-06-03 09:20:52 +02:00
1 parent e34c18757d
commit 3e8c86d880
14 files changed
+506 -136

No files matched your search

@@ -14,7 +14,8 @@ import { createDict } from "@/lib";
import { ComplianceId, Requirement } from "@/types/compliance";
import { FindingProps, FindingsResponse } from "@/types/components";
import { ComplianceCustomDetails } from "../compliance-custom-details/ens-details";
import { ENSCustomDetails } from "../compliance-custom-details/ens-details";
import { ISOCustomDetails } from "../compliance-custom-details/iso-details";
interface ClientAccordionContentProps {
requirement: Requirement;
@@ -150,7 +151,14 @@ export const ClientAccordionContent = ({
case "ens_rd2022_aws":
return (
<div className="w-full">
<ComplianceCustomDetails requirement={requirement} />
<ENSCustomDetails requirement={requirement} />
</div>
);
case "iso27001_2013_aws":
case "iso27001_2022_aws":
return (
<div className="w-full">
<ISOCustomDetails requirement={requirement} />
</div>
);
default:
@@ -1,5 +1,4 @@
import { FindingStatus, StatusFindingBadge } from "@/components/ui/table";
import { translateType } from "@/lib/compliance/ens";
interface ComplianceAccordionRequirementTitleProps {
type: string;
@@ -8,16 +7,12 @@ interface ComplianceAccordionRequirementTitleProps {
}
export const ComplianceAccordionRequirementTitle = ({
type,
name,
status,
}: ComplianceAccordionRequirementTitleProps) => {
return (
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-3/4 items-center gap-1">
<span className="whitespace-nowrap text-sm font-bold capitalize">
{translateType(type)}:
</span>
<span className="whitespace-nowrap text-sm uppercase">{name}</span>
</div>
<StatusFindingBadge status={status} />
@@ -5,6 +5,7 @@ interface ComplianceAccordionTitleProps {
pass: number;
fail: number;
manual?: number;
isParentLevel?: boolean;
}
export const ComplianceAccordionTitle = ({
@@ -12,6 +13,7 @@ export const ComplianceAccordionTitle = ({
pass,
fail,
manual = 0,
isParentLevel = false,
}: ComplianceAccordionTitleProps) => {
const total = pass + fail + manual;
const passPercentage = (pass / total) * 100;
@@ -30,7 +32,7 @@ export const ComplianceAccordionTitle = ({
</div>
<div className="mr-4 flex items-center gap-2">
<div className="hidden lg:block">
{total > 0 && (
{total > 0 && isParentLevel && (
<span className="whitespace-nowrap text-xs font-medium text-gray-600">
Requirements:
</span>
+1 -1
View File
@@ -73,7 +73,7 @@ export const ComplianceCard: React.FC<ComplianceCardProps> = ({
const navigateToDetail = () => {
// We will unlock this while developing the rest of complainces.
if (!id.includes("ens") && !id.includes("cis")) {
if (!id.includes("ens") && !id.includes("iso")) {
return;
}
@@ -1,6 +1,7 @@
import { translateType } from "@/lib/compliance/ens";
import { Requirement } from "@/types/compliance";
export const ComplianceCustomDetails = ({
export const ENSCustomDetails = ({
requirement,
}: {
requirement: Requirement;
@@ -11,27 +12,35 @@ export const ComplianceCustomDetails = ({
{requirement.description}
</div>
<div className="flex flex-col gap-2 text-sm">
<div className="flex items-center gap-2">
<span className="font-medium">Type:</span>
<span className="capitalize">
{translateType(requirement.type as string)}
</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium">Level:</span>
<span className="capitalize">{requirement.nivel}</span>
</div>
{requirement.dimensiones && requirement.dimensiones.length > 0 && (
<div className="flex items-center gap-2">
<span className="font-medium">Dimensions:</span>
<div className="flex flex-wrap gap-1">
{requirement.dimensiones.map(
(dimension: string, index: number) => (
<span
key={index}
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
>
{dimension}
</span>
),
)}
{requirement.dimensiones &&
Array.isArray(requirement.dimensiones) &&
requirement.dimensiones.length > 0 && (
<div className="flex items-center gap-2">
<span className="font-medium">Dimensions:</span>
<div className="flex flex-wrap gap-1">
{requirement.dimensiones.map(
(dimension: string, index: number) => (
<span
key={index}
className="rounded-full bg-gray-100 px-2 py-0.5 text-xs capitalize dark:bg-prowler-blue-400"
>
{dimension}
</span>
),
)}
</div>
</div>
</div>
)}
)}
</div>
</div>
);
@@ -0,0 +1,23 @@
import { Requirement } from "@/types/compliance";
export const ISOCustomDetails = ({
requirement,
}: {
requirement: Requirement;
}) => {
return (
<div className="mb-4">
<div className="mb-2 text-sm text-gray-600">
{requirement.description}
</div>
<div className="flex flex-col gap-2 text-sm">
{requirement.objetive_name && (
<div className="flex items-center gap-2">
<span className="font-medium">Objective:</span>
<span>{requirement.objetive_name}</span>
</div>
)}
</div>
</div>
);
};
@@ -12,17 +12,10 @@ import {
} from "recharts";
import { translateType } from "@/lib/compliance/ens";
type FailedSectionItem = {
name: string;
total: number;
types: {
[key: string]: number;
};
};
import { FailedSection } from "@/types/compliance";
interface FailedSectionsListProps {
sections: FailedSectionItem[];
sections: FailedSection[];
}
const title = (
@@ -43,7 +36,7 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
case "refuerzo":
return "#7FB5FF"; // Increased contrast from #B5D7FF
default:
return "#868994";
return "#ff5356";
}
};
@@ -56,9 +49,28 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
}));
const allTypes = Array.from(
new Set(sections.flatMap((section) => Object.keys(section.types))),
new Set(sections.flatMap((section) => Object.keys(section.types || {}))),
);
// Add empty bars to complete up to 5 bars for better distribution
while (chartData.length < 5) {
const emptyBar: any = { name: "" };
allTypes.forEach((type) => {
emptyBar[type] = 0;
});
chartData.push(emptyBar);
}
// Calculate the maximum value to ensure proper scaling
const maxValue = Math.max(
...chartData.map((item) =>
allTypes.reduce((sum, type) => sum + ((item as any)[type] || 0), 0),
),
);
// Set minimum domain to ensure bars are always visible
const domainMax = Math.max(maxValue, 1);
// Check if there are no failed sections
if (!sections || sections.length === 0) {
return (
@@ -73,14 +85,14 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
return (
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
{title}
<div className="mt-4">{title}</div>
<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 30, left: 40, bottom: 5 }}
margin={{ top: 16, right: 30, left: 40, bottom: 5 }}
maxBarSize={40}
>
<XAxis
@@ -88,6 +100,9 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
fontSize={12}
axisLine={false}
tickLine={false}
allowDecimals={false}
hide={true}
domain={[0, domainMax]}
tick={{
fontSize: 12,
fill: theme === "dark" ? "#94a3b8" : "#374151",
@@ -105,34 +120,59 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
tickLine={false}
/>
<Tooltip
contentStyle={{
backgroundColor: theme === "dark" ? "#1e293b" : "white",
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
borderRadius: "6px",
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
fontSize: "12px",
padding: "8px 12px",
color: theme === "dark" ? "white" : "black",
content={(props) => {
if (!props.active || !props.payload || !props.payload.length) {
return null;
}
const data = props.payload[0].payload;
if (!data.name || data.name === "") {
return null;
}
const hasValues = allTypes.some((type) => data[type] > 0);
if (!hasValues) {
return null;
}
return (
<div
style={{
backgroundColor: theme === "dark" ? "#1e293b" : "white",
border: `1px solid ${theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)"}`,
borderRadius: "6px",
boxShadow: "0px 4px 12px rgba(0, 0, 0, 0.15)",
fontSize: "12px",
padding: "8px 12px",
color: theme === "dark" ? "white" : "black",
}}
>
{props.payload.map((entry: any, index: number) => (
<div key={index} style={{ color: entry.color }}>
{translateType(entry.dataKey)}: {entry.value}
</div>
))}
</div>
);
}}
formatter={(value: number, name: string) => [
value,
translateType(name),
]}
cursor={false}
/>
<Legend
formatter={(value) => translateType(value)}
wrapperStyle={{
fontSize: "10px",
display: "flex",
justifyContent: "center",
width: "100%",
}}
iconType="circle"
layout="horizontal"
verticalAlign="bottom"
height={36}
/>
{allTypes.length > 1 && (
<Legend
formatter={(value) => translateType(value)}
wrapperStyle={{
fontSize: "10px",
display: "flex",
justifyContent: "center",
width: "100%",
paddingTop: "16px",
marginBottom: "4px",
}}
iconType="circle"
layout="horizontal"
verticalAlign="bottom"
/>
)}
{allTypes.map((type, i) => (
<Bar
key={type}
+4 -4
View File
@@ -118,7 +118,7 @@ export const Accordion = ({
return (
<NextUIAccordion
className={cn("w-full", className)}
className={cn("w-full !px-0", className)}
variant={variant}
selectionMode={selectionMode}
selectedKeys={expandedKeys}
@@ -137,12 +137,12 @@ export const Accordion = ({
isDisabled={item.isDisabled}
indicator={<ChevronDown className="text-gray-500" />}
classNames={{
base: index === 0 || index === 1 ? "my-2" : "my-1",
base: index === 0 || index === 1 ? "my-1" : "my-1",
title: "text-sm font-medium max-w-full overflow-hidden truncate",
subtitle: "text-xs text-gray-500",
trigger:
"p-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center",
content: "p-2",
"py-2 px-2 rounded-lg data-[hover=true]:bg-gray-50 dark:data-[hover=true]:bg-gray-800/50 w-full flex items-center",
content: "px-0 py-1",
}}
>
<AccordionContent