import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; // ─── Types ──────────────────────────────────────────────────────────────────── interface MLAnomaly { ip: string; ja4: string; host: string; hits: number; fuzzing_index: number; hit_velocity: number; temporal_entropy: number; is_fake_navigation: boolean; ua_ch_mismatch: boolean; sni_host_mismatch: boolean; is_ua_rotating: boolean; path_diversity_ratio: number; anomalous_payload_ratio: number; asn_label: string; bot_name: string; attack_type: string; } interface RadarData { ip: string; fuzzing_score: number; velocity_score: number; fake_nav_score: number; ua_mismatch_score: number; sni_mismatch_score: number; orphan_score: number; path_repetition_score: number; payload_anomaly_score: number; } interface ScatterPoint { ip: string; ja4: string; fuzzing_index: number; hit_velocity: number; hits: number; attack_type: string; } // ─── Helpers ────────────────────────────────────────────────────────────────── function formatNumber(n: number): string { return n.toLocaleString('fr-FR'); } function attackTypeEmoji(type: string): string { switch (type) { case 'brute_force': return '🔑'; case 'flood': return '🌊'; case 'scraper': return '🕷️'; case 'spoofing': return '🎭'; case 'scanner': return '🔍'; default: return '❓'; } } function attackTypeColor(type: string): string { switch (type) { case 'brute_force': return '#ef4444'; case 'flood': return '#3b82f6'; case 'scraper': return '#a855f7'; case 'spoofing': return '#f97316'; case 'scanner': return '#eab308'; default: return '#6b7280'; } } function fuzzingBadgeClass(value: number): string { if (value >= 200) return 'bg-threat-critical/20 text-threat-critical'; if (value >= 100) return 'bg-threat-high/20 text-threat-high'; if (value >= 50) return 'bg-threat-medium/20 text-threat-medium'; return 'bg-background-card text-text-secondary'; } // ─── Sub-components ─────────────────────────────────────────────────────────── function LoadingSpinner() { return (
); } function ErrorMessage({ message }: { message: string }) { return (
⚠️ {message}
); } // ─── Radar Chart (SVG octagonal) ───────────────────────────────────────────── const RADAR_AXES = [ { key: 'fuzzing_score', label: 'Fuzzing' }, { key: 'velocity_score', label: 'Vélocité' }, { key: 'fake_nav_score', label: 'Fausse nav' }, { key: 'ua_mismatch_score', label: 'UA/CH mismatch' }, { key: 'sni_mismatch_score', label: 'SNI mismatch' }, { key: 'orphan_score', label: 'Orphan ratio' }, { key: 'path_repetition_score', label: 'Répétition URL' }, { key: 'payload_anomaly_score', label: 'Payload anormal' }, ] as const; type RadarKey = typeof RADAR_AXES[number]['key']; function RadarChart({ data }: { data: RadarData }) { const size = 280; const cx = size / 2; const cy = size / 2; const maxR = 100; const n = RADAR_AXES.length; const angleOf = (i: number) => (i * 2 * Math.PI) / n - Math.PI / 2; const pointFor = (i: number, r: number): [number, number] => { const a = angleOf(i); return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }; // Background rings (at 25%, 50%, 75%, 100%) const rings = [25, 50, 75, 100]; const dataPoints = RADAR_AXES.map((axis, i) => { const val = Math.min((data[axis.key as RadarKey] ?? 0), 100); return pointFor(i, (val / 100) * maxR); }); const polygonPoints = dataPoints.map(([x, y]) => `${x},${y}`).join(' '); return ( {/* Background rings */} {rings.map((r) => { const pts = RADAR_AXES.map((_, i) => { const [x, y] = pointFor(i, (r / 100) * maxR); return `${x},${y}`; }).join(' '); return ( ); })} {/* Axis lines */} {RADAR_AXES.map((_, i) => { const [x, y] = pointFor(i, maxR); return ( ); })} {/* Data polygon */} {/* Data dots */} {dataPoints.map(([x, y], i) => ( ))} {/* Axis labels */} {RADAR_AXES.map((axis, i) => { const [x, y] = pointFor(i, maxR + 18); const anchor = x < cx - 5 ? 'end' : x > cx + 5 ? 'start' : 'middle'; return ( {axis.label} ); })} {/* Percentage labels on vertical axis */} {rings.map((r) => { const [, y] = pointFor(0, (r / 100) * maxR); return ( {r} ); })} ); } // ─── Scatter plot ───────────────────────────────────────────────────────────── function ScatterPlot({ points }: { points: ScatterPoint[] }) { const [tooltip, setTooltip] = useState<{ ip: string; type: string; x: number; y: number } | null>(null); const W = 600; const H = 200; const padL = 40; const padB = 30; const padT = 10; const padR = 20; const maxX = 350; const maxY = 1; const toSvgX = (v: number) => padL + ((v / maxX) * (W - padL - padR)); const toSvgY = (v: number) => padT + ((1 - v / maxY) * (H - padT - padB)); // X axis ticks const xTicks = [0, 50, 100, 150, 200, 250, 300, 350]; const yTicks = [0, 0.25, 0.5, 0.75, 1.0]; return (
setTooltip(null)} > {/* Grid lines */} {xTicks.map((v) => ( ))} {yTicks.map((v) => ( ))} {/* X axis */} {xTicks.map((v) => ( {v} ))} Fuzzing Index → {/* Y axis */} {yTicks.map((v) => ( {v.toFixed(2)} ))} {/* Data points */} {points.map((pt, i) => { const x = toSvgX(Math.min(pt.fuzzing_index, maxX)); const y = toSvgY(Math.min(pt.hit_velocity, maxY)); const color = attackTypeColor(pt.attack_type); return ( setTooltip({ ip: pt.ip, type: pt.attack_type, x, y })} /> ); })} {/* Tooltip */} {tooltip && ( {tooltip.ip} {attackTypeEmoji(tooltip.type)} {tooltip.type} )}
); } // ─── Main Component ─────────────────────────────────────────────────────────── export function MLFeaturesView() { const navigate = useNavigate(); const [anomalies, setAnomalies] = useState([]); const [anomaliesLoading, setAnomaliesLoading] = useState(true); const [anomaliesError, setAnomaliesError] = useState(null); const [scatter, setScatter] = useState([]); const [scatterLoading, setScatterLoading] = useState(true); const [scatterError, setScatterError] = useState(null); const [selectedIP, setSelectedIP] = useState(null); const [radarData, setRadarData] = useState(null); const [radarLoading, setRadarLoading] = useState(false); const [radarError, setRadarError] = useState(null); useEffect(() => { const fetchAnomalies = async () => { try { const res = await fetch('/api/ml/top-anomalies?limit=50'); if (!res.ok) throw new Error('Erreur chargement des anomalies'); const data: { items: MLAnomaly[] } = await res.json(); setAnomalies(data.items ?? []); } catch (err) { setAnomaliesError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setAnomaliesLoading(false); } }; const fetchScatter = async () => { try { const res = await fetch('/api/ml/scatter?limit=200'); if (!res.ok) throw new Error('Erreur chargement du scatter'); const data: { points: ScatterPoint[] } = await res.json(); setScatter(data.points ?? []); } catch (err) { setScatterError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setScatterLoading(false); } }; fetchAnomalies(); fetchScatter(); }, []); const loadRadar = async (ip: string) => { if (selectedIP === ip) { setSelectedIP(null); setRadarData(null); return; } setSelectedIP(ip); setRadarLoading(true); setRadarError(null); try { const res = await fetch(`/api/ml/ip/${encodeURIComponent(ip)}/radar`); if (!res.ok) throw new Error('Erreur chargement du radar'); const data: RadarData = await res.json(); setRadarData(data); } catch (err) { setRadarError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setRadarLoading(false); } }; return (
{/* Header */}

