From f6571c2b1019f033ad1ddd368a0fd7e4af034f74 Mon Sep 17 00:00:00 2001 From: Khalid MOUTAOUAKIL Date: Mon, 6 Apr 2026 16:48:29 +0200 Subject: [PATCH] Flow map: filtre par domaine/zone, recherche serveur avec autocompletion Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routers/audit_full.py | 76 ++++++++++++++++++++++++++- app/templates/audit_full_flowmap.html | 59 ++++++++++++++------- 2 files changed, 114 insertions(+), 21 deletions(-) diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index df8e657..afbfc27 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -157,12 +157,86 @@ async def audit_full_flow_map(request: Request, db=Depends(get_db)): if not user: return RedirectResponse(url="/login") - flows = get_flow_map(db) + domain_filter = request.query_params.get("domain", "") + server_filter = request.query_params.get("server", "").strip() + + # Domaines + zones pour le dropdown + all_domains = db.execute(text( + "SELECT code, name, 'domain' as type FROM domains ORDER BY name" + )).fetchall() + all_zones = db.execute(text( + "SELECT name as code, name, 'zone' as type FROM zones ORDER BY name" + )).fetchall() + + # Serveurs audites pour l'autocompletion + audited_servers = db.execute(text(""" + SELECT DISTINCT hostname FROM server_audit_full WHERE status = 'ok' ORDER BY hostname + """)).fetchall() + + if server_filter: + # Flux pour un serveur specifique (IN + OUT) + flows = db.execute(text(""" + SELECT source_hostname, source_ip, dest_ip, dest_port, + dest_hostname, process_name, direction, state, + COUNT(*) as cnt + FROM network_flow_map nfm + JOIN server_audit_full saf ON nfm.audit_id = saf.id + WHERE saf.id IN ( + SELECT DISTINCT ON (hostname) id FROM server_audit_full + WHERE status = 'ok' ORDER BY hostname, audit_date DESC + ) + AND (nfm.source_hostname = :srv OR nfm.dest_hostname = :srv) + AND nfm.source_hostname != nfm.dest_hostname + AND nfm.dest_hostname IS NOT NULL + GROUP BY source_hostname, source_ip, dest_ip, dest_port, + dest_hostname, process_name, direction, state + ORDER BY source_hostname + """), {"srv": server_filter}).fetchall() + elif domain_filter: + # Flux pour un domaine ou une zone + # D'abord chercher comme zone + hostnames = [r.hostname for r in db.execute(text(""" + SELECT s.hostname FROM servers s + JOIN zones z ON s.zone_id = z.id WHERE z.name = :name + """), {"name": domain_filter}).fetchall()] + if not hostnames: + # Sinon comme domaine + hostnames = [r.hostname for r in db.execute(text(""" + SELECT s.hostname FROM servers s + JOIN domain_environments de ON s.domain_env_id = de.id + JOIN domains d ON de.domain_id = d.id WHERE d.code = :dc + """), {"dc": domain_filter}).fetchall()] + if hostnames: + flows = db.execute(text(""" + SELECT source_hostname, source_ip, dest_ip, dest_port, + dest_hostname, process_name, direction, state, + COUNT(*) as cnt + FROM network_flow_map nfm + JOIN server_audit_full saf ON nfm.audit_id = saf.id + WHERE saf.id IN ( + SELECT DISTINCT ON (hostname) id FROM server_audit_full + WHERE status = 'ok' ORDER BY hostname, audit_date DESC + ) + AND (nfm.source_hostname = ANY(:hosts) OR nfm.dest_hostname = ANY(:hosts)) + AND nfm.source_hostname != COALESCE(nfm.dest_hostname, '') + AND nfm.dest_hostname IS NOT NULL + GROUP BY source_hostname, source_ip, dest_ip, dest_port, + dest_hostname, process_name, direction, state + ORDER BY source_hostname + """), {"hosts": hostnames}).fetchall() + else: + flows = [] + else: + flows = get_flow_map(db) + app_map = get_app_map(db) ctx = base_context(request, db, user) ctx.update({ "app_name": APP_NAME, "flows": flows, "app_map": app_map, + "all_domains": all_domains, "all_zones": all_zones, + "audited_servers": audited_servers, + "domain_filter": domain_filter, "server_filter": server_filter, }) return templates.TemplateResponse("audit_full_flowmap.html", ctx) diff --git a/app/templates/audit_full_flowmap.html b/app/templates/audit_full_flowmap.html index c4ceef2..d3e0783 100644 --- a/app/templates/audit_full_flowmap.html +++ b/app/templates/audit_full_flowmap.html @@ -4,14 +4,35 @@ < Retour

Carte des flux reseau

-
- - {% for d in all_domains %}{% endfor %} + + {% for z in all_zones %}{% endfor %} + + + {% for d in all_domains %}{% endfor %} + - - -
+
+ + + {% for s in audited_servers %} +
+ + {% if domain_filter or server_filter %}Reset{% endif %} + + + {% if server_filter %}Serveur: {{ server_filter }} + {% elif domain_filter %}Domaine/Zone: {{ domain_filter }} + {% else %}Vue globale{% endif %} +
@@ -190,10 +211,11 @@ function render() { g.setAttribute("transform", "translate(" + n.x + "," + n.y + ")"); g.style.cursor = "pointer"; + var isTarget = (serverFilter && n.id === serverFilter); var circle = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - circle.setAttribute("r", "6"); - circle.setAttribute("fill", "#22c55e"); - circle.setAttribute("stroke", "#0a0e17"); + circle.setAttribute("r", isTarget ? "10" : "6"); + circle.setAttribute("fill", isTarget ? "#facc15" : "#22c55e"); + circle.setAttribute("stroke", isTarget ? "#facc15" : "#0a0e17"); circle.setAttribute("stroke-width", "2"); g.appendChild(circle); @@ -247,17 +269,14 @@ function resetZoom() { svg.setAttribute("viewBox", "0 0 " + W + " " + H); } -function filterGraph() { - var search = document.getElementById("filter-search").value.toLowerCase(); - var domain = document.getElementById("filter-domain").value; - // Pour le filtre domaine, on filtre cote client par prefix hostname (approximation) - nodes.forEach(function(n) { - n.hidden = false; - if (search && n.id.toLowerCase().indexOf(search) === -1) n.hidden = true; - }); - render(); - var vis = nodes.filter(function(n) { return !n.hidden; }).length; - document.getElementById("stats-nodes").textContent = vis + " serveurs"; +// Highlight du serveur filtre +var serverFilter = "{{ server_filter }}"; +if (serverFilter && nodeIdx[serverFilter] !== undefined) { + var targetNode = nodes[nodeIdx[serverFilter]]; + // Centrer la vue sur ce noeud + viewBox.x = targetNode.x - W / 2; + viewBox.y = targetNode.y - H / 2; + svg.setAttribute("viewBox", viewBox.x + " " + viewBox.y + " " + viewBox.w + " " + viewBox.h); } {% endblock %}