- 🔥 Brute Force & Credential Stuffing (view_form_bruteforce_detected) - 🧬 TCP/OS Spoofing (view_tcp_spoofing_detected, 86K détections) - 📡 Header Fingerprint Clustering (agg_header_fingerprint_1h, 1374 clusters) - ⏱️ Heatmap Temporelle (agg_host_ip_ja4_1h, pic à 20h) - 🌍 Botnets Distribués / JA4 spread (view_host_ja4_anomalies) - 🔄 Rotation JA4 & Persistance (view_host_ip_ja4_rotation + view_ip_recurrence) - 🤖 Features ML / Radar (view_ai_features_1h, radar SVG + scatter plot) Backend: 7 nouveaux router FastAPI avec requêtes ClickHouse optimisées Frontend: 7 nouveaux composants React + navigation 'Analyse Avancée' dans la sidebar Fixes: alias fuzzing_index → max_fuzzing (ORDER BY ClickHouse), normalisation IPs ::ffff: Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
146 lines
4.8 KiB
Python
146 lines
4.8 KiB
Python
"""
|
||
Endpoints pour la heatmap temporelle (hits par heure / hôte)
|
||
"""
|
||
from collections import defaultdict
|
||
from fastapi import APIRouter, HTTPException, Query
|
||
|
||
from ..database import db
|
||
|
||
router = APIRouter(prefix="/api/heatmap", tags=["heatmap"])
|
||
|
||
|
||
@router.get("/hourly")
|
||
async def get_heatmap_hourly():
|
||
"""Hits agrégés par heure sur les 72 dernières heures."""
|
||
try:
|
||
sql = """
|
||
SELECT
|
||
toHour(window_start) AS hour,
|
||
sum(hits) AS hits,
|
||
uniq(replaceRegexpAll(toString(src_ip), '^::ffff:', '')) AS unique_ips,
|
||
max(max_requests_per_sec) AS max_rps
|
||
FROM mabase_prod.agg_host_ip_ja4_1h
|
||
WHERE window_start >= now() - INTERVAL 72 HOUR
|
||
GROUP BY hour
|
||
ORDER BY hour ASC
|
||
"""
|
||
result = db.query(sql)
|
||
hours = [
|
||
{
|
||
"hour": int(row[0]),
|
||
"hits": int(row[1]),
|
||
"unique_ips": int(row[2]),
|
||
"max_rps": int(row[3]),
|
||
}
|
||
for row in result.result_rows
|
||
]
|
||
return {"hours": hours}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/top-hosts")
|
||
async def get_heatmap_top_hosts(limit: int = Query(20, ge=1, le=100)):
|
||
"""Hôtes les plus ciblés avec répartition horaire sur 24h."""
|
||
try:
|
||
# Aggregate overall stats per host
|
||
agg_sql = """
|
||
SELECT
|
||
host,
|
||
sum(hits) AS total_hits,
|
||
uniq(replaceRegexpAll(toString(src_ip), '^::ffff:', '')) AS unique_ips,
|
||
uniq(ja4) AS unique_ja4s
|
||
FROM mabase_prod.agg_host_ip_ja4_1h
|
||
WHERE window_start >= now() - INTERVAL 72 HOUR
|
||
GROUP BY host
|
||
ORDER BY total_hits DESC
|
||
LIMIT %(limit)s
|
||
"""
|
||
agg_res = db.query(agg_sql, {"limit": limit})
|
||
top_hosts = [str(r[0]) for r in agg_res.result_rows]
|
||
host_stats = {
|
||
str(r[0]): {
|
||
"host": str(r[0]),
|
||
"total_hits": int(r[1]),
|
||
"unique_ips": int(r[2]),
|
||
"unique_ja4s":int(r[3]),
|
||
}
|
||
for r in agg_res.result_rows
|
||
}
|
||
|
||
if not top_hosts:
|
||
return {"items": []}
|
||
|
||
# Hourly breakdown per host
|
||
hourly_sql = """
|
||
SELECT
|
||
host,
|
||
toHour(window_start) AS hour,
|
||
sum(hits) AS hits
|
||
FROM mabase_prod.agg_host_ip_ja4_1h
|
||
WHERE window_start >= now() - INTERVAL 72 HOUR
|
||
AND host IN %(hosts)s
|
||
GROUP BY host, hour
|
||
"""
|
||
hourly_res = db.query(hourly_sql, {"hosts": top_hosts})
|
||
|
||
hourly_map: dict = defaultdict(lambda: [0] * 24)
|
||
for row in hourly_res.result_rows:
|
||
h = str(row[0])
|
||
hour = int(row[1])
|
||
hits = int(row[2])
|
||
hourly_map[h][hour] += hits
|
||
|
||
items = []
|
||
for host in top_hosts:
|
||
entry = dict(host_stats[host])
|
||
entry["hourly_hits"] = hourly_map[host]
|
||
items.append(entry)
|
||
|
||
return {"items": items}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/matrix")
|
||
async def get_heatmap_matrix():
|
||
"""Matrice top-15 hôtes × 24 heures (sum hits) sur les 72 dernières heures."""
|
||
try:
|
||
top_sql = """
|
||
SELECT host, sum(hits) AS total_hits
|
||
FROM mabase_prod.agg_host_ip_ja4_1h
|
||
WHERE window_start >= now() - INTERVAL 72 HOUR
|
||
GROUP BY host
|
||
ORDER BY total_hits DESC
|
||
LIMIT 15
|
||
"""
|
||
top_res = db.query(top_sql)
|
||
top_hosts = [str(r[0]) for r in top_res.result_rows]
|
||
|
||
if not top_hosts:
|
||
return {"hosts": [], "matrix": []}
|
||
|
||
cell_sql = """
|
||
SELECT
|
||
host,
|
||
toHour(window_start) AS hour,
|
||
sum(hits) AS hits
|
||
FROM mabase_prod.agg_host_ip_ja4_1h
|
||
WHERE window_start >= now() - INTERVAL 72 HOUR
|
||
AND host IN %(hosts)s
|
||
GROUP BY host, hour
|
||
"""
|
||
cell_res = db.query(cell_sql, {"hosts": top_hosts})
|
||
|
||
matrix_map: dict = defaultdict(lambda: [0] * 24)
|
||
for row in cell_res.result_rows:
|
||
h = str(row[0])
|
||
hour = int(row[1])
|
||
hits = int(row[2])
|
||
matrix_map[h][hour] += hits
|
||
|
||
matrix = [matrix_map[h] for h in top_hosts]
|
||
return {"hosts": top_hosts, "matrix": matrix}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=str(e))
|