🤖 Analyse Features ML

Visualisation des features ML pour la détection d'anomalies comportementales.

{/* Main two-column layout */}
{/* Left: anomalies table */}

Top anomalies

{anomaliesLoading ? ( ) : anomaliesError ? (
) : (
{anomalies.map((item) => ( loadRadar(item.ip)} className={`border-b border-border cursor-pointer transition-colors ${ selectedIP === item.ip ? 'bg-accent-primary/10' : 'hover:bg-background-card' }`} > ))}
IP Host Hits Fuzzing Type Signaux
{item.ip} {item.host || '—'} {formatNumber(item.hits)} {Math.round(item.fuzzing_index)} {attackTypeEmoji(item.attack_type)} {item.ua_ch_mismatch && ⚠️} {item.is_fake_navigation && 🎭} {item.is_ua_rotating && 🔄} {item.sni_host_mismatch && 🌐}
)}
{/* Right: Radar chart */}

Radar ML {selectedIP ? — {selectedIP} : ''}

{!selectedIP ? (
Cliquez sur une IP
pour afficher le radar
) : radarLoading ? (
) : radarError ? ( ) : radarData ? ( <>
) : null}
{/* Scatter plot */}

Nuage de points — Fuzzing Index × Vélocité

{scatterLoading ? ( ) : scatterError ? ( ) : ( <> {/* Legend */}
{['brute_force', 'flood', 'scraper', 'spoofing', 'scanner'].map((type) => ( {attackTypeEmoji(type)} {type} ))}
)}
); }