mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-05-19 18:53:33 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9c140dbbd |
+4
-3
@@ -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<string, Set<string>>();
|
||||
|
||||
nodes.forEach((n) => {
|
||||
if (isFindingNode(n.labels)) findingNodeIds.add(n.id);
|
||||
if (isProwlerFindingNode(n.labels)) findingNodeIds.add(n.id);
|
||||
});
|
||||
|
||||
const resourcesWithFindings = new Set<string>();
|
||||
@@ -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",
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+20
@@ -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(<NodeDetailPanel node={findingNode} />);
|
||||
@@ -65,6 +74,17 @@ describe("NodeDetailPanel", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render the view finding button for cloud-provider finding resources", () => {
|
||||
// Given/When
|
||||
render(<NodeDetailPanel node={guardDutyNode} />);
|
||||
|
||||
// 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();
|
||||
|
||||
+3
-6
@@ -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 (
|
||||
<div className="flex flex-col gap-6">
|
||||
@@ -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 (
|
||||
|
||||
+2
-4
@@ -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 (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, unknown>,
|
||||
): 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, unknown>,
|
||||
): 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,6 +18,7 @@ export {
|
||||
resolveHiddenFindingIds,
|
||||
} from "./graph-utils";
|
||||
export { layoutWithDagre } from "./layout";
|
||||
export { isProwlerFindingLabel, isProwlerFindingNode } from "./node-types";
|
||||
export {
|
||||
NODE_CATEGORY,
|
||||
type NodeCategory,
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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";
|
||||
|
||||
+4
-4
@@ -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 ?? [])
|
||||
|
||||
@@ -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<HTMLElement> {
|
||||
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<string>();
|
||||
@@ -512,9 +511,7 @@ export class AttackPathPageHarness {
|
||||
async expandAllFindings(): Promise<void> {
|
||||
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<string>();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user