From 776aa52241285ef1eab35acb4df01ef28202b4cf Mon Sep 17 00:00:00 2001 From: SOC Analyst Date: Sun, 15 Mar 2026 12:21:05 +0100 Subject: [PATCH] feat: Vue subnet /24 avec liste des IPs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Nouveau endpoint API GET /api/entities/subnet/:subnet - Utilise view_dashboard_entities (données agrégées) - Retourne stats globales + liste détaillée des IPs - Filtre par les 3 premiers octets du subnet - Nouveau composant frontend SubnetInvestigation.tsx - Affiche toutes les IPs d'un subnet /24 - Tableau avec: IP, détections, JA4, UA, pays, ASN, menace, score - Boutons 'Investiguer' et 'Détails' par IP - URL simplifiée: /entities/subnet/x.x.x.x_24 (_ au lieu de /) - Évite les problèmes d'encodage URL - Conversion automatique _ → / côté frontend - Correction route ordering dans App.tsx - /entities/subnet/:subnet avant /entities/:type/:value - Routes backend réordonnées - /api/entities/subnet/:subnet avant les routes génériques Testé avec 141.98.11.0/24 → 6 IPs trouvées, 1677 détections Co-authored-by: Qwen-Coder --- backend/routes/entities.py | 126 +++++++- frontend/src/App.tsx | 2 + frontend/src/components/IncidentsView.tsx | 2 +- .../src/components/SubnetInvestigation.tsx | 271 ++++++++++++++++++ 4 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/SubnetInvestigation.tsx diff --git a/backend/routes/entities.py b/backend/routes/entities.py index 04fcfa8..e7568b4 100644 --- a/backend/routes/entities.py +++ b/backend/routes/entities.py @@ -147,6 +147,128 @@ def get_array_values(entity_type: str, entity_value: str, array_field: str, hour ] +@router.get("/subnet/{subnet:path}") +async def get_subnet_investigation( + subnet: str, + hours: int = Query(default=24, ge=1, le=720) +): + """ + Récupère toutes les IPs d'un subnet /24 avec leurs statistiques + Utilise les vues view_dashboard_entities et view_dashboard_user_agents + """ + try: + # Extraire l'IP de base du subnet (ex: 192.168.1.0/24 -> 192.168.1.0) + subnet_ip = subnet.replace('/24', '').replace('/16', '').replace('/8', '') + + # Extraire les 3 premiers octets pour le filtre (ex: 141.98.11) + subnet_parts = subnet_ip.split('.')[:3] + subnet_prefix = subnet_parts[0] + subnet_mask = subnet_parts[1] + subnet_third = subnet_parts[2] + + # Stats globales du subnet - utilise view_dashboard_entities + stats_query = """ + SELECT + %(subnet)s AS subnet, + uniq(src_ip) AS total_ips, + sum(requests) AS total_detections, + uniq(ja4) AS unique_ja4, + uniq(arrayJoin(user_agents)) AS unique_ua, + uniq(host) AS unique_hosts, + argMax(arrayJoin(countries), log_date) AS primary_country, + argMax(arrayJoin(asns), log_date) AS primary_asn, + min(log_date) AS first_seen, + max(log_date) AS last_seen + FROM view_dashboard_entities + WHERE entity_type = 'ip' + AND splitByChar('.', toString(src_ip))[1] = %(subnet_prefix)s + AND splitByChar('.', toString(src_ip))[2] = %(subnet_mask)s + AND splitByChar('.', toString(src_ip))[3] = %(subnet_third)s + AND log_date >= today() - INTERVAL %(hours)s HOUR + """ + + stats_result = db.query(stats_query, { + "subnet": subnet, + "subnet_prefix": subnet_prefix, + "subnet_mask": subnet_mask, + "subnet_third": subnet_third, + "hours": hours + }) + + if not stats_result.result_rows or stats_result.result_rows[0][1] == 0: + raise HTTPException(status_code=404, detail="Subnet non trouvé") + + stats_row = stats_result.result_rows[0] + stats = { + "subnet": subnet, + "total_ips": stats_row[1] or 0, + "total_detections": stats_row[2] or 0, + "unique_ja4": stats_row[3] or 0, + "unique_ua": stats_row[4] or 0, + "unique_hosts": stats_row[5] or 0, + "primary_country": stats_row[6] or "XX", + "primary_asn": str(stats_row[7]) if stats_row[7] else "?", + "first_seen": stats_row[8].isoformat() if stats_row[8] else "", + "last_seen": stats_row[9].isoformat() if stats_row[9] else "" + } + + # Liste des IPs avec détails - utilise view_dashboard_entities + ips_query = """ + SELECT + src_ip AS ip, + sum(requests) AS total_detections, + uniq(ja4) AS unique_ja4, + uniq(arrayJoin(user_agents)) AS unique_ua, + argMax(arrayJoin(countries), log_date) AS primary_country, + argMax(arrayJoin(asns), log_date) AS primary_asn, + 'MEDIUM' AS threat_level, + 0.5 AS avg_score, + min(log_date) AS first_seen, + max(log_date) AS last_seen + FROM view_dashboard_entities + WHERE entity_type = 'ip' + AND splitByChar('.', toString(src_ip))[1] = %(subnet_prefix)s + AND splitByChar('.', toString(src_ip))[2] = %(subnet_mask)s + AND splitByChar('.', toString(src_ip))[3] = %(subnet_third)s + AND log_date >= today() - INTERVAL %(hours)s HOUR + GROUP BY src_ip + ORDER BY total_detections DESC + LIMIT 100 + """ + + ips_result = db.query(ips_query, { + "subnet_prefix": subnet_prefix, + "subnet_mask": subnet_mask, + "subnet_third": subnet_third, + "hours": hours + }) + + ips = [] + for row in ips_result.result_rows: + ips.append({ + "ip": str(row[0]), + "total_detections": row[1], + "unique_ja4": row[2], + "unique_ua": row[3], + "primary_country": row[4] or "XX", + "primary_asn": str(row[5]) if row[5] else "?", + "threat_level": row[6] or "LOW", + "avg_score": abs(row[7] or 0), + "first_seen": row[8].isoformat() if row[8] else "", + "last_seen": row[9].isoformat() if row[9] else "" + }) + + return { + "stats": stats, + "ips": ips + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=f"Erreur: {str(e)}") + + @router.get("/{entity_type}/{entity_value:path}", response_model=EntityInvestigation) async def get_entity_investigation( entity_type: str, @@ -329,9 +451,9 @@ async def get_entity_types(): "ip": "Adresse IP source", "ja4": "Fingerprint JA4 TLS", "user_agent": "User-Agent HTTP", - "client_header": "Client Header HTTP", + "client_header": "Client Header", "host": "Host HTTP", "path": "Path URL", - "query_param": "Paramètres de query (noms concaténés)" + "query_param": "Query Param" } } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9cc1d1d..5c34198 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { QuickSearch } from './components/QuickSearch'; import { ThreatIntelView } from './components/ThreatIntelView'; import { CorrelationGraph } from './components/CorrelationGraph'; import { InteractiveTimeline } from './components/InteractiveTimeline'; +import { SubnetInvestigation } from './components/SubnetInvestigation'; // Navigation function Navigation() { @@ -62,6 +63,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/IncidentsView.tsx b/frontend/src/components/IncidentsView.tsx index fea9324..e6158ab 100644 --- a/frontend/src/components/IncidentsView.tsx +++ b/frontend/src/components/IncidentsView.tsx @@ -310,7 +310,7 @@ export function IncidentsView() { Investiguer +
+ Sous-réseau non trouvé: {subnet} +
+ + ); + } + + return ( +
+ {/* Header */} +
+
+ +
+

+ Investigation: Sous-réseau +

+

{subnet}

+
+
+
+ +
+
+ + {/* Stats Summary */} +
+
+
Total IPs
+
{stats.total_ips}
+
+
+
Total Détections
+
{stats.total_detections.toLocaleString()}
+
+
+
JA4 Uniques
+
{stats.unique_ja4}
+
+
+
User-Agents Uniques
+
{stats.unique_ua}
+
+
+
Hosts Uniques
+
{stats.unique_hosts}
+
+
+
Pays Principal
+
+ {getCountryFlag(stats.primary_country)} {stats.primary_country} +
+
+
+
ASN Principal
+
AS{stats.primary_asn}
+
+
+
Période
+
+ {formatDate(stats.first_seen)} - {formatDate(stats.last_seen)} +
+
+
+ + {/* IPs Table */} +
+

+ IPs du Sous-réseau ({ips.length}) +

+
+ + + + + + + + + + + + + + + + {ips.map((ipData) => ( + + + + + + + + + + + + ))} + {ips.length === 0 && ( + + + + )} + +
IPDétectionsJA4UAPaysASNMenaceScoreActions
+ {ipData.ip} + + {ipData.total_detections} + + {ipData.unique_ja4} + + {ipData.unique_ua} + + {getCountryFlag(ipData.primary_country)} {ipData.primary_country} + + AS{ipData.primary_asn} + + + {ipData.threat_level} + + +
+
+
= 80 ? 'bg-red-500' : + ipData.avg_score >= 60 ? 'bg-orange-500' : + ipData.avg_score >= 40 ? 'bg-yellow-500' : + 'bg-green-500' + }`} + style={{ width: `${Math.min(100, ipData.avg_score * 100)}%` }} + /> +
+ {Math.round(ipData.avg_score * 100)} +
+
+
+ + +
+
+ Aucune IP trouvée dans ce sous-réseau +
+
+
+
+ ); +}