- Use two separate //go:generate directives (Ja4Tc for tc_capture.c, Ja4Ssl
for uprobe_ssl.c) to avoid duplicate LICENSE symbol and multi-file clang issue
- Update loader.go to hold tcObjs/sslObjs separately with correct field names:
UprobeSslSetFd, UprobeSslReadEntry, UretprobeSslReadExit,
KprobeAccept4Entry, KretprobeAccept4Exit
- Add systemd-rpm-macros to all three RPM build stages (el8/el9/el10)
so that %{_unitdir} macro resolves correctly
- RPMs now build successfully for el8, el9, el10
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
271 lines
11 KiB
Bash
271 lines
11 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
|
|
[ "$BUILD_ONLY" = true ] && { log "Build terminé (--build-only)."; exit 0; }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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")
|
|
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)
|
|
local ja4_count ja4_uniq
|
|
ja4_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs_raw WHERE ja4 != ''")
|
|
ja4_uniq=$(ch_query "SELECT count(DISTINCT ja4) FROM ja4_logs.http_logs_raw WHERE ja4 != ''")
|
|
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_raw WHERE ja4 != '' LIMIT 1")
|
|
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)
|
|
local l34_count
|
|
l34_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs_raw WHERE ttl > 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 ttl, mss, window_size FROM ja4_logs.http_logs_raw WHERE ttl > 0 LIMIT 1 FORMAT TabSeparated")
|
|
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_raw WHERE method != ''")
|
|
methods=$(ch_query "SELECT groupArray(method) FROM (SELECT DISTINCT method FROM ja4_logs.http_logs_raw WHERE method != '' ORDER BY method)")
|
|
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. HTTP/2 SETTINGS capturés (uprobe + parsing preface H2)
|
|
local h2_count
|
|
h2_count=$(ch_query "SELECT count() FROM ja4_logs.http_logs_raw WHERE h2_settings != ''")
|
|
if [ "${h2_count:-0}" -gt 0 ] 2>/dev/null; then
|
|
pass "HTTP/2 SETTINGS : $h2_count connexions H2 avec preface capturée"
|
|
local h2_sample
|
|
h2_sample=$(ch_query "SELECT h2_settings FROM ja4_logs.http_logs_raw WHERE h2_settings != '' LIMIT 1")
|
|
log " Exemple H2 SETTINGS : $h2_sample"
|
|
else
|
|
warn "Pas de SETTINGS HTTP/2 (trafic h2 absent ou ALPN négociation échouée)"
|
|
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_raw WHERE method != ''")
|
|
corr_yes=$(ch_query "SELECT count() FROM ja4_logs.http_logs_raw WHERE correlated = true AND method != ''")
|
|
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(maxkeepalives) FROM ja4_logs.http_logs_raw")
|
|
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
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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_summary
|
|
}
|