Compare commits

...

4 Commits

9 changed files with 44 additions and 5 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -305,7 +305,7 @@ async def qualys_resync_assets(request: Request, db=Depends(get_db)):
if result.get("ok") and result.get("assets"): if result.get("ok") and result.get("assets"):
_save_api_results_to_db(db, result["assets"]) _save_api_results_to_db(db, result["assets"])
ok += 1 ok += 1
return RedirectResponse(url=_bulk_return_url(form, f"resync_{ok}"), status_code=303) return RedirectResponse(url=_bulk_return_url(form, f"resync_{ok}") + "&force=1", status_code=303)
@router.get("/qualys/bulk/tags-for-assets") @router.get("/qualys/bulk/tags-for-assets")
@ -466,11 +466,28 @@ async def qualys_search(request: Request, db=Depends(get_db),
all_tags = db.execute(text("SELECT qualys_tag_id, name FROM qualys_tags ORDER BY name")).fetchall() all_tags = db.execute(text("SELECT qualys_tag_id, name FROM qualys_tags ORDER BY name")).fetchall()
# KPI : total / avec vuln / sans vuln + filtrage vuln_filter (with|zero)
vuln_filter = request.query_params.get("vuln_filter", "")
def _vuln_total(a):
vc = vuln_map.get(str(a.ip_address), {})
if isinstance(vc, dict):
return int(vc.get("total", 0) or 0)
return int(vc or 0)
kpi_total = len(assets) if assets else 0
kpi_with_vuln = sum(1 for a in assets if _vuln_total(a) > 0) if assets else 0
kpi_zero_vuln = kpi_total - kpi_with_vuln
if assets and vuln_filter == "with":
assets = [a for a in assets if _vuln_total(a) > 0]
elif assets and vuln_filter == "zero":
assets = [a for a in assets if _vuln_total(a) == 0]
ctx = base_context(request, db, user) ctx = base_context(request, db, user)
ctx.update({ ctx.update({
"app_name": APP_NAME, "assets": assets, "search": search, "app_name": APP_NAME, "assets": assets, "search": search,
"field": field, "api_msg": api_msg, "field": field, "api_msg": api_msg,
"all_tags": all_tags, "vuln_map": vuln_map, "all_tags": all_tags, "vuln_map": vuln_map,
"kpi_total": kpi_total, "kpi_with_vuln": kpi_with_vuln, "kpi_zero_vuln": kpi_zero_vuln,
"vuln_filter": vuln_filter,
"cache_info": cache_info, "cache_info": cache_info,
"can_edit_qualys": can_edit(perms, "qualys"), "can_edit_qualys": can_edit(perms, "qualys"),
"msg": request.query_params.get("msg"), "msg": request.query_params.get("msg"),
@ -499,7 +516,7 @@ def qualys_agents_page(request: Request, db=Depends(get_db)):
# Serveurs sans agent Qualys (match via server_id pour gerer alias/IP) # Serveurs sans agent Qualys (match via server_id pour gerer alias/IP)
# Exclut les workstations Win10/11 = portables/postes dev # Exclut les workstations Win10/11 = portables/postes dev
no_agent_rows = db.execute(text(""" no_agent_rows = db.execute(text("""
SELECT s.hostname, s.os_family, s.etat, d.name as domain, e.name as env, z.name as zone SELECT s.hostname, s.os_family, s.os_version, s.etat, d.name as domain, e.name as env, z.name as zone
FROM servers s FROM servers s
LEFT JOIN domain_environments de ON s.domain_env_id = de.id LEFT JOIN domain_environments de ON s.domain_env_id = de.id
LEFT JOIN domains d ON de.domain_id = d.id LEFT JOIN domains d ON de.domain_id = d.id
@ -516,7 +533,7 @@ def qualys_agents_page(request: Request, db=Depends(get_db)):
AND COALESCE(s.os_version, '') NOT ILIKE '%Workstation%' AND COALESCE(s.os_version, '') NOT ILIKE '%Workstation%'
ORDER BY s.hostname ORDER BY s.hostname
""")).fetchall() """)).fetchall()
no_agent = [{"hostname": r.hostname, "os_family": r.os_family, "etat": r.etat, no_agent = [{"hostname": r.hostname, "os_family": r.os_family, "os_version": r.os_version or "", "etat": r.etat,
"domain": r.domain or "", "env": r.env or "", "zone": r.zone or ""} for r in no_agent_rows] "domain": r.domain or "", "env": r.env or "", "zone": r.zone or ""} for r in no_agent_rows]
# Agents inactifs # Agents inactifs
@ -591,7 +608,7 @@ async def export_no_agent_csv(request: Request, db=Depends(get_db)):
return RedirectResponse(url="/qualys/agents") return RedirectResponse(url="/qualys/agents")
import io, csv as _csv import io, csv as _csv
rows = db.execute(text(""" rows = db.execute(text("""
SELECT s.hostname, s.os_family, s.etat, d.name as domain, e.name as env, z.name as zone SELECT s.hostname, s.os_family, s.os_version, s.etat, d.name as domain, e.name as env, z.name as zone
FROM servers s FROM servers s
LEFT JOIN domain_environments de ON s.domain_env_id = de.id LEFT JOIN domain_environments de ON s.domain_env_id = de.id
LEFT JOIN domains d ON de.domain_id = d.id LEFT JOIN domains d ON de.domain_id = d.id
@ -604,7 +621,7 @@ async def export_no_agent_csv(request: Request, db=Depends(get_db)):
w = _csv.writer(output, delimiter=";") w = _csv.writer(output, delimiter=";")
w.writerow(["Hostname", "OS", "Domaine", "Environnement", "Zone", "Etat"]) w.writerow(["Hostname", "OS", "Domaine", "Environnement", "Zone", "Etat"])
for r in rows: for r in rows:
w.writerow([r.hostname, r.os_family or "", r.domain or "", r.env or "", r.zone or "", r.etat or ""]) w.writerow([r.hostname, r.os_family or "", r.os_version or "", r.domain or "", r.env or "", r.zone or "", r.etat or ""])
output.seek(0) output.seek(0)
return StreamingResponse( return StreamingResponse(
iter(["\ufeff" + output.getvalue()]), iter(["\ufeff" + output.getvalue()]),

View File

@ -215,6 +215,7 @@ function refreshAgents(mode) {
<thead><tr> <thead><tr>
<th class="text-left p-2">Hostname</th> <th class="text-left p-2">Hostname</th>
<th class="p-2">OS</th> <th class="p-2">OS</th>
<th class="p-2">Version OS</th>
<th class="p-2">Domaine</th> <th class="p-2">Domaine</th>
<th class="p-2">Env</th> <th class="p-2">Env</th>
<th class="p-2">Zone</th> <th class="p-2">Zone</th>
@ -231,6 +232,7 @@ function refreshAgents(mode) {
"> ">
<td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td> <td class="p-2 font-mono text-cyber-accent">{{ s.hostname }}</td>
<td class="p-2 text-center">{{ s.os_family or '-' }}</td> <td class="p-2 text-center">{{ s.os_family or '-' }}</td>
<td class="p-2 text-center text-xs text-gray-300">{{ s.os_version or '-' }}</td>
<td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td> <td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td>
<td class="p-2 text-center">{{ s.env or '-' }}</td> <td class="p-2 text-center">{{ s.env or '-' }}</td>
<td class="p-2 text-center">{% if s.zone == 'DMZ' %}<span class="badge badge-red">DMZ</span>{% else %}{{ s.zone or '-' }}{% endif %}</td> <td class="p-2 text-center">{% if s.zone == 'DMZ' %}<span class="badge badge-red">DMZ</span>{% else %}{{ s.zone or '-' }}{% endif %}</td>

View File

@ -48,6 +48,26 @@
</div> </div>
{% endif %} {% endif %}
{% if search and kpi_total is defined %}
<div class="grid grid-cols-3 gap-3 mb-3">
<a href="/qualys/search?field={{ field }}&search={{ search }}" class="card p-3 hover:border-cyber-accent transition {% if not vuln_filter %}border-cyber-accent{% endif %}">
<div class="text-xs text-gray-400 uppercase">Total</div>
<div class="text-2xl font-bold text-cyber-accent">{{ kpi_total }}</div>
<div class="text-xs text-gray-500">{% if vuln_filter %}Voir tous{% else %}Affichage actuel{% endif %}</div>
</a>
<a href="/qualys/search?field={{ field }}&search={{ search }}&vuln_filter=with" class="card p-3 hover:border-cyber-red transition {% if vuln_filter == 'with' %}border-cyber-red{% endif %}">
<div class="text-xs text-gray-400 uppercase">Avec vulnerabilites</div>
<div class="text-2xl font-bold text-red-400">{{ kpi_with_vuln }}</div>
<div class="text-xs text-gray-500">Cliquer pour filtrer</div>
</a>
<a href="/qualys/search?field={{ field }}&search={{ search }}&vuln_filter=zero" class="card p-3 hover:border-cyber-green transition {% if vuln_filter == 'zero' %}border-cyber-green{% endif %}">
<div class="text-xs text-gray-400 uppercase">Sans vulnerabilite</div>
<div class="text-2xl font-bold text-cyber-green">{{ kpi_zero_vuln }}</div>
<div class="text-xs text-gray-500">Cliquer pour filtrer</div>
</a>
</div>
{% endif %}
<!-- Panel détail --> <!-- Panel détail -->
<div id="detail-loading" class="card mb-4 p-6 flex items-center justify-center gap-3" style="display:none;"> <div id="detail-loading" class="card mb-4 p-6 flex items-center justify-center gap-3" style="display:none;">
<div style="width:20px;height:20px;border:2px solid #22c55e;border-top-color:transparent;border-radius:50%;animation:spin 0.8s linear infinite;"></div> <div style="width:20px;height:20px;border:2px solid #22c55e;border-top-color:transparent;border-radius:50%;animation:spin 0.8s linear infinite;"></div>