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 %} +
+ + +
+ + + + {% for s in services %} + + + + + + {% endfor %} +
ServiceEnabledPIDUserExec
{{ s.name }}{{ s.enabled }}{{ s.pid }}{{ s.user }}{{ s.exec[:80] }}
+
+ + +
+ + + + {% for p in processes %} + + + + + + + {% endfor %} +
PIDUserExeCWDCmdlineRestart
{{ p.pid }}{{ p.user }}{{ (p.exe or '')[:40] }}{{ (p.cwd or '')[:30] }}{{ (p.cmdline or '')[:60] }}{{ (p.restart_hint or '')[:60] }}
+
+ + +
+ + + + {% for lp in listen_ports %} + + + + + + + {% endfor %} +
ProtoAddr:PortPIDProcessUserService
{{ lp.proto }}{{ lp.addr_port }}{{ lp.pid }}{{ lp.process }}{{ lp.user }}{{ lp.service }}
+
+ + +
+ + + + {% for c in connections %} + + + + + + + + {% endfor %} +
DirLocalRemotePIDProcessUserState
{{ c.direction }}{{ c.local }}{{ c.remote }}{{ c.pid }}{{ c.process }}{{ c.user }}{{ c.state }}
+
+ + +
+ {% if flux_in %} +
+
Flux entrants
+ + + + {% for f in flux_in %} + + + + + + {% endfor %} +
PortServiceProcessNbSources
{{ f.port }}{{ f.service }}{{ f.process }}{{ f.count }}{{ f.sources }}
+
+ {% endif %} + {% if flux_out %} +
+
Flux sortants
+ + + + {% for f in flux_out %} + + + + + + {% endfor %} +
DestinationPortServiceProcessNb
{{ f.dest_ip }}{{ f.dest_port }}{{ f.service }}{{ f.process }}{{ f.count }}
+
+ {% endif %} + {% if flows %} +
+
Carte flux (resolu)
+ + + + {% for f in flows %} + + + + + + + {% endfor %} +
DirIP destPortServeur destProcessState
{{ f.direction }}{{ f.dest_ip }}{{ f.dest_port }}{{ f.dest_hostname or '-' }}{{ f.process_name }}{{ f.state }}
+
+ {% endif %} +
+ + +
+ + + + {% for d in disk_usage %} + + + + + + {% endfor %} +
MountTailleUtiliseDispo%
{{ d.mount }}{{ d.size }}{{ d.used }}{{ d.avail }}{{ d.pct }}%
+
+ + +
+ {% 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
+ + + + {% for c in correlation %} + + + + + + + + {% endfor %} +
ProcessUserPIDPortsINOUTDestinations
{{ c.process }}{{ c.user }}{{ c.pid }}{{ c.listen_ports }}{{ c.conn_in }}{{ c.conn_out }}{{ c.remote_dests }}
+
+ {% endif %} + {% if outbound %} +
+
Process sortants uniquement
+ + + + {% for o in outbound %} + + + + + {% endfor %} +
ProcessUserPIDDestinations
{{ o.process }}{{ o.user }}{{ o.pid }}{{ o.dests }}
+
+ {% 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 +
+ + + + + + + + + + + + + + {% for f in flows %} + + + + + + + + + + + + {% endfor %} + +
DirSourceIP sourceDestinationIP destPortProcessStateNb
{{ 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 }}
+
+{% 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

+
+
+
+ + +
+ Carte flux +
+
+ +{% 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 %} +
+ + + + + + + + + + + + + + + {% for a in audits %} + + + + + + + + + + + + + {% endfor %} + +
HostnameOSKernelUptimeServicesProcessPortsConnRebootDate
{{ 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 '-' }}
+
+{% 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 %}