feat(qualys/agents): audit en background thread + page d'attente auto-refresh (fix ERR_CONNECTION_RESET sur audits longs)

This commit is contained in:
Pierre & Lumière 2026-04-27 23:25:50 +02:00
parent 26e05d63ac
commit cdcb85917d
3 changed files with 102 additions and 7 deletions

View File

@ -1354,16 +1354,29 @@ async def qualys_asset_delete(request: Request, asset_id: int, db=Depends(get_db
@router.get("/qualys/agents/{hostname}/audit-qualys", response_class=HTMLResponse)
def qualys_agent_audit_page(hostname: str, request: Request, db=Depends(get_db)):
"""Audit cible Qualys Agent : status service + version + logs (agent + systeme)."""
def qualys_agent_audit_page(hostname: str, request: Request, db=Depends(get_db),
refresh: int = 0):
"""Audit cible Qualys Agent (async). Background thread + page auto-refresh."""
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
perms = get_user_perms(db, user)
if not can_view(perms, "qualys"):
return RedirectResponse(url="/dashboard")
from app.services.realtime_audit_service import audit_qualys_agent_only
audit = audit_qualys_agent_only(hostname)
from app.services.realtime_audit_service import (
start_qualys_audit_async, get_qualys_audit_state
)
state = get_qualys_audit_state(hostname)
if refresh or not state:
start_qualys_audit_async(hostname, force=bool(refresh))
state = get_qualys_audit_state(hostname)
ctx = base_context(request, db, user)
ctx.update({"audit": audit, "hostname": hostname})
ctx.update({
"hostname": hostname,
"audit_status": (state or {}).get("status", "pending"),
"audit_started_at": (state or {}).get("started_at"),
"audit_finished_at": (state or {}).get("finished_at"),
"audit": (state or {}).get("result"),
"audit_error": (state or {}).get("error"),
})
return templates.TemplateResponse("qualys_agent_audit.html", ctx)

View File

@ -606,6 +606,58 @@ QUALYS_AGENT_CMDS = {
}
import threading as _threading
_qualys_audit_cache = {} # hostname -> {status, result, started_at, finished_at, error}
_qualys_audit_lock = _threading.Lock()
def start_qualys_audit_async(hostname, force=False):
"""Lance audit_qualys_agent_only en background. Reuse run pending récent (<2min)."""
with _qualys_audit_lock:
existing = _qualys_audit_cache.get(hostname)
if existing and existing.get("status") == "pending" and not force:
age = (datetime.now() - existing["started_at"]).total_seconds()
if age < 120:
return False
_qualys_audit_cache[hostname] = {
"status": "pending",
"result": None,
"started_at": datetime.now(),
"finished_at": None,
"error": None,
}
def _runner():
try:
res = audit_qualys_agent_only(hostname)
with _qualys_audit_lock:
state = _qualys_audit_cache.get(hostname, {})
state.update({
"status": "ok",
"result": res,
"finished_at": datetime.now(),
})
_qualys_audit_cache[hostname] = state
except Exception as ex:
with _qualys_audit_lock:
state = _qualys_audit_cache.get(hostname, {})
state.update({
"status": "error",
"error": str(ex),
"finished_at": datetime.now(),
})
_qualys_audit_cache[hostname] = state
t = _threading.Thread(target=_runner, daemon=True)
t.start()
return True
def get_qualys_audit_state(hostname):
with _qualys_audit_lock:
return dict(_qualys_audit_cache.get(hostname, {})) or None
def audit_qualys_agent_only(hostname):
"""Audit cible Qualys Agent uniquement: status service + version + logs.
Utilise _resolve + _connect + _run comme audit_single_server.

View File

@ -2,17 +2,45 @@
{% block title %}Audit Qualys Agent — {{ hostname }}{% endblock %}
{% block content %}
{% if audit_status == 'pending' %}
<meta http-equiv="refresh" content="3">
{% endif %}
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-xl font-bold text-cyber-accent">Audit Qualys Agent</h2>
<p class="text-xs text-gray-500 mt-1 font-mono">{{ hostname }}{% if audit.resolved_fqdn %} → {{ audit.resolved_fqdn }}{% endif %}</p>
<p class="text-xs text-gray-500 mt-1 font-mono">{{ hostname }}{% if audit and audit.resolved_fqdn %} → {{ audit.resolved_fqdn }}{% endif %}</p>
</div>
<div style="display:flex;gap:8px">
<a href="/qualys/agents/{{ hostname }}/audit-qualys" class="btn-sm bg-cyber-border text-gray-300 px-3 py-2 text-xs">Relancer</a>
<a href="/qualys/agents/{{ hostname }}/audit-qualys?refresh=1" class="btn-sm bg-cyber-border text-gray-300 px-3 py-2 text-xs">Relancer</a>
<a href="/qualys/agents#inactive-list" class="btn-sm bg-cyber-border text-gray-300 px-3 py-2 text-xs">← Retour</a>
</div>
</div>
{% if audit_status == 'pending' %}
<!-- Bandeau "audit en cours" -->
<div class="card p-4 mb-4" style="border:1px solid #f59e0b;background:rgba(245,158,11,0.08)">
<div class="flex items-center gap-3 text-sm">
<svg style="width:24px;height:24px;animation:spin 1s linear infinite" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2">
<circle cx="12" cy="12" r="10" stroke-opacity="0.3"/><path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"/>
</svg>
<div>
<span class="font-bold text-cyber-yellow">⏳ Audit en cours…</span>
<span class="text-xs text-gray-400 ml-2">Connexion SSH + collecte status, version, logs (1030s typique)</span>
<div class="text-xs text-gray-500 mt-1">Démarré : {{ audit_started_at.strftime('%H:%M:%S') if audit_started_at else '?' }} — page rafraichie auto toutes les 3s</div>
</div>
</div>
</div>
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
{% elif audit_status == 'error' %}
<div class="card p-4 mb-4" style="border:1px solid #ef4444;background:rgba(239,68,68,0.08)">
<span class="font-bold text-cyber-red">✗ Erreur audit</span>
<pre style="background:#0b0f1a;color:#e5e7eb;padding:10px;border-radius:4px;font-size:11px;margin-top:8px;white-space:pre-wrap">{{ audit_error or '(pas de détail)' }}</pre>
</div>
{% else %}
<!-- Bandeau statut connexion -->
<div class="card p-3 mb-4" style="
{% if audit.status == 'OK' %}border:1px solid #22c55e;background:rgba(34,197,94,0.08);
@ -70,4 +98,6 @@
{% endif %}
{% endif %}
{% endblock %}