From f9c140dbbd8bc81995604752c7262834b847b37a Mon Sep 17 00:00:00 2001 From: Alan Buscaglia Date: Tue, 19 May 2026 18:17:38 +0200 Subject: [PATCH] fix(ui): classify attack path findings exactly - Treat only ProwlerFinding as a clickable finding node - Keep GuardDuty and Inspector findings as graph resources - Add regression coverage for provider finding resources --- .../_components/graph/attack-path-graph.tsx | 7 +-- .../_components/graph/graph-legend.tsx | 3 +- .../node-detail/node-detail-panel.test.tsx | 20 ++++++++ .../node-detail/node-detail-panel.tsx | 9 ++-- .../_components/node-detail/node-overview.tsx | 6 +-- .../(workflow)/query-builder/_lib/export.ts | 4 +- .../query-builder/_lib/graph-colors.ts | 6 ++- .../query-builder/_lib/graph-utils.ts | 10 ++-- .../(workflow)/query-builder/_lib/index.ts | 1 + .../query-builder/_lib/layout.test.ts | 48 +++++++++++++++++++ .../(workflow)/query-builder/_lib/layout.ts | 4 +- .../query-builder/_lib/node-types.ts | 8 ++++ .../query-builder/_lib/node-visuals.test.ts | 22 +++++++++ .../query-builder/_lib/node-visuals.ts | 4 +- .../attack-paths-page.browser.test.tsx | 8 ++-- .../attack-paths-page.harness.ts | 9 ++-- .../query-builder/attack-paths-page.tsx | 10 ++-- 17 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-types.ts diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx index 6e9c608f64..4cfa466788 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/attack-path-graph.tsx @@ -31,9 +31,10 @@ import { getNodeColor, getPathEdges, GRAPH_EDGE_HIGHLIGHT_COLOR, + isProwlerFindingNode, resolveHiddenFindingIds, } from "../../_lib"; -import { isFindingNode, layoutWithDagre } from "../../_lib/layout"; +import { layoutWithDagre } from "../../_lib/layout"; import { FindingNode } from "./nodes/finding-node"; import { InternetNode } from "./nodes/internet-node"; import { ResourceNode } from "./nodes/resource-node"; @@ -387,7 +388,7 @@ const GraphCanvas = ({ const findingToResources = new Map>(); nodes.forEach((n) => { - if (isFindingNode(n.labels)) findingNodeIds.add(n.id); + if (isProwlerFindingNode(n.labels)) findingNodeIds.add(n.id); }); const resourcesWithFindings = new Set(); @@ -459,7 +460,7 @@ const GraphCanvas = ({ hidden: hiddenFindingIds.has(node.id), className: cn( node.className, - isFindingNode(node.data.graphNode.labels) || + isProwlerFindingNode(node.data.graphNode.labels) || resourcesWithFindings.has(node.id) ? "cursor-pointer" : "cursor-default", diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-legend.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-legend.tsx index 03e17cfd3e..c16105140d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-legend.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/graph-legend.tsx @@ -23,6 +23,7 @@ import { GRAPH_NODE_COLORS, } from "../../_lib/graph-colors"; import { resolveHiddenFindingIds } from "../../_lib/graph-utils"; +import { isProwlerFindingNode } from "../../_lib/node-types"; import { NODE_CATEGORY, resolveNodeVisual } from "../../_lib/node-visuals"; const LEGEND_PREVIEW = { @@ -197,7 +198,7 @@ const edgeItems: LegendEdgeItem[] = [ ]; const isFindingNode = (node: GraphNode): boolean => - node.labels.some((label) => label.toLowerCase().includes("finding")); + isProwlerFindingNode(node.labels); const getGraphEdges = ( data: AttackPathGraphData, diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx index d7209c68c8..5f7612657d 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.test.tsx @@ -50,6 +50,15 @@ const resourceNode: GraphNode = { }, }; +const guardDutyNode: GraphNode = { + id: "guard-duty-node-id", + labels: ["GuardDutyFinding"], + properties: { + id: "guard-duty-123", + title: "Port probe", + }, +}; + describe("NodeDetailPanel", () => { it("renders the view finding button only for finding nodes", () => { const { rerender } = render(); @@ -65,6 +74,17 @@ describe("NodeDetailPanel", () => { ).not.toBeInTheDocument(); }); + it("does not render the view finding button for cloud-provider finding resources", () => { + // Given/When + render(); + + // Then + expect( + screen.queryByRole("button", { name: /view finding/i }), + ).not.toBeInTheDocument(); + expect(screen.getByText("Node findings")).toBeInTheDocument(); + }); + it("calls onViewFinding with the node finding id", async () => { const user = userEvent.setup(); const onViewFinding = vi.fn(); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx index 97d8657bfd..78e3a57497 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-detail-panel.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/sheet/sheet"; import type { GraphNode } from "@/types/attack-paths"; +import { isProwlerFindingNode } from "../../_lib"; import { NodeFindings } from "./node-findings"; import { NodeOverview } from "./node-overview"; import { NodeResources } from "./node-resources"; @@ -37,9 +38,7 @@ export const NodeDetailContent = ({ onViewFinding?: (findingId: string) => void; viewFindingLoading?: boolean; }) => { - const isProwlerFinding = node?.labels.some((label) => - label.toLowerCase().includes("finding"), - ); + const isProwlerFinding = isProwlerFindingNode(node.labels); return (
@@ -105,9 +104,7 @@ export const NodeDetailPanel = ({ }: NodeDetailPanelProps) => { const isOpen = node !== null; - const isProwlerFinding = node?.labels.some((label) => - label.toLowerCase().includes("finding"), - ); + const isProwlerFinding = node ? isProwlerFindingNode(node.labels) : false; const findingId = node ? String(node.properties?.id || node.id) : ""; return ( diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx index 2e294cf069..330f6fa418 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/node-detail/node-overview.tsx @@ -5,7 +5,7 @@ import { CodeSnippet } from "@/components/ui/code-snippet/code-snippet"; import { DateWithTime } from "@/components/ui/entities/date-with-time"; import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths"; -import { formatNodeLabels } from "../../_lib"; +import { formatNodeLabels, isProwlerFindingNode } from "../../_lib"; interface NodeOverviewProps { node: GraphNode; @@ -25,9 +25,7 @@ export const NodeOverview = ({ node }: NodeOverviewProps) => { return String(value); }; - const isFinding = node.labels.some((label) => - label.toLowerCase().includes("finding"), - ); + const isFinding = isProwlerFindingNode(node.labels); return (
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/export.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/export.ts index a10e0d474e..851ac027ff 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/export.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/export.ts @@ -22,6 +22,7 @@ import { RESOURCE_NODE_DIMENSIONS, } from "./node-dimensions"; import { getNodeLabelDisplay } from "./node-label-lines"; +import { isProwlerFindingNode } from "./node-types"; import { resolveNodeVisual } from "./node-visuals"; interface ExportGraphOptions { @@ -67,8 +68,7 @@ const downloadDataUrl = (dataUrl: string, filename: string) => { document.body.removeChild(link); }; -const isFindingNode = (labels: string[]) => - labels.some((label) => label.toLowerCase().includes("finding")); +const isFindingNode = isProwlerFindingNode; const getGraphEdges = (graphData: AttackPathGraphData): GraphEdge[] => { if (graphData.edges?.length) return graphData.edges; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts index 6c79d6c60a..49abf16ad3 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-colors.ts @@ -1,3 +1,5 @@ +import { isProwlerFindingNode } from "./node-types"; + /** * Color constants for attack path graph visualization * Colors chosen to work well in both light and dark themes @@ -67,7 +69,7 @@ export const getNodeColor = ( labels: string[], properties?: Record, ): string => { - const isFinding = labels.some((l) => l.toLowerCase().includes("finding")); + const isFinding = isProwlerFindingNode(labels); if (isFinding && properties?.severity) { const severity = String(properties.severity).toLowerCase(); if (severity === "critical") return GRAPH_NODE_COLORS.critical; @@ -100,7 +102,7 @@ export const getNodeBorderColor = ( labels: string[], properties?: Record, ): string => { - const isFinding = labels.some((l) => l.toLowerCase().includes("finding")); + const isFinding = isProwlerFindingNode(labels); if (isFinding && properties?.severity) { const severity = String(properties.severity).toLowerCase(); if (severity === "critical") return GRAPH_NODE_BORDER_COLORS.critical; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-utils.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-utils.ts index e77126039f..c730f7a699 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-utils.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/graph-utils.ts @@ -4,6 +4,8 @@ import type { AttackPathGraphData } from "@/types/attack-paths"; +import { isProwlerFindingNode } from "./node-types"; + export const resolveHiddenFindingIds = ({ expandedResources, findingNodeIds, @@ -103,11 +105,11 @@ export const computeFilteredSubgraph = ( // Also include findings directly connected to the selected node const nodeLabelMap = new Map(nodes.map((n) => [n.id, n.labels])); edges.forEach((edge) => { - const sourceIsFinding = (nodeLabelMap.get(edge.source) ?? []).some((l) => - l.toLowerCase().includes("finding"), + const sourceIsFinding = isProwlerFindingNode( + nodeLabelMap.get(edge.source) ?? [], ); - const targetIsFinding = (nodeLabelMap.get(edge.target) ?? []).some((l) => - l.toLowerCase().includes("finding"), + const targetIsFinding = isProwlerFindingNode( + nodeLabelMap.get(edge.target) ?? [], ); // Include findings connected to the selected node diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts index 261a27cf8d..50fb64495e 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/index.ts @@ -18,6 +18,7 @@ export { resolveHiddenFindingIds, } from "./graph-utils"; export { layoutWithDagre } from "./layout"; +export { isProwlerFindingLabel, isProwlerFindingNode } from "./node-types"; export { NODE_CATEGORY, type NodeCategory, diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts index 7d8cb20c96..87a117771c 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.test.ts @@ -17,6 +17,18 @@ const resourceNode: GraphNode = { properties: { name: "bucket-1" }, }; +const guardDutyNode: GraphNode = { + id: "guard-duty-1", + labels: ["GuardDutyFinding"], + properties: { title: "Port probe", severity: "high" }, +}; + +const inspectorNode: GraphNode = { + id: "inspector-1", + labels: ["AWSInspectorFinding"], + properties: { title: "Package vulnerability", severity: "high" }, +}; + const internetNode: GraphNode = { id: "internet-1", labels: ["Internet"], @@ -54,6 +66,42 @@ describe("layoutWithDagre", () => { }); }); + it("treats cloud-provider finding resources as resource nodes", () => { + const { rfNodes } = layoutWithDagre( + [findingNode, guardDutyNode, inspectorNode], + [], + ); + + const byId = new Map(rfNodes.map((n) => [n.id, n])); + + expect(byId.get("finding-1")?.type).toBe("finding"); + expect(byId.get("guard-duty-1")).toMatchObject({ + type: "resource", + width: 136, + height: 124, + }); + expect(byId.get("inspector-1")?.type).toBe("resource"); + }); + + it("does not animate edges that only touch cloud-provider finding resources", () => { + const { rfEdges } = layoutWithDagre( + [resourceNode, guardDutyNode], + [ + { + id: "e1", + source: "guard-duty-1", + target: "resource-1", + type: "AFFECTS", + }, + ], + ); + + expect(rfEdges[0]).toMatchObject({ + animated: false, + className: "resource-edge", + }); + }); + it("is deterministic: same input produces equal output across runs", () => { const nodes = [findingNode, resourceNode]; const edges: GraphEdge[] = [ diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts index cb67d11083..c7997ecd08 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/layout.ts @@ -13,6 +13,7 @@ import { INTERNET_NODE_DIMENSIONS, RESOURCE_NODE_DIMENSIONS, } from "./node-dimensions"; +import { isProwlerFindingNode } from "./node-types"; // Container relationships that get reversed for proper hierarchy const CONTAINER_RELATIONS = new Set([ @@ -34,8 +35,7 @@ const NODE_TYPE = { type NodeType = (typeof NODE_TYPE)[keyof typeof NODE_TYPE]; -export const isFindingNode = (labels: string[]): boolean => - labels.some((l) => l.toLowerCase().includes("finding")); +const isFindingNode = isProwlerFindingNode; const getNodeType = (labels: string[]): NodeType => { if (isFindingNode(labels)) return NODE_TYPE.FINDING; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-types.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-types.ts new file mode 100644 index 0000000000..5de9eb6da5 --- /dev/null +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-types.ts @@ -0,0 +1,8 @@ +const normalizeNodeLabel = (label: string): string => + label.toLowerCase().replace(/[^a-z0-9]/g, ""); + +export const isProwlerFindingLabel = (label: string): boolean => + normalizeNodeLabel(label) === "prowlerfinding"; + +export const isProwlerFindingNode = (labels: string[]): boolean => + labels.some(isProwlerFindingLabel); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.test.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.test.ts index cb5f70f06e..48790f29e7 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.test.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.test.ts @@ -193,6 +193,28 @@ describe("resolveNodeVisual", () => { }); }); + it("should resolve cloud-provider finding resources as non-finding nodes", () => { + // Given + const guardDutyNode = buildNode(["GuardDutyFinding"], { + title: "Port probe", + severity: "high", + }); + const inspectorNode = buildNode(["AWSInspectorFinding"], { + title: "Package vulnerability", + severity: "high", + }); + + // When + const guardDutyVisual = resolveNodeVisual(guardDutyNode); + const inspectorVisual = resolveNodeVisual(inspectorNode); + + // Then + expect(guardDutyVisual.category).not.toBe(NODE_CATEGORY.FINDING); + expect(guardDutyVisual.description).toBe("Guard Duty Finding"); + expect(inspectorVisual.category).not.toBe(NODE_CATEGORY.FINDING); + expect(inspectorVisual.description).toBe("Aws Inspector Finding"); + }); + it("should resolve finding icons from severity", () => { // Given const findingNodes = [ diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.ts index 34c7b15450..19a45d6f85 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_lib/node-visuals.ts @@ -48,6 +48,7 @@ import { import type { GraphNode, GraphNodePropertyValue } from "@/types/attack-paths"; import { formatNodeLabel } from "./format"; +import { isProwlerFindingLabel } from "./node-types"; export const NODE_CATEGORY = { FINDING: "finding", @@ -390,8 +391,7 @@ const normalizeLabel = (label: string): string => const isKnownNodeLabel = (label: string): label is KnownNodeLabel => label in KNOWN_NODE_VISUALS; -const isFindingLabel = (label: string): boolean => - normalizeLabel(label).includes("finding"); +const isFindingLabel = isProwlerFindingLabel; const isInternetLabel = (label: string): boolean => normalizeLabel(label) === "internet"; diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.browser.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.browser.test.tsx index 9e455a4b6d..b3b8f3b417 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.browser.test.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.browser.test.tsx @@ -35,8 +35,8 @@ vi.mock("@/actions/findings", async () => { }); import { useGraphStore } from "./_hooks/use-graph-state"; -import { getPathEdges } from "./_lib"; -import { isFindingNode, layoutWithDagre } from "./_lib/layout"; +import { getPathEdges, isProwlerFindingNode } from "./_lib"; +import { layoutWithDagre } from "./_lib/layout"; import AttackPathsPage from "./attack-paths-page"; import { fixtures, type PageFixture } from "./attack-paths-page.fixtures"; import { AttackPathPageHarness } from "./attack-paths-page.harness"; @@ -218,7 +218,7 @@ describe("running a query", () => { if (!fixture.queryResult) throw new Error("Expected graph fixture data"); const visibleNodes = fixture.queryResult.nodes.filter( - (node) => !isFindingNode(node.labels), + (node) => !isProwlerFindingNode(node.labels), ); const visibleNodeIds = new Set(visibleNodes.map((node) => node.id)); const visibleEdges = (fixture.queryResult.relationships ?? []) @@ -427,7 +427,7 @@ describe("exploring the graph", () => { const findingIds = new Set( (fixture.queryResult?.nodes ?? []) - .filter((node) => isFindingNode(node.labels)) + .filter((node) => isProwlerFindingNode(node.labels)) .map((node) => node.id), ); const visibleEdges = (fixture.queryResult?.relationships ?? []) diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts index dff9ccdb51..373e9943ba 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.harness.ts @@ -7,6 +7,7 @@ import { vi } from "vitest"; import { userEvent } from "vitest/browser"; +import { isProwlerFindingNode } from "./_lib"; import type { PageFixture } from "./attack-paths-page.fixtures"; export class AttackPathPageHarness { @@ -442,9 +443,7 @@ export class AttackPathPageHarness { async clickFirstResourceNodeWithoutFindings(): Promise { const findingIds = new Set( (this.fixture.queryResult?.nodes ?? []) - .filter((n) => - n.labels.some((l) => l.toLowerCase().includes("finding")), - ) + .filter((n) => isProwlerFindingNode(n.labels)) .map((n) => n.id), ); const resourceWithFindingIds = new Set(); @@ -512,9 +511,7 @@ export class AttackPathPageHarness { async expandAllFindings(): Promise { const findingIds = new Set( (this.fixture.queryResult?.nodes ?? []) - .filter((n) => - n.labels.some((l) => l.toLowerCase().includes("finding")), - ) + .filter((n) => isProwlerFindingNode(n.labels)) .map((n) => n.id), ); const resourceWithFindingIds = new Set(); diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx index 992fdee5ef..d3bad16888 100644 --- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx +++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/attack-paths-page.tsx @@ -55,7 +55,7 @@ import { import type { GraphHandle } from "./_components/graph/attack-path-graph"; import { useGraphState } from "./_hooks/use-graph-state"; import { useQueryBuilder } from "./_hooks/use-query-builder"; -import { exportGraphAsPNG } from "./_lib"; +import { exportGraphAsPNG, isProwlerFindingNode } from "./_lib"; /** * Attack Paths @@ -287,9 +287,7 @@ export default function AttackPathsPage() { }; const handleNodeClick = (node: GraphNode) => { - const isFinding = node.labels.some((label) => - label.toLowerCase().includes("finding"), - ); + const isFinding = isProwlerFindingNode(node.labels); if (isFinding) { if (findingNavigationInFlightRef.current) { @@ -313,9 +311,7 @@ export default function AttackPathsPage() { if (edge.source !== node.id && edge.target !== node.id) return false; const otherId = edge.source === node.id ? edge.target : edge.source; const otherNode = sourceData.nodes?.find(({ id }) => id === otherId); - return otherNode?.labels.some((label) => - label.toLowerCase().includes("finding"), - ); + return otherNode ? isProwlerFindingNode(otherNode.labels) : false; }); if (hasFindings) {