Qualys sync dual mode: diff (rapide, lastCheckedIn) + full (complet)

- refresh_all_agents accepte mode='diff'|'full' (defaut diff)
- Mode diff: filtre Qualys lastCheckedIn > qualys_last_diff_sync (settings)
- Mode full: pull tous les assets (comme avant)
- Skip early-exit en diff si dernier diff < 5 min
- 2 boutons UI: 'Sync rapide (diff)' et 'Sync complete'
- JS refreshAgents(mode) passe le mode en query param
This commit is contained in:
Pierre & Lumière 2026-04-15 12:33:48 +02:00
parent 55f81de986
commit a62f9a4146
3 changed files with 60 additions and 21 deletions

View File

@ -518,7 +518,8 @@ def qualys_agents_page(request: Request, db=Depends(get_db)):
@router.post("/qualys/agents/refresh")
def qualys_agents_refresh(request: Request, db=Depends(get_db)):
def qualys_agents_refresh(request: Request, db=Depends(get_db), mode: str = "diff"):
"""Sync Qualys. mode=diff (rapide, depuis dernier sync) ou full (complet)."""
from fastapi.responses import JSONResponse
user = get_current_user(request)
if not user:
@ -528,7 +529,7 @@ def qualys_agents_refresh(request: Request, db=Depends(get_db)):
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
from ..services.qualys_service import refresh_all_agents
try:
stats = refresh_all_agents(db)
stats = refresh_all_agents(db, mode=mode)
if stats.get("busy"):
return JSONResponse(stats, status_code=409)
if not stats.get("ok"):

View File

