Export CSV patching avec filtres (année, scope, domaine, recherche)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalid MOUTAOUAKIL 2026-04-07 01:30:17 +02:00
parent 7f5e5c83eb
commit 769e199735
2 changed files with 74 additions and 1 deletions

View File

@ -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)

View File

@ -13,6 +13,8 @@
<a href="/audit-full/patching?year={{ year }}" class="btn-sm {% if not scope %}bg-cyber-accent text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">Tous</a>
<a href="/audit-full/patching?year={{ year }}&scope=secops" class="btn-sm {% if scope == 'secops' %}bg-cyber-green text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">SecOps</a>
<a href="/audit-full/patching?year={{ year }}&scope=other" class="btn-sm {% if scope == 'other' %}bg-cyber-yellow text-black{% else %}bg-cyber-border text-gray-400{% endif %} px-3 py-1">Hors SecOps</a>
<span class="text-gray-600 mx-1">|</span>
<a href="/audit-full/patching/export-csv?year={{ year }}{% if scope %}&scope={{ scope }}{% endif %}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}" class="btn-sm bg-cyber-green text-black px-3 py-1">CSV</a>
</div>
</div>
@ -187,7 +189,7 @@
<tr class="hover:bg-cyber-hover cursor-pointer" onclick="location.href='/audit-full/{{ s.id }}'">
<td class="p-2 font-mono text-cyber-accent font-bold">{{ s.hostname }}</td>
<td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td>
<td class="p-2 text-center"><span class="badge {% if s.env == 'Production' %}badge-green{% elif s.env == 'Recette' %}badge-yellow{% else %}badge-gray{% endif %}"title="{{ s.env or '' }}">{{ (s.env or '-')[:6] }}</span></td>
<td class="p-2 text-center"><span class="badge {% if s.env == 'Production' %}badge-green{% elif s.env == 'Recette' %}badge-yellow{% else %}badge-gray{% endif %}">{{ (s.env or '-')[:6] }}</span></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 font-bold {% if (s.patch_count or 0) >= 2 %}text-cyber-green{% elif (s.patch_count or 0) == 1 %}text-green-300{% else %}text-cyber-red{% endif %}">{{ s.patch_count or 0 }}</td>
<td class="p-2 font-mono text-gray-400">{% if s.patch_weeks %}{% for w in s.patch_weeks.split(',') %}<span class="inline-block px-1 rounded text-xs {% if w == 'S15' %}bg-green-900/30 text-cyber-green{% else %}bg-cyber-border text-gray-400{% endif %} mr-1">{{ w }}</span>{% endfor %}{% else %}-{% endif %}</td>