mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-23 20:42:02 +00:00
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
This commit is contained in:
@@ -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 (
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -205,7 +205,10 @@ const SSRComplianceContent = async ({
|
||||
fail={totalRequirements.fail}
|
||||
manual={totalRequirements.manual}
|
||||
/>
|
||||
<TopFailedSectionsCard sections={topFailedSections} />
|
||||
<TopFailedSectionsCard
|
||||
sections={topFailedResult.items}
|
||||
dataType={topFailedResult.type}
|
||||
/>
|
||||
{/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[372px] w-full flex-col sm:min-w-[500px]"
|
||||
>
|
||||
<CardHeader>
|
||||
<CardTitle>Top Failed Sections</CardTitle>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-1 items-center justify-start">
|
||||
<HorizontalBarChart data={barData} />
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user