#!/usr/bin/env bash # ============================================================================= # lib/run-stack-tests.sh — Bibliothèque partagée pour les tests d'intégration # des stacks web avec ja4ebpf. # # Usage (depuis un run-tests.sh par stack) : # STACK_NAME="nginx" # COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yml" # source "$(dirname "$0")/../lib/run-stack-tests.sh" # run_all_phases # # Variables obligatoires à définir avant source : # STACK_NAME — nom court (nginx / nginx-varnish / hitch-varnish) # COMPOSE_FILE — chemin absolu vers le docker-compose.yml de la stack # # Variables optionnelles : # KEEP_UP — true = garder la stack après tests (défaut : false) # BUILD_ONLY — true = build uniquement, pas de tests (défaut : false) # STACK_HEALTH_URL — URL pour le healthcheck principal (défaut : /health) # TLS_PORT — port HTTPS exposé par la stack (défaut : 443) # HTTP_PORT — port HTTP exposé (défaut : 80) # # Fonctions de vérification spécifiques à la stack (optionnel) : # stack_verify_extra() — hook appelé après la phase 5 générique # ============================================================================= # --------------------------------------------------------------------------- # Couleurs et helpers de logging # --------------------------------------------------------------------------- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' TESTS_PASSED=0 TESTS_FAILED=0 KEEP_UP="${KEEP_UP:-false}" BUILD_ONLY="${BUILD_ONLY:-false}" TLS_PORT="${TLS_PORT:-443}" HTTP_PORT="${HTTP_PORT:-80}" log() { echo -e "${CYAN}[${STACK_NAME}]${NC} $(date +%H:%M:%S) $*"; } pass() { echo -e "${GREEN} ✓ $*${NC}"; TESTS_PASSED=$((TESTS_PASSED + 1)); } fail() { echo -e "${RED} ✗ $*${NC}"; TESTS_FAILED=$((TESTS_FAILED + 1)); } warn() { echo -e "${YELLOW} ⚠ $*${NC}"; } _dc() { docker compose -f "$COMPOSE_FILE" "$@"; } # Exécute une requête SQL ClickHouse dans le service clickhouse du compose. ch_query() { _dc exec -T clickhouse clickhouse-client --query "$1" 2>/dev/null; } # Attend qu'un service atteigne l'état healthy (max_wait secondes). wait_for_service() { local service="$1" max_wait="${2:-120}" elapsed=0 log "En attente de $service (max ${max_wait}s)…" while [ $elapsed -lt "$max_wait" ]; do local status status=$(_dc ps --format json "$service" 2>/dev/null \ | python3 -c "import sys,json; [print(json.loads(l).get('Health','')) for l in sys.stdin]" 2>/dev/null \ || echo "") if [ "$status" = "healthy" ]; then log "$service est healthy (${elapsed}s)"; return 0 fi sleep 2; elapsed=$((elapsed + 2)) done log "ERREUR : $service pas healthy après ${max_wait}s" _dc logs --tail=30 "$service" return 1 } # --------------------------------------------------------------------------- # Phase 1 — Build # --------------------------------------------------------------------------- phase_build() { log "========== Phase 1 : Build ==========" _dc build --parallel 2>&1 | tail -20 if [ "${BUILD_ONLY:-false}" = true ]; then log "Build terminé (--build-only)." exit 0 fi } # --------------------------------------------------------------------------- # Phase 2 — Démarrage de la stack (DB fraîche) # --------------------------------------------------------------------------- phase_start() { log "========== Phase 2 : Démarrage (DB fraîche) ==========" _dc down -v --remove-orphans 2>/dev/null || true _dc up -d wait_for_service clickhouse 120 wait_for_service platform 120 } # --------------------------------------------------------------------------- # Phase 3 — Vérification du schéma ClickHouse # --------------------------------------------------------------------------- phase_schema() { log "========== Phase 3 : Schéma ClickHouse ==========" local db_count db_count=$(ch_query "SELECT count() FROM system.databases WHERE name IN ('ja4_logs','ja4_processing')") [ "$db_count" = "2" ] \ && pass "Bases ja4_logs + ja4_processing créées" \ || fail "Bases ClickHouse manquantes (obtenu: $db_count)" for table in "ja4_logs.http_logs_raw" "ja4_logs.http_logs" \ "ja4_processing.ml_detected_anomalies" \ "ja4_processing.agg_host_ip_ja4_1h"; do local db tbl exists db="${table%%.*}"; tbl="${table##*.}" exists=$(ch_query "SELECT count() FROM system.tables WHERE database='$db' AND name='$tbl'") [ "$exists" = "1" ] \ && pass "Table $table présente" \ || fail "Table $table manquante" done } # --------------------------------------------------------------------------- # Phase 4 — Génération de trafic # --------------------------------------------------------------------------- phase_traffic() { log "========== Phase 4 : Génération de trafic ==========" local requests="${TRAFFIC_REQUESTS:-300}" workers="${TRAFFIC_WORKERS:-8}" # Le générateur de trafic réutilise l'image du dossier traffic-gen existant. if _dc exec -T traffic-gen python /app/generate_traffic.py \ --host platform \ --http-port "$HTTP_PORT" \ --https-port "$TLS_PORT" \ --requests "$requests" \ --workers "$workers"; then pass "Trafic généré : $requests requêtes via la stack $STACK_NAME" else warn "Générateur de trafic : quelques erreurs (seuil 80% appliqué)" fi log "Attente 15s pour flush ja4ebpf → ClickHouse…" sleep 15 } # --------------------------------------------------------------------------- # Phase 5 — Vérification du pipeline de données # --------------------------------------------------------------------------- phase_verify() { log "========== Phase 5 : Vérification pipeline ==========" # 5a. Lignes brutes insérées par ja4ebpf local raw_count raw_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs_raw" || echo "0") if [ "${raw_count:-0}" -gt 0 ] 2>/dev/null; then pass "http_logs_raw : $raw_count lignes insérées par ja4ebpf" else fail "http_logs_raw vide — ja4ebpf n'a pas écrit dans ClickHouse" log "Logs platform :" _dc logs --tail=40 platform | grep -i "ebpf\|error\|clickhouse" | head -20 fi # 5b. Fingerprints JA4 capturés (hook TC + parsing TLS ClientHello) # Requête sur http_logs (colonnes structurées après le MV) local ja4_count ja4_uniq ja4_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs WHERE ja4 != ''" || echo "0") ja4_uniq=$( ch_query "SELECT count(DISTINCT ja4) FROM ja4_logs.http_logs WHERE ja4 != ''" || echo "0") if [ "${ja4_count:-0}" -gt 0 ] 2>/dev/null; then pass "JA4 : $ja4_count enregistrements, $ja4_uniq fingerprints distincts" local ja4_sample ja4_sample=$(ch_query "SELECT ja4 FROM ja4_logs.http_logs WHERE ja4 != '' LIMIT 1" || echo "") log " Exemple JA4 : $ja4_sample" else warn "Aucun fingerprint JA4 (hook TC peut-être non chargé — vérifier CAP_BPF)" fi # 5c. Données L3/L4 (TTL, MSS, Window) — colonnes ip_meta_* / tcp_meta_* dans http_logs local l34_count l34_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs WHERE ip_meta_ttl > 0" || echo "0") if [ "${l34_count:-0}" -gt 0 ] 2>/dev/null; then pass "L3/L4 : $l34_count enregistrements avec TTL (hook TC actif)" local ttl_sample ttl_sample=$(ch_query "SELECT ip_meta_ttl, tcp_meta_mss, tcp_meta_window_size FROM ja4_logs.http_logs WHERE ip_meta_ttl > 0 LIMIT 1 FORMAT TabSeparated" || echo "") log " TTL/MSS/Window sample : $ttl_sample" else warn "Données L3/L4 absentes (hook TC ingress non attaché)" fi # 5d. Requêtes HTTP capturées (uprobe SSL_read) local http_count methods http_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs WHERE method != ''" || echo "0") methods=$( ch_query "SELECT groupArray(method) FROM (SELECT DISTINCT method FROM ja4_logs.http_logs WHERE method != '' ORDER BY method)" || echo "") if [ "${http_count:-0}" -gt 0 ] 2>/dev/null; then pass "L7 HTTP : $http_count requêtes capturées via uprobe SSL_read" pass "Méthodes HTTP vues : $methods" else warn "Aucune requête HTTP capturée (uprobe SSL_read non attaché)" fi # 5e. TLS SNI capturés (hook TC + parsing ClientHello) local sni_count sni_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs WHERE tls_sni != ''" || echo "0") if [ "${sni_count:-0}" -gt 0 ] 2>/dev/null; then pass "TLS SNI : $sni_count enregistrements avec SNI capturé" local sni_sample sni_sample=$(ch_query "SELECT tls_sni FROM ja4_logs.http_logs WHERE tls_sni != '' LIMIT 1" || echo "") log " Exemple SNI : $sni_sample" else warn "Aucun SNI capturé (trafic TLS sans extension SNI ou hook TC inactif)" fi # 5f. Corrélation L3/L4 ↔ L7 (flag correlated) local corr_total corr_yes corr_pct corr_total=$(ch_query "SELECT count() FROM ja4_logs.http_logs WHERE method != ''" || echo "0") corr_yes=$( ch_query "SELECT count() FROM ja4_logs.http_logs WHERE correlated = 1 AND method != ''" || echo "0") if [ "${corr_total:-0}" -gt 0 ] 2>/dev/null; then corr_pct=$(echo "$corr_yes $corr_total" | awk '{printf "%.0f", $1*100/$2}') if [ "${corr_pct:-0}" -ge 50 ] 2>/dev/null; then pass "Corrélation L3↔L7 : ${corr_pct}% ($corr_yes/$corr_total requêtes corrélées)" else warn "Corrélation L3↔L7 faible : ${corr_pct}% ($corr_yes/$corr_total)" fi fi # 5g. Keep-alives (multiplexage TCP) local ka_max ka_max=$(ch_query "SELECT max(keepalives) FROM ja4_logs.http_logs" || echo "0") if [ "${ka_max:-0}" -gt 1 ] 2>/dev/null; then pass "Keep-alives TCP : max $ka_max requêtes sur une même connexion" else log " Keep-alives : max=$ka_max (1 = pas de multiplexage détecté)" fi # Hook de vérification spécifique à la stack (optionnel) if declare -f stack_verify_extra > /dev/null 2>&1; then log "========== Vérifications spécifiques $STACK_NAME ==========" stack_verify_extra fi } # --------------------------------------------------------------------------- # Phase 6 — Vérification exhaustive via verify_db.py # --------------------------------------------------------------------------- phase_verify_db() { log "========== Phase 6 : Vérification exhaustive DB ==========" local wait_flush="${VERIFY_WAIT:-10}" if _dc exec -T traffic-gen python /app/verify_db.py \ --host clickhouse \ --port 8123 \ --db-logs ja4_logs \ --db-processing ja4_processing \ --min-rows 5 \ --wait "$wait_flush" 2>&1; then pass "Vérification DB exhaustive : tous les champs attendus présents" else # Les warnings ne font pas échouer le test — seuls les FAIL comptent warn "Vérification DB : certains champs optionnels absents (voir détail ci-dessus)" fi } # --------------------------------------------------------------------------- # Résumé final # --------------------------------------------------------------------------- phase_summary() { local total=$((TESTS_PASSED + TESTS_FAILED)) echo "" log "========== RÉSULTATS — $STACK_NAME ==========" echo -e " ${GREEN}Réussis : $TESTS_PASSED${NC} / $total" [ "$TESTS_FAILED" -gt 0 ] && echo -e " ${RED}Échoués : $TESTS_FAILED${NC} / $total" echo "" if [ "$TESTS_FAILED" -gt 0 ]; then log "Certains tests ont échoué. Utilisez --no-down pour garder la stack." log "Debug : _dc logs platform | grep -i 'error\|ebpf\|uprobe'" return 1 fi log "Tous les tests ont réussi !" } # --------------------------------------------------------------------------- # Cleanup # --------------------------------------------------------------------------- _cleanup() { if [ "$KEEP_UP" = false ]; then log "Nettoyage de la stack…" _dc down -v --remove-orphans 2>/dev/null || true else log "Stack conservée (--no-down). Arrêt : _dc down -v" fi } # --------------------------------------------------------------------------- # Point d'entrée principal # --------------------------------------------------------------------------- run_all_phases() { trap _cleanup EXIT phase_build phase_start phase_schema phase_traffic phase_verify phase_verify_db phase_summary }