mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-31 07:44:21 +00:00
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:
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:
|
||||
|
||||
-5
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user