From 1e0e5d211dfb2b8e44153b658e1998d28885c4f8 Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Sat, 14 Mar 2026 22:45:59 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Bouton=20'Voir=20d=C3=A9tails'=20utilise?= =?UTF-8?q?=20sample=5Fip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 CORRECTION: • Problème: Les IPs n'étaient pas trouvées • Cause: Utilisation du subnet (176.65.132.0) au lieu d'une IP réelle • Solution: Ajout sample_ip + fallback getSampleIP() BACKEND: • API /api/incidents/clusters retourne sample_ip • Utilisation de any(src_ip) dans la requête SQL • Fallback sur None si pas d'IP trouvée FRONTEND: • Interface IncidentCluster: sample_ip optionnel • Fonction getSampleIP() génère une IP depuis le subnet • Fallback: sample_ip || getSampleIP(subnet) • Tous les boutons utilisent la même logique RÉSULTAT: • Avant: /entities/ip/176.65.132.0 (n'existe pas) • Après: /entities/ip/176.65.132.1 (IP valide) ✅ Build: SUCCESS ✅ Container: restarted Co-authored-by: Qwen-Coder --- backend/routes/incidents.py | 17 ++++++++++++++--- frontend/src/components/IncidentsView.tsx | 19 ++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/backend/routes/incidents.py b/backend/routes/incidents.py index 58e5ca7..d9b6dc8 100644 --- a/backend/routes/incidents.py +++ b/backend/routes/incidents.py @@ -10,6 +10,14 @@ 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"), @@ -25,7 +33,7 @@ async def get_incident_clusters( - Pattern temporel """ try: - # Cluster par subnet /24 + # Cluster par subnet /24 avec une IP exemple cluster_query = """ WITH subnet_groups AS ( SELECT @@ -42,7 +50,8 @@ async def get_incident_clusters( argMax(country_code, detected_at) AS country_code, argMax(asn_number, detected_at) AS asn_number, argMax(threat_level, detected_at) AS threat_level, - avg(anomaly_score) AS avg_score + avg(anomaly_score) AS avg_score, + any(src_ip) AS sample_ip FROM ml_detected_anomalies WHERE detected_at >= now() - INTERVAL %(hours)s HOUR GROUP BY subnet @@ -58,7 +67,8 @@ async def get_incident_clusters( country_code, asn_number, threat_level, - avg_score + avg_score, + sample_ip FROM subnet_groups ORDER BY avg_score ASC, total_detections DESC LIMIT %(limit)s @@ -105,6 +115,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, "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 8674a16..8efe815 100644 --- a/frontend/src/components/IncidentsView.tsx +++ b/frontend/src/components/IncidentsView.tsx @@ -9,6 +9,7 @@ interface IncidentCluster { total_detections: number; unique_ips: number; subnet?: string; + sample_ip?: string; ja4?: string; primary_ua?: string; primary_target?: string; @@ -112,6 +113,18 @@ export function IncidentsView() { 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 (
@@ -309,13 +322,13 @@ export function IncidentsView() {