Compare commits

..

9 Commits

Author SHA1 Message Date
Alan Buscaglia
f03d83872e refactor(ui): improve type safety in compliance module
- Add optional requirements property to Framework interface for flat structures
- Replace union type with const-based pattern for TopFailedDataType
- Remove all 'as any' casts from commons.tsx and mitre.tsx
- Extract helper functions (buildTopFailedResult, hasFlatStructure, incrementFailedCount) for DRY
- Type Maps properly with explicit generic parameters
- Add proper return types to findOrCreateCategory and findOrCreateControl
- Replace any[] with unknown[] in ENSAttributesMetadata
- Fix React import to use named imports (createElement, ReactNode)
2025-12-05 14:22:04 +01:00
Alan Buscaglia
bb620022f5 fix(ui): use severity sum for failed findings count in Risk Plot
Ensure tooltip and horizontal bar chart show consistent numbers by
using the sum of severity counts instead of separate API field
2025-12-05 13:40:54 +01:00
Alan Buscaglia
27a81defec refactor(ui): simplify Risk Plot - use ThreatScore directly, remove mocks
KISS/DRY improvements:
- Remove unnecessary convertToRiskScore - use ThreatScore (0-100) directly
- Reuse RiskPlotPoint type from actions instead of duplicate ScatterPoint
- Remove CustomLegend wrapper - inline ChartLegend usage
- Remove mock data file (risk-plot-view.tsx)
- Rename 'Risk Score' to 'Threat Score' for consistency
- Update XAxis domain from 0-10 to 0-100
- Simplify handlers with functional updates
2025-12-05 13:28:05 +01:00
Alan Buscaglia
a81293d2ea fix(ui): remove rounding from Risk Score conversion to preserve decimals 2025-12-05 13:22:34 +01:00
Alan Buscaglia
80427dd127 fix(ui): show raw Risk Score percentage without rounding 2025-12-05 13:20:20 +01:00
Alan Buscaglia
14e9506b87 fix(ui): simplify Risk Score tooltip format
- Remove dotted line separator
- Show percentage inline with Risk Score label
2025-12-05 13:19:53 +01:00
Alan Buscaglia
3e72d575d4 fix(ui): format Risk Score tooltip to match ThreatScore style
- Display Risk Score as percentage (0-100%) with dotted line separator
- Match visual style of ThreatScore tooltip component
2025-12-05 13:17:44 +01:00
Alan Buscaglia
79825d35fc docs(ui): add Risk Plot to CHANGELOG 2025-12-05 13:14:04 +01:00
Alan Buscaglia
6215c1ba46 fix(ui): correct Risk Score calculation and add severity percentages
- Fix Risk Score formula: higher score = better (same as ThreatScore)
- Add percentage calculation for severity breakdown in bar chart
- Improve type safety in createScatterDotShape with proper JSDoc
- Replace inline styles with Tailwind classes on HTML elements
- Add documentation explaining CSS variables usage for Recharts compatibility
2025-12-05 13:07:14 +01:00
15 changed files with 281 additions and 177 deletions

View File

