patchcenter/app/templates/qualys_search.html
Khalid MOUTAOUAKIL 8e62b1fb11 Qualys complet, contacts, audit refactoré, bulk serveurs
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>
2026-04-05 00:47:26 +02:00

183 lines
10 KiB
HTML

{% extends 'base.html' %}
{% block title %}Recherche Qualys{% endblock %}
{% block content %}
<h2 class="text-xl font-bold text-cyber-accent mb-4">Recherche Assets Qualys</h2>
<!-- Recherche -->
<form method="GET" class="card p-3 mb-4 flex gap-3 items-end flex-wrap">
<div>
<label class="text-xs text-gray-500">Champ</label>
<select name="field" class="text-xs py-1 px-2" id="search-field" onchange="
var inp = document.getElementById('search-input');
var sel = document.getElementById('tag-select');
if (this.value === 'tag') { inp.style.display='none'; sel.style.display='block'; sel.name='search'; inp.name=''; }
else { inp.style.display='block'; sel.style.display='none'; inp.name='search'; sel.name=''; }
">
<option value="hostname" {% if field == 'hostname' %}selected{% endif %}>Hostname</option>
<option value="ip" {% if field == 'ip' %}selected{% endif %}>IP</option>
<option value="tag" {% if field == 'tag' %}selected{% endif %}>Tag</option>
</select>
</div>
<div class="flex-1">
<label class="text-xs text-gray-500">Recherche</label>
<input type="text" id="search-input" name="search" value="{% if field != 'tag' %}{{ search or '' }}{% endif %}" placeholder="Hostname ou IP..." class="w-full" {% if field == 'tag' %}style="display:none" name=""{% endif %}>
<select id="tag-select" {% if field == 'tag' %}name="search"{% else %}name="" style="display:none"{% endif %} class="w-full" size="1" style="{% if field != 'tag' %}display:none;{% endif %} max-height:200px; overflow-y:auto;">
<option value="">— Choisir un tag —</option>
{% for t in all_tags %}<option value="{{ t.name }}" {% if field == 'tag' and search == t.name %}selected{% endif %}>{{ t.name }}</option>{% endfor %}
</select>
</div>
<button type="submit" class="btn-primary px-4 py-1 text-sm">Rechercher</button>
{% if search %}<a href="/qualys/search/export?search={{ search }}&field={{ field }}" class="btn-sm bg-cyber-green text-black">Export CSV</a>{% endif %}
</form>
{% set msg = request.query_params.get('msg') %}
{% if msg %}
<div class="mb-3 p-2 rounded text-sm bg-green-900/30 text-cyber-green">
{% if msg.startswith('resync_') %}{{ msg.split('_')[1] }} asset(s) resynchronisé(s).{% elif msg.startswith('bulk_add_') %}Tags ajoutés: {{ msg.split('_')[2] }} OK, {{ msg.split('_')[3] }} KO.{% elif msg.startswith('bulk_rm_') %}Tags retirés: {{ msg.split('_')[2] }} OK, {{ msg.split('_')[3] }} KO.{% endif %}
</div>
{% endif %}
{% if search %}
<p class="text-xs text-gray-500 mb-2">
{% if api_msg %}{{ api_msg }}{% else %}{{ assets|length }} résultat(s){% endif %}
</p>
{% endif %}
<!-- Panel détail -->
<div id="qualys-detail" class="card mb-4 p-5" style="display:none"></div>
<!-- Résultats -->
{% if assets %}
<!-- Bulk actions -->
{% if can_edit_qualys %}
<div id="bulk-tag-bar" class="card p-3 mb-2 flex gap-3 items-center" style="display:none">
<span class="text-xs text-gray-400" id="bulk-tag-count">0 sélectionné(s)</span>
<form method="POST" action="/qualys/bulk/add-tag" class="flex gap-2 items-center relative">
<input type="hidden" name="asset_ids" id="bulk-tag-ids">
<input type="hidden" name="return_search" value="{{ search or '' }}">
<input type="hidden" name="return_field" value="{{ field or 'hostname' }}">
<input type="hidden" name="tag_id" id="bulk-add-tag-id">
<input type="text" id="bulk-add-input" class="text-xs py-1 px-2 w-40 font-mono" placeholder="Tag à ajouter..." autocomplete="off"
onkeyup="filterBulkTags(this, 'bulk-add-dd')" onfocus="document.getElementById('bulk-add-dd').style.display='block'">
<div id="bulk-add-dd" class="absolute top-8 left-0 bg-cyber-card border border-cyber-border rounded overflow-y-auto text-xs z-50" style="display:none; max-height:150px; width:250px">
{% for t in all_tags %}<div class="px-2 py-1 hover:bg-cyber-border/30 cursor-pointer bulk-tag-opt" data-name="{{ t.name }}"
onclick="document.getElementById('bulk-add-tag-id').value='{{ t.qualys_tag_id }}'; document.getElementById('bulk-add-input').value='{{ t.name }}'; document.getElementById('bulk-add-dd').style.display='none'">
<span class="font-mono">{{ t.name }}</span></div>{% endfor %}
</div>
<button type="submit" class="btn-sm bg-cyber-accent text-black">+ Ajouter</button>
</form>
<form method="POST" action="/qualys/bulk/remove-tag" class="flex gap-2 items-center relative">
<input type="hidden" name="asset_ids" id="bulk-tag-ids2">
<input type="hidden" name="return_search" value="{{ search or '' }}">
<input type="hidden" name="return_field" value="{{ field or 'hostname' }}">
<input type="hidden" name="tag_id" id="bulk-rm-tag-id">
<input type="text" id="bulk-rm-input" class="text-xs py-1 px-2 w-40 font-mono" placeholder="Tag à retirer..." autocomplete="off"
onkeyup="filterBulkTags(this, 'bulk-rm-dd')" onfocus="loadRemoveTags(); document.getElementById('bulk-rm-dd').style.display='block'">
<div id="bulk-rm-dd" class="absolute top-8 left-0 bg-cyber-card border border-cyber-border rounded overflow-y-auto text-xs z-50" style="display:none; max-height:150px; width:250px">
<div class="px-2 py-1 text-gray-500">Chargement...</div>
</div>
<button type="submit" class="btn-sm bg-red-900/30 text-cyber-red">- Retirer</button>
</form>
<form method="POST" action="/qualys/resync-assets" class="flex gap-2 items-center">
<input type="hidden" name="asset_ids" id="bulk-tag-ids3">
<input type="hidden" name="return_search" value="{{ search or '' }}">
<input type="hidden" name="return_field" value="{{ field or 'hostname' }}">
<button type="submit" class="btn-sm bg-cyber-border text-cyber-accent">Resync Qualys</button>
</form>
</div>
<script>
function filterBulkTags(input, ddId) {
var q = input.value.toLowerCase();
var dd = document.getElementById(ddId);
dd.style.display = 'block';
dd.querySelectorAll('.bulk-tag-opt').forEach(function(el) {
el.style.display = el.dataset.name.toLowerCase().includes(q) ? '' : 'none';
});
}
document.addEventListener('click', function(e) {
['bulk-add-dd', 'bulk-rm-dd'].forEach(function(id) {
var dd = document.getElementById(id);
if (dd && !dd.contains(e.target) && e.target.id !== 'bulk-add-input' && e.target.id !== 'bulk-rm-input') dd.style.display = 'none';
});
});
function loadRemoveTags() {
var ids = document.getElementById('bulk-tag-ids2').value;
if (!ids) return;
fetch('/qualys/bulk/tags-for-assets?asset_ids=' + ids)
.then(r => r.json())
.then(tags => {
var dd = document.getElementById('bulk-rm-dd');
dd.innerHTML = '';
if (tags.length === 0) { dd.innerHTML = '<div class="px-2 py-1 text-gray-500">Aucun tag STAT</div>'; return; }
tags.forEach(function(t) {
var div = document.createElement('div');
div.className = 'px-2 py-1 hover:bg-cyber-border/30 cursor-pointer bulk-tag-opt';
div.dataset.name = t.name;
div.innerHTML = '<span class="font-mono">' + t.name + '</span> <span class="text-gray-500 text-[9px]">(' + t.count + ')</span>';
div.onclick = function() {
document.getElementById('bulk-rm-tag-id').value = t.id;
document.getElementById('bulk-rm-input').value = t.name;
dd.style.display = 'none';
};
dd.appendChild(div);
});
});
}
function updateBulkTag() {
var checks = document.querySelectorAll('input[name=asset_chk]:checked');
var bar = document.getElementById('bulk-tag-bar');
if (checks.length > 0) {
bar.style.display = 'flex';
document.getElementById('bulk-tag-count').textContent = checks.length + ' sélectionné(s)';
var ids = Array.from(checks).map(c => c.value).join(',');
document.getElementById('bulk-tag-ids').value = ids;
document.getElementById('bulk-tag-ids2').value = ids;
document.getElementById('bulk-tag-ids3').value = ids;
} else { bar.style.display = 'none'; }
}
</script>
{% endif %}
<div class="card overflow-x-auto">
<table class="w-full table-cyber text-xs">
<thead><tr>
{% if can_edit_qualys %}<th class="p-2 w-6"><input type="checkbox" onchange="document.querySelectorAll('input[name=asset_chk]').forEach(c=>c.checked=this.checked); updateBulkTag()"></th>{% endif %}
<th class="text-left p-2">Hostname</th>
<th class="p-2">IP</th>
<th class="p-2">OS</th>
<th class="p-2">Agent</th>
<th class="text-left p-2">Tags</th>
<th class="p-2">Actions</th>
</tr></thead>
<tbody>
{% for a in assets %}
{% set hn = a.hostname %}
{% set ip = a.ip_address %}
{% set os = a.os %}
{% set agent = a.agent_status %}
{% set tl = a.tags_list %}
{% set qid = a.qualys_asset_id %}
<tr>
{% if can_edit_qualys %}<td class="p-2 text-center" onclick="event.stopPropagation()">{% if qid %}<input type="checkbox" name="asset_chk" value="{{ qid }}" onchange="updateBulkTag()">{% endif %}</td>{% endif %}
<td class="p-2 font-mono text-cyber-accent">{{ hn or '-' }}</td>
<td class="p-2 text-center text-gray-400">{{ ip or '-' }}</td>
<td class="p-2 text-center text-gray-400" title="{{ os or '' }}">{{ (os or '-')[:30] }}</td>
<td class="p-2 text-center">
{% if agent %}<span class="badge {% if 'ACTIVE' in agent %}badge-green{% else %}badge-gray{% endif %}">{{ agent[:10] }}</span>
{% else %}<span class="text-gray-600 text-xs">N/A</span>{% endif %}
</td>
<td class="p-2 text-gray-400" style="max-width:300px">{{ (tl or '-')[:80] }}</td>
<td class="p-2 text-center">
{% if qid %}
<button class="btn-sm bg-cyber-border text-cyber-accent"
hx-get="/qualys/asset/{{ qid }}" hx-target="#qualys-detail" hx-swap="innerHTML"
onclick="document.getElementById('qualys-detail').style.display='block'; window.scrollTo({top:0,behavior:'smooth'})">Détail</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
{% endblock %}