import { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import DataTable, { Column } from './ui/DataTable'; // ─── Types ──────────────────────────────────────────────────────────────────── interface TcpSpoofingOverview { total_entries: number; unique_ips: number; no_tcp_data: number; with_tcp_data: number; linux_fingerprint: number; windows_fingerprint: number; ttl_distribution: { ttl: number; count: number; ips: number }[]; window_size_distribution: { window_size: number; count: number }[]; } interface TcpSpoofingItem { ip: string; ja4: string; tcp_ttl: number; tcp_window_size: number; first_ua: string; suspected_os: string; declared_os: string; spoof_flag: boolean; } interface OsMatrixEntry { suspected_os: string; declared_os: string; count: number; is_spoof: boolean; } type ActiveTab = 'detections' | 'matrix'; // ─── Helpers ────────────────────────────────────────────────────────────────── function formatNumber(n: number): string { return n.toLocaleString('fr-FR'); } function ttlColor(ttl: number): string { if (ttl === 0) return 'text-threat-critical'; if (ttl < 48 || ttl > 200) return 'text-threat-critical'; if (ttl < 60 || (ttl > 70 && ttl <= 80)) return 'text-threat-medium'; if (ttl >= 60 && ttl <= 70) return 'text-threat-low'; return 'text-text-secondary'; } // ─── Sub-components ─────────────────────────────────────────────────────────── function StatCard({ label, value, accent }: { label: string; value: string | number; accent?: string }) { return (
{label} {value}
); } function LoadingSpinner() { return (
); } function ErrorMessage({ message }: { message: string }) { return (
⚠️ {message}
); } // ─── Detections DataTable ───────────────────────────────────────────────────── function TcpDetectionsTable({ items, navigate, }: { items: TcpSpoofingItem[]; navigate: (path: string) => void; }) { const columns = useMemo((): Column[] => [ { key: 'ip', label: 'IP', render: (v: string) => {v}, }, { key: 'ja4', label: 'JA4', render: (v: string) => ( {v ? `${v.slice(0, 14)}…` : '—'} ), }, { key: 'tcp_ttl', label: 'TTL observé', align: 'right', render: (v: number) => ( {v} ), }, { key: 'tcp_window_size', label: 'Fenêtre TCP', align: 'right', render: (v: number) => ( {formatNumber(v)} ), }, { key: 'suspected_os', label: 'OS suspecté', render: (v: string) => {v || '—'}, }, { key: 'declared_os', label: 'OS déclaré', render: (v: string) => {v || '—'}, }, { key: 'spoof_flag', label: 'Spoof', sortable: false, render: (v: boolean) => v ? ( 🚨 Spoof ) : null, }, { key: '_actions', label: '', sortable: false, render: (_: unknown, row: TcpSpoofingItem) => ( ), }, ], [navigate]); return ( ); } // ─── Main Component ─────────────────────────────────────────────────────────── export function TcpSpoofingView() { const navigate = useNavigate(); const [activeTab, setActiveTab] = useState('detections'); const [spoofOnly, setSpoofOnly] = useState(false); const [overview, setOverview] = useState(null); const [overviewLoading, setOverviewLoading] = useState(true); const [overviewError, setOverviewError] = useState(null); const [items, setItems] = useState([]); const [itemsLoading, setItemsLoading] = useState(true); const [itemsError, setItemsError] = useState(null); const [matrix, setMatrix] = useState([]); const [matrixLoading, setMatrixLoading] = useState(false); const [matrixError, setMatrixError] = useState(null); const [matrixLoaded, setMatrixLoaded] = useState(false); const [filterText, setFilterText] = useState(''); useEffect(() => { const fetchOverview = async () => { setOverviewLoading(true); try { const res = await fetch('/api/tcp-spoofing/overview'); if (!res.ok) throw new Error('Erreur chargement overview'); const data: TcpSpoofingOverview = await res.json(); setOverview(data); } catch (err) { setOverviewError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setOverviewLoading(false); } }; fetchOverview(); }, []); useEffect(() => { const fetchItems = async () => { setItemsLoading(true); setItemsError(null); try { const params = new URLSearchParams({ limit: '200' }); if (spoofOnly) params.set('spoof_only', 'true'); const res = await fetch(`/api/tcp-spoofing/list?${params}`); if (!res.ok) throw new Error('Erreur chargement des détections'); const data: { items: TcpSpoofingItem[]; total: number } = await res.json(); setItems(data.items ?? []); } catch (err) { setItemsError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setItemsLoading(false); } }; fetchItems(); }, [spoofOnly]); const loadMatrix = async () => { if (matrixLoaded) return; setMatrixLoading(true); try { const res = await fetch('/api/tcp-spoofing/matrix'); if (!res.ok) throw new Error('Erreur chargement matrice OS'); const data: { matrix: OsMatrixEntry[] } = await res.json(); setMatrix(data.matrix ?? []); setMatrixLoaded(true); } catch (err) { setMatrixError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { setMatrixLoading(false); } }; const handleTabChange = (tab: ActiveTab) => { setActiveTab(tab); if (tab === 'matrix') loadMatrix(); }; const filteredItems = items.filter( (item) => (!spoofOnly || item.spoof_flag) && (!filterText || item.ip.includes(filterText) || item.suspected_os.toLowerCase().includes(filterText.toLowerCase()) || item.declared_os.toLowerCase().includes(filterText.toLowerCase())) ); // Build matrix axes const suspectedOSes = [...new Set(matrix.map((e) => e.suspected_os))].sort(); const declaredOSes = [...new Set(matrix.map((e) => e.declared_os))].sort(); const matrixMax = matrix.reduce((m, e) => Math.max(m, e.count), 1); function matrixCellColor(count: number): string { if (count === 0) return 'bg-background-card'; const ratio = count / matrixMax; if (ratio >= 0.75) return 'bg-threat-critical/80'; if (ratio >= 0.5) return 'bg-threat-high/70'; if (ratio >= 0.25) return 'bg-threat-medium/60'; return 'bg-threat-low/40'; } const tabs: { id: ActiveTab; label: string }[] = [ { id: 'detections', label: '📋 Détections' }, { id: 'matrix', label: '📊 Matrice OS' }, ]; return (
{/* Header */}

