Export CSV avec filtres (domaine/zone/recherche/KPI), BOM UTF-8

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalid MOUTAOUAKIL 2026-04-06 17:20:35 +02:00
parent c42708db75
commit df03852c86
2 changed files with 76 additions and 1 deletions

View File

@ -1,7 +1,7 @@
"""Router Audit Complet — import JSON, liste, detail, carte flux, carte applicative""" """Router Audit Complet — import JSON, liste, detail, carte flux, carte applicative"""
import json import json
from fastapi import APIRouter, Request, Depends, UploadFile, File from fastapi import APIRouter, Request, Depends, UploadFile, File
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy import text from sqlalchemy import text
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, base_context from ..dependencies import get_db, get_current_user, get_user_perms, can_view, base_context
@ -192,6 +192,80 @@ async def audit_full_import(request: Request, db=Depends(get_db),
) )
@router.get("/audit-full/export-csv")
async def audit_full_export_csv(request: Request, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
import io, csv
filtre = request.query_params.get("filter", "")
search = request.query_params.get("q", "").strip()
domain = request.query_params.get("domain", "")
audits = get_latest_audits(db, limit=9999)
# Memes filtres que la page liste
if filtre == "reboot":
audits = [a for a in audits if a.reboot_required]
elif filtre == "uptime":
audits = [a for a in audits if a.uptime and ("month" in a.uptime or "year" in a.uptime)]
elif filtre and filtre.startswith("app_"):
app_patterns = {
"app_postgres": "postgres", "app_mariadb": "mariadb|mysqld",
"app_hana": "hdb|sapstart|HANA", "app_oracle": "ora_pmon|oracle",
"app_httpd": "httpd", "app_nginx": "nginx", "app_haproxy": "haproxy",
"app_tomcat": "tomcat|catalina", "app_nodejs": "node",
"app_redis": "redis", "app_mongodb": "mongod",
"app_elastic": "elasticsearch|opensearch", "app_container": "docker|podman",
"app_java": "java|\\.jar",
}
pattern = app_patterns.get(filtre, "")
if pattern:
ids = {r.id for r in db.execute(text("""
SELECT id FROM server_audit_full
WHERE status = 'ok'
AND (services::text ~* :pat OR listen_ports::text ~* :pat OR processes::text ~* :pat)
AND id IN (SELECT DISTINCT ON (hostname) id FROM server_audit_full WHERE status = 'ok' ORDER BY hostname, audit_date DESC)
"""), {"pat": pattern}).fetchall()}
audits = [a for a in audits if a.id in ids]
if domain:
zone_servers = {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 = :name"
), {"name": domain}).fetchall()}
if zone_servers:
audits = [a for a in audits if a.hostname in zone_servers]
else:
domain_servers = {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()}
audits = [a for a in audits if a.hostname in domain_servers]
if search:
q = search.lower()
audits = [a for a in audits if q in a.hostname.lower()]
# Generer CSV
output = io.StringIO()
writer = csv.writer(output, delimiter=";")
writer.writerow(["Hostname", "OS", "Kernel", "Uptime", "Services", "Processus",
"Ports", "Connexions", "Reboot requis", "Date audit"])
for a in audits:
writer.writerow([
a.hostname, a.os_release or "", a.kernel or "", a.uptime or "",
a.svc_count, a.proc_count, a.port_count, a.conn_count,
"Oui" if a.reboot_required else "Non",
a.audit_date.strftime("%Y-%m-%d %H:%M") if a.audit_date else "",
])
output.seek(0)
return StreamingResponse(
iter(["\ufeff" + output.getvalue()]),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=audit_serveurs.csv"},
)
@router.get("/audit-full/flow-map", response_class=HTMLResponse) @router.get("/audit-full/flow-map", response_class=HTMLResponse)
async def audit_full_flow_map(request: Request, db=Depends(get_db)): async def audit_full_flow_map(request: Request, db=Depends(get_db)):
user = get_current_user(request) user = get_current_user(request)

View File

@ -11,6 +11,7 @@
<input type="file" name="file" accept=".json" class="text-xs" required> <input type="file" name="file" accept=".json" class="text-xs" required>
<button type="submit" class="btn-primary px-4 py-2 text-sm" data-loading="Import en cours...|Insertion des donnees">Importer JSON</button> <button type="submit" class="btn-primary px-4 py-2 text-sm" data-loading="Import en cours...|Insertion des donnees">Importer JSON</button>
</form> </form>
<a href="/audit-full/export-csv{% if filter %}?filter={{ filter }}{% endif %}{% if search %}&q={{ search }}{% endif %}{% if domain %}&domain={{ domain }}{% endif %}" class="btn-sm bg-cyber-green text-black px-4 py-2">Exporter CSV</a>
<a href="/audit-full/flow-map" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Carte flux</a> <a href="/audit-full/flow-map" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Carte flux</a>
</div> </div>
</div> </div>