Qualys: - Recherche API temps réel + cache 24h base locale - Tags: liste DYN/STAT, mapping V3 (DOM-*, TYP-*, APP-*), nb assets cliquable - CRUD tags: créer STAT, supprimer, resync API - Détail asset: infos + décodage nomenclature V3 + tags assignés - Ajout/retrait tag unitaire avec autocomplete filtrable - Bulk add/remove tag en masse avec dropdown filtrable - Tags retirer: charge dynamiquement les STAT assignés aux assets sélectionnés - Resync assets sélectionnés + retour même recherche Contacts: - 50 contacts importés avec 93 scopes (domaine/app/serveur/zone par env) - 13 rôles (responsable_domaine, ra_prod, ra_recette, referent_technique...) - Recherche par nom/email/serveur (affiche contacts liés) - CRUD complet: éditer, scopes, activer/désactiver, supprimer - Serveurs liés calculés dynamiquement depuis les scopes Audit: - Restructuré: Audit général + sous-menu Spécifique - Dernier audit global affiché avec date - Lancer audit général avec exclusions (domaines/zones) et parallélisme - KPIs Qualys KO et S1 KO cliquables - Export CSV Serveurs: - Actions groupées bulk (domaine, env, tier, état, owner, licence) - Dashboard: KPI EOL ajouté - Filtre état: EOL + en décommissionnement ajoutés - 138 serveurs EOL importés depuis Qualys (owner=na, hors périmètre) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
6.5 KiB
HTML
107 lines
6.5 KiB
HTML
<div>
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-bold text-cyber-accent">{{ a.name or a.hostname }}</h3>
|
|
<button onclick="document.getElementById('qualys-detail').style.display='none'" class="text-gray-500 hover:text-white text-xl">×</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<!-- Infos -->
|
|
<div>
|
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Informations</h4>
|
|
<div class="space-y-1 text-xs">
|
|
<div><span class="text-gray-500">Hostname:</span> <span class="font-mono">{{ a.hostname }}</span></div>
|
|
<div><span class="text-gray-500">IP:</span> <span class="font-mono text-cyber-green">{{ a.ip_address or '-' }}</span></div>
|
|
<div><span class="text-gray-500">FQDN:</span> <span class="font-mono">{{ a.fqdn or '-' }}</span></div>
|
|
<div><span class="text-gray-500">OS:</span> {{ a.os or '-' }}</div>
|
|
<div><span class="text-gray-500">Agent:</span> <span class="badge {% if a.agent_status and 'ACTIVE' in a.agent_status %}badge-green{% else %}badge-red{% endif %}">{{ a.agent_status or '-' }}</span></div>
|
|
<div><span class="text-gray-500">Version:</span> {{ a.agent_version or '-' }}</div>
|
|
<div><span class="text-gray-500">Dernier check-in:</span> {{ a.last_checkin or '-' }}</div>
|
|
<div><span class="text-gray-500">Qualys ID:</span> {{ a.qualys_asset_id }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Décodage -->
|
|
<div>
|
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Décodage nomenclature</h4>
|
|
<div class="space-y-1 text-xs">
|
|
<div><span class="text-gray-500">Type:</span> {{ decoded.type }}</div>
|
|
<div><span class="text-gray-500">Environnement:</span> {{ decoded.env }}</div>
|
|
<div><span class="text-gray-500">Domaine:</span> {{ decoded.domain }}</div>
|
|
</div>
|
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mt-3 mb-1">Tags suggérés</h4>
|
|
<div class="flex flex-wrap gap-1">
|
|
{% for t in decoded.tags %}
|
|
{% set found = false %}
|
|
{% for ct in tags %}{% if ct.name == t %}{% set found = true %}{% endif %}{% endfor %}
|
|
<span class="badge {% if found %}badge-green{% else %}badge-red{% endif %}" title="{% if found %}Assigné{% else %}Manquant{% endif %}">{{ t }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags assignés -->
|
|
<div class="mt-4">
|
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Tags assignés ({{ tags|length }})</h4>
|
|
<div class="space-y-1">
|
|
{% for t in tags %}
|
|
<div class="flex items-center gap-2 text-xs">
|
|
<span class="badge {% if t.is_dynamic %}badge-blue{% else %}badge-yellow{% endif %}">{{ 'DYN' if t.is_dynamic else 'STAT' }}</span>
|
|
<span class="font-mono text-cyber-accent">{{ t.name }}</span>
|
|
{% if not t.is_dynamic %}
|
|
<form method="POST" action="/qualys/asset/{{ a.qualys_asset_id }}/tag/remove" style="display:inline"
|
|
hx-post="/qualys/asset/{{ a.qualys_asset_id }}/tag/remove" hx-target="#tag-result" hx-swap="innerHTML">
|
|
<input type="hidden" name="tag_id" value="{{ t.qualys_tag_id }}">
|
|
<button class="text-gray-600 hover:text-cyber-red" title="Retirer ce tag">✕</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% if not tags %}<span class="text-xs text-gray-500">Aucun tag</span>{% endif %}
|
|
</div>
|
|
<div id="tag-result" class="mt-1"></div>
|
|
</div>
|
|
|
|
<!-- Ajouter un tag -->
|
|
<div class="mt-4">
|
|
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Ajouter un tag</h4>
|
|
<form hx-post="/qualys/asset/{{ a.qualys_asset_id }}/tag/add" hx-target="#tag-result" hx-swap="innerHTML" class="flex gap-2 items-center">
|
|
<input type="hidden" name="tag_id" id="add-tag-id-{{ a.qualys_asset_id }}">
|
|
<input type="text" id="add-tag-input-{{ a.qualys_asset_id }}" class="text-xs py-1 px-2 flex-1 font-mono" placeholder="Filtrer et sélectionner un tag..." autocomplete="off"
|
|
onkeyup="filterTags(this, '{{ a.qualys_asset_id }}')" onfocus="document.getElementById('tag-dropdown-{{ a.qualys_asset_id }}').style.display='block'">
|
|
<button type="submit" class="btn-sm bg-cyber-accent text-black">Ajouter</button>
|
|
</form>
|
|
<div id="tag-dropdown-{{ a.qualys_asset_id }}" class="bg-cyber-card border border-cyber-border rounded mt-1 overflow-y-auto text-xs" style="display:none; max-height:150px">
|
|
{% set assigned_ids = tags | map(attribute='qualys_tag_id') | list %}
|
|
{% for t in all_tags %}
|
|
{% if t.qualys_tag_id not in assigned_ids %}
|
|
<div class="px-2 py-1 hover:bg-cyber-border/30 cursor-pointer tag-option" data-id="{{ t.qualys_tag_id }}" data-name="{{ t.name }}"
|
|
onclick="selectTag('{{ a.qualys_asset_id }}', '{{ t.qualys_tag_id }}', '{{ t.name }}')">
|
|
<span class="font-mono">{{ t.name }}</span>
|
|
<span class="badge {% if t.is_dynamic %}badge-blue{% else %}badge-yellow{% endif %} text-[8px] ml-1">{{ 'DYN' if t.is_dynamic else 'STAT' }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<script>
|
|
function filterTags(input, aid) {
|
|
var q = input.value.toLowerCase();
|
|
var dd = document.getElementById('tag-dropdown-' + aid);
|
|
dd.style.display = 'block';
|
|
dd.querySelectorAll('.tag-option').forEach(function(el) {
|
|
el.style.display = el.dataset.name.toLowerCase().includes(q) ? '' : 'none';
|
|
});
|
|
}
|
|
function selectTag(aid, tid, tname) {
|
|
document.getElementById('add-tag-id-' + aid).value = tid;
|
|
document.getElementById('add-tag-input-' + aid).value = tname;
|
|
document.getElementById('tag-dropdown-' + aid).style.display = 'none';
|
|
}
|
|
document.addEventListener('click', function(e) {
|
|
document.querySelectorAll('[id^=tag-dropdown-]').forEach(function(dd) {
|
|
if (!dd.contains(e.target) && !e.target.id.startsWith('add-tag-input-')) dd.style.display = 'none';
|
|
});
|
|
});
|
|
</script>
|
|
</div>
|