mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-21 02:44:16 +00:00
fix(ui): show threat map data for okta and google workspace accounts (#11563)
Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com>
This commit is contained in:
1 parent
75decbbedf
commit
d6528b674e
5 files changed
+181
-4
No files matched your search
@@ -6,6 +6,7 @@ All notable changes to the **Prowler UI** are documented in this file.
|
||||
|
||||
### 🐞 Fixed
|
||||
|
||||
- Threat Map no longer shows an empty map for accounts that only have Okta or Google Workspace scans [(#11542)](https://github.com/prowler-cloud/prowler/pull/11542)
|
||||
- Compliance attributes requests now pass the selected scan, so multi-provider universal frameworks (e.g. CSA CCM) load the check IDs of the scan's provider and Azure/GCP requirement details show their findings instead of appearing empty [(#11546)](https://github.com/prowler-cloud/prowler/pull/11546)
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { adaptRegionsOverviewToThreatMap } from "./threat-map.adapter";
|
||||
import type { RegionsOverviewResponse } from "./types";
|
||||
|
||||
function buildRegionsResponse(
|
||||
rows: Array<{ providerType: string; region: string }>,
|
||||
): RegionsOverviewResponse {
|
||||
return {
|
||||
data: rows.map(({ providerType, region }, index) => ({
|
||||
type: "regions-overview",
|
||||
id: `region-${index}`,
|
||||
attributes: {
|
||||
provider_type: providerType,
|
||||
region,
|
||||
total: 10,
|
||||
fail: 4,
|
||||
muted: 0,
|
||||
pass: 6,
|
||||
},
|
||||
})),
|
||||
meta: { version: "v1" },
|
||||
};
|
||||
}
|
||||
|
||||
describe("adaptRegionsOverviewToThreatMap", () => {
|
||||
it("maps okta regions to a global location", () => {
|
||||
const response = buildRegionsResponse([
|
||||
{ providerType: "okta", region: "global" },
|
||||
]);
|
||||
|
||||
const result = adaptRegionsOverviewToThreatMap(response);
|
||||
|
||||
expect(result.locations).toHaveLength(1);
|
||||
expect(result.locations[0]).toMatchObject({
|
||||
providerType: "okta",
|
||||
region: "global",
|
||||
name: "Okta - Global",
|
||||
totalFindings: 10,
|
||||
failFindings: 4,
|
||||
});
|
||||
expect(result.regions).toEqual(["global"]);
|
||||
});
|
||||
|
||||
it("maps googleworkspace regions to a global location", () => {
|
||||
const response = buildRegionsResponse([
|
||||
{ providerType: "googleworkspace", region: "global" },
|
||||
]);
|
||||
|
||||
const result = adaptRegionsOverviewToThreatMap(response);
|
||||
|
||||
expect(result.locations).toHaveLength(1);
|
||||
expect(result.locations[0]).toMatchObject({
|
||||
providerType: "googleworkspace",
|
||||
region: "global",
|
||||
name: "Google Workspace - Global",
|
||||
totalFindings: 10,
|
||||
failFindings: 4,
|
||||
});
|
||||
expect(result.regions).toEqual(["global"]);
|
||||
});
|
||||
});
|
||||
@@ -261,6 +261,19 @@ const ALIBABACLOUD_COORDINATES: Record<string, { lat: number; lng: number }> = {
|
||||
global: { lat: 30.3, lng: 120.2 }, // Global fallback (Hangzhou HQ)
|
||||
};
|
||||
|
||||
// Okta is a SaaS identity platform without user-facing regions
|
||||
const OKTA_COORDINATES: Record<string, { lat: number; lng: number }> = {
|
||||
global: { lat: 37.8, lng: -122.4 }, // Global fallback (San Francisco HQ)
|
||||
};
|
||||
|
||||
// Google Workspace is a SaaS suite without user-facing regions
|
||||
const GOOGLEWORKSPACE_COORDINATES: Record<
|
||||
string,
|
||||
{ lat: number; lng: number }
|
||||
> = {
|
||||
global: { lat: 37.4, lng: -122.1 }, // Global fallback (Mountain View HQ)
|
||||
};
|
||||
|
||||
const PROVIDER_COORDINATES: Record<
|
||||
string,
|
||||
Record<string, { lat: number; lng: number }>
|
||||
@@ -277,6 +290,8 @@ const PROVIDER_COORDINATES: Record<
|
||||
oraclecloud: ORACLECLOUD_COORDINATES,
|
||||
mongodbatlas: MONGODBATLAS_COORDINATES,
|
||||
alibabacloud: ALIBABACLOUD_COORDINATES,
|
||||
okta: OKTA_COORDINATES,
|
||||
googleworkspace: GOOGLEWORKSPACE_COORDINATES,
|
||||
};
|
||||
|
||||
// Returns [lng, lat] format for D3/GeoJSON compatibility
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ThreatMap } from "./threat-map";
|
||||
import type { ThreatMapData } from "./threat-map.types";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
}));
|
||||
|
||||
vi.mock("./horizontal-bar-chart", () => ({
|
||||
HorizontalBarChart: () => <div data-testid="bar-chart" />,
|
||||
}));
|
||||
|
||||
function buildLocation(providerType: string, region: string) {
|
||||
return {
|
||||
id: `${providerType}-${region}`,
|
||||
name: `${providerType} - ${region}`,
|
||||
region,
|
||||
regionCode: region,
|
||||
providerType,
|
||||
coordinates: [-122.4, 37.8] as [number, number],
|
||||
totalFindings: 10,
|
||||
failFindings: 4,
|
||||
riskLevel: "high" as const,
|
||||
severityData: [
|
||||
{ name: "Fail", value: 4, percentage: 40 },
|
||||
{ name: "Pass", value: 6, percentage: 60 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("ThreatMap region selector", () => {
|
||||
it("auto-selects the region when it is the only one available", () => {
|
||||
const data: ThreatMapData = {
|
||||
locations: [
|
||||
buildLocation("okta", "global"),
|
||||
buildLocation("googleworkspace", "global"),
|
||||
],
|
||||
regions: ["global"],
|
||||
};
|
||||
|
||||
render(<ThreatMap data={data} />);
|
||||
|
||||
const select = screen.getByRole("combobox", {
|
||||
name: "Filter threat map by region",
|
||||
});
|
||||
expect(select).toHaveValue("global");
|
||||
expect(screen.getByText("Global Regions")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText("Select a location on the map to view details"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps All Regions as default when there are multiple regions", () => {
|
||||
const data: ThreatMapData = {
|
||||
locations: [
|
||||
buildLocation("aws", "us-east-1"),
|
||||
buildLocation("okta", "global"),
|
||||
],
|
||||
regions: ["global", "us-east-1"],
|
||||
};
|
||||
|
||||
render(<ThreatMap data={data} />);
|
||||
|
||||
const select = screen.getByRole("combobox", {
|
||||
name: "Filter threat map by region",
|
||||
});
|
||||
expect(select).toHaveValue("All Regions");
|
||||
expect(
|
||||
screen.getByRole("option", { name: "All Regions" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the global option capitalized while keeping its filter value", () => {
|
||||
const data: ThreatMapData = {
|
||||
locations: [
|
||||
buildLocation("aws", "us-east-1"),
|
||||
buildLocation("okta", "global"),
|
||||
],
|
||||
regions: ["global", "us-east-1"],
|
||||
};
|
||||
|
||||
render(<ThreatMap data={data} />);
|
||||
|
||||
const globalOption = screen.getByRole("option", { name: "Global" });
|
||||
expect(globalOption).toHaveValue("global");
|
||||
expect(
|
||||
screen.getByRole("option", { name: "us-east-1" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -124,7 +124,11 @@ export function ThreatMap({
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const [selectedRegion, setSelectedRegion] = useState("All Regions");
|
||||
// With a single region "All Regions" adds nothing, so it starts selected
|
||||
const hasSingleRegion = data.regions.length === 1;
|
||||
const [selectedRegion, setSelectedRegion] = useState(
|
||||
hasSingleRegion ? data.regions[0] : "All Regions",
|
||||
);
|
||||
const [worldData, setWorldData] = useState<FeatureCollection | null>(null);
|
||||
const [isLoadingMap, setIsLoadingMap] = useState(true);
|
||||
const [dimensions, setDimensions] = useState<{
|
||||
@@ -424,10 +428,12 @@ export function ThreatMap({
|
||||
onChange={(e) => setSelectedRegion(e.target.value)}
|
||||
className="border-border-neutral-primary bg-bg-neutral-secondary text-text-neutral-primary appearance-none rounded-lg border px-4 py-2 pr-10 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||
>
|
||||
<option value="All Regions">All Regions</option>
|
||||
{!hasSingleRegion && (
|
||||
<option value="All Regions">All Regions</option>
|
||||
)}
|
||||
{sortedRegions.map((region) => (
|
||||
<option key={region} value={region}>
|
||||
{region}
|
||||
{region.toLowerCase() === "global" ? "Global" : region}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -467,7 +473,7 @@ export function ThreatMap({
|
||||
<div 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">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="bg-data-critical h-3 w-3 rounded"
|
||||
className="bg-bg-data-critical h-3 w-3 rounded"
|
||||
/>
|
||||
<span className="text-text-neutral-primary text-sm font-medium">
|
||||
{locationCount} Locations
|
||||
|
||||
Reference in new issue
Block a user