feat: Graph de corrélations complet + Fix IPv4
🕸️ GRAPH DE CORRÉLATIONS ENRICH NOUVEAUX NŒUDS AFFICHÉS: • IP Source (centre) - Bleu • Subnet /24 - Violet • ASN - Orange • JA4 (jusqu'à 8) - Vert • User-Agent (jusqu'à 6) - Rouge • Host (jusqu'à 6) - Jaune • Pays - Gris • Path URL (jusqu'à 4) - Cyan • Query Params (jusqu'à 4) - Rose FONCTIONNALITÉS: • Positionnement circulaire autour de l'IP • Filtres par type de nœud (checkboxes) • Légende complète • Statistiques en temps réel • Zoom/Pan/Scroll • Noeuds déplaçables • Arêtes animées avec labels • Code couleur par classification UA FIX IPv4: • Suppression du prefix ::ffff: • cleanIP() appliqué partout • Affichage IP propre dans tous les composants UI/UX: • Panneau de filtres (top-left) • Légende (top-right) • Stats (bottom-left) • Contrôles zoom intégrés • Background grille • Arêtes avec flèches directionnelles PERFORMANCES: • Max 30 nœuds affichés • Build: OK • Container: healthy Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -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<GraphData>({ nodes: [], edges: [] });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
// Filtres
|
||||
const [filters, setFilters] = useState<FilterState>({
|
||||
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<string, { x: number; y: number }>();
|
||||
|
||||
// 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: (
|
||||
<div className="p-3 bg-blue-500/20 border border-blue-500 rounded-lg">
|
||||
<div className="text-xs text-blue-400 font-bold">IP SOURCE</div>
|
||||
<div className="text-sm text-white font-mono">{ip}</div>
|
||||
<div className="p-3 bg-blue-600 border-2 border-blue-400 rounded-lg shadow-lg min-w-[180px]">
|
||||
<div className="text-xs text-blue-200 font-bold mb-1">🌐 IP SOURCE</div>
|
||||
<div className="text-sm text-white font-mono font-bold">{cleanIpAddress}</div>
|
||||
<div className="text-xs text-blue-200 mt-2">
|
||||
{variability?.total_detections?.toLocaleString() || 0} détections
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-purple-500/20 border border-purple-500 rounded-lg">
|
||||
<div className="text-xs text-purple-400 font-bold">SUBNET /24</div>
|
||||
<div className="text-sm text-white font-mono">{subnet.subnet}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{subnet.total_in_subnet} IPs</div>
|
||||
<div className="p-3 bg-purple-600 border-2 border-purple-400 rounded-lg shadow-lg min-w-[180px]">
|
||||
<div className="text-xs text-purple-200 font-bold mb-1">🔷 SUBNET /24</div>
|
||||
<div className="text-sm text-white font-mono">{subnetClean}</div>
|
||||
<div className="text-xs text-purple-200 mt-2">
|
||||
{subnet.total_in_subnet || 0} IPs actives
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-orange-500/20 border border-orange-500 rounded-lg">
|
||||
<div className="text-xs text-orange-400 font-bold">ASN</div>
|
||||
<div className="text-sm text-white">AS{subnet.asn_number}</div>
|
||||
<div className="text-xs text-gray-400 mt-1 truncate max-w-[150px]">{subnet.asn_org || 'Unknown'}</div>
|
||||
<div className="p-3 bg-orange-600 border-2 border-orange-400 rounded-lg shadow-lg min-w-[180px]">
|
||||
<div className="text-xs text-orange-200 font-bold mb-1">🏢 ASN</div>
|
||||
<div className="text-sm text-white font-bold">AS{subnet.asn_number}</div>
|
||||
<div className="text-xs text-orange-200 mt-1 truncate max-w-[160px]">
|
||||
{subnet.asn_org || 'Unknown'}
|
||||
</div>
|
||||
<div className="text-xs text-orange-200 mt-1">
|
||||
{subnet.total_in_asn?.toLocaleString() || 0} IPs totales
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-green-500/20 border border-green-500 rounded-lg">
|
||||
<div className="text-xs text-green-400 font-bold">🔐 JA4</div>
|
||||
<div className="text-xs text-white font-mono truncate max-w-[180px]">{ja4.value}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{ja4.count} détections</div>
|
||||
<div className="p-3 bg-green-600 border-2 border-green-400 rounded-lg shadow-lg min-w-[200px]">
|
||||
<div className="text-xs text-green-200 font-bold mb-1">🔐 JA4 Fingerprint</div>
|
||||
<div className="text-xs text-white font-mono break-all">{ja4.value}</div>
|
||||
<div className="text-xs text-green-200 mt-2">
|
||||
{ja4.count} détections • {ja4.percentage?.toFixed(1) || 0}%
|
||||
</div>
|
||||
{ja4.unique_ips && (
|
||||
<div className="text-xs text-green-200">
|
||||
{ja4.unique_ips} IPs uniques
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-red-500/20 border border-red-500 rounded-lg">
|
||||
<div className="text-xs text-red-400 font-bold">🤖 UA</div>
|
||||
<div className="text-xs text-white truncate max-w-[180px]">{ua.value}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{ua.percentage.toFixed(0)}%</div>
|
||||
<div className={`p-3 bg-red-600 border-2 ${borderColor} rounded-lg shadow-lg min-w-[220px]`}>
|
||||
<div className="text-xs text-red-200 font-bold mb-1">
|
||||
🤖 User-Agent {classification !== 'normal' && `(${classification.toUpperCase()})`}
|
||||
</div>
|
||||
<div className="text-xs text-white break-all max-h-[80px] overflow-y-auto font-mono">
|
||||
{ua.value}
|
||||
</div>
|
||||
<div className="text-xs text-red-200 mt-2 flex items-center justify-between">
|
||||
<span>{ua.count} détections</span>
|
||||
<span>{ua.percentage?.toFixed(1) || 0}%</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-yellow-500/20 border border-yellow-500 rounded-lg">
|
||||
<div className="text-xs text-yellow-400 font-bold">🌍 PAYS</div>
|
||||
<div className="text-lg">{getCountryFlag(country.value)}</div>
|
||||
<div className="text-sm text-white">{country.value}</div>
|
||||
<div className="text-xs text-gray-400 mt-1">{country.percentage.toFixed(0)}%</div>
|
||||
<div className="p-3 bg-gray-600 border-2 border-gray-400 rounded-lg shadow-lg min-w-[150px] text-center">
|
||||
<div className="text-xs text-gray-200 font-bold mb-1">🌍 PAYS</div>
|
||||
<div className="text-4xl mb-1">{getCountryFlag(country.value)}</div>
|
||||
<div className="text-sm text-white font-bold">{country.value}</div>
|
||||
<div className="text-xs text-gray-200 mt-1">
|
||||
{country.percentage?.toFixed(0) || 0}% • {country.count} détections
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-yellow-600 border-2 border-yellow-400 rounded-lg shadow-lg min-w-[180px]">
|
||||
<div className="text-xs text-yellow-200 font-bold mb-1">🖥️ Host</div>
|
||||
<div className="text-sm text-white break-all font-mono">{host}</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-cyan-600 border-2 border-cyan-400 rounded-lg shadow-lg min-w-[200px]">
|
||||
<div className="text-xs text-cyan-200 font-bold mb-1">📁 Path URL</div>
|
||||
<div className="text-xs text-white break-all font-mono">{path.value}</div>
|
||||
<div className="text-xs text-cyan-200 mt-1">
|
||||
{path.count} req • {path.percentage?.toFixed(1) || 0}%
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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: (
|
||||
<div className="p-3 bg-pink-600 border-2 border-pink-400 rounded-lg shadow-lg min-w-[180px]">
|
||||
<div className="text-xs text-pink-200 font-bold mb-1">🔑 Query Params</div>
|
||||
<div className="text-xs text-white break-all font-mono">{qp.value}</div>
|
||||
<div className="text-xs text-pink-200 mt-1">
|
||||
{qp.count} fois
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
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 (
|
||||
<div className="flex items-center justify-center" style={{ height }}>
|
||||
<div className="text-text-secondary">Chargement du graph...</div>
|
||||
<div className="text-text-secondary">Chargement du graph de corrélations...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -234,7 +463,7 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps
|
||||
<div className="flex items-center justify-center" style={{ height }}>
|
||||
<div className="text-text-secondary text-center">
|
||||
<div className="text-4xl mb-2">🕸️</div>
|
||||
<div className="text-sm">Aucune corrélation trouvée</div>
|
||||
<div className="text-sm">Aucune corrélation trouvée pour {cleanIP(ip)}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -256,9 +485,135 @@ export function CorrelationGraph({ ip, height = '500px' }: CorrelationGraphProps
|
||||
zoomOnScroll={true}
|
||||
panOnScroll={true}
|
||||
panOnDrag={true}
|
||||
minZoom={0.2}
|
||||
maxZoom={2}
|
||||
>
|
||||
<Background color="#374151" gap={20} size={1} />
|
||||
<Controls className="bg-background-card border border-background-card rounded-lg" />
|
||||
|
||||
{/* Panneau de filtres */}
|
||||
<Panel position="top-left" className="bg-background-secondary border border-background-card rounded-lg p-3 shadow-lg">
|
||||
<div className="text-xs font-bold text-text-primary mb-2">Filtres</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showIP}
|
||||
onChange={() => toggleFilter('showIP')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">IP</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showSubnet}
|
||||
onChange={() => toggleFilter('showSubnet')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">Subnet</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showASN}
|
||||
onChange={() => toggleFilter('showASN')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">ASN</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showJA4}
|
||||
onChange={() => toggleFilter('showJA4')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">JA4</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showUA}
|
||||
onChange={() => toggleFilter('showUA')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">UA</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showHost}
|
||||
onChange={() => toggleFilter('showHost')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">Hosts</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showCountry}
|
||||
onChange={() => toggleFilter('showCountry')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">Pays</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filters.showPath}
|
||||
onChange={() => toggleFilter('showPath')}
|
||||
className="rounded bg-background-card border-background-card text-accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">Paths</span>
|
||||
</label>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* Légende */}
|
||||
<Panel position="top-right" className="bg-background-secondary border border-background-card rounded-lg p-3 shadow-lg">
|
||||
<div className="text-xs font-bold text-text-primary mb-2">Légende</div>
|
||||
<div className="space-y-1 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-blue-600"></div>
|
||||
<span className="text-text-primary">IP Source</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-purple-600"></div>
|
||||
<span className="text-text-primary">Subnet /24</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-orange-600"></div>
|
||||
<span className="text-text-primary">ASN</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-green-600"></div>
|
||||
<span className="text-text-primary">JA4</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-red-600"></div>
|
||||
<span className="text-text-primary">User-Agent</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-yellow-600"></div>
|
||||
<span className="text-text-primary">Host</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-3 h-3 rounded bg-gray-600"></div>
|
||||
<span className="text-text-primary">Pays</span>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
{/* Stats rapides */}
|
||||
<Panel position="bottom-left" className="bg-background-secondary border border-background-card rounded-lg p-3 shadow-lg">
|
||||
<div className="text-xs font-bold text-text-primary mb-2">Statistiques</div>
|
||||
<div className="text-xs text-text-secondary space-y-1">
|
||||
<div>Nœuds: {nodes.length}</div>
|
||||
<div>Arêtes: {edges.length}</div>
|
||||
<div>IP: {cleanIP(ip)}</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user