fix: TCP spoofing — corrélation OS uniquement si données TCP valides

Problème: TTL=0 (proxy/CDN) ne permet pas de fingerprinter l'OS d'origine.
Les entrées sans données TCP étaient faussement flagguées comme spoofs.

Corrections backend (tcp_spoofing.py):
- Règle: spoof_flag=True UNIQUEMENT si TTL dans plage OS connue (52-65 Linux, 110-135 Windows)
  ET OS déclaré incompatible avec l'OS fingerprinté
- TTL=0 → 'Unknown' (pas de corrélation possible)
- TTL hors plage connue → 'Unknown' (pas de corrélation possible)
- /list: filtre WHERE tcp_ttl > 0 (exclut les entrées sans données TCP)
- /list: paramètre spoof_only=true → filtre SQL sur plages TTL corrélables uniquement
- /overview: nouvelles métriques (with_tcp_data, no_tcp_data, linux_fingerprint, windows_fingerprint)
- /matrix: ajout is_spoof par cellule

Corrections frontend (TcpSpoofingView.tsx):
- Stat cards: total entries, avec TCP, fingerprint Linux/Windows (plus TTL<60)
- Bandeau informatif: nombre d'entrées sans données TCP exclues
- Checkbox 'Spoofs uniquement' → re-fetch avec spoof_only=true (filtre SQL)
- Matrice OS: cellules de vrai spoof surlignées en rouge avec icône 🚨
- useEffect séparé pour overview et items (items se recharge si spoofOnly change)

Résultat: 36 699 entrées Linux/Mac (TTL 52-65), dont 16 226 spoofant Windows UA

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
SOC Analyst
2026-03-16 00:05:19 +01:00
parent e2bc4a47cd
commit 735d8b6101
2 changed files with 164 additions and 66 deletions

View File

@ -4,12 +4,14 @@ import { useNavigate } from 'react-router-dom';
// ─── Types ────────────────────────────────────────────────────────────────────
interface TcpSpoofingOverview {
total_detections: number;
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 }[];
low_ttl_count: number;
zero_ttl_count: number;
}
interface TcpSpoofingItem {
@ -27,6 +29,7 @@ interface OsMatrixEntry {
suspected_os: string;
declared_os: string;
count: number;
is_spoof: boolean;
}
type ActiveTab = 'detections' | 'matrix';
@ -79,6 +82,8 @@ export function TcpSpoofingView() {
const [activeTab, setActiveTab] = useState<ActiveTab>('detections');
const [spoofOnly, setSpoofOnly] = useState(false);
const [overview, setOverview] = useState<TcpSpoofingOverview | null>(null);
const [overviewLoading, setOverviewLoading] = useState(true);
const [overviewError, setOverviewError] = useState<string | null>(null);
@ -108,10 +113,17 @@ export function TcpSpoofingView() {
setOverviewLoading(false);
}
};
fetchOverview();
}, []);
useEffect(() => {
const fetchItems = async () => {
setItemsLoading(true);
setItemsError(null);
try {
const res = await fetch('/api/tcp-spoofing/list?limit=100');
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 ?? []);
@ -121,9 +133,8 @@ export function TcpSpoofingView() {
setItemsLoading(false);
}
};
fetchOverview();
fetchItems();
}, []);
}, [spoofOnly]);
const loadMatrix = async () => {
if (matrixLoaded) return;
@ -148,10 +159,11 @@ export function TcpSpoofingView() {
const filteredItems = items.filter(
(item) =>
!filterText ||
item.ip.includes(filterText) ||
item.suspected_os.toLowerCase().includes(filterText.toLowerCase()) ||
item.declared_os.toLowerCase().includes(filterText.toLowerCase())
(!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
@ -189,12 +201,20 @@ export function TcpSpoofingView() {
) : 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>
<>
<div className="grid grid-cols-4 gap-4">
<StatCard label="Total entrées" value={formatNumber(overview.total_entries)} accent="text-text-primary" />
<StatCard label="Avec données TCP" value={formatNumber(overview.with_tcp_data)} accent="text-threat-medium" />
<StatCard label="Fingerprint Linux" value={formatNumber(overview.linux_fingerprint)} accent="text-threat-low" />
<StatCard label="Fingerprint Windows" value={formatNumber(overview.windows_fingerprint)} accent="text-accent-primary" />
</div>
<div className="bg-background-card border border-border rounded-lg px-4 py-3 text-sm text-text-secondary flex items-center gap-2">
<span className="text-threat-medium">⚠️</span>
<span>
<strong className="text-text-primary">{formatNumber(overview.no_tcp_data)}</strong> entrées sans données TCP (TTL=0, passées par proxy/CDN) — exclues de l'analyse de corrélation.
</span>
</div>
</>
) : null}
{/* Tabs */}
@ -217,7 +237,7 @@ export function TcpSpoofingView() {
{/* Détections tab */}
{activeTab === 'detections' && (
<>
<div className="flex gap-3">
<div className="flex gap-3 items-center">
<input
type="text"
placeholder="Filtrer par IP ou OS..."
@ -225,6 +245,15 @@ export function TcpSpoofingView() {
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"
/>
<label className="flex items-center gap-2 text-sm text-text-secondary cursor-pointer select-none">
<input
type="checkbox"
checked={spoofOnly}
onChange={(e) => setSpoofOnly(e.target.checked)}
className="accent-accent-primary"
/>
Spoofs uniquement (TTL corrélé + OS mismatch)
</label>
</div>
<div className="bg-background-secondary rounded-lg border border-border overflow-hidden">
{itemsLoading ? (
@ -322,14 +351,24 @@ export function TcpSpoofingView() {
<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>
))}
{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 (
<td
key={ci}
className={`px-3 py-2 text-center border border-border font-mono ${
isSpoofCell && count > 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)) : '—'}
</td>
);
})}
<td className="px-3 py-2 text-center border border-border font-semibold text-text-primary bg-background-card">
{formatNumber(rowTotal)}
</td>