@@ -1206,13 +1206,7 @@
"awstransform": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-west-2",
"us-east-1"
],
"aws-cn": [],
@@ -3225,7 +3219,6 @@
"ap-southeast-1",
"ap-southeast-2",
"ap-southeast-5",
"ap-southeast-7",
"ca-central-1",
"eu-central-1",
"eu-central-2",
@@ -6053,6 +6046,27 @@
]
}
},
"iotfleethub": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"iotfleetwise": {
"regions": {
"aws": [
@@ -8077,15 +8091,6 @@
"aws-us-gov": []
}
},
"nova-act": {
"regions": {
"aws": [
"us-east-1"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"oam": {
"regions": {
"aws": [
@@ -8917,6 +8922,25 @@
"aws-us-gov": []
}
},
"qldb-session": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"quicksight": {
"regions": {
"aws": [
@@ -9354,7 +9378,6 @@
"ap-southeast-3",
"ap-southeast-4",
"ap-southeast-5",
"ap-southeast-6",
"ap-southeast-7",
"ca-central-1",
"ca-west-1",
@@ -9987,28 +10010,6 @@
]
}
},
"s3vectors": {
"regions": {
"aws": [
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"eu-central-1",
"eu-north-1",
"eu-west-1",
"eu-west-2",
"eu-west-3",
"us-east-1",
"us-east-2",
"us-west-2"
],
"aws-cn": [],
"aws-us-gov": []
}
},
"sagemaker": {
"regions": {
"aws": [

View File

@@ -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.

View File

@@ -6,7 +6,6 @@ All notable changes to the **Prowler UI** are documented in this file.
### 🚀 Added
- Risk Plot component with interactive legend and severity navigation to Overview page [(#9469)](https://github.com/prowler-cloud/prowler/pull/9469)
- Navigation progress bar for page transitions using Next.js `onRouterTransitionStart` [(#9465)](https://github.com/prowler-cloud/prowler/pull/9465)
- Finding Severity Over Time chart component to Overview page [(#9405)](https://github.com/prowler-cloud/prowler/pull/9405)
- Attack Surface component to Overview page [(#9412)](https://github.com/prowler-cloud/prowler/pull/9412)
@@ -24,6 +23,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

View File

@@ -1,9 +1,5 @@
"use server";
import {
getDateFromForTimeRange,
type TimeRange,
} from "@/app/(prowler)/_new-overview/severity-over-time/_constants/time-range.constants";
import { apiBaseUrl, getAuthHeaders } from "@/lib";
import { handleApiResponse } from "@/lib/server-actions-helper";
@@ -13,6 +9,20 @@ import {
FindingsSeverityOverTimeResponse,
} from "./types";
const TIME_RANGE_VALUES = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
type TimeRange = (typeof TIME_RANGE_VALUES)[keyof typeof TIME_RANGE_VALUES];
const TIME_RANGE_DAYS: Record<TimeRange, number> = {
"5D": 5,
"1W": 7,
"1M": 30,
};
export type SeverityTrendsResult =
| { status: "success"; data: AdaptedSeverityTrendsResponse }
| { status: "empty" }
@@ -66,9 +76,21 @@ export const getSeverityTrendsByTimeRange = async ({
timeRange: TimeRange;
filters?: Record<string, string | string[] | undefined>;
}): Promise<SeverityTrendsResult> => {
const days = TIME_RANGE_DAYS[timeRange];
if (!days) {
console.error("Invalid time range provided");
return { status: "error" };
}
const endDate = new Date();
const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1000);
const dateFrom = startDate.toISOString().split("T")[0];
const dateFilters = {
...filters,
"filter[date_from]": getDateFromForTimeRange(timeRange),
date_from: dateFrom,
};
return getFindingsSeverityTrends({ filters: dateFilters });

View File

@@ -13,7 +13,6 @@ import {
SeverityLevel,
} from "@/types/severities";
import { DEFAULT_TIME_RANGE } from "../_constants/time-range.constants";
import { type TimeRange, TimeRangeSelector } from "./time-range-selector";
interface FindingSeverityOverTimeProps {
@@ -25,7 +24,7 @@ export const FindingSeverityOverTime = ({
}: FindingSeverityOverTimeProps) => {
const router = useRouter();
const searchParams = useSearchParams();
const [timeRange, setTimeRange] = useState<TimeRange>(DEFAULT_TIME_RANGE);
const [timeRange, setTimeRange] = useState<TimeRange>("5D");
const [data, setData] = useState<LineDataPoint[]>(initialData);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

View File

@@ -2,12 +2,14 @@
import { cn } from "@/lib/utils";
import {
TIME_RANGE_OPTIONS,
type TimeRange,
} from "../_constants/time-range.constants";
const TIME_RANGE_OPTIONS = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
export type { TimeRange };
export type TimeRange =
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
interface TimeRangeSelectorProps {
value: TimeRange;

View File

@@ -1 +0,0 @@
export * from "./time-range.constants";

View File

@@ -1,23 +0,0 @@
export const TIME_RANGE_OPTIONS = {
FIVE_DAYS: "5D",
ONE_WEEK: "1W",
ONE_MONTH: "1M",
} as const;
export type TimeRange =
(typeof TIME_RANGE_OPTIONS)[keyof typeof TIME_RANGE_OPTIONS];
export const TIME_RANGE_DAYS: Record<TimeRange, number> = {
"5D": 5,
"1W": 7,
"1M": 30,
};
export const DEFAULT_TIME_RANGE: TimeRange = "5D";
export const getDateFromForTimeRange = (timeRange: TimeRange): string => {
const days = TIME_RANGE_DAYS[timeRange];
const date = new Date();
date.setDate(date.getDate() - days);
return date.toISOString().split("T")[0];
};

View File

@@ -1,11 +1,10 @@
import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends";
import { getFindingsSeverityTrends } from "@/actions/overview/severity-trends";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/shadcn";
import { pickFilterParams } from "../_lib/filter-params";
import { SSRComponentProps } from "../_types";
import { FindingSeverityOverTime } from "./_components/finding-severity-over-time";
import { FindingSeverityOverTimeSkeleton } from "./_components/finding-severity-over-time.skeleton";
import { DEFAULT_TIME_RANGE } from "./_constants/time-range.constants";
export { FindingSeverityOverTimeSkeleton };
@@ -26,11 +25,7 @@ export const FindingSeverityOverTimeSSR = async ({
searchParams,
}: SSRComponentProps) => {
const filters = pickFilterParams(searchParams);
const result = await getSeverityTrendsByTimeRange({
timeRange: DEFAULT_TIME_RANGE,
filters,
});
const result = await getFindingsSeverityTrends({ filters });
if (result.status === "error") {
return <EmptyState message="Failed to load severity trends data" />;

View File

@@ -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>

View File

@@ -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 (
<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} />

View File

@@ -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<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 = (
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<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) => {
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;

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 { 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<string, ComplianceMapper> => ({
@@ -97,7 +97,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
getTopFailedSections: getMITRETopFailedSections,
calculateCategoryHeatmapData: calculateMITRECategoryHeatmapData,
getDetailsComponent: (requirement: Requirement) =>
React.createElement(MITRECustomDetails, { requirement }),
createElement(MITRECustomDetails, { requirement }),
},
ProwlerThreatScore: {
mapComplianceData: mapThetaComplianceData,
@@ -168,7 +168,7 @@ const getComplianceMappers = (): Record<string, ComplianceMapper> => ({
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<string, ComplianceMapper> => ({
calculateCategoryHeatmapData: (data: Framework[]) =>
calculateCategoryHeatmapData(data),
getDetailsComponent: (requirement: Requirement) =>
React.createElement(CCCCustomDetails, { requirement }),
createElement(CCCCustomDetails, { requirement }),
},
});

View File

@@ -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<string, number>;
}
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: (
<ComplianceAccordionRequirementTitle
type=""
name={requirement.name}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
key={`content-${itemKey}`}
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
items: [],
};
},
);
return {
key: itemKey,
title: (
<ComplianceAccordionRequirementTitle
type=""
name={requirement.name}
status={requirement.status as FindingStatus}
/>
),
content: (
<ClientAccordionContent
key={`content-${itemKey}`}
requirement={requirement}
scanId={scanId || ""}
framework={framework.name}
disableFindings={
requirement.check_ids.length === 0 && requirement.manual === 0
}
/>
),
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<string, FailedSectionData>();
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) || {

View File

@@ -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<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 {
@@ -92,7 +107,7 @@ export interface ENSAttributesMetadata {
Nivel: string;
Dimensiones: string[];
ModoEjecucion: string;
Dependencias: any[];
Dependencias: unknown[];
}
export interface ISO27001AttributesMetadata {