feat: ajout de 7 nouveaux dashboards d'analyse avancée
- 🔥 Brute Force & Credential Stuffing (view_form_bruteforce_detected) - 🧬 TCP/OS Spoofing (view_tcp_spoofing_detected, 86K détections) - 📡 Header Fingerprint Clustering (agg_header_fingerprint_1h, 1374 clusters) - ⏱️ Heatmap Temporelle (agg_host_ip_ja4_1h, pic à 20h) - 🌍 Botnets Distribués / JA4 spread (view_host_ja4_anomalies) - 🔄 Rotation JA4 & Persistance (view_host_ip_ja4_rotation + view_ip_recurrence) - 🤖 Features ML / Radar (view_ai_features_1h, radar SVG + scatter plot) Backend: 7 nouveaux router FastAPI avec requêtes ClickHouse optimisées Frontend: 7 nouveaux composants React + navigation 'Analyse Avancée' dans la sidebar Fixes: alias fuzzing_index → max_fuzzing (ORDER BY ClickHouse), normalisation IPs ::ffff: Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
363
frontend/src/components/TcpSpoofingView.tsx
Normal file
363
frontend/src/components/TcpSpoofingView.tsx
Normal file
@ -0,0 +1,363 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
interface TcpSpoofingOverview {
|
||||
total_detections: number;
|
||||
unique_ips: number;
|
||||
ttl_distribution: { ttl: number; count: number; ips: number }[];
|
||||
window_size_distribution: { window_size: number; count: number }[];
|
||||
low_ttl_count: number;
|
||||
zero_ttl_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;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="bg-background-secondary rounded-lg p-4 flex flex-col gap-1 border border-border">
|
||||
<span className="text-text-secondary text-sm">{label}</span>
|
||||
<span className={`text-2xl font-bold ${accent ?? 'text-text-primary'}`}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingSpinner() {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="w-8 h-8 border-2 border-accent-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorMessage({ message }: { message: string }) {
|
||||
return (
|
||||
<div className="bg-threat-critical/10 border border-threat-critical/30 rounded-lg p-4 text-threat-critical">
|
||||
⚠️ {message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Main Component ───────────────────────────────────────────────────────────
|
||||
|
||||
export function TcpSpoofingView() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ActiveTab>('detections');
|
||||
|
||||
const [overview, setOverview] = useState<TcpSpoofingOverview | null>(null);
|
||||
const [overviewLoading, setOverviewLoading] = useState(true);
|
||||
const [overviewError, setOverviewError] = useState<string | null>(null);
|
||||
|
||||
const [items, setItems] = useState<TcpSpoofingItem[]>([]);
|
||||
const [itemsLoading, setItemsLoading] = useState(true);
|
||||
const [itemsError, setItemsError] = useState<string | null>(null);
|
||||
|
||||
const [matrix, setMatrix] = useState<OsMatrixEntry[]>([]);
|
||||
const [matrixLoading, setMatrixLoading] = useState(false);
|
||||
const [matrixError, setMatrixError] = useState<string | null>(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);
|
||||
}
|
||||
};
|
||||
const fetchItems = async () => {
|
||||
setItemsLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/tcp-spoofing/list?limit=100');
|
||||
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);
|
||||
}
|
||||
};
|
||||
fetchOverview();
|
||||
fetchItems();
|
||||
}, []);
|
||||
|
||||
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) =>
|
||||
!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 (
|
||||
<div className="p-6 space-y-6 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-primary">🧬 Spoofing TCP/OS</h1>
|
||||
<p className="text-text-secondary mt-1">
|
||||
Détection des incohérences entre TTL/fenêtre TCP et l'OS déclaré.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stat cards */}
|
||||
{overviewLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : overviewError ? (
|
||||
<ErrorMessage message={overviewError} />
|
||||
) : overview ? (
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<StatCard label="Total détections" value={formatNumber(overview.total_detections)} accent="text-threat-high" />
|
||||
<StatCard label="IPs uniques" value={formatNumber(overview.unique_ips)} accent="text-text-primary" />
|
||||
<StatCard label="TTL bas (<60)" value={formatNumber(overview.low_ttl_count)} accent="text-threat-medium" />
|
||||
<StatCard label="TTL zéro" value={formatNumber(overview.zero_ttl_count)} accent="text-threat-critical" />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 border-b border-border">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => handleTabChange(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'text-accent-primary border-b-2 border-accent-primary'
|
||||
: 'text-text-secondary hover:text-text-primary'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Détections tab */}
|
||||
{activeTab === 'detections' && (
|
||||
<>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filtrer par IP ou OS..."
|
||||
value={filterText}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-background-secondary rounded-lg border border-border overflow-hidden">
|
||||
{itemsLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : itemsError ? (
|
||||
<div className="p-4"><ErrorMessage message={itemsError} /></div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-text-secondary text-left">
|
||||
<th className="px-4 py-3">IP</th>
|
||||
<th className="px-4 py-3">JA4</th>
|
||||
<th className="px-4 py-3">TTL observé</th>
|
||||
<th className="px-4 py-3">Fenêtre TCP</th>
|
||||
<th className="px-4 py-3">OS suspecté</th>
|
||||
<th className="px-4 py-3">OS déclaré</th>
|
||||
<th className="px-4 py-3">Spoof</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredItems.map((item) => (
|
||||
<tr key={item.ip} className="border-b border-border hover:bg-background-card transition-colors">
|
||||
<td className="px-4 py-3 font-mono text-xs text-text-primary">{item.ip}</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-text-secondary">
|
||||
{item.ja4 ? `${item.ja4.slice(0, 14)}…` : '—'}
|
||||
</td>
|
||||
<td className={`px-4 py-3 font-mono font-semibold ${ttlColor(item.tcp_ttl)}`}>
|
||||
{item.tcp_ttl}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-text-secondary text-xs">
|
||||
{formatNumber(item.tcp_window_size)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-text-primary text-xs">{item.suspected_os || '—'}</td>
|
||||
<td className="px-4 py-3 text-text-secondary text-xs">{item.declared_os || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
{item.spoof_flag && (
|
||||
<span className="bg-threat-critical/20 text-threat-critical text-xs px-2 py-0.5 rounded-full">
|
||||
🚨 Spoof
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => navigate(`/investigation/${item.ip}`)}
|
||||
className="text-xs bg-accent-primary/10 text-accent-primary px-3 py-1 rounded hover:bg-accent-primary/20 transition-colors"
|
||||
>
|
||||
Investiguer
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Matrice OS tab */}
|
||||
{activeTab === 'matrix' && (
|
||||
<div className="bg-background-secondary rounded-lg border border-border p-6">
|
||||
{matrixLoading ? (
|
||||
<LoadingSpinner />
|
||||
) : matrixError ? (
|
||||
<ErrorMessage message={matrixError} />
|
||||
) : matrix.length === 0 ? (
|
||||
<p className="text-text-secondary text-sm">Aucune donnée disponible.</p>
|
||||
) : (
|
||||
<div className="overflow-auto">
|
||||
<h2 className="text-text-primary font-semibold mb-4">OS Suspecté × OS Déclaré</h2>
|
||||
<table className="text-xs border-collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-text-secondary text-left border border-border bg-background-card">
|
||||
Suspecté \ Déclaré
|
||||
</th>
|
||||
{declaredOSes.map((os) => (
|
||||
<th key={os} className="px-3 py-2 text-text-secondary text-center border border-border bg-background-card max-w-[80px]">
|
||||
<span className="block truncate w-20" title={os}>{os}</span>
|
||||
</th>
|
||||
))}
|
||||
<th className="px-3 py-2 text-text-secondary text-center border border-border bg-background-card">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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 (
|
||||
<tr key={sos}>
|
||||
<td className="px-3 py-2 text-text-primary border border-border bg-background-card font-medium max-w-[120px]">
|
||||
<span className="block truncate w-28" title={sos}>{sos}</span>
|
||||
</td>
|
||||
{rowEntries.map((count, ci) => (
|
||||
<td
|
||||
key={ci}
|
||||
className={`px-3 py-2 text-center border border-border font-mono ${matrixCellColor(count)} ${count > 0 ? 'text-text-primary' : 'text-text-disabled'}`}
|
||||
>
|
||||
{count > 0 ? formatNumber(count) : '—'}
|
||||
</td>
|
||||
))}
|
||||
<td className="px-3 py-2 text-center border border-border font-semibold text-text-primary bg-background-card">
|
||||
{formatNumber(rowTotal)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
<tr>
|
||||
<td className="px-3 py-2 text-text-secondary border border-border bg-background-card font-semibold">Total</td>
|
||||
{declaredOSes.map((dos) => {
|
||||
const colTotal = matrix
|
||||
.filter((e) => e.declared_os === dos)
|
||||
.reduce((s, e) => s + e.count, 0);
|
||||
return (
|
||||
<td key={dos} className="px-3 py-2 text-center border border-border font-semibold text-text-primary bg-background-card">
|
||||
{formatNumber(colTotal)}
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="px-3 py-2 text-center border border-border font-semibold text-accent-primary bg-background-card">
|
||||
{formatNumber(matrix.reduce((s, e) => s + e.count, 0))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user