72 lines
4.5 KiB
Bash
72 lines
4.5 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
set -u
|
||
|
|
|
||
|
|
BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
|
|
cd "$BASE"
|
||
|
|
FAILURES=0
|
||
|
|
pass(){ printf 'PASS %s\n' "$1"; }
|
||
|
|
warn(){ printf 'WARN %s\n' "$1"; }
|
||
|
|
fail(){ printf 'FAIL %s\n' "$1"; FAILURES=$((FAILURES+1)); }
|
||
|
|
|
||
|
|
echo "=== Identité et système ==="
|
||
|
|
echo "Date UTC: $(date -u +%FT%TZ)"
|
||
|
|
echo "Utilisateur: $(id -un) uid=$(id -u)"
|
||
|
|
[[ "$(id -u)" != 0 ]] && pass "processus d'audit non-root" || fail "audit exécuté en root"
|
||
|
|
uname -a
|
||
|
|
|
||
|
|
echo "=== Ports réellement en écoute ==="
|
||
|
|
ss -tulpn 2>/dev/null || { warn "ss indisponible"; netstat -tulpn 2>/dev/null || true; }
|
||
|
|
PROXY_PORT="$(sed -n 's/^CODEXPI_PROXY_PORT=//p' .env 2>/dev/null | tail -n 1)"
|
||
|
|
PROXY_PORT="${PROXY_PORT:-8765}"
|
||
|
|
if ss -lnt 2>/dev/null | grep -Eq "(:|\\])${PROXY_PORT}([[:space:]]|$)"; then pass "nginx CodexPi écoute sur le port réseau ${PROXY_PORT}"; else fail "reverse proxy absent du port ${PROXY_PORT}"; fi
|
||
|
|
if ss -lnt 2>/dev/null | grep -Eq "(^|[[:space:]])(0\\.0\\.0\\.0|\\*|\\[::\\]):(8000|8001)"; then fail "FastAPI ou Django est publié directement"; else pass "ports internes 8000/8001 non publiés"; fi
|
||
|
|
docker compose ps 2>/dev/null || warn "stack Docker non démarrée"
|
||
|
|
|
||
|
|
echo "=== Tailscale ==="
|
||
|
|
if command -v tailscale >/dev/null 2>&1; then
|
||
|
|
tailscale version | head -n 1
|
||
|
|
tailscale ip -4 2>/dev/null || warn "aucune IP Tailscale"
|
||
|
|
tailscale status 2>/dev/null || warn "Tailscale non connecté"
|
||
|
|
tailscale serve status 2>/dev/null || true
|
||
|
|
tailscale funnel status 2>/dev/null || true
|
||
|
|
else warn "Tailscale absent"; fi
|
||
|
|
|
||
|
|
echo "=== Firewall et fail2ban (lecture seule) ==="
|
||
|
|
if command -v ufw >/dev/null 2>&1; then sudo -n ufw status verbose 2>/dev/null || ufw status verbose 2>/dev/null || warn "UFW présent mais état lisible seulement avec sudo"; else warn "UFW absent"; fi
|
||
|
|
if command -v fail2ban-client >/dev/null 2>&1; then sudo -n fail2ban-client status 2>/dev/null || fail2ban-client status 2>/dev/null || warn "fail2ban présent mais état non lisible"; else warn "fail2ban absent"; fi
|
||
|
|
|
||
|
|
echo "=== HTTP et protections applicatives ==="
|
||
|
|
HEADERS="$(mktemp)"
|
||
|
|
trap 'rm -f "$HEADERS"' EXIT
|
||
|
|
if curl -fsS --max-time 5 -D "$HEADERS" -o /dev/null "http://127.0.0.1:${PROXY_PORT}/"; then
|
||
|
|
grep -Eqi '^content-security-policy:' "$HEADERS" && pass "CSP" || fail "CSP absente"
|
||
|
|
grep -Eqi '^x-frame-options:[[:space:]]*DENY' "$HEADERS" && pass "X-Frame-Options" || fail "X-Frame-Options"
|
||
|
|
grep -Eqi '^x-content-type-options:[[:space:]]*nosniff' "$HEADERS" && pass "nosniff" || fail "nosniff"
|
||
|
|
grep -Eqi '^referrer-policy:[[:space:]]*no-referrer' "$HEADERS" && pass "Referrer-Policy" || fail "Referrer-Policy"
|
||
|
|
CODE="$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PROXY_PORT}/api/auth/me")"
|
||
|
|
[[ "$CODE" == 401 ]] && pass "endpoint privé refuse une session anonyme" || fail "endpoint privé anonyme: HTTP $CODE"
|
||
|
|
else fail "endpoint HTTP local inaccessible"; fi
|
||
|
|
|
||
|
|
echo "=== Fichiers, secrets et mode debug ==="
|
||
|
|
stat -c '%A %a %U:%G %n' data data/secret_key data/django_secret_key data/security.log 2>/dev/null || warn "certains fichiers de données n'existent pas encore"
|
||
|
|
[[ ! -e data/secret_key || "$(stat -c '%a' data/secret_key)" == 600 ]] && pass "secret_key en 0600" || fail "permissions secret_key"
|
||
|
|
[[ ! -e data/security.log || "$(stat -c '%a' data/security.log)" == 600 ]] && pass "security.log en 0600" || fail "permissions security.log"
|
||
|
|
[[ ! -e data/django_secret_key || "$(stat -c '%a' data/django_secret_key)" == 600 ]] && pass "django_secret_key en 0600" || fail "permissions django_secret_key"
|
||
|
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||
|
|
if ./scripts/check-secrets.sh --worktree >/dev/null; then
|
||
|
|
pass "secrets, clés et fichiers confidentiels absents des sources publiables"
|
||
|
|
else
|
||
|
|
fail "secret, clé ou fichier confidentiel détecté dans les sources publiables"
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
grep -RInE 'debug[[:space:]]*=[[:space:]]*true|DEBUG[[:space:]]*=[[:space:]]*True|--reload' backend django_ui scripts --exclude-dir=__pycache__ && fail "mode debug/reload détecté" || pass "modes debug Django/FastAPI désactivés"
|
||
|
|
|
||
|
|
echo "=== Tests ciblés ==="
|
||
|
|
docker compose exec -T django-web python3 manage.py check --deploy --fail-level ERROR 2>/dev/null && pass "Django system check" || fail "Django system check"
|
||
|
|
docker compose exec -T api python3 -m compileall -q /app/backend 2>/dev/null && pass "FastAPI compileall" || fail "FastAPI compileall"
|
||
|
|
|
||
|
|
echo "=== Résultat ==="
|
||
|
|
if [[ "$FAILURES" -eq 0 ]]; then echo "AUDIT PASS (les WARN nécessitent une revue humaine)"; exit 0; fi
|
||
|
|
echo "AUDIT FAIL: $FAILURES contrôle(s) en échec"
|
||
|
|
exit 1
|