feat(dashboard): SOC workflow overhaul — sidebar nav, doc tooltips, full-width layout
- base.html: collapsible sidebar navigation, doc tooltip system, JS helpers (fmtNum, fmtPct, fmtDuration, ecGrid, buildTable, docHTML) - overview.html: SOC command center with stacked timeline, live alerts, campaigns panel, browser donut, 6 KPIs - detections.html: threat color dots, raw score column, click-to-navigate rows - network.html: JA4 rotation, brute-force, persistent threats tables, 6 KPIs - ip_detail.html: ASN/country KPIs, AE/XGB/campaign columns, enriched features - scores/traffic/features/models/classify: page_title blocks + doc tooltips - api.py: 9 new endpoints (campaigns, brute-force, ja4-rotation, recurrence, cascade, alerts, timeline-detail, ua-rotation) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -803,3 +803,187 @@ async def classifications() -> dict[str, Any]:
|
||||
except Exception as exc:
|
||||
logger.exception("classifications query failed")
|
||||
return {"data": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/campaigns — HDBSCAN bot campaign clusters
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/campaigns")
|
||||
async def campaigns() -> dict[str, Any]:
|
||||
"""Campagnes de bots détectées par clustering HDBSCAN."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT campaign_id, "
|
||||
f"count() AS members, "
|
||||
f"min(detected_at) AS first_seen, max(detected_at) AS last_seen, "
|
||||
f"avg(anomaly_score) AS avg_score, "
|
||||
f"max(anomaly_score) AS max_score, "
|
||||
f"uniqExact(src_ip) AS unique_ips, "
|
||||
f"groupUniqArray(10)(ja4) AS ja4_list, "
|
||||
f"groupUniqArray(5)(asn_org) AS asn_list, "
|
||||
f"groupUniqArray(5)(country_code) AS countries "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE campaign_id != '' AND campaign_id != '0' "
|
||||
"AND detected_at >= now() - INTERVAL 7 DAY "
|
||||
"GROUP BY campaign_id "
|
||||
"ORDER BY members DESC LIMIT 50"
|
||||
)
|
||||
return {"campaigns": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("campaigns query failed")
|
||||
return {"campaigns": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/brute-force — Form brute-force detection
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/brute-force")
|
||||
async def brute_force() -> dict[str, Any]:
|
||||
"""Détection de brute-force / credential stuffing via view_form_bruteforce_detected."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, host, "
|
||||
f"post_count, distinct_paths, first_seen, last_seen "
|
||||
f"FROM {_DB}.view_form_bruteforce_detected "
|
||||
"ORDER BY post_count DESC LIMIT 100"
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("brute-force query failed")
|
||||
return {"data": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/ja4-rotation — JA4 fingerprint rotation detection
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/ja4-rotation")
|
||||
async def ja4_rotation() -> dict[str, Any]:
|
||||
"""IPs présentant une rotation de fingerprints JA4 (évasion potentielle)."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, host, "
|
||||
f"distinct_ja4, ja4_list, total_hits, window_start "
|
||||
f"FROM {_DB}.view_host_ip_ja4_rotation "
|
||||
"ORDER BY distinct_ja4 DESC LIMIT 100"
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("ja4-rotation query failed")
|
||||
return {"data": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/recurrence — Persistent threat IPs
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/recurrence")
|
||||
async def recurrence() -> dict[str, Any]:
|
||||
"""IPs récurrentes détectées sur plusieurs fenêtres temporelles."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, "
|
||||
f"recurrence, worst_score, worst_threat, "
|
||||
f"first_seen, last_seen, "
|
||||
f"top_ja4, top_host "
|
||||
f"FROM {_DB}.view_ip_recurrence "
|
||||
"ORDER BY recurrence DESC, worst_score DESC LIMIT 100"
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("recurrence query failed")
|
||||
return {"data": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/cascade/{ip} — Resource cascade for headless detection
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/cascade/{ip}")
|
||||
async def cascade(ip: str) -> dict[str, Any]:
|
||||
"""Cascade de ressources (détection navigateurs headless) pour une IP."""
|
||||
clean_ip = ip.replace("::ffff:", "")
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, host, "
|
||||
f"page_count, avg_sub_delay_ms, stddev_sub_delay_ms, "
|
||||
f"max_sub_resources, window_start "
|
||||
f"FROM {_DB}.view_resource_cascade_1h "
|
||||
"WHERE src_ip = toIPv6({ip:String}) "
|
||||
"ORDER BY window_start DESC LIMIT 50",
|
||||
{"ip": clean_ip},
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("cascade query failed for %s", ip)
|
||||
return {"data": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/alerts — Live alert feed (recent HIGH/CRITICAL)
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/alerts")
|
||||
async def alerts(
|
||||
limit: int = Query(20, ge=1, le=100),
|
||||
) -> dict[str, Any]:
|
||||
"""Flux d'alertes en temps réel (CRITICAL, HIGH, KNOWN_BOT)."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT detected_at, toString(src_ip) AS src_ip, "
|
||||
f"anomaly_score, threat_level, ja4, host, "
|
||||
f"asn_org, country_code, bot_name, campaign_id, "
|
||||
f"hits, hit_velocity, reason "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE detected_at >= now() - INTERVAL 1 DAY "
|
||||
"ORDER BY detected_at DESC "
|
||||
f"LIMIT {{lim:UInt32}}",
|
||||
{"lim": limit},
|
||||
)
|
||||
return {"alerts": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("alerts query failed")
|
||||
return {"alerts": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/timeline-detail — Hourly threat-level breakdown
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/timeline-detail")
|
||||
async def timeline_detail() -> dict[str, Any]:
|
||||
"""Timeline horaire avec ventilation par threat level."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toStartOfHour(detected_at) AS hour, "
|
||||
f"threat_level, count() AS cnt "
|
||||
f"FROM {_DB}.ml_detected_anomalies "
|
||||
"WHERE detected_at >= now() - INTERVAL 1 DAY "
|
||||
"GROUP BY hour, threat_level "
|
||||
"ORDER BY hour"
|
||||
)
|
||||
# Pivot: group by hour
|
||||
hours: dict[str, dict] = {}
|
||||
for r in rows:
|
||||
h = str(r["hour"])
|
||||
if h not in hours:
|
||||
hours[h] = {"hour": h}
|
||||
hours[h][r["threat_level"]] = r["cnt"]
|
||||
return {"timeline": list(hours.values())}
|
||||
except Exception as exc:
|
||||
logger.exception("timeline-detail query failed")
|
||||
return {"timeline": []}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/ua-rotation — User-Agent rotation detection
|
||||
# ---------------------------------------------------------------------------
|
||||
@router.get("/ua-rotation")
|
||||
async def ua_rotation() -> dict[str, Any]:
|
||||
"""IPs avec rotation de User-Agent (évasion potentielle)."""
|
||||
try:
|
||||
rows = query(
|
||||
f"SELECT toString(src_ip) AS src_ip, ja4, "
|
||||
f"distinct_ua_count, ua_samples, total_requests, window_start "
|
||||
f"FROM {_DB}.view_dashboard_user_agents "
|
||||
"ORDER BY distinct_ua_count DESC LIMIT 100"
|
||||
)
|
||||
return {"data": rows}
|
||||
except Exception as exc:
|
||||
logger.exception("ua-rotation query failed")
|
||||
return {"data": []}
|
||||
|
||||
Reference in New Issue
Block a user