#!/bin/bash # ═══════════════════════════════════════════════════════════════ # L99 SSO + AUTHENTIK NON-REGRESSION v4.0 — EXHAUSTIF # Tests: auth redirect, rd= param, SSO script injection, # WebSocket, Authentik health, Docker status, flow API # Cron: */6h | Alerte: Telegram # ═══════════════════════════════════════════════════════════════ LOG_DIR="/var/log/wevia-director" LOG_FILE="$LOG_DIR/l99-sso.log" RESULT_FILE="$LOG_DIR/l99-sso-latest.json" mkdir -p "$LOG_DIR" DATE=$(date "+%Y-%m-%d %H:%M") PASS=0; FAIL=0; TOTAL=0; FAILS="" # ═══ SECTION 1: AUTHENTIK INFRASTRUCTURE ═══ echo "[$DATE] === SECTION 1: AUTHENTIK INFRA ===" >> "$LOG_FILE" # 1.1 Docker containers for c in authentik-server authentik-worker authentik-db authentik-redis; do TOTAL=$((TOTAL+1)) ST=$(docker ps --format "{{.Status}}" --filter "name=^${c}$" 2>/dev/null) if echo "$ST" | grep -q "Up"; then PASS=$((PASS+1)) else FAIL=$((FAIL+1)); FAILS="$FAILS\n• docker:$c DOWN" fi done # 1.2 Authentik API health TOTAL=$((TOTAL+1)) API_CODE=$(curl -s --max-time 5 -o /dev/null -w "%{http_code}" "http://127.0.0.1:9090/api/v3/root/config/" 2>/dev/null) if [ "$API_CODE" = "200" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• authentik-api HTTP:$API_CODE"; fi # 1.3 Flow executor API TOTAL=$((TOTAL+1)) FLOW_CODE=$(curl -s --max-time 5 -o /dev/null -w "%{http_code}" "http://127.0.0.1:9090/api/v3/flows/executor/default-authentication-flow/" -H "Host: weval-consulting.com" 2>/dev/null) if [ "$FLOW_CODE" = "200" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• flow-executor HTTP:$FLOW_CODE"; fi # 1.4 WebSocket endpoint TOTAL=$((TOTAL+1)) WS_CODE=$(curl -s --max-time 5 -o /dev/null -w "%{http_code}" "http://127.0.0.1:9090/ws/client/" 2>/dev/null) if [ "$WS_CODE" = "200" ] || [ "$WS_CODE" = "101" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• websocket HTTP:$WS_CODE"; fi # 1.5 Flow HTML loads FlowInterface JS TOTAL=$((TOTAL+1)) HAS_FLOW=$(curl -s --max-time 5 "http://127.0.0.1:9090/if/flow/default-authentication-flow/" -H "Host: weval-consulting.com" 2>/dev/null | grep -c "FlowInterface") if [ "$HAS_FLOW" -ge 1 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• flow-html no FlowInterface JS"; fi # 1.6 Version pinned (not :latest) TOTAL=$((TOTAL+1)) IMG=$(docker inspect authentik-server --format "{{.Config.Image}}" 2>/dev/null) if echo "$IMG" | grep -q ":latest"; then FAIL=$((FAIL+1)); FAILS="$FAILS\n• version NOT pinned ($IMG)"; else PASS=$((PASS+1)); fi # 1.7 Nginx WebSocket config TOTAL=$((TOTAL+1)) WS_NGINX=$(grep -c "proxy_set_header Upgrade" /etc/nginx/sites-enabled/weval-consulting 2>/dev/null) if [ "$WS_NGINX" -ge 3 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• nginx missing WebSocket headers ($WS_NGINX found)"; fi # 1.8 Nginx immutable flag TOTAL=$((TOTAL+1)) IMMUTABLE=$(lsattr /etc/nginx/sites-enabled/weval-consulting 2>/dev/null | grep -c "i") if [ "$IMMUTABLE" -ge 1 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• nginx NOT immutable"; fi # ═══ SECTION 2: SSO REDIRECT — ALL PROTECTED PAGES ═══ echo "[$DATE] === SECTION 2: SSO REDIRECT ===" >> "$LOG_FILE" PAGES=$(grep -B1 "auth_request /outpost" /etc/nginx/sites-enabled/weval-consulting 2>/dev/null | grep "location" | sed "s/.*location[= ]*//" | sed "s/ {.*//" | grep -v "^~" | grep -v "^/api/" | sort -u) for page in $PAGES; do TOTAL=$((TOTAL+1)) CODE=$(curl -s -o /tmp/sso_wb -w "%{http_code}" --max-time 5 -k "https://127.0.0.1$page" -H "Host: weval-consulting.com" 2>/dev/null) RD=$(curl -s -o /dev/null -w "%{redirect_url}" --max-time 5 -k "https://127.0.0.1$page" -H "Host: weval-consulting.com" 2>/dev/null) HAS_SCRIPT=$(grep -c "weval_sso_target" /tmp/sso_wb 2>/dev/null) OK=0 if [ "$CODE" = "302" ] && echo "$RD" | grep -q "rd="; then OK=1; elif [ "$CODE" = "200" ] && [ "$HAS_SCRIPT" -ge 1 ]; then OK=1; fi if [ "$OK" = "1" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• $page (HTTP:$CODE)"; fi done rm -f /tmp/sso_wb # ═══ SECTION 3: SSO SCRIPT INJECTION ═══ echo "[$DATE] === SECTION 3: SSO SCRIPT INJECTION ===" >> "$LOG_FILE" # Test sub_filter on homepage (always accessible) TOTAL=$((TOTAL+1)) HOME_SCRIPT=$(curl -s --max-time 5 -k "https://127.0.0.1/" -H "Host: weval-consulting.com" 2>/dev/null | grep -c "weval_sso_target") if [ "$HOME_SCRIPT" -ge 1 ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); FAILS="$FAILS\n• homepage missing sso_script"; fi # ═══ RESULTS ═══ [ "$TOTAL" = "0" ] && TOTAL=1 SCORE=$((PASS*100/TOTAL)) cat > "$RESULT_FILE" << JSONEOF {"date":"$DATE","total":$TOTAL,"pass":$PASS,"fail":$FAIL,"score":$SCORE,"failures":"$(echo -e "$FAILS" | tr '\n' '|')","version":"v4.0","sections":{"infra":8,"pages":$(echo "$PAGES" | wc -w),"injection":1}} JSONEOF echo "[$DATE] L99-SSO-v4: $PASS/$TOTAL ($SCORE%)" >> "$LOG_FILE" # Telegram alert if failures if [ $FAIL -gt 0 ]; then GROQ_KEY=$(grep "^GROQ_KEY" /etc/weval/secrets.env 2>/dev/null | cut -d= -f2) DIAG="" if [ -n "$GROQ_KEY" ]; then PROMPT="SSO test failed: $PASS/$TOTAL. Failures:$FAILS. Diagnose in 2 lines." PROMPT_ESC=$(echo "$PROMPT" | python3 -c "import sys,json;print(json.dumps(sys.stdin.read()))") DIAG=$(curl -s --max-time 15 "https://api.groq.com/openai/v1/chat/completions" \ -H "Authorization: Bearer $GROQ_KEY" -H "Content-Type: application/json" \ -d "{\"model\":\"llama-3.3-70b-versatile\",\"messages\":[{\"role\":\"user\",\"content\":$PROMPT_ESC}],\"max_tokens\":150}" 2>/dev/null \ | python3 -c "import sys,json;print(json.load(sys.stdin).get('choices',[{}])[0].get('message',{}).get('content',''))" 2>/dev/null) echo "[$DATE] IA: $DIAG" >> "$LOG_FILE" fi TG_TOKEN=$(cat /etc/weval/tg_bot_token 2>/dev/null) if [ -n "$TG_TOKEN" ]; then MSG="⚠️ L99 SSO v4 REGRESSION\n📊 $PASS/$TOTAL ($SCORE%)\n❌$FAILS" [ -n "$DIAG" ] && MSG="$MSG\n\n🧠 $DIAG" curl -s "https://api.telegram.org/bot${TG_TOKEN}/sendMessage" \ -d "chat_id=7605775322" --data-urlencode "text=$MSG" > /dev/null 2>&1 fi echo "L99-SSO-v4: $PASS/$TOTAL ($SCORE%) ❌" echo -e "FAILURES:$FAILS" else echo "L99-SSO-v4: $PASS/$TOTAL ($SCORE%) ✅" fi exit $FAIL