diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 8ce0a2c..7ca1ea1 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -15,7 +15,7 @@ async def dashboard(request: Request, db=Depends(get_db)): if not user: return RedirectResponse(url="/login") - # Stats + # Stats generales stats = {} stats["total_servers"] = db.execute(text("SELECT COUNT(*) FROM servers")).scalar() stats["patchable"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE patch_os_owner='secops' AND etat='en_production'")).scalar() @@ -42,7 +42,80 @@ async def dashboard(request: Request, db=Depends(get_db)): # Par tier tiers = db.execute(text("SELECT tier, COUNT(*) FROM servers GROUP BY tier ORDER BY tier")).fetchall() + # ── Stats patching 2026 ── + patch_stats = db.execute(text(""" + SELECT + COUNT(*) as audited, + COUNT(*) FILTER (WHERE last_patch_year = 2026) as patched_2026, + COUNT(*) FILTER (WHERE last_patch_year = 2025) as patched_2025_only, + COUNT(*) FILTER (WHERE last_patch_year IS NULL OR last_patch_week IS NULL) as never_patched, + COUNT(*) FILTER (WHERE patch_count_2026 >= 1) as patched_once, + COUNT(*) FILTER (WHERE patch_count_2026 >= 2) as patched_twice, + COUNT(*) FILTER (WHERE patch_count_2026 >= 3) as patched_thrice, + COUNT(*) FILTER (WHERE reboot_required = true) as needs_reboot + FROM server_audit_full + WHERE status = 'ok' + AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) + """)).fetchone() + + # Frequence patching par semaine 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() + + # Patching par domaine + patch_by_domain = db.execute(text(""" + SELECT d.name as domain, d.code, + COUNT(DISTINCT saf.hostname) as total, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.patch_count_2026 >= 2) as patched_twice, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year IS NULL) as never + FROM server_audit_full saf + JOIN servers s ON saf.server_id = s.id + JOIN domain_environments de ON s.domain_env_id = de.id + JOIN domains d ON de.domain_id = d.id + WHERE saf.status = 'ok' + AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) + GROUP BY d.name, d.code, d.display_order + ORDER BY d.display_order + """)).fetchall() + + # Patching par environnement + patch_by_env = db.execute(text(""" + SELECT e.name as env, + COUNT(DISTINCT saf.hostname) as total, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched + FROM server_audit_full saf + JOIN servers s ON saf.server_id = s.id + JOIN domain_environments de ON s.domain_env_id = de.id + JOIN environments e ON de.environment_id = e.id + WHERE saf.status = 'ok' + AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) + GROUP BY e.name ORDER BY e.name + """)).fetchall() + + # Patching par zone (DMZ, LAN, EMV) + patch_by_zone = db.execute(text(""" + SELECT z.name as zone, + COUNT(DISTINCT saf.hostname) as total, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year = 2026) as patched, + COUNT(DISTINCT saf.hostname) FILTER (WHERE saf.last_patch_year IS NULL) as never + FROM server_audit_full saf + JOIN servers s ON saf.server_id = s.id + JOIN zones z ON s.zone_id = z.id + WHERE saf.status = 'ok' + AND saf.id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC) + GROUP BY z.name ORDER BY z.name + """)).fetchall() + return templates.TemplateResponse("dashboard.html", { "request": request, "user": user, "app_name": APP_NAME, - "stats": stats, "domains": domains, "tiers": tiers + "stats": stats, "domains": domains, "tiers": tiers, + "patch_stats": patch_stats, "patch_weekly": patch_weekly, + "patch_by_domain": patch_by_domain, "patch_by_env": patch_by_env, + "patch_by_zone": patch_by_zone, }) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 8d1281f..aeb0c2f 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -3,41 +3,153 @@ {% block content %}

Dashboard

