"use client"; import { useRef, useEffect, useState } from "react"; export interface AttackNode { id: string; label: string; type: "attacker" | "entry_point" | "pivot" | "target"; risk_level: "none" | "low" | "medium" | "high" | "critical"; } export interface AttackEdge { source: string; target: string; } interface AttackPathVisualizerProps { nodes: AttackNode[]; edges: AttackEdge[]; narrative?: string; } const getNodeColors = (type: string) => { const colors: Record = { attacker: { bg: "bg-red-950/60", border: "border-red-700/60", text: "text-red-300", }, entry_point: { bg: "bg-orange-950/60", border: "border-orange-700/60", text: "text-orange-300", }, pivot: { bg: "bg-amber-950/60", border: "border-amber-700/60", text: "text-amber-300", }, target: { bg: "bg-blue-950/60", border: "border-blue-700/60", text: "text-blue-300", }, }; return colors[type] || { bg: "bg-vault-dark", border: "border-vault-border", text: "text-vault-muted" }; }; const getRiskColor = (level: string) => { const colors: Record = { none: "#6b7280", low: "#3b82f6", medium: "#f59e0b", high: "#ef4444", critical: "#dc2626", }; return colors[level] || "#6b7280"; }; export default function AttackPathVisualizer({ nodes, edges, narrative, }: AttackPathVisualizerProps) { const svgRef = useRef(null); const [hoveredNode, setHoveredNode] = useState(null); if (!nodes || nodes.length === 0) { return (
No attack path data available
); } // Calculate positions for linear layout (left to right) const padding = 40; const nodeWidth = 120; const nodeHeight = 80; const svgWidth = nodes.length * (nodeWidth + 60) + padding * 2; const svgHeight = 200; const nodePositions: Record = {}; nodes.forEach((node, index) => { nodePositions[node.id] = { x: padding + index * (nodeWidth + 60), y: svgHeight / 2 - nodeHeight / 2, }; }); return (
{narrative && (

Attack Narrative

{narrative}

)}
{/* Edges/Arrows */} {edges.map((edge, idx) => { const source = nodePositions[edge.source]; const target = nodePositions[edge.target]; if (!source || !target) return null; const x1 = source.x + nodeWidth / 2; const y1 = source.y + nodeHeight / 2; const x2 = target.x - nodeWidth / 2; const y2 = target.y + nodeHeight / 2; return ( ); })} {/* Nodes */} {nodes.map((node) => { const pos = nodePositions[node.id]; if (!pos) return null; const colors = getNodeColors(node.type); const isHovered = hoveredNode === node.id; return ( {/* Node background */} setHoveredNode(node.id)} onMouseLeave={() => setHoveredNode(null)} /> {/* Node label */} {node.label} {/* Node type */} {node.type.replace(/_/g, " ")} {/* Risk level indicator */} ); })}
{/* Legend */}
{["attacker", "entry_point", "pivot", "target"].map((type) => { const colors = getNodeColors(type); return (
{type.replace(/_/g, " ")}
); })}
{/* Risk level legend */}

Risk Level

{["none", "low", "medium", "high", "critical"].map((level) => (
{level}
))}
); }