mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-28 11:02:20 +00:00
Compare commits
9 Commits
aws-region
...
feat/PROWL
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f03d83872e | ||
|
|
bb620022f5 | ||
|
|
27a81defec | ||
|
|
a81293d2ea | ||
|
|
80427dd127 | ||
|
|
14e9506b87 | ||
|
|
3e72d575d4 | ||
|
|
79825d35fc | ||
|
|
6215c1ba46 |
@@ -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": [
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./time-range.constants";
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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" />;
|
||||
|
||||
@@ -195,7 +195,7 @@ const SSRComplianceContent = async ({
|
||||
{ pass: 0, fail: 0, manual: 0 },
|
||||
);
|
||||
const accordionItems = mapper.toAccordionItems(data, scanId);
|
||||
const topFailedSections = mapper.getTopFailedSections(data);
|
||||
const topFailedResult = mapper.getTopFailedSections(data);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
@@ -205,7 +205,10 @@ const SSRComplianceContent = async ({
|
||||
fail={totalRequirements.fail}
|
||||
manual={totalRequirements.manual}
|
||||
/>
|
||||
<TopFailedSectionsCard sections={topFailedSections} />
|
||||
<TopFailedSectionsCard
|
||||
sections={topFailedResult.items}
|
||||
dataType={topFailedResult.type}
|
||||
/>
|
||||
{/* <SectionsFailureRateCard categories={categoryHeatmapData} /> */}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,14 +3,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} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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) || {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user