Files
Charon/scripts/cerberus_integration.sh

662 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Brief: Full integration test for Cerberus security stack
# Tests all security features working together:
# - WAF (Coraza) for payload inspection
# - Rate Limiting for volume abuse prevention
# - Security handler ordering in Caddy config
#
# Test Cases:
# - TC-1: Verify all features enabled via /api/v1/security/status
# - TC-2: Verify handler order in Caddy config
# - TC-3: WAF blocking doesn't consume rate limit quota
# - TC-4: Legitimate traffic flows through all layers
# - TC-5: Basic latency check
# Ensure we operate from repo root
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_ROOT"
# ============================================================================
# Configuration
# ============================================================================
CONTAINER_NAME="charon-cerberus-test"
BACKEND_CONTAINER="cerberus-backend"
TEST_DOMAIN="cerberus.test.local"
# Use unique non-conflicting ports
API_PORT=8480
HTTP_PORT=8481
HTTPS_PORT=8444
CADDY_ADMIN_PORT=2319
# Rate limit config for testing
RATE_LIMIT_REQUESTS=5
RATE_LIMIT_WINDOW_SEC=30
# ============================================================================
# Colors for output
# ============================================================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; }
# ============================================================================
# Test counters
# ============================================================================
PASSED=0
FAILED=0
pass_test() {
PASSED=$((PASSED + 1))
echo -e " ${GREEN}✓ PASS${NC}"
}
fail_test() {
FAILED=$((FAILED + 1))
echo -e " ${RED}✗ FAIL${NC}: $1"
}
# Assert HTTP status code
assert_http() {
local expected=$1
local actual=$2
local desc=$3
if [ "$actual" = "$expected" ]; then
log_info "$desc: HTTP $actual"
PASSED=$((PASSED + 1))
else
log_error "$desc: HTTP $actual (expected $expected)"
FAILED=$((FAILED + 1))
fi
}
# ============================================================================
# Helper Functions
# ============================================================================
# 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 ${CONTAINER_NAME} 2>&1 | tail -150 || echo "Could not retrieve container logs"
echo ""
echo "=== Caddy Admin API Config ==="
curl -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null | head -300 || echo "Could not retrieve Caddy config"
echo ""
echo "=== Security Config in API ==="
curl -s -b "${TMP_COOKIE:-/dev/null}" "http://localhost:${API_PORT}/api/v1/security/config" 2>/dev/null || echo "Could not retrieve security config"
echo ""
echo "=== Security Status ==="
curl -s -b "${TMP_COOKIE:-/dev/null}" "http://localhost:${API_PORT}/api/v1/security/status" 2>/dev/null || echo "Could not retrieve security status"
echo ""
echo "=== Security Rulesets ==="
curl -s -b "${TMP_COOKIE:-/dev/null}" "http://localhost:${API_PORT}/api/v1/security/rulesets" 2>/dev/null || echo "Could not retrieve rulesets"
echo ""
echo "=============================================="
echo "=== END DEBUG INFO ==="
echo "=============================================="
}
# Cleanup function
cleanup() {
log_info "Cleaning up test resources..."
docker rm -f ${CONTAINER_NAME} 2>/dev/null || true
docker rm -f ${BACKEND_CONTAINER} 2>/dev/null || true
rm -f "${TMP_COOKIE:-}" 2>/dev/null || true
log_info "Cleanup complete"
}
# Set up trap to dump debug info on any error and always cleanup
trap on_failure ERR
trap cleanup EXIT
echo "=============================================="
echo "=== Cerberus Full Integration Test Starting ==="
echo "=============================================="
echo ""
# Check dependencies
if ! command -v docker >/dev/null 2>&1; then
log_error "docker is not available; aborting"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
log_error "curl is not available; aborting"
exit 1
fi
# ============================================================================
# Step 1: Build image if needed
# ============================================================================
if ! docker image inspect charon:local >/dev/null 2>&1; then
log_info "Building charon:local image..."
docker build -t charon:local .
else
log_info "Using existing charon:local image"
fi
# ============================================================================
# Step 2: Start containers
# ============================================================================
log_info "Stopping any existing test containers..."
docker rm -f ${CONTAINER_NAME} 2>/dev/null || true
docker rm -f ${BACKEND_CONTAINER} 2>/dev/null || true
# Ensure network exists
if ! docker network inspect containers_default >/dev/null 2>&1; then
log_info "Creating containers_default network..."
docker network create containers_default
fi
log_info "Starting httpbin backend container..."
docker pull mccutchen/go-httpbin 2>/dev/null || true
docker run -d --name ${BACKEND_CONTAINER} --network containers_default -e PORT=80 mccutchen/go-httpbin
log_info "Starting Charon container with ALL Cerberus features enabled..."
docker run -d --name ${CONTAINER_NAME} \
--cap-add=SYS_PTRACE --security-opt seccomp=unconfined \
--network containers_default \
-p ${HTTP_PORT}:80 -p ${HTTPS_PORT}:443 -p ${API_PORT}:8080 -p ${CADDY_ADMIN_PORT}:2019 \
-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 CERBERUS_SECURITY_CERBERUS_ENABLED=true \
-e CHARON_SECURITY_WAF_MODE=block \
-e CERBERUS_SECURITY_RATELIMIT_MODE=enabled \
-e CERBERUS_SECURITY_ACL_ENABLED=true \
-v charon_cerberus_test_data:/app/data \
-v caddy_cerberus_test_data:/data \
-v caddy_cerberus_test_config:/config \
charon:local
log_info "Waiting for Charon API to be ready..."
for i in {1..30}; do
if curl -s -f "http://localhost:${API_PORT}/api/v1/health" >/dev/null 2>&1; then
log_info "Charon API is ready"
break
fi
if [ $i -eq 30 ]; then
log_error "Charon API failed to start"
exit 1
fi
echo -n '.'
sleep 1
done
echo ""
log_info "Waiting for httpbin backend to be ready..."
for i in {1..45}; do
if docker exec ${CONTAINER_NAME} sh -c "wget -qO /dev/null http://${BACKEND_CONTAINER}/get" >/dev/null 2>&1; then
log_info "httpbin backend is ready"
break
fi
if [ $i -eq 45 ]; then
log_error "httpbin backend failed to start"
exit 1
fi
echo -n '.'
sleep 1
done
echo ""
# ============================================================================
# Step 3: Register user and authenticate
# ============================================================================
log_info "Registering admin user and logging in..."
TMP_COOKIE=$(mktemp)
curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"cerberus-test@example.local","password":"password123","name":"Cerberus Tester"}' \
"http://localhost:${API_PORT}/api/v1/auth/register" >/dev/null 2>&1 || true
curl -s -X POST -H "Content-Type: application/json" \
-d '{"email":"cerberus-test@example.local","password":"password123"}' \
-c "${TMP_COOKIE}" \
"http://localhost:${API_PORT}/api/v1/auth/login" >/dev/null
log_info "Authentication complete"
# ============================================================================
# Step 4: Create proxy host
# ============================================================================
log_info "Creating proxy host '${TEST_DOMAIN}' pointing to backend..."
PROXY_HOST_PAYLOAD=$(cat <<EOF
{
"name": "cerberus-test-backend",
"domain_names": "${TEST_DOMAIN}",
"forward_scheme": "http",
"forward_host": "${BACKEND_CONTAINER}",
"forward_port": 80,
"enabled": true
}
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:${API_PORT}/api/v1/proxy-hosts")
CREATE_STATUS=$(echo "$CREATE_RESP" | tail -n1)
if [ "$CREATE_STATUS" = "201" ]; then
log_info "Proxy host created successfully"
else
log_info "Proxy host may already exist (status: $CREATE_STATUS)"
fi
# Wait for Caddy to apply config
sleep 3
# ============================================================================
# Step 5: Create WAF ruleset (XSS protection)
# ============================================================================
log_info "Creating XSS WAF ruleset..."
XSS_RULESET=$(cat <<'EOF'
{
"name": "cerberus-xss",
"content": "SecRule REQUEST_BODY|ARGS|ARGS_NAMES \"<script\" \"id:99001,phase:2,deny,status:403,msg:'XSS Attack Detected'\""
}
EOF
)
XSS_RESP=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" \
-d "${XSS_RULESET}" \
-b "${TMP_COOKIE}" \
"http://localhost:${API_PORT}/api/v1/security/rulesets")
XSS_STATUS=$(echo "$XSS_RESP" | tail -n1)
if [ "$XSS_STATUS" = "200" ] || [ "$XSS_STATUS" = "201" ]; then
log_info "XSS ruleset created"
else
log_warn "XSS ruleset creation returned status: $XSS_STATUS"
fi
# ============================================================================
# Step 6: Enable WAF in block mode + configure rate limiting
# ============================================================================
log_info "Enabling WAF (block mode) and rate limiting (${RATE_LIMIT_REQUESTS} req / ${RATE_LIMIT_WINDOW_SEC} sec)..."
SECURITY_CONFIG=$(cat <<EOF
{
"name": "default",
"enabled": true,
"waf_mode": "block",
"waf_rules_source": "cerberus-xss",
"rate_limit_enable": true,
"rate_limit_requests": ${RATE_LIMIT_REQUESTS},
"rate_limit_window_sec": ${RATE_LIMIT_WINDOW_SEC},
"rate_limit_burst": 1,
"admin_whitelist": "127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"
}
EOF
)
SEC_RESP=$(curl -s -w "\n%{http_code}" -X POST -H "Content-Type: application/json" \
-d "${SECURITY_CONFIG}" \
-b "${TMP_COOKIE}" \
"http://localhost:${API_PORT}/api/v1/security/config")
SEC_STATUS=$(echo "$SEC_RESP" | tail -n1)
if [ "$SEC_STATUS" = "200" ]; then
log_info "Security configuration applied"
else
log_warn "Security config returned status: $SEC_STATUS"
fi
# Wait for Caddy to reload with all security features
log_info "Waiting for Caddy to apply security configuration..."
sleep 5
echo ""
echo "=============================================="
echo "=== Running Cerberus Integration Test Cases ==="
echo "=============================================="
echo ""
# ============================================================================
# TC-1: Verify all features enabled via /api/v1/security/status
# ============================================================================
log_test "TC-1: Verify All Features Enabled"
STATUS_RESP=$(curl -s -b "${TMP_COOKIE}" "http://localhost:${API_PORT}/api/v1/security/status")
# Check Cerberus enabled (nested: "cerberus":{"enabled":true})
if echo "$STATUS_RESP" | grep -qE '"cerberus":\s*\{[^}]*"enabled":\s*true'; then
log_info " ✓ Cerberus enabled"
PASSED=$((PASSED + 1))
else
fail_test "Cerberus not enabled in status response"
fi
# Check WAF mode (nested: "waf":{"mode":"block",...})
if echo "$STATUS_RESP" | grep -qE '"waf":\s*\{[^}]*"mode":\s*"block"'; then
log_info " ✓ WAF mode is 'block'"
PASSED=$((PASSED + 1))
else
fail_test "WAF mode not set to 'block'"
fi
# Check rate limit enabled (nested: "rate_limit":{"enabled":true,...})
if echo "$STATUS_RESP" | grep -qE '"rate_limit":\s*\{[^}]*"enabled":\s*true'; then
log_info " ✓ Rate limit enabled"
PASSED=$((PASSED + 1))
else
fail_test "Rate limit not enabled"
fi
# ============================================================================
# TC-2: Verify handler order in Caddy config (ROUTE-AWARE)
# ============================================================================
log_test "TC-2: Verify Handler Order in Caddy Config"
# HARD REQUIREMENT: Check if jq is available (no fallback mode)
if ! command -v jq >/dev/null 2>&1; then
fail_test "jq is required for handler order verification. Install: apt-get install jq / brew install jq"
return 1
fi
# Fetch Caddy config with timeout and retry
CADDY_CONFIG=""
for attempt in 1 2 3; do
CADDY_CONFIG=$(curl --max-time 10 -sL "http://localhost:${CADDY_ADMIN_PORT}/config/" 2>/dev/null || echo "")
if [ -n "$CADDY_CONFIG" ]; then
break
fi
log_warn " Attempt $attempt/3: Failed to fetch Caddy config, retrying..."
sleep 2
done
if [ -z "$CADDY_CONFIG" ]; then
fail_test "Could not retrieve Caddy config after 3 attempts"
return 1
fi
# Validate JSON structure before processing
if ! echo "$CADDY_CONFIG" | jq empty 2>/dev/null; then
fail_test "Retrieved Caddy config is not valid JSON"
return 1
fi
# Validate expected structure exists
if ! echo "$CADDY_CONFIG" | jq -e '.apps.http.servers.charon_server.routes' >/dev/null 2>&1; then
fail_test "Caddy config missing expected route structure (.apps.http.servers.charon_server.routes)"
return 1
fi
# Get route count with validation
TOTAL_ROUTES=$(echo "$CADDY_CONFIG" | jq -r '.apps.http.servers.charon_server.routes | length' 2>/dev/null || echo "0")
# Validate route count is numeric and non-negative
if ! [[ "$TOTAL_ROUTES" =~ ^[0-9]+$ ]]; then
fail_test "Invalid route count (not numeric): $TOTAL_ROUTES"
return 1
fi
log_info " Found $TOTAL_ROUTES routes in Caddy config"
if [ "$TOTAL_ROUTES" -eq 0 ]; then
fail_test "No routes found in Caddy config"
return 1
fi
# Define EXACT emergency paths (must match config.go lines 687-691)
readonly EMERGENCY_PATHS=(
"/api/v1/emergency/security-reset"
"/api/v1/emergency/*"
"/emergency/security-reset"
"/emergency/*"
)
ROUTES_VERIFIED=0
EMERGENCY_ROUTES_SKIPPED=0
# Use bash arithmetic loop instead of seq
for ((i=0; i<TOTAL_ROUTES; i++)); do
# Validate route exists at index
if ! echo "$CADDY_CONFIG" | jq -e ".apps.http.servers.charon_server.routes[$i]" >/dev/null 2>&1; then
log_warn " Route $i: Missing or invalid route structure, skipping"
continue
fi
# Check if this route has EXACT emergency path matches
IS_EMERGENCY_ROUTE=false
for emergency_path in "${EMERGENCY_PATHS[@]}"; do
# EXACT path comparison (not substring matching)
EXACT_MATCH=$(echo "$CADDY_CONFIG" | jq -r "
.apps.http.servers.charon_server.routes[$i].match[]? |
select(.path != null) |
.path[]? |
select(. == \"$emergency_path\")" 2>/dev/null | wc -l | tr -d ' ')
# Validate match count is numeric
if ! [[ "$EXACT_MATCH" =~ ^[0-9]+$ ]]; then
log_warn " Route $i: Invalid match count for path '$emergency_path', skipping"
continue
fi
if [ "$EXACT_MATCH" -gt 0 ]; then
IS_EMERGENCY_ROUTE=true
break
fi
done
if [ "$IS_EMERGENCY_ROUTE" = true ]; then
log_info " Route $i: Emergency route (security bypass by design) - skipping"
EMERGENCY_ROUTES_SKIPPED=$((EMERGENCY_ROUTES_SKIPPED + 1))
continue
fi
# Main route - verify handler order
log_info " Route $i: Main route - verifying handler order..."
# Validate handlers array exists
if ! echo "$CADDY_CONFIG" | jq -e ".apps.http.servers.charon_server.routes[$i].handle" >/dev/null 2>&1; then
log_warn " Route $i: No handlers found, skipping"
continue
fi
# Find indices of security handlers and reverse_proxy
WAF_IDX=$(echo "$CADDY_CONFIG" | jq -r "[.apps.http.servers.charon_server.routes[$i].handle[]?.handler] | map(if . == \"waf\" then true else false end) | index(true) // -1" 2>/dev/null || echo "-1")
RATE_IDX=$(echo "$CADDY_CONFIG" | jq -r "[.apps.http.servers.charon_server.routes[$i].handle[]?.handler] | map(if . == \"rate_limit\" then true else false end) | index(true) // -1" 2>/dev/null || echo "-1")
PROXY_IDX=$(echo "$CADDY_CONFIG" | jq -r "[.apps.http.servers.charon_server.routes[$i].handle[]?.handler] | map(if . == \"reverse_proxy\" then true else false end) | index(true) // -1" 2>/dev/null || echo "-1")
# Validate all indices are numeric
if ! [[ "$WAF_IDX" =~ ^-?[0-9]+$ ]] || ! [[ "$RATE_IDX" =~ ^-?[0-9]+$ ]] || ! [[ "$PROXY_IDX" =~ ^-?[0-9]+$ ]]; then
fail_test "Invalid handler indices in route $i (not numeric)"
return 1
fi
# Verify WAF comes before reverse_proxy (if present)
if [ "$WAF_IDX" -ge 0 ] && [ "$PROXY_IDX" -ge 0 ]; then
if [ "$WAF_IDX" -lt "$PROXY_IDX" ]; then
log_info " ✓ WAF (index $WAF_IDX) before reverse_proxy (index $PROXY_IDX)"
else
fail_test "WAF must appear before reverse_proxy in route $i (WAF=$WAF_IDX, proxy=$PROXY_IDX)"
return 1
fi
fi
# Verify rate_limit comes before reverse_proxy (if present)
if [ "$RATE_IDX" -ge 0 ] && [ "$PROXY_IDX" -ge 0 ]; then
if [ "$RATE_IDX" -lt "$PROXY_IDX" ]; then
log_info " ✓ rate_limit (index $RATE_IDX) before reverse_proxy (index $PROXY_IDX)"
else
fail_test "rate_limit must appear before reverse_proxy in route $i (rate=$RATE_IDX, proxy=$PROXY_IDX)"
return 1
fi
fi
ROUTES_VERIFIED=$((ROUTES_VERIFIED + 1))
done
log_info " Summary: Verified $ROUTES_VERIFIED main routes, skipped $EMERGENCY_ROUTES_SKIPPED emergency routes"
if [ "$ROUTES_VERIFIED" -gt 0 ]; then
log_info " ✓ Handler order correct in all main routes"
PASSED=$((PASSED + 1))
else
log_warn " No main routes found to verify (all routes are emergency routes?)"
# Don't fail if only emergency routes exist - this may be valid in some configs
PASSED=$((PASSED + 1))
fi
# ============================================================================
# TC-3: WAF blocking doesn't consume rate limit quota
# ============================================================================
log_test "TC-3: WAF Blocking Doesn't Consume Rate Limit"
log_info " Sending 3 malicious requests (should be blocked by WAF with 403)..."
WAF_BLOCKED=0
for i in 1 2 3; do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Host: ${TEST_DOMAIN}" \
"http://localhost:${HTTP_PORT}/get?q=%3Cscript%3Ealert(1)%3C/script%3E")
if [ "$CODE" = "403" ]; then
WAF_BLOCKED=$((WAF_BLOCKED + 1))
log_info " Malicious request $i: HTTP $CODE (WAF blocked) ✓"
else
log_warn " Malicious request $i: HTTP $CODE (expected 403)"
fi
done
if [ $WAF_BLOCKED -eq 3 ]; then
log_info " ✓ All 3 malicious requests blocked by WAF"
PASSED=$((PASSED + 1))
else
fail_test "Not all malicious requests were blocked by WAF ($WAF_BLOCKED/3)"
fi
log_info " Sending ${RATE_LIMIT_REQUESTS} legitimate requests (should all succeed with 200)..."
LEGIT_SUCCESS=0
for i in $(seq 1 ${RATE_LIMIT_REQUESTS}); do
CODE=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Host: ${TEST_DOMAIN}" \
"http://localhost:${HTTP_PORT}/get?name=john&id=$i")
if [ "$CODE" = "200" ]; then
LEGIT_SUCCESS=$((LEGIT_SUCCESS + 1))
log_info " Legitimate request $i: HTTP $CODE"
else
log_warn " Legitimate request $i: HTTP $CODE (expected 200)"
fi
sleep 0.1
done
if [ $LEGIT_SUCCESS -eq ${RATE_LIMIT_REQUESTS} ]; then
log_info " ✓ All ${RATE_LIMIT_REQUESTS} legitimate requests succeeded"
PASSED=$((PASSED + 1))
else
fail_test "Not all legitimate requests succeeded ($LEGIT_SUCCESS/${RATE_LIMIT_REQUESTS})"
fi
# ============================================================================
# TC-4: Legitimate traffic flows through all layers
# ============================================================================
log_test "TC-4: Legitimate Traffic Flows Through All Layers"
# Wait for rate limit window to reset
log_info " Waiting for rate limit window to reset (${RATE_LIMIT_WINDOW_SEC} seconds + buffer)..."
sleep $((RATE_LIMIT_WINDOW_SEC + 2))
log_info " Sending 10 legitimate requests..."
FLOW_SUCCESS=0
for i in $(seq 1 10); do
BODY=$(curl -s -H "Host: ${TEST_DOMAIN}" "http://localhost:${HTTP_PORT}/get?test=$i")
if echo "$BODY" | grep -q "args\|headers\|origin\|url"; then
FLOW_SUCCESS=$((FLOW_SUCCESS + 1))
echo " Request $i: ✓ Success (reached upstream)"
else
echo " Request $i: ✗ Failed (response: ${BODY:0:100}...)"
fi
# Space out requests to avoid hitting rate limit
sleep 0.5
done
log_info " Total successful: $FLOW_SUCCESS/10"
if [ $FLOW_SUCCESS -ge 5 ]; then
log_info " ✓ Legitimate traffic flowing through all layers"
PASSED=$((PASSED + 1))
else
fail_test "Too many legitimate requests failed ($FLOW_SUCCESS/10)"
fi
# ============================================================================
# TC-5: Basic latency check
# ============================================================================
log_test "TC-5: Basic Latency Check"
# Wait for rate limit window to reset again
log_info " Waiting for rate limit window to reset..."
sleep $((RATE_LIMIT_WINDOW_SEC + 2))
# Measure latency for a single request
LATENCY=$(curl -s -o /dev/null -w "%{time_total}" \
-H "Host: ${TEST_DOMAIN}" \
"http://localhost:${HTTP_PORT}/get")
log_info " Single request latency: ${LATENCY}s"
# Convert to milliseconds for comparison (using awk since bc may not be available)
LATENCY_MS=$(echo "$LATENCY" | awk '{printf "%.0f", $1 * 1000}')
if [ "$LATENCY_MS" -lt 5000 ]; then
log_info " ✓ Latency ${LATENCY_MS}ms is within acceptable range (<5000ms)"
PASSED=$((PASSED + 1))
else
fail_test "Latency ${LATENCY_MS}ms exceeds threshold"
fi
# ============================================================================
# Results Summary
# ============================================================================
echo ""
echo "=============================================="
echo "=== Cerberus Full Integration Test Results ==="
echo "=============================================="
echo ""
echo -e " ${GREEN}Passed:${NC} $PASSED"
echo -e " ${RED}Failed:${NC} $FAILED"
echo ""
if [ $FAILED -eq 0 ]; then
echo "=============================================="
echo "=== ALL CERBERUS INTEGRATION TESTS PASSED ==="
echo "=============================================="
echo ""
exit 0
else
echo "=============================================="
echo "=== CERBERUS TESTS FAILED ==="
echo "=============================================="
echo ""
exit 1
fi