Audit complet: import JSON, carte flux, carte applicative
- server_audit_full_service.py: SSH PSMP/cle, parsing, stockage JSONB, flow map - server_audit.sh: script bash avec sudo (compatible PSMP cybsecope) - audit_full router: import JSON, liste, detail, carte flux - Templates: liste audits, detail 8 onglets, carte flux + carte applicative - Jointures: server_id via servers, dest_server via server_ips - Sous-menu Audit > Complet dans la sidebar Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
833c4fc3d2
commit
20cd9c7d80
@ -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("/")
|
||||
|
||||
104
app/routers/audit_full.py
Normal file
104
app/routers/audit_full.py
Normal file
@ -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)
|
||||
412
app/scripts/server_audit.sh
Executable file
412
app/scripts/server_audit.sh
Executable file
@ -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 "########################################################"
|
||||
496
app/services/server_audit_full_service.py
Normal file
496
app/services/server_audit_full_service.py
Normal file
@ -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
|
||||
232
app/templates/audit_full_detail.html
Normal file
232
app/templates/audit_full_detail.html
Normal file
@ -0,0 +1,232 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}{{ a.hostname }}{% endblock %}
|
||||
{% block content %}
|
||||
<a href="/audit-full" class="text-xs text-gray-500 hover:text-gray-300">< Retour</a>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-cyber-accent">{{ a.hostname }}</h2>
|
||||
<p class="text-xs text-gray-500">{{ a.os_release }} | {{ a.kernel }} | {{ a.uptime }}</p>
|
||||
</div>
|
||||
<div class="flex gap-2 text-xs">
|
||||
{% for iface in interfaces %}{% if iface.ip != '127.0.0.1' %}
|
||||
<span class="badge badge-blue">{{ iface.ip }}{{ iface.mask }} ({{ iface.iface }})</span>
|
||||
{% endif %}{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI -->
|
||||
<div class="grid grid-cols-6 gap-3 mb-4">
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold text-cyber-accent">{{ services|length }}</div><div class="text-xs text-gray-500">Services</div></div>
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold text-cyber-accent">{{ processes|length }}</div><div class="text-xs text-gray-500">Process</div></div>
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold text-cyber-accent">{{ listen_ports|length }}</div><div class="text-xs text-gray-500">Ports</div></div>
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold text-cyber-accent">{{ connections|length }}</div><div class="text-xs text-gray-500">Connexions</div></div>
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold {% if a.reboot_required %}text-cyber-red{% else %}text-cyber-green{% endif %}">{% if a.reboot_required %}Oui{% else %}Non{% endif %}</div><div class="text-xs text-gray-500">Reboot</div></div>
|
||||
<div class="card p-3 text-center"><div class="text-lg font-bold {% if a.services_failed %}text-cyber-red{% else %}text-cyber-green{% endif %}">{% if a.services_failed %}KO{% else %}OK{% endif %}</div><div class="text-xs text-gray-500">Failed svc</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Onglets -->
|
||||
<div x-data="{ tab: 'services' }">
|
||||
<div class="flex gap-1 mb-3 flex-wrap">
|
||||
{% for t, label in [('services','Services'),('processes','Processus'),('ports','Ports'),('connections','Connexions'),('flux','Flux'),('disk','Disque'),('firewall','Firewall'),('correlation','Correlation')] %}
|
||||
<button @click="tab='{{ t }}'" class="px-3 py-1 text-xs rounded" :class="tab==='{{ t }}' ? 'bg-cyber-accent text-black font-bold' : 'bg-cyber-border text-gray-400'">{{ label }}</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div x-show="tab==='services'" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="text-left p-2">Service</th><th class="p-2">Enabled</th><th class="p-2">PID</th><th class="p-2">User</th><th class="text-left p-2">Exec</th>
|
||||
</tr></thead><tbody>
|
||||
{% for s in services %}<tr>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ s.name }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.enabled == 'enabled' %}badge-green{% else %}badge-gray{% endif %}">{{ s.enabled }}</span></td>
|
||||
<td class="p-2 text-center font-mono">{{ s.pid }}</td>
|
||||
<td class="p-2 text-center">{{ s.user }}</td>
|
||||
<td class="p-2 text-gray-400">{{ s.exec[:80] }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<!-- Processus -->
|
||||
<div x-show="tab==='processes'" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="p-2">PID</th><th class="p-2">User</th><th class="text-left p-2">Exe</th><th class="text-left p-2">CWD</th><th class="text-left p-2">Cmdline</th><th class="text-left p-2">Restart</th>
|
||||
</tr></thead><tbody>
|
||||
{% for p in processes %}<tr class="{% if '/applis' in (p.cwd or '') %}bg-green-900/10{% endif %}">
|
||||
<td class="p-2 font-mono text-center">{{ p.pid }}</td>
|
||||
<td class="p-2 text-center">{{ p.user }}</td>
|
||||
<td class="p-2 font-mono text-gray-400">{{ (p.exe or '')[:40] }}</td>
|
||||
<td class="p-2 font-mono text-gray-500">{{ (p.cwd or '')[:30] }}</td>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ (p.cmdline or '')[:60] }}</td>
|
||||
<td class="p-2 text-gray-400">{{ (p.restart_hint or '')[:60] }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<!-- Ports -->
|
||||
<div x-show="tab==='ports'" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="p-2">Proto</th><th class="p-2">Addr:Port</th><th class="p-2">PID</th><th class="p-2">Process</th><th class="p-2">User</th><th class="p-2">Service</th>
|
||||
</tr></thead><tbody>
|
||||
{% for lp in listen_ports %}<tr>
|
||||
<td class="p-2 text-center">{{ lp.proto }}</td>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ lp.addr_port }}</td>
|
||||
<td class="p-2 text-center font-mono">{{ lp.pid }}</td>
|
||||
<td class="p-2 text-center">{{ lp.process }}</td>
|
||||
<td class="p-2 text-center">{{ lp.user }}</td>
|
||||
<td class="p-2 text-center text-gray-400">{{ lp.service }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<!-- Connexions -->
|
||||
<div x-show="tab==='connections'" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="p-2">Dir</th><th class="p-2">Local</th><th class="p-2">Remote</th><th class="p-2">PID</th><th class="p-2">Process</th><th class="p-2">User</th><th class="p-2">State</th>
|
||||
</tr></thead><tbody>
|
||||
{% for c in connections %}<tr class="{% if c.state == 'CLOSE-WAIT' %}bg-red-900/10{% endif %}">
|
||||
<td class="p-2 text-center"><span class="badge {% if c.direction == 'IN' %}badge-green{% else %}badge-yellow{% endif %}">{{ c.direction }}</span></td>
|
||||
<td class="p-2 font-mono">{{ c.local }}</td>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ c.remote }}</td>
|
||||
<td class="p-2 text-center font-mono">{{ c.pid }}</td>
|
||||
<td class="p-2 text-center">{{ c.process }}</td>
|
||||
<td class="p-2 text-center">{{ c.user }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if c.state == 'ESTAB' %}badge-green{% elif c.state == 'CLOSE-WAIT' %}badge-red{% else %}badge-gray{% endif %}">{{ c.state }}</span></td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<!-- Flux -->
|
||||
<div x-show="tab==='flux'" class="space-y-3">
|
||||
{% if flux_in %}
|
||||
<div class="card overflow-x-auto">
|
||||
<div class="p-2 border-b border-cyber-border"><span class="text-xs font-bold text-cyber-green">Flux entrants</span></div>
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="p-2">Port</th><th class="p-2">Service</th><th class="p-2">Process</th><th class="p-2">Nb</th><th class="text-left p-2">Sources</th>
|
||||
</tr></thead><tbody>
|
||||
{% for f in flux_in %}<tr>
|
||||
<td class="p-2 text-center font-mono text-cyber-accent">{{ f.port }}</td>
|
||||
<td class="p-2 text-center">{{ f.service }}</td>
|
||||
<td class="p-2 text-center">{{ f.process }}</td>
|
||||
<td class="p-2 text-center font-bold">{{ f.count }}</td>
|
||||
<td class="p-2 font-mono text-gray-400">{{ f.sources }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if flux_out %}
|
||||
<div class="card overflow-x-auto">
|
||||
<div class="p-2 border-b border-cyber-border"><span class="text-xs font-bold text-cyber-yellow">Flux sortants</span></div>
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="text-left p-2">Destination</th><th class="p-2">Port</th><th class="p-2">Service</th><th class="p-2">Process</th><th class="p-2">Nb</th>
|
||||
</tr></thead><tbody>
|
||||
{% for f in flux_out %}<tr>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ f.dest_ip }}</td>
|
||||
<td class="p-2 text-center">{{ f.dest_port }}</td>
|
||||
<td class="p-2 text-center">{{ f.service }}</td>
|
||||
<td class="p-2 text-center">{{ f.process }}</td>
|
||||
<td class="p-2 text-center font-bold">{{ f.count }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if flows %}
|
||||
<div class="card overflow-x-auto">
|
||||
<div class="p-2 border-b border-cyber-border"><span class="text-xs font-bold text-cyber-accent">Carte flux (resolu)</span></div>
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="p-2">Dir</th><th class="p-2">IP dest</th><th class="p-2">Port</th><th class="p-2">Serveur dest</th><th class="p-2">Process</th><th class="p-2">State</th>
|
||||
</tr></thead><tbody>
|
||||
{% for f in flows %}<tr>
|
||||
<td class="p-2 text-center"><span class="badge {% if f.direction == 'IN' %}badge-green{% else %}badge-yellow{% endif %}">{{ f.direction }}</span></td>
|
||||
<td class="p-2 font-mono">{{ f.dest_ip }}</td>
|
||||
<td class="p-2 text-center">{{ f.dest_port }}</td>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ f.dest_hostname or '-' }}</td>
|
||||
<td class="p-2 text-center">{{ f.process_name }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if f.state == 'ESTAB' %}badge-green{% elif f.state == 'CLOSE-WAIT' %}badge-red{% else %}badge-gray{% endif %}">{{ f.state }}</span></td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Disque -->
|
||||
<div x-show="tab==='disk'" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="text-left p-2">Mount</th><th class="p-2">Taille</th><th class="p-2">Utilise</th><th class="p-2">Dispo</th><th class="p-2">%</th>
|
||||
</tr></thead><tbody>
|
||||
{% for d in disk_usage %}<tr class="{% if d.pct >= 90 %}bg-red-900/20{% elif d.pct >= 80 %}bg-yellow-900/10{% endif %}">
|
||||
<td class="p-2 font-mono">{{ d.mount }}</td>
|
||||
<td class="p-2 text-center">{{ d.size }}</td>
|
||||
<td class="p-2 text-center">{{ d.used }}</td>
|
||||
<td class="p-2 text-center">{{ d.avail }}</td>
|
||||
<td class="p-2 text-center font-bold {% if d.pct >= 90 %}text-cyber-red{% elif d.pct >= 80 %}text-cyber-yellow{% else %}text-cyber-green{% endif %}">{{ d.pct }}%</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
|
||||
<!-- Firewall -->
|
||||
<div x-show="tab==='firewall'" class="space-y-3">
|
||||
{% if firewall.policy %}
|
||||
<div class="card p-3">
|
||||
<span class="text-xs font-bold text-cyber-accent">Policy iptables :</span>
|
||||
{% for chain, pol in firewall.policy.items() %}
|
||||
<span class="badge {% if pol == 'DROP' %}badge-red{% elif pol == 'ACCEPT' %}badge-green{% else %}badge-gray{% endif %} ml-2">{{ chain }}={{ pol or '?' }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if firewall.firewalld %}
|
||||
<div class="card p-3">
|
||||
<span class="text-xs font-bold text-cyber-accent">Firewalld :</span>
|
||||
{% for z in firewall.firewalld %}
|
||||
<div class="mt-1 text-xs">Zone <span class="text-cyber-yellow">{{ z.zone }}</span> : services={{ z.services }} ports={{ z.ports }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if conn_wait %}
|
||||
<div class="card p-3">
|
||||
<span class="text-xs font-bold text-cyber-accent">Connexions en attente :</span>
|
||||
{% for cw in conn_wait %}
|
||||
<span class="badge {% if cw.state == 'CLOSE-WAIT' %}badge-red{% else %}badge-gray{% endif %} ml-2">{{ cw.state }}={{ cw.count }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Correlation -->
|
||||
<div x-show="tab==='correlation'" class="space-y-3">
|
||||
{% if correlation %}
|
||||
<div class="card overflow-x-auto">
|
||||
<div class="p-2 border-b border-cyber-border"><span class="text-xs font-bold text-cyber-accent">Matrice process / ports / flux</span></div>
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="text-left p-2">Process</th><th class="p-2">User</th><th class="p-2">PID</th><th class="p-2">Ports</th><th class="p-2">IN</th><th class="p-2">OUT</th><th class="text-left p-2">Destinations</th>
|
||||
</tr></thead><tbody>
|
||||
{% for c in correlation %}<tr>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ c.process }}</td>
|
||||
<td class="p-2 text-center">{{ c.user }}</td>
|
||||
<td class="p-2 text-center font-mono">{{ c.pid }}</td>
|
||||
<td class="p-2 text-center font-mono text-cyber-yellow">{{ c.listen_ports }}</td>
|
||||
<td class="p-2 text-center font-bold text-cyber-green">{{ c.conn_in }}</td>
|
||||
<td class="p-2 text-center font-bold text-cyber-yellow">{{ c.conn_out }}</td>
|
||||
<td class="p-2 font-mono text-gray-400">{{ c.remote_dests }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if outbound %}
|
||||
<div class="card overflow-x-auto">
|
||||
<div class="p-2 border-b border-cyber-border"><span class="text-xs font-bold text-cyber-yellow">Process sortants uniquement</span></div>
|
||||
<table class="w-full table-cyber text-xs"><thead><tr>
|
||||
<th class="text-left p-2">Process</th><th class="p-2">User</th><th class="p-2">PID</th><th class="text-left p-2">Destinations</th>
|
||||
</tr></thead><tbody>
|
||||
{% for o in outbound %}<tr>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ o.process }}</td>
|
||||
<td class="p-2 text-center">{{ o.user }}</td>
|
||||
<td class="p-2 text-center font-mono">{{ o.pid }}</td>
|
||||
<td class="p-2 font-mono text-gray-400">{{ o.dests }}</td>
|
||||
</tr>{% endfor %}
|
||||
</tbody></table>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
70
app/templates/audit_full_flowmap.html
Normal file
70
app/templates/audit_full_flowmap.html
Normal file
@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Carte flux{% endblock %}
|
||||
{% block content %}
|
||||
<a href="/audit-full" class="text-xs text-gray-500 hover:text-gray-300">< Retour</a>
|
||||
<h2 class="text-xl font-bold text-cyber-accent mb-4">Carte des flux reseau</h2>
|
||||
|
||||
{% if flows %}
|
||||
<div class="card overflow-x-auto mb-4">
|
||||
<div class="p-2 border-b border-cyber-border">
|
||||
<span class="text-xs font-bold text-cyber-accent">{{ flows|length }} flux inter-serveurs</span>
|
||||
</div>
|
||||
<table class="w-full table-cyber text-xs">
|
||||
<thead><tr>
|
||||
<th class="p-2">Dir</th>
|
||||
<th class="text-left p-2">Source</th>
|
||||
<th class="p-2">IP source</th>
|
||||
<th class="text-left p-2">Destination</th>
|
||||
<th class="p-2">IP dest</th>
|
||||
<th class="p-2">Port</th>
|
||||
<th class="p-2">Process</th>
|
||||
<th class="p-2">State</th>
|
||||
<th class="p-2">Nb</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for f in flows %}
|
||||
<tr class="{% if f.state == 'CLOSE-WAIT' %}bg-red-900/10{% endif %}">
|
||||
<td class="p-2 text-center"><span class="badge {% if f.direction == 'IN' %}badge-green{% else %}badge-yellow{% endif %}">{{ f.direction }}</span></td>
|
||||
<td class="p-2 font-mono text-cyber-accent">{{ f.source_hostname }}</td>
|
||||
<td class="p-2 font-mono text-gray-500 text-center">{{ f.source_ip }}</td>
|
||||
<td class="p-2 font-mono {% if f.dest_hostname %}text-cyber-accent{% else %}text-gray-400{% endif %}">{{ f.dest_hostname or '-' }}</td>
|
||||
<td class="p-2 font-mono text-gray-500 text-center">{{ f.dest_ip }}</td>
|
||||
<td class="p-2 text-center font-bold">{{ f.dest_port }}</td>
|
||||
<td class="p-2 text-center">{{ f.process_name }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if f.state == 'ESTAB' %}badge-green{% elif f.state == 'CLOSE-WAIT' %}badge-red{% else %}badge-gray{% endif %}">{{ f.state }}</span></td>
|
||||
<td class="p-2 text-center">{{ f.cnt }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card p-8 text-center text-gray-500">Aucun flux. Importez des rapports JSON d'abord.</div>
|
||||
{% endif %}
|
||||
|
||||
{% if app_map %}
|
||||
<h3 class="text-lg font-bold text-cyber-accent mb-3">Carte applicative</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
{% for app_name, app in app_map.items() %}
|
||||
<div class="card p-3">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<span class="text-sm font-bold text-cyber-yellow">{{ app_name }}</span>
|
||||
<span class="badge badge-blue">{{ app.servers|length }} serveur(s)</span>
|
||||
</div>
|
||||
{% if app.ports %}
|
||||
<div class="text-xs text-gray-500 mb-2">Ports: {% for p in app.ports %}<span class="font-mono text-cyber-accent">{{ p }}</span>{% if not loop.last %}, {% endif %}{% endfor %}</div>
|
||||
{% endif %}
|
||||
<div class="space-y-1">
|
||||
{% for s in app.servers %}
|
||||
<div class="text-xs font-mono">
|
||||
<span class="text-cyber-accent">{{ s.hostname }}</span>
|
||||
<span class="text-gray-500">user={{ s.user }}</span>
|
||||
<span class="text-gray-600">{{ s.cmdline[:50] }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
66
app/templates/audit_full_list.html
Normal file
66
app/templates/audit_full_list.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Audit complet{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-cyber-accent">Audit complet serveurs</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">Applicatif + reseau + correlation — import JSON depuis le standalone</p>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<form method="POST" action="/audit-full/import" enctype="multipart/form-data" class="flex gap-2 items-center">
|
||||
<input type="file" name="file" accept=".json" class="text-xs" required>
|
||||
<button type="submit" class="btn-primary px-4 py-2 text-sm" data-loading="Import en cours...|Insertion des donnees">Importer JSON</button>
|
||||
</form>
|
||||
<a href="/audit-full/flow-map" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Carte flux</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if msg %}
|
||||
<div class="mb-3 p-2 rounded text-sm {% if 'error' in msg %}bg-red-900/30 text-cyber-red{% else %}bg-green-900/30 text-cyber-green{% endif %}">
|
||||
{% 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 %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if audits %}
|
||||
<div class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber text-xs">
|
||||
<thead><tr>
|
||||
<th class="text-left p-2">Hostname</th>
|
||||
<th class="p-2">OS</th>
|
||||
<th class="p-2">Kernel</th>
|
||||
<th class="p-2">Uptime</th>
|
||||
<th class="p-2">Services</th>
|
||||
<th class="p-2">Process</th>
|
||||
<th class="p-2">Ports</th>
|
||||
<th class="p-2">Conn</th>
|
||||
<th class="p-2">Reboot</th>
|
||||
<th class="p-2">Date</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for a in audits %}
|
||||
<tr class="hover:bg-cyber-hover cursor-pointer" onclick="location.href='/audit-full/{{ a.id }}'">
|
||||
<td class="p-2 font-mono text-cyber-accent font-bold">{{ a.hostname }}</td>
|
||||
<td class="p-2 text-center text-gray-400">{{ (a.os_release or '')[:30] }}</td>
|
||||
<td class="p-2 text-center font-mono text-gray-500">{{ (a.kernel or '')[:25] }}</td>
|
||||
<td class="p-2 text-center text-gray-400">{{ (a.uptime or '')[:20] }}</td>
|
||||
<td class="p-2 text-center">{{ a.svc_count }}</td>
|
||||
<td class="p-2 text-center">{{ a.proc_count }}</td>
|
||||
<td class="p-2 text-center">{{ a.port_count }}</td>
|
||||
<td class="p-2 text-center">{{ a.conn_count }}</td>
|
||||
<td class="p-2 text-center">{% if a.reboot_required %}<span class="text-cyber-red">Oui</span>{% else %}<span class="text-cyber-green">Non</span>{% endif %}</td>
|
||||
<td class="p-2 text-center text-gray-500">{{ a.audit_date.strftime('%d/%m %H:%M') if a.audit_date else '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card p-8 text-center text-gray-500">
|
||||
<p class="text-sm">Aucun audit importe.</p>
|
||||
<p class="text-xs mt-2">Lancez le standalone sur vos serveurs puis importez le JSON ici.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -63,6 +63,7 @@
|
||||
{% if p.planning %}<a href="/planning" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'planning' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Planning</a>{% endif %}
|
||||
{% if p.audit %}<a href="/audit" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if request.url.path == '/audit' %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Audit</a>{% endif %}
|
||||
{% if p.audit in ('edit', 'admin') %}<a href="/audit/specific" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'specific' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Spécifique</a>{% endif %}
|
||||
{% if p.audit %}<a href="/audit-full" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'audit-full' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Complet</a>{% endif %}
|
||||
{% if p.servers %}<a href="/contacts" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'contacts' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Contacts</a>{% endif %}
|
||||
{% if p.users %}<a href="/users" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'users' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Utilisateurs</a>{% endif %}
|
||||
{% if p.settings %}<a href="/settings" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'settings' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Settings</a>{% endif %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user