#!/usr/bin/env bash set -euo pipefail # ⚠️ DEPRECATED: This script is deprecated and will be removed in v2.0.0 # Please use: .github/skills/scripts/skill-runner.sh integration-test-coraza # For more info: docs/AGENT_SKILLS_MIGRATION.md echo "⚠️ WARNING: This script is deprecated and will be removed in v2.0.0" >&2 echo " Please use: .github/skills/scripts/skill-runner.sh integration-test-coraza" >&2 echo " For more info: docs/AGENT_SKILLS_MIGRATION.md" >&2 echo "" >&2 sleep 1 # Brief: Integration test for Coraza WAF using Docker Compose and built image # Steps: # 1. Build the local image: docker build -t charon:local . # 2. Start docker-compose.local.yml: docker compose -f .docker/compose/docker-compose.local.yml up -d # 3. Wait for API to be ready and then configure a ruleset that blocks a simple signature # 4. Request a path containing the signature and verify 403 (or WAF block response) # Ensure we operate from repo root PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$PROJECT_ROOT" # ============================================================================ # Helper Functions # ============================================================================ # Verifies WAF handler is present in Caddy config with correct ruleset verify_waf_config() { local expected_ruleset="${1:-integration-xss}" local retries=10 local wait=3 echo "Verifying WAF config (expecting ruleset: ${expected_ruleset})..." for i in $(seq 1 $retries); do # Fetch Caddy config via admin API local caddy_config caddy_config=$(curl -s http://localhost:2019/config 2>/dev/null || echo "") if [ -z "$caddy_config" ]; then echo " Attempt $i/$retries: Caddy admin API not responding, retrying..." sleep $wait continue fi # Check for WAF handler if echo "$caddy_config" | grep -q '"handler":"waf"'; then echo " ✓ WAF handler found in Caddy config" # Also verify the directives include our ruleset if echo "$caddy_config" | grep -q "$expected_ruleset"; then echo " ✓ Ruleset '${expected_ruleset}' found in directives" return 0 else echo " ⚠ WAF handler present but ruleset '${expected_ruleset}' not found in directives" fi else echo " Attempt $i/$retries: WAF handler not found, waiting..." fi sleep $wait done echo " ✗ WAF handler verification failed after $retries attempts" return 1 } # Dumps debug information on failure on_failure() { local exit_code=$? echo "" echo "==============================================" echo "=== FAILURE DEBUG INFO (exit code: $exit_code) ===" echo "==============================================" echo "" echo "=== Charon API Logs (last 150 lines) ===" docker logs charon-debug 2>&1 | tail -150 || echo "Could not retrieve container logs" echo "" echo "=== Caddy Admin API Config ===" curl -s http://localhost:2019/config 2>/dev/null | head -300 || echo "Could not retrieve Caddy config" echo "" echo "=== Ruleset Files in Container ===" docker exec charon-debug sh -c 'ls -la /app/data/caddy/coraza/rulesets/ 2>/dev/null' || echo "No rulesets directory found" echo "" echo "=== Ruleset File Contents ===" docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/*.conf 2>/dev/null' || echo "No ruleset files found" echo "" echo "=== Security Config in API ===" curl -s http://localhost:8080/api/v1/security/config 2>/dev/null || echo "Could not retrieve security config" echo "" echo "=== Proxy Hosts ===" curl -s http://localhost:8080/api/v1/proxy-hosts 2>/dev/null | head -50 || echo "Could not retrieve proxy hosts" echo "" echo "==============================================" echo "=== END DEBUG INFO ===" echo "==============================================" } # Set up trap to dump debug info on any error trap on_failure ERR echo "Starting Coraza integration test..." if ! command -v docker >/dev/null 2>&1; then echo "docker is not available; aborting" exit 1 fi # Build the image if it doesn't already exist (CI workflow builds it beforehand) if ! docker image inspect charon:local >/dev/null 2>&1; then echo "Building charon:local image..." docker build -t charon:local . else echo "Using existing charon:local image" fi # Run charon using docker run to ensure we pass CHARON_SECURITY_WAF_MODE and control network membership for integration docker rm -f charon-debug >/dev/null 2>&1 || true if ! docker network inspect containers_default >/dev/null 2>&1; then docker network create containers_default fi # NOTE: We intentionally do NOT mount $(pwd)/backend or $(pwd)/frontend/dist here. # In CI, frontend/dist does not exist (it's built inside the Docker image). # Mounting a non-existent directory would override the built frontend with an empty dir. # For local development with hot-reload, use .docker/compose/docker-compose.local.yml instead. docker run -d --name charon-debug --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --network containers_default -p 80:80 -p 443:443 -p 8080:8080 -p 2019:2019 -p 2345:2345 \ -e CHARON_ENV=development -e CHARON_DEBUG=1 -e CHARON_HTTP_PORT=8080 -e CHARON_DB_PATH=/app/data/charon.db -e CHARON_FRONTEND_DIR=/app/frontend/dist \ -e CHARON_CADDY_ADMIN_API=http://localhost:2019 -e CHARON_CADDY_CONFIG_DIR=/app/data/caddy -e CHARON_CADDY_BINARY=caddy -e CHARON_IMPORT_CADDYFILE=/import/Caddyfile \ -e CHARON_IMPORT_DIR=/app/data/imports -e CHARON_ACME_STAGING=false -e CHARON_SECURITY_WAF_MODE=block \ -v charon_data:/app/data -v caddy_data:/data -v caddy_config:/config -v /var/run/docker.sock:/var/run/docker.sock:ro charon:local echo "Waiting for Charon API to be ready..." for i in {1..30}; do if curl -s -f http://localhost:8080/api/v1/ >/dev/null 2>&1; then break fi echo -n '.' sleep 1 done echo "Skipping unauthenticated ruleset creation (will register and create with cookie later)..." echo "Creating a backend container for proxy host..." # ensure the overlay network exists (docker-compose uses containers_default) CREATED_NETWORK=0 if ! docker network inspect containers_default >/dev/null 2>&1; then docker network create containers_default CREATED_NETWORK=1 fi docker rm -f coraza-backend >/dev/null 2>&1 || true docker run -d --name coraza-backend --network containers_default kennethreitz/httpbin echo "Waiting for httpbin backend to be ready..." for i in {1..20}; do # Check if container is running and has network connectivity if docker exec charon-debug sh -c 'wget -q -O- http://coraza-backend/get 2>/dev/null || curl -s http://coraza-backend/get' >/dev/null 2>&1; then echo "✓ httpbin backend is ready" break fi if [ $i -eq 20 ]; then echo "✗ httpbin backend failed to start" echo "Container status:" docker ps -a --filter name=coraza-backend echo "Container logs:" docker logs coraza-backend 2>&1 | tail -20 exit 1 fi echo -n '.' sleep 1 done echo "Registering admin user and logging in to retrieve session cookie..." TMP_COOKIE=$(mktemp) curl -s -X POST -H "Content-Type: application/json" -d '{"email":"integration@example.local","password":"password123","name":"Integration Tester"}' http://localhost:8080/api/v1/auth/register >/dev/null || true curl -s -X POST -H "Content-Type: application/json" -d '{"email":"integration@example.local","password":"password123"}' -c ${TMP_COOKIE} http://localhost:8080/api/v1/auth/login >/dev/null echo "Creating proxy host 'integration.local' pointing to backend..." PROXY_HOST_PAYLOAD=$(cat <\" \"id:12345,phase:2,deny,status:403,msg:'XSS blocked'\""} EOF ) curl -s -X POST -H "Content-Type: application/json" -d "${RULESET}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/security/rulesets echo "Enable WAF globally and set ruleset source to integration-xss..." SEC_CFG_PAYLOAD='{"name":"default","enabled":true,"waf_mode":"block","waf_rules_source":"integration-xss","admin_whitelist":"0.0.0.0/0"}' curl -s -X POST -H "Content-Type: application/json" -d "${SEC_CFG_PAYLOAD}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/security/config echo "Waiting for Caddy to apply WAF configuration..." sleep 10 # Verify WAF handler is properly configured before proceeding # Note: This is advisory - if admin API is restarting we'll proceed anyway if ! verify_waf_config "integration-xss"; then echo "WARNING: WAF configuration verification failed (admin API may be restarting)" echo "Proceeding with test anyway..." fi echo "Apply rules and test payload..." # create minimal proxy host if needed; omitted here for brevity; test will target local Caddy root echo "Verifying Caddy config has WAF handler..." curl -s http://localhost:2019/config | grep -E '"handler":"waf"' || echo "WARNING: WAF handler not found in initial config check" echo "Inspecting ruleset file inside container..." docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/integration-xss-*.conf' || echo "WARNING: Could not read ruleset file" echo "" echo "=== Testing BLOCK mode ===" MAX_RETRIES=3 BLOCK_SUCCESS=0 for attempt in $(seq 1 $MAX_RETRIES); do RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" -d "" -H "Host: integration.local" http://localhost/post) if [ "$RESPONSE" = "403" ]; then echo "✓ Coraza WAF blocked payload as expected (HTTP 403) in BLOCK mode" BLOCK_SUCCESS=1 break fi if [ $attempt -eq $MAX_RETRIES ]; then echo "✗ Unexpected response code: $RESPONSE (expected 403) in BLOCK mode after $MAX_RETRIES attempts" exit 1 fi echo " Attempt $attempt: Got $RESPONSE, retrying in 2s..." sleep 2 done echo "" echo "=== Testing MONITOR mode (DetectionOnly) ===" echo "Switching WAF to monitor mode..." SEC_CFG_MONITOR='{"name":"default","enabled":true,"waf_mode":"monitor","waf_rules_source":"integration-xss","admin_whitelist":"0.0.0.0/0"}' curl -s -X POST -H "Content-Type: application/json" -d "${SEC_CFG_MONITOR}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/security/config echo "Wait for Caddy to apply monitor mode config..." sleep 12 # Verify WAF handler is still present after mode switch # Note: This is advisory - if admin API is restarting we'll proceed anyway if ! verify_waf_config "integration-xss"; then echo "WARNING: WAF config verification failed after mode switch (admin API may be restarting)" echo "Proceeding with test anyway..." fi echo "Inspecting ruleset file (should now have DetectionOnly)..." docker exec charon-debug sh -c 'cat /app/data/caddy/coraza/rulesets/integration-xss-*.conf | head -5' || true MONITOR_SUCCESS=0 for attempt in $(seq 1 $MAX_RETRIES); do RESPONSE_MONITOR=$(curl -s -o /dev/null -w "%{http_code}" -d "" -H "Host: integration.local" http://localhost/post) if [ "$RESPONSE_MONITOR" = "200" ]; then echo "✓ Coraza WAF in MONITOR mode allowed payload through (HTTP 200) as expected" MONITOR_SUCCESS=1 break fi if [ $attempt -eq $MAX_RETRIES ]; then echo "✗ Unexpected response code: $RESPONSE_MONITOR (expected 200) in MONITOR mode after $MAX_RETRIES attempts" echo " Note: Monitor mode should log but not block" exit 1 fi echo " Attempt $attempt: Got $RESPONSE_MONITOR, retrying in 2s..." sleep 2 done echo "" echo "=== All Coraza integration tests passed ===" echo "Cleaning up..." # Delete the integration test proxy host from DB before stopping container echo "Removing integration test proxy host from database..." INTEGRATION_UUID=$(curl -s -b ${TMP_COOKIE} http://localhost:8080/api/v1/proxy-hosts | grep -o '"uuid":"[^"]*"[^}]*"domain_names":"integration.local"' | head -n1 | grep -o '"uuid":"[^"]*"' | sed 's/"uuid":"\([^"]*\)"/\1/') if [ -n "$INTEGRATION_UUID" ]; then curl -s -X DELETE -b ${TMP_COOKIE} "http://localhost:8080/api/v1/proxy-hosts/${INTEGRATION_UUID}?delete_uptime=true" >/dev/null echo "✓ Deleted integration proxy host ${INTEGRATION_UUID}" fi docker rm -f coraza-backend || true if [ "$CREATED_NETWORK" -eq 1 ]; then docker network rm containers_default || true fi docker rm -f charon-debug || true rm -f ${TMP_COOKIE} echo "Done"