mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-23 20:04:27 +00:00
186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
/**
|
|
* Pure Dagre layout adapter for React Flow
|
|
* Converts normalized GraphNode[] + GraphEdge[] to positioned RF nodes
|
|
*/
|
|
|
|
import { Graph, layout as dagreLayout } from "@dagrejs/dagre";
|
|
import { type Edge, type Node, Position } from "@xyflow/react";
|
|
|
|
import { orientEdgeForLayout } from "@/lib/attack-paths/edge-orientation";
|
|
import {
|
|
GROUP_NODE_LABEL,
|
|
OUTCOME_NODE_LABEL,
|
|
} from "@/lib/attack-paths/group-graph";
|
|
import {
|
|
FINDING_NODE_DIMENSIONS,
|
|
GROUP_NODE_DIMENSIONS,
|
|
INTERNET_NODE_DIMENSIONS,
|
|
OUTCOME_NODE_DIMENSIONS,
|
|
RESOURCE_NODE_DIMENSIONS,
|
|
} from "@/lib/attack-paths/node-dimensions";
|
|
import { isProwlerFindingNode } from "@/lib/attack-paths/node-types";
|
|
import type { GraphEdge, GraphNode } from "@/types/attack-paths";
|
|
|
|
interface NodeData extends Record<string, unknown> {
|
|
graphNode: GraphNode;
|
|
}
|
|
|
|
// Shared with the `NODE_TYPES` component registry in attack-path-graph so the
|
|
// type strings can't drift apart.
|
|
export const NODE_TYPE = {
|
|
FINDING: "finding",
|
|
INTERNET: "internet",
|
|
RESOURCE: "resource",
|
|
// React Flow reserves "group" for its built-in container node (styled via
|
|
// .react-flow__node-group), so use a distinct type for our custom node.
|
|
GROUP: "classGroup",
|
|
OUTCOME: "outcome",
|
|
} as const;
|
|
|
|
type NodeType = (typeof NODE_TYPE)[keyof typeof NODE_TYPE];
|
|
|
|
const isFindingNode = isProwlerFindingNode;
|
|
|
|
const getNodeType = (labels: string[]): NodeType => {
|
|
// Synthetic view nodes are tagged with a reserved label.
|
|
if (labels.includes(OUTCOME_NODE_LABEL)) return NODE_TYPE.OUTCOME;
|
|
if (labels.includes(GROUP_NODE_LABEL)) return NODE_TYPE.GROUP;
|
|
if (isFindingNode(labels)) return NODE_TYPE.FINDING;
|
|
if (labels.some((l) => l.toLowerCase() === "internet"))
|
|
return NODE_TYPE.INTERNET;
|
|
return NODE_TYPE.RESOURCE;
|
|
};
|
|
|
|
const getNodeDimensions = (
|
|
type: NodeType,
|
|
): { width: number; height: number } => {
|
|
if (type === NODE_TYPE.FINDING)
|
|
return {
|
|
width: FINDING_NODE_DIMENSIONS.WIDTH,
|
|
height: FINDING_NODE_DIMENSIONS.HEIGHT,
|
|
};
|
|
if (type === NODE_TYPE.INTERNET)
|
|
return {
|
|
width: INTERNET_NODE_DIMENSIONS.DIAMETER,
|
|
height: INTERNET_NODE_DIMENSIONS.DIAMETER,
|
|
};
|
|
if (type === NODE_TYPE.GROUP)
|
|
return {
|
|
width: GROUP_NODE_DIMENSIONS.WIDTH,
|
|
height: GROUP_NODE_DIMENSIONS.HEIGHT,
|
|
};
|
|
if (type === NODE_TYPE.OUTCOME)
|
|
return {
|
|
width: OUTCOME_NODE_DIMENSIONS.WIDTH,
|
|
height: OUTCOME_NODE_DIMENSIONS.HEIGHT,
|
|
};
|
|
return {
|
|
width: RESOURCE_NODE_DIMENSIONS.WIDTH,
|
|
height: RESOURCE_NODE_DIMENSIONS.HEIGHT,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Pure layout function: computes positioned React Flow nodes from graph data.
|
|
* Deterministic — same inputs always produce same outputs.
|
|
*/
|
|
export const layoutWithDagre = (
|
|
nodes: GraphNode[],
|
|
edges: GraphEdge[],
|
|
): { rfNodes: Node<NodeData>[]; rfEdges: Edge[] } => {
|
|
const g = new Graph({ multigraph: true });
|
|
g.setGraph({
|
|
rankdir: "LR",
|
|
nodesep: 80,
|
|
ranksep: 150,
|
|
marginx: 50,
|
|
marginy: 50,
|
|
});
|
|
g.setDefaultEdgeLabel(() => ({}));
|
|
|
|
// Add nodes with type-based dimensions
|
|
nodes.forEach((node) => {
|
|
const type = getNodeType(node.labels);
|
|
const { width, height } = getNodeDimensions(type);
|
|
g.setNode(node.id, { label: node.id, width, height });
|
|
});
|
|
|
|
// Add edges, reversing container relationships for proper hierarchy
|
|
edges.forEach((edge) => {
|
|
const [sourceId, targetId] = orientEdgeForLayout(
|
|
edge.source,
|
|
edge.target,
|
|
edge.type,
|
|
);
|
|
|
|
if (sourceId && targetId) {
|
|
g.setEdge(
|
|
sourceId,
|
|
targetId,
|
|
{
|
|
id: edge.id,
|
|
originalSource: edge.source,
|
|
originalTarget: edge.target,
|
|
},
|
|
edge.id,
|
|
);
|
|
}
|
|
});
|
|
|
|
dagreLayout(g);
|
|
|
|
// Build RF nodes from layout
|
|
const rfNodes: Node<NodeData>[] = nodes.map((node) => {
|
|
const dagreNode = g.node(node.id);
|
|
const type = getNodeType(node.labels);
|
|
const { width, height } = getNodeDimensions(type);
|
|
|
|
return {
|
|
id: node.id,
|
|
type,
|
|
position: {
|
|
x: dagreNode.x - width / 2,
|
|
y: dagreNode.y - height / 2,
|
|
},
|
|
sourcePosition: Position.Right,
|
|
targetPosition: Position.Left,
|
|
data: { graphNode: node },
|
|
width,
|
|
height,
|
|
};
|
|
});
|
|
|
|
// Build RF edges from dagre edges (using layout order, not original)
|
|
const rfEdges: Edge[] = g
|
|
.edges()
|
|
.map((e: { v: string; w: string; name?: string }) => {
|
|
const edgeData = g.edge(e) as {
|
|
id?: string;
|
|
originalSource: string;
|
|
originalTarget: string;
|
|
};
|
|
|
|
// Check if either end is a finding node
|
|
const sourceNode = nodes.find((n) => n.id === e.v);
|
|
const targetNode = nodes.find((n) => n.id === e.w);
|
|
const hasFinding =
|
|
isFindingNode(sourceNode?.labels ?? []) ||
|
|
isFindingNode(targetNode?.labels ?? []);
|
|
|
|
return {
|
|
id: edgeData.id ?? e.name ?? `${e.v}-${e.w}`,
|
|
source: e.v,
|
|
target: e.w,
|
|
animated: hasFinding,
|
|
className: hasFinding ? "finding-edge" : "resource-edge",
|
|
data: {
|
|
pathKey: `${e.v}-${e.w}`,
|
|
originalSource: edgeData.originalSource,
|
|
originalTarget: edgeData.originalTarget,
|
|
},
|
|
};
|
|
});
|
|
|
|
return { rfNodes, rfEdges };
|
|
};
|