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;