From df0fe1387bc7d11aec54c016bd536c444abcb389 Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Sat, 14 Mar 2026 22:53:00 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20IPs=20IPv4=20avec=20notation=20::ffff:?= =?UTF-8?q?=20corrig=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 PROBLÈME: • ClickHouse stocke les IPv4 en IPv6 (::ffff:x.x.x.x) • Les requêtes SQL utilisaient toString() → '::ffff:1.2.3.4' • Impossible de naviguer vers /entities/ip/::ffff:1.2.3.4 ✅ SOLUTION: • Utilisation de IPv4NumToString(toIPv4(src_ip)) • Convertit ::ffff:x.x.x.x → x.x.x.x • Filtre isIPv4MappedIPv6() pour les IPv4 uniquement BACKEND: • Requête SQL mise à jour avec IPv4NumToString() • sample_ip retourne maintenant 'x.x.x.x' (propre) • subnet retourne 'x.x.x.0/24' (propre) FRONTEND: • Suppression cleanIP() et getSampleIP() (inutiles) • Utilisation directe: cluster.sample_ip || cluster.subnet?.split('/')[0] • Tous les boutons utilisent la même logique RÉSULTAT: • Avant: /entities/ip/::ffff:176.65.132.0 ❌ • Après: /entities/ip/176.65.132.1 ✅ ✅ Build: SUCCESS ✅ Container: restarted Co-authored-by: Qwen-Coder --- backend/routes/incidents.py | 21 ++++++---------- frontend/src/components/IncidentsView.tsx | 30 +++++------------------ 2 files changed, 14 insertions(+), 37 deletions(-) diff --git a/backend/routes/incidents.py b/backend/routes/incidents.py index d9b6dc8..a2203aa 100644 --- a/backend/routes/incidents.py +++ b/backend/routes/incidents.py @@ -10,14 +10,6 @@ from ..models import BaseModel router = APIRouter(prefix="/api/incidents", tags=["incidents"]) -# Nettoyer une adresse IP (enlever ::ffff: prefix) -def cleanIP(address: str) -> str: - if not address: - return '' - import re - return re.sub(r'^::ffff:', '', address, flags=re.IGNORECASE) - - @router.get("/clusters") async def get_incident_clusters( hours: int = Query(24, ge=1, le=168, description="Fenêtre temporelle en heures"), @@ -34,13 +26,15 @@ async def get_incident_clusters( """ try: # Cluster par subnet /24 avec une IP exemple + # Note: src_ip est en IPv6, les IPv4 sont stockés comme ::ffff:x.x.x.x + # toIPv4() convertit les IPv4-mapped, IPv4NumToString() retourne l'IPv4 en notation x.x.x.x cluster_query = """ WITH subnet_groups AS ( SELECT concat( - splitByChar('.', toString(src_ip))[1], '.', - splitByChar('.', toString(src_ip))[2], '.', - splitByChar('.', toString(src_ip))[3], '.0/24' + splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[1], '.', + splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[2], '.', + splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[3], '.0/24' ) AS subnet, count() AS total_detections, uniq(src_ip) AS unique_ips, @@ -51,9 +45,10 @@ async def get_incident_clusters( argMax(asn_number, detected_at) AS asn_number, argMax(threat_level, detected_at) AS threat_level, avg(anomaly_score) AS avg_score, - any(src_ip) AS sample_ip + any(IPv4NumToString(toIPv4(src_ip))) AS sample_ip FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL %(hours)s HOUR + AND isIPv4MappedIPv6(src_ip) -- Filtre uniquement les IPv4 GROUP BY subnet HAVING total_detections >= 2 ) @@ -115,7 +110,7 @@ async def get_incident_clusters( "total_detections": row[1], "unique_ips": row[2], "subnet": row[0], - "sample_ip": cleanIP(row[10]) if row[10] else None, + "sample_ip": row[10] if row[10] else row[0].split('/')[0], "ja4": row[5] or "", "primary_ua": "python-requests", "primary_target": "Unknown", diff --git a/frontend/src/components/IncidentsView.tsx b/frontend/src/components/IncidentsView.tsx index 8efe815..fea9324 100644 --- a/frontend/src/components/IncidentsView.tsx +++ b/frontend/src/components/IncidentsView.tsx @@ -107,24 +107,6 @@ export function IncidentsView() { return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397)); }; - // Nettoyer une adresse IP (enlever ::ffff: prefix) - const cleanIP = (address: string): string => { - if (!address) return ''; - return address.replace(/^::ffff:/i, ''); - }; - - // Générer une IP exemple depuis un subnet - const getSampleIP = (subnet: string): string => { - const clean = cleanIP(subnet); - const ipParts = clean.replace('/24', '').split('.'); - if (ipParts.length === 4) { - // Remplacer le dernier octet par 1 - ipParts[3] = '1'; - return ipParts.join('.'); - } - return cleanIP(subnet.split('/')[0]); - }; - if (loading) { return (
@@ -268,7 +250,7 @@ export function IncidentsView() { {cluster.id} | - {cleanIP(cluster.subnet || '')} + {cluster.subnet || ''}
@@ -322,13 +304,13 @@ export function IncidentsView() {