- investigation_summary.py: nouveau endpoint GET /api/investigation/{ip}/summary
agrège 6 sources (ML, bruteforce, TCP spoofing, JA4 rotation, persistance, timeline 24h)
en un score de risque 0-100 avec signaux détaillés
- InvestigationView.tsx: widget IPActivitySummary avec jauge Risk Score SVG,
badges multi-sources et mini-timeline 24h barres
- metrics.py: endpoint GET /api/metrics/baseline — comparaison 24h vs hier
(total détections, IPs uniques, alertes CRITICAL) avec % de variation
- IncidentsView.tsx: widget baseline avec ▲▼ sur le dashboard principal
- rotation.py: endpoints /sophistication et /proactive-hunt
Score sophistication = JOIN 3 tables (rotation×10 + récurrence×20 + log(bf+1)×5)
Chasse proactive = IPs récurrentes sous le seuil ML (abs(score) < 0.5)
- RotationView.tsx: onglets 🏆 Sophistication et 🕵️ Chasse proactive
avec tier APT-like/Advanced/Automated/Basic et boutons investigation
- detections.py: LEFT JOIN asn_reputation, badge coloré rouge/orange/vert
selon label (bot/scanner → score 0.05, human → 0.9)
- models.py: ajout champs asn_score et asn_rep_label dans Detection
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
166 lines
7.7 KiB
Python
166 lines
7.7 KiB
Python
"""
|
|
Endpoint d'investigation enrichie pour une IP donnée.
|
|
Agrège en une seule requête les données provenant de toutes les sources :
|
|
ml_detected_anomalies, view_form_bruteforce_detected, view_tcp_spoofing_detected,
|
|
agg_host_ip_ja4_1h (rotation JA4), view_ip_recurrence, view_ai_features_1h.
|
|
"""
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from ..database import db
|
|
|
|
router = APIRouter(prefix="/api/investigation", tags=["investigation"])
|
|
|
|
|
|
@router.get("/{ip}/summary")
|
|
async def get_ip_full_summary(ip: str):
|
|
"""
|
|
Synthèse complète pour une IP : toutes les sources en un appel.
|
|
Normalise l'IP (accepte ::ffff:x.x.x.x ou x.x.x.x).
|
|
"""
|
|
clean_ip = ip.replace("::ffff:", "").strip()
|
|
try:
|
|
# ── 1. Score ML / features ─────────────────────────────────────────────
|
|
ml_sql = """
|
|
SELECT
|
|
max(abs(anomaly_score)) AS max_score,
|
|
any(threat_level) AS threat_level,
|
|
any(bot_name) AS bot_name,
|
|
count() AS total_detections,
|
|
uniq(host) AS distinct_hosts,
|
|
uniq(ja4) AS distinct_ja4
|
|
FROM mabase_prod.ml_detected_anomalies
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
"""
|
|
ml_res = db.query(ml_sql, {"ip": clean_ip})
|
|
ml_row = ml_res.result_rows[0] if ml_res.result_rows else None
|
|
ml_data = {
|
|
"max_score": round(float(ml_row[0] or 0), 2) if ml_row else 0,
|
|
"threat_level": str(ml_row[1] or "") if ml_row else "",
|
|
"attack_type": str(ml_row[2] or "") if ml_row else "",
|
|
"total_detections": int(ml_row[3] or 0) if ml_row else 0,
|
|
"distinct_hosts": int(ml_row[4] or 0) if ml_row else 0,
|
|
"distinct_ja4": int(ml_row[5] or 0) if ml_row else 0,
|
|
}
|
|
|
|
# ── 2. Brute force ─────────────────────────────────────────────────────
|
|
bf_sql = """
|
|
SELECT
|
|
uniq(host) AS hosts_attacked,
|
|
sum(hits) AS total_hits,
|
|
sum(query_params_count) AS total_params,
|
|
groupArray(3)(host) AS top_hosts
|
|
FROM mabase_prod.view_form_bruteforce_detected
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
"""
|
|
bf_res = db.query(bf_sql, {"ip": clean_ip})
|
|
bf_row = bf_res.result_rows[0] if bf_res.result_rows else None
|
|
bf_data = {
|
|
"active": bool(bf_row and int(bf_row[1] or 0) > 0),
|
|
"hosts_attacked": int(bf_row[0] or 0) if bf_row else 0,
|
|
"total_hits": int(bf_row[1] or 0) if bf_row else 0,
|
|
"total_params": int(bf_row[2] or 0) if bf_row else 0,
|
|
"top_hosts": [str(h) for h in (bf_row[3] or [])] if bf_row else [],
|
|
}
|
|
|
|
# ── 3. TCP spoofing ────────────────────────────────────────────────────
|
|
tcp_sql = """
|
|
SELECT tcp_ttl, first_ua
|
|
FROM mabase_prod.view_tcp_spoofing_detected
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
AND tcp_ttl > 0
|
|
LIMIT 1
|
|
"""
|
|
tcp_res = db.query(tcp_sql, {"ip": clean_ip})
|
|
tcp_data = {"detected": False, "tcp_ttl": None, "suspected_os": None}
|
|
if tcp_res.result_rows:
|
|
ttl = int(tcp_res.result_rows[0][0])
|
|
if 52 <= ttl <= 65:
|
|
sus_os = "Linux/Mac"
|
|
elif 110 <= ttl <= 135:
|
|
sus_os = "Windows"
|
|
else:
|
|
sus_os = "Unknown"
|
|
ua = str(tcp_res.result_rows[0][1] or "")
|
|
dec_os = "Windows" if "Windows" in ua else ("macOS" if "Mac OS X" in ua else "Linux/Android" if "Linux" in ua else "Unknown")
|
|
spoof = sus_os != "Unknown" and dec_os != "Unknown" and sus_os != dec_os
|
|
tcp_data = {
|
|
"detected": spoof,
|
|
"tcp_ttl": ttl,
|
|
"suspected_os": sus_os,
|
|
"declared_os": dec_os,
|
|
}
|
|
|
|
# ── 4. JA4 rotation ────────────────────────────────────────────────────
|
|
rot_sql = """
|
|
SELECT distinct_ja4_count, total_hits
|
|
FROM mabase_prod.view_host_ip_ja4_rotation
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
LIMIT 1
|
|
"""
|
|
rot_res = db.query(rot_sql, {"ip": clean_ip})
|
|
rot_data = {"rotating": False, "distinct_ja4_count": 0}
|
|
if rot_res.result_rows:
|
|
row = rot_res.result_rows[0]
|
|
cnt = int(row[0] or 0)
|
|
rot_data = {"rotating": cnt > 1, "distinct_ja4_count": cnt, "total_hits": int(row[1] or 0)}
|
|
|
|
# ── 5. Persistance ─────────────────────────────────────────────────────
|
|
pers_sql = """
|
|
SELECT recurrence, worst_score, worst_threat_level, first_seen, last_seen
|
|
FROM mabase_prod.view_ip_recurrence
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
LIMIT 1
|
|
"""
|
|
pers_res = db.query(pers_sql, {"ip": clean_ip})
|
|
pers_data = {"persistent": False, "recurrence": 0}
|
|
if pers_res.result_rows:
|
|
row = pers_res.result_rows[0]
|
|
pers_data = {
|
|
"persistent": True,
|
|
"recurrence": int(row[0] or 0),
|
|
"worst_score": round(float(row[1] or 0), 2),
|
|
"worst_threat_level":str(row[2] or ""),
|
|
"first_seen": str(row[3]),
|
|
"last_seen": str(row[4]),
|
|
}
|
|
|
|
# ── 6. Timeline 24h ────────────────────────────────────────────────────
|
|
tl_sql = """
|
|
SELECT
|
|
toHour(window_start) AS hour,
|
|
sum(hits) AS hits,
|
|
groupUniqArray(3)(ja4) AS ja4s
|
|
FROM mabase_prod.agg_host_ip_ja4_1h
|
|
WHERE replaceRegexpAll(toString(src_ip), '^::ffff:', '') = %(ip)s
|
|
AND window_start >= now() - INTERVAL 24 HOUR
|
|
GROUP BY hour
|
|
ORDER BY hour ASC
|
|
"""
|
|
tl_res = db.query(tl_sql, {"ip": clean_ip})
|
|
timeline = [
|
|
{"hour": int(r[0]), "hits": int(r[1]), "ja4s": [str(j) for j in (r[2] or [])]}
|
|
for r in tl_res.result_rows
|
|
]
|
|
|
|
# ── Global risk score (heuristic) ──────────────────────────────────────
|
|
risk = 0
|
|
risk += min(50, ml_data["max_score"] * 50)
|
|
if bf_data["active"]: risk += 20
|
|
if tcp_data["detected"]: risk += 15
|
|
if rot_data["rotating"]: risk += min(15, rot_data["distinct_ja4_count"] * 3)
|
|
if pers_data["persistent"]: risk += min(10, pers_data["recurrence"] * 2)
|
|
risk = min(100, round(risk))
|
|
|
|
return {
|
|
"ip": clean_ip,
|
|
"risk_score": risk,
|
|
"ml": ml_data,
|
|
"bruteforce": bf_data,
|
|
"tcp_spoofing":tcp_data,
|
|
"ja4_rotation":rot_data,
|
|
"persistence": pers_data,
|
|
"timeline_24h":timeline,
|
|
}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|