#!/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-crowdsec-decisions # 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-crowdsec-decisions" >&2 echo " For more info: docs/AGENT_SKILLS_MIGRATION.md" >&2 echo "" >&2 sleep 1 # Brief: Integration test for CrowdSec Decision Management # Steps: # 1. Build the local image if not present: docker build -t charon:local . # 2. Start Charon container with CrowdSec/Cerberus features enabled # 3. Test CrowdSec status endpoint # 4. Test decisions list (expect empty initially) # 5. Test ban IP operation # 6. Verify ban appears in decisions list # 7. Test unban IP operation # 8. Verify IP removed from decisions # 9. Test export endpoint # 10. Test LAPI health endpoint # 11. Clean up test resources # # Note: CrowdSec binary may not be available in test container # Tests gracefully handle this scenario and skip operations requiring cscli # Ensure we operate from repo root PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$PROJECT_ROOT" # ============================================================================ # Configuration # ============================================================================ CONTAINER_NAME="charon-crowdsec-decision-test" TEST_IP="192.168.100.100" TEST_DURATION="1h" TEST_REASON="Integration test ban" # Use same non-conflicting ports as rate_limit_integration.sh API_PORT=8280 HTTP_PORT=8180 HTTPS_PORT=8143 CADDY_ADMIN_PORT=2119 # ============================================================================ # 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 SKIPPED=0 pass_test() { PASSED=$((PASSED + 1)) echo -e " ${GREEN}✓ PASS${NC}" } fail_test() { FAILED=$((FAILED + 1)) echo -e " ${RED}✗ FAIL${NC}: $1" } skip_test() { SKIPPED=$((SKIPPED + 1)) echo -e " ${YELLOW}⊘ SKIP${NC}: $1" } # ============================================================================ # 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 100 lines) ===" docker logs ${CONTAINER_NAME} 2>&1 | tail -100 || echo "Could not retrieve container logs" echo "" echo "=== CrowdSec Status ===" curl -s -b "${TMP_COOKIE:-/dev/null}" "http://localhost:${API_PORT}/api/v1/admin/crowdsec/status" 2>/dev/null || echo "Could not retrieve CrowdSec status" 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 rm -f "${TMP_COOKIE:-}" 2>/dev/null || true log_info "Cleanup complete" } # Set up trap to dump debug info on any error trap on_failure ERR echo "==============================================" echo "=== CrowdSec Decision Integration Test ===" 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 if ! command -v jq >/dev/null 2>&1; then log_error "jq 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 Charon container # ============================================================================ log_info "Stopping any existing test containers..." docker rm -f ${CONTAINER_NAME} 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 Charon container with CrowdSec 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 FEATURE_CERBERUS_ENABLED=true \ -e CERBERUS_SECURITY_CROWDSEC_MODE=local \ -v charon_crowdsec_test_data:/app/data \ -v caddy_crowdsec_test_data:/data \ -v caddy_crowdsec_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/" >/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 "" # ============================================================================ # 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":"crowdsec@example.local","password":"password123","name":"CrowdSec 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":"crowdsec@example.local","password":"password123"}' \ -c "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/auth/login" >/dev/null log_info "Authentication complete" echo "" # ============================================================================ # Pre-flight CrowdSec Startup Checks (TC-0 series) # ============================================================================ echo "==============================================" echo "=== Pre-flight CrowdSec Startup Checks ===" echo "==============================================" echo "" # ---------------------------------------------------------------------------- # TC-0: Verify CrowdSec agent started successfully # ---------------------------------------------------------------------------- log_test "TC-0: Verify CrowdSec agent started successfully" CROWDSEC_READY=$(docker logs ${CONTAINER_NAME} 2>&1 | grep -c "CrowdSec LAPI is ready" || echo "0") CROWDSEC_FATAL=$(docker logs ${CONTAINER_NAME} 2>&1 | grep -c "no datasource enabled" || echo "0") if [ "$CROWDSEC_FATAL" -ge 1 ]; then fail_test "CRITICAL: CrowdSec failed with 'no datasource enabled' - acquis.yaml is missing or empty" echo "" log_error "CrowdSec is fundamentally broken. Cannot proceed with tests." echo "" echo "=== Container Logs (CrowdSec related) ===" docker logs ${CONTAINER_NAME} 2>&1 | grep -i "crowdsec\|acquis\|datasource" | tail -30 echo "" cleanup exit 1 elif [ "$CROWDSEC_READY" -ge 1 ]; then log_info " CrowdSec LAPI is ready (found startup message in logs)" pass_test else # CrowdSec may not have started yet or may not be available CROWDSEC_STARTED=$(docker logs ${CONTAINER_NAME} 2>&1 | grep -c "Starting CrowdSec" || echo "0") if [ "$CROWDSEC_STARTED" -ge 1 ]; then log_info " CrowdSec startup initiated (may still be initializing)" pass_test else log_warn " CrowdSec startup message not found (may not be enabled or binary missing)" pass_test fi fi # ---------------------------------------------------------------------------- # TC-0b: Verify acquisition config exists # ---------------------------------------------------------------------------- log_test "TC-0b: Verify acquisition config exists" ACQUIS_CONTENT=$(docker exec ${CONTAINER_NAME} cat /etc/crowdsec/acquis.yaml 2>/dev/null || echo "") ACQUIS_HAS_SOURCE=$(echo "$ACQUIS_CONTENT" | grep -c "source:" || echo "0") if [ "$ACQUIS_HAS_SOURCE" -ge 1 ]; then log_info " Acquisition config found with datasource definition" # Show first few lines for debugging log_info " Config preview:" echo "$ACQUIS_CONTENT" | head -5 | sed 's/^/ /' pass_test elif [ -n "$ACQUIS_CONTENT" ]; then fail_test "CRITICAL: acquis.yaml exists but has no 'source:' definition" echo "" log_error "CrowdSec will fail to start without a valid datasource. Cannot proceed." echo "Content found:" echo "$ACQUIS_CONTENT" | head -10 | sed 's/^/ /' echo "" cleanup exit 1 else # acquis.yaml doesn't exist - this might be okay if CrowdSec mode is disabled MODE_CHECK=$(docker exec ${CONTAINER_NAME} printenv CERBERUS_SECURITY_CROWDSEC_MODE 2>/dev/null || echo "disabled") if [ "$MODE_CHECK" = "local" ]; then fail_test "CRITICAL: acquis.yaml missing but CROWDSEC_MODE=local" log_error "CrowdSec local mode enabled but no acquisition config exists." cleanup exit 1 else log_warn " acquis.yaml not found (acceptable if CrowdSec mode is disabled)" pass_test fi fi # ---------------------------------------------------------------------------- # TC-0c: Verify hub items installed # ---------------------------------------------------------------------------- log_test "TC-0c: Verify hub items installed (at least one parser)" PARSER_COUNT=$(docker exec ${CONTAINER_NAME} cscli parsers list -o json 2>/dev/null | jq 'length' 2>/dev/null || echo "0") if [ "$PARSER_COUNT" = "0" ] || [ -z "$PARSER_COUNT" ]; then # cscli may not be available or no parsers installed CSCLI_EXISTS=$(docker exec ${CONTAINER_NAME} which cscli 2>/dev/null || echo "") if [ -z "$CSCLI_EXISTS" ]; then log_warn " cscli not available - cannot verify hub items" pass_test else log_warn " No parsers installed (CrowdSec may not detect attacks)" pass_test fi else log_info " Found $PARSER_COUNT parser(s) installed" # List a few for debugging docker exec ${CONTAINER_NAME} cscli parsers list 2>/dev/null | head -5 | sed 's/^/ /' || true pass_test fi echo "" # ============================================================================ # Detect CrowdSec/cscli availability # ============================================================================ log_info "Detecting CrowdSec/cscli availability..." CSCLI_AVAILABLE=true # Check decisions endpoint to detect cscli availability DETECT_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/decisions" 2>/dev/null || echo '{"error":"request failed"}') if echo "$DETECT_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$DETECT_RESP" | jq -r '.error') if [[ "$ERROR_MSG" == *"cscli"* ]] || [[ "$ERROR_MSG" == *"not available"* ]]; then CSCLI_AVAILABLE=false log_warn "cscli is NOT available in container - ban/unban tests will be SKIPPED" fi fi if [ "$CSCLI_AVAILABLE" = "true" ]; then log_info "cscli appears to be available" fi echo "" # ============================================================================ # Test Cases # ============================================================================ echo "==============================================" echo "=== Running CrowdSec Decision Test Cases ===" echo "==============================================" echo "" # ---------------------------------------------------------------------------- # TC-1: Start CrowdSec (may fail if binary not available - that's OK) # ---------------------------------------------------------------------------- log_test "TC-1: Start CrowdSec process" START_RESP=$(curl -s -X POST -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/start" 2>/dev/null || echo '{"error":"request failed"}') if echo "$START_RESP" | jq -e '.status == "started"' >/dev/null 2>&1; then log_info " CrowdSec started: $(echo "$START_RESP" | jq -c)" pass_test elif echo "$START_RESP" | jq -e '.error' >/dev/null 2>&1; then # CrowdSec binary may not be available - this is acceptable ERROR_MSG=$(echo "$START_RESP" | jq -r '.error // "unknown"') if [[ "$ERROR_MSG" == *"not found"* ]] || [[ "$ERROR_MSG" == *"not available"* ]] || [[ "$ERROR_MSG" == *"executable"* ]]; then skip_test "CrowdSec binary not available in container" else log_warn " Start returned error: $ERROR_MSG (continuing with tests)" pass_test fi else log_warn " Unexpected response: $START_RESP" pass_test fi # ---------------------------------------------------------------------------- # TC-2: Get CrowdSec status # ---------------------------------------------------------------------------- log_test "TC-2: Get CrowdSec status" STATUS_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/status" 2>/dev/null || echo '{"error":"request failed"}') if echo "$STATUS_RESP" | jq -e 'has("running")' >/dev/null 2>&1; then RUNNING=$(echo "$STATUS_RESP" | jq -r '.running') PID=$(echo "$STATUS_RESP" | jq -r '.pid // 0') log_info " Status: running=$RUNNING, pid=$PID" pass_test else fail_test "Status endpoint returned unexpected response: $STATUS_RESP" fi # ---------------------------------------------------------------------------- # TC-3: List decisions (expect empty initially, or error if cscli unavailable) # ---------------------------------------------------------------------------- log_test "TC-3: List decisions (expect empty or cscli error)" DECISIONS_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/decisions" 2>/dev/null || echo '{"error":"request failed"}') if echo "$DECISIONS_RESP" | jq -e 'has("decisions")' >/dev/null 2>&1; then TOTAL=$(echo "$DECISIONS_RESP" | jq -r '.total // 0') # Check if there's also an error field (cscli not available returns both decisions:[] and error) if echo "$DECISIONS_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$DECISIONS_RESP" | jq -r '.error') if [[ "$ERROR_MSG" == *"cscli"* ]] || [[ "$ERROR_MSG" == *"not available"* ]]; then log_info " Decisions endpoint working - returns error as expected (cscli unavailable)" pass_test else log_info " Decisions count: $TOTAL (with error: $ERROR_MSG)" pass_test fi else log_info " Decisions count: $TOTAL" pass_test fi elif echo "$DECISIONS_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$DECISIONS_RESP" | jq -r '.error') if [[ "$ERROR_MSG" == *"cscli"* ]] || [[ "$ERROR_MSG" == *"not available"* ]]; then log_info " Decisions endpoint correctly reports cscli unavailable" pass_test else log_warn " Decisions returned error: $ERROR_MSG (acceptable)" pass_test fi else fail_test "Decisions endpoint returned unexpected response: $DECISIONS_RESP" fi # ---------------------------------------------------------------------------- # TC-4: Ban test IP (192.168.100.100) with 1h duration # ---------------------------------------------------------------------------- log_test "TC-4: Ban test IP (${TEST_IP}) with ${TEST_DURATION} duration" # Skip if cscli is not available if [ "$CSCLI_AVAILABLE" = "false" ]; then skip_test "cscli not available - ban operation requires cscli" BAN_SUCCEEDED=false else BAN_PAYLOAD=$(cat </dev/null || echo '{"error":"request failed"}') if echo "$BAN_RESP" | jq -e '.status == "banned"' >/dev/null 2>&1; then log_info " Ban successful: $(echo "$BAN_RESP" | jq -c)" pass_test BAN_SUCCEEDED=true elif echo "$BAN_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$BAN_RESP" | jq -r '.error') if [[ "$ERROR_MSG" == *"cscli"* ]] || [[ "$ERROR_MSG" == *"not available"* ]] || [[ "$ERROR_MSG" == *"not found"* ]] || [[ "$ERROR_MSG" == *"failed to ban"* ]]; then skip_test "cscli not available for ban operation (error: $ERROR_MSG)" BAN_SUCCEEDED=false # Update global flag since we now know cscli is unavailable CSCLI_AVAILABLE=false else fail_test "Ban failed: $ERROR_MSG" BAN_SUCCEEDED=false fi else fail_test "Ban returned unexpected response: $BAN_RESP" BAN_SUCCEEDED=false fi fi # ---------------------------------------------------------------------------- # TC-5: Verify ban appears in decisions list # ---------------------------------------------------------------------------- log_test "TC-5: Verify ban appears in decisions list" if [ "$CSCLI_AVAILABLE" = "false" ]; then skip_test "cscli not available - cannot verify ban in decisions" elif [ "${BAN_SUCCEEDED:-false}" = "true" ]; then # Give CrowdSec a moment to register the decision sleep 1 VERIFY_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/decisions" 2>/dev/null || echo '{"decisions":[]}') if echo "$VERIFY_RESP" | jq -e ".decisions[] | select(.value == \"${TEST_IP}\")" >/dev/null 2>&1; then log_info " Ban verified in decisions list" pass_test elif echo "$VERIFY_RESP" | jq -e '.error' >/dev/null 2>&1; then skip_test "cscli not available for verification" else # May not find it if CrowdSec is not fully operational log_warn " Ban not found in decisions (CrowdSec may not be fully operational)" pass_test fi else skip_test "Ban operation was skipped, cannot verify" fi # ---------------------------------------------------------------------------- # TC-6: Unban the test IP # ---------------------------------------------------------------------------- log_test "TC-6: Unban the test IP (${TEST_IP})" if [ "$CSCLI_AVAILABLE" = "false" ]; then skip_test "cscli not available - unban operation requires cscli" UNBAN_SUCCEEDED=false elif [ "${BAN_SUCCEEDED:-false}" = "true" ]; then UNBAN_RESP=$(curl -s -X DELETE -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/ban/${TEST_IP}" 2>/dev/null || echo '{"error":"request failed"}') if echo "$UNBAN_RESP" | jq -e '.status == "unbanned"' >/dev/null 2>&1; then log_info " Unban successful: $(echo "$UNBAN_RESP" | jq -c)" pass_test UNBAN_SUCCEEDED=true elif echo "$UNBAN_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$UNBAN_RESP" | jq -r '.error') if [[ "$ERROR_MSG" == *"cscli"* ]] || [[ "$ERROR_MSG" == *"not available"* ]]; then skip_test "cscli not available for unban operation" UNBAN_SUCCEEDED=false else fail_test "Unban failed: $ERROR_MSG" UNBAN_SUCCEEDED=false fi else fail_test "Unban returned unexpected response: $UNBAN_RESP" UNBAN_SUCCEEDED=false fi else skip_test "Ban operation was skipped, cannot unban" UNBAN_SUCCEEDED=false fi # ---------------------------------------------------------------------------- # TC-7: Verify IP removed from decisions # ---------------------------------------------------------------------------- log_test "TC-7: Verify IP removed from decisions" if [ "$CSCLI_AVAILABLE" = "false" ]; then skip_test "cscli not available - cannot verify removal from decisions" elif [ "${UNBAN_SUCCEEDED:-false}" = "true" ]; then # Give CrowdSec a moment to remove the decision sleep 1 REMOVAL_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/decisions" 2>/dev/null || echo '{"decisions":[]}') FOUND=$(echo "$REMOVAL_RESP" | jq -r ".decisions[] | select(.value == \"${TEST_IP}\") | .value" 2>/dev/null || echo "") if [ -z "$FOUND" ]; then log_info " IP successfully removed from decisions" pass_test else log_warn " IP still present in decisions (may take time to propagate)" pass_test fi else skip_test "Unban operation was skipped, cannot verify removal" fi # ---------------------------------------------------------------------------- # TC-8: Test export endpoint (should return tar.gz or 404 if no config) # ---------------------------------------------------------------------------- log_test "TC-8: Test export endpoint" EXPORT_FILE=$(mktemp --suffix=.tar.gz) EXPORT_HTTP_CODE=$(curl -s -b "${TMP_COOKIE}" \ -o "${EXPORT_FILE}" -w "%{http_code}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/export" 2>/dev/null || echo "000") if [ "$EXPORT_HTTP_CODE" = "200" ]; then if [ -s "${EXPORT_FILE}" ]; then EXPORT_SIZE=$(ls -lh "${EXPORT_FILE}" 2>/dev/null | awk '{print $5}') log_info " Export successful: ${EXPORT_SIZE}" pass_test else log_info " Export returned empty file (no config to export)" pass_test fi elif [ "$EXPORT_HTTP_CODE" = "404" ]; then log_info " Export returned 404 (no CrowdSec config exists - expected)" pass_test elif [ "$EXPORT_HTTP_CODE" = "500" ]; then # May fail if config directory doesn't exist log_info " Export returned 500 (config directory may not exist - acceptable)" pass_test else fail_test "Export returned unexpected HTTP code: $EXPORT_HTTP_CODE" fi rm -f "${EXPORT_FILE}" 2>/dev/null || true # ---------------------------------------------------------------------------- # TC-10: Test LAPI health endpoint # ---------------------------------------------------------------------------- log_test "TC-10: Test LAPI health endpoint" LAPI_RESP=$(curl -s -b "${TMP_COOKIE}" \ "http://localhost:${API_PORT}/api/v1/admin/crowdsec/lapi/health" 2>/dev/null || echo '{"error":"request failed"}') if echo "$LAPI_RESP" | jq -e 'has("healthy")' >/dev/null 2>&1; then HEALTHY=$(echo "$LAPI_RESP" | jq -r '.healthy') LAPI_URL=$(echo "$LAPI_RESP" | jq -r '.lapi_url // "not configured"') log_info " LAPI Health: healthy=$HEALTHY, url=$LAPI_URL" pass_test elif echo "$LAPI_RESP" | jq -e '.error' >/dev/null 2>&1; then ERROR_MSG=$(echo "$LAPI_RESP" | jq -r '.error') log_info " LAPI Health check returned error: $ERROR_MSG (acceptable - LAPI may not be configured)" pass_test else # Any response from the endpoint is acceptable log_info " LAPI Health response: $(echo "$LAPI_RESP" | head -c 200)" pass_test fi # ============================================================================ # Results Summary # ============================================================================ echo "" echo "==============================================" echo "=== CrowdSec Decision Integration Results ===" echo "==============================================" echo "" echo -e " ${GREEN}Passed:${NC} $PASSED" echo -e " ${RED}Failed:${NC} $FAILED" echo -e " ${YELLOW}Skipped:${NC} $SKIPPED" echo "" if [ "$CSCLI_AVAILABLE" = "false" ]; then echo -e " ${YELLOW}Note:${NC} cscli was not available in container - ban/unban tests were skipped" echo " This is expected behavior for the current charon:local image." echo "" fi # Cleanup cleanup if [ $FAILED -eq 0 ]; then if [ $SKIPPED -gt 0 ]; then echo "==============================================" echo "=== CROWDSEC TESTS PASSED (with skips) ===" echo "==============================================" echo "=== ALL CROWDSEC DECISION TESTS PASSED ===" echo "==============================================" else echo "==============================================" echo "=== ALL CROWDSEC DECISION TESTS PASSED ===" echo "==============================================" fi echo "" exit 0 else echo "==============================================" echo "=== CROWDSEC DECISION TESTS FAILED ===" echo "==============================================" echo "" exit 1 fi