feat: CIS compliance detail view (#7913)

Co-authored-by: Víctor Fernández Poyatos <victor@prowler.com>
This commit is contained in:
Alejandro Bailo
2025-06-03 15:47:46 +02:00
committed by GitHub
parent 9ee78fe65f
commit 49e501c4be
28 changed files with 2336 additions and 211 deletions
+1
View File
@@ -12,6 +12,7 @@ All notable changes to the **Prowler UI** are documented in this file.
- Add GCP credential method (Account Service Key). [(#7872)](https://github.com/prowler-cloud/prowler/pull/7872)
- Add compliance detail view: ENS [(#7853)](https://github.com/prowler-cloud/prowler/pull/7853)
- 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)
### 🔄 Changed
@@ -9,16 +9,24 @@ import {
} from "@/actions/compliances";
import { getProvider } from "@/actions/providers";
import { getScans } from "@/actions/scans";
import { ClientAccordionWrapper } from "@/components/compliance/compliance-accordion/client-accordion-wrapper";
import { ComplianceHeader } from "@/components/compliance/compliance-header/compliance-header";
import { SkeletonAccordion } from "@/components/compliance/compliance-skeleton-accordion";
import { FailedSectionsChart } from "@/components/compliance/failed-sections-chart";
import { FailedSectionsChartSkeleton } from "@/components/compliance/failed-sections-chart-skeleton";
import { RequirementsChart } from "@/components/compliance/requirements-chart";
import { RequirementsChartSkeleton } from "@/components/compliance/requirements-chart-skeleton";
import {
BarChart,
BarChartSkeleton,
ClientAccordionWrapper,
ComplianceHeader,
HeatmapChart,
HeatmapChartSkeleton,
PieChart,
PieChartSkeleton,
SkeletonAccordion,
} from "@/components/compliance";
import { getComplianceIcon } from "@/components/icons/compliance/IconCompliance";
import { ContentLayout } from "@/components/ui";
import { getComplianceMapper } from "@/lib/compliance/commons";
import {
calculateCategoryHeatmapData,
calculateRegionHeatmapData,
getComplianceMapper,
} from "@/lib/compliance/commons";
import { ScanProps } from "@/types";
import { Framework, RequirementsTotals } from "@/types/compliance";
@@ -27,17 +35,23 @@ interface ComplianceDetailSearchParams {
version?: string;
scanId?: string;
"filter[region__in]"?: string;
"filter[cis_profile_level]"?: string;
}
const ComplianceLogo = ({ logoPath }: { logoPath: string }) => {
const ComplianceIconSmall = ({
logoPath,
title,
}: {
logoPath: string;
title: string;
}) => {
return (
<div className="relative ml-auto hidden h-[200px] w-[200px] flex-shrink-0 md:block">
<div className="relative h-6 w-6 flex-shrink-0">
<Image
src={logoPath}
alt="Compliance Logo"
alt={`${title} logo`}
fill
priority
className="object-contain"
className="h-10 w-10 min-w-10 rounded-md border-1 border-gray-300 bg-white object-contain p-[2px]"
/>
</div>
);
@@ -45,17 +59,13 @@ const ComplianceLogo = ({ logoPath }: { logoPath: string }) => {
const ChartsWrapper = ({
children,
logoPath,
}: {
children: React.ReactNode;
logoPath?: string;
}) => {
return (
<div className="mb-8 flex w-full">
<div className="flex flex-col items-center gap-16 lg:flex-row">
{children}
</div>
{logoPath && <ComplianceLogo logoPath={logoPath} />}
<div className="mb-8 flex w-full flex-col items-center justify-between lg:flex-row">
{children}
</div>
);
};
@@ -70,6 +80,7 @@ export default async function ComplianceDetail({
const { compliancetitle } = params;
const { complianceId, version, scanId } = searchParams;
const regionFilter = searchParams["filter[region__in]"];
const cisProfileFilter = searchParams["filter[cis_profile_level]"];
const logoPath = getComplianceIcon(compliancetitle);
// Create a key that includes region filter for Suspense
@@ -88,31 +99,33 @@ export default async function ComplianceDetail({
});
// Expand scans with provider information
const expandedScansData = await Promise.all(
scansData.data.map(async (scan: ScanProps) => {
const providerId = scan.relationships?.provider?.data?.id;
const expandedScansData = scansData?.data?.length
? await Promise.all(
scansData.data.map(async (scan: ScanProps) => {
const providerId = scan.relationships?.provider?.data?.id;
if (!providerId) {
return { ...scan, providerInfo: null };
}
if (!providerId) {
return { ...scan, providerInfo: null };
}
const formData = new FormData();
formData.append("id", providerId);
const formData = new FormData();
formData.append("id", providerId);
const providerData = await getProvider(formData);
const providerData = await getProvider(formData);
return {
...scan,
providerInfo: providerData?.data
? {
provider: providerData.data.attributes.provider,
uid: providerData.data.attributes.uid,
alias: providerData.data.attributes.alias,
}
: null,
};
}),
);
return {
...scan,
providerInfo: providerData?.data
? {
provider: providerData.data.attributes.provider,
uid: providerData.data.attributes.uid,
alias: providerData.data.attributes.alias,
}
: null,
};
}),
)
: [];
const selectedScanId = scanId || expandedScansData[0]?.id || null;
@@ -126,11 +139,21 @@ export default async function ComplianceDetail({
const uniqueRegions = metadataInfoData?.data?.attributes?.regions || [];
return (
<ContentLayout title={pageTitle} icon="fluent-mdl2:compliance-audit">
<ContentLayout
title={pageTitle}
icon={
logoPath ? (
<ComplianceIconSmall logoPath={logoPath} title={compliancetitle} />
) : (
"fluent-mdl2:compliance-audit"
)
}
>
<ComplianceHeader
scans={expandedScansData}
uniqueRegions={uniqueRegions}
showSearch={false}
framework={compliancetitle}
/>
<Suspense
@@ -138,8 +161,9 @@ export default async function ComplianceDetail({
fallback={
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<RequirementsChartSkeleton />
<FailedSectionsChartSkeleton />
<PieChartSkeleton />
<BarChartSkeleton />
<HeatmapChartSkeleton />
</ChartsWrapper>
<SkeletonAccordion />
</div>
@@ -149,18 +173,57 @@ export default async function ComplianceDetail({
complianceId={complianceId}
scanId={selectedScanId}
region={regionFilter}
filter={cisProfileFilter}
logoPath={logoPath}
uniqueRegions={uniqueRegions}
isRegionFiltered={
!!regionFilter &&
regionFilter.split(",").length < uniqueRegions.length
}
/>
</Suspense>
</ContentLayout>
);
}
const getComplianceData = async (
complianceId: string,
scanId: string,
region?: string,
): Promise<Framework[]> => {
const SSRComplianceContent = async ({
complianceId,
scanId,
region,
filter,
logoPath,
uniqueRegions,
isRegionFiltered,
}: {
complianceId: string;
scanId: string;
region?: string;
filter?: string;
logoPath?: string;
uniqueRegions: string[];
isRegionFiltered: boolean;
}) => {
if (!scanId) {
return (
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<PieChart pass={0} fail={0} manual={0} />
<BarChart sections={[]} />
<HeatmapChart
regions={[]}
categories={[]}
isRegionFiltered={
!!region && region.split(",").length < uniqueRegions.length
}
filteredRegionName={region}
/>
</ChartsWrapper>
<ClientAccordionWrapper items={[]} defaultExpandedKeys={[]} />
</div>
);
}
// Get compliance data and attributes once
const [attributesData, requirementsData] = await Promise.all([
getComplianceAttributes(complianceId),
getComplianceRequirements({
@@ -172,40 +235,25 @@ const getComplianceData = async (
// Determine framework from the first attribute item
const framework = attributesData?.data?.[0]?.attributes?.framework;
// Get the appropriate mapper for this framework
const mapper = getComplianceMapper(framework);
const mappedData = mapper.mapComplianceData(attributesData, requirementsData);
const data = mapper.mapComplianceData(
attributesData,
requirementsData,
filter,
);
return mappedData;
};
// Calculate region heatmap data using already obtained data
const regionHeatmapData = await calculateRegionHeatmapData(
complianceId,
scanId,
uniqueRegions,
attributesData,
mapper,
);
const categoryHeatmapData = calculateCategoryHeatmapData(data);
const SSRComplianceContent = async ({
complianceId,
scanId,
region,
logoPath,
}: {
complianceId: string;
scanId: string;
region?: string;
logoPath?: string;
}) => {
if (!scanId) {
return (
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<RequirementsChart pass={0} fail={0} manual={0} />
<FailedSectionsChart sections={[]} />
</ChartsWrapper>
<ClientAccordionWrapper items={[]} defaultExpandedKeys={[]} />
</div>
);
}
const data = await getComplianceData(complianceId, scanId, region);
const totalRequirements: RequirementsTotals = data.reduce(
(acc, framework) => ({
(acc: RequirementsTotals, framework: Framework) => ({
pass: acc.pass + framework.pass,
fail: acc.fail + framework.fail,
manual: acc.manual + framework.manual,
@@ -213,12 +261,6 @@ const SSRComplianceContent = async ({
{ pass: 0, fail: 0, manual: 0 },
);
// Get the framework to determine which mapper to use
const attributesData = await getComplianceAttributes(complianceId);
const framework = attributesData?.data?.[0]?.attributes?.framework;
// Get the appropriate mapper for this framework
const mapper = getComplianceMapper(framework);
const accordionItems = mapper.toAccordionItems(data, scanId);
const topFailedSections = mapper.getTopFailedSections(data);
@@ -229,12 +271,18 @@ const SSRComplianceContent = async ({
return (
<div className="space-y-8">
<ChartsWrapper logoPath={logoPath}>
<RequirementsChart
<PieChart
pass={totalRequirements.pass}
fail={totalRequirements.fail}
manual={totalRequirements.manual}
/>
<FailedSectionsChart sections={topFailedSections} />
<BarChart sections={topFailedSections} />
<HeatmapChart
regions={regionHeatmapData}
categories={categoryHeatmapData}
isRegionFiltered={isRegionFiltered}
filteredRegionName={region}
/>
</ChartsWrapper>
<Spacer className="h-1 w-full rounded-full bg-gray-200 dark:bg-gray-800" />
@@ -11,20 +11,22 @@ import {
import { Accordion } from "@/components/ui/accordion/Accordion";
import { DataTable } from "@/components/ui/table";
import { createDict } from "@/lib";
import { getComplianceMapper } from "@/lib/compliance/commons";
import { ComplianceId, Requirement } from "@/types/compliance";
import { FindingProps, FindingsResponse } from "@/types/components";
import { ENSCustomDetails } from "../compliance-custom-details/ens-details";
import { ISOCustomDetails } from "../compliance-custom-details/iso-details";
interface ClientAccordionContentProps {
requirement: Requirement;
scanId: string;
framework: string;
disableFindings?: boolean;
}
export const ClientAccordionContent = ({
requirement,
framework,
scanId,
disableFindings = false,
}: ClientAccordionContentProps) => {
const [findings, setFindings] = useState<FindingsResponse | null>(null);
const [expandedFindings, setExpandedFindings] = useState<FindingProps[]>([]);
@@ -41,6 +43,7 @@ export const ClientAccordionContent = ({
useEffect(() => {
async function loadFindings() {
if (
!disableFindings &&
requirement.check_ids?.length > 0 &&
requirement.status !== "No findings" &&
(loadedPageRef.current !== pageNumber ||
@@ -96,7 +99,29 @@ export const ClientAccordionContent = ({
}
loadFindings();
}, [requirement, scanId, pageNumber, sort, region]);
}, [requirement, scanId, pageNumber, sort, region, disableFindings]);
const renderDetails = () => {
if (!complianceId) {
return null;
}
const mapper = getComplianceMapper(framework);
const detailsComponent = mapper.getDetailsComponent(requirement);
return <div className="w-full">{detailsComponent}</div>;
};
if (disableFindings) {
return (
<div className="w-full">
{renderDetails()}
<p className="text-sm text-gray-500">
This requirement has no checks; therefore, there are no findings.
</p>
</div>
);
}
const checks = requirement.check_ids || [];
const checksList = (
@@ -142,30 +167,6 @@ export const ClientAccordionContent = ({
return <div>There are no findings for this regions</div>;
};
const renderDetails = () => {
if (!complianceId) {
return null;
}
switch (complianceId) {
case "ens_rd2022_aws":
return (
<div className="w-full">
<ENSCustomDetails requirement={requirement} />
</div>
);
case "iso27001_2013_aws":
case "iso27001_2022_aws":
return (
<div className="w-full">
<ISOCustomDetails requirement={requirement} />
</div>
);
default:
return null;
}
};
return (
<div className="w-full">
{renderDetails()}
@@ -13,7 +13,7 @@ export const ComplianceAccordionRequirementTitle = ({
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 uppercase">{name}</span>
<span className="whitespace-nowrap text-sm">{name}</span>
</div>
<StatusFindingBadge status={status} />
</div>
+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("iso")) {
if (!id.includes("ens") && !id.includes("iso") && !id.includes("cis_")) {
return;
}
@@ -3,7 +3,7 @@
import { useTheme } from "next-themes";
import {
Bar,
BarChart,
BarChart as RechartsBarChart,
Legend,
ResponsiveContainer,
Tooltip,
@@ -24,7 +24,7 @@ const title = (
</h3>
);
export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
export const BarChart = ({ sections }: FailedSectionsListProps) => {
const { theme } = useTheme();
const getTypeColor = (type: string) => {
@@ -84,16 +84,16 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
}
return (
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
<div className="mt-4">{title}</div>
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
<div>{title}</div>
<div className="h-[320px] w-full">
<div className="h-full w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
<RechartsBarChart
data={chartData}
layout="vertical"
margin={{ top: 16, right: 30, left: 40, bottom: 5 }}
maxBarSize={40}
margin={{ top: 12, bottom: 0 }}
maxBarSize={32}
>
<XAxis
type="number"
@@ -111,10 +111,15 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
<YAxis
type="category"
dataKey="name"
width={100}
width={1}
tick={{
fontSize: 12,
fill: theme === "dark" ? "#94a3b8" : "#374151",
textAnchor: "start",
style: {
transform: "translateX(10px) translateY(-26px)",
},
width: 400,
}}
axisLine={false}
tickLine={false}
@@ -157,22 +162,6 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
}}
cursor={false}
/>
{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}
@@ -182,7 +171,21 @@ export const FailedSectionsChart = ({ sections }: FailedSectionsListProps) => {
radius={i === allTypes.length - 1 ? [0, 4, 4, 0] : [0, 0, 0, 0]}
/>
))}
</BarChart>
<Legend
formatter={(value) => translateType(value)}
wrapperStyle={{
fontSize: "10px",
display: "flex",
justifyContent: "center",
width: "100%",
paddingTop: "16px",
marginBottom: "16px",
}}
iconType="circle"
layout="horizontal"
verticalAlign="bottom"
/>
</RechartsBarChart>
</ResponsiveContainer>
</div>
</div>
@@ -0,0 +1,165 @@
"use client";
import { useTheme } from "next-themes";
import { useState } from "react";
import { CategoryData, RegionData } from "@/types/compliance";
interface HeatmapChartProps {
regions: RegionData[];
categories?: CategoryData[];
isRegionFiltered?: boolean; // Indicates if a region filter is active
filteredRegionName?: string; // Name of the filtered region
}
const getHeatmapColor = (percentage: number): string => {
if (percentage === 0) return "#10b981"; // Green for 0% failures
if (percentage <= 25) return "#eab308"; // Yellow
if (percentage <= 50) return "#f97316"; // Orange
if (percentage <= 100) return "#ef4444"; // Red
return "#ef4444";
};
const capitalizeFirstLetter = (text: string): string => {
const lowerText = text.toLowerCase();
const firstLetterIndex = lowerText.search(/[a-zA-Z]/);
if (firstLetterIndex === -1) return text; // No letters found
return (
lowerText.slice(0, firstLetterIndex) +
lowerText.charAt(firstLetterIndex).toUpperCase() +
lowerText.slice(firstLetterIndex + 1)
);
};
export const HeatmapChart = ({
regions,
categories = [],
isRegionFiltered = false,
}: HeatmapChartProps) => {
const { theme } = useTheme();
const [hoveredItem, setHoveredItem] = useState<
RegionData | CategoryData | null
>(null);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
// Determine what data to show and prepare it
const dataToShow = isRegionFiltered ? categories : regions;
const heatmapData = dataToShow
.filter((item) => item.totalRequirements > 0)
.sort((a, b) => b.failurePercentage - a.failurePercentage)
.slice(0, 9); // Exactly 9 items for 3x3 grid
// Check if there are no items with data
if (!dataToShow || dataToShow.length === 0 || heatmapData.length === 0) {
const noDataMessage = isRegionFiltered
? "No category data available"
: "No regional data available";
return (
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[400px]">
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
{isRegionFiltered
? "Categories Failure Rate"
: "Failure Rate by Region"}
</h3>
<div className="flex h-[320px] w-full items-center justify-center">
<p className="text-sm text-gray-500">{noDataMessage}</p>
</div>
</div>
);
}
const handleMouseEnter = (
item: RegionData | CategoryData,
event: React.MouseEvent,
) => {
setHoveredItem(item);
setMousePosition({ x: event.clientX, y: event.clientY });
};
const handleMouseMove = (event: React.MouseEvent) => {
setMousePosition({ x: event.clientX, y: event.clientY });
};
const handleMouseLeave = () => {
setHoveredItem(null);
};
return (
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
<div>
<h3 className="whitespace-nowrap text-xs font-semibold uppercase tracking-wide">
{isRegionFiltered
? "Categories Failure Rate"
: "Failure Rate by Region"}
</h3>
</div>
<div className="h-full w-full p-4">
{/* 3x3 Grid */}
<div className="grid h-full w-full grid-cols-3 gap-1">
{heatmapData.map((item) => (
<div
key={item.name}
className="flex items-center justify-center rounded border"
style={{
backgroundColor: getHeatmapColor(item.failurePercentage),
borderColor: theme === "dark" ? "#374151" : "#e5e7eb",
}}
onMouseEnter={(e) => handleMouseEnter(item, e)}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<div className="text-center">
<div
className="text-xs font-semibold"
style={{
color: theme === "dark" ? "#ffffff" : "#000000",
}}
>
{isRegionFiltered
? capitalizeFirstLetter(item.name)
: item.name}
</div>
<div
className="text-xs"
style={{
color: theme === "dark" ? "#ffffff" : "#000000",
}}
>
{item.failurePercentage}%
</div>
</div>
</div>
))}
</div>
{/* Custom Tooltip */}
{hoveredItem && (
<div
className="pointer-events-none fixed z-50 rounded border px-3 py-2 text-xs shadow-lg"
style={{
left: mousePosition.x + 10,
top: mousePosition.y - 10,
backgroundColor: theme === "dark" ? "#1e293b" : "white",
borderColor: theme === "dark" ? "#475569" : "rgba(0, 0, 0, 0.1)",
color: theme === "dark" ? "white" : "black",
}}
>
<div className="mb-1 font-semibold">
{isRegionFiltered
? capitalizeFirstLetter(hoveredItem.name)
: hoveredItem.name}
</div>
<div>Failure Rate: {hoveredItem.failurePercentage}%</div>
<div>
Failed: {hoveredItem.failedRequirements}/
{hoveredItem.totalRequirements}
</div>
</div>
)}
</div>
</div>
);
};
@@ -1,11 +1,17 @@
"use client";
import { useTheme } from "next-themes";
import { Cell, Label, Pie, PieChart, Tooltip } from "recharts";
import {
Cell,
Label,
Pie,
PieChart as RechartsPieChart,
Tooltip,
} from "recharts";
import { ChartConfig, ChartContainer } from "@/components/ui/chart/Chart";
interface RequirementsChartProps {
interface PieChartProps {
pass: number;
fail: number;
manual: number;
@@ -29,11 +35,7 @@ const chartConfig = {
},
} satisfies ChartConfig;
export const RequirementsChart = ({
pass,
fail,
manual,
}: RequirementsChartProps) => {
export const PieChart = ({ pass, fail, manual }: PieChartProps) => {
const { theme } = useTheme();
const chartData = [
@@ -119,7 +121,7 @@ export const RequirementsChart = ({
config={chartConfig}
className="aspect-square w-[200px] min-w-[200px]"
>
<PieChart>
<RechartsPieChart>
<Tooltip
cursor={false}
content={<CustomTooltip active={false} payload={[]} />}
@@ -168,7 +170,7 @@ export const RequirementsChart = ({
}}
/>
</Pie>
</PieChart>
</RechartsPieChart>
</ChartContainer>
<div className="mt-2 grid grid-cols-3 gap-4">
@@ -0,0 +1,150 @@
import ReactMarkdown from "react-markdown";
import { Requirement } from "@/types/compliance";
interface CISDetailsProps {
requirement: Requirement;
}
export const CISCustomDetails = ({ requirement }: CISDetailsProps) => {
const processReferences = (
references: string | number | string[] | undefined,
): string[] => {
if (typeof references !== "string") return [];
// Use regex to extract all URLs that start with https://
const urlRegex = /https:\/\/[^:]+/g;
const urls = references.match(urlRegex);
return urls || [];
};
return (
<div className="space-y-4">
{requirement.profile && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Profile Level
</h4>
<p className="text-sm">{requirement.profile}</p>
</div>
)}
{requirement.subsection && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
SubSection
</h4>
<p className="text-sm">{requirement.subsection}</p>
</div>
)}
{requirement.assessment_status && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Assessment Status
</h4>
<p className="text-sm">{requirement.assessment_status}</p>
</div>
)}
{requirement.description && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Description
</h4>
<p className="text-sm">{requirement.description}</p>
</div>
)}
{requirement.rationale_statement && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Rationale Statement
</h4>
<p className="text-sm">{requirement.rationale_statement}</p>
</div>
)}
{requirement.impact_statement && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Impact Statement
</h4>
<p className="text-sm">{requirement.impact_statement}</p>
</div>
)}
{requirement.remediation_procedure &&
typeof requirement.remediation_procedure === "string" && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Remediation Procedure
</h4>
{/* Prettier -> "plugins": ["prettier-plugin-tailwindcss"] is not ready yet to "prose": */}
{/* eslint-disable-next-line */}
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{requirement.remediation_procedure}</ReactMarkdown>
</div>
</div>
)}
{requirement.audit_procedure &&
typeof requirement.audit_procedure === "string" && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Audit Procedure
</h4>
{/* eslint-disable-next-line */}
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown>{requirement.audit_procedure}</ReactMarkdown>
</div>
</div>
)}
{requirement.additional_information && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Additional Information
</h4>
<p className="whitespace-pre-wrap text-sm">
{requirement.additional_information}
</p>
</div>
)}
{requirement.default_value && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
Default Value
</h4>
<p className="text-sm">{requirement.default_value}</p>
</div>
)}
{requirement.references && (
<div>
<h4 className="text-muted-foreground mb-1 text-sm font-medium">
References
</h4>
<div className="text-sm">
{processReferences(requirement.references).map(
(url: string, index: number) => (
<div key={index}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="break-all text-blue-600 underline hover:text-blue-800"
>
{url}
</a>
</div>
),
)}
</div>
</div>
)}
</div>
);
};
@@ -13,6 +13,7 @@ interface ComplianceHeaderProps {
uniqueRegions: string[];
showSearch?: boolean;
showRegionFilter?: boolean;
framework?: string; // Framework name to show specific filters
}
export const ComplianceHeader = ({
@@ -20,25 +21,46 @@ export const ComplianceHeader = ({
uniqueRegions,
showSearch = true,
showRegionFilter = true,
framework,
}: ComplianceHeaderProps) => {
const frameworkFilters = [];
// Add CIS Profile Level filter if framework is CIS
if (framework === "CIS") {
frameworkFilters.push({
key: "cis_profile_level",
labelCheckboxGroup: "Level",
values: ["Level 1", "Level 2"],
index: 0, // Show first
showSelectAll: false, // No "Select All" option since Level 2 includes Level 1
defaultValues: ["Level 2"], // Default to Level 2 selected (which includes Level 1)
});
}
// Prepare region filters
const regionFilters = showRegionFilter
? [
{
key: "region__in",
labelCheckboxGroup: "Regions",
values: uniqueRegions,
index: 1, // Show after framework filters
defaultToSelectAll: true, // Default to all regions selected
},
]
: [];
const allFilters = [...frameworkFilters, ...regionFilters];
return (
<>
{showSearch && <FilterControls search />}
<Spacer y={8} />
<DataCompliance scans={scans} />
{showRegionFilter && (
{allFilters.length > 0 && (
<>
<Spacer y={8} />
<DataTableFilterCustom
filters={[
{
key: "region__in",
labelCheckboxGroup: "Regions",
values: uniqueRegions,
},
]}
defaultOpen={true}
/>
<DataTableFilterCustom filters={allFilters} defaultOpen={true} />
</>
)}
<Spacer y={12} />
+18 -1
View File
@@ -1,4 +1,21 @@
export * from "./compliance-accordion/client-accordion-content";
export * from "./compliance-accordion/client-accordion-wrapper";
export * from "./compliance-accordion/compliance-accordion-requeriment-title";
export * from "./compliance-accordion/compliance-accordion-title";
export * from "./compliance-card";
export * from "./compliance-charts/bar-chart";
export * from "./compliance-charts/heatmap-chart";
export * from "./compliance-charts/pie-chart";
export * from "./compliance-custom-details/cis-details";
export * from "./compliance-custom-details/ens-details";
export * from "./compliance-custom-details/iso-details";
export * from "./compliance-header/compliance-header";
export * from "./compliance-header/compliance-scan-info";
export * from "./compliance-skeleton-grid";
export * from "./compliance-header/data-compliance";
export * from "./compliance-header/select-scan-compliance-data";
export * from "./no-scans-available";
export * from "./skeletons/bar-chart-skeleton";
export * from "./skeletons/compliance-accordion-skeleton";
export * from "./skeletons/compliance-grid-skeleton";
export * from "./skeletons/heatmap-chart-skeleton";
export * from "./skeletons/pie-chart-skeleton";
@@ -2,7 +2,7 @@
import { Skeleton } from "@nextui-org/react";
export const FailedSectionsChartSkeleton = () => {
export const BarChartSkeleton = () => {
return (
<div className="flex w-[400px] flex-col items-center justify-between lg:w-[600px]">
{/* Title skeleton */}
@@ -0,0 +1,28 @@
"use client";
import { Skeleton } from "@nextui-org/react";
export const HeatmapChartSkeleton = () => {
return (
<div className="flex h-[320px] w-[400px] flex-col items-center justify-between lg:w-[400px]">
{/* Title skeleton */}
<Skeleton className="h-4 w-36 rounded-lg">
<div className="h-4 bg-default-200" />
</Skeleton>
{/* Heatmap area skeleton - 3x3 grid like the real component */}
<div className="h-full w-full p-4">
<div className="grid h-full w-full grid-cols-3 gap-1">
{Array.from({ length: 9 }).map((_, index) => (
<Skeleton
key={index}
className="flex items-center justify-center rounded border"
>
<div className="h-full w-full bg-default-200" />
</Skeleton>
))}
</div>
</div>
</div>
);
};
@@ -2,7 +2,7 @@
import { Skeleton } from "@nextui-org/react";
export const RequirementsChartSkeleton = () => {
export const PieChartSkeleton = () => {
return (
<div className="flex h-[320px] flex-col items-center justify-between">
{/* Title skeleton */}
@@ -1,12 +1,13 @@
import { Suspense, use } from "react";
import { ReactNode, Suspense, use } from "react";
import { getUserInfo } from "@/actions/users/users";
import { Navbar } from "../nav-bar/navbar";
import { SkeletonContentLayout } from "./skeleton-content-layout";
interface ContentLayoutProps {
title: string;
icon: string;
icon: string | ReactNode;
children: React.ReactNode;
}
@@ -12,7 +12,13 @@ import {
} from "@nextui-org/react";
import { ChevronDown, X } from "lucide-react";
import { useSearchParams } from "next/navigation";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { CustomDropdownFilterProps } from "@/types";
@@ -25,6 +31,7 @@ export const CustomDropdownFilter = ({
const searchParams = useSearchParams();
const [groupSelected, setGroupSelected] = useState(new Set<string>());
const [isOpen, setIsOpen] = useState(false);
const hasUserInteracted = useRef(false);
const filterValues = useMemo(() => filter?.values || [], [filter?.values]);
const selectedValues = Array.from(groupSelected).filter(
@@ -42,24 +49,66 @@ export const CustomDropdownFilter = ({
useEffect(() => {
if (activeFilterValue.length > 0) {
const newSelection = new Set(activeFilterValue);
if (newSelection.size === filterValues.length) {
if (
newSelection.size === filterValues.length &&
filter?.showSelectAll !== false
) {
newSelection.add("all");
}
setGroupSelected(newSelection);
} else {
setGroupSelected(new Set());
} else if (!hasUserInteracted.current) {
// Handle default behavior when no URL params exist
// Only apply defaults if user hasn't interacted yet
// Only set visual state, don't trigger URL changes automatically
if (filter?.defaultToSelectAll && filterValues.length > 0) {
const newSelection = new Set(filterValues);
if (filter?.showSelectAll !== false) {
newSelection.add("all");
}
setGroupSelected(newSelection);
// DON'T notify parent automatically - wait for user interaction
} else if (filter?.defaultValues && filter.defaultValues.length > 0) {
// Handle specific default values
const validDefaultValues = filter.defaultValues.filter((value) =>
filterValues.includes(value),
);
const newSelection = new Set(validDefaultValues);
// Add "all" if all items are selected and showSelectAll is not false
if (
validDefaultValues.length === filterValues.length &&
filter?.showSelectAll !== false
) {
newSelection.add("all");
}
setGroupSelected(newSelection);
// DON'T notify parent automatically - wait for user interaction
} else {
setGroupSelected(new Set());
}
}
}, [activeFilterValue, filterValues.length]);
}, [
activeFilterValue,
filterValues,
filter?.defaultToSelectAll,
filter?.defaultValues,
filter?.showSelectAll,
]);
const updateSelection = useCallback(
(newValues: string[]) => {
// Mark that user has interacted with the filter
hasUserInteracted.current = true;
const actualValues = newValues.filter((key) => key !== "all");
const newSelection = new Set(actualValues);
// Auto-add "all" if all items are selected
// Auto-add "all" if all items are selected and showSelectAll is not false
if (
actualValues.length === filterValues.length &&
filterValues.length > 0
filterValues.length > 0 &&
filter?.showSelectAll !== false
) {
newSelection.add("all");
}
@@ -69,7 +118,7 @@ export const CustomDropdownFilter = ({
// Notify parent with actual values (excluding "all")
onFilterChange?.(filter.key, actualValues);
},
[filterValues.length, onFilterChange, filter.key],
[filterValues.length, onFilterChange, filter.key, filter?.showSelectAll],
);
const onSelectionChange = useCallback(
@@ -194,16 +243,20 @@ export const CustomDropdownFilter = ({
onValueChange={onSelectionChange}
className="font-bold"
>
<Checkbox
classNames={{
label: "text-small font-normal",
wrapper: "checkbox-update",
}}
value="all"
>
Select All
</Checkbox>
<Divider orientation="horizontal" className="mt-2" />
{filter?.showSelectAll !== false && (
<>
<Checkbox
classNames={{
label: "text-small font-normal",
wrapper: "checkbox-update",
}}
value="all"
>
Select All
</Checkbox>
<Divider orientation="horizontal" className="mt-2" />
</>
)}
<ScrollShadow
hideScrollBar
className="flex max-h-96 max-w-full flex-col gap-y-2 py-2"
+15 -7
View File
@@ -1,13 +1,15 @@
import { Icon } from "@iconify/react";
import { ReactNode } from "react";
import { ThemeSwitch } from "@/components/ThemeSwitch";
import { UserProfileProps } from "@/types";
import { SheetMenu } from "../sidebar/sheet-menu";
import { UserNav } from "../user-nav/user-nav";
interface NavbarProps {
title: string;
icon: string;
icon: string | ReactNode;
user: UserProfileProps;
}
@@ -17,12 +19,18 @@ export function Navbar({ title, icon, user }: NavbarProps) {
<div className="mx-4 flex h-14 items-center sm:mx-8">
<div className="flex items-center space-x-2">
<SheetMenu />
<Icon
className="text-default-500"
height={24}
icon={icon}
width={24}
/>
{typeof icon === "string" ? (
<Icon
className="text-default-500"
height={24}
icon={icon}
width={24}
/>
) : (
<div className="flex h-10 w-10 items-center justify-center [&>*]:h-full [&>*]:w-full">
{icon}
</div>
)}
<h1 className="text-sm font-bold text-default-700">{title}</h1>
</div>
<div className="flex flex-1 items-center justify-end gap-3">
+204
View File
@@ -0,0 +1,204 @@
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,
CISAttributesMetadata,
Framework,
Requirement,
RequirementItemData,
RequirementsData,
RequirementStatus,
} from "@/types/compliance";
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
filter?: string, // "Level 1" or "Level 2" or undefined (show all)
): Framework[] => {
const attributes = attributesData?.data || [];
const requirements = requirementsData?.data || [];
// Create a map for quick lookup of requirements by id
const requirementsMap = new Map<string, RequirementItemData>();
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 CISAttributesMetadata[];
const attrs = metadataArray?.[0];
if (!attrs) continue;
// Apply profile filter
if (filter === "Level 1" && attrs.Profile !== "Level 1") {
continue; // Skip Level 2 requirements when Level 1 is selected
}
// Get corresponding requirement data
const requirementData = requirementsMap.get(id);
if (!requirementData) continue;
const frameworkName = attributeItem.attributes.framework;
const sectionName = attrs.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);
}
const normalizedSectionName = sectionName.replace(/^(\d+)\s/, "$1. ");
let category = framework.categories.find(
(c) => c.name === normalizedSectionName,
);
if (!category) {
category = {
name: normalizedSectionName,
pass: 0,
fail: 0,
manual: 0,
controls: [],
};
framework.categories.push(category);
}
// Create a control for this requirement (each requirement is its own control)
const controlLabel = `${id} - ${description}`;
const control = {
label: controlLabel,
pass: 0,
fail: 0,
manual: 0,
requirements: [] as Requirement[],
};
// Create requirement
const finalStatus: RequirementStatus = status as RequirementStatus;
const requirement: Requirement = {
name: requirementName,
description: attrs.Description,
status: finalStatus,
check_ids: checks,
pass: finalStatus === "PASS" ? 1 : 0,
fail: finalStatus === "FAIL" ? 1 : 0,
manual: finalStatus === "MANUAL" ? 1 : 0,
profile: attrs.Profile,
subsection: attrs.SubSection || "",
assessment_status: attrs.AssessmentStatus,
rationale_statement: attrs.RationaleStatement,
impact_statement: attrs.ImpactStatement,
remediation_procedure: attrs.RemediationProcedure,
audit_procedure: attrs.AuditProcedure,
additional_information: attrs.AdditionalInformation,
default_value: attrs.DefaultValue || "",
references: attrs.References,
};
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: (
<ComplianceAccordionTitle
label={category.name}
pass={category.pass}
fail={category.fail}
manual={category.manual}
isParentLevel={true}
/>
),
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: (
<ComplianceAccordionRequirementTitle
type=""
name={control.label}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
items: [],
};
}),
};
}),
);
};
+163 -1
View File
@@ -1,11 +1,23 @@
import React from "react";
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 { AccordionItemProps } from "@/components/ui/accordion/Accordion";
import {
AttributesData,
CategoryData,
FailedSection,
Framework,
RegionData,
Requirement,
RequirementsData,
} from "@/types/compliance";
import {
mapComplianceData as mapCISComplianceData,
toAccordionItems as toCISAccordionItems,
} from "./cis";
import {
mapComplianceData as mapENSComplianceData,
toAccordionItems as toENSAccordionItems,
@@ -19,12 +31,14 @@ export interface ComplianceMapper {
mapComplianceData: (
attributesData: AttributesData,
requirementsData: RequirementsData,
filter?: string,
) => Framework[];
toAccordionItems: (
data: Framework[],
scanId: string | undefined,
) => AccordionItemProps[];
getTopFailedSections: (mappedData: Framework[]) => FailedSection[];
getDetailsComponent: (requirement: Requirement) => React.ReactNode;
}
// Common function for getting top failed sections
@@ -70,11 +84,22 @@ const complianceMappers: Record<string, ComplianceMapper> = {
mapComplianceData: mapENSComplianceData,
toAccordionItems: toENSAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(ENSCustomDetails, { requirement }),
},
ISO27001: {
mapComplianceData: mapISOComplianceData,
toAccordionItems: toISOAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(ISOCustomDetails, { requirement }),
},
CIS: {
mapComplianceData: mapCISComplianceData,
toAccordionItems: toCISAccordionItems,
getTopFailedSections,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(CISCustomDetails, { requirement }),
},
};
@@ -83,7 +108,7 @@ const defaultMapper: ComplianceMapper = complianceMappers.ENS;
/**
* Get the appropriate compliance mapper based on the framework name
* @param framework - The framework name (e.g., "ENS", "ISO27001")
* @param framework - The framework name (e.g., "ENS", "ISO27001", "CIS")
* @returns ComplianceMapper object with specific functions for the framework
*/
export const getComplianceMapper = (framework?: string): ComplianceMapper => {
@@ -93,3 +118,140 @@ export const getComplianceMapper = (framework?: string): ComplianceMapper => {
return complianceMappers[framework] || defaultMapper;
};
export const calculateRegionHeatmapData = async (
complianceId: string,
scanId: string,
uniqueRegions: string[],
attributesData: AttributesData,
mapper: ComplianceMapper,
): Promise<RegionData[]> => {
if (!complianceId || !scanId || !uniqueRegions?.length) {
return [];
}
try {
const { getComplianceRequirements } = await import("@/actions/compliances");
// Get data for each region in parallel
const regionPromises = uniqueRegions.map(async (region) => {
try {
// Only need to fetch requirements data per region
const regionRequirementsData = await getComplianceRequirements({
complianceId,
scanId,
region, // Filter by specific region
});
// Map the data using the provided mapper
const mappedData = mapper.mapComplianceData(
attributesData,
regionRequirementsData,
);
// Calculate totals for this region
const regionTotals = mappedData.reduce(
(acc, framework) => ({
pass: acc.pass + framework.pass,
fail: acc.fail + framework.fail,
manual: acc.manual + framework.manual,
}),
{ pass: 0, fail: 0, manual: 0 },
);
const totalRequirements =
regionTotals.pass + regionTotals.fail + regionTotals.manual;
const failurePercentage =
totalRequirements > 0
? Math.round((regionTotals.fail / totalRequirements) * 100)
: 0;
return {
name: region,
failurePercentage,
totalRequirements,
failedRequirements: regionTotals.fail,
};
} catch (error) {
console.error(`Error fetching data for region ${region}:`, error);
return {
name: region,
failurePercentage: 0,
totalRequirements: 0,
failedRequirements: 0,
};
}
});
const regionData = await Promise.all(regionPromises);
// Filter, sort and limit to top 9 regions for 3x3 grid
const filteredData = regionData
.filter((region) => region.totalRequirements > 0)
.sort((a, b) => b.failurePercentage - a.failurePercentage)
.slice(0, 9);
return filteredData;
} catch (error) {
console.error("Error calculating region heatmap data:", error);
return [];
}
};
export const calculateCategoryHeatmapData = (
complianceData: Framework[],
): CategoryData[] => {
if (!complianceData?.length) {
return [];
}
try {
const categoryMap = new Map<
string,
{ pass: number; fail: number; manual: number }
>();
// Aggregate data by category
complianceData.forEach((framework) => {
framework.categories.forEach((category) => {
const existing = categoryMap.get(category.name) || {
pass: 0,
fail: 0,
manual: 0,
};
categoryMap.set(category.name, {
pass: existing.pass + category.pass,
fail: existing.fail + category.fail,
manual: existing.manual + category.manual,
});
});
});
const categoryData: CategoryData[] = Array.from(categoryMap.entries()).map(
([name, stats]) => {
const totalRequirements = stats.pass + stats.fail + stats.manual;
const failurePercentage =
totalRequirements > 0
? Math.round((stats.fail / totalRequirements) * 100)
: 0;
return {
name,
failurePercentage,
totalRequirements,
failedRequirements: stats.fail,
};
},
);
const filteredData = categoryData
.filter((category) => category.totalRequirements > 0)
.sort((a, b) => b.failurePercentage - a.failurePercentage)
.slice(0, 9); // Show top 9 categories
return filteredData;
} catch (error) {
console.error("Error calculating category heatmap data:", error);
return [];
}
};
+9 -4
View File
@@ -14,6 +14,10 @@ import {
} from "@/types/compliance";
export const translateType = (type: string) => {
if (!type) {
return "";
}
switch (type.toLowerCase()) {
case "requisito":
return "Requirement";
@@ -223,12 +227,13 @@ export const toAccordionItems = (
<ClientAccordionContent
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 &&
requirement.manual === 0
}
/>
),
items: [],
isDisabled:
requirement.check_ids.length === 0 &&
requirement.manual === 0,
};
}),
isDisabled:
+5 -3
View File
@@ -192,12 +192,14 @@ export const toAccordionItems = (
<ClientAccordionContent
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 &&
requirement.manual === 0
}
/>
),
items: [],
isDisabled:
requirement.check_ids.length === 0 &&
requirement.manual === 0,
};
}),
isDisabled:
+1219 -7
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,6 +14,7 @@
"@radix-ui/react-toast": "^1.2.4",
"@react-aria/ssr": "3.9.4",
"@react-aria/visually-hidden": "3.8.12",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-table": "^8.19.3",
"add": "^2.0.6",
"alert": "^6.0.2",
@@ -35,6 +36,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"react-markdown": "^10.1.0",
"recharts": "^2.15.2",
"server-only": "^0.0.1",
"shadcn-ui": "^0.2.3",
+4 -3
View File
@@ -171,9 +171,9 @@ module.exports = {
"100%": { left: "100%", width: "100%" },
},
dropArrow: {
'0%': { transform: 'translateY(-8px)', opacity: '0' },
'50%': { opacity: '1' },
'100%': { transform: 'translateY(0)', opacity: '1' },
"0%": { transform: "translateY(-8px)", opacity: "0" },
"50%": { opacity: "1" },
"100%": { transform: "translateY(0)", opacity: "1" },
},
},
animation: {
@@ -188,6 +188,7 @@ module.exports = {
},
plugins: [
require("tailwindcss-animate"),
require("@tailwindcss/typography"),
nextui({
themes: {
dark: {
+36 -1
View File
@@ -3,7 +3,13 @@ export type RequirementStatus = "PASS" | "FAIL" | "MANUAL" | "No findings";
export type ComplianceId =
| "ens_rd2022_aws"
| "iso27001_2013_aws"
| "iso27001_2022_aws";
| "iso27001_2022_aws"
| "cis_1.4_aws"
| "cis_1.5_aws"
| "cis_2.0_aws"
| "cis_3.0_aws"
| "cis_4.0_aws"
| "cis_5.0_aws";
export interface CompliancesOverview {
data: ComplianceOverviewData[];
@@ -91,6 +97,21 @@ export interface ISO27001AttributesMetadata {
Check_Summary: string;
}
export interface CISAttributesMetadata {
Section: string;
SubSection: string | null;
Profile: string; // "Level 1" or "Level 2"
AssessmentStatus: string; // "Manual" or "Automated"
Description: string;
RationaleStatement: string;
ImpactStatement: string;
RemediationProcedure: string;
AuditProcedure: string;
AdditionalInformation: string;
DefaultValue: string | null;
References: string;
}
export interface AttributesItemData {
type: "compliance-requirements-attributes";
id: string;
@@ -123,3 +144,17 @@ export interface AttributesData {
export interface RequirementsData {
data: RequirementItemData[];
}
export interface RegionData {
name: string;
failurePercentage: number;
totalRequirements: number;
failedRequirements: number;
}
export interface CategoryData {
name: string;
failurePercentage: number;
totalRequirements: number;
failedRequirements: number;
}
+3
View File
@@ -6,6 +6,9 @@ export interface FilterOption {
values: string[];
valueLabelMapping?: Array<{ [uid: string]: ProviderAccountProps }>;
index?: number;
showSelectAll?: boolean;
defaultToSelectAll?: boolean;
defaultValues?: string[];
}
export interface CustomDropdownFilterProps {