@ -544,28 +544,46 @@ def get_cache_stats():
return _cache.stats()
def refresh_all_agents(db):
"""Rafraichit tous les agents depuis l'API Qualys QPS (bulk, paginé)"""
def refresh_all_agents(db, mode="diff"):
"""Rafraichit les agents depuis l'API Qualys QPS.
mode='diff' (defaut) : ne pull que les assets dont lastCheckedIn > dernier sync diff
Court (~30s), pour cron frequent.
mode='full' : pull tous les assets matchant le filtre tag.
Long (5-10 min), pour ménage hebdo.
"""
global _refresh_running
if not _refresh_lock.acquire(blocking=False):
return {"ok": False, "msg": "Une synchronisation Qualys est déjà en cours", "busy": True}
_refresh_running = True
_refresh_cancel.clear()
try:
return _refresh_all_agents_impl(db)
return _refresh_all_agents_impl(db, mode=mode)
finally:
_refresh_running = False
_refresh_lock.release()
def _refresh_all_agents_impl(db):
def _refresh_all_agents_impl(db, mode="diff"):
"""Implémentation réelle du refresh (appelée sous verrou)"""
# Early exit si tous les assets ont moins de 40 min (pas besoin d'appeler Qualys)
total = db.execute(text("SELECT COUNT(*) FROM qualys_assets")).scalar() or 0
if total > 0:
stale = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE updated_at < now() - interval '40 minutes'")).scalar() or 0
if stale == 0:
return {"ok": True, "msg": f"Tous les {total} assets sont récents (< 40 min), rien à faire", "skipped_all": True}
from .secrets_service import get_secret, set_secret
from datetime import datetime, timezone
# En mode diff : recupere le timestamp du dernier diff sync
last_diff_iso = None
if mode == "diff":
last_diff_iso = get_secret(db, "qualys_last_diff_sync")
# Early exit seulement en diff : si tous recents ET dernier diff < 30 min
total = db.execute(text("SELECT COUNT(*) FROM qualys_assets")).scalar() or 0
if total > 0 and last_diff_iso:
try:
last_dt = datetime.fromisoformat(last_diff_iso.replace("Z", "+00:00"))
age_min = (datetime.now(timezone.utc) - last_dt).total_seconds() / 60
if age_min < 5:
return {"ok": True, "msg": f"Diff sync deja effectue il y a {int(age_min)} min, rien a faire",
"skipped_all": True, "mode": mode}
except Exception:
pass
qualys_url, qualys_user, qualys_pass, qualys_proxy = _get_qualys_creds(db)
if not qualys_user:
@ -600,6 +618,9 @@ def _refresh_all_agents_impl(db):
criteria = [{"field": "tagName", "operator": "CONTAINS", "value": tag_filter}]
if last_id:
criteria.append({"field": "id", "operator": "GREATER", "value": str(last_id)})
# Mode diff : ajoute filtre lastCheckedIn > timestamp dernier diff sync
if mode == "diff" and last_diff_iso:
criteria.append({"field": "lastCheckedIn", "operator": "GREATER", "value": last_diff_iso})
payload = {"ServiceRequest": {
"preferences": {"limitResults": 100},
"filters": {"Criteria": criteria}
@ -733,7 +754,16 @@ def _refresh_all_agents_impl(db):
last_id = new_last_id
stats["ok"] = True
stats["msg"] = f"{stats['created']} créés, {stats['updated']} mis à jour ({stats['pages']} pages, {stats['errors']} erreurs, {len(tag_filters)} filtres)"
stats["mode"] = mode
stats["msg"] = f"[{mode}] {stats['created']} créés, {stats['updated']} mis à jour ({stats['pages']} pages, {stats['errors']} erreurs, {len(tag_filters)} filtres)"
# Memorise le timestamp pour le prochain diff sync
if mode == "diff":
try:
now_iso = datetime.now(timezone.utc).isoformat()
set_secret(db, "qualys_last_diff_sync", now_iso, "Timestamp dernier sync Qualys diff")
db.commit()
except Exception:
pass
return stats

View File

@ -7,8 +7,11 @@
<p class="text-xs text-gray-500 mt-1">Activation keys et versions des agents déployés</p>
</div>
<div style="display:flex;gap:8px">
<button id="btn-refresh" class="btn-primary px-4 py-2 text-sm" onclick="refreshAgents()">
Rafraîchir depuis Qualys
<button id="btn-refresh-diff" class="btn-primary px-4 py-2 text-sm" onclick="refreshAgents('diff')" title="Pull seulement les assets modifies depuis le dernier sync">
Sync rapide (diff)
</button>
<button id="btn-refresh-full" class="btn-sm bg-cyber-yellow text-black px-4 py-2 text-sm" onclick="refreshAgents('full')" title="Pull complet (5-10 min). A faire 1x par jour">
Sync complete
</button>
<a href="/qualys/deploy" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Déployer</a>
<a href="/qualys/search" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Recherche</a>
@ -43,22 +46,26 @@ function cancelRefresh() {
});
}
function refreshAgents() {
var btn = document.getElementById('btn-refresh');
function refreshAgents(mode) {
mode = mode || 'diff';
var btnDiff = document.getElementById('btn-refresh-diff');
var btnFull = document.getElementById('btn-refresh-full');
var overlay = document.getElementById('refresh-overlay');
var timer = document.getElementById('refresh-timer');
var msgDiv = document.getElementById('refresh-msg');
btn.disabled = true;
if (btnDiff) btnDiff.disabled = true;
if (btnFull) btnFull.disabled = true;
overlay.style.display = 'flex';
msgDiv.style.display = 'none';
var t0 = Date.now();
var iv = setInterval(function(){ timer.textContent = Math.floor((Date.now()-t0)/1000) + 's'; }, 1000);
fetch('/qualys/agents/refresh', {method:'POST', credentials:'same-origin'})
fetch('/qualys/agents/refresh?mode=' + encodeURIComponent(mode), {method:'POST', credentials:'same-origin'})
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok, data:d}; }); })
.then(function(res){
clearInterval(iv);
overlay.style.display = 'none';
btn.disabled = false;
if (btnDiff) btnDiff.disabled = false;
if (btnFull) btnFull.disabled = false;
if(res.ok && res.data.ok){
msgDiv.style.background = '#1a5a2e';
msgDiv.style.color = '#8f8';
@ -75,7 +82,8 @@ function refreshAgents() {
.catch(function(err){
clearInterval(iv);
overlay.style.display = 'none';
btn.disabled = false;
if (btnDiff) btnDiff.disabled = false;
if (btnFull) btnFull.disabled = false;
msgDiv.style.background = '#5a1a1a';
msgDiv.style.color = '#ff3366';
msgDiv.textContent = 'Erreur réseau : ' + err.message;