fix: Bouton 'Voir détails' utilise sample_ip

🐛 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 <qwen-coder@alibabacloud.com>
This commit is contained in:
SOC Analyst
2026-03-14 22:45:59 +01:00
parent 4434fcee80
commit 1e0e5d211d
2 changed files with 30 additions and 6 deletions

View File

@ -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",

View File

@ -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 (
<div className="flex items-center justify-center h-64">
@ -309,13 +322,13 @@ export function IncidentsView() {
<div className="flex gap-2">
<button
onClick={() => navigate(`/investigation/${cleanIP(cluster.subnet?.split('/')[0] || '')}`)}
onClick={() => navigate(`/investigation/${cleanIP(cluster.sample_ip || getSampleIP(cluster.subnet || ''))}`)}
className="px-3 py-1.5 bg-accent-primary text-white rounded text-sm hover:bg-accent-primary/80 transition-colors"
>
Investiguer
</button>
<button
onClick={() => navigate(`/entities/ip/${cleanIP(cluster.subnet?.split('/')[0] || '')}`)}
onClick={() => navigate(`/entities/ip/${cleanIP(cluster.sample_ip || getSampleIP(cluster.subnet || ''))}`)}
className="px-3 py-1.5 bg-background-card text-text-primary rounded text-sm hover:bg-background-card/80 transition-colors"
>
Voir détails
@ -323,7 +336,7 @@ export function IncidentsView() {
<button
onClick={() => {
// Quick classify
navigate(`/bulk-classify?ips=${encodeURIComponent(cleanIP(cluster.subnet?.split('/')[0] || ''))}`);
navigate(`/bulk-classify?ips=${encodeURIComponent(cleanIP(cluster.sample_ip || getSampleIP(cluster.subnet || '')))}`);
}}
className="px-3 py-1.5 bg-background-card text-text-primary rounded text-sm hover:bg-background-card/80 transition-colors"
>