Files
ja4-platform/tests/integration/lib/run-stack-tests.sh
toto f85a10b012 feat: pipeline L7 HTTP complet + infrastructure tests VM
Correctifs pipeline L7 (uprobe SSL_read) :
- uprobe_ssl.c : ssl_set_fd ne retourne plus tôt quand fd_conn_map est
  vide (accept4 non disponible en Docker). Sauvegarde ssl_ptr→{fd,0,0}
  pour permettre le fallback /proc côté Go.
- main.go : consumeSSLEvents reécrit avec routeur magic-bytes complet :
  * HTTP/2 preface → extraction SETTINGS + conversion correlation.HTTP2Settings
  * HTTP/1.x requête → method, path, query, headers, header_order_sig
  * HTTP/1.x réponse → status_code
  * Fallback /proc/<tgid>/fd/<fd> quand src_ip=0 (accept4 absent)
- writer/clickhouse.go : export header_order_signature ajouté

Nouveaux packages :
- internal/parser/http1.go : parseur HTTP/1.x (IsHTTP1Request,
  ParseHTTP1Request, IsHTTP1Response, ParseHTTP1Response)
- internal/parser/http1_test.go : 11 tests unitaires (28 total passent)
- internal/procutil/proc_lookup.go : résolution fd→IP via /proc avec cache
  TTL 5s (FDCache). Supporte /proc/PID/net/tcp et tcp6, IPv4-mappé IPv6.

Infrastructure tests VM (tests/vm/) :
- Vagrantfile : VM Rocky Linux 9 KVM, 4 CPU / 4 GB RAM
- provision.sh : installation toolchain eBPF + Go + Docker + nginx
- run-tests-vm.sh : suite de test complète dans la VM (L3/L4+TLS+L7)
- README.md : guide d'installation et d'utilisation
- Makefile : cibles vm-up, vm-down, vm-ssh, test-vm-nginx, test-vm-all,
  vm-rebuild-ja4ebpf

Corrections stack Docker :
- Dockerfiles nginx/apache/nginx-varnish/hitch-varnish : suppression des
  références à shared/go/ja4common/ (répertoire supprimé)
- clickhouse-init.sh : restauré depuis git, seed anubis_ua_rules obsolète
  supprimé (table REGEXP_TREE supprimée du schéma)
- traffic-gen : ajout HTTP/1.0 (http.client) et HTTP/2 (httpx)
- verify_db.py : script de vérification 35 checks (L3/L4/TLS/L7/corrélation)
- run-stack-tests.sh : phase 6 verify_db ajoutée

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-12 02:37:00 +02:00

297 lines
13 KiB
Bash

#!/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
}