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:
pedrooot
2025-12-05 13:20:37 +01:00
parent 06aac82e96
commit aad4b13cab
6 changed files with 82 additions and 21 deletions
@@ -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} />
+50 -9
View File
@@ -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 = (
+2 -2
View File
@@ -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;
}
+9 -6
View File
@@ -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
+7
View File
@@ -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;