Qualys agents: bouton Rafraichir + cron 6h
- refresh_all_agents(): bulk sync tous les assets depuis API Qualys QPS - Bouton "Rafraîchir depuis Qualys" sur la page agents - Cron toutes les 6h: refresh_agents.py (prod + demo) - Message de confirmation après refresh
This commit is contained in:
parent
6411774004
commit
4fa5f67c32
@ -512,10 +512,29 @@ async def qualys_agents_page(request: Request, db=Depends(get_db)):
|
|||||||
ctx.update({
|
ctx.update({
|
||||||
"app_name": APP_NAME, "keys": keys, "summary": summary,
|
"app_name": APP_NAME, "keys": keys, "summary": summary,
|
||||||
"no_agent_servers": no_agent, "inactive_agents": inactive,
|
"no_agent_servers": no_agent, "inactive_agents": inactive,
|
||||||
|
"msg": request.query_params.get("msg", ""),
|
||||||
})
|
})
|
||||||
return templates.TemplateResponse("qualys_agents.html", ctx)
|
return templates.TemplateResponse("qualys_agents.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/qualys/agents/refresh")
|
||||||
|
async def qualys_agents_refresh(request: Request, db=Depends(get_db)):
|
||||||
|
user = get_current_user(request)
|
||||||
|
if not user:
|
||||||
|
return RedirectResponse(url="/login")
|
||||||
|
perms = get_user_perms(db, user)
|
||||||
|
if not can_edit(perms, "qualys"):
|
||||||
|
return RedirectResponse(url="/qualys/agents")
|
||||||
|
from ..services.qualys_service import refresh_all_agents
|
||||||
|
try:
|
||||||
|
stats = refresh_all_agents(db)
|
||||||
|
msg = f"refresh_ok_{stats.get('created',0)}_{stats.get('updated',0)}"
|
||||||
|
except Exception as e:
|
||||||
|
import traceback; traceback.print_exc()
|
||||||
|
msg = "refresh_error"
|
||||||
|
return RedirectResponse(url=f"/qualys/agents?msg={msg}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/qualys/agents/export-no-agent")
|
@router.get("/qualys/agents/export-no-agent")
|
||||||
async def export_no_agent_csv(request: Request, db=Depends(get_db)):
|
async def export_no_agent_csv(request: Request, db=Depends(get_db)):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
|
|||||||
16
app/scripts/refresh_agents.py
Normal file
16
app/scripts/refresh_agents.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Cron script: refresh Qualys agents data every 6h"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "/opt/patchcenter")
|
||||||
|
|
||||||
|
from app.database import SessionLocal, SessionLocalDemo
|
||||||
|
from app.services.qualys_service import refresh_all_agents
|
||||||
|
|
||||||
|
for factory, name in [(SessionLocal, "prod"), (SessionLocalDemo, "demo")]:
|
||||||
|
try:
|
||||||
|
db = factory()
|
||||||
|
result = refresh_all_agents(db)
|
||||||
|
print(f"[{name}] {result.get('msg', result)}")
|
||||||
|
db.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[{name}] ERROR: {e}")
|
||||||
@ -532,3 +532,87 @@ def invalidate_search_cache():
|
|||||||
def get_cache_stats():
|
def get_cache_stats():
|
||||||
"""Stats du cache"""
|
"""Stats du cache"""
|
||||||
return _cache.stats()
|
return _cache.stats()
|
||||||
|
|
||||||
|
|
||||||
|
def refresh_all_agents(db):
|
||||||
|
"""Rafraichit tous les agents depuis l'API Qualys QPS (bulk)"""
|
||||||
|
qualys_url, qualys_user, qualys_pass, qualys_proxy = _get_qualys_creds(db)
|
||||||
|
if not qualys_user:
|
||||||
|
return {"ok": False, "msg": "Credentials Qualys non configurés"}
|
||||||
|
proxies = {"https": qualys_proxy, "http": qualys_proxy} if qualys_proxy else None
|
||||||
|
|
||||||
|
stats = {"created": 0, "updated": 0, "errors": 0}
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = requests.post(
|
||||||
|
f"{qualys_url}/qps/rest/2.0/search/am/hostasset",
|
||||||
|
json={"ServiceRequest": {"preferences": {"limitResults": 1000}}},
|
||||||
|
auth=(qualys_user, qualys_pass),
|
||||||
|
verify=False, timeout=120, proxies=proxies,
|
||||||
|
headers={"X-Requested-With": "PatchCenter", "Content-Type": "application/json"})
|
||||||
|
except Exception as e:
|
||||||
|
return {"ok": False, "msg": str(e)}
|
||||||
|
|
||||||
|
if r.status_code != 200 or "SUCCESS" not in r.text:
|
||||||
|
return {"ok": False, "msg": f"API HTTP {r.status_code}"}
|
||||||
|
|
||||||
|
for block in r.text.split("<HostAsset>")[1:]:
|
||||||
|
block = block.split("</HostAsset>")[0]
|
||||||
|
try:
|
||||||
|
asset_id = (parse_xml(block, "id") or [""])[0]
|
||||||
|
name = (parse_xml(block, "name") or [""])[0]
|
||||||
|
hostname = name.split(".")[0].lower() if name else ""
|
||||||
|
address = (parse_xml(block, "address") or [""])[0]
|
||||||
|
fqdn = (parse_xml(block, "fqdn") or [""])[0]
|
||||||
|
os_val = (parse_xml(block, "os") or [""])[0]
|
||||||
|
|
||||||
|
agent_status = ""
|
||||||
|
agent_version = ""
|
||||||
|
last_checkin = None
|
||||||
|
if "<agentInfo>" in block:
|
||||||
|
ab = block[block.find("<agentInfo>"):block.find("</agentInfo>")]
|
||||||
|
agent_status = (parse_xml(ab, "status") or [""])[0]
|
||||||
|
agent_version = (parse_xml(ab, "agentVersion") or [""])[0]
|
||||||
|
lc = (parse_xml(ab, "lastCheckedIn") or [""])[0]
|
||||||
|
last_checkin = lc if lc else None
|
||||||
|
|
||||||
|
os_family = None
|
||||||
|
if any(k in os_val.lower() for k in ("linux", "red hat", "centos", "debian")):
|
||||||
|
os_family = "linux"
|
||||||
|
elif "windows" in os_val.lower():
|
||||||
|
os_family = "windows"
|
||||||
|
|
||||||
|
# Match server
|
||||||
|
srv = db.execute(text("SELECT id FROM servers WHERE LOWER(hostname)=LOWER(:h)"),
|
||||||
|
{"h": hostname}).fetchone()
|
||||||
|
server_id = srv.id if srv else None
|
||||||
|
|
||||||
|
existing = db.execute(text("SELECT id FROM qualys_assets WHERE qualys_asset_id=:qid"),
|
||||||
|
{"qid": int(asset_id)}).fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
db.execute(text("""UPDATE qualys_assets SET
|
||||||
|
name=:name, hostname=:hn, fqdn=:fqdn, ip_address=:ip, os=:os, os_family=:osf,
|
||||||
|
agent_status=:ast, agent_version=:av, last_checkin=:lc, server_id=:sid, updated_at=now()
|
||||||
|
WHERE qualys_asset_id=:qid"""),
|
||||||
|
{"qid": int(asset_id), "name": name, "hn": hostname, "fqdn": fqdn or None,
|
||||||
|
"ip": address or None, "os": os_val, "osf": os_family,
|
||||||
|
"ast": agent_status, "av": agent_version, "lc": last_checkin, "sid": server_id})
|
||||||
|
stats["updated"] += 1
|
||||||
|
else:
|
||||||
|
db.execute(text("""INSERT INTO qualys_assets
|
||||||
|
(qualys_asset_id, name, hostname, fqdn, ip_address, os, os_family,
|
||||||
|
agent_status, agent_version, last_checkin, server_id)
|
||||||
|
VALUES (:qid, :name, :hn, :fqdn, :ip, :os, :osf, :ast, :av, :lc, :sid)"""),
|
||||||
|
{"qid": int(asset_id), "name": name, "hn": hostname, "fqdn": fqdn or None,
|
||||||
|
"ip": address or None, "os": os_val, "osf": os_family,
|
||||||
|
"ast": agent_status, "av": agent_version, "lc": last_checkin, "sid": server_id})
|
||||||
|
stats["created"] += 1
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
stats["errors"] += 1
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
stats["ok"] = True
|
||||||
|
stats["msg"] = f"{stats['created']} créés, {stats['updated']} mis à jour"
|
||||||
|
return stats
|
||||||
|
|||||||
@ -6,9 +6,28 @@
|
|||||||
<h2 class="text-xl font-bold text-cyber-accent">Agents Qualys</h2>
|
<h2 class="text-xl font-bold text-cyber-accent">Agents Qualys</h2>
|
||||||
<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>
|
||||||
<a href="/qualys/search" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Recherche</a>
|
<div style="display:flex;gap:8px">
|
||||||
|
<form method="POST" action="/qualys/agents/refresh" style="display:inline">
|
||||||
|
<button type="submit" class="btn-primary px-4 py-2 text-sm"
|
||||||
|
onclick="this.disabled=true;this.textContent='Rafraîchissement...'">
|
||||||
|
Rafraîchir depuis Qualys
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if 'refresh_ok' in msg %}
|
||||||
|
<div style="background:#1a5a2e;color:#8f8;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
|
||||||
|
Données rafraîchies depuis Qualys.
|
||||||
|
</div>
|
||||||
|
{% elif msg == 'refresh_error' %}
|
||||||
|
<div style="background:#5a1a1a;color:#ff3366;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
|
||||||
|
Erreur lors du rafraîchissement.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- KPIs agents -->
|
<!-- KPIs agents -->
|
||||||
<div style="display:flex;flex-wrap:nowrap;gap:8px;margin-bottom:16px;">
|
<div style="display:flex;flex-wrap:nowrap;gap:8px;margin-bottom:16px;">
|
||||||
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-accent">{{ summary.total_assets or 0 }}</div><div class="text-xs text-gray-500">Total assets</div></div>
|
<div class="card p-3 text-center" style="flex:1;min-width:0"><div class="text-2xl font-bold text-cyber-accent">{{ summary.total_assets or 0 }}</div><div class="text-xs text-gray-500">Total assets</div></div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user