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:
@ -10,6 +10,14 @@ from ..models import BaseModel
|
|||||||
router = APIRouter(prefix="/api/incidents", tags=["incidents"])
|
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")
|
@router.get("/clusters")
|
||||||
async def get_incident_clusters(
|
async def get_incident_clusters(
|
||||||
hours: int = Query(24, ge=1, le=168, description="Fenêtre temporelle en heures"),
|
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
|
- Pattern temporel
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Cluster par subnet /24
|
# Cluster par subnet /24 avec une IP exemple
|
||||||
cluster_query = """
|
cluster_query = """
|
||||||
WITH subnet_groups AS (
|
WITH subnet_groups AS (
|
||||||
SELECT
|
SELECT
|
||||||
@ -42,7 +50,8 @@ async def get_incident_clusters(
|
|||||||
argMax(country_code, detected_at) AS country_code,
|
argMax(country_code, detected_at) AS country_code,
|
||||||
argMax(asn_number, detected_at) AS asn_number,
|
argMax(asn_number, detected_at) AS asn_number,
|
||||||
argMax(threat_level, detected_at) AS threat_level,
|
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
|
FROM ml_detected_anomalies
|
||||||
WHERE detected_at >= now() - INTERVAL %(hours)s HOUR
|
WHERE detected_at >= now() - INTERVAL %(hours)s HOUR
|
||||||
GROUP BY subnet
|
GROUP BY subnet
|
||||||
@ -58,7 +67,8 @@ async def get_incident_clusters(
|
|||||||
country_code,
|
country_code,
|
||||||
asn_number,
|
asn_number,
|
||||||
threat_level,
|
threat_level,
|
||||||
avg_score
|
avg_score,
|
||||||
|
sample_ip
|
||||||
FROM subnet_groups
|
FROM subnet_groups
|
||||||
ORDER BY avg_score ASC, total_detections DESC
|
ORDER BY avg_score ASC, total_detections DESC
|
||||||
LIMIT %(limit)s
|
LIMIT %(limit)s
|
||||||
@ -105,6 +115,7 @@ async def get_incident_clusters(
|
|||||||
"total_detections": row[1],
|
"total_detections": row[1],
|
||||||
"unique_ips": row[2],
|
"unique_ips": row[2],
|
||||||
"subnet": row[0],
|
"subnet": row[0],
|
||||||
|
"sample_ip": cleanIP(row[10]) if row[10] else None,
|
||||||
"ja4": row[5] or "",
|
"ja4": row[5] or "",
|
||||||
"primary_ua": "python-requests",
|
"primary_ua": "python-requests",
|
||||||
"primary_target": "Unknown",
|
"primary_target": "Unknown",
|
||||||
|
|||||||
@ -9,6 +9,7 @@ interface IncidentCluster {
|
|||||||
total_detections: number;
|
total_detections: number;
|
||||||
unique_ips: number;
|
unique_ips: number;
|
||||||
subnet?: string;
|
subnet?: string;
|
||||||
|
sample_ip?: string;
|
||||||
ja4?: string;
|
ja4?: string;
|
||||||
primary_ua?: string;
|
primary_ua?: string;
|
||||||
primary_target?: string;
|
primary_target?: string;
|
||||||
@ -112,6 +113,18 @@ export function IncidentsView() {
|
|||||||
return address.replace(/^::ffff:/i, '');
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@ -309,13 +322,13 @@ export function IncidentsView() {
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<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"
|
className="px-3 py-1.5 bg-accent-primary text-white rounded text-sm hover:bg-accent-primary/80 transition-colors"
|
||||||
>
|
>
|
||||||
Investiguer
|
Investiguer
|
||||||
</button>
|
</button>
|
||||||
<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"
|
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
|
Voir détails
|
||||||
@ -323,7 +336,7 @@ export function IncidentsView() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Quick classify
|
// 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"
|
className="px-3 py-1.5 bg-background-card text-text-primary rounded text-sm hover:bg-background-card/80 transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user