320 lines
14 KiB
Bash
Executable File
320 lines
14 KiB
Bash
Executable File
#!/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 -e PORT=80 mccutchen/go-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 -qO /dev/null 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 <<EOF
|
|
{
|
|
"name": "integration-backend",
|
|
"domain_names": "integration.local",
|
|
"forward_scheme": "http",
|
|
"forward_host": "coraza-backend",
|
|
"forward_port": 80,
|
|
"enabled": true,
|
|
"advanced_config": "{\"handler\":\"waf\",\"ruleset_name\":\"integration-xss\"}"
|
|
}
|
|
EOF
|
|
)
|
|
CREATE_RESP=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" -d "${PROXY_HOST_PAYLOAD}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/proxy-hosts)
|
|
CREATE_STATUS=$(echo "$CREATE_RESP" | tail -n1)
|
|
if [ "$CREATE_STATUS" != "201" ]; then
|
|
echo "Proxy host create failed or already exists; attempting to update existing host..."
|
|
# Find the existing host UUID by searching for the domain in the proxy-hosts list
|
|
EXISTING_UUID=$(curl -s -b ${TMP_COOKIE} http://localhost:8080/api/v1/proxy-hosts | grep -o '{[^}]*"domain_names":"integration.local"[^}]*}' | head -n1 | grep -o '"uuid":"[^"]*"' | sed 's/"uuid":"\([^"]*\)"/\1/')
|
|
if [ -n "$EXISTING_UUID" ]; then
|
|
echo "Updating existing host $EXISTING_UUID with Coraza handler"
|
|
curl -s -X PUT -H "Content-Type: application/json" -d "${PROXY_HOST_PAYLOAD}" -b ${TMP_COOKIE} http://localhost:8080/api/v1/proxy-hosts/$EXISTING_UUID
|
|
else
|
|
echo "Could not find existing host; create response:"
|
|
echo "$CREATE_RESP"
|
|
fi
|
|
fi
|
|
|
|
echo "Give Caddy a moment to apply configuration..."
|
|
sleep 3
|
|
|
|
echo "Creating simple WAF ruleset (XSS block)..."
|
|
RULESET=$(cat <<'EOF'
|
|
{"name":"integration-xss","content":"SecRule REQUEST_BODY \"<script>\" \"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":"127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"}'
|
|
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 "<script>alert(1)</script>" -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":"127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"}'
|
|
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 "<script>alert(1)</script>" -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"
|