mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
feat(ui): integrate threat map with regions API endpoint (#9324)
Co-authored-by: alejandrobailo <alejandrobailo94@gmail.com>
This commit is contained in:
@@ -9,6 +9,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
- Compliance Watchlist component to Overview page [(#9199)](https://github.com/prowler-cloud/prowler/pull/9199)
|
||||
- Service Watchlist component to Overview page [(#9316)](https://github.com/prowler-cloud/prowler/pull/9316)
|
||||
- Risk Pipeline component with Sankey chart to Overview page [(#9317)](https://github.com/prowler-cloud/prowler/pull/9317)
|
||||
- Threat Map component to Overview Page [(#9324)](https://github.com/prowler-cloud/prowler/pull/9324)
|
||||
|
||||
## [1.14.0] (Prowler v5.14.0)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./overview";
|
||||
export * from "./overview.adapter";
|
||||
export * from "./sankey.adapter";
|
||||
export * from "./threat-map.adapter";
|
||||
export * from "./types";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { handleApiResponse } from "@/lib/server-actions-helper";
|
||||
import {
|
||||
FindingsSeverityOverviewResponse,
|
||||
ProvidersOverviewResponse,
|
||||
RegionsOverviewResponse,
|
||||
ServicesOverviewResponse,
|
||||
} from "./types";
|
||||
|
||||
@@ -178,3 +179,31 @@ export const getThreatScore = async ({
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRegionsOverview = async ({
|
||||
filters = {},
|
||||
}: {
|
||||
filters?: Record<string, string | string[] | undefined>;
|
||||
} = {}): Promise<RegionsOverviewResponse | undefined> => {
|
||||
const headers = await getAuthHeaders({ contentType: false });
|
||||
|
||||
const url = new URL(`${apiBaseUrl}/overviews/regions`);
|
||||
|
||||
// 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 regions overview:", error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,52 +1,26 @@
|
||||
import { getProviderDisplayName } from "@/types/providers";
|
||||
|
||||
import {
|
||||
FindingsSeverityOverviewResponse,
|
||||
ProviderOverview,
|
||||
ProvidersOverviewResponse,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* Sankey chart node structure
|
||||
*/
|
||||
export interface SankeyNode {
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sankey chart link structure
|
||||
*/
|
||||
export interface SankeyLink {
|
||||
source: number;
|
||||
target: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sankey chart data structure
|
||||
*/
|
||||
export interface SankeyData {
|
||||
nodes: SankeyNode[];
|
||||
links: SankeyLink[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider display name mapping
|
||||
* Maps provider IDs to user-friendly display names
|
||||
* These names must match the COLOR_MAP keys in sankey-chart.tsx
|
||||
*/
|
||||
const PROVIDER_DISPLAY_NAMES: Record<string, string> = {
|
||||
aws: "AWS",
|
||||
azure: "Azure",
|
||||
gcp: "Google Cloud",
|
||||
kubernetes: "Kubernetes",
|
||||
github: "GitHub",
|
||||
m365: "Microsoft 365",
|
||||
iac: "Infrastructure as Code",
|
||||
oraclecloud: "Oracle Cloud Infrastructure",
|
||||
};
|
||||
|
||||
/**
|
||||
* Aggregated provider data after grouping by provider type
|
||||
*/
|
||||
interface AggregatedProvider {
|
||||
id: string;
|
||||
displayName: string;
|
||||
@@ -54,19 +28,9 @@ interface AggregatedProvider {
|
||||
fail: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider types to exclude from the Sankey chart
|
||||
*/
|
||||
const EXCLUDED_PROVIDERS = new Set(["mongo", "mongodb", "mongodbatlas"]);
|
||||
|
||||
/**
|
||||
* Aggregates multiple provider entries by provider type (id)
|
||||
* Since the API can return multiple entries for the same provider type,
|
||||
* we need to sum up their findings
|
||||
*
|
||||
* @param providers - Raw provider overview data from API
|
||||
* @returns Aggregated providers with summed findings
|
||||
*/
|
||||
// API can return multiple entries for the same provider type, so we sum their findings
|
||||
function aggregateProvidersByType(
|
||||
providers: ProviderOverview[],
|
||||
): AggregatedProvider[] {
|
||||
@@ -75,10 +39,7 @@ function aggregateProvidersByType(
|
||||
for (const provider of providers) {
|
||||
const { id, attributes } = provider;
|
||||
|
||||
// Skip excluded providers
|
||||
if (EXCLUDED_PROVIDERS.has(id)) {
|
||||
continue;
|
||||
}
|
||||
if (EXCLUDED_PROVIDERS.has(id)) continue;
|
||||
|
||||
const existing = aggregated.get(id);
|
||||
|
||||
@@ -88,7 +49,7 @@ function aggregateProvidersByType(
|
||||
} else {
|
||||
aggregated.set(id, {
|
||||
id,
|
||||
displayName: PROVIDER_DISPLAY_NAMES[id] || id,
|
||||
displayName: getProviderDisplayName(id),
|
||||
pass: attributes.findings.pass,
|
||||
fail: attributes.findings.fail,
|
||||
});
|
||||
@@ -98,9 +59,6 @@ function aggregateProvidersByType(
|
||||
return Array.from(aggregated.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Severity display names in order
|
||||
*/
|
||||
const SEVERITY_ORDER = [
|
||||
"Critical",
|
||||
"High",
|
||||
@@ -110,18 +68,8 @@ const SEVERITY_ORDER = [
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Adapts providers overview and findings severity API responses to Sankey chart format
|
||||
*
|
||||
* Creates a 2-level flow visualization:
|
||||
* - Level 1: Cloud providers (AWS, Azure, GCP, etc.)
|
||||
* - Level 2: Severity breakdown (Critical, High, Medium, Low, Informational)
|
||||
*
|
||||
* The severity distribution is calculated proportionally based on each provider's
|
||||
* fail count relative to the total fails across all providers.
|
||||
*
|
||||
* @param providersResponse - Raw API response from /overviews/providers
|
||||
* @param severityResponse - Raw API response from /overviews/findings_severity
|
||||
* @returns Sankey chart data with nodes and links
|
||||
* Adapts providers overview and findings severity API responses to Sankey chart format.
|
||||
* Severity distribution is calculated proportionally based on each provider's fail count.
|
||||
*/
|
||||
export function adaptProvidersOverviewToSankey(
|
||||
providersResponse: ProvidersOverviewResponse | undefined,
|
||||
@@ -131,34 +79,23 @@ export function adaptProvidersOverviewToSankey(
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
|
||||
// Aggregate providers by type
|
||||
const aggregatedProviders = aggregateProvidersByType(providersResponse.data);
|
||||
|
||||
// Filter out providers with no findings (only need fail > 0 for severity view)
|
||||
const providersWithFailures = aggregatedProviders.filter((p) => p.fail > 0);
|
||||
|
||||
if (providersWithFailures.length === 0) {
|
||||
return { nodes: [], links: [] };
|
||||
}
|
||||
|
||||
// Build nodes array: providers first, then severities
|
||||
const providerNodes: SankeyNode[] = providersWithFailures.map((p) => ({
|
||||
name: p.displayName,
|
||||
}));
|
||||
|
||||
const severityNodes: SankeyNode[] = SEVERITY_ORDER.map((severity) => ({
|
||||
name: severity,
|
||||
}));
|
||||
|
||||
const nodes = [...providerNodes, ...severityNodes];
|
||||
|
||||
// Calculate severity start index (after provider nodes)
|
||||
const severityStartIndex = providerNodes.length;
|
||||
|
||||
// Build links from each provider to severities
|
||||
const links: SankeyLink[] = [];
|
||||
|
||||
// If we have severity data, distribute proportionally
|
||||
if (severityResponse?.data?.attributes) {
|
||||
const { critical, high, medium, low, informational } =
|
||||
severityResponse.data.attributes;
|
||||
@@ -167,18 +104,15 @@ export function adaptProvidersOverviewToSankey(
|
||||
const totalSeverity = severityValues.reduce((sum, v) => sum + v, 0);
|
||||
|
||||
if (totalSeverity > 0) {
|
||||
// Calculate total fails across all providers
|
||||
const totalFails = providersWithFailures.reduce(
|
||||
(sum, p) => sum + p.fail,
|
||||
0,
|
||||
);
|
||||
|
||||
providersWithFailures.forEach((provider, sourceIndex) => {
|
||||
// Calculate this provider's proportion of total fails
|
||||
const providerRatio = provider.fail / totalFails;
|
||||
|
||||
severityValues.forEach((severityValue, severityIndex) => {
|
||||
// Distribute severity proportionally to this provider
|
||||
const value = Math.round(severityValue * providerRatio);
|
||||
|
||||
if (value > 0) {
|
||||
@@ -192,7 +126,7 @@ export function adaptProvidersOverviewToSankey(
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Fallback: if no severity data, just show fail counts to a generic "Fail" node
|
||||
// Fallback when no severity data available
|
||||
const failNode: SankeyNode = { name: "Fail" };
|
||||
nodes.push(failNode);
|
||||
const failIndex = nodes.length - 1;
|
||||
272
ui/actions/overview/threat-map.adapter.ts
Normal file
272
ui/actions/overview/threat-map.adapter.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import { getProviderDisplayName } from "@/types/providers";
|
||||
|
||||
import { RegionsOverviewResponse } from "./types";
|
||||
|
||||
export interface ThreatMapLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
region: string;
|
||||
coordinates: [number, number];
|
||||
totalFindings: number;
|
||||
riskLevel: "low-high" | "high" | "critical";
|
||||
severityData: Array<{
|
||||
name: string;
|
||||
value: number;
|
||||
percentage?: number;
|
||||
color?: string;
|
||||
}>;
|
||||
change?: number;
|
||||
}
|
||||
|
||||
export interface ThreatMapData {
|
||||
locations: ThreatMapLocation[];
|
||||
regions: string[];
|
||||
}
|
||||
|
||||
const AWS_REGION_COORDINATES: Record<string, { lat: number; lng: number }> = {
|
||||
"us-east-1": { lat: 37.5, lng: -77.5 }, // N. Virginia
|
||||
"us-east-2": { lat: 40.0, lng: -83.0 }, // Ohio
|
||||
"us-west-1": { lat: 37.8, lng: -122.4 }, // N. California
|
||||
"us-west-2": { lat: 45.5, lng: -122.7 }, // Oregon
|
||||
"af-south-1": { lat: -33.9, lng: 18.4 }, // Cape Town
|
||||
"ap-east-1": { lat: 22.3, lng: 114.2 }, // Hong Kong
|
||||
"ap-south-1": { lat: 19.1, lng: 72.9 }, // Mumbai
|
||||
"ap-south-2": { lat: 17.4, lng: 78.5 }, // Hyderabad
|
||||
"ap-northeast-1": { lat: 35.7, lng: 139.7 }, // Tokyo
|
||||
"ap-northeast-2": { lat: 37.6, lng: 127.0 }, // Seoul
|
||||
"ap-northeast-3": { lat: 34.7, lng: 135.5 }, // Osaka
|
||||
"ap-southeast-1": { lat: 1.4, lng: 103.8 }, // Singapore
|
||||
"ap-southeast-2": { lat: -33.9, lng: 151.2 }, // Sydney
|
||||
"ap-southeast-3": { lat: -6.2, lng: 106.8 }, // Jakarta
|
||||
"ap-southeast-4": { lat: -37.8, lng: 144.96 }, // Melbourne
|
||||
"ca-central-1": { lat: 45.5, lng: -73.6 }, // Montreal
|
||||
"ca-west-1": { lat: 51.0, lng: -114.1 }, // Calgary
|
||||
"eu-central-1": { lat: 50.1, lng: 8.7 }, // Frankfurt
|
||||
"eu-central-2": { lat: 47.4, lng: 8.5 }, // Zurich
|
||||
"eu-west-1": { lat: 53.3, lng: -6.3 }, // Ireland
|
||||
"eu-west-2": { lat: 51.5, lng: -0.1 }, // London
|
||||
"eu-west-3": { lat: 48.9, lng: 2.3 }, // Paris
|
||||
"eu-north-1": { lat: 59.3, lng: 18.1 }, // Stockholm
|
||||
"eu-south-1": { lat: 45.5, lng: 9.2 }, // Milan
|
||||
"eu-south-2": { lat: 40.4, lng: -3.7 }, // Spain
|
||||
"il-central-1": { lat: 32.1, lng: 34.8 }, // Tel Aviv
|
||||
"me-central-1": { lat: 25.3, lng: 55.3 }, // UAE
|
||||
"me-south-1": { lat: 26.1, lng: 50.6 }, // Bahrain
|
||||
"sa-east-1": { lat: -23.5, lng: -46.6 }, // São Paulo
|
||||
};
|
||||
|
||||
const AZURE_REGION_COORDINATES: Record<string, { lat: number; lng: number }> = {
|
||||
eastus: { lat: 37.5, lng: -79.0 },
|
||||
eastus2: { lat: 36.7, lng: -78.9 },
|
||||
westus: { lat: 37.8, lng: -122.4 },
|
||||
westus2: { lat: 47.6, lng: -122.3 },
|
||||
westus3: { lat: 33.4, lng: -112.1 },
|
||||
centralus: { lat: 41.6, lng: -93.6 },
|
||||
northcentralus: { lat: 41.9, lng: -87.6 },
|
||||
southcentralus: { lat: 29.4, lng: -98.5 },
|
||||
westcentralus: { lat: 40.9, lng: -110.0 },
|
||||
canadacentral: { lat: 43.7, lng: -79.4 },
|
||||
canadaeast: { lat: 46.8, lng: -71.2 },
|
||||
brazilsouth: { lat: -23.5, lng: -46.6 },
|
||||
northeurope: { lat: 53.3, lng: -6.3 },
|
||||
westeurope: { lat: 52.4, lng: 4.9 },
|
||||
uksouth: { lat: 51.5, lng: -0.1 },
|
||||
ukwest: { lat: 53.4, lng: -3.0 },
|
||||
francecentral: { lat: 46.3, lng: 2.4 },
|
||||
francesouth: { lat: 43.8, lng: 2.1 },
|
||||
switzerlandnorth: { lat: 47.5, lng: 8.5 },
|
||||
switzerlandwest: { lat: 46.2, lng: 6.1 },
|
||||
germanywestcentral: { lat: 50.1, lng: 8.7 },
|
||||
germanynorth: { lat: 53.1, lng: 8.8 },
|
||||
norwayeast: { lat: 59.9, lng: 10.7 },
|
||||
norwaywest: { lat: 58.97, lng: 5.73 },
|
||||
swedencentral: { lat: 60.67, lng: 17.14 },
|
||||
polandcentral: { lat: 52.23, lng: 21.01 },
|
||||
italynorth: { lat: 45.5, lng: 9.2 },
|
||||
spaincentral: { lat: 40.4, lng: -3.7 },
|
||||
australiaeast: { lat: -33.9, lng: 151.2 },
|
||||
australiasoutheast: { lat: -37.8, lng: 145.0 },
|
||||
australiacentral: { lat: -35.3, lng: 149.1 },
|
||||
eastasia: { lat: 22.3, lng: 114.2 },
|
||||
southeastasia: { lat: 1.3, lng: 103.8 },
|
||||
japaneast: { lat: 35.7, lng: 139.7 },
|
||||
japanwest: { lat: 34.7, lng: 135.5 },
|
||||
koreacentral: { lat: 37.6, lng: 127.0 },
|
||||
koreasouth: { lat: 35.2, lng: 129.0 },
|
||||
centralindia: { lat: 18.6, lng: 73.9 },
|
||||
southindia: { lat: 12.9, lng: 80.2 },
|
||||
westindia: { lat: 19.1, lng: 72.9 },
|
||||
uaenorth: { lat: 25.3, lng: 55.3 },
|
||||
uaecentral: { lat: 24.5, lng: 54.4 },
|
||||
southafricanorth: { lat: -26.2, lng: 28.0 },
|
||||
southafricawest: { lat: -34.0, lng: 18.5 },
|
||||
israelcentral: { lat: 32.1, lng: 34.8 },
|
||||
qatarcentral: { lat: 25.3, lng: 51.5 },
|
||||
};
|
||||
|
||||
const GCP_REGION_COORDINATES: Record<string, { lat: number; lng: number }> = {
|
||||
"us-central1": { lat: 41.3, lng: -95.9 }, // Iowa
|
||||
"us-east1": { lat: 33.2, lng: -80.0 }, // South Carolina
|
||||
"us-east4": { lat: 39.0, lng: -77.5 }, // Northern Virginia
|
||||
"us-east5": { lat: 39.96, lng: -82.99 }, // Columbus
|
||||
"us-south1": { lat: 32.8, lng: -96.8 }, // Dallas
|
||||
"us-west1": { lat: 45.6, lng: -122.8 }, // Oregon
|
||||
"us-west2": { lat: 34.1, lng: -118.2 }, // Los Angeles
|
||||
"us-west3": { lat: 40.8, lng: -111.9 }, // Salt Lake City
|
||||
"us-west4": { lat: 36.2, lng: -115.1 }, // Las Vegas
|
||||
"northamerica-northeast1": { lat: 45.5, lng: -73.6 }, // Montreal
|
||||
"northamerica-northeast2": { lat: 43.7, lng: -79.4 }, // Toronto
|
||||
"southamerica-east1": { lat: -23.5, lng: -46.6 }, // São Paulo
|
||||
"southamerica-west1": { lat: -33.4, lng: -70.6 }, // Santiago
|
||||
"europe-north1": { lat: 60.6, lng: 27.0 }, // Finland
|
||||
"europe-west1": { lat: 50.4, lng: 3.8 }, // Belgium
|
||||
"europe-west2": { lat: 51.5, lng: -0.1 }, // London
|
||||
"europe-west3": { lat: 50.1, lng: 8.7 }, // Frankfurt
|
||||
"europe-west4": { lat: 53.4, lng: 6.8 }, // Netherlands
|
||||
"europe-west6": { lat: 47.4, lng: 8.5 }, // Zurich
|
||||
"europe-west8": { lat: 45.5, lng: 9.2 }, // Milan
|
||||
"europe-west9": { lat: 48.9, lng: 2.3 }, // Paris
|
||||
"europe-west10": { lat: 52.5, lng: 13.4 }, // Berlin
|
||||
"europe-west12": { lat: 45.0, lng: 7.7 }, // Turin
|
||||
"europe-central2": { lat: 52.2, lng: 21.0 }, // Warsaw
|
||||
"europe-southwest1": { lat: 40.4, lng: -3.7 }, // Madrid
|
||||
"asia-east1": { lat: 24.0, lng: 121.0 }, // Taiwan
|
||||
"asia-east2": { lat: 22.3, lng: 114.2 }, // Hong Kong
|
||||
"asia-northeast1": { lat: 35.7, lng: 139.7 }, // Tokyo
|
||||
"asia-northeast2": { lat: 34.7, lng: 135.5 }, // Osaka
|
||||
"asia-northeast3": { lat: 37.6, lng: 127.0 }, // Seoul
|
||||
"asia-south1": { lat: 19.1, lng: 72.9 }, // Mumbai
|
||||
"asia-south2": { lat: 28.6, lng: 77.2 }, // Delhi
|
||||
"asia-southeast1": { lat: 1.4, lng: 103.8 }, // Singapore
|
||||
"asia-southeast2": { lat: -6.2, lng: 106.8 }, // Jakarta
|
||||
"australia-southeast1": { lat: -33.9, lng: 151.2 }, // Sydney
|
||||
"australia-southeast2": { lat: -37.8, lng: 145.0 }, // Melbourne
|
||||
"me-central1": { lat: 25.3, lng: 51.5 }, // Doha
|
||||
"me-central2": { lat: 24.5, lng: 54.4 }, // Dammam
|
||||
"me-west1": { lat: 32.1, lng: 34.8 }, // Tel Aviv
|
||||
"africa-south1": { lat: -26.2, lng: 28.0 }, // Johannesburg
|
||||
};
|
||||
|
||||
const PROVIDER_COORDINATES: Record<
|
||||
string,
|
||||
Record<string, { lat: number; lng: number }>
|
||||
> = {
|
||||
aws: AWS_REGION_COORDINATES,
|
||||
azure: AZURE_REGION_COORDINATES,
|
||||
gcp: GCP_REGION_COORDINATES,
|
||||
};
|
||||
|
||||
// Returns [lng, lat] format for D3/GeoJSON compatibility
|
||||
function getRegionCoordinates(
|
||||
providerType: string,
|
||||
region: string,
|
||||
): [number, number] | null {
|
||||
const coords =
|
||||
PROVIDER_COORDINATES[providerType.toLowerCase()]?.[region.toLowerCase()];
|
||||
return coords ? [coords.lng, coords.lat] : null;
|
||||
}
|
||||
|
||||
function getRiskLevel(failRate: number): "low-high" | "high" | "critical" {
|
||||
if (failRate >= 0.5) return "critical";
|
||||
if (failRate >= 0.25) return "high";
|
||||
return "low-high";
|
||||
}
|
||||
|
||||
// CSS variables are used for Recharts inline styles, not className
|
||||
function buildSeverityData(fail: number, pass: number, muted: number) {
|
||||
const total = fail + pass + muted;
|
||||
const pct = (value: number) =>
|
||||
total > 0 ? Math.round((value / total) * 100) : 0;
|
||||
|
||||
return [
|
||||
{
|
||||
name: "Fail",
|
||||
value: fail,
|
||||
percentage: pct(fail),
|
||||
color: "var(--color-bg-fail)",
|
||||
},
|
||||
{
|
||||
name: "Pass",
|
||||
value: pass,
|
||||
percentage: pct(pass),
|
||||
color: "var(--color-bg-pass)",
|
||||
},
|
||||
{
|
||||
name: "Muted",
|
||||
value: muted,
|
||||
percentage: pct(muted),
|
||||
color: "var(--color-bg-data-muted)",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Formats "europe-west10" → "Europe West 10"
|
||||
function formatRegionCode(region: string): string {
|
||||
return region
|
||||
.split(/[-_]/)
|
||||
.map((part) => {
|
||||
const match = part.match(/^([a-zA-Z]+)(\d+)$/);
|
||||
if (match) {
|
||||
const [, text, number] = match;
|
||||
return `${text.charAt(0).toUpperCase()}${text.slice(1).toLowerCase()} ${number}`;
|
||||
}
|
||||
return part.charAt(0).toUpperCase() + part.slice(1).toLowerCase();
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function formatRegionName(providerType: string, region: string): string {
|
||||
return `${getProviderDisplayName(providerType)} - ${formatRegionCode(region)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts regions overview API response to threat map format.
|
||||
*/
|
||||
export function adaptRegionsOverviewToThreatMap(
|
||||
regionsResponse: RegionsOverviewResponse | undefined,
|
||||
): ThreatMapData {
|
||||
if (!regionsResponse?.data || regionsResponse.data.length === 0) {
|
||||
return {
|
||||
locations: [],
|
||||
regions: [],
|
||||
};
|
||||
}
|
||||
|
||||
const locations: ThreatMapLocation[] = [];
|
||||
const regionSet = new Set<string>();
|
||||
|
||||
for (const regionData of regionsResponse.data) {
|
||||
const { id, attributes } = regionData;
|
||||
const coordinates = getRegionCoordinates(
|
||||
attributes.provider_type,
|
||||
attributes.region,
|
||||
);
|
||||
|
||||
if (!coordinates) continue;
|
||||
|
||||
const providerRegion = getProviderDisplayName(attributes.provider_type);
|
||||
regionSet.add(providerRegion);
|
||||
|
||||
const failRate =
|
||||
attributes.total > 0 ? attributes.fail / attributes.total : 0;
|
||||
|
||||
locations.push({
|
||||
id,
|
||||
name: formatRegionName(attributes.provider_type, attributes.region),
|
||||
region: providerRegion,
|
||||
coordinates,
|
||||
totalFindings: attributes.fail,
|
||||
riskLevel: getRiskLevel(failRate),
|
||||
severityData: buildSeverityData(
|
||||
attributes.fail,
|
||||
attributes.pass,
|
||||
attributes.muted,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
locations,
|
||||
regions: Array.from(regionSet).sort(),
|
||||
};
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
// Providers Overview Types
|
||||
// Corresponds to the /overviews/providers endpoint
|
||||
|
||||
export interface ProviderOverviewFindings {
|
||||
pass: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProviderOverviewResources {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProviderOverviewAttributes {
|
||||
findings: ProviderOverviewFindings;
|
||||
resources: ProviderOverviewResources;
|
||||
}
|
||||
|
||||
export interface ProviderOverview {
|
||||
type: "providers-overview";
|
||||
id: string;
|
||||
attributes: ProviderOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ProvidersOverviewResponse {
|
||||
data: ProviderOverview[];
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Services Overview Types
|
||||
// Corresponds to the /overviews/services endpoint
|
||||
|
||||
export interface ServiceOverviewAttributes {
|
||||
total: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
pass: number;
|
||||
}
|
||||
|
||||
export interface ServiceOverview {
|
||||
type: "services-overview";
|
||||
id: string;
|
||||
attributes: ServiceOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ServicesOverviewResponse {
|
||||
data: ServiceOverview[];
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ThreatScore Snapshot Types
|
||||
// Corresponds to the ThreatScoreSnapshot model from the API
|
||||
|
||||
export interface CriticalRequirement {
|
||||
requirement_id: string;
|
||||
risk_level: number;
|
||||
weight: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type SectionScores = Record<string, number>;
|
||||
|
||||
export interface ThreatScoreSnapshotAttributes {
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
scan: string | null;
|
||||
provider: string | null;
|
||||
compliance_id: string;
|
||||
overall_score: string;
|
||||
score_delta: string | null;
|
||||
section_scores: SectionScores;
|
||||
critical_requirements: CriticalRequirement[];
|
||||
total_requirements: number;
|
||||
passed_requirements: number;
|
||||
failed_requirements: number;
|
||||
manual_requirements: number;
|
||||
total_findings: number;
|
||||
passed_findings: number;
|
||||
failed_findings: number;
|
||||
}
|
||||
|
||||
export interface ThreatScoreSnapshot {
|
||||
id: string;
|
||||
type: "threatscore-snapshots";
|
||||
attributes: ThreatScoreSnapshotAttributes;
|
||||
}
|
||||
|
||||
export interface ThreatScoreResponse {
|
||||
data: ThreatScoreSnapshot[];
|
||||
}
|
||||
|
||||
// Findings Severity Overview Types
|
||||
// Corresponds to the /overviews/findings_severity endpoint
|
||||
|
||||
export interface FindingsSeverityAttributes {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
informational: number;
|
||||
}
|
||||
|
||||
export interface FindingsSeverityOverview {
|
||||
type: "findings-severity-overview";
|
||||
id: string;
|
||||
attributes: FindingsSeverityAttributes;
|
||||
}
|
||||
|
||||
export interface FindingsSeverityOverviewResponse {
|
||||
data: FindingsSeverityOverview;
|
||||
meta: {
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Filters for ThreatScore endpoint
|
||||
export interface ThreatScoreFilters {
|
||||
snapshot_id?: string;
|
||||
provider_id?: string;
|
||||
provider_id__in?: string;
|
||||
provider_type?: string;
|
||||
provider_type__in?: string;
|
||||
scan_id?: string;
|
||||
scan_id__in?: string;
|
||||
compliance_id?: string;
|
||||
compliance_id__in?: string;
|
||||
inserted_at?: string;
|
||||
inserted_at__gte?: string;
|
||||
inserted_at__lte?: string;
|
||||
overall_score__gte?: string;
|
||||
overall_score__lte?: string;
|
||||
}
|
||||
5
ui/actions/overview/types/common.ts
Normal file
5
ui/actions/overview/types/common.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Common types shared across overview endpoints
|
||||
|
||||
export interface OverviewResponseMeta {
|
||||
version: string;
|
||||
}
|
||||
23
ui/actions/overview/types/findings-severity.ts
Normal file
23
ui/actions/overview/types/findings-severity.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
// Findings Severity Overview Types
|
||||
// Corresponds to the /overviews/findings_severity endpoint
|
||||
|
||||
import { OverviewResponseMeta } from "./common";
|
||||
|
||||
export interface FindingsSeverityAttributes {
|
||||
critical: number;
|
||||
high: number;
|
||||
medium: number;
|
||||
low: number;
|
||||
informational: number;
|
||||
}
|
||||
|
||||
export interface FindingsSeverityOverview {
|
||||
type: "findings-severity-overview";
|
||||
id: string;
|
||||
attributes: FindingsSeverityAttributes;
|
||||
}
|
||||
|
||||
export interface FindingsSeverityOverviewResponse {
|
||||
data: FindingsSeverityOverview;
|
||||
meta: OverviewResponseMeta;
|
||||
}
|
||||
6
ui/actions/overview/types/index.ts
Normal file
6
ui/actions/overview/types/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export * from "./common";
|
||||
export * from "./findings-severity";
|
||||
export * from "./providers";
|
||||
export * from "./regions";
|
||||
export * from "./services";
|
||||
export * from "./threat-score";
|
||||
31
ui/actions/overview/types/providers.ts
Normal file
31
ui/actions/overview/types/providers.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// Providers Overview Types
|
||||
// Corresponds to the /overviews/providers endpoint
|
||||
|
||||
import { OverviewResponseMeta } from "./common";
|
||||
|
||||
export interface ProviderOverviewFindings {
|
||||
pass: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProviderOverviewResources {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface ProviderOverviewAttributes {
|
||||
findings: ProviderOverviewFindings;
|
||||
resources: ProviderOverviewResources;
|
||||
}
|
||||
|
||||
export interface ProviderOverview {
|
||||
type: "providers-overview";
|
||||
id: string;
|
||||
attributes: ProviderOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ProvidersOverviewResponse {
|
||||
data: ProviderOverview[];
|
||||
meta: OverviewResponseMeta;
|
||||
}
|
||||
24
ui/actions/overview/types/regions.ts
Normal file
24
ui/actions/overview/types/regions.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
// Regions Overview Types
|
||||
// Corresponds to the /overviews/regions endpoint
|
||||
|
||||
import { OverviewResponseMeta } from "./common";
|
||||
|
||||
export interface RegionOverviewAttributes {
|
||||
provider_type: string;
|
||||
region: string;
|
||||
total: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
pass: number;
|
||||
}
|
||||
|
||||
export interface RegionOverview {
|
||||
type: "regions-overview";
|
||||
id: string;
|
||||
attributes: RegionOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface RegionsOverviewResponse {
|
||||
data: RegionOverview[];
|
||||
meta: OverviewResponseMeta;
|
||||
}
|
||||
22
ui/actions/overview/types/services.ts
Normal file
22
ui/actions/overview/types/services.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Services Overview Types
|
||||
// Corresponds to the /overviews/services endpoint
|
||||
|
||||
import { OverviewResponseMeta } from "./common";
|
||||
|
||||
export interface ServiceOverviewAttributes {
|
||||
total: number;
|
||||
fail: number;
|
||||
muted: number;
|
||||
pass: number;
|
||||
}
|
||||
|
||||
export interface ServiceOverview {
|
||||
type: "services-overview";
|
||||
id: string;
|
||||
attributes: ServiceOverviewAttributes;
|
||||
}
|
||||
|
||||
export interface ServicesOverviewResponse {
|
||||
data: ServiceOverview[];
|
||||
meta: OverviewResponseMeta;
|
||||
}
|
||||
58
ui/actions/overview/types/threat-score.ts
Normal file
58
ui/actions/overview/types/threat-score.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// ThreatScore Snapshot Types
|
||||
// Corresponds to the ThreatScoreSnapshot model from the API
|
||||
|
||||
export interface CriticalRequirement {
|
||||
requirement_id: string;
|
||||
risk_level: number;
|
||||
weight: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export type SectionScores = Record<string, number>;
|
||||
|
||||
export interface ThreatScoreSnapshotAttributes {
|
||||
id: string;
|
||||
inserted_at: string;
|
||||
scan: string | null;
|
||||
provider: string | null;
|
||||
compliance_id: string;
|
||||
overall_score: string;
|
||||
score_delta: string | null;
|
||||
section_scores: SectionScores;
|
||||
critical_requirements: CriticalRequirement[];
|
||||
total_requirements: number;
|
||||
passed_requirements: number;
|
||||
failed_requirements: number;
|
||||
manual_requirements: number;
|
||||
total_findings: number;
|
||||
passed_findings: number;
|
||||
failed_findings: number;
|
||||
}
|
||||
|
||||
export interface ThreatScoreSnapshot {
|
||||
id: string;
|
||||
type: "threatscore-snapshots";
|
||||
attributes: ThreatScoreSnapshotAttributes;
|
||||
}
|
||||
|
||||
export interface ThreatScoreResponse {
|
||||
data: ThreatScoreSnapshot[];
|
||||
}
|
||||
|
||||
// Filters for ThreatScore endpoint
|
||||
export interface ThreatScoreFilters {
|
||||
snapshot_id?: string;
|
||||
provider_id?: string;
|
||||
provider_id__in?: string;
|
||||
provider_type?: string;
|
||||
provider_type__in?: string;
|
||||
scan_id?: string;
|
||||
scan_id__in?: string;
|
||||
compliance_id?: string;
|
||||
compliance_id__in?: string;
|
||||
inserted_at?: string;
|
||||
inserted_at__gte?: string;
|
||||
inserted_at__lte?: string;
|
||||
overall_score__gte?: string;
|
||||
overall_score__lte?: string;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { getSeverityTrendsByTimeRange } from "@/actions/overview/severity-trends
|
||||
import { LineChart } from "@/components/graphs/line-chart";
|
||||
import { LineConfig, LineDataPoint } from "@/components/graphs/types";
|
||||
import { Skeleton } from "@/components/shadcn";
|
||||
import { SEVERITY_LINE_CONFIGS } from "@/types/severities";
|
||||
|
||||
import { type TimeRange, TimeRangeSelector } from "./time-range-selector";
|
||||
|
||||
@@ -70,36 +71,10 @@ export const FindingSeverityOverTime = ({
|
||||
};
|
||||
});
|
||||
|
||||
// Define line configurations for each severity level
|
||||
const lines: LineConfig[] = [
|
||||
{
|
||||
dataKey: "informational",
|
||||
color: "var(--color-bg-data-info)",
|
||||
label: "Informational",
|
||||
},
|
||||
{
|
||||
dataKey: "low",
|
||||
color: "var(--color-bg-data-low)",
|
||||
label: "Low",
|
||||
},
|
||||
{
|
||||
dataKey: "medium",
|
||||
color: "var(--color-bg-data-medium)",
|
||||
label: "Medium",
|
||||
},
|
||||
{
|
||||
dataKey: "high",
|
||||
color: "var(--color-bg-data-high)",
|
||||
label: "High",
|
||||
},
|
||||
{
|
||||
dataKey: "critical",
|
||||
color: "var(--color-bg-data-critical)",
|
||||
label: "Critical",
|
||||
},
|
||||
];
|
||||
// Build line configurations from shared severity configs
|
||||
const lines: LineConfig[] = [...SEVERITY_LINE_CONFIGS];
|
||||
|
||||
// Only add muted line if data contains it
|
||||
// Only add muted line if data contains it (CSS var for Recharts inline styles)
|
||||
if (data.some((item) => item.muted !== undefined)) {
|
||||
lines.push({
|
||||
dataKey: "muted",
|
||||
|
||||
@@ -7,12 +7,12 @@ export const GRAPH_TABS = [
|
||||
id: "risk-pipeline",
|
||||
label: "Risk Pipeline",
|
||||
},
|
||||
{
|
||||
id: "threat-map",
|
||||
label: "Threat Map",
|
||||
},
|
||||
// TODO: Uncomment when ready to enable other tabs
|
||||
// {
|
||||
// id: "threat-map",
|
||||
// label: "Threat Map",
|
||||
// },
|
||||
// {
|
||||
// id: "risk-radar",
|
||||
// label: "Risk Radar",
|
||||
// },
|
||||
|
||||
@@ -7,10 +7,10 @@ import { FindingsViewSSR } from "./findings-view";
|
||||
import { GraphsTabsClient } from "./graphs-tabs-client";
|
||||
import { GRAPH_TABS, type TabId } from "./graphs-tabs-config";
|
||||
import { RiskPipelineViewSSR } from "./risk-pipeline-view/risk-pipeline-view.ssr";
|
||||
import { ThreatMapViewSSR } from "./threat-map-view/threat-map-view.ssr";
|
||||
// TODO: Uncomment when ready to enable other tabs
|
||||
// import { RiskPlotView } from "./risk-plot/risk-plot-view";
|
||||
// import { RiskRadarViewSSR } from "./risk-radar-view/risk-radar-view.ssr";
|
||||
// import { ThreatMapViewSSR } from "./threat-map-view/threat-map-view.ssr";
|
||||
|
||||
const LoadingFallback = () => (
|
||||
<div className="border-border-neutral-primary bg-bg-neutral-secondary flex w-full flex-col space-y-4 rounded-lg border p-4">
|
||||
@@ -24,8 +24,8 @@ type GraphComponent = React.ComponentType<{ searchParams: SearchParamsProps }>;
|
||||
const GRAPH_COMPONENTS: Record<TabId, GraphComponent> = {
|
||||
findings: FindingsViewSSR as GraphComponent,
|
||||
"risk-pipeline": RiskPipelineViewSSR as GraphComponent,
|
||||
"threat-map": ThreatMapViewSSR as GraphComponent,
|
||||
// TODO: Uncomment when ready to enable other tabs
|
||||
// "threat-map": ThreatMapViewSSR as GraphComponent,
|
||||
// "risk-radar": RiskRadarViewSSR as GraphComponent,
|
||||
// "risk-plot": RiskPlotView as GraphComponent,
|
||||
};
|
||||
|
||||
@@ -1,98 +1,34 @@
|
||||
import {
|
||||
adaptRegionsOverviewToThreatMap,
|
||||
getRegionsOverview,
|
||||
} from "@/actions/overview";
|
||||
import { ThreatMap } from "@/components/graphs/threat-map";
|
||||
import { SearchParamsProps } from "@/types";
|
||||
|
||||
// Mock data - replace with actual API call
|
||||
const mockThreatMapData = {
|
||||
locations: [
|
||||
{
|
||||
id: "us-east-1",
|
||||
name: "US East-1",
|
||||
region: "North America",
|
||||
coordinates: [-75.1551, 40.2206] as [number, number],
|
||||
totalFindings: 455,
|
||||
riskLevel: "critical" as const,
|
||||
severityData: [
|
||||
{ name: "Critical", value: 432 },
|
||||
{ name: "High", value: 1232 },
|
||||
{ name: "Medium", value: 221 },
|
||||
{ name: "Low", value: 543 },
|
||||
{ name: "Info", value: 10 },
|
||||
],
|
||||
change: 5,
|
||||
},
|
||||
{
|
||||
id: "eu-west-1",
|
||||
name: "EU West-1",
|
||||
region: "Europe",
|
||||
coordinates: [-6.2597, 53.3498] as [number, number],
|
||||
totalFindings: 320,
|
||||
riskLevel: "high" as const,
|
||||
severityData: [
|
||||
{ name: "Critical", value: 200 },
|
||||
{ name: "High", value: 900 },
|
||||
{ name: "Medium", value: 180 },
|
||||
{ name: "Low", value: 400 },
|
||||
{ name: "Info", value: 15 },
|
||||
],
|
||||
change: -2,
|
||||
},
|
||||
{
|
||||
id: "ap-southeast-1",
|
||||
name: "AP Southeast-1",
|
||||
region: "Asia Pacific",
|
||||
coordinates: [103.8198, 1.3521] as [number, number],
|
||||
totalFindings: 280,
|
||||
riskLevel: "high" as const,
|
||||
severityData: [
|
||||
{ name: "Critical", value: 150 },
|
||||
{ name: "High", value: 800 },
|
||||
{ name: "Medium", value: 160 },
|
||||
{ name: "Low", value: 350 },
|
||||
{ name: "Info", value: 8 },
|
||||
],
|
||||
change: 3,
|
||||
},
|
||||
{
|
||||
id: "ca-central-1",
|
||||
name: "CA Central-1",
|
||||
region: "North America",
|
||||
coordinates: [-95.7129, 56.1304] as [number, number],
|
||||
totalFindings: 190,
|
||||
riskLevel: "high" as const,
|
||||
severityData: [
|
||||
{ name: "Critical", value: 100 },
|
||||
{ name: "High", value: 600 },
|
||||
{ name: "Medium", value: 120 },
|
||||
{ name: "Low", value: 280 },
|
||||
{ name: "Info", value: 5 },
|
||||
],
|
||||
change: 1,
|
||||
},
|
||||
{
|
||||
id: "ap-northeast-1",
|
||||
name: "AP Northeast-1",
|
||||
region: "Asia Pacific",
|
||||
coordinates: [139.6917, 35.6895] as [number, number],
|
||||
totalFindings: 240,
|
||||
riskLevel: "high" as const,
|
||||
severityData: [
|
||||
{ name: "Critical", value: 120 },
|
||||
{ name: "High", value: 700 },
|
||||
{ name: "Medium", value: 140 },
|
||||
{ name: "Low", value: 320 },
|
||||
{ name: "Info", value: 12 },
|
||||
],
|
||||
change: 4,
|
||||
},
|
||||
],
|
||||
regions: ["North America", "Europe", "Asia Pacific"],
|
||||
};
|
||||
import { pickFilterParams } from "../../../lib/filter-params";
|
||||
|
||||
export async function ThreatMapViewSSR() {
|
||||
// TODO: Call server action to fetch threat map data
|
||||
export async function ThreatMapViewSSR({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: SearchParamsProps;
|
||||
}) {
|
||||
const filters = pickFilterParams(searchParams);
|
||||
const regionsResponse = await getRegionsOverview({ filters });
|
||||
const threatMapData = adaptRegionsOverviewToThreatMap(regionsResponse);
|
||||
|
||||
if (threatMapData.locations.length === 0) {
|
||||
return (
|
||||
<div className="flex h-[460px] w-full items-center justify-center">
|
||||
<p className="text-text-neutral-tertiary text-sm">
|
||||
No region data available
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full flex-1 overflow-hidden">
|
||||
<ThreatMap data={mockThreatMapData} height={350} />
|
||||
<ThreatMap data={threatMapData} height={460} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Skeleton,
|
||||
} from "@/components/shadcn";
|
||||
import { calculatePercentage } from "@/lib/utils";
|
||||
import { SEVERITY_FILTER_MAP } from "@/types/severities";
|
||||
|
||||
interface ProviderAttributes {
|
||||
uid: string;
|
||||
@@ -67,16 +68,7 @@ export const RiskSeverityChart = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Map severity name to lowercase for the filter
|
||||
const severityMap: Record<string, string> = {
|
||||
Critical: "critical",
|
||||
High: "high",
|
||||
Medium: "medium",
|
||||
Low: "low",
|
||||
Info: "informational",
|
||||
};
|
||||
|
||||
const severity = severityMap[dataPoint.name];
|
||||
const severity = SEVERITY_FILTER_MAP[dataPoint.name];
|
||||
if (severity) {
|
||||
params.set("filter[severity__in]", severity);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export const WatchlistCard = ({
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[405px] min-w-[328px] flex-1 flex-col justify-between md:max-w-[312px]"
|
||||
className="flex min-h-[405px] min-w-[312px] flex-col justify-between"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>{title}</CardTitle>
|
||||
@@ -165,10 +165,7 @@ export const WatchlistCard = ({
|
||||
|
||||
export function WatchlistCardSkeleton() {
|
||||
return (
|
||||
<Card
|
||||
variant="base"
|
||||
className="flex min-h-[500px] min-w-[328px] flex-col md:max-w-[312px]"
|
||||
>
|
||||
<Card variant="base" className="flex min-h-[500px] min-w-[312px] flex-col">
|
||||
<CardTitle>
|
||||
<Skeleton className="h-7 w-[168px] rounded-xl" />
|
||||
</CardTitle>
|
||||
|
||||
@@ -53,12 +53,11 @@ export default async function Home({
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-6 md:flex-row md:items-stretch">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="mt-6 flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-6 md:flex-row">
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ComplianceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
<Suspense fallback={<WatchlistCardSkeleton />}>
|
||||
<ServiceWatchlistSSR searchParams={resolvedSearchParams} />
|
||||
</Suspense>
|
||||
|
||||
@@ -4,32 +4,12 @@ import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Rectangle, ResponsiveContainer, Sankey, Tooltip } from "recharts";
|
||||
|
||||
import {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
GitHubProviderBadge,
|
||||
IacProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
} from "@/components/icons/providers-badge";
|
||||
import { IconSvgProps } from "@/types";
|
||||
import { PROVIDER_ICONS } from "@/components/icons/providers-badge";
|
||||
import { initializeChartColors } from "@/lib/charts/colors";
|
||||
import { SEVERITY_FILTER_MAP } from "@/types/severities";
|
||||
|
||||
import { ChartTooltip } from "./shared/chart-tooltip";
|
||||
|
||||
// Map node names to their corresponding provider icon components
|
||||
const PROVIDER_ICONS: Record<string, React.FC<IconSvgProps>> = {
|
||||
AWS: AWSProviderBadge,
|
||||
Azure: AzureProviderBadge,
|
||||
"Google Cloud": GCPProviderBadge,
|
||||
Kubernetes: KS8ProviderBadge,
|
||||
"Microsoft 365": M365ProviderBadge,
|
||||
GitHub: GitHubProviderBadge,
|
||||
"Infrastructure as Code": IacProviderBadge,
|
||||
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,
|
||||
};
|
||||
|
||||
interface SankeyNode {
|
||||
name: string;
|
||||
newFindings?: number;
|
||||
@@ -74,77 +54,6 @@ interface NodeTooltipState {
|
||||
const TOOLTIP_OFFSET_PX = 10;
|
||||
const MIN_LINK_WIDTH = 4;
|
||||
|
||||
// Map severity node names to their filter values for the findings page
|
||||
const SEVERITY_FILTER_MAP: Record<string, string> = {
|
||||
Critical: "critical",
|
||||
High: "high",
|
||||
Medium: "medium",
|
||||
Low: "low",
|
||||
Informational: "informational",
|
||||
};
|
||||
|
||||
// Map color names to CSS variable names defined in globals.css
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
// Status colors
|
||||
Success: "--color-bg-pass",
|
||||
Pass: "--color-bg-pass",
|
||||
Fail: "--color-bg-fail",
|
||||
// Provider colors
|
||||
AWS: "--color-bg-data-aws",
|
||||
Azure: "--color-bg-data-azure",
|
||||
"Google Cloud": "--color-bg-data-gcp",
|
||||
Kubernetes: "--color-bg-data-kubernetes",
|
||||
"Microsoft 365": "--color-bg-data-m365",
|
||||
GitHub: "--color-bg-data-github",
|
||||
"Infrastructure as Code": "--color-bg-data-muted",
|
||||
"Oracle Cloud Infrastructure": "--color-bg-data-muted",
|
||||
// Severity colors
|
||||
Critical: "--color-bg-data-critical",
|
||||
High: "--color-bg-data-high",
|
||||
Medium: "--color-bg-data-medium",
|
||||
Low: "--color-bg-data-low",
|
||||
Info: "--color-bg-data-info",
|
||||
Informational: "--color-bg-data-info",
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute color value from CSS variable name at runtime.
|
||||
* SVG fill attributes cannot directly resolve CSS variables,
|
||||
* so we extract computed values from globals.css CSS variables.
|
||||
* Falls back to black (#000000) if variable not found or access fails.
|
||||
*
|
||||
* @param colorName - Key in COLOR_MAP (e.g., "AWS", "Fail")
|
||||
* @returns Computed CSS variable value or fallback color
|
||||
*/
|
||||
const getColorVariable = (colorName: string): string => {
|
||||
const varName = COLOR_MAP[colorName];
|
||||
if (!varName) return "#000000";
|
||||
|
||||
try {
|
||||
if (typeof document === "undefined") {
|
||||
// SSR context - return fallback
|
||||
return "#000000";
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(varName)
|
||||
.trim() || "#000000"
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
// CSS variables not loaded or access failed - return fallback
|
||||
return "#000000";
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize all color variables from CSS
|
||||
const initializeColors = (): Record<string, string> => {
|
||||
const colors: Record<string, string> = {};
|
||||
for (const [colorName] of Object.entries(COLOR_MAP)) {
|
||||
colors[colorName] = getColorVariable(colorName);
|
||||
}
|
||||
return colors;
|
||||
};
|
||||
|
||||
interface TooltipPayload {
|
||||
payload: {
|
||||
source?: { name: string };
|
||||
@@ -476,7 +385,7 @@ export function SankeyChart({ data, height = 400 }: SankeyChartProps) {
|
||||
|
||||
// Initialize colors from CSS variables on mount
|
||||
useEffect(() => {
|
||||
setColors(initializeColors());
|
||||
setColors(initializeChartColors());
|
||||
}, []);
|
||||
|
||||
const handleLinkHover = (
|
||||
|
||||
@@ -512,23 +512,20 @@ export function ThreatMap({
|
||||
position={tooltipPosition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="mt-3 flex items-center gap-2"
|
||||
role="status"
|
||||
aria-label={`${filteredLocations.length} threat locations on map`}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 rounded-full"
|
||||
style={{ backgroundColor: "var(--bg-data-critical)" }}
|
||||
/>
|
||||
<span
|
||||
className="text-sm"
|
||||
style={{ color: "var(--text-neutral-tertiary)" }}
|
||||
className="border-border-neutral-primary bg-bg-neutral-secondary absolute bottom-4 left-4 flex items-center gap-2 rounded-full border px-3 py-1.5"
|
||||
role="status"
|
||||
aria-label={`${filteredLocations.length} threat locations on map`}
|
||||
>
|
||||
{filteredLocations.length} Locations
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 rounded"
|
||||
style={{ backgroundColor: "var(--bg-data-critical)" }}
|
||||
/>
|
||||
<span className="text-text-neutral-primary text-sm font-medium">
|
||||
{filteredLocations.length} Locations
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
@@ -542,9 +539,13 @@ export function ThreatMap({
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-4">
|
||||
<div
|
||||
className="mb-1 flex items-center gap-2"
|
||||
className="mb-1 flex items-center"
|
||||
aria-label={`Selected location: ${selectedLocation.name}`}
|
||||
>
|
||||
<MapPin
|
||||
size={21}
|
||||
style={{ color: "var(--color-text-text-error)" }}
|
||||
/>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="bg-pass-primary h-2 w-2 rounded-full"
|
||||
|
||||
@@ -1,8 +1,33 @@
|
||||
export * from "./aws-provider-badge";
|
||||
export * from "./azure-provider-badge";
|
||||
export * from "./gcp-provider-badge";
|
||||
export * from "./github-provider-badge";
|
||||
export * from "./iac-provider-badge";
|
||||
export * from "./ks8-provider-badge";
|
||||
export * from "./m365-provider-badge";
|
||||
export * from "./oraclecloud-provider-badge";
|
||||
import { IconSvgProps } from "@/types";
|
||||
|
||||
import { AWSProviderBadge } from "./aws-provider-badge";
|
||||
import { AzureProviderBadge } from "./azure-provider-badge";
|
||||
import { GCPProviderBadge } from "./gcp-provider-badge";
|
||||
import { GitHubProviderBadge } from "./github-provider-badge";
|
||||
import { IacProviderBadge } from "./iac-provider-badge";
|
||||
import { KS8ProviderBadge } from "./ks8-provider-badge";
|
||||
import { M365ProviderBadge } from "./m365-provider-badge";
|
||||
import { OracleCloudProviderBadge } from "./oraclecloud-provider-badge";
|
||||
|
||||
export {
|
||||
AWSProviderBadge,
|
||||
AzureProviderBadge,
|
||||
GCPProviderBadge,
|
||||
GitHubProviderBadge,
|
||||
IacProviderBadge,
|
||||
KS8ProviderBadge,
|
||||
M365ProviderBadge,
|
||||
OracleCloudProviderBadge,
|
||||
};
|
||||
|
||||
// Map provider display names to their icon components
|
||||
export const PROVIDER_ICONS: Record<string, React.FC<IconSvgProps>> = {
|
||||
AWS: AWSProviderBadge,
|
||||
Azure: AzureProviderBadge,
|
||||
"Google Cloud": GCPProviderBadge,
|
||||
Kubernetes: KS8ProviderBadge,
|
||||
"Microsoft 365": M365ProviderBadge,
|
||||
GitHub: GitHubProviderBadge,
|
||||
"Infrastructure as Code": IacProviderBadge,
|
||||
"Oracle Cloud Infrastructure": OracleCloudProviderBadge,
|
||||
};
|
||||
|
||||
55
ui/lib/charts/colors.ts
Normal file
55
ui/lib/charts/colors.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// CSS variable names for chart colors defined in globals.css
|
||||
export const CHART_COLOR_MAP: Record<string, string> = {
|
||||
// Status colors
|
||||
Success: "--color-bg-pass",
|
||||
Pass: "--color-bg-pass",
|
||||
Fail: "--color-bg-fail",
|
||||
// Provider colors
|
||||
AWS: "--color-bg-data-aws",
|
||||
Azure: "--color-bg-data-azure",
|
||||
"Google Cloud": "--color-bg-data-gcp",
|
||||
Kubernetes: "--color-bg-data-kubernetes",
|
||||
"Microsoft 365": "--color-bg-data-m365",
|
||||
GitHub: "--color-bg-data-github",
|
||||
"Infrastructure as Code": "--color-bg-data-muted",
|
||||
"Oracle Cloud Infrastructure": "--color-bg-data-muted",
|
||||
// Severity colors
|
||||
Critical: "--color-bg-data-critical",
|
||||
High: "--color-bg-data-high",
|
||||
Medium: "--color-bg-data-medium",
|
||||
Low: "--color-bg-data-low",
|
||||
Info: "--color-bg-data-info",
|
||||
Informational: "--color-bg-data-info",
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute color value from CSS variable name at runtime.
|
||||
* SVG fill attributes cannot directly resolve CSS variables,
|
||||
* so we extract computed values from globals.css CSS variables.
|
||||
* Falls back to black (#000000) if variable not found or access fails.
|
||||
*/
|
||||
export function getChartColor(colorName: string): string {
|
||||
const varName = CHART_COLOR_MAP[colorName];
|
||||
if (!varName) return "#000000";
|
||||
|
||||
try {
|
||||
if (typeof document === "undefined") {
|
||||
return "#000000";
|
||||
}
|
||||
return (
|
||||
getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(varName)
|
||||
.trim() || "#000000"
|
||||
);
|
||||
} catch {
|
||||
return "#000000";
|
||||
}
|
||||
}
|
||||
|
||||
export function initializeChartColors(): Record<string, string> {
|
||||
const colors: Record<string, string> = {};
|
||||
for (const [colorName] of Object.entries(CHART_COLOR_MAP)) {
|
||||
colors[colorName] = getChartColor(colorName);
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
@@ -11,6 +11,24 @@ export const PROVIDER_TYPES = [
|
||||
|
||||
export type ProviderType = (typeof PROVIDER_TYPES)[number];
|
||||
|
||||
export const PROVIDER_DISPLAY_NAMES: Record<ProviderType, string> = {
|
||||
aws: "AWS",
|
||||
azure: "Azure",
|
||||
gcp: "Google Cloud",
|
||||
kubernetes: "Kubernetes",
|
||||
m365: "Microsoft 365",
|
||||
github: "GitHub",
|
||||
iac: "Infrastructure as Code",
|
||||
oraclecloud: "Oracle Cloud Infrastructure",
|
||||
};
|
||||
|
||||
export function getProviderDisplayName(providerId: string): string {
|
||||
return (
|
||||
PROVIDER_DISPLAY_NAMES[providerId.toLowerCase() as ProviderType] ||
|
||||
providerId
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProviderProps {
|
||||
id: string;
|
||||
type: "providers";
|
||||
@@ -99,8 +117,8 @@ export interface ProvidersApiResponse {
|
||||
included?: Array<{
|
||||
type: string;
|
||||
id: string;
|
||||
attributes: any;
|
||||
relationships?: any;
|
||||
attributes: Record<string, unknown>;
|
||||
relationships?: Record<string, unknown>;
|
||||
}>;
|
||||
meta: {
|
||||
pagination: {
|
||||
|
||||
70
ui/types/severities.ts
Normal file
70
ui/types/severities.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const SEVERITY_LEVELS = [
|
||||
"critical",
|
||||
"high",
|
||||
"medium",
|
||||
"low",
|
||||
"informational",
|
||||
] as const;
|
||||
|
||||
export type SeverityLevel = (typeof SEVERITY_LEVELS)[number];
|
||||
|
||||
export const SEVERITY_DISPLAY_NAMES: Record<SeverityLevel, string> = {
|
||||
critical: "Critical",
|
||||
high: "High",
|
||||
medium: "Medium",
|
||||
low: "Low",
|
||||
informational: "Informational",
|
||||
};
|
||||
|
||||
// CSS variables for chart libraries (Recharts) that require inline style color values
|
||||
export const SEVERITY_COLORS: Record<SeverityLevel, string> = {
|
||||
critical: "var(--color-bg-data-critical)",
|
||||
high: "var(--color-bg-data-high)",
|
||||
medium: "var(--color-bg-data-medium)",
|
||||
low: "var(--color-bg-data-low)",
|
||||
informational: "var(--color-bg-data-info)",
|
||||
};
|
||||
|
||||
export const SEVERITY_FILTER_MAP: Record<string, SeverityLevel> = {
|
||||
Critical: "critical",
|
||||
High: "high",
|
||||
Medium: "medium",
|
||||
Low: "low",
|
||||
Info: "informational",
|
||||
Informational: "informational",
|
||||
};
|
||||
|
||||
export interface SeverityLineConfig {
|
||||
dataKey: SeverityLevel;
|
||||
color: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Pre-built line configs for charts (ordered from lowest to highest severity)
|
||||
export const SEVERITY_LINE_CONFIGS: SeverityLineConfig[] = [
|
||||
{
|
||||
dataKey: "informational",
|
||||
color: SEVERITY_COLORS.informational,
|
||||
label: SEVERITY_DISPLAY_NAMES.informational,
|
||||
},
|
||||
{
|
||||
dataKey: "low",
|
||||
color: SEVERITY_COLORS.low,
|
||||
label: SEVERITY_DISPLAY_NAMES.low,
|
||||
},
|
||||
{
|
||||
dataKey: "medium",
|
||||
color: SEVERITY_COLORS.medium,
|
||||
label: SEVERITY_DISPLAY_NAMES.medium,
|
||||
},
|
||||
{
|
||||
dataKey: "high",
|
||||
color: SEVERITY_COLORS.high,
|
||||
label: SEVERITY_DISPLAY_NAMES.high,
|
||||
},
|
||||
{
|
||||
dataKey: "critical",
|
||||
color: SEVERITY_COLORS.critical,
|
||||
label: SEVERITY_DISPLAY_NAMES.critical,
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user