mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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) || {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user