fix: IPs IPv4 avec notation ::ffff: corrigée
🐛 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 <qwen-coder@alibabacloud.com>
This commit is contained in:
@ -10,14 +10,6 @@ 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"),
|
||||||
@ -34,13 +26,15 @@ async def get_incident_clusters(
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Cluster par subnet /24 avec une IP exemple
|
# 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 = """
|
cluster_query = """
|
||||||
WITH subnet_groups AS (
|
WITH subnet_groups AS (
|
||||||
SELECT
|
SELECT
|
||||||
concat(
|
concat(
|
||||||
splitByChar('.', toString(src_ip))[1], '.',
|
splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[1], '.',
|
||||||
splitByChar('.', toString(src_ip))[2], '.',
|
splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[2], '.',
|
||||||
splitByChar('.', toString(src_ip))[3], '.0/24'
|
splitByChar('.', IPv4NumToString(toIPv4(src_ip)))[3], '.0/24'
|
||||||
) AS subnet,
|
) AS subnet,
|
||||||
count() AS total_detections,
|
count() AS total_detections,
|
||||||
uniq(src_ip) AS unique_ips,
|
uniq(src_ip) AS unique_ips,
|
||||||
@ -51,9 +45,10 @@ async def get_incident_clusters(
|
|||||||
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
|
any(IPv4NumToString(toIPv4(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
|
||||||
|
AND isIPv4MappedIPv6(src_ip) -- Filtre uniquement les IPv4
|
||||||
GROUP BY subnet
|
GROUP BY subnet
|
||||||
HAVING total_detections >= 2
|
HAVING total_detections >= 2
|
||||||
)
|
)
|
||||||
@ -115,7 +110,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,
|
"sample_ip": row[10] if row[10] else row[0].split('/')[0],
|
||||||
"ja4": row[5] or "",
|
"ja4": row[5] or "",
|
||||||
"primary_ua": "python-requests",
|
"primary_ua": "python-requests",
|
||||||
"primary_target": "Unknown",
|
"primary_target": "Unknown",
|
||||||
|
|||||||
@ -107,24 +107,6 @@ export function IncidentsView() {
|
|||||||
return code.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397));
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
@ -268,7 +250,7 @@ export function IncidentsView() {
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-bold text-text-primary">{cluster.id}</span>
|
<span className="text-lg font-bold text-text-primary">{cluster.id}</span>
|
||||||
<span className="text-text-secondary">|</span>
|
<span className="text-text-secondary">|</span>
|
||||||
<span className="font-mono text-sm text-text-primary">{cleanIP(cluster.subnet || '')}</span>
|
<span className="font-mono text-sm text-text-primary">{cluster.subnet || ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
@ -322,13 +304,13 @@ export function IncidentsView() {
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate(`/investigation/${cleanIP(cluster.sample_ip || getSampleIP(cluster.subnet || ''))}`)}
|
onClick={() => navigate(`/investigation/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)}
|
||||||
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.sample_ip || getSampleIP(cluster.subnet || ''))}`)}
|
onClick={() => navigate(`/entities/ip/${cluster.sample_ip || cluster.subnet?.split('/')[0] || ''}`)}
|
||||||
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
|
||||||
@ -336,7 +318,7 @@ export function IncidentsView() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Quick classify
|
// Quick classify
|
||||||
navigate(`/bulk-classify?ips=${encodeURIComponent(cleanIP(cluster.sample_ip || getSampleIP(cluster.subnet || '')))}`);
|
navigate(`/bulk-classify?ips=${encodeURIComponent(cluster.sample_ip || cluster.subnet?.split('/')[0] || '')}`);
|
||||||
}}
|
}}
|
||||||
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"
|
||||||
>
|
>
|
||||||
@ -351,7 +333,7 @@ export function IncidentsView() {
|
|||||||
objects: [{
|
objects: [{
|
||||||
type: 'indicator',
|
type: 'indicator',
|
||||||
id: `indicator--${cluster.id}`,
|
id: `indicator--${cluster.id}`,
|
||||||
pattern: `[ipv4-addr:value = '${cleanIP(cluster.subnet?.split('/')[0] || '')}'`,
|
pattern: `[ipv4-addr:value = '${cluster.subnet?.split('/')[0] || ''}'`,
|
||||||
pattern_type: 'stix'
|
pattern_type: 'stix'
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
@ -415,7 +397,7 @@ export function IncidentsView() {
|
|||||||
>
|
>
|
||||||
<td className="px-4 py-3 text-text-secondary">{index + 1}</td>
|
<td className="px-4 py-3 text-text-secondary">{index + 1}</td>
|
||||||
<td className="px-4 py-3 font-mono text-sm text-text-primary">
|
<td className="px-4 py-3 font-mono text-sm text-text-primary">
|
||||||
{cleanIP(cluster.subnet?.split('/')[0] || 'Unknown')}
|
{cluster.subnet?.split('/')[0] || 'Unknown'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-text-secondary">IP</td>
|
<td className="px-4 py-3 text-sm text-text-secondary">IP</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user