From aad4b13cab889ff99038073d1c8b5f08efdfe1c6 Mon Sep 17 00:00:00 2001 From: pedrooot Date: Fri, 5 Dec 2025 13:20:37 +0100 Subject: [PATCH] fix(ui): show Top Failed Requirements for compliances without section hierarchy For compliances like RBI that have a flat structure (requirements directly in framework without categories/sections), the Top Failed Sections chart was showing empty because the logic only iterated over framework.categories. This fix: - Detects flat structure compliances (framework.requirements without categories) - Shows 'Top Failed Requirements' for flat structures - Keeps 'Top Failed Sections' for hierarchical structures (like ENS) - Updates getTopFailedSections to return TopFailedResult with type metadata - Adds dynamic title to TopFailedSectionsCard component --- .../compliance/[compliancetitle]/page.tsx | 7 ++- .../top-failed-sections-card.tsx | 11 +++- ui/lib/compliance/commons.tsx | 59 ++++++++++++++++--- ui/lib/compliance/compliance-mapper.ts | 4 +- ui/lib/compliance/mitre.tsx | 15 +++-- ui/types/compliance.ts | 7 +++ 6 files changed, 82 insertions(+), 21 deletions(-) diff --git a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx index 4061478f94..974b8edede 100644 --- a/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx +++ b/ui/app/(prowler)/compliance/[compliancetitle]/page.tsx @@ -195,7 +195,7 @@ const SSRComplianceContent = async ({ { pass: 0, fail: 0, manual: 0 }, ); const accordionItems = mapper.toAccordionItems(data, scanId); - const topFailedSections = mapper.getTopFailedSections(data); + const topFailedResult = mapper.getTopFailedSections(data); return (
@@ -205,7 +205,10 @@ const SSRComplianceContent = async ({ fail={totalRequirements.fail} manual={totalRequirements.manual} /> - + {/* */}
diff --git a/ui/components/compliance/compliance-charts/top-failed-sections-card.tsx b/ui/components/compliance/compliance-charts/top-failed-sections-card.tsx index 70de7d6bc4..a24962b891 100644 --- a/ui/components/compliance/compliance-charts/top-failed-sections-card.tsx +++ b/ui/components/compliance/compliance-charts/top-failed-sections-card.tsx @@ -3,14 +3,16 @@ import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { BarDataPoint } from "@/components/graphs/types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; -import { FailedSection } from "@/types/compliance"; +import { FailedSection, TopFailedDataType } from "@/types/compliance"; interface TopFailedSectionsCardProps { sections: FailedSection[]; + dataType?: TopFailedDataType; } export function TopFailedSectionsCard({ sections, + dataType = "sections", }: TopFailedSectionsCardProps) { // Transform FailedSection[] to BarDataPoint[] const total = sections.reduce((sum, section) => sum + section.total, 0); @@ -22,13 +24,18 @@ export function TopFailedSectionsCard({ color: "var(--bg-fail-primary)", })); + const title = + dataType === "requirements" + ? "Top Failed Requirements" + : "Top Failed Sections"; + return ( - Top Failed Sections + {title} diff --git a/ui/lib/compliance/commons.tsx b/ui/lib/compliance/commons.tsx index 3a1462cce5..85ca6cf696 100644 --- a/ui/lib/compliance/commons.tsx +++ b/ui/lib/compliance/commons.tsx @@ -1,12 +1,12 @@ import { CategoryData, - FailedSection, Framework, Requirement, REQUIREMENT_STATUS, RequirementItemData, RequirementsData, RequirementStatus, + TopFailedResult, } from "@/types/compliance"; export const updateCounters = ( @@ -24,9 +24,48 @@ export const updateCounters = ( export const getTopFailedSections = ( mappedData: Framework[], -): FailedSection[] => { +): TopFailedResult => { const failedSectionMap = new Map(); + // Check if we have a flat structure (requirements directly in framework) + const hasFlatStructure = mappedData.some((framework) => { + const directRequirements = (framework as any).requirements || []; + return directRequirements.length > 0 && framework.categories.length === 0; + }); + + if (hasFlatStructure) { + // Handle flat structure: count failed requirements directly + mappedData.forEach((framework) => { + const directRequirements = + ((framework as any).requirements as Requirement[]) || []; + + directRequirements.forEach((requirement) => { + if (requirement.status === REQUIREMENT_STATUS.FAIL) { + const requirementName = requirement.name; + + if (!failedSectionMap.has(requirementName)) { + failedSectionMap.set(requirementName, { total: 0, types: {} }); + } + + const requirementData = failedSectionMap.get(requirementName); + requirementData.total += 1; + + const type = (requirement.type as string) || "Fails"; + requirementData.types[type] = (requirementData.types[type] || 0) + 1; + } + }); + }); + + return { + items: Array.from(failedSectionMap.entries()) + .map(([name, data]) => ({ name, ...data })) + .sort((a, b) => b.total - a.total) + .slice(0, 5), + type: "requirements", + }; + } + + // Handle hierarchical structure: count by category (section) mappedData.forEach((framework) => { framework.categories.forEach((category) => { category.controls.forEach((control) => { @@ -41,10 +80,9 @@ export const getTopFailedSections = ( const sectionData = failedSectionMap.get(sectionName); sectionData.total += 1; - const type = requirement.type || "Fails"; + const type = (requirement.type as string) || "Fails"; - sectionData.types[type as string] = - (sectionData.types[type as string] || 0) + 1; + sectionData.types[type] = (sectionData.types[type] || 0) + 1; } }); }); @@ -52,10 +90,13 @@ export const getTopFailedSections = ( }); // Convert in descending order and slice top 5 - return Array.from(failedSectionMap.entries()) - .map(([name, data]) => ({ name, ...data })) - .sort((a, b) => b.total - a.total) - .slice(0, 5); // Top 5 + return { + items: Array.from(failedSectionMap.entries()) + .map(([name, data]) => ({ name, ...data })) + .sort((a, b) => b.total - a.total) + .slice(0, 5), + type: "sections", + }; }; export const calculateCategoryHeatmapData = ( diff --git a/ui/lib/compliance/compliance-mapper.ts b/ui/lib/compliance/compliance-mapper.ts index da47130491..86160cca19 100644 --- a/ui/lib/compliance/compliance-mapper.ts +++ b/ui/lib/compliance/compliance-mapper.ts @@ -14,10 +14,10 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion"; import { AttributesData, CategoryData, - FailedSection, Framework, Requirement, RequirementsData, + TopFailedResult, } from "@/types/compliance"; import { @@ -74,7 +74,7 @@ export interface ComplianceMapper { data: Framework[], scanId: string | undefined, ) => AccordionItemProps[]; - getTopFailedSections: (mappedData: Framework[]) => FailedSection[]; + getTopFailedSections: (mappedData: Framework[]) => TopFailedResult; calculateCategoryHeatmapData: (complianceData: Framework[]) => CategoryData[]; getDetailsComponent: (requirement: Requirement) => React.ReactNode; } diff --git a/ui/lib/compliance/mitre.tsx b/ui/lib/compliance/mitre.tsx index 706cfcb36a..9f59ff1c75 100644 --- a/ui/lib/compliance/mitre.tsx +++ b/ui/lib/compliance/mitre.tsx @@ -5,13 +5,13 @@ import { FindingStatus } from "@/components/ui/table/status-finding-badge"; import { AttributesData, CategoryData, - FailedSection, Framework, MITREAttributesMetadata, Requirement, REQUIREMENT_STATUS, RequirementsData, RequirementStatus, + TopFailedResult, } from "@/types/compliance"; import { @@ -149,7 +149,7 @@ export const toAccordionItems = ( // Custom function for MITRE to get top failed sections grouped by tactics export const getTopFailedSections = ( mappedData: Framework[], -): FailedSection[] => { +): TopFailedResult => { const failedSectionMap = new Map(); mappedData.forEach((framework) => { @@ -175,10 +175,13 @@ export const getTopFailedSections = ( }); // Convert in descending order and slice top 5 - return Array.from(failedSectionMap.entries()) - .map(([name, data]) => ({ name, ...data })) - .sort((a, b) => b.total - a.total) - .slice(0, 5); // Top 5 + return { + items: Array.from(failedSectionMap.entries()) + .map(([name, data]) => ({ name, ...data })) + .sort((a, b) => b.total - a.total) + .slice(0, 5), + type: "sections", + }; }; // Custom function for MITRE to calculate category heatmap data grouped by tactics diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts index 2920c8e98b..7b566d5e67 100644 --- a/ui/types/compliance.ts +++ b/ui/types/compliance.ts @@ -76,6 +76,13 @@ export interface FailedSection { types?: { [key: string]: number }; } +export type TopFailedDataType = "sections" | "requirements"; + +export interface TopFailedResult { + items: FailedSection[]; + type: TopFailedDataType; +} + export interface RequirementsTotals { pass: number; fail: number;