From 1e63cec37ca3a051605bd7b204ff0c65988460a5 Mon Sep 17 00:00:00 2001 From: Adam Outler Date: Fri, 31 Oct 2025 22:24:08 +0000 Subject: [PATCH] Revise tests. Use docker-compose.yml where possible --- .../docker-compose.missing-caps.yml | 49 ++ ...mpose.mount-test.active_config_mounted.yml | 4 + ...pose.mount-test.active_config_no-mount.yml | 4 + ...mpose.mount-test.active_config_ramdisk.yml | 4 + ...se.mount-test.active_config_unwritable.yml | 4 + .../docker-compose.mount-test.api_mounted.yml | 4 + ...docker-compose.mount-test.api_no-mount.yml | 4 + .../docker-compose.mount-test.api_ramdisk.yml | 4 + ...cker-compose.mount-test.api_unwritable.yml | 4 + ...cker-compose.mount-test.config_mounted.yml | 4 + ...ker-compose.mount-test.config_no-mount.yml | 4 + ...cker-compose.mount-test.config_ramdisk.yml | 4 + ...r-compose.mount-test.config_unwritable.yml | 4 + .../docker-compose.mount-test.db_mounted.yml | 4 + .../docker-compose.mount-test.db_no-mount.yml | 4 + .../docker-compose.mount-test.db_ramdisk.yml | 4 + ...ocker-compose.mount-test.db_unwritable.yml | 4 + .../docker-compose.mount-test.log_mounted.yml | 4 + ...docker-compose.mount-test.log_no-mount.yml | 4 + .../docker-compose.mount-test.log_ramdisk.yml | 4 + ...cker-compose.mount-test.log_unwritable.yml | 4 + .../docker-compose.mount-test.run_mounted.yml | 4 + ...docker-compose.mount-test.run_no-mount.yml | 4 + .../docker-compose.mount-test.run_ramdisk.yml | 4 + ...cker-compose.mount-test.run_unwritable.yml | 4 + .../test_all_docker_composes.sh | 69 ++ .../configurations/test_results.log | 150 ++++ .../test_container_environment.py | 771 ++++++------------ .../test_docker_compose_scenarios.py | 440 ++++++++++ .../test_mount_diagnostics_pytest.py | 22 +- test/docker_tests/test_ports_available.py | 240 ++++++ 31 files changed, 1311 insertions(+), 526 deletions(-) create mode 100644 test/docker_tests/configurations/docker-compose.missing-caps.yml create mode 100755 test/docker_tests/configurations/test_all_docker_composes.sh create mode 100644 test/docker_tests/configurations/test_results.log create mode 100644 test/docker_tests/test_docker_compose_scenarios.py create mode 100644 test/docker_tests/test_ports_available.py diff --git a/test/docker_tests/configurations/docker-compose.missing-caps.yml b/test/docker_tests/configurations/docker-compose.missing-caps.yml new file mode 100644 index 00000000..2bd4b1f7 --- /dev/null +++ b/test/docker_tests/configurations/docker-compose.missing-caps.yml @@ -0,0 +1,49 @@ +services: + netalertx: + # Missing capabilities configuration for testing + network_mode: ${NETALERTX_NETWORK_MODE:-host} + build: + context: ../../../ + dockerfile: Dockerfile + image: netalertx-test + container_name: netalertx-test-missing-caps + read_only: true + cap_drop: + - ALL # Drop all capabilities to test missing capabilities scenario + + volumes: + - type: volume + source: netalertx_config + target: /app/config + read_only: false + + - type: volume + source: netalertx_db + target: /app/db + read_only: false + + - type: bind + source: /etc/localtime + target: /etc/localtime + read_only: true + + environment: + LISTEN_ADDR: ${LISTEN_ADDR:-0.0.0.0} + PORT: ${PORT:-20211} + APP_CONF_OVERRIDE: ${GRAPHQL_PORT:-20212} + ALWAYS_FRESH_INSTALL: ${ALWAYS_FRESH_INSTALL:-false} + NETALERTX_DEBUG: ${NETALERTX_DEBUG:-0} + + mem_limit: 2048m + mem_reservation: 1024m + cpu_shares: 512 + pids_limit: 512 + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +volumes: + netalertx_config: + netalertx_db: \ No newline at end of file diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_mounted.yml index b1d297cc..a3c001a6 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper nginx config mount +# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as writable and mounted +# - No configuration warnings for nginx config path +# - Custom PORT configuration should work when nginx config is writable services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_no-mount.yml index 2adb03d7..cad378e6 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows warning about missing nginx config mount +# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as not mounted +# - Warning message about nginx configuration mount being missing +# - Custom PORT configuration may not work properly services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_ramdisk.yml index 60bba0c9..272cb299 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows performance warning for nginx config on RAM disk +# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as mounted on tmpfs (RAM disk) +# - Performance issue warning since nginx config should be persistent +# - Custom PORT configuration may have performance implications services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_unwritable.yml index 40be42b6..f122ffce 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.active_config_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable nginx config partition +# - SYSTEM_SERVICES_ACTIVE_CONFIG shows as mounted but unwritable (❌ in Writeable column) +# - 35-nginx-config.sh detects permission error and exits with code 1 +# - Container startup fails because nginx configuration cannot be written for custom ports services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_mounted.yml index 1bcc2cfa..72fb7b7d 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper API mount +# - NETALERTX_API shows as writable and mounted +# - No configuration warnings for API path +# - API data persistence works correctly services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_no-mount.yml index f56d4adb..418e3249 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows mount error for API directory +# - NETALERTX_API shows as not mounted +# - Mount error since API directory should be mounted for proper operation +# - API functionality may be limited services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_ramdisk.yml index 1f0bf521..1f8e09d2 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows performance warning for API on RAM disk +# - NETALERTX_API shows as mounted on tmpfs (RAM disk) +# - Performance issue warning since API data should be on persistent storage +# - API data will be lost on container restart services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_unwritable.yml index 185acda4..aa3bbb64 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.api_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable API partition +# - NETALERTX_API shows as mounted but unwritable (❌ in Writeable column) +# - API directory must be writable for proper operation +# - Container startup fails because API functionality cannot work without write access services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_mounted.yml index 5eef516b..faa7fda7 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper config mount +# - NETALERTX_CONFIG shows as writable and mounted +# - No configuration warnings for config path +# - Configuration persistence works correctly services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_no-mount.yml index 5a5284a0..a1dbf7d2 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows mount error for config directory +# - NETALERTX_CONFIG shows as not mounted +# - Mount error since config directory should be mounted for proper operation +# - Configuration may not persist across restarts services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_ramdisk.yml index 0c57f6cb..c638d900 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows dataloss risk warning for config on RAM disk +# - NETALERTX_CONFIG shows as mounted on tmpfs (RAM disk) +# - Dataloss risk warning since config data should be persistent +# - Configuration will be lost on container restart services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_unwritable.yml index b9f323b9..fb674536 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.config_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable config partition +# - NETALERTX_CONFIG shows as mounted but unwritable (❌ in Writeable column) +# - 30-writable-config.sh detects permission error and exits with code 1 +# - Container startup fails because config files cannot be written to services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_mounted.yml index c78ebb13..f94f1af9 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper database mount +# - NETALERTX_DB shows as writable and mounted +# - No configuration warnings for database path +# - Database persistence works correctly services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_no-mount.yml index 2d49713f..27e9d78a 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows mount error warning but continues running +# - NETALERTX_DB shows as not mounted (❌ in Mount column) but path gets created +# - Warning message displayed about configuration issues +# - Container continues because database directory can be created in writable filesystem services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml index 2a23610b..fed7fb46 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows dataloss risk warning for database on RAM disk +# - NETALERTX_DB shows as mounted on tmpfs (RAM disk) +# - Dataloss risk warning since database should be persistent +# - Database will be lost on container restart services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml index d59e8e53..edb91750 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.db_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable database partition +# - NETALERTX_DB shows as mounted but unwritable (❌ in Writeable column) +# - 30-writable-config.sh detects permission error and exits with code 1 +# - Container startup fails because database files cannot be written to services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml index 54d8c958..75abf3fc 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper log mount +# - NETALERTX_LOG shows as mounted and writable +# - No mount warnings since logs can be non-persistent +# - Container starts normally with logging enabled services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_no-mount.yml index 93691de1..1c0ee284 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows mount error warning but continues running +# - NETALERTX_LOG shows as not mounted (❌ in Mount column) +# - Warning message displayed about configuration issues +# - Container continues to run despite the mount error services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_ramdisk.yml index 8644a253..00374378 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows dataloss risk warning for logs on RAM disk +# - NETALERTX_LOG shows as mounted on tmpfs (RAM disk) +# - Dataloss risk warning since logs may be lost on restart +# - Container starts but logs may not persist services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_unwritable.yml index b72808cd..28709451 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.log_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable log partition +# - NETALERTX_LOG shows as mounted but unwritable (❌ in Writeable column) +# - 25-mandatory-folders.sh cannot create required log files and fails +# - Container startup fails because logging infrastructure cannot be initialized services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_mounted.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_mounted.yml index 3bd1403b..cc396202 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_mounted.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_mounted.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container starts successfully with proper run mount +# - NETALERTX_RUN shows as mounted and writable +# - No mount warnings since run directory can be non-persistent +# - Container starts normally with runtime files enabled services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_no-mount.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_no-mount.yml index f928bfb4..946fb459 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_no-mount.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_no-mount.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows mount error warning but continues running +# - NETALERTX_RUN shows as not mounted (❌ in Mount column) +# - Warning message displayed about configuration issues +# - Container continues to run despite the mount error services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_ramdisk.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_ramdisk.yml index b0eabfc2..e50844d6 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_ramdisk.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_ramdisk.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container shows dataloss risk warning for run on RAM disk +# - NETALERTX_RUN shows as mounted on tmpfs (RAM disk) +# - Dataloss risk warning since runtime files may be lost on restart +# - Container starts but runtime state may not persist services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_unwritable.yml b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_unwritable.yml index 74bab889..23ea5612 100644 --- a/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_unwritable.yml +++ b/test/docker_tests/configurations/mount-tests/docker-compose.mount-test.run_unwritable.yml @@ -1,3 +1,7 @@ +# Expected outcome: Container fails to start due to unwritable run partition +# - NETALERTX_RUN shows as mounted but unwritable (❌ in Writeable column) +# - 25-mandatory-folders.sh cannot create required runtime files and fails +# - Container startup fails because runtime infrastructure cannot be initialized services: netalertx: network_mode: host diff --git a/test/docker_tests/configurations/test_all_docker_composes.sh b/test/docker_tests/configurations/test_all_docker_composes.sh new file mode 100755 index 00000000..b5701179 --- /dev/null +++ b/test/docker_tests/configurations/test_all_docker_composes.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# test_all_docker_composes.sh - Test all docker-compose configurations +# Extracts comments from each file and runs the container for 10 seconds + +LOG_FILE="/workspaces/NetAlertX/test/docker_tests/configurations/test_results.log" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "Starting Docker Compose Tests - $(date)" > "$LOG_FILE" +echo "==========================================" >> "$LOG_FILE" + +# Function to extract comments from docker-compose file +extract_comments() { + local file="$1" + echo "File: $(basename "$file")" >> "$LOG_FILE" + echo "----------------------------------------" >> "$LOG_FILE" + + # Extract lines starting with # until we hit a non-comment line + awk ' + /^#/ { + # Remove the # and any leading/trailing whitespace + comment = substr($0, 2) + sub(/^ */, "", comment) + sub(/ *$/, "", comment) + if (comment != "") { + print comment + } + } + /^[^#]/ && !/^$/ { + exit + } + ' "$file" >> "$LOG_FILE" + + echo "" >> "$LOG_FILE" +} + +# Function to run docker-compose test +run_test() { + local file="$1" + local dirname=$(dirname "$file") + local basename=$(basename "$file") + + echo "Testing: $basename" >> "$LOG_FILE" + echo "Directory: $dirname" >> "$LOG_FILE" + echo "" >> "$LOG_FILE" + + # Change to the directory containing the docker-compose file + cd "$dirname" + + # Run docker-compose up with timeout + echo "Running docker-compose up..." >> "$LOG_FILE" + timeout 10s docker-compose -f "$basename" up 2>&1 >> "$LOG_FILE" + + # Clean up + docker-compose -f "$basename" down -v 2>/dev/null || true + docker volume prune -f 2>/dev/null || true + + echo "" >> "$LOG_FILE" + echo "==========================================" >> "$LOG_FILE" + echo "" >> "$LOG_FILE" +} + +# Find all docker-compose files +find "$SCRIPT_DIR" -name "docker-compose*.yml" -type f | sort | while read -r file; do + extract_comments "$file" + run_test "$file" +done + +echo "All tests completed - $(date)" >> "$LOG_FILE" +echo "Results saved to: $LOG_FILE" \ No newline at end of file diff --git a/test/docker_tests/configurations/test_results.log b/test/docker_tests/configurations/test_results.log new file mode 100644 index 00000000..6e1a4eec --- /dev/null +++ b/test/docker_tests/configurations/test_results.log @@ -0,0 +1,150 @@ +Starting Docker Compose Tests - Fri Oct 31 20:00:39 UTC 2025 +========================================== +File: docker-compose.missing-caps.yml +---------------------------------------- + +Testing: docker-compose.missing-caps.yml +Directory: /workspaces/NetAlertX/test/docker_tests/configurations + +Running docker-compose up... +Attaching to netalertx-test-missing-caps + netalertx-test-missing-caps exited with code 255 + +========================================== + +File: docker-compose.readonly.yml +---------------------------------------- + +Testing: docker-compose.readonly.yml +Directory: /workspaces/NetAlertX/test/docker_tests/configurations + +Running docker-compose up... +Attaching to netalertx-test-readonly +netalertx-test-readonly |  +netalertx-test-readonly | _ _ _ ___ _ _ __ __ +netalertx-test-readonly | | \ | | | | / _ \| | | | \ \ / / +netalertx-test-readonly | | \| | ___| |_/ /_\ \ | ___ _ __| |_ \ V / +netalertx-test-readonly | | . |/ _ \ __| _ | |/ _ \ __| __|/ \ +netalertx-test-readonly | | |\ | __/ |_| | | | | __/ | | |_/ /^\ \ +netalertx-test-readonly | \_| \_/\___|\__\_| |_/_|\___|_| \__\/ \/ +netalertx-test-readonly | +netalertx-test-readonly |  Network intruder and presence detector. +netalertx-test-readonly | https://netalertx.com +netalertx-test-readonly | +netalertx-test-readonly | +netalertx-test-readonly | Startup pre-checks +netalertx-test-readonly | --> storage permission +netalertx-test-readonly | --> mounts.py +netalertx-test-readonly | --> first run config +netalertx-test-readonly | --> first run db +netalertx-test-readonly | --> mandatory folders +netalertx-test-readonly | --> writable config +netalertx-test-readonly | --> nginx config +netalertx-test-readonly | nginx config: FAILED with 1 +netalertx-test-readonly | Failure detected in: /entrypoint.d/35-nginx-config.sh +netalertx-test-readonly | --> user netalertx +netalertx-test-readonly | --> host mode network +netalertx-test-readonly | --> layer 2 capabilities +netalertx-test-readonly | --> excessive capabilities +netalertx-test-readonly | excessive capabilities: FAILED with 2 +netalertx-test-readonly | Failure detected in: /entrypoint.d/90-excessive-capabilities.sh +netalertx-test-readonly | --> appliance integrity +netalertx-test-readonly | --> ports available +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ⚠️ Port Warning: Application port 20211 is already in use. +netalertx-test-readonly | +netalertx-test-readonly | The main application (defined by $PORT) may fail to start. +netalertx-test-readonly | +netalertx-test-readonly | https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ⚠️ Port Warning: GraphQL API port 20212 is already in use. +netalertx-test-readonly | +netalertx-test-readonly | The GraphQL API (defined by $APP_CONF_OVERRIDE or $GRAPHQL_PORT) +netalertx-test-readonly | may fail to start. +netalertx-test-readonly | +netalertx-test-readonly | https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | Container startup checks failed with exit code 2. +netalertx-test-readonly | NETALERTX_DEBUG is set to 1, will not shut down other services if one fails. +netalertx-test-readonly | Starting /usr/sbin/crond -c "/services/config/crond" -f -L "/app/log/crond.log" >>"/app/log/crond.log" 2>&1 & +netalertx-test-readonly | Starting /usr/sbin/php-fpm83 -y "/services/config/php/php-fpm.conf" -F >>"/app/log/app.php_errors.log" 2>/dev/stderr & +netalertx-test-readonly | Crond stopped! (exit 1) +netalertx-test-readonly | php-fpm stopped! (exit 1) +netalertx-test-readonly | Starting python3 -m server > /app/log/stdout.log 2> >(tee /app/log/stderr.log >&2) + netalertx-test-readonly exited with code 0 +netalertx-test-readonly | --> first run config +netalertx-test-readonly | --> first run db +netalertx-test-readonly | --> mandatory folders +netalertx-test-readonly | --> writable config +netalertx-test-readonly | --> nginx config +netalertx-test-readonly | nginx config: FAILED with 1 +netalertx-test-readonly | Failure detected in: /entrypoint.d/35-nginx-config.sh +netalertx-test-readonly | --> user netalertx +netalertx-test-readonly | --> host mode network +netalertx-test-readonly | --> layer 2 capabilities +netalertx-test-readonly | --> excessive capabilities +netalertx-test-readonly | excessive capabilities: FAILED with 2 +netalertx-test-readonly | Failure detected in: /entrypoint.d/90-excessive-capabilities.sh +netalertx-test-readonly | --> appliance integrity +netalertx-test-readonly | --> ports available +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ⚠️ Port Warning: Application port 20211 is already in use. +netalertx-test-readonly | +netalertx-test-readonly | The main application (defined by $PORT) may fail to start. +netalertx-test-readonly | +netalertx-test-readonly | https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | ⚠️ Port Warning: GraphQL API port 20212 is already in use. +netalertx-test-readonly | +netalertx-test-readonly | The GraphQL API (defined by $APP_CONF_OVERRIDE or $GRAPHQL_PORT) +netalertx-test-readonly | may fail to start. +netalertx-test-readonly | +netalertx-test-readonly | https://github.com/jokob-sk/NetAlertX/blob/main/docs/docker-troubleshooting/port-conflicts.md +netalertx-test-readonly | ══════════════════════════════════════════════════════════════════════════════ +netalertx-test-readonly | Container startup checks failed with exit code 2. +netalertx-test-readonly | NETALERTX_DEBUG is set to 1, will not shut down other services if one fails. +netalertx-test-readonly | Starting /usr/sbin/crond -c "/services/config/crond" -f -L "/app/log/crond.log" >>"/app/log/crond.log" 2>&1 & +netalertx-test-readonly | Starting /usr/sbin/php-fpm83 -y "/services/config/php/php-fpm.conf" -F >>"/app/log/app.php_errors.log" 2>/dev/stderr & +netalertx-test-readonly | php-fpm stopped! (exit 1) +netalertx-test-readonly | Crond stopped! (exit 1) +netalertx-test-readonly | Starting python3 -m server > /app/log/stdout.log 2> >(tee /app/log/stderr.log >&2) + +========================================== + +File: docker-compose.writable.yml +---------------------------------------- + +Testing: docker-compose.writable.yml +Directory: /workspaces/NetAlertX/test/docker_tests/configurations + +Running docker-compose up... + +========================================== + +File: docker-compose.mount-test.active_config_mounted.yml +---------------------------------------- +Expected outcome: Container starts successfully with proper nginx config mount +- SYSTEM_SERVICES_ACTIVE_CONFIG shows as writable and mounted +- No configuration warnings for nginx config path +- Custom PORT configuration should work when nginx config is writable + +Testing: docker-compose.mount-test.active_config_mounted.yml +Directory: /workspaces/NetAlertX/test/docker_tests/configurations/mount-tests + +Running docker-compose up... + +========================================== + +File: docker-compose.mount-test.active_config_no-mount.yml +---------------------------------------- +Expected outcome: Container shows warning about missing nginx config mount +- SYSTEM_SERVICES_ACTIVE_CONFIG shows as not mounted +- Warning message about nginx configuration mount being missing +- Custom PORT configuration may not work properly + +Testing: docker-compose.mount-test.active_config_no-mount.yml +Directory: /workspaces/NetAlertX/test/docker_tests/configurations/mount-tests + +Running docker-compose up... diff --git a/test/docker_tests/test_container_environment.py b/test/docker_tests/test_container_environment.py index d847000f..e440d142 100644 --- a/test/docker_tests/test_container_environment.py +++ b/test/docker_tests/test_container_environment.py @@ -263,10 +263,9 @@ def _run_container( ) result.output = stdouterr # Print container output for debugging in every test run. - try: - print("\n--- CONTAINER out ---\n", result.output) - except Exception: - pass + print("\n--- CONTAINER OUTPUT START ---") + print(result.output) + print("--- CONTAINER OUTPUT END ---\n") return result @@ -313,485 +312,6 @@ def _restore_zero_perm_dir(paths: dict[str, pathlib.Path], key: str) -> None: -def test_root_owned_app_db_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - - Check script: check-app-permissions.sh - Sample message: "⚠️ ATTENTION: Write permission denied. The application cannot write to..." - """ - paths = _setup_mount_tree(tmp_path, "root_app_db") - _chown_root(paths["app_db"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-app-db", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_db"]), result.args) - finally: - _chown_netalertx(paths["app_db"]) - - -def test_root_owned_app_config_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - """ - paths = _setup_mount_tree(tmp_path, "root_app_config") - _chown_root(paths["app_config"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-app-config", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_config"]), result.args) - assert result.returncode != 0 - finally: - _chown_netalertx(paths["app_config"]) - - -def test_root_owned_app_log_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - """ - paths = _setup_mount_tree(tmp_path, "root_app_log") - _chown_root(paths["app_log"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-app-log", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_log"]), result.args) - assert result.returncode != 0 - finally: - _chown_netalertx(paths["app_log"]) - - -def test_root_owned_app_api_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - """ - paths = _setup_mount_tree(tmp_path, "root_app_api") - _chown_root(paths["app_api"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-app-api", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_api"]), result.args) - assert result.returncode != 0 - finally: - _chown_netalertx(paths["app_api"]) - - -def test_root_owned_nginx_conf_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - """ - paths = _setup_mount_tree(tmp_path, "root_nginx_conf") - _chown_root(paths["nginx_conf"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-nginx-conf", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["nginx_conf"]), result.args) - assert result.returncode != 0 - finally: - _chown_netalertx(paths["nginx_conf"]) - - -def test_root_owned_services_run_mount(tmp_path: pathlib.Path) -> None: - """Test root-owned mounts - simulates mounting host directories owned by root. - - 1. Root-Owned Mounts: Simulates mounting host directories owned by root - (common with docker run -v /host/path:/app/db). - Tests each required mount point when owned by root user. - Expected: Warning about permission issues, guidance to fix ownership. - """ - paths = _setup_mount_tree(tmp_path, "root_services_run") - _chown_root(paths["services_run"]) - volumes = _build_volume_args(paths) - try: - result = _run_container("root-services-run", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["services_run"]), result.args) - assert result.returncode != 0 - finally: - _chown_netalertx(paths["services_run"]) - - -def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - - Check script: check-app-permissions.sh - Sample messages: "⚠️ ATTENTION: Write permission denied. The application cannot write to..." - "⚠️ ATTENTION: Read permission denied. The application cannot read from..." - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_db") - _setup_zero_perm_dir(paths, "app_db") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-db", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_db"]), result.args) - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "app_db") - - -def test_zero_permissions_app_db_file(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_db_file") - (paths["app_db"] / "app.db").chmod(0) - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-db-file", volumes) - _assert_contains(result, "Write permission denied", result.args) - assert result.returncode != 0 - finally: - (paths["app_db"] / "app.db").chmod(0o600) - - -def test_zero_permissions_app_config_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_config") - _setup_zero_perm_dir(paths, "app_config") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-config", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_config"]), result.args) - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "app_config") - - -def test_zero_permissions_app_config_file(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_config_file") - (paths["app_config"] / "app.conf").chmod(0) - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-config-file", volumes) - _assert_contains(result, "Write permission denied", result.args) - assert result.returncode != 0 - finally: - (paths["app_config"] / "app.conf").chmod(0o600) - - -def test_zero_permissions_app_log_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_log") - _setup_zero_perm_dir(paths, "app_log") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-log", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_log"]), result.args) - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "app_log") - - -def test_zero_permissions_app_api_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_app_api") - _setup_zero_perm_dir(paths, "app_api") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-app-api", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_api"]), result.args) - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "app_api") - - -def test_zero_permissions_nginx_conf_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_nginx_conf") - _setup_zero_perm_dir(paths, "nginx_conf") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-nginx-conf", volumes, user="20211:20211") - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "nginx_conf") - - -def test_zero_permissions_services_run_dir(tmp_path: pathlib.Path) -> None: - """Test zero permissions - simulates mounting directories/files with no permissions. - - 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). - Tests directories and files with no read/write/execute permissions. - Expected: "Write permission denied" error with path, guidance to fix permissions. - """ - paths = _setup_mount_tree(tmp_path, "chmod_services_run") - _setup_zero_perm_dir(paths, "services_run") - volumes = _build_volume_args(paths) - try: - result = _run_container("chmod-services-run", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["services_run"]), result.args) - assert result.returncode != 0 - finally: - _restore_zero_perm_dir(paths, "services_run") - - -def test_readonly_app_db_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_app_db") - volumes = _build_volume_args(paths, read_only={"app_db"}) - result = _run_container("readonly-app-db", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_db"]), result.args) - assert result.returncode != 0 - - -def test_readonly_app_config_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_app_config") - volumes = _build_volume_args(paths, read_only={"app_config"}) - result = _run_container("readonly-app-config", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_config"]), result.args) - assert result.returncode != 0 - - -def test_readonly_app_log_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_app_log") - volumes = _build_volume_args(paths, read_only={"app_log"}) - result = _run_container("readonly-app-log", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_log"]), result.args) - assert result.returncode != 0 - - -def test_readonly_app_api_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_app_api") - volumes = _build_volume_args(paths, read_only={"app_api"}) - result = _run_container("readonly-app-api", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["app_api"]), result.args) - assert result.returncode != 0 - - -def test_readonly_nginx_conf_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_nginx_conf") - volumes = _build_volume_args(paths, read_only={"nginx_conf"}) - result = _run_container("readonly-nginx-conf", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/services/config/nginx/conf.active", result.args) - assert result.returncode != 0 - - -def test_readonly_services_run_mount(tmp_path: pathlib.Path) -> None: - """Test readonly mounts - simulates read-only volume mounts in containers. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when mounted read-only. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "readonly_services_run") - volumes = _build_volume_args(paths, read_only={"services_run"}) - result = _run_container("readonly-services-run", volumes) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, str(VOLUME_MAP["services_run"]), result.args) - assert result.returncode != 0 - - -def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None: - """Test custom port configuration without writable nginx config mount. - - 4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT - without mounting nginx config. Container starts but uses default address. - Expected: Container starts but uses default address, warning about missing config mount. - - Check script: check-nginx-config.sh - Sample messages: "⚠️ ATTENTION: Nginx configuration mount /services/config/nginx/conf.active is missing." - "⚠️ ATTENTION: Unable to write to /services/config/nginx/conf.active/netalertx.conf." - """ - paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf") - paths["nginx_conf"].chmod(0o500) - volumes = _build_volume_args(paths) - try: - result = _run_container( - "custom-port-ro-conf", - volumes, - env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"}, - ) - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/services/config/nginx/conf.active", result.args) - assert result.returncode != 0 - finally: - paths["nginx_conf"].chmod(0o755) - -def test_missing_mount_app_db(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - ... - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_app_db") - volumes = _build_volume_args(paths, skip={"app_db"}) - # CHANGE: Run as root (0:0) to bypass all permission checks on other mounts. - result = _run_container("missing-mount-app-db", volumes, user="20211:20211") - # Acknowledge the original intent to check for permission denial (now implicit via root) - # _assert_contains(result, "Write permission denied", result.args) # No longer needed, as root user is used - - # Robust assertion: check for both the warning and the path - if "not a persistent mount" not in result.output or "/app/db" not in result.output: - print("\n--- DEBUG CONTAINER OUTPUT ---\n", result.output) - raise AssertionError("Expected persistent mount warning for /app/db in container output.") - - -def test_missing_mount_app_config(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when missing. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_app_config") - volumes = _build_volume_args(paths, skip={"app_config"}) - result = _run_container("missing-mount-app-config", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/app/config", result.args) - - -def test_missing_mount_app_log(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when missing. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_app_log") - volumes = _build_volume_args(paths, skip={"app_log"}) - result = _run_container("missing-mount-app-log", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/app/log", result.args) - - -def test_missing_mount_app_api(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when missing. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_app_api") - volumes = _build_volume_args(paths, skip={"app_api"}) - result = _run_container("missing-mount-app-api", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/app/api", result.args) - - -def test_missing_mount_nginx_conf(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when missing. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_nginx_conf") - volumes = _build_volume_args(paths, skip={"nginx_conf"}) - result = _run_container("missing-mount-nginx-conf", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/services/config/nginx/conf.active", result.args) - assert result.returncode != 0 - - -def test_missing_mount_services_run(tmp_path: pathlib.Path) -> None: - """Test missing required mounts - simulates forgetting to mount persistent volumes. - - 3. Missing Required Mounts: Simulates forgetting to mount required persistent volumes - in read-only containers. Tests each required mount point when missing. - Expected: "Write permission denied" error with path, guidance to add volume mounts. - """ - paths = _setup_mount_tree(tmp_path, "missing_mount_services_run") - volumes = _build_volume_args(paths, skip={"services_run"}) - result = _run_container("missing-mount-services-run", volumes, user="20211:20211") - _assert_contains(result, "Write permission denied", result.args) - _assert_contains(result, "/services/run", result.args) - _assert_contains(result, "Container startup checks failed with exit code", result.args) - - def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None: """Test missing required capabilities - simulates insufficient container privileges. @@ -799,8 +319,8 @@ def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None: NET_BIND_SERVICE capabilities. Required for ARP scanning and network operations. Expected: "exec /bin/sh: operation not permitted" error, guidance to add capabilities. - Check script: check-cap.sh - Sample message: "⚠️ ATTENTION: Raw network capabilities are missing. Tools that rely on NET_RAW..." + Check script: N/A (capability check happens at container runtime) + Sample message: "exec /bin/sh: operation not permitted" """ paths = _setup_mount_tree(tmp_path, "missing_caps") volumes = _build_volume_args(paths) @@ -820,8 +340,8 @@ def test_running_as_root_is_blocked(tmp_path: pathlib.Path) -> None: dedicated netalertx user. Warning about security risks, special permission fix mode. Expected: Warning about security risks, guidance to use UID 20211. - Check script: check-app-permissions.sh - Sample message: "⚠️ ATTENTION: NetAlertX is running as root (UID 0). This defeats every hardening..." + Check script: /entrypoint.d/0-storage-permission.sh + Sample message: "🚨 CRITICAL SECURITY ALERT: NetAlertX is running as ROOT (UID 0)!" """ paths = _setup_mount_tree(tmp_path, "run_as_root") volumes = _build_volume_args(paths) @@ -843,7 +363,7 @@ def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None: of netalertx user. Permission errors due to incorrect user context. Expected: Permission errors, guidance to use correct user. - Check script: check-user-netalertx.sh + Check script: /entrypoint.d/60-user-netalertx.sh Sample message: "⚠️ ATTENTION: NetAlertX is running as UID 1000:1000. Hardened permissions..." """ paths = _setup_mount_tree(tmp_path, "run_as_1000") @@ -885,15 +405,21 @@ def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None: 9. Missing Configuration File: Simulates corrupted/missing app.conf. Container automatically regenerates default configuration on startup. Expected: Automatic regeneration of default configuration. + + Check script: /entrypoint.d/15-first-run-config.sh + Sample message: "Default configuration written to" """ base = tmp_path / "missing_app_conf_base" paths = _setup_fixed_mount_tree(base) - _chown_netalertx(paths["app_config"]) + # Ensure directories are writable and owned by netalertx user so container can operate + for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]: + paths[key].chmod(0o777) + _chown_netalertx(paths[key]) (paths["app_config"] / "testfile.txt").write_text("test") volumes = _build_volume_args(paths) - result = _run_container("missing-app-conf", volumes) + result = _run_container("missing-app-conf", volumes, sleep_seconds=5) _assert_contains(result, "Default configuration written to", result.args) - assert result.returncode != 0 + assert result.returncode == 0 def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None: @@ -902,54 +428,253 @@ def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None: 10. Missing Database File: Simulates corrupted/missing app.db. Container automatically creates initial database schema on startup. Expected: Automatic creation of initial database schema. + + Check script: /entrypoint.d/20-first-run-db.sh + Sample message: "Building initial database schema" """ base = tmp_path / "missing_app_db_base" paths = _setup_fixed_mount_tree(base) _chown_netalertx(paths["app_db"]) (paths["app_db"] / "testfile.txt").write_text("test") volumes = _build_volume_args(paths) - result = _run_container("missing-app-db", volumes, user="20211:20211") + result = _run_container("missing-app-db", volumes, user="20211:20211", sleep_seconds=5) _assert_contains(result, "Building initial database schema", result.args) assert result.returncode != 0 -def test_tmpfs_config_mount_warns(tmp_path: pathlib.Path) -> None: - """Test tmpfs instead of volumes - simulates using tmpfs for persistent data. +def test_custom_port_without_writable_conf(tmp_path: pathlib.Path) -> None: + """Test custom port configuration without writable nginx config mount. - 11. Tmpfs Instead of Volumes: Simulates using tmpfs mounts instead of persistent volumes - (data loss on restart). Tests config and db directories mounted as tmpfs. - Expected: "Read permission denied" error, guidance to use persistent volumes. + 4. Custom Port Without Nginx Config Mount: Simulates setting custom LISTEN_ADDR/PORT + without mounting nginx config. Container starts but uses default address. + Expected: Container starts but uses default address, warning about missing config mount. - Check scripts: check-storage.sh, check-storage-extra.sh - Sample message: "⚠️ ATTENTION: /app/config is not a persistent mount. Your data in this directory..." + Check script: check-nginx-config.sh + Sample messages: "⚠️ ATTENTION: Nginx configuration mount /services/config/nginx/conf.active is missing." + "⚠️ ATTENTION: Unable to write to /services/config/nginx/conf.active/netalertx.conf." + + TODO: Custom ports can only be assigned when we have the PORT=something, and in that case + the /config.active partition shows up in the messages. It SHOULD exit if port is specified + and not writeable and I'm not sure it will. + + RESOLVED: When PORT is specified but nginx config is not writable, the container warns + "Unable to write to /services/config/nginx/conf.active/netalertx.conf" but does NOT exit. + It continues with startup and fails later for other reasons if any directories are not writable. """ - paths = _setup_mount_tree(tmp_path, "tmpfs_config") - volumes = _build_volume_args(paths, skip={"app_config"}) - extra = ["--mount", "type=tmpfs,destination=/app/config"] - result = _run_container( - "tmpfs-config", - volumes, - extra_args=extra, - ) - _assert_contains(result, "not a persistent mount.", result.args) - _assert_contains(result, "/app/config", result.args) + paths = _setup_mount_tree(tmp_path, "custom_port_ro_conf") + # Ensure other directories are writable so container gets to nginx config check + for key in ["app_db", "app_config", "app_log", "app_api", "services_run"]: + paths[key].chmod(0o777) + paths["nginx_conf"].chmod(0o500) + volumes = _build_volume_args(paths) + try: + result = _run_container( + "custom-port-ro-conf", + volumes, + env={"PORT": "24444", "LISTEN_ADDR": "127.0.0.1"}, + user="20211:20211", + sleep_seconds=5, + ) + _assert_contains(result, "Unable to write to", result.args) + _assert_contains(result, "/services/config/nginx/conf.active/netalertx.conf", result.args) + # TODO: Should this exit when PORT is specified but nginx config is not writable? + # Currently it just warns and continues + assert result.returncode != 0 + finally: + paths["nginx_conf"].chmod(0o755) +def test_zero_permissions_app_db_dir(tmp_path: pathlib.Path) -> None: + """Test zero permissions - simulates mounting directories/files with no permissions. - -def test_tmpfs_db_mount_warns(tmp_path: pathlib.Path) -> None: - """Test tmpfs instead of volumes - simulates using tmpfs for persistent data. - - 11. Tmpfs Instead of Volumes: Simulates using tmpfs mounts instead of persistent volumes - (data loss on restart). Tests config and db directories mounted as tmpfs. - Expected: "Read permission denied" error, guidance to use persistent volumes. + 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). + Tests directories and files with no read/write/execute permissions. + Expected: Mounts table shows ❌ for writeable status, configuration issues detected. """ - paths = _setup_mount_tree(tmp_path, "tmpfs_db") - volumes = _build_volume_args(paths, skip={"app_db"}) - extra = ["--mount", "type=tmpfs,destination=/app/db"] + paths = _setup_mount_tree(tmp_path, "chmod_app_db") + _setup_zero_perm_dir(paths, "app_db") + volumes = _build_volume_args(paths) + try: + result = _run_container("chmod-app-db", volumes, user="20211:20211") + # Check that the mounts table shows the app_db directory as not writeable + _assert_contains(result, "/app/db | ❌ |", result.args) + # Check that configuration issues are detected + _assert_contains(result, "Configuration issues detected", result.args) + assert result.returncode != 0 + finally: + _restore_zero_perm_dir(paths, "app_db") + + +def test_zero_permissions_app_config_dir(tmp_path: pathlib.Path) -> None: + """Test zero permissions - simulates mounting directories/files with no permissions. + + 2. Zero Permissions: Simulates mounting directories/files with no permissions (chmod 000). + Tests directories and files with no read/write/execute permissions. + Expected: Mounts table shows ❌ for writeable status, configuration issues detected. + """ + paths = _setup_mount_tree(tmp_path, "chmod_app_config") + _setup_zero_perm_dir(paths, "app_config") + volumes = _build_volume_args(paths) + try: + result = _run_container("chmod-app-config", volumes, user="20211:20211") + # Check that the mounts table shows the app_config directory as not writeable + _assert_contains(result, "/app/config | ❌ |", result.args) + # Check that configuration issues are detected + _assert_contains(result, "Configuration issues detected", result.args) + assert result.returncode != 0 + finally: + _restore_zero_perm_dir(paths, "app_config") + + +def test_mandatory_folders_creation(tmp_path: pathlib.Path) -> None: + """Test mandatory folders creation - simulates missing plugins log directory. + + 1. Mandatory Folders: Simulates missing required directories and log files. + Container automatically creates plugins log, system services run log/tmp directories, + and required log files on startup. + Expected: Automatic creation of all required directories and files. + + Check script: 25-mandatory-folders.sh + Sample message: "Creating Plugins log" + """ + paths = _setup_mount_tree(tmp_path, "mandatory_folders") + # Remove the plugins log directory to simulate missing mandatory folder + plugins_log_dir = paths["app_log"] / "plugins" + if plugins_log_dir.exists(): + shutil.rmtree(plugins_log_dir) + + # Ensure other directories are writable and owned by netalertx user so container gets past mounts.py + for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]: + paths[key].chmod(0o777) + _chown_netalertx(paths[key]) # Ensure all directories are owned by netalertx + + volumes = _build_volume_args(paths) + result = _run_container("mandatory-folders", volumes, user="20211:20211", sleep_seconds=5) + _assert_contains(result, "Creating Plugins log", result.args) + # The container will fail at writable config due to permission issues, but we just want to verify + # that mandatory folders creation ran successfully + + +def test_writable_config_validation(tmp_path: pathlib.Path) -> None: + """Test writable config validation - simulates read-only config file. + + 3. Writable Config Validation: Simulates config file with read-only permissions. + Container verifies it can read from and write to critical config and database files. + Expected: "Read permission denied" warning for config file. + + Check script: 30-writable-config.sh + Sample message: "Read permission denied" + """ + paths = _setup_mount_tree(tmp_path, "writable_config") + # Make config file read-only but keep directories writable so container gets past mounts.py + config_file = paths["app_config"] / "app.conf" + config_file.chmod(0o400) # Read-only for owner + + # Ensure directories are writable and owned by netalertx user so container gets past mounts.py + for key in ["app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"]: + paths[key].chmod(0o777) + _chown_netalertx(paths[key]) + + volumes = _build_volume_args(paths) + result = _run_container("writable-config", volumes, user="20211:20211", sleep_seconds=5.0) + _assert_contains(result, "Read permission denied", result.args) + + +def test_excessive_capabilities_warning(tmp_path: pathlib.Path) -> None: + """Test excessive capabilities detection - simulates container with extra capabilities. + + 11. Excessive Capabilities: Simulates container with capabilities beyond the required + NET_ADMIN, NET_RAW, and NET_BIND_SERVICE. + Expected: Warning about excessive capabilities detected. + + Check script: 90-excessive-capabilities.sh + Sample message: "Excessive capabilities detected" + """ + paths = _setup_mount_tree(tmp_path, "excessive_caps") + volumes = _build_volume_args(paths) + # Add excessive capabilities beyond the required ones result = _run_container( - "tmpfs-db", + "excessive-caps", volumes, - extra_args=extra, + extra_args=["--cap-add=SYS_ADMIN", "--cap-add=NET_BROADCAST"], + sleep_seconds=5, ) - _assert_contains(result, "not a persistent mount.", result.args) - _assert_contains(result, "/app/db", result.args) + _assert_contains(result, "Excessive capabilities detected", result.args) + _assert_contains(result, "bounding caps:", result.args) + # This warning doesn't cause failure by itself, but other issues might +def test_appliance_integrity_read_write_mode(tmp_path: pathlib.Path) -> None: + """Test appliance integrity - simulates running with read-write root filesystem. + + 12. Appliance Integrity: Simulates running container with read-write root filesystem + instead of read-only mode. + Expected: Warning about running in read-write mode instead of read-only. + + Check script: 95-appliance-integrity.sh + Sample message: "Container is running as read-write, not in read-only mode" + """ + paths = _setup_mount_tree(tmp_path, "appliance_integrity") + volumes = _build_volume_args(paths) + # Container runs read-write by default (not mounting root as read-only) + result = _run_container("appliance-integrity", volumes, sleep_seconds=5) + _assert_contains(result, "Container is running as read-write, not in read-only mode", result.args) + _assert_contains(result, "read-only: true", result.args) + # This warning doesn't cause failure by itself, but other issues might + + +def test_mount_analysis_ram_disk_performance(tmp_path: pathlib.Path) -> None: + """Test mount analysis for RAM disk performance issues. + + Tests 10-mounts.py detection of persistent paths on RAM disks (tmpfs) which can cause + performance issues and data loss on container restart. + Expected: Mounts table shows ❌ for RAMDisk on persistent paths, performance warnings. + + Check script: 10-mounts.py + Sample message: "Configuration issues detected" + """ + paths = _setup_mount_tree(tmp_path, "ram_disk_mount") + # Mount persistent paths (db, config) on tmpfs to simulate RAM disk + volumes = [ + (str(paths["app_log"]), "/app/log", False), + (str(paths["app_api"]), "/app/api", False), + (str(paths["services_run"]), "/services/run", False), + (str(paths["nginx_conf"]), "/services/config/nginx/conf.active", False), + ] + # Use tmpfs mounts for persistent paths with proper permissions + extra_args = ["--tmpfs", "/app/db:uid=20211,gid=20211,mode=755", "--tmpfs", "/app/config:uid=20211,gid=20211,mode=755"] + result = _run_container("ram-disk-mount", volumes=volumes, extra_args=extra_args, user="20211:20211") + # Check that mounts table shows RAM disk detection for persistent paths + _assert_contains(result, "/app/db | ✅ | ✅ | ❌ | ➖ | ❌", result.args) + _assert_contains(result, "/app/config | ✅ | ✅ | ❌ | ➖ | ❌", result.args) + # Check that configuration issues are detected due to dataloss risk + _assert_contains(result, "Configuration issues detected", result.args) assert result.returncode != 0 + + +def test_mount_analysis_dataloss_risk(tmp_path: pathlib.Path) -> None: + """Test mount analysis for dataloss risk on non-persistent filesystems. + + Tests 10-mounts.py detection when persistent database/config paths are + mounted on non-persistent filesystems (tmpfs, ramfs). + Expected: Mounts table shows dataloss risk warnings for persistent paths on tmpfs. + + Check script: 10-mounts.py + Sample message: "Configuration issues detected" + """ + paths = _setup_mount_tree(tmp_path, "dataloss_risk") + # Mount persistent paths (db, config) on tmpfs to simulate non-persistent storage + volumes = [ + (str(paths["app_log"]), "/app/log", False), + (str(paths["app_api"]), "/app/api", False), + (str(paths["services_run"]), "/services/run", False), + (str(paths["nginx_conf"]), "/services/config/nginx/conf.active", False), + ] + # Use tmpfs mounts for persistent paths with proper permissions + extra_args = ["--tmpfs", "/app/db:uid=20211,gid=20211,mode=755", "--tmpfs", "/app/config:uid=20211,gid=20211,mode=755"] + result = _run_container("dataloss-risk", volumes=volumes, extra_args=extra_args, user="20211:20211") + # Check that mounts table shows dataloss risk for persistent paths on tmpfs + _assert_contains(result, "/app/db | ✅ | ✅ | ❌ | ➖ | ❌", result.args) + _assert_contains(result, "/app/config | ✅ | ✅ | ❌ | ➖ | ❌", result.args) + # Check that configuration issues are detected due to dataloss risk + _assert_contains(result, "Configuration issues detected", result.args) + assert result.returncode != 0 + + + diff --git a/test/docker_tests/test_docker_compose_scenarios.py b/test/docker_tests/test_docker_compose_scenarios.py new file mode 100644 index 00000000..6c64bb1d --- /dev/null +++ b/test/docker_tests/test_docker_compose_scenarios.py @@ -0,0 +1,440 @@ +''' +Docker Compose integration tests for NetAlertX startup scenarios. + +This set of tests requires netalertx-test image built and docker compose. +Ensure netalertx-test image is built prior to starting these tests. +''' + +import os +import pathlib +import subprocess +import time +import pytest + +IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test") + +# Path to test configurations +CONFIG_DIR = pathlib.Path(__file__).parent / "configurations" + +pytestmark = [pytest.mark.docker, pytest.mark.compose] + + +def _run_docker_compose(compose_file: pathlib.Path, project_name: str, timeout: int = 30, env_vars: dict = None) -> subprocess.CompletedProcess: + """Run docker compose up and capture output.""" + cmd = [ + "docker", "compose", + "-f", str(compose_file), + "-p", project_name, + "up", + "--abort-on-container-exit", + "--timeout", str(timeout) + ] + + env = os.environ.copy() + if env_vars: + env.update(env_vars) + + try: + result = subprocess.run( + cmd, + cwd=compose_file.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout + 10, + env=env, + check=False, + ) + except subprocess.TimeoutExpired: + # Clean up on timeout + subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"], + cwd=compose_file.parent, check=False) + raise + + # Always clean up + subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"], + cwd=compose_file.parent, check=False) + + # Combine stdout and stderr + result.output = result.stdout + result.stderr + return result + +import os +import pathlib +import subprocess +import tempfile +import time +import pytest +import yaml + +IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test") + +# Docker Compose configurations for different test scenarios +COMPOSE_CONFIGS = { + "missing_capabilities": { + "services": { + "netalertx": { + "image": IMAGE, + "network_mode": "host", + "userns_mode": "host", + "cap_drop": ["ALL"], # Drop all capabilities + "tmpfs": ["/tmp:mode=777"], + "volumes": [ + "./test_data/app_db:/app/db", + "./test_data/app_config:/app/config", + "./test_data/app_log:/app/log", + "./test_data/app_api:/app/api", + "./test_data/nginx_conf:/services/config/nginx/conf.active", + "./test_data/services_run:/services/run" + ], + "environment": { + "TZ": "UTC" + } + } + } + }, + "host_network": { + "services": { + "netalertx": { + "image": IMAGE, + "network_mode": "host", + "userns_mode": "host", + "cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"], + "tmpfs": ["/tmp:mode=777"], + "volumes": [ + "./test_data/app_db:/app/db", + "./test_data/app_config:/app/config", + "./test_data/app_log:/app/log", + "./test_data/app_api:/app/api", + "./test_data/nginx_conf:/services/config/nginx/conf.active", + "./test_data/services_run:/services/run" + ], + "environment": { + "TZ": "UTC" + } + } + } + }, + "normal_startup": { + "services": { + "netalertx": { + "image": IMAGE, + "network_mode": "host", + "userns_mode": "host", + "cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"], + "user": "20211:20211", + "tmpfs": ["/tmp:mode=777"], + "volumes": [ + "./test_data/app_db:/app/db", + "./test_data/app_config:/app/config", + "./test_data/app_log:/app/log", + "./test_data/app_api:/app/api", + "./test_data/nginx_conf:/services/config/nginx/conf.active", + "./test_data/services_run:/services/run" + ], + "environment": { + "TZ": "UTC" + } + } + } + } +} + +pytestmark = [pytest.mark.docker, pytest.mark.compose] + + +def _create_test_data_dirs(base_dir: pathlib.Path) -> None: + """Create test data directories with proper permissions.""" + dirs = ["app_db", "app_config", "app_log", "app_api", "nginx_conf", "services_run"] + for dir_name in dirs: + dir_path = base_dir / "test_data" / dir_name + dir_path.mkdir(parents=True, exist_ok=True) + dir_path.chmod(0o755) + + # Create basic config file + config_file = base_dir / "test_data" / "app_config" / "app.conf" + if not config_file.exists(): + config_file.write_text("# Test configuration\n") + + # Create basic db file + db_file = base_dir / "test_data" / "app_db" / "app.db" + if not db_file.exists(): + # Create a minimal SQLite database + import sqlite3 + conn = sqlite3.connect(str(db_file)) + conn.close() + + +def _run_docker_compose(compose_file: pathlib.Path, project_name: str, timeout: int = 30, env_vars: dict = None) -> subprocess.CompletedProcess: + """Run docker compose up and capture output.""" + cmd = [ + "docker", "compose", + "-f", str(compose_file), + "-p", project_name, + "up", + "--abort-on-container-exit", + "--timeout", str(timeout) + ] + + try: + result = subprocess.run( + cmd, + cwd=compose_file.parent, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=timeout + 10, + check=False, + ) + except subprocess.TimeoutExpired: + # Clean up on timeout + subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"], + cwd=compose_file.parent, check=False) + raise + + # Always clean up + subprocess.run(["docker", "compose", "-f", str(compose_file), "-p", project_name, "down", "-v"], + cwd=compose_file.parent, check=False) + + # Combine stdout and stderr + result.output = result.stdout + result.stderr + return result + + +def test_missing_capabilities_compose() -> None: + """Test missing required capabilities using docker compose. + + Uses docker-compose.missing-caps.yml which drops all capabilities. + Expected: "exec /bin/sh: operation not permitted" error. + """ + compose_file = CONFIG_DIR / "docker-compose.missing-caps.yml" + result = _run_docker_compose(compose_file, "netalertx-missing-caps") + + # Check for expected error + assert "exec /bin/sh: operation not permitted" in result.output + assert result.returncode != 0 + + +def test_host_network_compose(tmp_path: pathlib.Path) -> None: + """Test host networking mode using docker compose. + + Simulates running with network_mode: host. + Expected: Container starts successfully with host networking. + """ + base_dir = tmp_path / "host_network" + base_dir.mkdir() + + # Create test data directories + _create_test_data_dirs(base_dir) + + # Create compose file + compose_config = COMPOSE_CONFIGS["host_network"].copy() + compose_file = base_dir / "docker-compose.yml" + with open(compose_file, 'w') as f: + yaml.dump(compose_config, f) + + # Run docker compose + result = _run_docker_compose(compose_file, "netalertx-host-net") + + # Check that it doesn't fail with network-related errors + assert "not running with --network=host" not in result.output + # Container should start (may fail later for other reasons, but network should be OK) + + +def test_host_network_compose() -> None: + """Test host networking mode using docker compose. + + Uses docker-compose.readonly.yml with host networking. + Expected: Container starts successfully with host networking. + """ + compose_file = CONFIG_DIR / "docker-compose.readonly.yml" + result = _run_docker_compose(compose_file, "netalertx-host-net") + + # Check that it doesn't fail with network-related errors + assert "not running with --network=host" not in result.output + # Container should start (may fail later for other reasons, but network should be OK) + + +def test_custom_port_with_unwritable_nginx_config_compose() -> None: + """Test custom port configuration with unwritable nginx config using docker compose. + + Uses docker-compose.mount-test.active_config_unwritable.yml with PORT=24444. + Expected: Container shows warning about unable to write nginx config. + """ + compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.active_config_unwritable.yml" + result = _run_docker_compose(compose_file, "netalertx-custom-port", env_vars={"PORT": "24444"}) + + # Check for nginx config write failure warning + assert "Unable to write to /services/config/nginx/conf.active/netalertx.conf" in result.output + # Container should still attempt to start but may fail for other reasons + # The key is that the nginx config write warning appears + + +def test_host_network_compose(tmp_path: pathlib.Path) -> None: + """Test host networking mode using docker compose. + + Simulates running with network_mode: host. + Expected: Container starts successfully with host networking. + """ + base_dir = tmp_path / "host_network" + base_dir.mkdir() + + # Create test data directories + _create_test_data_dirs(base_dir) + + # Create compose file + compose_config = COMPOSE_CONFIGS["host_network"].copy() + compose_file = base_dir / "docker-compose.yml" + with open(compose_file, 'w') as f: + yaml.dump(compose_config, f) + + # Run docker compose + result = _run_docker_compose(compose_file, "netalertx-host-net") + + # Check that it doesn't fail with network-related errors + assert "not running with --network=host" not in result.output + # Container should start (may fail later for other reasons, but network should be OK) + + +def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None: + """Test normal startup with expected warnings using docker compose. + + Simulates proper configuration with all required settings. + Expected: Container starts and shows expected warnings with pipe characters (═). + This demonstrates what a "normal" startup looks like with warnings. + """ + base_dir = tmp_path / "normal_startup" + base_dir.mkdir() + + # Create test data directories with proper permissions + _create_test_data_dirs(base_dir) + + # Make sure directories are writable by netalertx user + for dir_name in ["app_db", "app_config", "app_log", "app_api", "nginx_conf", "services_run"]: + dir_path = base_dir / "test_data" / dir_name + dir_path.chmod(0o777) # Allow all users to write + + # Create compose file + compose_config = COMPOSE_CONFIGS["normal_startup"].copy() + compose_file = base_dir / "docker-compose.yml" + with open(compose_file, 'w') as f: + yaml.dump(compose_config, f) + + # Run docker compose + result = _run_docker_compose(compose_file, "netalertx-normal") + + # Check that expected warnings with pipe characters appear + # These are the typical warnings that appear in a "normal" startup + assert "⚠️ Warning: Excessive capabilities detected" in result.output + assert "⚠️ Warning: Container is running as read-write" in result.output + assert "═══" in result.output # Box drawing characters in warnings + + # Should not have critical permission errors (these indicate test setup issues) + assert "Write permission denied" not in result.output + assert "CRITICAL" in result.output # CRITICAL messages are expected when permissions fail + + +def test_ram_disk_mount_analysis_compose(tmp_path: pathlib.Path) -> None: + """Test mount analysis for RAM disk detection using docker compose. + + Simulates mounting persistent paths on tmpfs (RAM disk). + Expected: Mounts table shows ❌ for RAMDisk on persistent paths, dataloss warnings. + """ + base_dir = tmp_path / "ram_disk_test" + base_dir.mkdir() + + # Create test data directories + _create_test_data_dirs(base_dir) + + # Create compose file with tmpfs mounts for persistent paths + compose_config = { + "services": { + "netalertx": { + "image": IMAGE, + "network_mode": "host", + "userns_mode": "host", + "cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"], + "user": "20211:20211", + "tmpfs": [ + "/tmp:mode=777", + "/app/db", # RAM disk for persistent DB + "/app/config" # RAM disk for persistent config + ], + "volumes": [ + f"./test_data/app_log:/app/log", + f"./test_data/app_api:/app/api", + f"./test_data/nginx_conf:/services/config/nginx/conf.active", + f"./test_data/services_run:/services/run" + ], + "environment": { + "TZ": "UTC" + } + } + } + } + + compose_file = base_dir / "docker-compose.yml" + with open(compose_file, 'w') as f: + yaml.dump(compose_config, f) + + # Run docker compose + result = _run_docker_compose(compose_file, "netalertx-ram-disk") + + # Check that mounts table shows RAM disk detection and dataloss warnings + assert "Configuration issues detected" in result.output + assert "/app/db" in result.output + assert "/app/config" in result.output + assert result.returncode != 0 # Should fail due to dataloss risk + + +def test_dataloss_risk_mount_analysis_compose(tmp_path: pathlib.Path) -> None: + """Test mount analysis for dataloss risk using docker compose. + + Simulates mounting persistent paths on non-persistent tmpfs. + Expected: Mounts table shows dataloss risk warnings for persistent paths. + """ + base_dir = tmp_path / "dataloss_test" + base_dir.mkdir() + + # Create test data directories + _create_test_data_dirs(base_dir) + + # Create compose file with tmpfs for persistent data + compose_config = { + "services": { + "netalertx": { + "image": IMAGE, + "network_mode": "host", + "userns_mode": "host", + "cap_add": ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"], + "user": "20211:20211", + "tmpfs": [ + "/tmp:mode=777", + "/app/db:uid=20211,gid=20211", # Non-persistent for DB + "/app/config:uid=20211,gid=20211" # Non-persistent for config + ], + "volumes": [ + f"./test_data/app_log:/app/log", + f"./test_data/app_api:/app/api", + f"./test_data/nginx_conf:/services/config/nginx/conf.active", + f"./test_data/services_run:/services/run" + ], + "environment": { + "TZ": "UTC" + } + } + } + } + + compose_file = base_dir / "docker-compose.yml" + with open(compose_file, 'w') as f: + yaml.dump(compose_config, f) + + # Run docker compose + result = _run_docker_compose(compose_file, "netalertx-dataloss") + + # Check that mounts table shows dataloss risk detection + assert "Configuration issues detected" in result.output + assert "/app/db" in result.output + assert "/app/config" in result.output + assert result.returncode != 0 # Should fail due to dataloss risk \ No newline at end of file diff --git a/test/docker_tests/test_mount_diagnostics_pytest.py b/test/docker_tests/test_mount_diagnostics_pytest.py index 81318c56..8bb5e663 100644 --- a/test/docker_tests/test_mount_diagnostics_pytest.py +++ b/test/docker_tests/test_mount_diagnostics_pytest.py @@ -149,6 +149,7 @@ class TestScenario: is_persistent: bool docker_compose: str expected_issues: List[str] # List of expected issue types + expected_exit_code: int # Expected container exit code @pytest.fixture(scope="session") def netalertx_test_image(): @@ -209,13 +210,17 @@ def create_test_scenarios() -> List[TestScenario]: compose_file = f"docker-compose.mount-test.{path_name}_{scenario_name}.yml" + # Determine expected exit code + expected_exit_code = 1 if scenario_name == "unwritable" else 0 + scenarios.append(TestScenario( name=f"{path_name}_{scenario_name}", path_var=env_var, container_path=container_path, is_persistent=is_persistent, docker_compose=compose_file, - expected_issues=expected_issues + expected_issues=expected_issues, + expected_exit_code=expected_exit_code )) return scenarios @@ -246,7 +251,7 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario): try: # Wait for container to be ready import time - time.sleep(5) + time.sleep(3) # Check if container is still running container_name = f"netalertx-test-mount-{test_scenario.name}" @@ -256,7 +261,18 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario): ) if not result_ps.stdout.strip(): - # Container exited - this is expected for configurations with issues + # Container exited - check the exit code + result_inspect = subprocess.run( + ["docker", "inspect", container_name, "--format={{.State.ExitCode}}"], + capture_output=True, text=True + ) + actual_exit_code = int(result_inspect.stdout.strip()) + + # Assert the exit code matches expected + assert actual_exit_code == test_scenario.expected_exit_code, ( + f"Container {container_name} exited with code {actual_exit_code}, " + f"expected {test_scenario.expected_exit_code}" + ) # Check the logs to see if it detected the expected issues result_logs = subprocess.run( ["docker", "logs", container_name], diff --git a/test/docker_tests/test_ports_available.py b/test/docker_tests/test_ports_available.py new file mode 100644 index 00000000..0e7a7712 --- /dev/null +++ b/test/docker_tests/test_ports_available.py @@ -0,0 +1,240 @@ +''' +Tests for 99-ports-available.sh entrypoint script. +This script checks for port conflicts and availability. +''' + +import os +import pathlib +import subprocess +import time +import pytest + +IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test") +GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2")) + +VOLUME_MAP = { + "app_db": "/app/db", + "app_config": "/app/config", + "app_log": "/app/log", + "app_api": "/app/api", + "nginx_conf": "/services/config/nginx/conf.active", + "services_run": "/services/run", +} + +pytestmark = [pytest.mark.docker, pytest.mark.feature_complete] + + +@pytest.fixture(scope="function") +def dummy_container(tmp_path): + """Fixture that starts a dummy container to occupy ports for testing.""" + # Create a simple docker-compose file for the dummy container + compose_file = tmp_path / "docker-compose-dummy.yml" + with open(compose_file, 'w') as f: + f.write("version: '3.8'\n") + f.write("services:\n") + f.write(" dummy:\n") + f.write(" image: alpine:latest\n") + f.write(" network_mode: host\n") + f.write(" userns_mode: host\n") + f.write(" command: sh -c \"while true; do nc -l -p 20211 < /dev/null > /dev/null; done & while true; do nc -l -p 20212 < /dev/null > /dev/null; done & sleep 30\"\n") + + # Start the dummy container + import subprocess + result = subprocess.run( + ["docker-compose", "-f", str(compose_file), "up", "-d"], + capture_output=True, text=True + ) + if result.returncode != 0: + pytest.fail(f"Failed to start dummy container: {result.stderr}") + + # Wait a bit for the container to start listening + time.sleep(3) + + yield "dummy" + + # Cleanup + subprocess.run(["docker-compose", "-f", str(compose_file), "down"], capture_output=True) + + +def _setup_mount_tree(tmp_path: pathlib.Path, label: str) -> dict[str, pathlib.Path]: + """Set up mount tree for testing.""" + import uuid + import shutil + + base = tmp_path / f"{label}_mount_root" + if base.exists(): + shutil.rmtree(base) + base.mkdir(parents=True) + + paths = {} + for key, target in VOLUME_MAP.items(): + folder_name = f"{label}_{key.upper()}_INTENTIONAL_NETALERTX_TEST" + host_path = base / folder_name + host_path.mkdir(parents=True, exist_ok=True) + host_path.chmod(0o777) + paths[key] = host_path + + return paths + + +def _build_volume_args(paths: dict[str, pathlib.Path]) -> list[tuple[str, str, bool]]: + """Build volume arguments for docker run.""" + bindings = [] + for key, target in VOLUME_MAP.items(): + bindings.append((str(paths[key]), target, False)) + return bindings + + +def _run_container( + label: str, + volumes: list[tuple[str, str, bool]] | None = None, + *, + env: dict[str, str] | None = None, + user: str | None = None, + network_mode: str | None = "host", + extra_args: list[str] | None = None, +) -> subprocess.CompletedProcess[str]: + """Run a container and return the result.""" + import uuid + import re + + name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower() + cmd = ["docker", "run", "--rm", "--name", name] + + if network_mode: + cmd.extend(["--network", network_mode]) + cmd.extend(["--userns", "host"]) + cmd.extend(["--tmpfs", "/tmp:mode=777"]) + if user: + cmd.extend(["--user", user]) + if env: + for key, value in env.items(): + cmd.extend(["-e", f"{key}={value}"]) + if extra_args: + cmd.extend(extra_args) + for host_path, target, readonly in volumes or []: + mount = f"{host_path}:{target}" + if readonly: + mount += ":ro" + cmd.extend(["-v", mount]) + + # Copy the script content and run it + script_path = "/workspaces/NetAlertX/install/production-filesystem/entrypoint.d/99-ports-available.sh" + with open(script_path, 'r') as f: + script_content = f.read() + + # Use printf to avoid shell interpretation issues + script = f"printf '%s\\n' '{script_content.replace(chr(39), chr(39)+chr(92)+chr(39)+chr(39))}' > /tmp/ports-check.sh && chmod +x /tmp/ports-check.sh && sh /tmp/ports-check.sh" + cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script]) + + print(f"\n--- DOCKER CMD ---\n{' '.join(cmd)}\n--- END CMD ---\n") + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=30, + check=False, + ) + + # Combine and clean stdout and stderr + stdouterr = ( + re.sub(r'\x1b\[[0-9;]*m', '', result.stdout or '') + + re.sub(r'\x1b\[[0-9;]*m', '', result.stderr or '') + ) + result.output = stdouterr + print(f"\n--- CONTAINER stdout ---\n{result.stdout}") + print(f"\n--- CONTAINER stderr ---\n{result.stderr}") + print(f"\n--- CONTAINER combined ---\n{result.output}") + + return result + + +def _assert_contains(result, snippet: str, cmd: list[str] = None) -> None: + """Assert that the result output contains the given snippet.""" + if snippet not in result.output: + cmd_str = " ".join(cmd) if cmd else "" + raise AssertionError( + f"Expected to find '{snippet}' in container output.\n" + f"Got:\n{result.output}\n" + f"Container command:\n{cmd_str}" + ) + + +def _assert_not_contains(result, snippet: str, cmd: list[str] = None) -> None: + """Assert that the result output does not contain the given snippet.""" + if snippet in result.output: + cmd_str = " ".join(cmd) if cmd else "" + raise AssertionError( + f"Expected NOT to find '{snippet}' in container output.\n" + f"Got:\n{result.output}\n" + f"Container command:\n{cmd_str}" + ) + + +def test_ports_available_normal_case(tmp_path: pathlib.Path) -> None: + """Test ports available script with default ports (should pass without warnings). + + 99. Ports Available Check: Tests that the script runs without warnings + when ports 20211 and 20212 are available and not conflicting. + Expected: No warnings about port conflicts or ports in use. + + Check script: 99-ports-available.sh + """ + paths = _setup_mount_tree(tmp_path, "ports_normal") + volumes = _build_volume_args(paths) + result = _run_container("ports-normal", volumes, user="20211:20211", env={"PORT": "99991", "GRAPHQL_PORT": "99992"}) + + # Should not contain any port warnings + _assert_not_contains(result, "Configuration Warning: Both ports are set to") + _assert_not_contains(result, "Port Warning: Application port") + _assert_not_contains(result, "Port Warning: GraphQL API port") + assert result.returncode == 0 + + +def test_ports_conflict_same_number(tmp_path: pathlib.Path) -> None: + """Test ports available script when both ports are set to the same number. + + 99. Ports Available Check: Tests warning when PORT and GRAPHQL_PORT + are configured to the same value. + Expected: Warning about port conflict. + + Check script: 99-ports-available.sh + """ + paths = _setup_mount_tree(tmp_path, "ports_conflict") + volumes = _build_volume_args(paths) + result = _run_container( + "ports-conflict", + volumes, + user="20211:20211", + env={"PORT": "20211", "GRAPHQL_PORT": "20211"} + ) + + _assert_contains(result, "Configuration Warning: Both ports are set to 20211") + _assert_contains(result, "The Application port ($PORT) and the GraphQL API port") + _assert_contains(result, "are configured to use the") + _assert_contains(result, "same port. This will cause a conflict.") + assert result.returncode == 0 + + +def test_ports_in_use_warning(dummy_container, tmp_path: pathlib.Path) -> None: + """Test ports available script when ports are already in use. + + 99. Ports Available Check: Tests warning when configured ports + are already bound by another process. + Expected: Warning about ports being in use. + + Check script: 99-ports-available.sh + """ + paths = _setup_mount_tree(tmp_path, "ports_in_use") + volumes = _build_volume_args(paths) + result = _run_container( + "ports-in-use", + volumes, + user="20211:20211", + env={"PORT": "20211", "GRAPHQL_PORT": "20212"} + ) + + _assert_contains(result, "Port Warning: Application port 20211 is already in use") + _assert_contains(result, "Port Warning: GraphQL API port 20212 is already in use") + assert result.returncode == 0 \ No newline at end of file