120 lines
6.0 KiB
HTML
120 lines
6.0 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">Scan complet du parc Qualys (~6000 assets) puis filtrage côté code pour ne garder que les serveurs (~1300). Durée 2 à 4 min. 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 %}
|