fix(ui): show Top Failed Requirements for compliances without section hierarchy (#9471)

Co-authored-by: Alan Buscaglia <gentlemanprogramming@gmail.com>
This commit is contained in:
Pedro Martín
2025-12-09 16:28:47 +01:00
committed by GitHub
parent 20ed8b3d2d
commit 50286846e0
8 changed files with 204 additions and 94 deletions

View File

@@ -62,7 +62,7 @@ You are a code reviewer for the Prowler UI project. Analyze the full file conten
**RULES TO CHECK:** **RULES TO CHECK:**
1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }` 1. React Imports: NO `import * as React` or `import React, {` → Use `import { useState }`
2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const` 2. TypeScript: NO union types like `type X = "a" | "b"` → Use const-based: `const X = {...} as const`
3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. 3. Tailwind: NO `var()` or hex colors in className → Use Tailwind utilities and semantic color classes. Exception: `var()` is allowed when passing colors to chart/graph components that require CSS color strings (not Tailwind classes) for their APIs.
4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")` 4. cn(): Use for merging multiple classes or for conditionals (handles Tailwind conflicts with twMerge) → `cn(BUTTON_STYLES.base, BUTTON_STYLES.active, isLoading && "opacity-50")`
5. React 19: NO `useMemo`/`useCallback` without reason 5. React 19: NO `useMemo`/`useCallback` without reason
6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod. 6. Zod v4: Use `.min(1)` not `.nonempty()`, `z.email()` not `z.string().email()`. All inputs must be validated with Zod.

View File

@@ -24,6 +24,13 @@ All notable changes to the **Prowler UI** are documented in this file.
--- ---
## [1.14.3] (Prowler Unreleased)
### 🐞 Fixed
- Show top failed requirements in compliance specific view for compliance without sections [(#9471)](https://github.com/prowler-cloud/prowler/pull/9471)
---
## [1.14.2] (Prowler v5.14.2) ## [1.14.2] (Prowler v5.14.2)
### 🐞 Fixed ### 🐞 Fixed

View File

@@ -195,7 +195,7 @@ const SSRComplianceContent = async ({
{ pass: 0, fail: 0, manual: 0 }, { pass: 0, fail: 0, manual: 0 },
); );
const accordionItems = mapper.toAccordionItems(data, scanId); const accordionItems = mapper.toAccordionItems(data, scanId);
const topFailedSections = mapper.getTopFailedSections(data); const topFailedResult = mapper.getTopFailedSections(data);
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
@@ -205,7 +205,10 @@ const SSRComplianceContent = async ({
fail={totalRequirements.fail} fail={totalRequirements.fail}
manual={totalRequirements.manual} manual={totalRequirements.manual}
/> />
<TopFailedSectionsCard sections={topFailedSections} /> <TopFailedSectionsCard
sections={topFailedResult.items}
dataType={topFailedResult.type}
/>
{/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */} {/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */}
</div> </div>

View File

@@ -3,14 +3,20 @@
import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart"; import { HorizontalBarChart } from "@/components/graphs/horizontal-bar-chart";
import { BarDataPoint } from "@/components/graphs/types"; import { BarDataPoint } from "@/components/graphs/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { FailedSection } from "@/types/compliance"; import {
FailedSection,
TOP_FAILED_DATA_TYPE,
TopFailedDataType,
} from "@/types/compliance";
interface TopFailedSectionsCardProps { interface TopFailedSectionsCardProps {
sections: FailedSection[]; sections: FailedSection[];
dataType?: TopFailedDataType;
} }
export function TopFailedSectionsCard({ export function TopFailedSectionsCard({
sections, sections,
dataType = TOP_FAILED_DATA_TYPE.SECTIONS,
}: TopFailedSectionsCardProps) { }: TopFailedSectionsCardProps) {
// Transform FailedSection[] to BarDataPoint[] // Transform FailedSection[] to BarDataPoint[]
const total = sections.reduce((sum, section) => sum + section.total, 0); const total = sections.reduce((sum, section) => sum + section.total, 0);
@@ -22,13 +28,18 @@ export function TopFailedSectionsCard({
color: "var(--bg-fail-primary)", color: "var(--bg-fail-primary)",
})); }));
const title =
dataType === TOP_FAILED_DATA_TYPE.REQUIREMENTS
? "Top Failed Requirements"
: "Top Failed Sections";
return ( return (
<Card <Card
variant="base" variant="base"
className="flex min-h-[372px] w-full flex-col sm:min-w-[500px]" className="flex min-h-[372px] w-full flex-col sm:min-w-[500px]"
> >
<CardHeader> <CardHeader>
<CardTitle>Top Failed Sections</CardTitle> <CardTitle>{title}</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="flex flex-1 items-center justify-start"> <CardContent className="flex flex-1 items-center justify-start">
<HorizontalBarChart data={barData} /> <HorizontalBarChart data={barData} />

View File

@@ -1,14 +1,65 @@
import { import {
Category,
CategoryData, CategoryData,
Control,
FailedSection, FailedSection,
Framework, Framework,
Requirement,
REQUIREMENT_STATUS, REQUIREMENT_STATUS,
RequirementItemData, RequirementItemData,
RequirementsData, RequirementsData,
RequirementStatus, RequirementStatus,
TOP_FAILED_DATA_TYPE,
TopFailedDataType,
TopFailedResult,
} from "@/types/compliance"; } from "@/types/compliance";
// Type for the internal map used in getTopFailedSections
interface FailedSectionData {
total: number;
types: Record<string, number>;
}
/**
* Builds the TopFailedResult from the accumulated map data
*/
const buildTopFailedResult = (
map: Map<string, FailedSectionData>,
type: TopFailedDataType,
): TopFailedResult => ({
items: Array.from(map.entries())
.map(([name, data]): FailedSection => ({ name, ...data }))
.sort((a, b) => b.total - a.total)
.slice(0, 5),
type,
});
/**
* Checks if the framework uses a flat structure (requirements directly on framework)
* vs hierarchical structure (categories -> controls -> requirements)
*/
const hasFlatStructure = (frameworks: Framework[]): boolean =>
frameworks.some(
(framework) =>
(framework.requirements?.length ?? 0) > 0 &&
framework.categories.length === 0,
);
/**
* Increments the failed count for a given name in the map
*/
const incrementFailedCount = (
map: Map<string, FailedSectionData>,
name: string,
type: string,
): void => {
if (!map.has(name)) {
map.set(name, { total: 0, types: {} });
}
const data = map.get(name)!;
data.total += 1;
data.types[type] = (data.types[type] || 0) + 1;
};
export const updateCounters = ( export const updateCounters = (
target: { pass: number; fail: number; manual: number }, target: { pass: number; fail: number; manual: number },
status: RequirementStatus, status: RequirementStatus,
@@ -24,38 +75,45 @@ export const updateCounters = (
export const getTopFailedSections = ( export const getTopFailedSections = (
mappedData: Framework[], mappedData: Framework[],
): FailedSection[] => { ): TopFailedResult => {
const failedSectionMap = new Map(); const failedSectionMap = new Map<string, FailedSectionData>();
if (hasFlatStructure(mappedData)) {
// Handle flat structure: count failed requirements directly
mappedData.forEach((framework) => {
const directRequirements = framework.requirements ?? [];
directRequirements.forEach((requirement) => {
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
const type =
typeof requirement.type === "string" ? requirement.type : "Fails";
incrementFailedCount(failedSectionMap, requirement.name, type);
}
});
});
return buildTopFailedResult(
failedSectionMap,
TOP_FAILED_DATA_TYPE.REQUIREMENTS,
);
}
// Handle hierarchical structure: count by category (section)
mappedData.forEach((framework) => { mappedData.forEach((framework) => {
framework.categories.forEach((category) => { framework.categories.forEach((category) => {
category.controls.forEach((control) => { category.controls.forEach((control) => {
control.requirements.forEach((requirement) => { control.requirements.forEach((requirement) => {
if (requirement.status === REQUIREMENT_STATUS.FAIL) { if (requirement.status === REQUIREMENT_STATUS.FAIL) {
const sectionName = category.name; const type =
typeof requirement.type === "string" ? requirement.type : "Fails";
if (!failedSectionMap.has(sectionName)) { incrementFailedCount(failedSectionMap, category.name, type);
failedSectionMap.set(sectionName, { total: 0, types: {} });
}
const sectionData = failedSectionMap.get(sectionName);
sectionData.total += 1;
const type = requirement.type || "Fails";
sectionData.types[type as string] =
(sectionData.types[type as string] || 0) + 1;
} }
}); });
}); });
}); });
}); });
// Convert in descending order and slice top 5 return buildTopFailedResult(failedSectionMap, TOP_FAILED_DATA_TYPE.SECTIONS);
return Array.from(failedSectionMap.entries())
.map(([name, data]) => ({ name, ...data }))
.sort((a, b) => b.total - a.total)
.slice(0, 5); // Top 5
}; };
export const calculateCategoryHeatmapData = ( export const calculateCategoryHeatmapData = (
@@ -146,9 +204,9 @@ export const findOrCreateFramework = (
}; };
export const findOrCreateCategory = ( export const findOrCreateCategory = (
categories: any[], categories: Category[],
categoryName: string, categoryName: string,
) => { ): Category => {
let category = categories.find((c) => c.name === categoryName); let category = categories.find((c) => c.name === categoryName);
if (!category) { if (!category) {
category = { category = {
@@ -163,7 +221,10 @@ export const findOrCreateCategory = (
return category; return category;
}; };
export const findOrCreateControl = (controls: any[], controlLabel: string) => { export const findOrCreateControl = (
controls: Control[],
controlLabel: string,
): Control => {
let control = controls.find((c) => c.label === controlLabel); let control = controls.find((c) => c.label === controlLabel);
if (!control) { if (!control) {
control = { control = {
@@ -178,7 +239,7 @@ export const findOrCreateControl = (controls: any[], controlLabel: string) => {
return control; return control;
}; };
export const calculateFrameworkCounters = (frameworks: Framework[]) => { export const calculateFrameworkCounters = (frameworks: Framework[]): void => {
frameworks.forEach((framework) => { frameworks.forEach((framework) => {
// Reset framework counters // Reset framework counters
framework.pass = 0; framework.pass = 0;
@@ -186,9 +247,9 @@ export const calculateFrameworkCounters = (frameworks: Framework[]) => {
framework.manual = 0; framework.manual = 0;
// Handle flat structure (requirements directly in framework) // Handle flat structure (requirements directly in framework)
const directRequirements = (framework as any).requirements || []; const directRequirements = framework.requirements ?? [];
if (directRequirements.length > 0) { if (directRequirements.length > 0) {
directRequirements.forEach((requirement: Requirement) => { directRequirements.forEach((requirement) => {
updateCounters(framework, requirement.status); updateCounters(framework, requirement.status);
}); });
return; return;

View File

@@ -1,4 +1,4 @@
import React from "react"; import { createElement, ReactNode } from "react";
import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details"; import { AWSWellArchitectedCustomDetails } from "@/components/compliance/compliance-custom-details/aws-well-architected-details";
import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details"; import { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
@@ -14,10 +14,10 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
import { import {
AttributesData, AttributesData,
CategoryData, CategoryData,
FailedSection,
Framework, Framework,
Requirement, Requirement,
RequirementsData, RequirementsData,
TopFailedResult,
} from "@/types/compliance"; } from "@/types/compliance";
import { import {
@@ -74,9 +74,9 @@ export interface ComplianceMapper {
data: Framework[], data: Framework[],
scanId: string | undefined, scanId: string | undefined,
) => AccordionItemProps[]; ) => AccordionItemProps[];
getTopFailedSections: (mappedData: Framework[]) => FailedSection[]; getTopFailedSections: (mappedData: Framework[]) => TopFailedResult;
calculateCategoryHeatmapData: (complianceData: Framework[]) => CategoryData[]; calculateCategoryHeatmapData: (complianceData: Framework[]) => CategoryData[];
getDetailsComponent: (requirement: Requirement) => React.ReactNode; getDetailsComponent: (requirement: Requirement) => ReactNode;
} }
const getDefaultMapper = (): ComplianceMapper => ({ const getDefaultMapper = (): ComplianceMapper => ({
@@ -86,7 +86,7 @@ const getDefaultMapper = (): ComplianceMapper => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(GenericCustomDetails, { requirement }), createElement(GenericCustomDetails, { requirement }),
}); });
const getComplianceMappers = (): Record<string, ComplianceMapper> => ({ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
@@ -97,7 +97,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(C5CustomDetails, { requirement }), createElement(C5CustomDetails, { requirement }),
}, },
ENS: { ENS: {
mapComplianceData: mapENSComplianceData, mapComplianceData: mapENSComplianceData,
@@ -106,7 +106,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(ENSCustomDetails, { requirement }), createElement(ENSCustomDetails, { requirement }),
}, },
ISO27001: { ISO27001: {
mapComplianceData: mapISOComplianceData, mapComplianceData: mapISOComplianceData,
@@ -115,7 +115,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(ISOCustomDetails, { requirement }), createElement(ISOCustomDetails, { requirement }),
}, },
CIS: { CIS: {
mapComplianceData: mapCISComplianceData, mapComplianceData: mapCISComplianceData,
@@ -124,7 +124,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(CISCustomDetails, { requirement }), createElement(CISCustomDetails, { requirement }),
}, },
"AWS-Well-Architected-Framework-Security-Pillar": { "AWS-Well-Architected-Framework-Security-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData, mapComplianceData: mapAWSWellArchitectedComplianceData,
@@ -133,7 +133,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }), createElement(AWSWellArchitectedCustomDetails, { requirement }),
}, },
"AWS-Well-Architected-Framework-Reliability-Pillar": { "AWS-Well-Architected-Framework-Reliability-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData, mapComplianceData: mapAWSWellArchitectedComplianceData,
@@ -142,7 +142,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(AWSWellArchitectedCustomDetails, { requirement }), createElement(AWSWellArchitectedCustomDetails, { requirement }),
}, },
"KISA-ISMS-P": { "KISA-ISMS-P": {
mapComplianceData: mapKISAComplianceData, mapComplianceData: mapKISAComplianceData,
@@ -151,7 +151,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(KISACustomDetails, { requirement }), createElement(KISACustomDetails, { requirement }),
}, },
"MITRE-ATTACK": { "MITRE-ATTACK": {
mapComplianceData: mapMITREComplianceData, mapComplianceData: mapMITREComplianceData,
@@ -159,7 +159,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
getTopFailedSections: getMITRETopFailedSections, getTopFailedSections: getMITRETopFailedSections,
calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData, calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData,
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(MITRECustomDetails, { requirement }), createElement(MITRECustomDetails, { requirement }),
}, },
ProwlerThreatScore: { ProwlerThreatScore: {
mapComplianceData: mapThetaComplianceData, mapComplianceData: mapThetaComplianceData,
@@ -168,7 +168,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (complianceData: Framework[]) => calculateCategoryHeatmapData: (complianceData: Framework[]) =>
calculateCategoryHeatmapData(complianceData), calculateCategoryHeatmapData(complianceData),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(ThreatCustomDetails, { requirement }), createElement(ThreatCustomDetails, { requirement }),
}, },
CCC: { CCC: {
mapComplianceData: mapCCCComplianceData, mapComplianceData: mapCCCComplianceData,
@@ -177,7 +177,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) => calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data), calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) => getDetailsComponent: (requirement: Requirement) =>
React.createElement(CCCCustomDetails, { requirement }), createElement(CCCCustomDetails, { requirement }),
}, },
}); });

View File

@@ -12,6 +12,8 @@ import {
REQUIREMENT_STATUS, REQUIREMENT_STATUS,
RequirementsData, RequirementsData,
RequirementStatus, RequirementStatus,
TOP_FAILED_DATA_TYPE,
TopFailedResult,
} from "@/types/compliance"; } from "@/types/compliance";
import { import {
@@ -20,6 +22,12 @@ import {
findOrCreateFramework, findOrCreateFramework,
} from "./commons"; } from "./commons";
// Type for the internal map used in getTopFailedSections
interface FailedSectionData {
total: number;
types: Record<string, number>;
}
export const mapComplianceData = ( export const mapComplianceData = (
attributesData: AttributesData, attributesData: AttributesData,
requirementsData: RequirementsData, requirementsData: RequirementsData,
@@ -92,9 +100,9 @@ export const mapComplianceData = (
}) || [], }) || [],
}; };
// Add requirement directly to framework (store in a special property) // Add requirement directly to framework (flat structure - no categories)
(framework as any).requirements = (framework as any).requirements || []; framework.requirements = framework.requirements ?? [];
(framework as any).requirements.push(requirement); framework.requirements.push(requirement);
} }
// Calculate counters using common helper (works with flat structure) // Calculate counters using common helper (works with flat structure)
@@ -108,63 +116,63 @@ export const toAccordionItems = (
scanId: string | undefined, scanId: string | undefined,
): AccordionItemProps[] => { ): AccordionItemProps[] => {
return data.flatMap((framework) => { return data.flatMap((framework) => {
const requirements = (framework as any).requirements || []; const requirements = framework.requirements ?? [];
// Filter out requirements without metadata (can't be displayed in accordion) // Filter out requirements without metadata (can't be displayed in accordion)
const displayableRequirements = requirements.filter( const displayableRequirements = requirements.filter(
(requirement: Requirement) => requirement.hasMetadata !== false, (requirement) => requirement.hasMetadata !== false,
); );
return displayableRequirements.map( return displayableRequirements.map((requirement, i) => {
(requirement: Requirement, i: number) => { const itemKey = `${framework.name}-req-${i}`;
const itemKey = `${framework.name}-req-${i}`;
return { return {
key: itemKey, key: itemKey,
title: ( title: (
<ComplianceAccordionRequirementTitle <ComplianceAccordionRequirementTitle
type="" type=""
name={requirement.name} name={requirement.name}
status={requirement.status as FindingStatus} status={requirement.status as FindingStatus}
/> />
), ),
content: ( content: (
<ClientAccordionContent <ClientAccordionContent
key={`content-${itemKey}`} key={`content-${itemKey}`}
requirement={requirement} requirement={requirement}
scanId={scanId || ""} scanId={scanId || ""}
framework={framework.name} framework={framework.name}
disableFindings={ disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0 requirement.check_ids.length === 0 && requirement.manual === 0
} }
/> />
), ),
items: [], items: [],
}; };
}, });
);
}); });
}; };
// Custom function for MITRE to get top failed sections grouped by tactics // Custom function for MITRE to get top failed sections grouped by tactics
export const getTopFailedSections = ( export const getTopFailedSections = (
mappedData: Framework[], mappedData: Framework[],
): FailedSection[] => { ): TopFailedResult => {
const failedSectionMap = new Map(); const failedSectionMap = new Map<string, FailedSectionData>();
mappedData.forEach((framework) => { mappedData.forEach((framework) => {
const requirements = (framework as any).requirements || []; const requirements = framework.requirements ?? [];
requirements.forEach((requirement: Requirement) => { requirements.forEach((requirement) => {
if (requirement.status === REQUIREMENT_STATUS.FAIL) { if (requirement.status === REQUIREMENT_STATUS.FAIL) {
const tactics = (requirement.tactics as string[]) || []; const tactics = Array.isArray(requirement.tactics)
? (requirement.tactics as string[])
: [];
tactics.forEach((tactic) => { tactics.forEach((tactic) => {
if (!failedSectionMap.has(tactic)) { if (!failedSectionMap.has(tactic)) {
failedSectionMap.set(tactic, { total: 0, types: {} }); failedSectionMap.set(tactic, { total: 0, types: {} });
} }
const sectionData = failedSectionMap.get(tactic); const sectionData = failedSectionMap.get(tactic)!;
sectionData.total += 1; sectionData.total += 1;
const type = "Fails"; const type = "Fails";
@@ -175,10 +183,13 @@ export const getTopFailedSections = (
}); });
// Convert in descending order and slice top 5 // Convert in descending order and slice top 5
return Array.from(failedSectionMap.entries()) return {
.map(([name, data]) => ({ name, ...data })) items: Array.from(failedSectionMap.entries())
.sort((a, b) => b.total - a.total) .map(([name, data]): FailedSection => ({ name, ...data }))
.slice(0, 5); // Top 5 .sort((a, b) => b.total - a.total)
.slice(0, 5),
type: TOP_FAILED_DATA_TYPE.SECTIONS,
};
}; };
// Custom function for MITRE to calculate category heatmap data grouped by tactics // Custom function for MITRE to calculate category heatmap data grouped by tactics
@@ -197,10 +208,12 @@ export const calculateCategoryHeatmapData = (
// Aggregate data by tactics // Aggregate data by tactics
complianceData.forEach((framework) => { complianceData.forEach((framework) => {
const requirements = (framework as any).requirements || []; const requirements = framework.requirements ?? [];
requirements.forEach((requirement: Requirement) => { requirements.forEach((requirement) => {
const tactics = (requirement.tactics as string[]) || []; const tactics = Array.isArray(requirement.tactics)
? (requirement.tactics as string[])
: [];
tactics.forEach((tactic) => { tactics.forEach((tactic) => {
const existing = tacticMap.get(tactic) || { const existing = tacticMap.get(tactic) || {

View File

@@ -68,12 +68,27 @@ export interface Framework {
fail: number; fail: number;
manual: number; manual: number;
categories: Category[]; categories: Category[];
// Optional: flat structure for frameworks like MITRE that don't have categories
requirements?: Requirement[];
} }
export interface FailedSection { export interface FailedSection {
name: string; name: string;
total: number; total: number;
types?: { [key: string]: number }; types?: Record<string, number>;
}
export const TOP_FAILED_DATA_TYPE = {
SECTIONS: "sections",
REQUIREMENTS: "requirements",
} as const;
export type TopFailedDataType =
(typeof TOP_FAILED_DATA_TYPE)[keyof typeof TOP_FAILED_DATA_TYPE];
export interface TopFailedResult {
items: FailedSection[];
type: TopFailedDataType;
} }
export interface RequirementsTotals { export interface RequirementsTotals {
@@ -92,7 +107,7 @@ export interface ENSAttributesMetadata {
Nivel: string; Nivel: string;
Dimensiones: string[]; Dimensiones: string[];
ModoEjecucion: string; ModoEjecucion: string;
Dependencias: any[]; Dependencias: unknown[];
} }
export interface ISO27001AttributesMetadata { export interface ISO27001AttributesMetadata {