diff --git a/frontend/src/components/CorrelationGraph.tsx b/frontend/src/components/CorrelationGraph.tsx index 57bbaef..5f73f93 100644 --- a/frontend/src/components/CorrelationGraph.tsx +++ b/frontend/src/components/CorrelationGraph.tsx @@ -6,6 +6,7 @@ import ReactFlow, { useNodesState, useEdgesState, MarkerType, + Panel, } from 'reactflow'; import 'reactflow/dist/style.css'; import { useEffect, useState } from 'react'; @@ -20,59 +21,112 @@ interface GraphData { edges: Edge[]; } -export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps) { +interface FilterState { + showIP: boolean; + showSubnet: boolean; + showASN: boolean; + showJA4: boolean; + showUA: boolean; + showHost: boolean; + showCountry: boolean; + showPath: boolean; + showQueryParam: boolean; +} + +export function CorrelationGraph({ ip, height = '600px' }: CorrelationGraphProps) { const [graphData, setGraphData] = useState({ nodes: [], edges: [] }); const [loading, setLoading] = useState(true); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + // Filtres + const [filters, setFilters] = useState({ + showIP: true, + showSubnet: true, + showASN: true, + showJA4: true, + showUA: true, + showHost: true, + showCountry: true, + showPath: false, + showQueryParam: false, + }); + + // Nettoyer une adresse IP (enlever ::ffff: prefix) + const cleanIP = (address: string): string => { + if (!address) return ''; + // Enlever le préfixe IPv6-mapped IPv4 + return address.replace(/^::ffff:/i, ''); + }; useEffect(() => { const fetchCorrelationData = async () => { setLoading(true); try { - // Fetch data from multiple endpoints to build the graph - const [variabilityResponse, subnetResponse] = await Promise.all([ - fetch(`/api/variability/ip/${encodeURIComponent(ip)}`), - fetch(`/api/analysis/${encodeURIComponent(ip)}/subnet`), + // Fetch toutes les données de corrélation + const [variabilityResponse, subnetResponse, entitiesResponse] = await Promise.all([ + fetch(`/api/variability/ip/${encodeURIComponent(cleanIP(ip))}`), + fetch(`/api/analysis/${encodeURIComponent(cleanIP(ip))}/subnet`), + fetch(`/api/entities/ip/${encodeURIComponent(cleanIP(ip))}`), ]); const variability = await variabilityResponse.json().catch(() => null); const subnet = await subnetResponse.json().catch(() => null); + const entities = await entitiesResponse.json().catch(() => null); const newNodes: Node[] = []; const newEdges: Edge[] = []; + const nodePositions = new Map(); - // Node IP (center) + // Positionnement en cercle + const centerX = 400; + const centerY = 300; + const radius = 200; + + // Node IP (centre) + const cleanIpAddress = cleanIP(ip); + nodePositions.set('ip', { x: centerX, y: centerY }); newNodes.push({ id: 'ip', type: 'default', data: { label: ( -
-
IP SOURCE
-
{ip}
+
+
🌐 IP SOURCE
+
{cleanIpAddress}
+
+ {variability?.total_detections?.toLocaleString() || 0} détections +
) }, - position: { x: 400, y: 250 }, - style: { background: 'transparent', border: 'none', width: 200 }, + position: { x: centerX, y: centerY }, + style: { background: 'transparent', border: 'none', width: 200, zIndex: 10 }, }); - // Subnet node - if (subnet?.subnet) { + // Subnet node (haut gauche) + if (filters.showSubnet && subnet?.subnet) { + const subnetClean = subnet.subnet.replace(/^::ffff:/i, ''); + const angle = (180 + 135) * (Math.PI / 180); + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + nodePositions.set('subnet', { x, y }); + newNodes.push({ id: 'subnet', type: 'default', data: { label: ( -
-
SUBNET /24
-
{subnet.subnet}
-
{subnet.total_in_subnet} IPs
+
+
🔷 SUBNET /24
+
{subnetClean}
+
+ {subnet.total_in_subnet || 0} IPs actives +
) }, - position: { x: 50, y: 100 }, + position: { x, y }, style: { background: 'transparent', border: 'none', width: 200 }, }); newEdges.push({ @@ -81,26 +135,38 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps target: 'subnet', type: 'smoothstep', animated: true, - style: { stroke: '#8b5cf6', strokeWidth: 2 }, - markerEnd: { type: MarkerType.ArrowClosed, color: '#8b5cf6' }, + style: { stroke: '#a855f7', strokeWidth: 3 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#a855f7' }, + label: 'appartient à', + labelStyle: { fill: '#a855f7', fontWeight: 600, fontSize: 12 }, }); } - // ASN node - if (subnet?.asn_number) { + // ASN node (haut droite) + if (filters.showASN && subnet?.asn_number) { + const angle = (180 + 45) * (Math.PI / 180); + const x = centerX + radius * Math.cos(angle); + const y = centerY + radius * Math.sin(angle); + nodePositions.set('asn', { x, y }); + newNodes.push({ id: 'asn', type: 'default', data: { label: ( -
-
ASN
-
AS{subnet.asn_number}
-
{subnet.asn_org || 'Unknown'}
+
+
🏢 ASN
+
AS{subnet.asn_number}
+
+ {subnet.asn_org || 'Unknown'} +
+
+ {subnet.total_in_asn?.toLocaleString() || 0} IPs totales +
) }, - position: { x: 50, y: 350 }, + position: { x, y }, style: { background: 'transparent', border: 'none', width: 200 }, }); newEdges.push({ @@ -110,26 +176,39 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps type: 'smoothstep', style: { stroke: '#f97316', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#f97316' }, + label: 'via', + labelStyle: { fill: '#f97316', fontWeight: 600, fontSize: 12 }, }); } - // JA4 nodes - if (variability?.attributes?.ja4) { - variability.attributes.ja4.slice(0, 5).forEach((ja4: any, idx: number) => { + // JA4 nodes (bas gauche) + if (filters.showJA4 && variability?.attributes?.ja4) { + variability.attributes.ja4.slice(0, 8).forEach((ja4: any, idx: number) => { const ja4Id = `ja4-${idx}`; + const angle = (225 + (idx * 15)) * (Math.PI / 180); + const x = centerX + (radius + 80) * Math.cos(angle); + const y = centerY + (radius + 80) * Math.sin(angle); + newNodes.push({ id: ja4Id, type: 'default', data: { label: ( -
-
🔐 JA4
-
{ja4.value}
-
{ja4.count} détections
+
+
🔐 JA4 Fingerprint
+
{ja4.value}
+
+ {ja4.count} détections • {ja4.percentage?.toFixed(1) || 0}% +
+ {ja4.unique_ips && ( +
+ {ja4.unique_ips} IPs uniques +
+ )}
) }, - position: { x: 700, y: 50 + (idx * 100) }, + position: { x, y }, style: { background: 'transparent', border: 'none', width: 220 }, }); newEdges.push({ @@ -139,28 +218,53 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps type: 'smoothstep', style: { stroke: '#22c55e', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#22c55e' }, + label: 'utilise', + labelStyle: { fill: '#22c55e', fontWeight: 600, fontSize: 12 }, }); }); } - // User-Agent nodes - if (variability?.attributes?.user_agents) { - variability.attributes.user_agents.slice(0, 3).forEach((ua: any, idx: number) => { + // User-Agent nodes (bas droite) + if (filters.showUA && variability?.attributes?.user_agents) { + variability.attributes.user_agents.slice(0, 6).forEach((ua: any, idx: number) => { const uaId = `ua-${idx}`; + const angle = (315 + (idx * 10)) * (Math.PI / 180); + const x = centerX + (radius + 80) * Math.cos(angle); + const y = centerY + (radius + 80) * Math.sin(angle); + + // Classification UA + const uaLower = ua.value.toLowerCase(); + let classification = 'normal'; + let borderColor = 'border-green-400'; + if (uaLower.includes('bot') || uaLower.includes('crawler') || uaLower.includes('spider')) { + classification = 'bot'; + borderColor = 'border-red-400'; + } else if (uaLower.includes('python') || uaLower.includes('curl') || uaLower.includes('wget')) { + classification = 'script'; + borderColor = 'border-yellow-400'; + } + newNodes.push({ id: uaId, type: 'default', data: { label: ( -
-
🤖 UA
-
{ua.value}
-
{ua.percentage.toFixed(0)}%
+
+
+ 🤖 User-Agent {classification !== 'normal' && `(${classification.toUpperCase()})`} +
+
+ {ua.value} +
+
+ {ua.count} détections + {ua.percentage?.toFixed(1) || 0}% +
) }, - position: { x: 700, y: 400 + (idx * 120) }, - style: { background: 'transparent', border: 'none', width: 220 }, + position: { x, y }, + style: { background: 'transparent', border: 'none', width: 240 }, }); newEdges.push({ id: `ip-ua-${idx}`, @@ -169,27 +273,35 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps type: 'smoothstep', style: { stroke: '#ef4444', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#ef4444' }, + label: 'utilise', + labelStyle: { fill: '#ef4444', fontWeight: 600, fontSize: 12 }, }); }); } - // Country node - if (variability?.attributes?.countries && variability.attributes.countries.length > 0) { + // Country node (bas centre) + if (filters.showCountry && variability?.attributes?.countries && variability.attributes.countries.length > 0) { const country = variability.attributes.countries[0]; + const angle = 270 * (Math.PI / 180); + const x = centerX + (radius - 50) * Math.cos(angle); + const y = centerY + (radius - 50) * Math.sin(angle); + newNodes.push({ id: 'country', type: 'default', data: { label: ( -
-
🌍 PAYS
-
{getCountryFlag(country.value)}
-
{country.value}
-
{country.percentage.toFixed(0)}%
+
+
🌍 PAYS
+
{getCountryFlag(country.value)}
+
{country.value}
+
+ {country.percentage?.toFixed(0) || 0}% • {country.count} détections +
) }, - position: { x: 400, y: 500 }, + position: { x, y }, style: { background: 'transparent', border: 'none', width: 150 }, }); newEdges.push({ @@ -199,6 +311,119 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps type: 'smoothstep', style: { stroke: '#eab308', strokeWidth: 2 }, markerEnd: { type: MarkerType.ArrowClosed, color: '#eab308' }, + label: 'localisé', + labelStyle: { fill: '#eab308', fontWeight: 600, fontSize: 12 }, + }); + } + + // Hosts (depuis entities) + if (filters.showHost && entities?.related?.hosts) { + entities.related.hosts.slice(0, 6).forEach((host: string, idx: number) => { + const hostId = `host-${idx}`; + const angle = (300 + (idx * 12)) * (Math.PI / 180); + const x = centerX + (radius + 150) * Math.cos(angle); + const y = centerY + (radius + 150) * Math.sin(angle); + + newNodes.push({ + id: hostId, + type: 'default', + data: { + label: ( +
+
🖥️ Host
+
{host}
+
+ ) + }, + position: { x, y }, + style: { background: 'transparent', border: 'none', width: 200 }, + }); + newEdges.push({ + id: `ip-host-${idx}`, + source: 'ip', + target: hostId, + type: 'smoothstep', + style: { stroke: '#eab308', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#eab308' }, + label: 'cible', + labelStyle: { fill: '#eab308', fontWeight: 600, fontSize: 12 }, + }); + }); + } + + // Paths (depuis entities) + if (filters.showPath && entities?.paths) { + entities.paths.slice(0, 4).forEach((path: any, idx: number) => { + const pathId = `path-${idx}`; + const angle = (320 + (idx * 8)) * (Math.PI / 180); + const x = centerX + (radius + 220) * Math.cos(angle); + const y = centerY + (radius + 220) * Math.sin(angle); + + newNodes.push({ + id: pathId, + type: 'default', + data: { + label: ( +
+
📁 Path URL
+
{path.value}
+
+ {path.count} req • {path.percentage?.toFixed(1) || 0}% +
+
+ ) + }, + position: { x, y }, + style: { background: 'transparent', border: 'none', width: 220 }, + }); + newEdges.push({ + id: `ip-path-${idx}`, + source: 'ip', + target: pathId, + type: 'smoothstep', + style: { stroke: '#06b6d4', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#06b6d4' }, + label: 'accède à', + labelStyle: { fill: '#06b6d4', fontWeight: 600, fontSize: 12 }, + }); + }); + } + + // Query Params (depuis entities) + if (filters.showQueryParam && entities?.query_params) { + entities.query_params.slice(0, 4).forEach((qp: any, idx: number) => { + const qpId = `qp-${idx}`; + const angle = (340 + (idx * 8)) * (Math.PI / 180); + const x = centerX + (radius + 220) * Math.cos(angle); + const y = centerY + (radius + 220) * Math.sin(angle); + + newNodes.push({ + id: qpId, + type: 'default', + data: { + label: ( +
+
🔑 Query Params
+
{qp.value}
+
+ {qp.count} fois +
+
+ ) + }, + position: { x, y }, + style: { background: 'transparent', border: 'none', width: 200 }, + }); + newEdges.push({ + id: `ip-qp-${idx}`, + source: 'ip', + target: qpId, + type: 'smoothstep', + style: { stroke: '#ec4899', strokeWidth: 2 }, + markerEnd: { type: MarkerType.ArrowClosed, color: '#ec4899' }, + label: 'avec', + labelStyle: { fill: '#ec4899', fontWeight: 600, fontSize: 12 }, + }); }); } @@ -215,16 +440,20 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps if (ip) { fetchCorrelationData(); } - }, [ip, setNodes, setEdges]); + }, [ip, filters, setNodes, setEdges]); const getCountryFlag = (code: string) => { return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); }; + const toggleFilter = (key: keyof FilterState) => { + setFilters(prev => ({ ...prev, [key]: !prev[key] })); + }; + if (loading) { return (
-
Chargement du graph...
+
Chargement du graph de corrélations...
); } @@ -234,7 +463,7 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps
🕸️
-
Aucune corrélation trouvée
+
Aucune corrélation trouvée pour {cleanIP(ip)}
); @@ -256,9 +485,135 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps zoomOnScroll={true} panOnScroll={true} panOnDrag={true} + minZoom={0.2} + maxZoom={2} > + + {/* Panneau de filtres */} + +
Filtres
+
+ + + + + + + + +
+
+ + {/* Légende */} + +
Légende
+
+
+
+ IP Source +
+
+
+ Subnet /24 +
+
+
+ ASN +
+
+
+ JA4 +
+
+
+ User-Agent +
+
+
+ Host +
+
+
+ Pays +
+
+
+ + {/* Stats rapides */} + +
Statistiques
+
+
Nœuds: {nodes.length}
+
Arêtes: {edges.length}
+
IP: {cleanIP(ip)}
+
+
);