Files
codexpi/scripts/security-audit.sh
CodexPi 995b17a6e2
Some checks are pending
Qualité / tests (push) Waiting to run
feat: publish CodexPi contest snapshot
2026-11-14 20:23:59 +01:00

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