diff --git a/backend/routes/bruteforce.py b/backend/routes/bruteforce.py index 77b9bc9..5085025 100644 --- a/backend/routes/bruteforce.py +++ b/backend/routes/bruteforce.py @@ -55,7 +55,7 @@ async def get_bruteforce_attackers(limit: int = Query(50, ge=1, le=500)): try: sql = """ SELECT - src_ip AS ip, + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS ip, uniq(host) AS distinct_hosts, sum(hits) AS total_hits, sum(query_params_count) AS total_params, @@ -105,3 +105,37 @@ async def get_bruteforce_timeline(): return {"hours": hours} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/host/{host:path}/attackers") +async def get_host_attackers(host: str, limit: int = Query(20, ge=1, le=200)): + """Top IPs attaquant un hôte spécifique, avec JA4 et type d'attaque.""" + try: + sql = """ + SELECT + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS ip, + sum(hits) AS total_hits, + sum(query_params_count) AS total_params, + argMax(ja4, hits) AS ja4, + max(hits) AS max_hits_per_window + FROM mabase_prod.view_form_bruteforce_detected + WHERE host = %(host)s + GROUP BY src_ip + ORDER BY total_hits DESC + LIMIT %(limit)s + """ + result = db.query(sql, {"host": host, "limit": limit}) + items = [] + for row in result.result_rows: + total_hits = int(row[1]) + total_params = int(row[2]) + items.append({ + "ip": str(row[0]), + "total_hits": total_hits, + "total_params":total_params, + "ja4": str(row[3] or ""), + "attack_type": "credential_stuffing" if total_hits > 0 and total_params / total_hits > 0.5 else "enumeration", + }) + return {"host": host, "items": items, "total": len(items)} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/routes/rotation.py b/backend/routes/rotation.py index d7507e8..a792f0a 100644 --- a/backend/routes/rotation.py +++ b/backend/routes/rotation.py @@ -14,7 +14,7 @@ async def get_ja4_rotators(limit: int = Query(50, ge=1, le=500)): try: sql = """ SELECT - src_ip AS ip, + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS ip, distinct_ja4_count, total_hits FROM mabase_prod.view_host_ip_ja4_rotation @@ -42,7 +42,7 @@ async def get_persistent_threats(limit: int = Query(100, ge=1, le=1000)): try: sql = """ SELECT - src_ip AS ip, + replaceRegexpAll(toString(src_ip), '^::ffff:', '') AS ip, recurrence, worst_score, worst_threat_level, diff --git a/frontend/src/components/BotnetMapView.tsx b/frontend/src/components/BotnetMapView.tsx index 3fb281b..9db8366 100644 --- a/frontend/src/components/BotnetMapView.tsx +++ b/frontend/src/components/BotnetMapView.tsx @@ -104,8 +104,8 @@ function BotnetRow({ try { const res = await fetch(`/api/botnets/ja4/${encodeURIComponent(item.ja4)}/countries?limit=30`); if (!res.ok) throw new Error('Erreur chargement des pays'); - const data: { countries: CountryEntry[] } = await res.json(); - setCountries(data.countries ?? []); + const data: { items: CountryEntry[] } = await res.json(); + setCountries(data.items ?? []); setCountriesLoaded(true); } catch (err) { setCountriesError(err instanceof Error ? err.message : 'Erreur inconnue'); diff --git a/frontend/src/components/BruteForceView.tsx b/frontend/src/components/BruteForceView.tsx index a605682..abe1e44 100644 --- a/frontend/src/components/BruteForceView.tsx +++ b/frontend/src/components/BruteForceView.tsx @@ -9,7 +9,7 @@ interface BruteForceTarget { total_hits: number; total_params: number; attack_type: string; - top_ja4: string[]; + top_ja4s: string[]; } interface BruteForceAttacker { @@ -63,6 +63,116 @@ function ErrorMessage({ message }: { message: string }) { // ─── Main Component ─────────────────────────────────────────────────────────── +interface HostAttacker { ip: string; total_hits: number; total_params: number; ja4: string; attack_type: string; } + +function TargetRow({ t, navigate }: { t: BruteForceTarget; navigate: (path: string) => void }) { + const [expanded, setExpanded] = useState(false); + const [hostAttackers, setHostAttackers] = useState([]); + const [loading, setLoading] = useState(false); + const [loaded, setLoaded] = useState(false); + + const toggle = async () => { + setExpanded(prev => !prev); + if (!loaded && !expanded) { + setLoading(true); + try { + const res = await fetch(`/api/bruteforce/host/${encodeURIComponent(t.host)}/attackers?limit=20`); + if (!res.ok) throw new Error('Erreur chargement'); + const data: { items: HostAttacker[] } = await res.json(); + setHostAttackers(data.items ?? []); + setLoaded(true); + } catch { /* ignore */ } + finally { setLoading(false); } + } + }; + + return ( + <> + + + {expanded ? '▾' : '▸'} + {t.host} + + {formatNumber(t.unique_ips)} + {formatNumber(t.total_hits)} + {formatNumber(t.total_params)} + + {t.attack_type === 'credential_stuffing' ? ( + 💳 Credential Stuffing + ) : ( + 🔍 Énumération + )} + + +
+ {(t.top_ja4s ?? []).slice(0, 2).map((ja4, i) => ( + + {ja4.slice(0, 12)}… + + ))} +
+ + + {expanded && ( + + + {loading ? ( +
+
+ Chargement des attaquants… +
+ ) : hostAttackers.length === 0 ? ( +

Aucun attaquant trouvé.

+ ) : ( +
+

+ Top {hostAttackers.length} IP attaquant {t.host} +

+ + + + + + + + + + + + + {hostAttackers.map((a) => ( + + + + + + + + + ))} + +
IPHitsParamsJA4Type
{a.ip}{formatNumber(a.total_hits)}{formatNumber(a.total_params)}{a.ja4 ? a.ja4.slice(0, 16) + '…' : '—'} + {a.attack_type === 'credential_stuffing' + ? 💳 + : 🔍 + } + + +
+
+ )} + + + )} + + ); +} + export function BruteForceView() { const navigate = useNavigate(); @@ -202,51 +312,17 @@ export function BruteForceView() { - + - {targets.map((t) => ( - - - - - - - - - + ))}
HostHost (cliquer pour détails) IPs distinctes Total hits Params combos Type d'attaque Top JA4
{t.host}{formatNumber(t.unique_ips)}{formatNumber(t.total_hits)}{formatNumber(t.total_params)} - {t.attack_type === 'credential_stuffing' ? ( - - 💳 Credential Stuffing - - ) : ( - - 🔍 Énumération - - )} - -
- {(t.top_ja4 ?? []).slice(0, 3).map((ja4) => ( - - {ja4.slice(0, 12)}… - - ))} -
-
- -
diff --git a/frontend/src/components/HeaderFingerprintView.tsx b/frontend/src/components/HeaderFingerprintView.tsx index a166f54..b1f9a56 100644 --- a/frontend/src/components/HeaderFingerprintView.tsx +++ b/frontend/src/components/HeaderFingerprintView.tsx @@ -103,8 +103,8 @@ function ClusterRow({ try { const res = await fetch(`/api/headers/cluster/${cluster.hash}/ips?limit=50`); if (!res.ok) throw new Error('Erreur chargement IPs'); - const data: { ips: ClusterIP[] } = await res.json(); - setClusterIPs(data.ips ?? []); + const data: { items: ClusterIP[] } = await res.json(); + setClusterIPs(data.items ?? []); setIpsLoaded(true); } catch (err) { setIpsError(err instanceof Error ? err.message : 'Erreur inconnue'); diff --git a/frontend/src/components/HeatmapView.tsx b/frontend/src/components/HeatmapView.tsx index 3eeb5d8..db47057 100644 --- a/frontend/src/components/HeatmapView.tsx +++ b/frontend/src/components/HeatmapView.tsx @@ -116,8 +116,8 @@ export function HeatmapView() { try { const res = await fetch('/api/heatmap/top-hosts?limit=20'); if (!res.ok) throw new Error('Erreur chargement top hosts'); - const data: { hosts: TopHost[] } = await res.json(); - setTopHosts(data.hosts ?? []); + const data: { items: TopHost[] } = await res.json(); + setTopHosts(data.items ?? []); } catch (err) { setTopHostsError(err instanceof Error ? err.message : 'Erreur inconnue'); } finally { diff --git a/frontend/src/components/RotationView.tsx b/frontend/src/components/RotationView.tsx index 3da3039..0cf0e32 100644 --- a/frontend/src/components/RotationView.tsx +++ b/frontend/src/components/RotationView.tsx @@ -96,8 +96,8 @@ function RotatorRow({ item }: { item: JA4Rotator }) { try { const res = await fetch(`/api/rotation/ip/${encodeURIComponent(item.ip)}/ja4-history`); if (!res.ok) throw new Error('Erreur chargement historique JA4'); - const data: { history: JA4HistoryEntry[] } = await res.json(); - setHistory(data.history ?? []); + const data: { ja4_history: JA4HistoryEntry[] } = await res.json(); + setHistory(data.ja4_history ?? []); setHistoryLoaded(true); } catch (err) { setHistoryError(err instanceof Error ? err.message : 'Erreur inconnue');