diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index f92984e..ba232b8 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -233,6 +233,7 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): year = int(request.query_params.get("year", "2026")) search = request.query_params.get("q", "").strip() domain = request.query_params.get("domain", "") + scope = request.query_params.get("scope", "") # secops, other, ou vide=tout page = int(request.query_params.get("page", "1")) sort = request.query_params.get("sort", "hostname") sort_dir = request.query_params.get("dir", "asc") @@ -241,6 +242,8 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): yr_count = "patch_count_2026" if year == 2026 else "patch_count_2025" yr_weeks = "patch_weeks_2026" if year == 2026 else "patch_weeks_2025" + # KPIs globaux + secops/autre + _latest = "id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') ORDER BY hostname, audit_date DESC)" kpis = db.execute(text( f"SELECT COUNT(*) as total," f" COUNT(*) FILTER (WHERE {yr_count} >= 1) as patched," @@ -248,8 +251,25 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): f" COUNT(*) FILTER (WHERE {yr_count} >= 2) as twice," f" COUNT(*) FILTER (WHERE {yr_count} >= 3) as thrice," f" COUNT(*) FILTER (WHERE {yr_count} = 0 OR {yr_count} IS NULL) as never" - f" FROM server_audit_full WHERE status IN ('ok','partial')" - f" AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status IN ('ok','partial') ORDER BY hostname, audit_date DESC)" + f" FROM server_audit_full WHERE status IN ('ok','partial') AND {_latest}" + )).fetchone() + + kpis_secops = db.execute(text( + f"SELECT COUNT(*) as total," + f" COUNT(*) FILTER (WHERE saf.{yr_count} >= 1) as patched," + f" COUNT(*) FILTER (WHERE saf.{yr_count} = 0 OR saf.{yr_count} IS NULL) as never" + f" FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id" + f" WHERE saf.status IN ('ok','partial') AND s.patch_os_owner = 'secops'" + f" AND saf.{_latest}" + )).fetchone() + + kpis_other = db.execute(text( + f"SELECT COUNT(*) as total," + f" COUNT(*) FILTER (WHERE saf.{yr_count} >= 1) as patched," + f" COUNT(*) FILTER (WHERE saf.{yr_count} = 0 OR saf.{yr_count} IS NULL) as never" + f" FROM server_audit_full saf JOIN servers s ON saf.server_id = s.id" + f" WHERE saf.status IN ('ok','partial') AND (s.patch_os_owner != 'secops' OR s.patch_os_owner IS NULL)" + f" AND saf.{_latest}" )).fetchone() patch_by_domain = db.execute(text( @@ -307,6 +327,18 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): if search: servers = [s for s in servers if search.lower() in s.hostname.lower()] + # Filtre scope secops / autre + if scope == "secops": + secops_hosts = {r.hostname for r in db.execute(text( + "SELECT hostname FROM servers WHERE patch_os_owner = 'secops'" + )).fetchall()} + servers = [s for s in servers if s.hostname in secops_hosts] + elif scope == "other": + secops_hosts = {r.hostname for r in db.execute(text( + "SELECT hostname FROM servers WHERE patch_os_owner = 'secops'" + )).fetchall()} + servers = [s for s in servers if s.hostname not in secops_hosts] + if sort == "hostname": servers.sort(key=lambda s: s.hostname.lower(), reverse=(sort_dir == "desc")) elif sort == "count": @@ -322,9 +354,11 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): ctx = base_context(request, db, user) ctx.update({ "app_name": APP_NAME, "year": year, "kpis": kpis, + "kpis_secops": kpis_secops, "kpis_other": kpis_other, "patch_by_domain": patch_by_domain, "patch_weekly": patch_weekly, "servers": servers_page, "all_domains": all_domains, "all_zones": all_zones, - "search": search, "domain": domain, "sort": sort, "sort_dir": sort_dir, + "search": search, "domain": domain, "scope": scope, + "sort": sort, "sort_dir": sort_dir, "page": page, "total_pages": total_pages, "total_filtered": total_filtered, }) return templates.TemplateResponse("audit_full_patching.html", ctx) @@ -504,24 +538,33 @@ async def audit_full_detail(request: Request, audit_id: int, db=Depends(get_db)) if not audit: return RedirectResponse(url="/audit-full") - # Flux pour ce serveur - flows = get_flow_map_for_server(db, audit.hostname) + def _j(val, default): + if val is None: return default + if isinstance(val, (list, dict)): return val + try: return json.loads(val) + except: return default + + # Serveur partial (pas encore audite via SSH) + is_partial = (audit.status == "partial") + + flows = [] if is_partial else get_flow_map_for_server(db, audit.hostname) ctx = base_context(request, db, user) ctx.update({ "app_name": APP_NAME, "a": audit, "flows": flows, - "services": audit.services if isinstance(audit.services, list) else json.loads(audit.services or "[]"), - "processes": audit.processes if isinstance(audit.processes, list) else json.loads(audit.processes or "[]"), - "listen_ports": audit.listen_ports if isinstance(audit.listen_ports, list) else json.loads(audit.listen_ports or "[]"), - "connections": audit.connections if isinstance(audit.connections, list) else json.loads(audit.connections or "[]"), - "flux_in": audit.flux_in if isinstance(audit.flux_in, list) else json.loads(audit.flux_in or "[]"), - "flux_out": audit.flux_out if isinstance(audit.flux_out, list) else json.loads(audit.flux_out or "[]"), - "disk_usage": audit.disk_usage if isinstance(audit.disk_usage, list) else json.loads(audit.disk_usage or "[]"), - "interfaces": audit.interfaces if isinstance(audit.interfaces, list) else json.loads(audit.interfaces or "[]"), - "correlation": audit.correlation_matrix if isinstance(audit.correlation_matrix, list) else json.loads(audit.correlation_matrix or "[]"), - "outbound": audit.outbound_only if isinstance(audit.outbound_only, list) else json.loads(audit.outbound_only or "[]"), - "firewall": audit.firewall if isinstance(audit.firewall, dict) else json.loads(audit.firewall or "{}"), - "conn_wait": audit.conn_wait if isinstance(audit.conn_wait, list) else json.loads(audit.conn_wait or "[]"), - "traffic": audit.traffic if isinstance(audit.traffic, list) else json.loads(audit.traffic or "[]"), + "is_partial": is_partial, + "services": _j(audit.services, []), + "processes": _j(audit.processes, []), + "listen_ports": _j(audit.listen_ports, []), + "connections": _j(audit.connections, []), + "flux_in": _j(audit.flux_in, []), + "flux_out": _j(audit.flux_out, []), + "disk_usage": _j(audit.disk_usage, []), + "interfaces": _j(audit.interfaces, []), + "correlation": _j(audit.correlation_matrix, []), + "outbound": _j(audit.outbound_only, []), + "firewall": _j(audit.firewall, {}), + "conn_wait": _j(audit.conn_wait, []), + "traffic": _j(audit.traffic, []), }) return templates.TemplateResponse("audit_full_detail.html", ctx) diff --git a/app/templates/audit_full_detail.html b/app/templates/audit_full_detail.html index 481ad64..e3f8eca 100644 --- a/app/templates/audit_full_detail.html +++ b/app/templates/audit_full_detail.html @@ -14,6 +14,14 @@ +{% if is_partial %} +
Ce serveur n'a pas encore ete audite via SSH (Windows, EMV...)
+{{ a.hostname }} — {{ a.os_release or 'OS inconnu' }}
+ {% if a.last_patch_week %}Dernier patch : {{ a.last_patch_week }} {{ a.last_patch_year }}
{% endif %} +