- -
-
-
{{ stats.total_servers }}
-
Serveurs
-
-
-
{{ stats.patchable }}
-
Patchables SecOps
-
-
-
{{ stats.linux }} / {{ stats.windows }}
-
Linux / Windows
-
-
-
{{ stats.qualys_tags }}
-
Tags Qualys
-
-
-
{{ stats.eol }}
-
EOL
-
+ +
+
{{ stats.total_servers }}
Serveurs
+
{{ stats.patchable }}
Patchables SecOps
+
{{ stats.linux }} / {{ stats.windows }}
Linux / Windows
+
{{ stats.qualys_tags }}
Tags Qualys
+
{{ stats.eol }}
EOL
- -
-

Par domaine

- - - - - - - - + +{% if patch_stats %} +
+

Patching 2026

+ + +
+ +
{{ patch_stats.audited }}
+
Audites
+
+ +
{{ patch_stats.patched_2026 }}
+
Patches 2026
+
+
+
{{ patch_stats.patched_once }}
+
1+ fois
+
+
+
{{ patch_stats.patched_twice }}
+
2+ fois
+
+
+
{{ patch_stats.patched_thrice }}
+
3+ fois
+
+
+
{{ patch_stats.patched_2025_only }}
+
2025 seul
+
+
+
{{ patch_stats.never_patched }}
+
Jamais
+
+
+
{{ patch_stats.needs_reboot }}
+
Reboot
+
+
+ + + {% set pct = (patch_stats.patched_2026 / patch_stats.audited * 100)|int if patch_stats.audited > 0 else 0 %} +
+
+ Couverture patching 2026 + {{ pct }}% +
+
+
+
+
+ + + {% if patch_weekly %} +
+
Serveurs patches par semaine
+
+ {% set max_cnt = patch_weekly|map(attribute='cnt')|max %} + {% for w in patch_weekly %} +
+
{{ w.cnt }}
+
+
{{ w.week }}
+
+ {% endfor %} +
+
+ {% endif %} + + +
+
+
Par domaine
+
DomaineTotalActifsLinuxWindows
+ + + {% for d in patch_by_domain %} + {% set pct_d = (d.patched / d.total * 100)|int if d.total > 0 else 0 %} + + + + + + + + {% endfor %} + +
DomaineTotalPatche2x%
{{ d.domain }}{{ d.total }}{{ d.patched }}{{ d.patched_twice }} + {{ pct_d }}% +
+
+ +
+ +
Par environnement
+ + + + {% for e in patch_by_env %} + {% set pct_e = (e.patched / e.total * 100)|int if e.total > 0 else 0 %} + + + + + + + {% endfor %} + +
EnvTotalPatche%
{{ e.env }}{{ e.total }}{{ e.patched }}{{ pct_e }}%
+ + +
Par zone reseau
+ + + + {% for z in patch_by_zone %} + {% set pct_z = (z.patched / z.total * 100)|int if z.total > 0 else 0 %} + + + + + + + + {% endfor %} + +
ZoneTotalPatcheJamais%
{{ z.zone }}{{ z.total }}{{ z.patched }}{{ z.never }}{{ pct_z }}%
+
+
+ +{% endif %} + + +
+

Inventaire par domaine

+ + {% for d in domains %} @@ -52,32 +164,27 @@
DomaineTotalActifsLinuxWindows
- -
-

Par tier

-
- {% for t in tiers %} -
-
{{ t[1] }}
-
{{ t[0] }}
+ +
+
+

Par tier

+
+ {% for t in tiers %} +
+
{{ t[1] }}
+
{{ t[0] }}
+
+ {% endfor %}
- {% endfor %}
-
- - -
-
- Decomissionnes - {{ stats.decom }} -
-
- EOL - {{ stats.eol }} -
-
- Assets Qualys - {{ stats.qualys_assets }} +
+

Quick stats

+
+
Decommissionnes{{ stats.decom }}
+
EOL{{ stats.eol }}
+
Assets Qualys{{ stats.qualys_assets }}
+
Tags Qualys{{ stats.qualys_tags }}
+
{% endblock %}