Dashboard patching 2026: KPIs, barre progression, graphe semaines, domaine/env/zone
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2b2fac7c13
commit
ed23cc3fb6
@ -15,7 +15,7 @@ async def dashboard(request: Request, db=Depends(get_db)):
|
|||||||
if not user:
|
if not user:
|
||||||
return RedirectResponse(url="/login")
|
return RedirectResponse(url="/login")
|
||||||
|
|
||||||
# Stats
|
# Stats generales
|
||||||
stats = {}
|
stats = {}
|
||||||
stats["total_servers"] = db.execute(text("SELECT COUNT(*) FROM servers")).scalar()
|
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()
|
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
|
# Par tier
|
||||||
tiers = db.execute(text("SELECT tier, COUNT(*) FROM servers GROUP BY tier ORDER BY tier")).fetchall()
|
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", {
|
return templates.TemplateResponse("dashboard.html", {
|
||||||
"request": request, "user": user, "app_name": APP_NAME,
|
"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,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,41 +3,153 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<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 -->
|
<!-- KPIs generaux -->
|
||||||
<div class="grid grid-cols-5 gap-4 mb-6">
|
<div style="display:flex;flex-wrap:nowrap;gap:8px;margin-bottom:16px;">
|
||||||
<div class="card p-4 text-center">
|
<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>
|
||||||
<div class="text-3xl font-bold text-cyber-accent">{{ stats.total_servers }}</div>
|
<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>
|
||||||
<div class="text-xs text-gray-500">Serveurs</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>
|
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-yellow">{{ stats.qualys_tags }}</div><div class="text-xs text-gray-500">Tags Qualys</div></div>
|
||||||
<div class="card p-4 text-center">
|
<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="text-3xl font-bold text-cyber-green">{{ stats.patchable }}</div>
|
|
||||||
<div class="text-xs text-gray-500">Patchables SecOps</div>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4 text-center">
|
|
||||||
<div class="text-3xl font-bold text-white">{{ stats.linux }} / {{ stats.windows }}</div>
|
|
||||||
<div class="text-xs text-gray-500">Linux / Windows</div>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4 text-center">
|
|
||||||
<div class="text-3xl font-bold text-cyber-yellow">{{ stats.qualys_tags }}</div>
|
|
||||||
<div class="text-xs text-gray-500">Tags Qualys</div>
|
|
||||||
</div>
|
|
||||||
<div class="card p-4 text-center">
|
|
||||||
<div class="text-3xl font-bold text-cyber-red">{{ stats.eol }}</div>
|
|
||||||
<div class="text-xs text-gray-500">EOL</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Par domaine -->
|
<!-- ═══════ PATCHING 2026 ═══════ -->
|
||||||
<div class="card p-4 mb-6">
|
{% if patch_stats %}
|
||||||
<h3 class="text-sm font-bold text-cyber-accent mb-3">Par domaine</h3>
|
<div class="card p-4 mb-4">
|
||||||
<table class="w-full table-cyber">
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Patching 2026</h3>
|
||||||
<thead><tr>
|
|
||||||
<th class="text-left p-2">Domaine</th>
|
<!-- KPIs patching -->
|
||||||
<th class="p-2">Total</th>
|
<div style="display:flex;flex-wrap:nowrap;gap:6px;margin-bottom:12px;">
|
||||||
<th class="p-2">Actifs</th>
|
<a href="/audit-full" class="card p-2 text-center hover:bg-cyber-hover" style="flex:1;min-width:0;background:#111827;">
|
||||||
<th class="p-2">Linux</th>
|
<div class="text-xl font-bold text-cyber-accent">{{ patch_stats.audited }}</div>
|
||||||
<th class="p-2">Windows</th>
|
<div style="font-size:10px;" class="text-gray-500">Audites</div>
|
||||||
</tr></thead>
|
</a>
|
||||||
|
<a href="/audit-full?filter=app_patch2026" class="card p-2 text-center hover:bg-cyber-hover" style="flex:1;min-width:0;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-cyber-green">{{ patch_stats.patched_2026 }}</div>
|
||||||
|
<div style="font-size:10px;" class="text-gray-500">Patches 2026</div>
|
||||||
|
</a>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-cyber-green">{{ patch_stats.patched_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;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-blue-400">{{ patch_stats.patched_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;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-purple-400">{{ patch_stats.patched_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;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-cyber-yellow">{{ patch_stats.patched_2025_only }}</div>
|
||||||
|
<div style="font-size:10px;" class="text-gray-500">2025 seul</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827;">
|
||||||
|
<div class="text-xl font-bold text-cyber-red">{{ patch_stats.never_patched }}</div>
|
||||||
|
<div style="font-size:10px;" class="text-gray-500">Jamais</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center" style="flex:1;min-width:0;background:#111827;">
|
||||||
|
<div class="text-xl font-bold {% if patch_stats.needs_reboot > 0 %}text-cyber-red{% else %}text-cyber-green{% endif %}">{{ patch_stats.needs_reboot }}</div>
|
||||||
|
<div style="font-size:10px;" class="text-gray-500">Reboot</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Barre de progression globale -->
|
||||||
|
{% set pct = (patch_stats.patched_2026 / patch_stats.audited * 100)|int if patch_stats.audited > 0 else 0 %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="flex justify-between text-xs mb-1">
|
||||||
|
<span class="text-gray-400">Couverture patching 2026</span>
|
||||||
|
<span class="font-bold {% if pct >= 80 %}text-cyber-green{% elif pct >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %}">{{ pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="height:8px;background:#1f2937;border-radius:4px;overflow:hidden;">
|
||||||
|
<div style="height:100%;width:{{ pct }}%;background:{% if pct >= 80 %}#22c55e{% elif pct >= 50 %}#eab308{% else %}#ef4444{% endif %};border-radius:4px;transition:width 0.5s;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Graphe frequence par semaine -->
|
||||||
|
{% if patch_weekly %}
|
||||||
|
<div class="mb-4">
|
||||||
|
<div class="text-xs text-gray-500 mb-2">Serveurs patches par semaine</div>
|
||||||
|
<div style="display:flex;align-items:flex-end;gap:2px;height:120px;">
|
||||||
|
{% set max_cnt = patch_weekly|map(attribute='cnt')|max %}
|
||||||
|
{% for w in patch_weekly %}
|
||||||
|
<div style="flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%;" title="{{ w.week }}: {{ w.cnt }} serveurs">
|
||||||
|
<div style="font-size:9px;color:#94a3b8;margin-bottom:2px;">{{ w.cnt }}</div>
|
||||||
|
<div style="width:100%;background:#22c55e;border-radius:2px 2px 0 0;min-height:2px;height:{{ (w.cnt / max_cnt * 100)|int }}%;opacity:0.8;"></div>
|
||||||
|
<div style="font-size:8px;color:#6b7280;margin-top:2px;transform:rotate(-45deg);white-space:nowrap;">{{ w.week }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Par domaine -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-500 mb-2">Par domaine</div>
|
||||||
|
<table class="w-full table-cyber text-xs">
|
||||||
|
<thead><tr><th class="text-left p-1">Domaine</th><th class="p-1">Total</th><th class="p-1">Patche</th><th class="p-1">2x</th><th class="p-1">%</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for d in patch_by_domain %}
|
||||||
|
{% set pct_d = (d.patched / d.total * 100)|int if d.total > 0 else 0 %}
|
||||||
|
<tr>
|
||||||
|
<td class="p-1">{{ d.domain }}</td>
|
||||||
|
<td class="p-1 text-center">{{ d.total }}</td>
|
||||||
|
<td class="p-1 text-center text-cyber-green">{{ d.patched }}</td>
|
||||||
|
<td class="p-1 text-center text-blue-400">{{ d.patched_twice }}</td>
|
||||||
|
<td class="p-1 text-center">
|
||||||
|
<span class="{% if pct_d >= 80 %}text-cyber-green{% elif pct_d >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %} font-bold">{{ pct_d }}%</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<!-- Par environnement -->
|
||||||
|
<div class="text-xs text-gray-500 mb-2">Par environnement</div>
|
||||||
|
<table class="w-full table-cyber text-xs mb-4">
|
||||||
|
<thead><tr><th class="text-left p-1">Env</th><th class="p-1">Total</th><th class="p-1">Patche</th><th class="p-1">%</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for e in patch_by_env %}
|
||||||
|
{% set pct_e = (e.patched / e.total * 100)|int if e.total > 0 else 0 %}
|
||||||
|
<tr>
|
||||||
|
<td class="p-1">{{ e.env }}</td>
|
||||||
|
<td class="p-1 text-center">{{ e.total }}</td>
|
||||||
|
<td class="p-1 text-center text-cyber-green">{{ e.patched }}</td>
|
||||||
|
<td class="p-1 text-center"><span class="{% if pct_e >= 80 %}text-cyber-green{% elif pct_e >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %} font-bold">{{ pct_e }}%</span></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Par zone -->
|
||||||
|
<div class="text-xs text-gray-500 mb-2">Par zone reseau</div>
|
||||||
|
<table class="w-full table-cyber text-xs">
|
||||||
|
<thead><tr><th class="text-left p-1">Zone</th><th class="p-1">Total</th><th class="p-1">Patche</th><th class="p-1">Jamais</th><th class="p-1">%</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for z in patch_by_zone %}
|
||||||
|
{% set pct_z = (z.patched / z.total * 100)|int if z.total > 0 else 0 %}
|
||||||
|
<tr>
|
||||||
|
<td class="p-1">{{ z.zone }}</td>
|
||||||
|
<td class="p-1 text-center">{{ z.total }}</td>
|
||||||
|
<td class="p-1 text-center text-cyber-green">{{ z.patched }}</td>
|
||||||
|
<td class="p-1 text-center text-cyber-red">{{ z.never }}</td>
|
||||||
|
<td class="p-1 text-center"><span class="{% if pct_z >= 80 %}text-cyber-green{% elif pct_z >= 50 %}text-cyber-yellow{% else %}text-cyber-red{% endif %} font-bold">{{ pct_z }}%</span></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Par domaine (inventaire) -->
|
||||||
|
<div class="card p-4 mb-4">
|
||||||
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Inventaire par domaine</h3>
|
||||||
|
<table class="w-full table-cyber text-xs">
|
||||||
|
<thead><tr><th class="text-left p-2">Domaine</th><th class="p-2">Total</th><th class="p-2">Actifs</th><th class="p-2">Linux</th><th class="p-2">Windows</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for d in domains %}
|
{% for d in domains %}
|
||||||
<tr>
|
<tr>
|
||||||
@ -52,10 +164,11 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Par tier -->
|
<!-- Par tier + quick stats -->
|
||||||
<div class="card p-4 mb-6">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="card p-4">
|
||||||
<h3 class="text-sm font-bold text-cyber-accent mb-3">Par tier</h3>
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Par tier</h3>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-3">
|
||||||
{% for t in tiers %}
|
{% for t in tiers %}
|
||||||
<div class="flex-1 text-center p-3 rounded" style="background: {% if t[0] == 'tier0' %}#ff336622{% elif t[0] == 'tier1' %}#ff880022{% elif t[0] == 'tier2' %}#ffcc0022{% else %}#00ff8822{% endif %}">
|
<div class="flex-1 text-center p-3 rounded" style="background: {% if t[0] == 'tier0' %}#ff336622{% elif t[0] == 'tier1' %}#ff880022{% elif t[0] == 'tier2' %}#ffcc0022{% else %}#00ff8822{% endif %}">
|
||||||
<div class="text-lg font-bold">{{ t[1] }}</div>
|
<div class="text-lg font-bold">{{ t[1] }}</div>
|
||||||
@ -63,21 +176,15 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick stats -->
|
|
||||||
<div class="grid grid-cols-3 gap-4">
|
|
||||||
<div class="card p-3">
|
|
||||||
<span class="text-xs text-gray-500">Decomissionnes</span>
|
|
||||||
<span class="float-right text-cyber-red font-bold">{{ stats.decom }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-3">
|
<div class="card p-4">
|
||||||
<span class="text-xs text-gray-500">EOL</span>
|
<h3 class="text-sm font-bold text-cyber-accent mb-3">Quick stats</h3>
|
||||||
<span class="float-right text-cyber-red font-bold">{{ stats.eol }}</span>
|
<div class="space-y-2 text-xs">
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">Decommissionnes</span><span class="text-cyber-red font-bold">{{ stats.decom }}</span></div>
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">EOL</span><span class="text-cyber-red font-bold">{{ stats.eol }}</span></div>
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">Assets Qualys</span><span class="text-cyber-accent font-bold">{{ stats.qualys_assets }}</span></div>
|
||||||
|
<div class="flex justify-between"><span class="text-gray-500">Tags Qualys</span><span class="text-cyber-yellow font-bold">{{ stats.qualys_tags }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card p-3">
|
|
||||||
<span class="text-xs text-gray-500">Assets Qualys</span>
|
|
||||||
<span class="float-right text-cyber-accent font-bold">{{ stats.qualys_assets }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user