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:
parent
55f81de986
commit
a62f9a4146
@ -518,7 +518,8 @@ def qualys_agents_page(request: Request, db=Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/qualys/agents/refresh")
|
@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
|
from fastapi.responses import JSONResponse
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
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)
|
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
|
||||||
from ..services.qualys_service import refresh_all_agents
|
from ..services.qualys_service import refresh_all_agents
|
||||||
try:
|
try:
|
||||||
stats = refresh_all_agents(db)
|
stats = refresh_all_agents(db, mode=mode)
|
||||||
if stats.get("busy"):
|
if stats.get("busy"):
|
||||||
return JSONResponse(stats, status_code=409)
|
return JSONResponse(stats, status_code=409)
|
||||||
if not stats.get("ok"):
|
if not stats.get("ok"):
|
||||||
|
|||||||
@ -544,28 +544,46 @@ def get_cache_stats():
|
|||||||
return _cache.stats()
|
return _cache.stats()
|
||||||
|
|
||||||
|
|
||||||
def refresh_all_agents(db):
|
def refresh_all_agents(db, mode="diff"):
|
||||||
"""Rafraichit tous les agents depuis l'API Qualys QPS (bulk, paginé)"""
|
"""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
|
global _refresh_running
|
||||||
if not _refresh_lock.acquire(blocking=False):
|
if not _refresh_lock.acquire(blocking=False):
|
||||||
return {"ok": False, "msg": "Une synchronisation Qualys est déjà en cours", "busy": True}
|
return {"ok": False, "msg": "Une synchronisation Qualys est déjà en cours", "busy": True}
|
||||||
_refresh_running = True
|
_refresh_running = True
|
||||||
_refresh_cancel.clear()
|
_refresh_cancel.clear()
|
||||||
try:
|
try:
|
||||||
return _refresh_all_agents_impl(db)
|
return _refresh_all_agents_impl(db, mode=mode)
|
||||||
finally:
|
finally:
|
||||||
_refresh_running = False
|
_refresh_running = False
|
||||||
_refresh_lock.release()
|
_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)"""
|
"""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)
|
from .secrets_service import get_secret, set_secret
|
||||||
total = db.execute(text("SELECT COUNT(*) FROM qualys_assets")).scalar() or 0
|
from datetime import datetime, timezone
|
||||||
if total > 0:
|
|
||||||
stale = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE updated_at < now() - interval '40 minutes'")).scalar() or 0
|
# En mode diff : recupere le timestamp du dernier diff sync
|
||||||
if stale == 0:
|
last_diff_iso = None
|
||||||
return {"ok": True, "msg": f"Tous les {total} assets sont récents (< 40 min), rien à faire", "skipped_all": True}
|
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)
|
qualys_url, qualys_user, qualys_pass, qualys_proxy = _get_qualys_creds(db)
|
||||||
if not qualys_user:
|
if not qualys_user:
|
||||||
@ -600,6 +618,9 @@ def _refresh_all_agents_impl(db):
|
|||||||
criteria = [{"field": "tagName", "operator": "CONTAINS", "value": tag_filter}]
|
criteria = [{"field": "tagName", "operator": "CONTAINS", "value": tag_filter}]
|
||||||
if last_id:
|
if last_id:
|
||||||
criteria.append({"field": "id", "operator": "GREATER", "value": str(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": {
|
payload = {"ServiceRequest": {
|
||||||
"preferences": {"limitResults": 100},
|
"preferences": {"limitResults": 100},
|
||||||
"filters": {"Criteria": criteria}
|
"filters": {"Criteria": criteria}
|
||||||
@ -733,7 +754,16 @@ def _refresh_all_agents_impl(db):
|
|||||||
last_id = new_last_id
|
last_id = new_last_id
|
||||||
|
|
||||||
stats["ok"] = True
|
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
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,8 +7,11 @@
|
|||||||
<p class="text-xs text-gray-500 mt-1">Activation keys et versions des agents déployés</p>
|
<p class="text-xs text-gray-500 mt-1">Activation keys et versions des agents déployés</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:8px">
|
<div style="display:flex;gap:8px">
|
||||||
<button id="btn-refresh" class="btn-primary px-4 py-2 text-sm" onclick="refreshAgents()">
|
<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">
|
||||||
Rafraîchir depuis Qualys
|
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>
|
</button>
|
||||||
<a href="/qualys/deploy" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Déployer</a>
|
<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>
|
<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() {
|
function refreshAgents(mode) {
|
||||||
var btn = document.getElementById('btn-refresh');
|
mode = mode || 'diff';
|
||||||
|
var btnDiff = document.getElementById('btn-refresh-diff');
|
||||||
|
var btnFull = document.getElementById('btn-refresh-full');
|
||||||
var overlay = document.getElementById('refresh-overlay');
|
var overlay = document.getElementById('refresh-overlay');
|
||||||
var timer = document.getElementById('refresh-timer');
|
var timer = document.getElementById('refresh-timer');
|
||||||
var msgDiv = document.getElementById('refresh-msg');
|
var msgDiv = document.getElementById('refresh-msg');
|
||||||
btn.disabled = true;
|
if (btnDiff) btnDiff.disabled = true;
|
||||||
|
if (btnFull) btnFull.disabled = true;
|
||||||
overlay.style.display = 'flex';
|
overlay.style.display = 'flex';
|
||||||
msgDiv.style.display = 'none';
|
msgDiv.style.display = 'none';
|
||||||
var t0 = Date.now();
|
var t0 = Date.now();
|
||||||
var iv = setInterval(function(){ timer.textContent = Math.floor((Date.now()-t0)/1000) + 's'; }, 1000);
|
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(r){ return r.json().then(function(d){ return {ok:r.ok, data:d}; }); })
|
||||||
.then(function(res){
|
.then(function(res){
|
||||||
clearInterval(iv);
|
clearInterval(iv);
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
btn.disabled = false;
|
if (btnDiff) btnDiff.disabled = false;
|
||||||
|
if (btnFull) btnFull.disabled = false;
|
||||||
if(res.ok && res.data.ok){
|
if(res.ok && res.data.ok){
|
||||||
msgDiv.style.background = '#1a5a2e';
|
msgDiv.style.background = '#1a5a2e';
|
||||||
msgDiv.style.color = '#8f8';
|
msgDiv.style.color = '#8f8';
|
||||||
@ -75,7 +82,8 @@ function refreshAgents() {
|
|||||||
.catch(function(err){
|
.catch(function(err){
|
||||||
clearInterval(iv);
|
clearInterval(iv);
|
||||||
overlay.style.display = 'none';
|
overlay.style.display = 'none';
|
||||||
btn.disabled = false;
|
if (btnDiff) btnDiff.disabled = false;
|
||||||
|
if (btnFull) btnFull.disabled = false;
|
||||||
msgDiv.style.background = '#5a1a1a';
|
msgDiv.style.background = '#5a1a1a';
|
||||||
msgDiv.style.color = '#ff3366';
|
msgDiv.style.color = '#ff3366';
|
||||||
msgDiv.textContent = 'Erreur réseau : ' + err.message;
|
msgDiv.textContent = 'Erreur réseau : ' + err.message;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user