From d6528b674e7e3f6c26ecb4b4c5ba88e624fd5e1d Mon Sep 17 00:00:00 2001 From: Prowler Bot Date: Fri, 12 Jun 2026 10:18:43 +0200 Subject: [PATCH] fix(ui): show threat map data for okta and google workspace accounts (#11563) Co-authored-by: Alejandro Bailo <59607668+alejandrobailo@users.noreply.github.com> --- ui/CHANGELOG.md | 1 + .../regions/threat-map.adapter.test.ts | 62 +++++++++++++ .../overview/regions/threat-map.adapter.ts | 15 +++ ui/components/graphs/threat-map.test.tsx | 93 +++++++++++++++++++ ui/components/graphs/threat-map.tsx | 14 ++- 5 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 ui/actions/overview/regions/threat-map.adapter.test.ts create mode 100644 ui/components/graphs/threat-map.test.tsx diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index 277fabb5b0..4e5aa06e45 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -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) --- diff --git a/ui/actions/overview/regions/threat-map.adapter.test.ts b/ui/actions/overview/regions/threat-map.adapter.test.ts new file mode 100644 index 0000000000..515825bb0a --- /dev/null +++ b/ui/actions/overview/regions/threat-map.adapter.test.ts @@ -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"]); + }); +}); diff --git a/ui/actions/overview/regions/threat-map.adapter.ts b/ui/actions/overview/regions/threat-map.adapter.ts index 0b7852b56a..6ee0958aad 100644 --- a/ui/actions/overview/regions/threat-map.adapter.ts +++ b/ui/actions/overview/regions/threat-map.adapter.ts @@ -261,6 +261,19 @@ const ALIBABACLOUD_COORDINATES: Record = { 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 = { + 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 @@ -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 diff --git a/ui/components/graphs/threat-map.test.tsx b/ui/components/graphs/threat-map.test.tsx new file mode 100644 index 0000000000..47d447bddc --- /dev/null +++ b/ui/components/graphs/threat-map.test.tsx @@ -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: () =>
, +})); + +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(); + + 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(); + + 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(); + + const globalOption = screen.getByRole("option", { name: "Global" }); + expect(globalOption).toHaveValue("global"); + expect( + screen.getByRole("option", { name: "us-east-1" }), + ).toBeInTheDocument(); + }); +}); diff --git a/ui/components/graphs/threat-map.tsx b/ui/components/graphs/threat-map.tsx index 42b44aa8d1..b73f81336c 100644 --- a/ui/components/graphs/threat-map.tsx +++ b/ui/components/graphs/threat-map.tsx @@ -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(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" > - + {!hasSingleRegion && ( + + )} {sortedRegions.map((region) => ( ))} @@ -467,7 +473,7 @@ export function ThreatMap({