diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index d3e8eed..215552a 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -224,6 +224,112 @@ async def audit_full_import(request: Request, db=Depends(get_db), ) +@router.get("/audit-full/patching", response_class=HTMLResponse) +async def audit_full_patching(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + + year = int(request.query_params.get("year", "2026")) + search = request.query_params.get("q", "").strip() + domain = request.query_params.get("domain", "") + page = int(request.query_params.get("page", "1")) + sort = request.query_params.get("sort", "hostname") + sort_dir = request.query_params.get("dir", "asc") + per_page = 30 + + 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 = db.execute(text( + f"SELECT COUNT(*) as total," + f" COUNT(*) FILTER (WHERE {yr_count} >= 1) as patched," + f" COUNT(*) FILTER (WHERE {yr_count} = 1) as once," + 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 = 'ok'" + f" AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + )).fetchone() + + patch_by_domain = db.execute(text( + f"SELECT d.name as domain, d.code," + f" COUNT(DISTINCT saf.hostname) as total," + f" COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.{yr_count} >= 1) as patched," + f" COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.{yr_count} >= 2) as twice," + f" COUNT(DISTINCT saf.hostname) 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" JOIN domain_environments de ON s.domain_env_id = de.id JOIN domains d ON de.domain_id = d.id" + f" WHERE saf.status = 'ok'" + f" AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + f" GROUP BY d.name, d.code, d.display_order ORDER BY d.display_order" + )).fetchall() + + patch_weekly = [] + if year == 2026: + patch_weekly = db.execute(text( + "SELECT last_patch_week as week, COUNT(*) as cnt FROM server_audit_full" + " WHERE status = 'ok' AND last_patch_year = 2026 AND last_patch_week IS NOT NULL" + " AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)" + " GROUP BY last_patch_week ORDER BY last_patch_week" + )).fetchall() + + 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() + + servers = db.execute(text( + f"SELECT DISTINCT ON (saf.hostname) saf.id, saf.hostname, saf.os_release," + f" saf.last_patch_date, saf.last_patch_week, saf.last_patch_year," + f" saf.{yr_count} as patch_count, saf.{yr_weeks} as patch_weeks," + f" d.name as domain, e.name as env, z.name as zone" + f" FROM server_audit_full saf" + f" LEFT JOIN servers s ON saf.server_id = s.id" + f" LEFT JOIN domain_environments de ON s.domain_env_id = de.id" + f" LEFT JOIN domains d ON de.domain_id = d.id" + f" LEFT JOIN environments e ON de.environment_id = e.id" + f" LEFT JOIN zones z ON s.zone_id = z.id" + f" WHERE saf.status = 'ok'" + f" ORDER BY saf.hostname, saf.audit_date DESC" + )).fetchall() + + if domain: + zone_hosts = {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 = :n" + ), {"n": domain}).fetchall()} + if zone_hosts: + servers = [s for s in servers if s.hostname in zone_hosts] + else: + dom_hosts = {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}).fetchall()} + servers = [s for s in servers if s.hostname in dom_hosts] + if search: + servers = [s for s in servers if search.lower() in s.hostname.lower()] + + if sort == "hostname": + servers.sort(key=lambda s: s.hostname.lower(), reverse=(sort_dir == "desc")) + elif sort == "count": + servers.sort(key=lambda s: s.patch_count or 0, reverse=(sort_dir == "desc")) + elif sort == "last": + servers.sort(key=lambda s: s.last_patch_week or "", reverse=(sort_dir == "desc")) + + total_filtered = len(servers) + total_pages = max(1, (total_filtered + per_page - 1) // per_page) + page = max(1, min(page, total_pages)) + servers_page = servers[(page - 1) * per_page : page * per_page] + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, "year": year, "kpis": kpis, + "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, + "page": page, "total_pages": total_pages, "total_filtered": total_filtered, + }) + return templates.TemplateResponse("audit_full_patching.html", ctx) + + @router.get("/audit-full/export-csv") async def audit_full_export_csv(request: Request, db=Depends(get_db)): user = get_current_user(request) diff --git a/app/templates/audit_full_patching.html b/app/templates/audit_full_patching.html new file mode 100644 index 0000000..c4c1e3f --- /dev/null +++ b/app/templates/audit_full_patching.html @@ -0,0 +1,134 @@ +{% extends 'base.html' %} +{% block title %}Patching {{ year }}{% endblock %} +{% block content %} +
+
+ < Audit complet +

Patching {{ year }}

+
+
+ 2025 + 2026 +
+
+ + +{% if kpis %} +{% set pct = (kpis.patched / kpis.total * 100)|int if kpis.total > 0 else 0 %} +
+
{{ kpis.total }}
Total
+
{{ kpis.patched }}
Patches
+
{{ kpis.once }}
1 fois
+
{{ kpis.twice }}
2+ fois
+
{{ kpis.thrice }}
3+ fois
+
{{ kpis.never }}
Jamais
+
+
{{ pct }}%
+
Couverture
+
+
+
+
+
+ + +
+ {% if patch_weekly %} +
+
Derniere semaine de patch par serveur
+
+ {% set max_cnt = patch_weekly|map(attribute='cnt')|max %} + {% for w in patch_weekly %} +
+
{{ w.cnt }}
+
+
{{ w.week }}
+
+ {% endfor %} +
+
+ {% endif %} +
+
Par domaine
+ + + + {% for d in patch_by_domain %} + {% set dp = (d.patched / d.total * 100)|int if d.total > 0 else 0 %} + + + + + + + + + {% endfor %} + +
DomaineTotalOK2xJamais%
{{ d.domain }}{{ d.total }}{{ d.patched }}{{ d.twice }}{{ d.never }} +
+
+
+ {{ dp }}% +
+
+
+{% endif %} + + +
+
+ + + + + {% if search or domain %}Reset{% endif %} +
+ {{ total_filtered }} serveur(s) +
+ + +{% set qs %}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}{% endset %} +
+ + + + + + + + + + + + {% for s in servers %} + + + + + + + + + + {% endfor %} + +
Hostname {% if sort == 'hostname' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}DomaineEnvZoneNb patches {% if sort == 'count' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}SemainesDernier {% if sort == 'last' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}
{{ s.hostname }}{{ s.domain or '-' }}{{ (s.env or '-')[:6] }}{% if s.zone == 'DMZ' %}DMZ{% else %}{{ s.zone or '-' }}{% endif %}{{ s.patch_count or 0 }}{% if s.patch_weeks %}{% for w in s.patch_weeks.split(',') %}{{ w }}{% endfor %}{% else %}-{% endif %}{% if s.last_patch_date %}{{ s.last_patch_date }}{% elif s.last_patch_week %}{{ s.last_patch_week }}{% else %}-{% endif %}
+ + {% if total_pages > 1 %} + {% set pqs %}&year={{ year }}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}{% if sort %}&sort={{ sort }}&dir={{ sort_dir }}{% endif %}{% endset %} +
+ Page {{ page }}/{{ total_pages }} +
+ {% if page > 1 %}1{% if page > 2 %}<{% endif %}{% endif %} + {{ page }} + {% if page < total_pages %}>{{ total_pages }}{% endif %} +
+
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index d01eab5..1f71eb7 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -64,6 +64,7 @@ {% if p.audit %}Audit{% endif %} {% if p.audit in ('edit', 'admin') %}Spécifique{% endif %} {% if p.audit %}Complet{% endif %} + {% if p.audit %}Patching{% endif %} {% if p.servers %}Contacts{% endif %} {% if p.users %}Utilisateurs{% endif %} {% if p.settings %}Settings{% endif %}