213 lines
17 KiB
HTML
213 lines
17 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Patching {{ year }}{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div>
|
|
<a href="/audit-full" class="text-xs text-gray-500 hover:text-gray-300">< Audit complet</a>
|
|
<h2 class="text-xl font-bold text-cyber-accent">Patching {{ year }}</h2>
|
|
</div>
|
|
<div class="flex gap-2 items-center">
|
|
<a href="/audit-full/patching?year=2025" class="btn-sm {% if year == 2025 %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">2025</a>
|
|
<a href="/audit-full/patching?year=2026" class="btn-sm {% if year == 2026 %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">2026</a>
|
|
<span class="text-gray-600 mx-1">|</span>
|
|
<a href="/audit-full/patching?year={{ year }}" class="btn-sm {% if not scope %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">Tous</a>
|
|
<a href="/audit-full/patching?year={{ year }}&scope=secops" class="btn-sm {% if scope == 'secops' %}bg-cyber-green text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">SecOps</a>
|
|
<a href="/audit-full/patching?year={{ year }}&scope=other" class="btn-sm {% if scope == 'other' %}bg-cyber-yellow text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">Hors SecOps</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KPIs par perimetre -->
|
|
{% if kpis_secops and kpis_other %}
|
|
<div style="display:flex;flex-wrap:nowrap;gap:6px;margin-bottom:8px;">
|
|
<a href="/audit-full/patching?year={{ year }}&scope=secops" class="card p-2 text-center hover:bg-cyber-hover {% if scope == 'secops' %}ring-1 ring-cyber-green{% endif %}" style="flex:1;min-width:0;background:#111827;">
|
|
<div class="text-xs text-gray-500 mb-1">SecOps</div>
|
|
<div class="text-lg font-bold text-cyber-green">{{ kpis_secops.patched }}<span class="text-gray-500 text-xs">/{{ kpis_secops.total }}</span></div>
|
|
{% set pct_s = (kpis_secops.patched / kpis_secops.total * 100)|int if kpis_secops.total > 0 else 0 %}
|
|
<div style="height:4px;background:#1f2937;border-radius:2px;margin-top:4px;"><div style="height:100%;width:{{ pct_s }}%;background:#22c55e;border-radius:2px;"></div></div>
|
|
<div style="font-size:10px;" class="{% if pct_s >= 80 %}text-cyber-green{% else %}text-cyber-yellow{% endif %} mt-1">{{ pct_s }}%</div>
|
|
</a>
|
|
<a href="/audit-full/patching?year={{ year }}&scope=other" class="card p-2 text-center hover:bg-cyber-hover {% if scope == 'other' %}ring-1 ring-cyber-yellow{% endif %}" style="flex:1;min-width:0;background:#111827;">
|
|
<div class="text-xs text-gray-500 mb-1">Hors SecOps</div>
|
|
<div class="text-lg font-bold text-cyber-yellow">{{ kpis_other.patched }}<span class="text-gray-500 text-xs">/{{ kpis_other.total }}</span></div>
|
|
{% set pct_o = (kpis_other.patched / kpis_other.total * 100)|int if kpis_other.total > 0 else 0 %}
|
|
<div style="height:4px;background:#1f2937;border-radius:2px;margin-top:4px;"><div style="height:100%;width:{{ pct_o }}%;background:#eab308;border-radius:2px;"></div></div>
|
|
<div style="font-size:10px;" class="{% if pct_o >= 80 %}text-cyber-green{% else %}text-cyber-yellow{% endif %} mt-1">{{ pct_o }}%</div>
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- KPIs globaux -->
|
|
{% if kpis %}
|
|
{% set pct = (kpis.patched / kpis.total * 100)|int if kpis.total > 0 else 0 %}
|
|
<div style="display:flex;flex-wrap:nowrap;gap:6px;margin-bottom:12px;">
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-cyber-accent">{{ kpis.total }}</div><div style="font-size:10px;" class="text-gray-500">Total</div></div>
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-cyber-green">{{ kpis.patched }}</div><div style="font-size:10px;" class="text-gray-500">Patchés</div></div>
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-green-300">{{ kpis.once }}</div><div style="font-size:10px;" class="text-gray-500">1 fois</div></div>
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-blue-400">{{ kpis.twice }}</div><div style="font-size:10px;" class="text-gray-500">2+ fois</div></div>
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-purple-400">{{ kpis.thrice }}</div><div style="font-size:10px;" class="text-gray-500">3+ fois</div></div>
|
|
<div class="card p-2 text-center" style="flex:1;min-width:0"><div class="text-xl font-bold text-cyber-red">{{ kpis.never }}</div><div style="font-size:10px;" class="text-gray-500">Jamais</div></div>
|
|
<div class="card p-2 text-center" style="flex:2;min-width:0">
|
|
<div class="text-xl font-bold {% if pct >= 80 %}text-cyber-green{% elif pct >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %}">{{ pct }}%</div>
|
|
<div style="font-size:10px;" class="text-gray-500">Couverture</div>
|
|
<div style="height:4px;background:#1f2937;border-radius:2px;margin-top:4px;">
|
|
<div style="height:100%;width:{{ pct }}%;background:{% if pct >= 80 %}#22c55e{% elif pct >= 50 %}#eab308{% else %}#ef4444{% endif %};border-radius:2px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparaison Y-1 -->
|
|
{% if compare and year == 2026 %}
|
|
{% set pct_current = (compare.current_patched / compare.current_total * 100)|int if compare.current_total > 0 else 0 %}
|
|
{% set pct_prev_same = (compare.prev_at_same_week / compare.prev_total * 100)|int if compare.prev_total > 0 else 0 %}
|
|
{% set pct_prev_total = (compare.prev_year_total / compare.current_total * 100)|int if compare.current_total > 0 else 0 %}
|
|
{% set diff_same = pct_current - pct_prev_same %}
|
|
<div class="card p-3 mb-4">
|
|
<div class="text-xs text-gray-500 mb-2">
|
|
Comparaison à même semaine (S{{ compare.compare_week }})
|
|
{% if not compare.prev_data_ok %}<span class="text-cyber-yellow ml-2">Données 2025 incomplètes</span>{% endif %}
|
|
</div>
|
|
<div style="display:flex;gap:12px;align-items:center;">
|
|
<!-- 2026 en cours -->
|
|
<div style="flex:1;">
|
|
<div class="flex justify-between text-xs mb-1">
|
|
<span class="text-cyber-accent font-bold">2026 (S{{ compare.compare_week }})</span>
|
|
<span class="font-bold {% if pct_current >= 80 %}text-cyber-green{% elif pct_current >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %}">{{ compare.current_patched }} / {{ compare.current_total }} ({{ pct_current }}%)</span>
|
|
</div>
|
|
<div style="height:10px;background:#1f2937;border-radius:4px;overflow:hidden;">
|
|
<div style="height:100%;width:{{ pct_current }}%;background:#22c55e;border-radius:4px;"></div>
|
|
</div>
|
|
</div>
|
|
<!-- 2025 meme semaine -->
|
|
<div style="flex:1;">
|
|
<div class="flex justify-between text-xs mb-1">
|
|
<span class="text-gray-400">2025 (S{{ compare.compare_week }})</span>
|
|
<span class="text-gray-400">{{ compare.prev_at_same_week }} / {{ compare.prev_total }} ({{ pct_prev_same }}%)</span>
|
|
</div>
|
|
<div style="height:10px;background:#1f2937;border-radius:4px;overflow:hidden;">
|
|
<div style="height:100%;width:{{ pct_prev_same }}%;background:#6b7280;border-radius:4px;"></div>
|
|
</div>
|
|
</div>
|
|
<!-- Ecart -->
|
|
<div style="min-width:110px;text-align:center;">
|
|
<div class="text-lg font-bold {% if diff_same >= 0 %}text-cyber-green{% else %}text-cyber-red{% endif %}">
|
|
{% if diff_same >= 0 %}+{% endif %}{{ diff_same }} pts
|
|
</div>
|
|
<div style="font-size:10px;" class="text-gray-500">vs 2025 même semaine</div>
|
|
</div>
|
|
</div>
|
|
<!-- Ligne 2025 total -->
|
|
<div class="mt-2 flex justify-between text-xs text-gray-500">
|
|
<span>2025 année complète : {{ compare.prev_year_total }} patchés ({{ pct_prev_total }}%)</span>
|
|
<span>Objectif 2026 : dépasser {{ compare.prev_year_total }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Graphe + domaines -->
|
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
|
{% if patch_weekly %}
|
|
<div class="card p-3">
|
|
<div class="text-xs text-gray-500 mb-2">Serveurs par semaine <span class="text-cyber-green">vert=patché</span> <span class="text-cyber-red">rouge=annulé/reporté</span></div>
|
|
<div style="display:flex;align-items:flex-end;gap:2px;height:100px;">
|
|
{% set max_cnt = patch_weekly|map(attribute='patched')|map('int')|max %}
|
|
{% for w in patch_weekly %}
|
|
{% set total = (w.patched|int) + (w.cancelled|int) %}
|
|
{% set max_total = max_cnt if max_cnt > 0 else 1 %}
|
|
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;" title="{{ w.week }}: {{ w.patched }} patchés, {{ w.cancelled }} annulés">
|
|
<div style="font-size:8px;color:#94a3b8;">{{ total }}</div>
|
|
<div style="width:100%;display:flex;flex-direction:column;justify-content:flex-end;height:{{ (total / max_total * 100)|int }}%;min-height:2px;">
|
|
{% if w.cancelled|int > 0 %}<div style="width:100%;background:#ef4444;min-height:2px;height:{{ (w.cancelled|int / total * 100)|int }}%;opacity:0.8;border-radius:2px 2px 0 0;"></div>{% endif %}
|
|
<div style="width:100%;background:#22c55e;min-height:2px;flex:1;opacity:0.8;{% if w.cancelled|int == 0 %}border-radius:2px 2px 0 0;{% endif %}"></div>
|
|
</div>
|
|
<div style="font-size:7px;color:#6b7280;margin-top:2px;transform:rotate(-45deg);white-space:nowrap;">{{ w.week }}</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<div class="card p-3">
|
|
<div class="text-xs text-gray-500 mb-2">Par domaine</div>
|
|
<table class="w-full text-xs">
|
|
<thead><tr><th class="text-left p-1">Domaine</th><th class="p-1">Total</th><th class="p-1">OK</th><th class="p-1">2x</th><th class="p-1">Jamais</th><th class="p-1">%</th></tr></thead>
|
|
<tbody>
|
|
{% for d in patch_by_domain %}
|
|
{% set dp = (d.patched / d.total * 100)|int if d.total > 0 else 0 %}
|
|
<tr>
|
|
<td class="p-1"><a href="/audit-full/patching?year={{ year }}&domain={{ d.code }}" class="hover:text-cyber-accent">{{ d.domain }}</a></td>
|
|
<td class="p-1 text-center"><a href="/audit-full/patching?year={{ year }}&domain={{ d.code }}" class="hover:text-cyber-accent">{{ d.total }}</a></td>
|
|
<td class="p-1 text-center"><a href="/audit-full/patching?year={{ year }}&domain={{ d.code }}&sort=count&dir=desc" class="text-cyber-green hover:underline">{{ d.patched }}</a></td>
|
|
<td class="p-1 text-center"><a href="/audit-full/patching?year={{ year }}&domain={{ d.code }}&sort=count&dir=desc" class="text-blue-400 hover:underline">{{ d.twice }}</a></td>
|
|
<td class="p-1 text-center"><a href="/audit-full/patching?year={{ year }}&domain={{ d.code }}&sort=count&dir=asc" class="text-cyber-red hover:underline">{{ d.never }}</a></td>
|
|
<td class="p-1 text-center">
|
|
<div style="display:inline-block;width:40px;height:6px;background:#1f2937;border-radius:3px;vertical-align:middle;">
|
|
<div style="height:100%;width:{{ dp }}%;background:{% if dp >= 80 %}#22c55e{% elif dp >= 50 %}#eab308{% else %}#ef4444{% endif %};border-radius:3px;"></div>
|
|
</div>
|
|
<span class="{% if dp >= 80 %}text-cyber-green{% elif dp >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %} font-bold ml-1">{{ dp }}%</span>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Filtres -->
|
|
<div class="card p-3 mb-4 flex gap-3 items-center flex-wrap">
|
|
<form method="GET" action="/audit-full/patching" class="flex gap-2 items-center flex-1">
|
|
<input type="hidden" name="year" value="{{ year }}">
|
|
{% if scope %}<input type="hidden" name="scope" value="{{ scope }}">{% endif %}
|
|
<input type="text" name="q" value="{{ search }}" placeholder="Rechercher..." class="text-xs py-1 px-2 flex-1 font-mono">
|
|
<select name="domain" class="text-xs py-1 px-2" onchange="this.form.submit()">
|
|
<option value="">Tous</option>
|
|
<optgroup label="Zones">{% for z in all_zones %}<option value="{{ z.code }}" {% if domain == z.code %}selected{% endif %}>{{ z.name }}</option>{% endfor %}</optgroup>
|
|
<optgroup label="Domaines">{% for d in all_domains %}<option value="{{ d.code }}" {% if domain == d.code %}selected{% endif %}>{{ d.name }}</option>{% endfor %}</optgroup>
|
|
</select>
|
|
<button type="submit" class="btn-primary px-3 py-1 text-xs">Filtrer</button>
|
|
{% if search or domain %}<a href="/audit-full/patching?year={{ year }}" class="text-xs text-gray-400">Reset</a>{% endif %}
|
|
</form>
|
|
<span class="text-xs text-gray-500">{{ total_filtered }} serveur(s)</span>
|
|
</div>
|
|
|
|
<!-- Tableau serveurs -->
|
|
{% set qs %}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}{% if scope %}&scope={{ scope }}{% endif %}{% endset %}
|
|
<div class="card overflow-x-auto">
|
|
<table class="w-full table-cyber text-xs">
|
|
<thead><tr>
|
|
<th class="text-left p-2"><a href="/audit-full/patching?year={{ year }}&sort=hostname&dir={% if sort == 'hostname' and sort_dir == 'asc' %}desc{% else %}asc{% endif %}{{ qs }}" class="hover:text-cyber-accent">Hostname {% if sort == 'hostname' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}</a></th>
|
|
<th class="p-2">Domaine</th>
|
|
<th class="p-2">Env</th>
|
|
<th class="p-2">Zone</th>
|
|
<th class="p-2"><a href="/audit-full/patching?year={{ year }}&sort=count&dir={% if sort == 'count' and sort_dir == 'desc' %}asc{% else %}desc{% endif %}{{ qs }}" class="hover:text-cyber-accent">Nb patchs {% if sort == 'count' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}</a></th>
|
|
<th class="text-left p-2">Semaines</th>
|
|
<th class="p-2"><a href="/audit-full/patching?year={{ year }}&sort=last&dir={% if sort == 'last' and sort_dir == 'desc' %}asc{% else %}desc{% endif %}{{ qs }}" class="hover:text-cyber-accent">Dernier {% if sort == 'last' %}{{ '▲' if sort_dir == 'asc' else '▼' }}{% endif %}</a></th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for s in servers %}
|
|
<tr class="hover:bg-cyber-hover cursor-pointer" onclick="location.href='/audit-full/{{ s.id }}'">
|
|
<td class="p-2 font-mono text-cyber-accent font-bold">{{ s.hostname }}</td>
|
|
<td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td>
|
|
<td class="p-2 text-center"><span class="badge {% if s.env == 'Production' %}badge-green{% elif s.env == 'Recette' %}badge-yellow{% else %}badge-gray{% endif %}"title="{{ s.env or '' }}">{{ (s.env or '-')[:6] }}</span></td>
|
|
<td class="p-2 text-center">{% if s.zone == 'DMZ' %}<span class="badge badge-red">DMZ</span>{% else %}{{ s.zone or '-' }}{% endif %}</td>
|
|
<td class="p-2 text-center font-bold {% if (s.patch_count or 0) >= 2 %}text-cyber-green{% elif (s.patch_count or 0) == 1 %}text-green-300{% else %}text-cyber-red{% endif %}">{{ s.patch_count or 0 }}</td>
|
|
<td class="p-2 font-mono text-gray-400">{% if s.patch_weeks %}{% for w in s.patch_weeks.split(',') %}<span class="inline-block px-1 rounded text-xs {% if w == 'S15' %}bg-green-900/30 text-cyber-green{% else %}bg-cyber-border text-gray-400{% endif %} mr-1">{{ w }}</span>{% endfor %}{% else %}-{% endif %}</td>
|
|
<td class="p-2 text-center font-mono {% if s.last_patch_year == year %}text-cyber-green{% elif s.last_patch_date %}text-cyber-yellow{% else %}text-cyber-red{% endif %}">{% if s.last_patch_date %}{{ s.last_patch_date }}{% elif s.last_patch_week %}{{ s.last_patch_week }}{% else %}-{% endif %}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{% if total_pages > 1 %}
|
|
{% set pqs %}&year={{ year }}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}{% if scope %}&scope={{ scope }}{% endif %}{% if sort %}&sort={{ sort }}&dir={{ sort_dir }}{% endif %}{% endset %}
|
|
<div class="flex justify-between items-center p-3 border-t border-cyber-border">
|
|
<span class="text-xs text-gray-500">Page {{ page }}/{{ total_pages }}</span>
|
|
<div class="flex gap-1">
|
|
{% if page > 1 %}<a href="/audit-full/patching?page=1{{ pqs }}" class="btn-sm bg-cyber-border text-gray-400 px-2 py-1 text-xs">1</a>{% if page > 2 %}<a href="/audit-full/patching?page={{ page-1 }}{{ pqs }}" class="btn-sm bg-cyber-border text-gray-400 px-2 py-1 text-xs"><</a>{% endif %}{% endif %}
|
|
<span class="btn-sm bg-cyber-accent text-black px-2 py-1 text-xs font-bold">{{ page }}</span>
|
|
{% if page < total_pages %}<a href="/audit-full/patching?page={{ page+1 }}{{ pqs }}" class="btn-sm bg-cyber-border text-gray-400 px-2 py-1 text-xs">></a><a href="/audit-full/patching?page={{ total_pages }}{{ pqs }}" class="btn-sm bg-cyber-border text-gray-400 px-2 py-1 text-xs">{{ total_pages }}</a>{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|