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>
183 lines
10 KiB
HTML
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 %}
|