mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-02-09 02:30:43 +00:00
feat(ui): add Risk Radar component with API integration (#9532)
This commit is contained in:
@@ -15,7 +15,6 @@ export interface AttackSurfaceItem {
|
||||
label: string;
|
||||
failedFindings: number;
|
||||
totalFindings: number;
|
||||
checkIds: string[];
|
||||
}
|
||||
|
||||
const ATTACK_SURFACE_LABELS: Record<AttackSurfaceId, string> = {
|
||||
@@ -39,7 +38,6 @@ function mapAttackSurfaceItem(item: AttackSurfaceOverview): AttackSurfaceItem {
|
||||
label: ATTACK_SURFACE_LABELS[id] || item.id,
|
||||
failedFindings: item.attributes.failed_findings,
|
||||
totalFindings: item.attributes.total_findings,
|
||||
checkIds: item.attributes.check_ids ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./findings";
|
||||
export * from "./providers";
|
||||
export * from "./regions";
|
||||
export * from "./risk-plot";
|
||||
export * from "./risk-radar";
|
||||
export * from "./services";
|
||||
export * from "./severity-trends";
|
||||
export * from "./threat-score";
|
||||
|
||||
3
ui/actions/overview/risk-radar/index.ts
Normal file
3
ui/actions/overview/risk-radar/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./risk-radar";
|
||||
export * from "./risk-radar.adapter";
|
||||
export * from "./types";
|
||||
110
ui/actions/overview/risk-radar/risk-radar.adapter.ts
Normal file
110
ui/actions/overview/risk-radar/risk-radar.adapter.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { RadarDataPoint } from "@/components/graphs/types";
|
||||
|
||||
import { CategoryOverview, CategoryOverviewResponse } from "./types";
|
||||
|
||||
// Category IDs from the API
|
||||
const CATEGORY_IDS = {
|
||||
E3: "e3",
|
||||
E5: "e5",
|
||||
ENCRYPTION: "encryption",
|
||||
FORENSICS_READY: "forensics-ready",
|
||||
IAM: "iam",
|
||||
INTERNET_EXPOSED: "internet-exposed",
|
||||
LOGGING: "logging",
|
||||
NETWORK: "network",
|
||||
PUBLICLY_ACCESSIBLE: "publicly-accessible",
|
||||
SECRETS: "secrets",
|
||||
STORAGE: "storage",
|
||||
THREAT_DETECTION: "threat-detection",
|
||||
TRUSTBOUNDARIES: "trustboundaries",
|
||||
UNUSED: "unused",
|
||||
} as const;
|
||||
|
||||
export type CategoryId = (typeof CATEGORY_IDS)[keyof typeof CATEGORY_IDS];
|
||||
|
||||
// Human-readable labels for category IDs
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
[CATEGORY_IDS.E3]: "E3",
|
||||
[CATEGORY_IDS.E5]: "E5",
|
||||
[CATEGORY_IDS.ENCRYPTION]: "Encryption",
|
||||
[CATEGORY_IDS.FORENSICS_READY]: "Forensics Ready",
|
||||
[CATEGORY_IDS.IAM]: "IAM",
|
||||
[CATEGORY_IDS.INTERNET_EXPOSED]: "Internet Exposed",
|
||||
[CATEGORY_IDS.LOGGING]: "Logging",
|
||||
[CATEGORY_IDS.NETWORK]: "Network",
|
||||
[CATEGORY_IDS.PUBLICLY_ACCESSIBLE]: "Publicly Accessible",
|
||||
[CATEGORY_IDS.SECRETS]: "Secrets",
|
||||
[CATEGORY_IDS.STORAGE]: "Storage",
|
||||
[CATEGORY_IDS.THREAT_DETECTION]: "Threat Detection",
|
||||
[CATEGORY_IDS.TRUSTBOUNDARIES]: "Trust Boundaries",
|
||||
[CATEGORY_IDS.UNUSED]: "Unused",
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a category ID to a human-readable label.
|
||||
* Falls back to capitalizing the ID if not found in the mapping.
|
||||
*/
|
||||
function getCategoryLabel(id: string): string {
|
||||
if (CATEGORY_LABELS[id]) {
|
||||
return CATEGORY_LABELS[id];
|
||||
}
|
||||
// Fallback: capitalize and replace hyphens with spaces
|
||||
return id
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the percentage of new failed findings relative to total failed findings.
|
||||
*/
|
||||
function calculateChangePercentage(
|
||||
newFailedFindings: number,
|
||||
failedFindings: number,
|
||||
): number {
|
||||
if (failedFindings === 0) return 0;
|
||||
return Math.round((newFailedFindings / failedFindings) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps a single category overview item to a RadarDataPoint.
|
||||
*/
|
||||
function mapCategoryToRadarPoint(item: CategoryOverview): RadarDataPoint {
|
||||
const { id, attributes } = item;
|
||||
const { failed_findings, new_failed_findings, severity } = attributes;
|
||||
|
||||
return {
|
||||
category: getCategoryLabel(id),
|
||||
categoryId: id,
|
||||
value: failed_findings,
|
||||
change: calculateChangePercentage(new_failed_findings, failed_findings),
|
||||
severityData: [
|
||||
{ name: "Critical", value: severity.critical },
|
||||
{ name: "High", value: severity.high },
|
||||
{ name: "Medium", value: severity.medium },
|
||||
{ name: "Low", value: severity.low },
|
||||
{ name: "Info", value: severity.informational },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts the category overview API response to RadarDataPoint[] format.
|
||||
* Filters out categories with no failed findings.
|
||||
*
|
||||
* @param response - The category overview API response
|
||||
* @returns An array of RadarDataPoint objects for the radar chart
|
||||
*/
|
||||
export function adaptCategoryOverviewToRadarData(
|
||||
response: CategoryOverviewResponse | undefined,
|
||||
): RadarDataPoint[] {
|
||||
if (!response?.data || response.data.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Map all categories to radar points, filtering out those with no failed findings
|
||||
return response.data
|
||||
.filter((item) => item.attributes.failed_findings > 0)
|
||||
.map(mapCategoryToRadarPoint)
|
||||
.sort((a, b) => b.value - a.value); // Sort by failed findings descending
|
||||
}
|
||||
34
ui/actions/overview/risk-radar/risk-radar.ts
Normal file
34
ui/actions/overview/risk-radar/risk-radar.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
"use server";
|
||||
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
import { CategoryOverviewResponse } from "./types";
|
||||
|
||||
export const getCategoryOverview = async ({
|
||||
filters = {},
|
||||
}: {
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}): Promise<CategoryOverviewResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/categories`);
|
||||
|
||||
// Handle multiple filters
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (key !== "filter[search]" && value !== undefined) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), {
|
||||
headers,
|
||||
});
|
||||
|
||||
return handleApiResponse(response);
|
||||
} catch (error) {
|
||||
console.error("Error fetching category overview:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
1
ui/actions/overview/risk-radar/types/index.ts
Normal file
1
ui/actions/overview/risk-radar/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./risk-radar.types";
|
||||
32
ui/actions/overview/risk-radar/types/risk-radar.types.ts
Normal file
32
ui/actions/overview/risk-radar/types/risk-radar.types.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// Category Overview Types
|
||||
// Corresponds to the /overviews/categories endpoint
|
||||
|
||||
interface OverviewResponseMeta {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface CategorySeverity {
|
||||
informational: number;
|
||||
low: number;
|
||||
medium: number;
|
||||
high: number;
|
||||
critical: number;
|
||||
}
|
||||
|
||||
export interface CategoryOverviewAttributes {
|
||||
total_findings: number;
|
||||
failed_findings: number;
|
||||
new_failed_findings: number;
|
||||
severity: CategorySeverity;
|
||||
}
|
||||
|
||||
export interface CategoryOverview {
|
||||
type: "category-overviews";
|
||||
id: string;
|
||||
attributes: CategoryOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface CategoryOverviewResponse {
|
||||
data: CategoryOverview[];
|
||||
meta: OverviewResponseMeta;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
getDateFromForTimeRange,
|
||||
type TimeRange,
|
||||
} from "@/app/(prowler)/_new-overview/severity-over-time/_constants/time-range.constants";
|
||||
} from "@/app/(prowler)/_overview/severity-over-time/_constants/time-range.constants";
|
||||
import { apiBaseUrl, getAuthHeaders } from "@/lib";
|
||||
import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user