🧬 Spoofing TCP/OS

Détection des incohérences entre TTL/fenêtre TCP et l'OS déclaré.

{/* Stat cards */} {overviewLoading ? ( ) : overviewError ? ( ) : overview ? ( <>
⚠️ {formatNumber(overview.no_tcp_data)} entrées sans données TCP (TTL=0, passées par proxy/CDN) — exclues de l'analyse de corrélation.
) : null} {/* Tabs */}
{tabs.map((tab) => ( ))}
{/* Détections tab */} {activeTab === 'detections' && ( <>
setFilterText(e.target.value)} className="bg-background-card border border-border rounded-lg px-3 py-2 text-sm text-text-primary placeholder-text-disabled focus:outline-none focus:border-accent-primary w-72" />
{itemsLoading ? ( ) : itemsError ? (
) : ( )}
)} {/* Matrice OS tab */} {activeTab === 'matrix' && (
{matrixLoading ? ( ) : matrixError ? ( ) : matrix.length === 0 ? (

Aucune donnée disponible.

) : (

OS Suspecté × OS Déclaré

{declaredOSes.map((os) => ( ))} {suspectedOSes.map((sos) => { const rowEntries = declaredOSes.map((dos) => { const entry = matrix.find((e) => e.suspected_os === sos && e.declared_os === dos); return entry?.count ?? 0; }); const rowTotal = rowEntries.reduce((s, v) => s + v, 0); return ( {rowEntries.map((count, ci) => { const dos = declaredOSes[ci]; const entry = matrix.find((e) => e.suspected_os === sos && e.declared_os === dos); const isSpoofCell = entry?.is_spoof ?? false; return ( ); })} ); })} {declaredOSes.map((dos) => { const colTotal = matrix .filter((e) => e.declared_os === dos) .reduce((s, e) => s + e.count, 0); return ( ); })}
Suspecté \ Déclaré {os} Total
{sos} 0 ? 'bg-threat-critical/25 text-threat-critical font-bold' : matrixCellColor(count) + (count > 0 ? ' text-text-primary' : ' text-text-disabled') }`} title={isSpoofCell ? '🚨 OS mismatch confirmé' : undefined} > {count > 0 ? (isSpoofCell ? `🚨 ${formatNumber(count)}` : formatNumber(count)) : '—'} {formatNumber(rowTotal)}
Total {formatNumber(colTotal)} {formatNumber(matrix.reduce((s, e) => s + e.count, 0))}
)}
)}
); }