diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py index 634a2de..25e1624 100644 --- a/app/routers/audit_full.py +++ b/app/routers/audit_full.py @@ -408,6 +408,77 @@ async def audit_full_patching(request: Request, db=Depends(get_db)): return templates.TemplateResponse("audit_full_patching.html", ctx) +@router.get("/audit-full/patching/export-csv") +async def patching_export_csv(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + + import io, csv + year = int(request.query_params.get("year", "2026")) + search = request.query_params.get("q", "").strip() + domain = request.query_params.get("domain", "") + scope = request.query_params.get("scope", "") + + yr_count = "patch_count_2026" if year == 2026 else "patch_count_2025" + yr_weeks = "patch_weeks_2026" if year == 2026 else "patch_weeks_2025" + + servers = db.execute(text( + f"SELECT DISTINCT ON (saf.hostname) saf.hostname," + f" saf.{yr_count} as patch_count, saf.{yr_weeks} as patch_weeks," + f" saf.last_patch_date, saf.last_patch_week, saf.last_patch_year," + f" saf.patch_status_2026," + f" d.name as domain, e.name as env, z.name as zone, s.os_family, s.etat" + f" FROM server_audit_full saf" + f" LEFT JOIN servers s ON saf.server_id = s.id" + f" LEFT JOIN domain_environments de ON s.domain_env_id = de.id" + f" LEFT JOIN domains d ON de.domain_id = d.id" + f" LEFT JOIN environments e ON de.environment_id = e.id" + f" LEFT JOIN zones z ON s.zone_id = z.id" + f" WHERE saf.status IN ('ok','partial')" + f" ORDER BY saf.hostname, saf.audit_date DESC" + )).fetchall() + + if scope == "secops": + secops = {r.hostname for r in db.execute(text("SELECT hostname FROM servers WHERE patch_os_owner = 'secops'")).fetchall()} + servers = [s for s in servers if s.hostname in secops] + elif scope == "other": + secops = {r.hostname for r in db.execute(text("SELECT hostname FROM servers WHERE patch_os_owner = 'secops'")).fetchall()} + servers = [s for s in servers if s.hostname not in secops] + if domain: + zone_hosts = {r.hostname for r in db.execute(text( + "SELECT s.hostname FROM servers s JOIN zones z ON s.zone_id = z.id WHERE z.name = :n" + ), {"n": domain}).fetchall()} + if zone_hosts: + servers = [s for s in servers if s.hostname in zone_hosts] + else: + dom_hosts = {r.hostname for r in db.execute(text( + "SELECT s.hostname FROM servers s JOIN domain_environments de ON s.domain_env_id = de.id" + " JOIN domains d ON de.domain_id = d.id WHERE d.code = :dc" + ), {"dc": domain}).fetchall()} + servers = [s for s in servers if s.hostname in dom_hosts] + if search: + servers = [s for s in servers if search.lower() in s.hostname.lower()] + + output = io.StringIO() + w = csv.writer(output, delimiter=";") + w.writerow(["Hostname", "OS", "Domaine", "Environnement", "Zone", "Etat", + "Nb patches", "Semaines", "Dernier patch", "Statut"]) + for s in servers: + w.writerow([ + s.hostname, s.os_family or "", s.domain or "", s.env or "", + s.zone or "", s.etat or "", s.patch_count or 0, + s.patch_weeks or "", s.last_patch_date or s.last_patch_week or "", + s.patch_status_2026 or "", + ]) + + output.seek(0) + return StreamingResponse( + iter(["\ufeff" + output.getvalue()]), + media_type="text/csv", + headers={"Content-Disposition": f"attachment; filename=patching_{year}.csv"}) + + @router.get("/audit-full/export-csv") async def audit_full_export_csv(request: Request, db=Depends(get_db)): user = get_current_user(request) diff --git a/app/templates/audit_full_patching.html b/app/templates/audit_full_patching.html index 942da9d..ff2cfec 100644 --- a/app/templates/audit_full_patching.html +++ b/app/templates/audit_full_patching.html @@ -13,6 +13,8 @@ Tous SecOps Hors SecOps + | + CSV @@ -187,7 +189,7 @@ {{ s.hostname }} {{ s.domain or '-' }} - {{ (s.env or '-')[:6] }} + {{ (s.env or '-')[:6] }} {% if s.zone == 'DMZ' %}DMZ{% else %}{{ s.zone or '-' }}{% endif %} {{ s.patch_count or 0 }} {% if s.patch_weeks %}{% for w in s.patch_weeks.split(',') %}{{ w }}{% endfor %}{% else %}-{% endif %}