feat(ui): add Risk Radar component with API integration (#9532)

This commit is contained in:
Alan Buscaglia
2025-12-15 17:02:21 +01:00
committed by GitHub
parent 3eb278cb9f
commit 8f361e7e8d
71 changed files with 431 additions and 309 deletions

View File

@@ -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 ?? [],
};
}

View File

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

View File

@@ -0,0 +1,3 @@
export * from "./risk-radar";
export * from "./risk-radar.adapter";
export * from "./types";

View 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
}

View 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;
}
};

View File

@@ -0,0 +1 @@
export * from "./risk-radar.types";

View 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;
}

View File

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