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 4e2fd7eef0..ca94b23909 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
@@ -498,7 +498,7 @@ const GraphCanvas = ({
onNodeMouseEnter={handleNodeMouseEnter}
onNodeMouseLeave={handleNodeMouseLeave}
fitView
- fitViewOptions={{ padding: 0.2, includeHiddenNodes: true }}
+ fitViewOptions={{ ...AUTO_FIT_OPTIONS, includeHiddenNodes: true }}
// Supported React Flow behavior: wheel over the graph zooms the
// viewport. The surrounding UX avoids using node details as inline
// content, so this no longer fights a below-graph details section.
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx
new file mode 100644
index 0000000000..8b004adfb5
--- /dev/null
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.test.tsx
@@ -0,0 +1,72 @@
+import { render, screen } from "@testing-library/react";
+import { type NodeProps } from "@xyflow/react";
+import { describe, expect, it, vi } from "vitest";
+
+import type { GraphNode } from "@/types/attack-paths";
+
+import { FindingNode } from "./finding-node";
+
+vi.mock("./hidden-handles", () => ({
+ HiddenHandles: () => null,
+}));
+
+const buildFindingNode = (severity: string, title: string): GraphNode => ({
+ id: `${severity}-finding`,
+ labels: ["ProwlerFinding"],
+ properties: { check_title: title, id: `${severity}-finding`, severity },
+});
+
+const buildNodeProps = (graphNode: GraphNode): NodeProps =>
+ ({
+ id: graphNode.id,
+ type: "finding",
+ data: { graphNode },
+ selected: false,
+ dragging: false,
+ zIndex: 0,
+ isConnectable: false,
+ positionAbsoluteX: 0,
+ positionAbsoluteY: 0,
+ }) as unknown as NodeProps;
+
+describe("FindingNode", () => {
+ describe("severity visuals", () => {
+ it("should render the critical finding risk icon with readable text", () => {
+ // Given
+ const props = buildNodeProps(
+ buildFindingNode("critical", "Root key exposed"),
+ );
+
+ // When
+ render();
+
+ // Then
+ expect(
+ screen.getByTestId("attack-path-finding-icon-critical"),
+ ).toHaveAccessibleName("Critical finding risk icon");
+ expect(screen.getByText("Root key exposed")).toBeInTheDocument();
+ expect(screen.getByText("critical")).toBeInTheDocument();
+ });
+
+ it("should render a distinct medium finding risk icon with readable text", () => {
+ // Given
+ const props = buildNodeProps(
+ buildFindingNode("medium", "Bucket lacks logging"),
+ );
+
+ // When
+ render();
+
+ // Then
+ expect(
+ screen.getByTestId("attack-path-finding-icon-medium"),
+ ).toHaveAccessibleName("Medium finding risk icon");
+ expect(
+ screen.queryByTestId("attack-path-finding-icon-critical"),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText("Bucket lacks")).toBeInTheDocument();
+ expect(screen.getByText("logging")).toBeInTheDocument();
+ expect(screen.getByText("medium")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx
index 8ccfb8bd49..01d53123d9 100644
--- a/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx
+++ b/ui/app/(prowler)/attack-paths/(workflow)/query-builder/_components/graph/nodes/finding-node.tsx
@@ -4,17 +4,46 @@ import { type NodeProps } from "@xyflow/react";
import type { GraphNode } from "@/types/attack-paths";
-import { resolveNodeColors, truncateLabel } from "../../../_lib";
+import { resolveNodeColors, resolveNodeVisual } from "../../../_lib";
import { HiddenHandles } from "./hidden-handles";
+import { getNodeLabelLines } from "./node-label-lines";
interface FindingNodeData {
graphNode: GraphNode;
[key: string]: unknown;
}
-const HEXAGON_WIDTH = 200;
-const HEXAGON_HEIGHT = 55;
-const TITLE_MAX_CHARS = 24;
+const NODE_WIDTH = 150;
+const NODE_HEIGHT = 112;
+const TITLE_MAX_CHARS = 18;
+const TITLE_MAX_LINES = 2;
+const BADGE_SIZE = 44;
+const BADGE_RADIUS = BADGE_SIZE / 2;
+const BADGE_CENTER_X = NODE_WIDTH / 2;
+const BADGE_CENTER_Y = 26;
+const BADGE_LEFT_X = BADGE_CENTER_X - BADGE_RADIUS;
+const BADGE_RIGHT_INSET = NODE_WIDTH - (BADGE_CENTER_X + BADGE_RADIUS);
+const ICON_SIZE = 28;
+const ICON_X = BADGE_CENTER_X - ICON_SIZE / 2;
+const ICON_Y = BADGE_CENTER_Y - ICON_SIZE / 2;
+const TEXT_X = BADGE_CENTER_X;
+const TITLE_Y = 66;
+const TITLE_LINE_HEIGHT = 13;
+const SEVERITY_Y = 94;
+
+const severityLabel = (severity: unknown): string | undefined => {
+ if (!severity) return undefined;
+ const rawSeverity = Array.isArray(severity) ? severity[0] : severity;
+ return String(rawSeverity).toLowerCase();
+};
+
+const toFindingIconTestId = (severity: string | undefined): string =>
+ `attack-path-finding-icon-${severity ?? "unknown"}`;
+
+const toAccessibleSeverity = (severity: string | undefined): string =>
+ severity
+ ? `${severity.charAt(0).toUpperCase()}${severity.slice(1)}`
+ : "Unknown";
export const FindingNode = ({ data, selected }: NodeProps) => {
const { graphNode } = data as FindingNodeData;
@@ -30,51 +59,96 @@ export const FindingNode = ({ data, selected }: NodeProps) => {
graphNode.properties?.id ||
"Finding",
);
- const displayTitle = truncateLabel(title, TITLE_MAX_CHARS);
+ const displayTitleLines = getNodeLabelLines(
+ title,
+ TITLE_MAX_CHARS,
+ TITLE_MAX_LINES,
+ );
+ const visual = resolveNodeVisual(graphNode);
+ const Icon = visual.Icon;
+ const severity = severityLabel(graphNode.properties?.severity);
+ const iconLabel = `${toAccessibleSeverity(severity)} finding risk icon`;
- // Hexagon SVG path
- const w = HEXAGON_WIDTH;
- const h = HEXAGON_HEIGHT;
- const sideInset = w * 0.15;
- const hexPath = `
- M ${sideInset} 0
- L ${w - sideInset} 0
- L ${w} ${h / 2}
- L ${w - sideInset} ${h}
- L ${sideInset} ${h}
- L 0 ${h / 2}
- Z
- `;
+ const badgeStrokeWidth = selected ? 4 : 2.5;
+ const glowRadius = selected ? 32 : 30;
+ const glowOpacity = selected ? 0.34 : 0.28;
return (
<>
-
-