patchcenter/app/templates/qualys_duplicates.html

120 lines
5.9 KiB
HTML

{% extends 'base.html' %}
{% block title %}Doublons Qualys{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold text-cyber-accent">Doublons Qualys (serveurs uniquement)</h2>
<a href="/qualys/duplicates?refresh=1" class="btn-sm bg-cyber-border text-cyber-accent">🔄 Re-scan API</a>
</div>
{% if scan_running %}
<div class="card p-4 mb-4 border-2 border-cyber-accent flex items-center gap-4" style="background:linear-gradient(90deg,#00334d,#003355);box-shadow:0 0 20px rgba(0,212,255,.3)">
<div style="border:4px solid rgba(0,212,255,.2); border-top:4px solid #00d4ff; border-radius:50%; width:42px; height:42px; animation:spin 0.9s linear infinite; flex-shrink:0"></div>
<div class="flex-1">
<div class="text-cyber-accent font-bold text-base">Scan API Qualys en cours...</div>
<div class="text-xs text-gray-400 mt-1">Recuperation de tous les assets via API (~6000), durée typique 2 à 4 minutes. La page se rafraîchira automatiquement.</div>
</div>
</div>
<script>setTimeout(function() { window.location.href = '/qualys/duplicates'; }, 15000);</script>
{% elif from_cache %}
<div class="text-xs text-gray-500 mb-2">Données du cache (TTL 10min). Re-scan pour forcer.</div>
{% endif %}
<div class="grid grid-cols-3 gap-3 mb-4">
<div class="card p-3 border-cyber-accent">
<div class="text-xs text-gray-500 uppercase">Total assets Qualys</div>
<div class="text-2xl font-bold text-cyber-accent">{{ dupes_data.total_assets }}</div>
</div>
<div class="card p-3 border-orange-700">
<div class="text-xs text-gray-500 uppercase">Hostnames en doublon</div>
<div class="text-2xl font-bold text-orange-500">{{ dupes_data.duplicate_hostnames }}</div>
</div>
<div class="card p-3 border-red-700">
<div class="text-xs text-gray-500 uppercase">Zombies a purger</div>
<div class="text-2xl font-bold text-red-500">{{ dupes_data.total_zombies }}</div>
<div class="text-xs text-gray-500">(= total - 1 par hostname doublonne)</div>
</div>
</div>
{% if not dupes_data.groups %}
<div class="card p-6 text-center text-gray-400">
Aucun doublon detecte. Si la page semble vide, clique sur "Re-scan API" pour forcer un scan complet.
</div>
{% else %}
<div class="card p-3">
<p class="text-xs text-gray-400 mb-2">
Le plus recent (en haut de chaque groupe) est probablement l'asset actif. Les autres sont des zombies (anciennes installations, ré-IPs, doublons de scan).
Bouton <strong>Supprimer</strong> = appel API Qualys <code>POST /qps/rest/2.0/delete/am/hostasset/{id}</code>.
{% if not can_delete %}<br><span class="text-yellow-400">Tu n'as pas la permission edit pour supprimer.</span>{% endif %}
</p>
<table class="w-full text-xs">
<thead class="text-gray-400 border-b border-cyber-border">
<tr>
<th class="text-left py-1">Hostname</th>
<th class="text-right py-1">Nb</th>
<th class="text-left py-1">ID Qualys</th>
<th class="text-left py-1">IP</th>
<th class="text-left py-1">Agent</th>
<th class="text-left py-1">Last check-in</th>
<th class="text-center py-1">Action</th>
</tr>
</thead>
<tbody>
{% for grp in dupes_data.groups %}
{% for a in grp.assets %}
<tr class="border-b border-cyber-border/30 hover:bg-cyber-card/50 {% if loop.first %}bg-green-900/10{% else %}bg-red-900/10{% endif %}">
<td class="py-1 font-mono">{% if loop.first %}<strong class="text-cyber-accent">{{ grp.shortname }}</strong> ({{ grp.count }}){% endif %}</td>
<td class="text-right py-1">{% if loop.first %}{{ loop.length }}{% endif %}</td>
<td class="py-1 font-mono text-gray-400">{{ a.id }}</td>
<td class="py-1 font-mono">{{ a.ip or '-' }}</td>
<td class="py-1">{{ a.agent_status or '-' }}</td>
<td class="py-1 font-mono {% if loop.first %}text-green-400{% else %}text-red-400{% endif %}">
{{ a.last_check[:16] if a.last_check else '(jamais)' }}
</td>
<td class="text-center py-1">
{% if loop.first %}
<span class="text-green-400 text-xs">✓ actif</span>
{% elif can_delete %}
<button onclick="delAsset({{ a.id }}, '{{ grp.shortname }}', '{{ a.ip }}', this)"
class="btn-sm bg-red-900/40 text-red-300 hover:bg-red-900/70" style="padding:2px 8px">
🗑 Supprimer
</button>
{% else %}
<span class="text-gray-500 text-xs">zombie</span>
{% endif %}
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
<script>
async function delAsset(id, name, ip, btn) {
if (!confirm("Supprimer DEFINITIVEMENT l'asset Qualys " + name + " (ID " + id + ", IP " + ip + ") ?\n\nCeci appelle l'API Qualys et ne peut pas etre annule.")) return;
btn.disabled = true;
btn.textContent = "...";
try {
const r = await fetch("/qualys/asset/" + id + "/delete", {method: "POST", credentials: "same-origin"});
const data = await r.json();
if (data.ok) {
btn.parentElement.parentElement.style.opacity = "0.3";
btn.outerHTML = '<span class="text-green-400 text-xs">✓ supprime</span>';
} else {
alert("Echec : " + data.msg);
btn.disabled = false;
btn.textContent = "🗑 Supprimer";
}
} catch (e) {
alert("Erreur reseau : " + e);
btn.disabled = false;
btn.textContent = "🗑 Supprimer";
}
}
</script>
{% endif %}
{% endblock %}