patchcenter/app/templates/qualys_search.html
Khalid MOUTAOUAKIL c139dfbaa2 Cache mémoire 10min pour Qualys API, bouton Resync temps réel, page Agents (activation keys + versions)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 23:04:48 +02:00

204 lines
12 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 %}
<div class="flex justify-between items-center mb-2">
<p class="text-xs text-gray-500">
{% if api_msg %}{{ api_msg }}{% else %}{{ assets|length }} résultat(s){% endif %}
{% if cache_info %}<span class="text-gray-600 ml-2">(cache: {{ cache_info.active }} entrées)</span>{% endif %}
</p>
<a href="/qualys/search?field={{ field }}&search={{ search }}&force=1" class="btn-sm bg-cyber-border text-cyber-accent px-3 py-1 text-xs" data-loading="Resync API...">Resync temps réel</a>
</div>
{% endif %}
<!-- Panel détail -->
<div id="qualys-detail" class="card mb-4 p-5" style="display:none"></div>
<!-- Panel vulnérabilités -->
<div id="vuln-detail" class="card mb-4" 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" data-loading="Ajout de tag en masse...|Appel API Qualys pour chaque asset">+ 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" data-loading="Retrait de tag en masse...|Appel API Qualys pour chaque asset">- 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" data-loading="Resynchronisation Qualys...|Mise à jour depuis API">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="p-2">Vulns</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-center">
{% set vc = vuln_map.get(ip|string, {}) if vuln_map else {} %}
{% if vc and vc.total > 0 %}
<button class="hover:opacity-80" hx-get="/qualys/vulns/{{ ip }}" hx-target="#vuln-detail" hx-swap="innerHTML"
onclick="document.getElementById('vuln-detail').style.display='block'; window.scrollTo({top:0,behavior:'smooth'})"
title="S3:{{ vc.severity3 }} S4:{{ vc.severity4 }} S5:{{ vc.severity5 }} | Confirmed:{{ vc.confirmed }} Potential:{{ vc.potential }}">
{% if vc.severity5 > 0 %}<span class="badge badge-red">{{ vc.severity5 }} crit</span> {% endif %}
{% if vc.severity4 > 0 %}<span class="badge badge-yellow">{{ vc.severity4 }} high</span> {% endif %}
{% if vc.severity3 > 0 %}<span class="text-gray-400 text-xs">+{{ vc.severity3 }} med</span>{% endif %}
</button>
{% elif vc is mapping %}<span class="text-cyber-green text-xs">0</span>
{% else %}<span class="text-gray-600 text-xs">-</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 %}