diff --git a/ui/.husky/pre-commit b/ui/.husky/pre-commit
index c82401e6e5..9e12cf3a85 100755
--- a/ui/.husky/pre-commit
+++ b/ui/.husky/pre-commit
@@ -62,7 +62,7 @@ You are a code reviewer for the Prowler UI project. Analyze the full file conten
**RULES TO CHECK:**
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`
-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")`
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.
diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md
index 7e40c7e078..ff45e1de04 100644
--- a/ui/CHANGELOG.md
+++ b/ui/CHANGELOG.md
@@ -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)
### 🐞 Fixed
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..f2429cb597 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,20 @@
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,
+ TOP_FAILED_DATA_TYPE,
+ TopFailedDataType,
+} from "@/types/compliance";
interface TopFailedSectionsCardProps {
sections: FailedSection[];
+ dataType?: TopFailedDataType;
}
export function TopFailedSectionsCard({
sections,
+ dataType = TOP_FAILED_DATA_TYPE.SECTIONS,
}: TopFailedSectionsCardProps) {
// Transform FailedSection[] to BarDataPoint[]
const total = sections.reduce((sum, section) => sum + section.total, 0);
@@ -22,13 +28,18 @@ export function TopFailedSectionsCard({
color: "var(--bg-fail-primary)",
}));
+ const title =
+ dataType === TOP_FAILED_DATA_TYPE.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..dc96678e2c 100644
--- a/ui/lib/compliance/commons.tsx
+++ b/ui/lib/compliance/commons.tsx
@@ -1,14 +1,65 @@
import {
+ Category,
CategoryData,
+ Control,
FailedSection,
Framework,
- Requirement,
REQUIREMENT_STATUS,
RequirementItemData,
RequirementsData,
RequirementStatus,
+ TOP_FAILED_DATA_TYPE,
+ TopFailedDataType,
+ TopFailedResult,
} from "@/types/compliance";
+// Type for the internal map used in getTopFailedSections
+interface FailedSectionData {
+ total: number;
+ types: Record;
+}
+
+/**
+ * Builds the TopFailedResult from the accumulated map data
+ */
+const buildTopFailedResult = (
+ map: Map,
+ 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,
+ 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 = (
target: { pass: number; fail: number; manual: number },
status: RequirementStatus,
@@ -24,38 +75,45 @@ export const updateCounters = (
export const getTopFailedSections = (
mappedData: Framework[],
-): FailedSection[] => {
- const failedSectionMap = new Map();
+): TopFailedResult => {
+ const failedSectionMap = new Map();
+ 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) => {
framework.categories.forEach((category) => {
category.controls.forEach((control) => {
control.requirements.forEach((requirement) => {
if (requirement.status === REQUIREMENT_STATUS.FAIL) {
- const sectionName = category.name;
-
- if (!failedSectionMap.has(sectionName)) {
- 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;
+ const type =
+ typeof requirement.type === "string" ? requirement.type : "Fails";
+ incrementFailedCount(failedSectionMap, category.name, type);
}
});
});
});
});
- // 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 buildTopFailedResult(failedSectionMap, TOP_FAILED_DATA_TYPE.SECTIONS);
};
export const calculateCategoryHeatmapData = (
@@ -146,9 +204,9 @@ export const findOrCreateFramework = (
};
export const findOrCreateCategory = (
- categories: any[],
+ categories: Category[],
categoryName: string,
-) => {
+): Category => {
let category = categories.find((c) => c.name === categoryName);
if (!category) {
category = {
@@ -163,7 +221,10 @@ export const findOrCreateCategory = (
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);
if (!control) {
control = {
@@ -178,7 +239,7 @@ export const findOrCreateControl = (controls: any[], controlLabel: string) => {
return control;
};
-export const calculateFrameworkCounters = (frameworks: Framework[]) => {
+export const calculateFrameworkCounters = (frameworks: Framework[]): void => {
frameworks.forEach((framework) => {
// Reset framework counters
framework.pass = 0;
@@ -186,9 +247,9 @@ export const calculateFrameworkCounters = (frameworks: Framework[]) => {
framework.manual = 0;
// Handle flat structure (requirements directly in framework)
- const directRequirements = (framework as any).requirements || [];
+ const directRequirements = framework.requirements ?? [];
if (directRequirements.length > 0) {
- directRequirements.forEach((requirement: Requirement) => {
+ directRequirements.forEach((requirement) => {
updateCounters(framework, requirement.status);
});
return;
diff --git a/ui/lib/compliance/compliance-mapper.ts b/ui/lib/compliance/compliance-mapper.ts
index da47130491..2446156e2b 100644
--- a/ui/lib/compliance/compliance-mapper.ts
+++ b/ui/lib/compliance/compliance-mapper.ts
@@ -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 { C5CustomDetails } from "@/components/compliance/compliance-custom-details/c5-details";
@@ -14,10 +14,10 @@ import { AccordionItemProps } from "@/components/ui/accordion/Accordion";
import {
AttributesData,
CategoryData,
- FailedSection,
Framework,
Requirement,
RequirementsData,
+ TopFailedResult,
} from "@/types/compliance";
import {
@@ -74,9 +74,9 @@ 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;
+ getDetailsComponent: (requirement: Requirement) => ReactNode;
}
const getDefaultMapper = (): ComplianceMapper => ({
@@ -86,7 +86,7 @@ const getDefaultMapper = (): ComplianceMapper => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(GenericCustomDetails, { requirement }),
+ createElement(GenericCustomDetails, { requirement }),
});
const getComplianceMappers = (): Record => ({
@@ -97,7 +97,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(C5CustomDetails, { requirement }),
+ createElement(C5CustomDetails, { requirement }),
},
ENS: {
mapComplianceData: mapENSComplianceData,
@@ -106,7 +106,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(ENSCustomDetails, { requirement }),
+ createElement(ENSCustomDetails, { requirement }),
},
ISO27001: {
mapComplianceData: mapISOComplianceData,
@@ -115,7 +115,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(ISOCustomDetails, { requirement }),
+ createElement(ISOCustomDetails, { requirement }),
},
CIS: {
mapComplianceData: mapCISComplianceData,
@@ -124,7 +124,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(CISCustomDetails, { requirement }),
+ createElement(CISCustomDetails, { requirement }),
},
"AWS-Well-Architected-Framework-Security-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData,
@@ -133,7 +133,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
+ createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
"AWS-Well-Architected-Framework-Reliability-Pillar": {
mapComplianceData: mapAWSWellArchitectedComplianceData,
@@ -142,7 +142,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(AWSWellArchitectedCustomDetails, { requirement }),
+ createElement(AWSWellArchitectedCustomDetails, { requirement }),
},
"KISA-ISMS-P": {
mapComplianceData: mapKISAComplianceData,
@@ -151,7 +151,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(KISACustomDetails, { requirement }),
+ createElement(KISACustomDetails, { requirement }),
},
"MITRE-ATTACK": {
mapComplianceData: mapMITREComplianceData,
@@ -159,7 +159,7 @@ const getComplianceMappers = (): Record => ({
getTopFailedSections: getMITRETopFailedSections,
calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData,
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(MITRECustomDetails, { requirement }),
+ createElement(MITRECustomDetails, { requirement }),
},
ProwlerThreatScore: {
mapComplianceData: mapThetaComplianceData,
@@ -168,7 +168,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (complianceData: Framework[]) =>
calculateCategoryHeatmapData(complianceData),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(ThreatCustomDetails, { requirement }),
+ createElement(ThreatCustomDetails, { requirement }),
},
CCC: {
mapComplianceData: mapCCCComplianceData,
@@ -177,7 +177,7 @@ const getComplianceMappers = (): Record => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
- React.createElement(CCCCustomDetails, { requirement }),
+ createElement(CCCCustomDetails, { requirement }),
},
});
diff --git a/ui/lib/compliance/mitre.tsx b/ui/lib/compliance/mitre.tsx
index 706cfcb36a..1358a2c240 100644
--- a/ui/lib/compliance/mitre.tsx
+++ b/ui/lib/compliance/mitre.tsx
@@ -12,6 +12,8 @@ import {
REQUIREMENT_STATUS,
RequirementsData,
RequirementStatus,
+ TOP_FAILED_DATA_TYPE,
+ TopFailedResult,
} from "@/types/compliance";
import {
@@ -20,6 +22,12 @@ import {
findOrCreateFramework,
} from "./commons";
+// Type for the internal map used in getTopFailedSections
+interface FailedSectionData {
+ total: number;
+ types: Record;
+}
+
export const mapComplianceData = (
attributesData: AttributesData,
requirementsData: RequirementsData,
@@ -92,9 +100,9 @@ export const mapComplianceData = (
}) || [],
};
- // Add requirement directly to framework (store in a special property)
- (framework as any).requirements = (framework as any).requirements || [];
- (framework as any).requirements.push(requirement);
+ // Add requirement directly to framework (flat structure - no categories)
+ framework.requirements = framework.requirements ?? [];
+ framework.requirements.push(requirement);
}
// Calculate counters using common helper (works with flat structure)
@@ -108,63 +116,63 @@ export const toAccordionItems = (
scanId: string | undefined,
): AccordionItemProps[] => {
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)
const displayableRequirements = requirements.filter(
- (requirement: Requirement) => requirement.hasMetadata !== false,
+ (requirement) => requirement.hasMetadata !== false,
);
- return displayableRequirements.map(
- (requirement: Requirement, i: number) => {
- const itemKey = `${framework.name}-req-${i}`;
+ return displayableRequirements.map((requirement, i) => {
+ const itemKey = `${framework.name}-req-${i}`;
- return {
- key: itemKey,
- title: (
-
- ),
- content: (
-
- ),
- items: [],
- };
- },
- );
+ return {
+ key: itemKey,
+ title: (
+
+ ),
+ content: (
+
+ ),
+ items: [],
+ };
+ });
});
};
// Custom function for MITRE to get top failed sections grouped by tactics
export const getTopFailedSections = (
mappedData: Framework[],
-): FailedSection[] => {
- const failedSectionMap = new Map();
+): TopFailedResult => {
+ const failedSectionMap = new Map();
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) {
- const tactics = (requirement.tactics as string[]) || [];
+ const tactics = Array.isArray(requirement.tactics)
+ ? (requirement.tactics as string[])
+ : [];
tactics.forEach((tactic) => {
if (!failedSectionMap.has(tactic)) {
failedSectionMap.set(tactic, { total: 0, types: {} });
}
- const sectionData = failedSectionMap.get(tactic);
+ const sectionData = failedSectionMap.get(tactic)!;
sectionData.total += 1;
const type = "Fails";
@@ -175,10 +183,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]): FailedSection => ({ name, ...data }))
+ .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
@@ -197,10 +208,12 @@ export const calculateCategoryHeatmapData = (
// Aggregate data by tactics
complianceData.forEach((framework) => {
- const requirements = (framework as any).requirements || [];
+ const requirements = framework.requirements ?? [];
- requirements.forEach((requirement: Requirement) => {
- const tactics = (requirement.tactics as string[]) || [];
+ requirements.forEach((requirement) => {
+ const tactics = Array.isArray(requirement.tactics)
+ ? (requirement.tactics as string[])
+ : [];
tactics.forEach((tactic) => {
const existing = tacticMap.get(tactic) || {
diff --git a/ui/types/compliance.ts b/ui/types/compliance.ts
index 2920c8e98b..5c77ed1ee0 100644
--- a/ui/types/compliance.ts
+++ b/ui/types/compliance.ts
@@ -68,12 +68,27 @@ export interface Framework {
fail: number;
manual: number;
categories: Category[];
+ // Optional: flat structure for frameworks like MITRE that don't have categories
+ requirements?: Requirement[];
}
export interface FailedSection {
name: string;
total: number;
- types?: { [key: string]: number };
+ types?: Record;
+}
+
+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 {
@@ -92,7 +107,7 @@ export interface ENSAttributesMetadata {
Nivel: string;
Dimensiones: string[];
ModoEjecucion: string;
- Dependencias: any[];
+ Dependencias: unknown[];
}
export interface ISO27001AttributesMetadata {