diff --git a/app/main.py b/app/main.py
index a92eb08..01fc31f 100644
--- a/app/main.py
+++ b/app/main.py
@@ -6,7 +6,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
from .config import APP_NAME, APP_VERSION
from .dependencies import get_current_user, get_user_perms
from .database import SessionLocal
-from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, safe_patching
+from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, safe_patching, audit_full
class PermissionsMiddleware(BaseHTTPMiddleware):
@@ -42,6 +42,7 @@ app.include_router(audit.router)
app.include_router(contacts.router)
app.include_router(qualys.router)
app.include_router(safe_patching.router)
+app.include_router(audit_full.router)
@app.get("/")
diff --git a/app/routers/audit_full.py b/app/routers/audit_full.py
new file mode 100644
index 0000000..27bc243
--- /dev/null
+++ b/app/routers/audit_full.py
@@ -0,0 +1,104 @@
+"""Router Audit Complet — import JSON, liste, detail, carte flux, carte applicative"""
+import json
+from fastapi import APIRouter, Request, Depends, UploadFile, File
+from fastapi.responses import HTMLResponse, RedirectResponse
+from fastapi.templating import Jinja2Templates
+from sqlalchemy import text
+from ..dependencies import get_db, get_current_user, get_user_perms, can_view, base_context
+from ..services.server_audit_full_service import (
+ import_json_report, get_latest_audits, get_audit_detail,
+ get_flow_map, get_flow_map_for_server, get_app_map,
+)
+from ..config import APP_NAME
+
+router = APIRouter()
+templates = Jinja2Templates(directory="app/templates")
+
+
+@router.get("/audit-full", response_class=HTMLResponse)
+async def audit_full_list(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_view(perms, "audit"):
+ return RedirectResponse(url="/dashboard")
+
+ audits = get_latest_audits(db)
+ ctx = base_context(request, db, user)
+ ctx.update({
+ "app_name": APP_NAME, "audits": audits,
+ "msg": request.query_params.get("msg"),
+ })
+ return templates.TemplateResponse("audit_full_list.html", ctx)
+
+
+@router.post("/audit-full/import")
+async def audit_full_import(request: Request, db=Depends(get_db),
+ file: UploadFile = File(...)):
+ user = get_current_user(request)
+ if not user:
+ return RedirectResponse(url="/login")
+
+ try:
+ content = await file.read()
+ json_data = json.loads(content.decode("utf-8-sig"))
+ imported, errors = import_json_report(db, json_data)
+ return RedirectResponse(
+ url=f"/audit-full?msg=imported_{imported}_{errors}",
+ status_code=303,
+ )
+ except Exception as e:
+ return RedirectResponse(
+ url=f"/audit-full?msg=error_{str(e)[:50]}",
+ status_code=303,
+ )
+
+
+@router.get("/audit-full/{audit_id}", response_class=HTMLResponse)
+async def audit_full_detail(request: Request, audit_id: int, db=Depends(get_db)):
+ user = get_current_user(request)
+ if not user:
+ return RedirectResponse(url="/login")
+
+ audit = get_audit_detail(db, audit_id)
+ if not audit:
+ return RedirectResponse(url="/audit-full")
+
+ # Flux pour ce serveur
+ flows = get_flow_map_for_server(db, audit.hostname)
+
+ ctx = base_context(request, db, user)
+ ctx.update({
+ "app_name": APP_NAME, "a": audit, "flows": flows,
+ "services": audit.services if isinstance(audit.services, list) else json.loads(audit.services or "[]"),
+ "processes": audit.processes if isinstance(audit.processes, list) else json.loads(audit.processes or "[]"),
+ "listen_ports": audit.listen_ports if isinstance(audit.listen_ports, list) else json.loads(audit.listen_ports or "[]"),
+ "connections": audit.connections if isinstance(audit.connections, list) else json.loads(audit.connections or "[]"),
+ "flux_in": audit.flux_in if isinstance(audit.flux_in, list) else json.loads(audit.flux_in or "[]"),
+ "flux_out": audit.flux_out if isinstance(audit.flux_out, list) else json.loads(audit.flux_out or "[]"),
+ "disk_usage": audit.disk_usage if isinstance(audit.disk_usage, list) else json.loads(audit.disk_usage or "[]"),
+ "interfaces": audit.interfaces if isinstance(audit.interfaces, list) else json.loads(audit.interfaces or "[]"),
+ "correlation": audit.correlation_matrix if isinstance(audit.correlation_matrix, list) else json.loads(audit.correlation_matrix or "[]"),
+ "outbound": audit.outbound_only if isinstance(audit.outbound_only, list) else json.loads(audit.outbound_only or "[]"),
+ "firewall": audit.firewall if isinstance(audit.firewall, dict) else json.loads(audit.firewall or "{}"),
+ "conn_wait": audit.conn_wait if isinstance(audit.conn_wait, list) else json.loads(audit.conn_wait or "[]"),
+ "traffic": audit.traffic if isinstance(audit.traffic, list) else json.loads(audit.traffic or "[]"),
+ })
+ return templates.TemplateResponse("audit_full_detail.html", ctx)
+
+
+@router.get("/audit-full/flow-map", response_class=HTMLResponse)
+async def audit_full_flow_map(request: Request, db=Depends(get_db)):
+ user = get_current_user(request)
+ if not user:
+ return RedirectResponse(url="/login")
+
+ flows = get_flow_map(db)
+ app_map = get_app_map(db)
+
+ ctx = base_context(request, db, user)
+ ctx.update({
+ "app_name": APP_NAME, "flows": flows, "app_map": app_map,
+ })
+ return templates.TemplateResponse("audit_full_flowmap.html", ctx)
diff --git a/app/scripts/server_audit.sh b/app/scripts/server_audit.sh
new file mode 100755
index 0000000..453455d
--- /dev/null
+++ b/app/scripts/server_audit.sh
@@ -0,0 +1,412 @@
+#!/bin/bash
+# Audit complet serveur — applicatif + reseau + correlation
+# Compatible RHEL 7/8/9
+# Usage : bash server_audit.sh
+
+HOSTNAME=$(hostname -s 2>/dev/null || hostname)
+DATE=$(date '+%Y-%m-%d %H:%M:%S')
+EXCLUDE_PROC="kworker|ksoftirq|migration|watchdog|kthread|rcu_|irq/|scsi|ata_|writeback|sshd:.*notty|bash /tmp|server_audit|capture_state|PM2.*God Daemon"
+EXCLUDE_SVC="auditd|chronyd|crond|dbus|firewalld|getty|irqbalance|kdump|lvm2|lvmetad|NetworkManager|polkit|rsyslog|sshd|sssd|systemd|tuned|qualys|sentinelone|zabbix|commvault|veeam|tina|login|agetty|mingetty|logind|accounts-daemon|udisksd"
+
+echo "########################################################"
+echo "# AUDIT COMPLET — $HOSTNAME"
+echo "# $DATE"
+echo "# OS: $(cat /etc/redhat-release 2>/dev/null || head -1 /etc/os-release 2>/dev/null)"
+echo "# Kernel: $(uname -r)"
+echo "# Uptime: $(uptime -p 2>/dev/null || uptime)"
+echo "########################################################"
+
+##############################################################
+# PARTIE 1 — APPLICATIF
+##############################################################
+
+echo ""
+echo "========================================================"
+echo " PARTIE 1 — ANALYSE APPLICATIVE"
+echo "========================================================"
+
+# ── 1.1 Services applicatifs running ──
+echo ""
+echo "=== 1.1 SERVICES APPLICATIFS RUNNING ==="
+echo "SERVICE|ENABLED|MAIN_PID|USER|EXEC_START"
+for svc in $(sudo systemctl list-units --type=service --state=running --no-pager --no-legend | awk '{print $1}'); do
+ svc_short=$(echo "$svc" | sed 's/.service//')
+ echo "$svc_short" | grep -qiE "$EXCLUDE_SVC" && continue
+ enabled=$(sudo systemctl is-enabled "$svc" 2>/dev/null)
+ pid=$(sudo systemctl show -p MainPID "$svc" 2>/dev/null | awk -F= '{print $2}')
+ user=$(sudo systemctl show -p User "$svc" 2>/dev/null | awk -F= '{print $2}')
+ execstart=""
+ [ -n "$pid" ] && [ "$pid" != "0" ] && [ -f "/proc/$pid/cmdline" ] && \
+ execstart=$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null | cut -c1-150)
+ if [ -z "$execstart" ]; then
+ execstart=$(sudo systemctl show -p ExecStart "$svc" 2>/dev/null | grep -oP '(?<=path=)\S+' | head -1)
+ fi
+ echo "$svc_short|$enabled|$pid|${user:-root}|$execstart"
+done
+
+# ── 1.2 Processus applicatifs ──
+echo ""
+echo "=== 1.2 PROCESSUS APPLICATIFS ==="
+echo "PID|PPID|USER|EXE|CWD|CMDLINE|RESTART_HINT"
+for pid in $(sudo ls -d /proc/[0-9]* 2>/dev/null | sed 's|/proc/||' | sort -n); do
+ [ ! -f /proc/$pid/cmdline ] && continue
+ cmd=$(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null)
+ [ -z "$cmd" ] && continue
+ echo "$cmd" | grep -qE '^\[' && continue
+ echo "$cmd" | grep -qiE "$EXCLUDE_PROC" && continue
+ user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ ppid=$(sudo awk '/PPid/{print $2}' /proc/$pid/status 2>/dev/null)
+ exe=$(sudo readlink /proc/$pid/exe 2>/dev/null)
+ cwd=$(sudo readlink /proc/$pid/cwd 2>/dev/null)
+ is_interesting=0
+ [ "$ppid" = "1" ] && is_interesting=1
+ echo "$cwd" | grep -qi "/applis" && is_interesting=1
+ echo "$exe" | grep -qiE "node|java|python|ruby|perl|php|tomcat" && is_interesting=1
+ echo "$cmd" | grep -qiE "/applis|\.jar|\.js |\.py |\.rb " && is_interesting=1
+ sudo ss -tlnp 2>/dev/null | grep "pid=$pid," >/dev/null && is_interesting=1
+ [ "$is_interesting" = "0" ] && continue
+ echo "$cmd" | grep -qiE "$EXCLUDE_SVC" && continue
+ # Exclure workers enfants (meme exe que le parent)
+ if [ "$ppid" != "1" ] && [ -n "$exe" ]; then
+ parent_exe=$(sudo readlink /proc/$ppid/exe 2>/dev/null)
+ [ "$exe" = "$parent_exe" ] && continue
+ # Exclure enfants dont le grandparent est PM2 God Daemon
+ grandppid=$(sudo awk '/PPid/{print $2}' /proc/$ppid/status 2>/dev/null)
+ if [ -n "$grandppid" ]; then
+ grandp_cmd=$(tr '\0' ' ' < /proc/$grandppid/cmdline 2>/dev/null)
+ echo "$grandp_cmd" | grep -qiE "PM2.*God Daemon" && continue
+ fi
+ fi
+ # Construire hint de redemarrage
+ hint=""
+ # Priorite 1: systemd
+ svc_match=$(sudo systemctl status $pid 2>/dev/null | head -1 | grep -oP '\S+\.service' | head -1)
+ if [ -z "$svc_match" ]; then
+ exe_name=$(basename "$exe" 2>/dev/null)
+ [ -n "$exe_name" ] && svc_match=$(sudo systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null | grep -i "$exe_name" | awk '{print $1}' | head -1)
+ fi
+ if [ -n "$svc_match" ]; then
+ hint="sudo systemctl restart $svc_match"
+ # Priorite 2: PM2
+ elif command -v pm2 &>/dev/null; then
+ pm2_name=""
+ pm2_json=""
+ for pm2_user in "$user" root $(sudo ps aux 2>/dev/null | grep -i 'PM2\|pm2' | grep -v grep | awk '{print $1}' | sort -u); do
+ pm2_json=$(su - "$pm2_user" -c "pm2 jlist 2>/dev/null" 2>/dev/null)
+ [ -z "$pm2_json" ] && continue
+ pm2_name=$(echo "$pm2_json" | grep -oP '"pid"\s*:\s*'$pid'\b[^}]*"name"\s*:\s*"\K[^"]+' 2>/dev/null | head -1)
+ [ -n "$pm2_name" ] && break
+ pm2_name=$(echo "$pm2_json" | grep -oP '"name"\s*:\s*"\K[^"]+(?=[^}]*"pid"\s*:\s*'$pid'\b)' 2>/dev/null | head -1)
+ [ -n "$pm2_name" ] && break
+ done
+ if [ -n "$pm2_name" ]; then
+ pm2_bin=$(which pm2 2>/dev/null || echo "/usr/local/bin/pm2")
+ pm2_exec=$(echo "$pm2_json" | grep -oP '"pm_exec_path"\s*:\s*"\K[^"]+' 2>/dev/null | head -1)
+ pm2_args=$(echo "$pm2_json" | grep -oP '"args"\s*:\s*\[\K[^\]]+' 2>/dev/null | head -1 | sed 's/"//g; s/,/ /g')
+ if [ -n "$pm2_exec" ] && [ -n "$pm2_args" ]; then
+ hint="su - $pm2_user -c '$pm2_bin start $pm2_exec --name $pm2_name -- $pm2_args'"
+ else
+ hint="su - $pm2_user -c '$pm2_bin restart $pm2_name'"
+ fi
+ fi
+ fi
+ # Priorite 3: start script dans cwd
+ if [ -z "$hint" ] && [ -n "$cwd" ] && [ -d "$cwd" ]; then
+ start_script=$(find "$cwd" -maxdepth 1 \( -name "start*" -o -name "run*" -o -name "*.sh" \) -executable 2>/dev/null | head -1)
+ if [ -n "$start_script" ]; then
+ hint="su - $user -c 'cd $cwd && $start_script'"
+ else
+ hint="su - $user -c 'cd $cwd && $(echo $cmd | cut -c1-120)'"
+ fi
+ fi
+ # Priorite 4: commande directe
+ if [ -z "$hint" ]; then
+ hint="su - $user -c '$(echo $cmd | cut -c1-120)'"
+ fi
+ echo "$pid|$ppid|$user|$exe|$cwd|$(echo $cmd | cut -c1-150)|$hint"
+done
+
+# ── 1.3 Services failed ──
+echo ""
+echo "=== 1.3 SERVICES EN ECHEC ==="
+failed=$(sudo systemctl list-units --type=service --state=failed --no-pager --no-legend 2>/dev/null)
+if [ -n "$failed" ]; then
+ echo "$failed"
+else
+ echo "Aucun service en echec"
+fi
+
+# ── 1.4 Needs-restarting ──
+echo ""
+echo "=== 1.4 NEEDS-RESTARTING ==="
+if ! command -v needs-restarting &>/dev/null; then
+ major=$(rpm -E %{rhel} 2>/dev/null || cat /etc/redhat-release 2>/dev/null | grep -oP '\d+' | head -1)
+ if [ "$major" -ge 9 ] 2>/dev/null; then
+ dnf install -y dnf-utils 2>&1 | tail -1
+ elif [ "$major" -ge 8 ] 2>/dev/null; then
+ dnf install -y yum-utils 2>&1 | tail -1
+ else
+ yum install -y yum-utils 2>&1 | tail -1
+ fi
+fi
+if command -v needs-restarting &>/dev/null; then
+ sudo needs-restarting -r 2>/dev/null
+ echo "EXIT_CODE=$?"
+ echo "--- Services a redemarrer ---"
+ sudo needs-restarting -s 2>/dev/null
+else
+ echo "sudo needs-restarting non disponible"
+fi
+
+# ── 1.5 Espace disque ──
+echo ""
+echo "=== 1.5 ESPACE DISQUE ==="
+df -h --output=target,size,used,avail,pcent 2>/dev/null | grep -vE '^(tmpfs|devtmpfs)' || df -h 2>/dev/null
+
+##############################################################
+# PARTIE 2 — RESEAU
+##############################################################
+
+echo ""
+echo "========================================================"
+echo " PARTIE 2 — ANALYSE RESEAU"
+echo "========================================================"
+
+# ── 2.1 Interfaces et IPs ──
+echo ""
+echo "=== 2.1 INTERFACES RESEAU ==="
+echo "INTERFACE|IP|MASK|STATE|MAC"
+ip -4 -o addr show 2>/dev/null | while read idx iface scope ip_mask rest; do
+ ip=$(echo "$ip_mask" | cut -d/ -f1)
+ mask=$(echo "$ip_mask" | cut -d/ -f2)
+ state=$(ip link show "$iface" 2>/dev/null | grep -oP '(?<=state )\S+' | head -1)
+ mac=$(ip link show "$iface" 2>/dev/null | grep -oP 'link/ether \K\S+' | head -1)
+ echo "$iface|$ip|/$mask|${state:-UP}|${mac:--}"
+done
+
+# ── 2.2 Routes ──
+echo ""
+echo "=== 2.2 TABLE DE ROUTAGE ==="
+echo "DESTINATION|GATEWAY|INTERFACE|METRIC"
+ip route show 2>/dev/null | while read line; do
+ dest=$(echo "$line" | awk '{print $1}')
+ gw=$(echo "$line" | grep -oP '(?<=via )\S+')
+ iface=$(echo "$line" | grep -oP '(?<=dev )\S+')
+ metric=$(echo "$line" | grep -oP '(?<=metric )\S+')
+ echo "$dest|${gw:-direct}|${iface:--}|${metric:--}"
+done
+
+# ── 2.3 Ports en ecoute ──
+echo ""
+echo "=== 2.3 PORTS EN ECOUTE ==="
+echo "PROTO|ADDR:PORT|PID|PROCESS|USER|SERVICE"
+sudo ss -tlnp 2>/dev/null | grep LISTEN | while read state recvq sendq local peer info; do
+ pid=$(echo "$info" | grep -oP 'pid=\K[0-9]+' | head -1)
+ proc=$(echo "$info" | grep -oP '"\K[^"]+' | head -1)
+ port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ user=""
+ [ -n "$pid" ] && user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ svc=$(getent services "$port/tcp" 2>/dev/null | awk '{print $1}')
+ echo "TCP|$local|${pid:--}|${proc:--}|${user:--}|${svc:--}"
+done
+sudo ss -ulnp 2>/dev/null | grep -v 'State' | while read state recvq sendq local peer info; do
+ [ -z "$local" ] && continue
+ pid=$(echo "$info" | grep -oP 'pid=\K[0-9]+' | head -1)
+ proc=$(echo "$info" | grep -oP '"\K[^"]+' | head -1)
+ port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ user=""
+ [ -n "$pid" ] && user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ svc=$(getent services "$port/udp" 2>/dev/null | awk '{print $1}')
+ echo "UDP|$local|${pid:--}|${proc:--}|${user:--}|${svc:--}"
+done
+
+# ── 2.4 Connexions etablies ──
+echo ""
+echo "=== 2.4 CONNEXIONS ETABLIES ==="
+echo "DIRECTION|PROTO|LOCAL|REMOTE|PID|PROCESS|USER|STATE"
+listen_ports=$(sudo ss -tlnp 2>/dev/null | grep LISTEN | grep -oP '\S+:(\d+)\s' | grep -oP ':\K\d+' | sort -u)
+sudo ss -tnp 2>/dev/null | grep -v 'State' | while read state recvq sendq local remote info; do
+ [ "$state" = "State" ] && continue
+ [ -z "$local" ] && continue
+ pid=$(echo "$info" | grep -oP 'pid=\K[0-9]+' | head -1)
+ proc=$(echo "$info" | grep -oP '"\K[^"]+' | head -1)
+ user=""
+ [ -n "$pid" ] && user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ local_port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ direction="OUT"
+ echo "$listen_ports" | grep -qx "$local_port" && direction="IN"
+ echo "$direction|TCP|$local|$remote|${pid:--}|${proc:--}|${user:--}|$state"
+done
+
+# ── 2.5 Resume flux entrants ──
+echo ""
+echo "=== 2.5 RESUME FLUX ENTRANTS (par port) ==="
+echo "PORT|SERVICE|PROCESS|NB_CONNEXIONS|SOURCES"
+for port in $(sudo ss -tnp 2>/dev/null | grep ESTAB | awk '{print $4}' | grep -oP ':\K\d+$' | sort -n | uniq); do
+ echo "$listen_ports" | grep -qx "$port" || continue
+ svc=$(getent services "$port/tcp" 2>/dev/null | awk '{print $1}')
+ proc=$(sudo ss -tlnp 2>/dev/null | grep ":$port " | grep -oP '"\K[^"]+' | head -1)
+ count=$(sudo ss -tnp 2>/dev/null | grep ESTAB | awk '{print $4}' | grep -P ":${port}$" | wc -l)
+ sources=$(sudo ss -tnp 2>/dev/null | grep ESTAB | awk -v p=":${port}$" '$4 ~ p {print $5}' | grep -oP '^[^:]+' | sort -u | head -10 | tr '\n' ',' | sed 's/,$//')
+ echo "$port|${svc:--}|${proc:--}|$count|${sources:--}"
+done
+
+# ── 2.6 Resume flux sortants ──
+echo ""
+echo "=== 2.6 RESUME FLUX SORTANTS (par destination:port) ==="
+echo "DEST_IP|DEST_PORT|SERVICE|PROCESS|NB_CONNEXIONS"
+sudo ss -tnp 2>/dev/null | grep ESTAB | while read state recvq sendq local remote info; do
+ local_port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ echo "$listen_ports" | grep -qx "$local_port" && continue
+ echo "$remote|$(echo "$info" | grep -oP '"\K[^"]+' | head -1)"
+done | sort | uniq -c | sort -rn | head -50 | while read count dest_info; do
+ dest_ip=$(echo "$dest_info" | grep -oP '^\S+(?=:\d+)')
+ dest_port=$(echo "$dest_info" | grep -oP ':\K\d+(?=\|)')
+ proc=$(echo "$dest_info" | awk -F'|' '{print $2}')
+ svc=$(getent services "$dest_port/tcp" 2>/dev/null | awk '{print $1}')
+ dns=$(timeout 2 host "$dest_ip" 2>/dev/null | grep -oP 'pointer \K\S+' | sed 's/\.$//' | head -1)
+ if [ -n "$dns" ]; then
+ echo "$dest_ip ($dns)|$dest_port|${svc:--}|${proc:--}|$count"
+ else
+ echo "$dest_ip|$dest_port|${svc:--}|${proc:--}|$count"
+ fi
+done
+
+# ── 2.7 Connexions en attente ──
+echo ""
+echo "=== 2.7 CONNEXIONS EN ATTENTE ==="
+echo "STATE|COUNT"
+for st in TIME-WAIT CLOSE-WAIT FIN-WAIT-1 FIN-WAIT-2 SYN-SENT SYN-RECV LAST-ACK CLOSING; do
+ cnt=$(sudo ss -tn state "$st" 2>/dev/null | grep -c -v 'State')
+ [ "$cnt" -gt 0 ] && echo "$st|$cnt"
+done
+
+# ── 2.8 Stats reseau ──
+echo ""
+echo "=== 2.8 STATISTIQUES RESEAU ==="
+echo "METRIC|VALUE"
+echo "TCP ESTABLISHED|$(sudo ss -tn state established 2>/dev/null | grep -c -v 'State')"
+echo "TCP LISTEN|$(sudo ss -tln 2>/dev/null | grep -c LISTEN)"
+echo "UDP LISTEN|$(sudo ss -uln 2>/dev/null | grep -c -v 'State')"
+
+# ── 2.9 Trafic par interface ──
+echo ""
+echo "=== 2.9 TRAFIC PAR INTERFACE ==="
+echo "INTERFACE|RX_BYTES|RX_PACKETS|RX_ERRORS|TX_BYTES|TX_PACKETS|TX_ERRORS"
+sudo cat /proc/net/dev 2>/dev/null | tail -n+3 | while read line; do
+ iface=$(echo "$line" | awk -F: '{print $1}' | tr -d ' ')
+ stats=$(echo "$line" | awk -F: '{print $2}')
+ rx_bytes=$(echo "$stats" | awk '{print $1}')
+ rx_pkt=$(echo "$stats" | awk '{print $2}')
+ rx_err=$(echo "$stats" | awk '{print $3}')
+ tx_bytes=$(echo "$stats" | awk '{print $9}')
+ tx_pkt=$(echo "$stats" | awk '{print $10}')
+ tx_err=$(echo "$stats" | awk '{print $11}')
+ [ "$iface" = "lo" ] && continue
+ rx_h=$(numfmt --to=iec "$rx_bytes" 2>/dev/null || echo "${rx_bytes}B")
+ tx_h=$(numfmt --to=iec "$tx_bytes" 2>/dev/null || echo "${tx_bytes}B")
+ echo "$iface|$rx_h|$rx_pkt|$rx_err|$tx_h|$tx_pkt|$tx_err"
+done
+
+# ── 2.10 Firewall ──
+echo ""
+echo "=== 2.10 FIREWALL ==="
+if command -v iptables &>/dev/null; then
+ echo "--- POLICY ---"
+ for chain in INPUT OUTPUT FORWARD; do
+ policy=$(iptables -L "$chain" -n 2>/dev/null | head -1 | grep -oP 'policy \K\w+')
+ echo "$chain|$policy"
+ done
+ echo "--- INPUT ---"
+ echo "NUM|TARGET|PROTO|SOURCE|DEST|PORT|INFO"
+ iptables -L INPUT -n -v --line-numbers 2>/dev/null | tail -n+3 | head -30 | while read num pkts bytes target prot opt in out source dest rest; do
+ port=$(echo "$rest" | grep -oP '(dpt|dpts):\K\S+' | head -1)
+ echo "$num|$target|$prot|$source|$dest|${port:--}|$rest"
+ done
+ echo "--- OUTPUT ---"
+ echo "NUM|TARGET|PROTO|SOURCE|DEST|PORT|INFO"
+ iptables -L OUTPUT -n -v --line-numbers 2>/dev/null | tail -n+3 | head -30 | while read num pkts bytes target prot opt in out source dest rest; do
+ port=$(echo "$rest" | grep -oP '(dpt|dpts):\K\S+' | head -1)
+ echo "$num|$target|$prot|$source|$dest|${port:--}|$rest"
+ done
+else
+ echo "iptables non disponible"
+fi
+if sudo systemctl is-active firewalld &>/dev/null 2>&1; then
+ echo "--- FIREWALLD ---"
+ echo "ZONE|SERVICES|PORTS"
+ for zone in $(firewall-cmd --get-active-zones 2>/dev/null | grep -v '^\s' | grep -v '^$'); do
+ svcs=$(firewall-cmd --zone="$zone" --list-services 2>/dev/null | tr ' ' ',')
+ ports=$(firewall-cmd --zone="$zone" --list-ports 2>/dev/null | tr ' ' ',')
+ echo "$zone|${svcs:--}|${ports:--}"
+ done
+fi
+
+##############################################################
+# PARTIE 3 — CORRELATION
+##############################################################
+
+echo ""
+echo "========================================================"
+echo " PARTIE 3 — CORRELATION PROCESS ↔ PORTS ↔ FLUX"
+echo "========================================================"
+
+echo ""
+echo "=== 3.1 MATRICE PROCESS → PORTS → CONNEXIONS ==="
+echo "PROCESS|USER|PID|LISTEN_PORTS|CONN_IN|CONN_OUT|REMOTE_DESTINATIONS"
+
+# Pour chaque process qui ecoute sur un port
+sudo ss -tlnp 2>/dev/null | grep LISTEN | while read state recvq sendq local peer info; do
+ pid=$(echo "$info" | grep -oP 'pid=\K[0-9]+' | head -1)
+ proc=$(echo "$info" | grep -oP '"\K[^"]+' | head -1)
+ [ -z "$pid" ] && continue
+ port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ echo "$pid|$proc|$port"
+done | sort -t'|' -k1,1n -u | while IFS='|' read pid proc port; do
+ user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ # Tous les ports en ecoute pour ce PID
+ ports=$(sudo ss -tlnp 2>/dev/null | grep "pid=$pid," | grep -oP '\S+:\K\d+(?=\s)' | sort -un | tr '\n' ',' | sed 's/,$//')
+ # Connexions entrantes sur ces ports
+ conn_in=0
+ for p in $(echo "$ports" | tr ',' ' '); do
+ c=$(sudo ss -tnp 2>/dev/null | grep ESTAB | awk '{print $4}' | grep -c ":${p}$")
+ conn_in=$((conn_in + c))
+ done
+ # Connexions sortantes de ce PID
+ conn_out=$(sudo ss -tnp 2>/dev/null | grep ESTAB | grep "pid=$pid," | while read st rq sq lc rm inf; do
+ lp=$(echo "$lc" | grep -oP ':\K\d+$')
+ echo "$listen_ports" | grep -qx "$lp" || echo OUT
+ done | wc -l)
+ # Destinations sortantes
+ dests=$(sudo ss -tnp 2>/dev/null | grep ESTAB | grep "pid=$pid," | awk '{print $5}' | sort -u | head -5 | tr '\n' ',' | sed 's/,$//')
+ echo "$proc|${user:--}|$pid|$ports|$conn_in|$conn_out|${dests:--}"
+done
+
+# Processus avec connexions sortantes mais sans port en ecoute
+echo ""
+echo "=== 3.2 PROCESS SORTANTS UNIQUEMENT (pas de port en ecoute) ==="
+echo "PROCESS|USER|PID|DESTINATIONS"
+sudo ss -tnp 2>/dev/null | grep ESTAB | while read state recvq sendq local remote info; do
+ pid=$(echo "$info" | grep -oP 'pid=\K[0-9]+' | head -1)
+ proc=$(echo "$info" | grep -oP '"\K[^"]+' | head -1)
+ [ -z "$pid" ] && continue
+ local_port=$(echo "$local" | grep -oP ':\K[0-9]+$')
+ # Verifier si ce process n'ecoute PAS
+ sudo ss -tlnp 2>/dev/null | grep "pid=$pid," >/dev/null && continue
+ echo "$pid|$proc|$remote"
+done | sort -t'|' -k1,1n -k3 | awk -F'|' '{
+ if ($1 != prev_pid) {
+ if (prev_pid != "") print prev_pid"|"prev_proc"|"dests
+ prev_pid=$1; prev_proc=$2; dests=$3
+ } else {
+ dests=dests","$3
+ }
+} END { if (prev_pid != "") print prev_pid"|"prev_proc"|"dests }' | while IFS='|' read pid proc dests; do
+ user=$(sudo stat -c %U /proc/$pid 2>/dev/null)
+ echo "$proc|${user:--}|$pid|$dests"
+done
+
+echo ""
+echo "########################################################"
+echo "# FIN AUDIT — $HOSTNAME — $(date '+%H:%M:%S')"
+echo "########################################################"
\ No newline at end of file
diff --git a/app/services/server_audit_full_service.py b/app/services/server_audit_full_service.py
new file mode 100644
index 0000000..3d8b363
--- /dev/null
+++ b/app/services/server_audit_full_service.py
@@ -0,0 +1,496 @@
+"""Service audit complet serveur — applicatif + reseau + correlation + carte flux
+Adapte du standalone SANEF corrige pour PatchCenter (FastAPI/PostgreSQL)
+"""
+import json
+import re
+import os
+import socket
+import logging
+from datetime import datetime
+from sqlalchemy import text
+
+logging.getLogger("paramiko").setLevel(logging.CRITICAL)
+logging.getLogger("paramiko.transport").setLevel(logging.CRITICAL)
+
+try:
+ import paramiko
+ PARAMIKO_OK = True
+except ImportError:
+ PARAMIKO_OK = False
+
+SSH_KEY_FILE = "/opt/patchcenter/keys/id_rsa_cybglobal.pem"
+PSMP_HOST = "psmp.sanef.fr"
+CYBR_USER = "CYBP01336"
+TARGET_USER = "cybsecope"
+SSH_TIMEOUT = 20
+
+ENV_DOMAINS = {
+ "prod": ".sanef.groupe",
+ "preprod": ".sanef.groupe",
+ "recette": ".sanef-rec.fr",
+ "test": ".sanef-rec.fr",
+ "dev": ".sanef-rec.fr",
+}
+
+BANNER_FILTERS = [
+ "GROUPE SANEF", "propriete du Groupe", "accederait", "emprisonnement",
+ "Article 323", "code penal", "Authorized uses only", "CyberArk",
+ "This session", "session is being",
+]
+
+SCRIPT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "scripts", "server_audit.sh")
+
+
+def _load_script():
+ with open(SCRIPT_PATH, "r", encoding="utf-8") as f:
+ return f.read()
+
+
+def _get_psmp_password(db=None):
+ if not db:
+ return None
+ try:
+ from .secrets_service import get_secret
+ return get_secret(db, "ssh_pwd_default_pass")
+ except Exception:
+ return None
+
+
+# ── DETECTION ENV + SSH (pattern SANEF corrige) ──
+
+def detect_env(hostname):
+ h = hostname.lower()
+ c = h[1] if len(h) > 1 else ""
+ if c == "p": return "prod"
+ elif c == "i": return "preprod"
+ elif c == "r": return "recette"
+ elif c == "v": return "test"
+ elif c == "d": return "dev"
+ return "recette"
+
+
+def _load_key():
+ if not os.path.exists(SSH_KEY_FILE):
+ return None
+ for cls in [paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey]:
+ try:
+ return cls.from_private_key_file(SSH_KEY_FILE)
+ except Exception:
+ continue
+ return None
+
+
+def _build_fqdn_candidates(hostname):
+ if "." in hostname:
+ return [hostname]
+ c = hostname[1] if len(hostname) > 1 else ""
+ if c in ("p", "i"):
+ return [f"{hostname}.sanef.groupe", f"{hostname}.sanef-rec.fr", hostname]
+ else:
+ return [f"{hostname}.sanef-rec.fr", f"{hostname}.sanef.groupe", hostname]
+
+
+def _try_psmp(fqdn, password):
+ if not password:
+ return None
+ try:
+ username = f"{CYBR_USER}@{TARGET_USER}@{fqdn}"
+ transport = paramiko.Transport((PSMP_HOST, 22))
+ transport.connect()
+ def handler(title, instructions, prompt_list):
+ return [password] * len(prompt_list)
+ transport.auth_interactive(username, handler)
+ client = paramiko.SSHClient()
+ client._transport = transport
+ return client
+ except Exception:
+ return None
+
+
+def _try_key(fqdn, key):
+ if not key:
+ return None
+ try:
+ client = paramiko.SSHClient()
+ client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ client.connect(fqdn, port=22, username=TARGET_USER, pkey=key,
+ timeout=SSH_TIMEOUT, look_for_keys=False, allow_agent=False)
+ return client
+ except Exception:
+ return None
+
+
+def ssh_connect(hostname, password=None):
+ fqdn_candidates = _build_fqdn_candidates(hostname)
+ key = _load_key()
+ for fqdn in fqdn_candidates:
+ if password:
+ client = _try_psmp(fqdn, password)
+ if client:
+ return client, None
+ if key:
+ client = _try_key(fqdn, key)
+ if client:
+ return client, None
+ return None, f"Connexion impossible sur {fqdn_candidates}"
+
+
+def ssh_run_script(client, script_content, timeout=300):
+ try:
+ chan = client._transport.open_session()
+ chan.settimeout(timeout)
+ chan.exec_command("bash -s")
+ chan.sendall(script_content.encode("utf-8"))
+ chan.shutdown_write()
+ out = b""
+ while True:
+ try:
+ chunk = chan.recv(8192)
+ if not chunk:
+ break
+ out += chunk
+ except Exception:
+ break
+ chan.close()
+ out_str = out.decode("utf-8", errors="replace")
+ if not out_str.strip():
+ return "", "Sortie vide"
+ lines = [l for l in out_str.splitlines() if not any(b in l for b in BANNER_FILTERS)]
+ return "\n".join(lines), None
+ except Exception as e:
+ return "", str(e)
+
+
+# ── PARSING ──
+
+def parse_audit_output(raw):
+ result = {
+ "hostname": "", "os_release": "", "kernel": "", "uptime": "",
+ "services": [], "processes": [], "services_failed": "",
+ "needs_restarting": "", "reboot_required": False, "disk_usage": [],
+ "interfaces": [], "routes": [], "listen_ports": [],
+ "connections": [], "flux_in": [], "flux_out": [],
+ "conn_wait": [], "net_stats": {}, "traffic": [],
+ "firewall": {"policy": {}, "input": [], "output": [], "firewalld": []},
+ "correlation_matrix": [], "outbound_only": [],
+ }
+ section = None
+ firewall_sub = None
+
+ for line in raw.splitlines():
+ ls = line.strip()
+ m = re.match(r"^# AUDIT COMPLET .+ (.+)$", ls)
+ if m: result["hostname"] = m.group(1); continue
+ m = re.match(r"^# OS: (.+)$", ls)
+ if m: result["os_release"] = m.group(1); continue
+ m = re.match(r"^# Kernel: (.+)$", ls)
+ if m: result["kernel"] = m.group(1); continue
+ m = re.match(r"^# Uptime: (.+)$", ls)
+ if m: result["uptime"] = m.group(1); continue
+ if "1.1 SERVICES APPLICATIFS" in ls: section = "services"; continue
+ elif "1.2 PROCESSUS APPLICATIFS" in ls: section = "processes"; continue
+ elif "1.3 SERVICES EN ECHEC" in ls: section = "services_failed"; continue
+ elif "1.4 NEEDS-RESTARTING" in ls: section = "needs_restarting"; continue
+ elif "1.5 ESPACE DISQUE" in ls: section = "disk"; continue
+ elif "2.1 INTERFACES" in ls: section = "interfaces"; continue
+ elif "2.2 TABLE DE ROUTAGE" in ls: section = "routes"; continue
+ elif "2.3 PORTS EN ECOUTE" in ls: section = "listen_ports"; continue
+ elif "2.4 CONNEXIONS ETABLIES" in ls: section = "connections"; continue
+ elif "2.5 RESUME FLUX ENTRANTS" in ls: section = "flux_in"; continue
+ elif "2.6 RESUME FLUX SORTANTS" in ls: section = "flux_out"; continue
+ elif "2.7 CONNEXIONS EN ATTENTE" in ls: section = "conn_wait"; continue
+ elif "2.8 STATISTIQUES" in ls: section = "net_stats"; continue
+ elif "2.9 TRAFIC" in ls: section = "traffic"; continue
+ elif "2.10 FIREWALL" in ls: section = "firewall"; firewall_sub = None; continue
+ elif "3.1 MATRICE" in ls: section = "correlation"; continue
+ elif "3.2 PROCESS SORTANTS" in ls: section = "outbound"; continue
+ elif ls.startswith("===") or ls.startswith("###"): section = None; continue
+ if not ls: continue
+ headers = ["SERVICE|","PID|PPID","PROTO|","DIRECTION|","PORT|","DEST_IP|",
+ "METRIC|","INTERFACE|","DESTINATION|","NUM|","PROCESS|USER",
+ "ZONE|","Mont","STATE|COUNT"]
+ if any(ls.startswith(h) for h in headers): continue
+ parts = ls.split("|")
+ if section == "services" and len(parts) >= 2:
+ result["services"].append({"name":parts[0],"enabled":parts[1],"pid":parts[2] if len(parts)>2 else "","user":parts[3] if len(parts)>3 else "","exec":parts[4] if len(parts)>4 else ""})
+ elif section == "processes" and len(parts) >= 6:
+ result["processes"].append({"pid":parts[0],"ppid":parts[1],"user":parts[2],"exe":parts[3],"cwd":parts[4],"cmdline":parts[5],"restart_hint":parts[6] if len(parts)>6 else ""})
+ elif section == "services_failed":
+ if ls != "Aucun service en echec": result["services_failed"] += ls + "\n"
+ elif section == "needs_restarting":
+ result["needs_restarting"] += ls + "\n"
+ if "EXIT_CODE=1" in ls: result["reboot_required"] = True
+ elif section == "disk":
+ p = ls.split()
+ if len(p) >= 5 and "%" in p[-1]:
+ try: result["disk_usage"].append({"mount":p[0],"size":p[1],"used":p[2],"avail":p[3],"pct":int(p[4].replace("%",""))})
+ except: pass
+ elif section == "interfaces" and len(parts) >= 3:
+ result["interfaces"].append({"iface":parts[0],"ip":parts[1],"mask":parts[2],"state":parts[3] if len(parts)>3 else "","mac":parts[4] if len(parts)>4 else ""})
+ elif section == "routes" and len(parts) >= 3:
+ result["routes"].append({"dest":parts[0],"gw":parts[1],"iface":parts[2],"metric":parts[3] if len(parts)>3 else ""})
+ elif section == "listen_ports" and len(parts) >= 3:
+ result["listen_ports"].append({"proto":parts[0],"addr_port":parts[1],"pid":parts[2],"process":parts[3] if len(parts)>3 else "","user":parts[4] if len(parts)>4 else "","service":parts[5] if len(parts)>5 else ""})
+ elif section == "connections" and len(parts) >= 5:
+ result["connections"].append({"direction":parts[0],"proto":parts[1],"local":parts[2],"remote":parts[3],"pid":parts[4],"process":parts[5] if len(parts)>5 else "","user":parts[6] if len(parts)>6 else "","state":parts[7] if len(parts)>7 else ""})
+ elif section == "flux_in" and len(parts) >= 3:
+ result["flux_in"].append({"port":parts[0],"service":parts[1],"process":parts[2],"count":parts[3] if len(parts)>3 else "0","sources":parts[4] if len(parts)>4 else ""})
+ elif section == "flux_out" and len(parts) >= 3:
+ result["flux_out"].append({"dest_ip":parts[0],"dest_port":parts[1],"service":parts[2],"process":parts[3] if len(parts)>3 else "","count":parts[4] if len(parts)>4 else "1"})
+ elif section == "conn_wait" and len(parts) == 2:
+ result["conn_wait"].append({"state":parts[0],"count":parts[1]})
+ elif section == "net_stats" and len(parts) == 2:
+ result["net_stats"][parts[0].strip()] = parts[1].strip()
+ elif section == "traffic" and len(parts) >= 5:
+ result["traffic"].append({"iface":parts[0],"rx_bytes":parts[1],"rx_pkt":parts[2],"rx_err":parts[3],"tx_bytes":parts[4],"tx_pkt":parts[5] if len(parts)>5 else "","tx_err":parts[6] if len(parts)>6 else ""})
+ elif section == "firewall":
+ if "POLICY" in ls: firewall_sub = "policy"; continue
+ elif "INPUT" in ls and "---" in ls: firewall_sub = "input"; continue
+ elif "OUTPUT" in ls and "---" in ls: firewall_sub = "output"; continue
+ elif "FIREWALLD" in ls: firewall_sub = "firewalld"; continue
+ if firewall_sub == "policy" and len(parts) == 2: result["firewall"]["policy"][parts[0]] = parts[1]
+ elif firewall_sub == "input" and len(parts) >= 3: result["firewall"]["input"].append(ls)
+ elif firewall_sub == "output" and len(parts) >= 3: result["firewall"]["output"].append(ls)
+ elif firewall_sub == "firewalld" and len(parts) >= 2: result["firewall"]["firewalld"].append({"zone":parts[0],"services":parts[1],"ports":parts[2] if len(parts)>2 else ""})
+ elif section == "correlation" and len(parts) >= 4:
+ result["correlation_matrix"].append({"process":parts[0],"user":parts[1],"pid":parts[2],"listen_ports":parts[3],"conn_in":parts[4] if len(parts)>4 else "0","conn_out":parts[5] if len(parts)>5 else "0","remote_dests":parts[6] if len(parts)>6 else ""})
+ elif section == "outbound" and len(parts) >= 3:
+ result["outbound_only"].append({"process":parts[0],"user":parts[1],"pid":parts[2],"dests":parts[3] if len(parts)>3 else ""})
+ result["services_failed"] = result["services_failed"].strip()
+ result["needs_restarting"] = result["needs_restarting"].strip()
+ return result
+
+
+# ── STOCKAGE DB ──
+
+def _resolve_server_id(db, hostname):
+ srv = db.execute(text(
+ "SELECT id FROM servers WHERE LOWER(hostname) = LOWER(:h)"
+ ), {"h": hostname.split(".")[0]}).fetchone()
+ return srv.id if srv else None
+
+
+def _resolve_dest_server(db, dest_ip):
+ row = db.execute(text("""
+ SELECT s.id, s.hostname FROM servers s
+ JOIN server_ips si ON s.id = si.server_id
+ WHERE si.ip_address = :ip::inet
+ LIMIT 1
+ """), {"ip": dest_ip}).fetchone()
+ return (row.id, row.hostname) if row else (None, None)
+
+
+def save_audit_to_db(db, parsed, raw_output="", status="ok", error_msg=None):
+ hostname = parsed.get("hostname", "")
+ if not hostname:
+ return None
+ server_id = _resolve_server_id(db, hostname)
+
+ row = db.execute(text("""
+ INSERT INTO server_audit_full (
+ server_id, hostname, audit_date, os_release, kernel, uptime,
+ services, processes, services_failed, needs_restarting, reboot_required,
+ disk_usage, interfaces, routes, listen_ports, connections,
+ flux_in, flux_out, conn_wait, net_stats, traffic, firewall,
+ correlation_matrix, outbound_only, raw_output, status, error_msg
+ ) VALUES (
+ :sid, :hn, NOW(), :os, :k, :up,
+ :svc, :proc, :sf, :nr, :rr,
+ :du, :iface, :rt, :lp, :conn,
+ :fi, :fo, :cw, :ns, :tr, :fw,
+ :cm, :ob, :raw, :st, :err
+ ) RETURNING id
+ """), {
+ "sid": server_id, "hn": hostname,
+ "os": parsed.get("os_release", ""), "k": parsed.get("kernel", ""),
+ "up": parsed.get("uptime", ""),
+ "svc": json.dumps(parsed.get("services", [])),
+ "proc": json.dumps(parsed.get("processes", [])),
+ "sf": parsed.get("services_failed", ""),
+ "nr": parsed.get("needs_restarting", ""),
+ "rr": parsed.get("reboot_required", False),
+ "du": json.dumps(parsed.get("disk_usage", [])),
+ "iface": json.dumps(parsed.get("interfaces", [])),
+ "rt": json.dumps(parsed.get("routes", [])),
+ "lp": json.dumps(parsed.get("listen_ports", [])),
+ "conn": json.dumps(parsed.get("connections", [])),
+ "fi": json.dumps(parsed.get("flux_in", [])),
+ "fo": json.dumps(parsed.get("flux_out", [])),
+ "cw": json.dumps(parsed.get("conn_wait", [])),
+ "ns": json.dumps(parsed.get("net_stats", {})),
+ "tr": json.dumps(parsed.get("traffic", [])),
+ "fw": json.dumps(parsed.get("firewall", {})),
+ "cm": json.dumps(parsed.get("correlation_matrix", [])),
+ "ob": json.dumps(parsed.get("outbound_only", [])),
+ "raw": raw_output, "st": status, "err": error_msg,
+ }).fetchone()
+
+ audit_id = row.id
+ _build_flow_map(db, audit_id, hostname, server_id, parsed)
+ return audit_id
+
+
+def _build_flow_map(db, audit_id, hostname, server_id, parsed):
+ local_ips = [i["ip"] for i in parsed.get("interfaces", []) if i["ip"] != "127.0.0.1"]
+ source_ip = local_ips[0] if local_ips else ""
+ for conn in parsed.get("connections", []):
+ remote = conn.get("remote", "")
+ m = re.match(r'^(.+):(\d+)$', remote)
+ if not m:
+ continue
+ dest_ip = m.group(1)
+ dest_port = int(m.group(2))
+ if dest_ip.startswith("127.") or dest_ip == "::1":
+ continue
+ dest_server_id, dest_hostname = _resolve_dest_server(db, dest_ip)
+ db.execute(text("""
+ INSERT INTO network_flow_map (
+ audit_id, source_server_id, source_hostname, source_ip,
+ dest_ip, dest_port, dest_hostname, dest_server_id,
+ process_name, process_user, direction,
+ connection_count, state, audit_date
+ ) VALUES (
+ :aid, :ssid, :shn, :sip,
+ :dip, :dp, :dhn, :dsid,
+ :pn, :pu, :dir, 1, :st, NOW()
+ )
+ """), {
+ "aid": audit_id, "ssid": server_id, "shn": hostname, "sip": source_ip,
+ "dip": dest_ip, "dp": dest_port, "dhn": dest_hostname, "dsid": dest_server_id,
+ "pn": conn.get("process", ""), "pu": conn.get("user", ""),
+ "dir": conn.get("direction", ""), "st": conn.get("state", ""),
+ })
+
+
+# ── IMPORT JSON (depuis standalone) ──
+
+def import_json_report(db, json_data):
+ servers = json_data.get("servers", [])
+ imported = 0
+ errors = 0
+ for srv in servers:
+ if srv.get("status") == "error":
+ errors += 1
+ continue
+ hostname = srv.get("hostname", "")
+ if not hostname:
+ continue
+ parsed = {k: srv.get(k, v) for k, v in {
+ "hostname": "", "os_release": "", "kernel": "", "uptime": "",
+ "services": [], "processes": [], "services_failed": "",
+ "needs_restarting": "", "reboot_required": False, "disk_usage": [],
+ "interfaces": [], "routes": [], "listen_ports": [],
+ "connections": [], "flux_in": [], "flux_out": [],
+ "conn_wait": [], "net_stats": {}, "traffic": [],
+ "firewall": {}, "correlation_matrix": [], "outbound_only": [],
+ }.items()}
+ save_audit_to_db(db, parsed)
+ imported += 1
+ db.commit()
+ return imported, errors
+
+
+# ── REQUETES ──
+
+def get_latest_audits(db, limit=100):
+ return db.execute(text("""
+ SELECT DISTINCT ON (hostname) id, server_id, hostname, audit_date,
+ os_release, kernel, uptime, status, reboot_required,
+ jsonb_array_length(COALESCE(services, '[]')) as svc_count,
+ jsonb_array_length(COALESCE(listen_ports, '[]')) as port_count,
+ jsonb_array_length(COALESCE(connections, '[]')) as conn_count,
+ jsonb_array_length(COALESCE(processes, '[]')) as proc_count
+ FROM server_audit_full
+ WHERE status = 'ok'
+ ORDER BY hostname, audit_date DESC
+ LIMIT :lim
+ """), {"lim": limit}).fetchall()
+
+
+def get_audit_detail(db, audit_id):
+ return db.execute(text(
+ "SELECT * FROM server_audit_full WHERE id = :id"
+ ), {"id": audit_id}).fetchone()
+
+
+def get_flow_map(db):
+ return db.execute(text("""
+ SELECT source_hostname, source_ip, dest_ip, dest_port,
+ dest_hostname, process_name, direction, state,
+ COUNT(*) as cnt
+ FROM network_flow_map nfm
+ JOIN server_audit_full saf ON nfm.audit_id = saf.id
+ WHERE saf.id IN (
+ SELECT DISTINCT ON (hostname) id FROM server_audit_full
+ WHERE status = 'ok' ORDER BY hostname, audit_date DESC
+ )
+ GROUP BY source_hostname, source_ip, dest_ip, dest_port,
+ dest_hostname, process_name, direction, state
+ ORDER BY source_hostname
+ """)).fetchall()
+
+
+def get_flow_map_for_server(db, hostname):
+ return db.execute(text("""
+ SELECT source_hostname, source_ip, dest_ip, dest_port,
+ dest_hostname, process_name, direction, state
+ FROM network_flow_map
+ WHERE audit_id = (
+ SELECT id FROM server_audit_full WHERE hostname = :h
+ ORDER BY audit_date DESC LIMIT 1
+ )
+ ORDER BY direction DESC, dest_ip
+ """), {"h": hostname}).fetchall()
+
+
+def get_flow_map_for_domain(db, domain_code):
+ return db.execute(text("""
+ SELECT nfm.source_hostname, nfm.source_ip, nfm.dest_ip, nfm.dest_port,
+ nfm.dest_hostname, nfm.process_name, nfm.direction, nfm.state
+ FROM network_flow_map nfm
+ JOIN server_audit_full saf ON nfm.audit_id = saf.id
+ JOIN servers s ON saf.server_id = s.id
+ JOIN domain_environments de ON s.domain_env_id = de.id
+ JOIN domains d ON de.domain_id = d.id
+ WHERE d.code = :dc
+ AND saf.id IN (
+ SELECT DISTINCT ON (hostname) id FROM server_audit_full
+ WHERE status = 'ok' ORDER BY hostname, audit_date DESC
+ )
+ ORDER BY nfm.source_hostname
+ """), {"dc": domain_code}).fetchall()
+
+
+def get_app_map(db):
+ audits = db.execute(text("""
+ SELECT DISTINCT ON (hostname) hostname, server_id, processes, listen_ports
+ FROM server_audit_full WHERE status = 'ok'
+ ORDER BY hostname, audit_date DESC
+ """)).fetchall()
+ app_groups = {}
+ for audit in audits:
+ processes = audit.processes if isinstance(audit.processes, list) else json.loads(audit.processes or "[]")
+ for proc in processes:
+ cwd = proc.get("cwd", "")
+ m = re.search(r'/applis/([^/]+)', cwd)
+ if not m:
+ continue
+ app_name = m.group(1)
+ if app_name not in app_groups:
+ app_groups[app_name] = {"servers": [], "ports": set()}
+ if audit.hostname not in [s["hostname"] for s in app_groups[app_name]["servers"]]:
+ app_groups[app_name]["servers"].append({
+ "hostname": audit.hostname,
+ "server_id": audit.server_id,
+ "user": proc.get("user", ""),
+ "cmdline": proc.get("cmdline", "")[:100],
+ "restart_hint": proc.get("restart_hint", "")[:100],
+ })
+ listen = audit.listen_ports if isinstance(audit.listen_ports, list) else json.loads(audit.listen_ports or "[]")
+ pid = proc.get("pid", "")
+ for lp in listen:
+ if lp.get("pid") == pid:
+ app_groups[app_name]["ports"].add(lp.get("addr_port", ""))
+ for k in app_groups:
+ app_groups[k]["ports"] = list(app_groups[k]["ports"])
+ return app_groups
diff --git a/app/templates/audit_full_detail.html b/app/templates/audit_full_detail.html
new file mode 100644
index 0000000..0642de0
--- /dev/null
+++ b/app/templates/audit_full_detail.html
@@ -0,0 +1,232 @@
+{% extends 'base.html' %}
+{% block title %}{{ a.hostname }}{% endblock %}
+{% block content %}
+< Retour
+
+
+
{{ a.hostname }}
+
{{ a.os_release }} | {{ a.kernel }} | {{ a.uptime }}
+
+
+ {% for iface in interfaces %}{% if iface.ip != '127.0.0.1' %}
+ {{ iface.ip }}{{ iface.mask }} ({{ iface.iface }})
+ {% endif %}{% endfor %}
+
+
+
+
+
+
{{ services|length }}
Services
+
{{ processes|length }}
Process
+
{{ listen_ports|length }}
Ports
+
{{ connections|length }}
Connexions
+
{% if a.reboot_required %}Oui{% else %}Non{% endif %}
Reboot
+
{% if a.services_failed %}KO{% else %}OK{% endif %}
Failed svc
+
+
+
+
+
+ {% for t, label in [('services','Services'),('processes','Processus'),('ports','Ports'),('connections','Connexions'),('flux','Flux'),('disk','Disque'),('firewall','Firewall'),('correlation','Correlation')] %}
+
+ {% endfor %}
+
+
+
+
+
+ | Service | Enabled | PID | User | Exec |
+
+ {% for s in services %}
+ | {{ s.name }} |
+ {{ s.enabled }} |
+ {{ s.pid }} |
+ {{ s.user }} |
+ {{ s.exec[:80] }} |
+
{% endfor %}
+
+
+
+
+
+
+ | PID | User | Exe | CWD | Cmdline | Restart |
+
+ {% for p in processes %}
+ | {{ p.pid }} |
+ {{ p.user }} |
+ {{ (p.exe or '')[:40] }} |
+ {{ (p.cwd or '')[:30] }} |
+ {{ (p.cmdline or '')[:60] }} |
+ {{ (p.restart_hint or '')[:60] }} |
+
{% endfor %}
+
+
+
+
+
+
+ | Proto | Addr:Port | PID | Process | User | Service |
+
+ {% for lp in listen_ports %}
+ | {{ lp.proto }} |
+ {{ lp.addr_port }} |
+ {{ lp.pid }} |
+ {{ lp.process }} |
+ {{ lp.user }} |
+ {{ lp.service }} |
+
{% endfor %}
+
+
+
+
+
+
+ | Dir | Local | Remote | PID | Process | User | State |
+
+ {% for c in connections %}
+ | {{ c.direction }} |
+ {{ c.local }} |
+ {{ c.remote }} |
+ {{ c.pid }} |
+ {{ c.process }} |
+ {{ c.user }} |
+ {{ c.state }} |
+
{% endfor %}
+
+
+
+
+
+ {% if flux_in %}
+
+
Flux entrants
+
+ | Port | Service | Process | Nb | Sources |
+
+ {% for f in flux_in %}
+ | {{ f.port }} |
+ {{ f.service }} |
+ {{ f.process }} |
+ {{ f.count }} |
+ {{ f.sources }} |
+
{% endfor %}
+
+
+ {% endif %}
+ {% if flux_out %}
+
+
Flux sortants
+
+ | Destination | Port | Service | Process | Nb |
+
+ {% for f in flux_out %}
+ | {{ f.dest_ip }} |
+ {{ f.dest_port }} |
+ {{ f.service }} |
+ {{ f.process }} |
+ {{ f.count }} |
+
{% endfor %}
+
+
+ {% endif %}
+ {% if flows %}
+
+
Carte flux (resolu)
+
+ | Dir | IP dest | Port | Serveur dest | Process | State |
+
+ {% for f in flows %}
+ | {{ f.direction }} |
+ {{ f.dest_ip }} |
+ {{ f.dest_port }} |
+ {{ f.dest_hostname or '-' }} |
+ {{ f.process_name }} |
+ {{ f.state }} |
+
{% endfor %}
+
+
+ {% endif %}
+
+
+
+
+
+ | Mount | Taille | Utilise | Dispo | % |
+
+ {% for d in disk_usage %}
+ | {{ d.mount }} |
+ {{ d.size }} |
+ {{ d.used }} |
+ {{ d.avail }} |
+ {{ d.pct }}% |
+
{% endfor %}
+
+
+
+
+
+ {% if firewall.policy %}
+
+ Policy iptables :
+ {% for chain, pol in firewall.policy.items() %}
+ {{ chain }}={{ pol or '?' }}
+ {% endfor %}
+
+ {% endif %}
+ {% if firewall.firewalld %}
+
+
Firewalld :
+ {% for z in firewall.firewalld %}
+
Zone {{ z.zone }} : services={{ z.services }} ports={{ z.ports }}
+ {% endfor %}
+
+ {% endif %}
+ {% if conn_wait %}
+
+ Connexions en attente :
+ {% for cw in conn_wait %}
+ {{ cw.state }}={{ cw.count }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
+ {% if correlation %}
+
+
Matrice process / ports / flux
+
+ | Process | User | PID | Ports | IN | OUT | Destinations |
+
+ {% for c in correlation %}
+ | {{ c.process }} |
+ {{ c.user }} |
+ {{ c.pid }} |
+ {{ c.listen_ports }} |
+ {{ c.conn_in }} |
+ {{ c.conn_out }} |
+ {{ c.remote_dests }} |
+
{% endfor %}
+
+
+ {% endif %}
+ {% if outbound %}
+
+
Process sortants uniquement
+
+ | Process | User | PID | Destinations |
+
+ {% for o in outbound %}
+ | {{ o.process }} |
+ {{ o.user }} |
+ {{ o.pid }} |
+ {{ o.dests }} |
+
{% endfor %}
+
+
+ {% endif %}
+
+
+{% endblock %}
diff --git a/app/templates/audit_full_flowmap.html b/app/templates/audit_full_flowmap.html
new file mode 100644
index 0000000..e5e9c5b
--- /dev/null
+++ b/app/templates/audit_full_flowmap.html
@@ -0,0 +1,70 @@
+{% extends 'base.html' %}
+{% block title %}Carte flux{% endblock %}
+{% block content %}
+< Retour
+Carte des flux reseau
+
+{% if flows %}
+
+
+ {{ flows|length }} flux inter-serveurs
+
+
+
+ | Dir |
+ Source |
+ IP source |
+ Destination |
+ IP dest |
+ Port |
+ Process |
+ State |
+ Nb |
+
+
+ {% for f in flows %}
+
+ | {{ f.direction }} |
+ {{ f.source_hostname }} |
+ {{ f.source_ip }} |
+ {{ f.dest_hostname or '-' }} |
+ {{ f.dest_ip }} |
+ {{ f.dest_port }} |
+ {{ f.process_name }} |
+ {{ f.state }} |
+ {{ f.cnt }} |
+
+ {% endfor %}
+
+
+
+{% else %}
+Aucun flux. Importez des rapports JSON d'abord.
+{% endif %}
+
+{% if app_map %}
+Carte applicative
+
+ {% for app_name, app in app_map.items() %}
+
+
+ {{ app_name }}
+ {{ app.servers|length }} serveur(s)
+
+ {% if app.ports %}
+
Ports: {% for p in app.ports %}{{ p }}{% if not loop.last %}, {% endif %}{% endfor %}
+ {% endif %}
+
+ {% for s in app.servers %}
+
+ {{ s.hostname }}
+ user={{ s.user }}
+ {{ s.cmdline[:50] }}
+
+ {% endfor %}
+
+
+ {% endfor %}
+
+{% endif %}
+{% endblock %}
diff --git a/app/templates/audit_full_list.html b/app/templates/audit_full_list.html
new file mode 100644
index 0000000..f0c770e
--- /dev/null
+++ b/app/templates/audit_full_list.html
@@ -0,0 +1,66 @@
+{% extends 'base.html' %}
+{% block title %}Audit complet{% endblock %}
+{% block content %}
+
+
+
Audit complet serveurs
+
Applicatif + reseau + correlation — import JSON depuis le standalone
+
+
+
+
+{% if msg %}
+
+ {% if msg.startswith('imported_') %}
+ {% set parts = msg.split('_') %}
+ {{ parts[1] }} serveur(s) importe(s){% if parts[2]|int > 0 %}, {{ parts[2] }} erreur(s){% endif %}.
+ {% elif msg.startswith('error_') %}Erreur: {{ msg[6:] }}{% endif %}
+
+{% endif %}
+
+{% if audits %}
+
+
+
+ | Hostname |
+ OS |
+ Kernel |
+ Uptime |
+ Services |
+ Process |
+ Ports |
+ Conn |
+ Reboot |
+ Date |
+
+
+ {% for a in audits %}
+
+ | {{ a.hostname }} |
+ {{ (a.os_release or '')[:30] }} |
+ {{ (a.kernel or '')[:25] }} |
+ {{ (a.uptime or '')[:20] }} |
+ {{ a.svc_count }} |
+ {{ a.proc_count }} |
+ {{ a.port_count }} |
+ {{ a.conn_count }} |
+ {% if a.reboot_required %}Oui{% else %}Non{% endif %} |
+ {{ a.audit_date.strftime('%d/%m %H:%M') if a.audit_date else '-' }} |
+
+ {% endfor %}
+
+
+
+{% else %}
+
+
Aucun audit importe.
+
Lancez le standalone sur vos serveurs puis importez le JSON ici.
+
+{% endif %}
+{% endblock %}
diff --git a/app/templates/base.html b/app/templates/base.html
index a315508..d01eab5 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -63,6 +63,7 @@
{% if p.planning %}Planning{% endif %}
{% if p.audit %}Audit{% endif %}
{% if p.audit in ('edit', 'admin') %}Spécifique{% endif %}
+ {% if p.audit %}Complet{% endif %}
{% if p.servers %}Contacts{% endif %}
{% if p.users %}Utilisateurs{% endif %}
{% if p.settings %}Settings{% endif %}