Dashboard: KPIs DMZ + patching 2026 depuis patch_history

- Stats DMZ (cliquable vers filtre zone)
- Patched 2026, never patched, last week (depuis patch_history Excel)
- Couverture patching = patched / patchable
- KPIs cards cliquables (lien vers /servers filtre pre-applique)
- Fix alias stats.eol -> stats.obsolete
This commit is contained in:
Pierre & Lumière 2026-04-14 21:45:36 +02:00
parent ec82a7cd1e
commit 6ec1c4575d
2 changed files with 37 additions and 5 deletions

View File

@ -28,6 +28,27 @@ async def dashboard(request: Request, db=Depends(get_db)):
stats["qualys_active"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%active%' AND agent_status NOT ILIKE '%inactive%'")).scalar() stats["qualys_active"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%active%' AND agent_status NOT ILIKE '%inactive%'")).scalar()
stats["qualys_inactive"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%inactive%'")).scalar() stats["qualys_inactive"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%inactive%'")).scalar()
stats["qualys_no_agent"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='Production' AND NOT EXISTS (SELECT 1 FROM qualys_assets qa WHERE LOWER(qa.hostname) = LOWER(servers.hostname))")).scalar() stats["qualys_no_agent"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='Production' AND NOT EXISTS (SELECT 1 FROM qualys_assets qa WHERE LOWER(qa.hostname) = LOWER(servers.hostname))")).scalar()
# Alias : template utilise stats.eol
stats["eol"] = stats["obsolete"]
# Zone DMZ
stats["dmz"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE zone_id = (SELECT id FROM zones WHERE is_dmz=true LIMIT 1)")).scalar()
# Patching depuis patch_history (Excel 2026)
stats["patched_history_2026"] = db.execute(text(
"SELECT COUNT(DISTINCT server_id) FROM patch_history WHERE EXTRACT(YEAR FROM date_patch)=2026"
)).scalar()
stats["patch_events_2026"] = db.execute(text(
"SELECT COUNT(*) FROM patch_history WHERE EXTRACT(YEAR FROM date_patch)=2026"
)).scalar()
stats["never_patched_2026"] = db.execute(text("""
SELECT COUNT(*) FROM servers s
WHERE s.etat='Production' AND s.patch_os_owner='secops'
AND NOT EXISTS (SELECT 1 FROM patch_history ph
WHERE ph.server_id=s.id AND EXTRACT(YEAR FROM ph.date_patch)=2026)
""")).scalar()
# Semaine la plus recente
stats["last_patch_week"] = db.execute(text(
"SELECT MAX(TO_CHAR(date_patch, 'IW')) FROM patch_history WHERE EXTRACT(YEAR FROM date_patch)=2026"
)).scalar()
# Par domaine # Par domaine
domains = db.execute(text(""" domains = db.execute(text("""

View File

@ -4,15 +4,26 @@
<h2 class="text-xl font-bold text-cyber-accent mb-4">Dashboard</h2> <h2 class="text-xl font-bold text-cyber-accent mb-4">Dashboard</h2>
<!-- KPIs generaux --> <!-- KPIs generaux -->
<div style="display:flex;flex-wrap:nowrap;gap:8px;margin-bottom:16px;"> <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;">
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-accent">{{ stats.total_servers }}</div><div class="text-xs text-gray-500">Serveurs</div></div> <a href="/servers" class="card p-3 text-center hover:bg-cyber-hover" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-accent">{{ stats.total_servers }}</div><div class="text-xs text-gray-500">Serveurs</div></a>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-green">{{ stats.patchable }}</div><div class="text-xs text-gray-500">Patchables SecOps</div></div> <a href="/servers?owner=secops&etat=Production" class="card p-3 text-center hover:bg-cyber-hover" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-green">{{ stats.patchable }}</div><div class="text-xs text-gray-500">Patchables SecOps</div></a>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-white">{{ stats.linux }} / {{ stats.windows }}</div><div class="text-xs text-gray-500">Linux / Windows</div></div> <div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-white">{{ stats.linux }} / {{ stats.windows }}</div><div class="text-xs text-gray-500">Linux / Windows</div></div>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-green">{{ stats.qualys_active }}</div><div class="text-xs text-gray-500">Agents actifs</div></div> <a href="/servers?zone=DMZ" class="card p-3 text-center hover:bg-cyber-hover" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-red">{{ stats.dmz }}</div><div class="text-xs text-gray-500">DMZ</div></a>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold {% if stats.qualys_no_agent > 0 %}text-cyber-red{% else %}text-cyber-green{% endif %}">{{ stats.qualys_no_agent }}</div><div class="text-xs text-gray-500">Sans agent</div></div> <a href="/qualys/agents" class="card p-3 text-center hover:bg-cyber-hover" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-green">{{ stats.qualys_active }}</div><div class="text-xs text-gray-500">Agents actifs</div></a>
<a href="/qualys/agents" class="card p-3 text-center hover:bg-cyber-hover" style="flex:1;min-width:0"><div class="text-2xl font-bold {% if stats.qualys_no_agent > 0 %}text-cyber-red{% else %}text-cyber-green{% endif %}">{{ stats.qualys_no_agent }}</div><div class="text-xs text-gray-500">Sans agent</div></a>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-red">{{ stats.eol }}</div><div class="text-xs text-gray-500">EOL</div></div> <div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-red">{{ stats.eol }}</div><div class="text-xs text-gray-500">EOL</div></div>
</div> </div>
<!-- KPI Patching depuis patch_history (Excel Plan de Patching 2026) -->
<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:16px;">
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-green">{{ stats.patched_history_2026 }}</div><div class="text-xs text-gray-500">Serveurs patchés 2026</div></div>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-accent">{{ stats.patch_events_2026 }}</div><div class="text-xs text-gray-500">Events patching 2026</div></div>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold {% if stats.never_patched_2026 > 0 %}text-cyber-red{% else %}text-cyber-green{% endif %}">{{ stats.never_patched_2026 }}</div><div class="text-xs text-gray-500">Jamais patchés 2026 (prod)</div></div>
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-yellow">S{{ stats.last_patch_week or '-' }}</div><div class="text-xs text-gray-500">Dernière semaine</div></div>
{% set pct_patched = (stats.patched_history_2026 / stats.patchable * 100)|int if stats.patchable > 0 else 0 %}
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold {% if pct_patched >= 80 %}text-cyber-green{% elif pct_patched >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %}">{{ pct_patched }}%</div><div class="text-xs text-gray-500">Couverture 2026</div></div>
</div>
<!-- ═══════ PATCHING 2026 ═══════ --> <!-- ═══════ PATCHING 2026 ═══════ -->
{% if patch_stats %} {% if patch_stats %}
<div class="card p-4 mb-4"> <div class="card p-4 mb-4">