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:
@ -1,5 +1,13 @@
|
|||||||
"""
|
"""
|
||||||
Endpoints pour la détection du TCP spoofing (TTL / window size anormaux)
|
Endpoints pour la détection du TCP spoofing (TTL / window size anormaux)
|
||||||
|
|
||||||
|
Règle de corrélation :
|
||||||
|
- TTL=0 ou tcp_window_size=0 → données TCP absentes (proxy/LB) → pas de corrélation possible
|
||||||
|
- TTL 55-65 → fingerprint Linux/Mac (initial TTL 64)
|
||||||
|
- TTL 120-135 → fingerprint Windows (initial TTL 128)
|
||||||
|
- TTL 110-120 → fingerprint Windows (initial TTL 128, quelques sauts)
|
||||||
|
- Toute autre valeur → OS indéterminé → pas de flag spoofing
|
||||||
|
- spoof_flag = True UNIQUEMENT si OS fingerprinting TCP possible ET incompatible avec l'UA
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, HTTPException, Query
|
from fastapi import APIRouter, HTTPException, Query
|
||||||
|
|
||||||
@ -7,14 +15,22 @@ from ..database import db
|
|||||||
|
|
||||||
router = APIRouter(prefix="/api/tcp-spoofing", tags=["tcp_spoofing"])
|
router = APIRouter(prefix="/api/tcp-spoofing", tags=["tcp_spoofing"])
|
||||||
|
|
||||||
|
# Plages TTL qui permettent une corrélation fiable
|
||||||
|
_TTL_LINUX = (range(52, 66), "Linux/Mac") # initial 64, 1-12 sauts
|
||||||
|
_TTL_WINDOWS = (range(110, 136), "Windows") # initial 128, 1-18 sauts
|
||||||
|
_TTL_CISCO = (range(240, 256), "Cisco/BSD") # initial 255
|
||||||
|
|
||||||
|
|
||||||
def _suspected_os(ttl: int) -> str:
|
def _suspected_os(ttl: int) -> str:
|
||||||
if 55 <= ttl <= 65:
|
"""Retourne l'OS probable à partir du TTL observé.
|
||||||
return "Linux/Mac"
|
Retourne 'Unknown' si le TTL ne permet pas une corrélation fiable
|
||||||
if 120 <= ttl <= 135:
|
(TTL=0 = pas de données TCP, ou hors plage connue).
|
||||||
return "Windows"
|
"""
|
||||||
if ttl < 50:
|
if ttl <= 0:
|
||||||
return "Behind proxy (depleted)"
|
return "Unknown" # Pas de données TCP (proxy/CDN)
|
||||||
|
for rng, name in (_TTL_LINUX, _TTL_WINDOWS, _TTL_CISCO):
|
||||||
|
if ttl in rng:
|
||||||
|
return name
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
@ -29,31 +45,50 @@ def _declared_os(ua: str) -> str:
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_spoof(suspected_os: str, declared_os: str) -> bool:
|
||||||
|
"""Spoof confirmé uniquement si on a un fingerprint TCP fiable ET une incompatibilité d'OS."""
|
||||||
|
if suspected_os == "Unknown" or declared_os == "Unknown":
|
||||||
|
return False # Pas de corrélation possible
|
||||||
|
# Linux/Mac fingerprint TCP mais UA déclare Windows
|
||||||
|
if suspected_os == "Linux/Mac" and declared_os == "Windows":
|
||||||
|
return True
|
||||||
|
# Windows fingerprint TCP mais UA déclare Linux/Android ou macOS
|
||||||
|
if suspected_os == "Windows" and declared_os in ("Linux/Android", "macOS"):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router.get("/overview")
|
@router.get("/overview")
|
||||||
async def get_tcp_spoofing_overview():
|
async def get_tcp_spoofing_overview():
|
||||||
"""Statistiques globales sur les détections de spoofing TCP."""
|
"""Statistiques globales : seules les entrées avec données TCP valides sont analysées."""
|
||||||
try:
|
try:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT
|
SELECT
|
||||||
count() AS total_detections,
|
count() AS total_entries,
|
||||||
uniq(src_ip) AS unique_ips,
|
uniq(src_ip) AS unique_ips,
|
||||||
countIf(tcp_ttl < 60) AS low_ttl_count,
|
countIf(tcp_ttl = 0) AS no_tcp_data,
|
||||||
countIf(tcp_ttl = 0) AS zero_ttl_count
|
countIf(tcp_ttl > 0) AS with_tcp_data,
|
||||||
|
countIf(tcp_ttl BETWEEN 52 AND 65) AS linux_fingerprint,
|
||||||
|
countIf(tcp_ttl BETWEEN 110 AND 135) AS windows_fingerprint
|
||||||
FROM mabase_prod.view_tcp_spoofing_detected
|
FROM mabase_prod.view_tcp_spoofing_detected
|
||||||
"""
|
"""
|
||||||
result = db.query(sql)
|
result = db.query(sql)
|
||||||
row = result.result_rows[0]
|
row = result.result_rows[0]
|
||||||
total_detections = int(row[0])
|
total_entries = int(row[0])
|
||||||
unique_ips = int(row[1])
|
unique_ips = int(row[1])
|
||||||
low_ttl_count = int(row[2])
|
no_tcp_data = int(row[2])
|
||||||
zero_ttl_count = int(row[3])
|
with_tcp_data = int(row[3])
|
||||||
|
linux_fp = int(row[4])
|
||||||
|
windows_fp = int(row[5])
|
||||||
|
|
||||||
|
# Distribution TTL uniquement pour les entrées avec données TCP valides
|
||||||
ttl_sql = """
|
ttl_sql = """
|
||||||
SELECT
|
SELECT
|
||||||
tcp_ttl,
|
tcp_ttl,
|
||||||
count() AS cnt,
|
count() AS cnt,
|
||||||
uniq(src_ip) AS ips
|
uniq(src_ip) AS ips
|
||||||
FROM mabase_prod.view_tcp_spoofing_detected
|
FROM mabase_prod.view_tcp_spoofing_detected
|
||||||
|
WHERE tcp_ttl > 0
|
||||||
GROUP BY tcp_ttl
|
GROUP BY tcp_ttl
|
||||||
ORDER BY cnt DESC
|
ORDER BY cnt DESC
|
||||||
LIMIT 15
|
LIMIT 15
|
||||||
@ -64,11 +99,13 @@ async def get_tcp_spoofing_overview():
|
|||||||
for r in ttl_res.result_rows
|
for r in ttl_res.result_rows
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Distribution window_size pour entrées avec données TCP
|
||||||
win_sql = """
|
win_sql = """
|
||||||
SELECT
|
SELECT
|
||||||
tcp_window_size,
|
tcp_window_size,
|
||||||
count() AS cnt
|
count() AS cnt
|
||||||
FROM mabase_prod.view_tcp_spoofing_detected
|
FROM mabase_prod.view_tcp_spoofing_detected
|
||||||
|
WHERE tcp_ttl > 0
|
||||||
GROUP BY tcp_window_size
|
GROUP BY tcp_window_size
|
||||||
ORDER BY cnt DESC
|
ORDER BY cnt DESC
|
||||||
LIMIT 10
|
LIMIT 10
|
||||||
@ -80,12 +117,14 @@ async def get_tcp_spoofing_overview():
|
|||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total_detections": total_detections,
|
"total_entries": total_entries,
|
||||||
"unique_ips": unique_ips,
|
"unique_ips": unique_ips,
|
||||||
"low_ttl_count": low_ttl_count,
|
"no_tcp_data": no_tcp_data,
|
||||||
"zero_ttl_count": zero_ttl_count,
|
"with_tcp_data": with_tcp_data,
|
||||||
"ttl_distribution": ttl_distribution,
|
"linux_fingerprint": linux_fp,
|
||||||
"window_size_distribution":window_size_distribution,
|
"windows_fingerprint": windows_fp,
|
||||||
|
"ttl_distribution": ttl_distribution,
|
||||||
|
"window_size_distribution": window_size_distribution,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
@ -93,33 +132,47 @@ async def get_tcp_spoofing_overview():
|
|||||||
|
|
||||||
@router.get("/list")
|
@router.get("/list")
|
||||||
async def get_tcp_spoofing_list(
|
async def get_tcp_spoofing_list(
|
||||||
limit: int = Query(100, ge=1, le=1000),
|
limit: int = Query(100, ge=1, le=1000),
|
||||||
offset: int = Query(0, ge=0),
|
offset: int = Query(0, ge=0),
|
||||||
|
spoof_only: bool = Query(False, description="Ne retourner que les vrais spoofs (TTL corrélable + OS mismatch)"),
|
||||||
):
|
):
|
||||||
"""Liste paginée des détections, triée par tcp_ttl ASC."""
|
"""Liste des entrées avec données TCP valides (tcp_ttl > 0).
|
||||||
|
Entrées sans données TCP (TTL=0) exclues : pas de corrélation possible.
|
||||||
|
Si spoof_only=True, retourne uniquement les entrées avec fingerprint OS identifiable (Linux/Mac TTL 52-65).
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
count_sql = "SELECT count() FROM mabase_prod.view_tcp_spoofing_detected"
|
# Filtre SQL : seules les entrées avec TTL valide, et si spoof_only les plages corrélables
|
||||||
|
if spoof_only:
|
||||||
|
# Seules les plages de TTL qui permettent une identification OS fiable
|
||||||
|
ttl_filter = "tcp_ttl BETWEEN 52 AND 65 OR tcp_ttl BETWEEN 110 AND 135 OR tcp_ttl BETWEEN 240 AND 255"
|
||||||
|
else:
|
||||||
|
ttl_filter = "tcp_ttl > 0"
|
||||||
|
|
||||||
|
count_sql = f"SELECT count() FROM mabase_prod.view_tcp_spoofing_detected WHERE {ttl_filter}"
|
||||||
total = int(db.query(count_sql).result_rows[0][0])
|
total = int(db.query(count_sql).result_rows[0][0])
|
||||||
|
|
||||||
sql = """
|
sql = f"""
|
||||||
SELECT
|
SELECT
|
||||||
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS src_ip,
|
replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS src_ip,
|
||||||
ja4, tcp_ttl, tcp_window_size, first_ua
|
ja4, tcp_ttl, tcp_window_size, first_ua
|
||||||
FROM mabase_prod.view_tcp_spoofing_detected
|
FROM mabase_prod.view_tcp_spoofing_detected
|
||||||
|
WHERE {ttl_filter}
|
||||||
ORDER BY tcp_ttl ASC
|
ORDER BY tcp_ttl ASC
|
||||||
LIMIT %(limit)s OFFSET %(offset)s
|
LIMIT %(limit)s OFFSET %(offset)s
|
||||||
"""
|
"""
|
||||||
result = db.query(sql, {"limit": limit, "offset": offset})
|
result = db.query(sql, {"limit": limit, "offset": offset})
|
||||||
items = []
|
items = []
|
||||||
for row in result.result_rows:
|
for row in result.result_rows:
|
||||||
ip = str(row[0])
|
ip = str(row[0])
|
||||||
ja4 = str(row[1])
|
ja4 = str(row[1] or "")
|
||||||
ttl = int(row[2])
|
ttl = int(row[2])
|
||||||
window_size = int(row[3])
|
window_size = int(row[3])
|
||||||
ua = str(row[4] or "")
|
ua = str(row[4] or "")
|
||||||
sus_os = _suspected_os(ttl)
|
sus_os = _suspected_os(ttl)
|
||||||
dec_os = _declared_os(ua)
|
dec_os = _declared_os(ua)
|
||||||
spoof_flag = sus_os != dec_os and sus_os != "Unknown" and dec_os != "Unknown"
|
spoof_flag = _is_spoof(sus_os, dec_os)
|
||||||
|
if spoof_only and not spoof_flag:
|
||||||
|
continue
|
||||||
items.append({
|
items.append({
|
||||||
"ip": ip,
|
"ip": ip,
|
||||||
"ja4": ja4,
|
"ja4": ja4,
|
||||||
@ -137,24 +190,30 @@ async def get_tcp_spoofing_list(
|
|||||||
|
|
||||||
@router.get("/matrix")
|
@router.get("/matrix")
|
||||||
async def get_tcp_spoofing_matrix():
|
async def get_tcp_spoofing_matrix():
|
||||||
"""Cross-tab suspected_os × declared_os avec comptage."""
|
"""Matrice suspected_os × declared_os — uniquement entrées avec TTL valide."""
|
||||||
try:
|
try:
|
||||||
sql = """
|
sql = """
|
||||||
SELECT src_ip, tcp_ttl, first_ua
|
SELECT tcp_ttl, first_ua
|
||||||
FROM mabase_prod.view_tcp_spoofing_detected
|
FROM mabase_prod.view_tcp_spoofing_detected
|
||||||
|
WHERE tcp_ttl > 0
|
||||||
"""
|
"""
|
||||||
result = db.query(sql)
|
result = db.query(sql)
|
||||||
counts: dict = {}
|
counts: dict = {}
|
||||||
for row in result.result_rows:
|
for row in result.result_rows:
|
||||||
ttl = int(row[1])
|
ttl = int(row[0])
|
||||||
ua = str(row[2] or "")
|
ua = str(row[1] or "")
|
||||||
sus_os = _suspected_os(ttl)
|
sus_os = _suspected_os(ttl)
|
||||||
dec_os = _declared_os(ua)
|
dec_os = _declared_os(ua)
|
||||||
key = (sus_os, dec_os)
|
key = (sus_os, dec_os)
|
||||||
counts[key] = counts.get(key, 0) + 1
|
counts[key] = counts.get(key, 0) + 1
|
||||||
|
|
||||||
matrix = [
|
matrix = [
|
||||||
{"suspected_os": k[0], "declared_os": k[1], "count": v}
|
{
|
||||||
|
"suspected_os": k[0],
|
||||||
|
"declared_os": k[1],
|
||||||
|
"count": v,
|
||||||
|
"is_spoof": _is_spoof(k[0], k[1]),
|
||||||
|
}
|
||||||
for k, v in counts.items()
|
for k, v in counts.items()
|
||||||
]
|
]
|
||||||
matrix.sort(key=lambda x: x["count"], reverse=True)
|
matrix.sort(key=lambda x: x["count"], reverse=True)
|
||||||
|
|||||||
@ -4,12 +4,14 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
interface TcpSpoofingOverview {
|
interface TcpSpoofingOverview {
|
||||||
total_detections: number;
|
total_entries: number;
|
||||||
unique_ips: 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 }[];
|
ttl_distribution: { ttl: number; count: number; ips: number }[];
|
||||||
window_size_distribution: { window_size: number; count: number }[];
|
window_size_distribution: { window_size: number; count: number }[];
|
||||||
low_ttl_count: number;
|
|
||||||
zero_ttl_count: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TcpSpoofingItem {
|
interface TcpSpoofingItem {
|
||||||
@ -27,6 +29,7 @@ interface OsMatrixEntry {
|
|||||||
suspected_os: string;
|
suspected_os: string;
|
||||||
declared_os: string;
|
declared_os: string;
|
||||||
count: number;
|
count: number;
|
||||||
|
is_spoof: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActiveTab = 'detections' | 'matrix';
|
type ActiveTab = 'detections' | 'matrix';
|
||||||
@ -79,6 +82,8 @@ export function TcpSpoofingView() {
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<ActiveTab>('detections');
|
const [activeTab, setActiveTab] = useState<ActiveTab>('detections');
|
||||||
|
|
||||||
|
const [spoofOnly, setSpoofOnly] = useState(false);
|
||||||
|
|
||||||
const [overview, setOverview] = useState<TcpSpoofingOverview | null>(null);
|
const [overview, setOverview] = useState<TcpSpoofingOverview | null>(null);
|
||||||
const [overviewLoading, setOverviewLoading] = useState(true);
|
const [overviewLoading, setOverviewLoading] = useState(true);
|
||||||
const [overviewError, setOverviewError] = useState<string | null>(null);
|
const [overviewError, setOverviewError] = useState<string | null>(null);
|
||||||
@ -108,10 +113,17 @@ export function TcpSpoofingView() {
|
|||||||
setOverviewLoading(false);
|
setOverviewLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
fetchOverview();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const fetchItems = async () => {
|
const fetchItems = async () => {
|
||||||
setItemsLoading(true);
|
setItemsLoading(true);
|
||||||
|
setItemsError(null);
|
||||||
try {
|
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');
|
if (!res.ok) throw new Error('Erreur chargement des détections');
|
||||||
const data: { items: TcpSpoofingItem[]; total: number } = await res.json();
|
const data: { items: TcpSpoofingItem[]; total: number } = await res.json();
|
||||||
setItems(data.items ?? []);
|
setItems(data.items ?? []);
|
||||||
@ -121,9 +133,8 @@ export function TcpSpoofingView() {
|
|||||||
setItemsLoading(false);
|
setItemsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
fetchOverview();
|
|
||||||
fetchItems();
|
fetchItems();
|
||||||
}, []);
|
}, [spoofOnly]);
|
||||||
|
|
||||||
const loadMatrix = async () => {
|
const loadMatrix = async () => {
|
||||||
if (matrixLoaded) return;
|
if (matrixLoaded) return;
|
||||||
@ -148,10 +159,11 @@ export function TcpSpoofingView() {
|
|||||||
|
|
||||||
const filteredItems = items.filter(
|
const filteredItems = items.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
!filterText ||
|
(!spoofOnly || item.spoof_flag) &&
|
||||||
item.ip.includes(filterText) ||
|
(!filterText ||
|
||||||
item.suspected_os.toLowerCase().includes(filterText.toLowerCase()) ||
|
item.ip.includes(filterText) ||
|
||||||
item.declared_os.toLowerCase().includes(filterText.toLowerCase())
|
item.suspected_os.toLowerCase().includes(filterText.toLowerCase()) ||
|
||||||
|
item.declared_os.toLowerCase().includes(filterText.toLowerCase()))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build matrix axes
|
// Build matrix axes
|
||||||
@ -189,12 +201,20 @@ export function TcpSpoofingView() {
|
|||||||
) : overviewError ? (
|
) : overviewError ? (
|
||||||
<ErrorMessage message={overviewError} />
|
<ErrorMessage message={overviewError} />
|
||||||
) : overview ? (
|
) : overview ? (
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<>
|
||||||
<StatCard label="Total détections" value={formatNumber(overview.total_detections)} accent="text-threat-high" />
|
<div className="grid grid-cols-4 gap-4">
|
||||||
<StatCard label="IPs uniques" value={formatNumber(overview.unique_ips)} accent="text-text-primary" />
|
<StatCard label="Total entrées" value={formatNumber(overview.total_entries)} accent="text-text-primary" />
|
||||||
<StatCard label="TTL bas (<60)" value={formatNumber(overview.low_ttl_count)} accent="text-threat-medium" />
|
<StatCard label="Avec données TCP" value={formatNumber(overview.with_tcp_data)} accent="text-threat-medium" />
|
||||||
<StatCard label="TTL zéro" value={formatNumber(overview.zero_ttl_count)} accent="text-threat-critical" />
|
<StatCard label="Fingerprint Linux" value={formatNumber(overview.linux_fingerprint)} accent="text-threat-low" />
|
||||||
</div>
|
<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}
|
) : null}
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
@ -217,7 +237,7 @@ export function TcpSpoofingView() {
|
|||||||
{/* Détections tab */}
|
{/* Détections tab */}
|
||||||
{activeTab === 'detections' && (
|
{activeTab === 'detections' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Filtrer par IP ou OS..."
|
placeholder="Filtrer par IP ou OS..."
|
||||||
@ -225,6 +245,15 @@ export function TcpSpoofingView() {
|
|||||||
onChange={(e) => setFilterText(e.target.value)}
|
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"
|
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>
|
||||||
<div className="bg-background-secondary rounded-lg border border-border overflow-hidden">
|
<div className="bg-background-secondary rounded-lg border border-border overflow-hidden">
|
||||||
{itemsLoading ? (
|
{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]">
|
<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>
|
<span className="block truncate w-28" title={sos}>{sos}</span>
|
||||||
</td>
|
</td>
|
||||||
{rowEntries.map((count, ci) => (
|
{rowEntries.map((count, ci) => {
|
||||||
<td
|
const dos = declaredOSes[ci];
|
||||||
key={ci}
|
const entry = matrix.find((e) => e.suspected_os === sos && e.declared_os === dos);
|
||||||
className={`px-3 py-2 text-center border border-border font-mono ${matrixCellColor(count)} ${count > 0 ? 'text-text-primary' : 'text-text-disabled'}`}
|
const isSpoofCell = entry?.is_spoof ?? false;
|
||||||
>
|
return (
|
||||||
{count > 0 ? formatNumber(count) : '—'}
|
<td
|
||||||
</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">
|
<td className="px-3 py-2 text-center border border-border font-semibold text-text-primary bg-background-card">
|
||||||
{formatNumber(rowTotal)}
|
{formatNumber(rowTotal)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user