Add unit tests and updated messages

This commit is contained in:
Adam Outler
2025-10-23 21:15:15 +00:00
parent 356cacab2b
commit 3b7830b922
15 changed files with 1052 additions and 61 deletions

View File

@@ -1,39 +0,0 @@
# Alpine Docker tests
This is intended to be run as Root user as permissions are altered. It will create and analyze the results of various configurations on containers. The test craeates a container, logs the results, terminates the container, then starts the next test
0. No errors on startup
1. missing config/db generation
2. After config/db generation
1. root user mount on
1. /app/db
2. /app/config
3. /app/log
4. /app/api
5. /services/config/nginx/conf.active
6. /services/run/
2. 000 permissions on
1. /app/db
2. /app/db/app.db
3. /app/config
4. /app/config/app.conf
5. /app/log
6. /app/api
7. /services/config/nginx/conf.active
8. /services/run/
3. Container read-only missing mounts
1. /app/db
2. /app/config
3. /app/log
4. /app/api
5. /services/config/nginx/conf.active
6. /services/run/
4. Custom port/listen address without /services/config/nginx/conf.active mount
5. Missing cap NET_ADMIN, NET_RAW, NET_BIND_SERVICE
6. Run as Root user
7. Run as user 1000
8. Run without network_mode host
9. Missing /app/config/app.conf
10. Missing /app/db/app.db
11. Ramdisk mounted on
1. /app/config
2. /app/db

View File

@@ -0,0 +1,951 @@
import os
import pathlib
import shutil
import subprocess
import uuid
import pytest
#TODO: test ALWAYS_FRESH_INSTALL
#TODO: test new named volume mount
IMAGE = os.environ.get("NETALERTX_TEST_IMAGE", "netalertx-test")
GRACE_SECONDS = float(os.environ.get("NETALERTX_TEST_GRACE", "2"))
DEFAULT_CAPS = ["NET_RAW", "NET_ADMIN", "NET_BIND_SERVICE"]
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]
def _unique_label(prefix: str) -> str:
return f"{prefix.upper()}__NETALERTX_INTENTIONAL__{uuid.uuid4().hex[:6]}"
def _create_docker_volume(prefix: str) -> str:
name = f"netalertx-test-{prefix}-{uuid.uuid4().hex[:8]}".lower()
subprocess.run(
["docker", "volume", "create", name],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return name
def _remove_docker_volume(name: str) -> None:
subprocess.run(
["docker", "volume", "rm", "-f", name],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def _chown_path(host_path: pathlib.Path, uid: int, gid: int) -> None:
"""Chown a host path using the test image with host user namespace."""
if not host_path.exists():
raise RuntimeError(f"Cannot chown missing path {host_path}")
cmd = [
"docker",
"run",
"--rm",
"--userns",
"host",
"--user",
"0:0",
"--entrypoint",
"/bin/chown",
"-v",
f"{host_path}:/mnt",
IMAGE,
"-R",
f"{uid}:{gid}",
"/mnt",
]
try:
subprocess.run(
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(f"Failed to chown {host_path} to {uid}:{gid}") from exc
def _setup_mount_tree(tmp_path: pathlib.Path, prefix: str, seed_config: bool = True, seed_db: bool = True) -> dict[str, pathlib.Path]:
label = _unique_label(prefix)
base = tmp_path / f"{label}_MOUNT_ROOT"
base.mkdir()
paths: dict[str, pathlib.Path] = {}
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)
# Make the directory writable so the container (running as UID 20211)
# can create files on first run even if the host owner differs.
try:
host_path.chmod(0o777)
except PermissionError:
# If we can't chmod (uncommon in CI), tests that require strict
# ownership will still run their own chown/chmod operations.
pass
paths[key] = host_path
if seed_config:
config_file = paths["app_config"] / "app.conf"
shutil.copyfile(
"/workspaces/NetAlertX/back/app.conf",
config_file,
)
config_file.chmod(0o600)
if seed_db:
db_file = paths["app_db"] / "app.db"
shutil.copyfile(
"/workspaces/NetAlertX/db/app.db",
db_file,
)
db_file.chmod(0o600)
_chown_netalertx(base)
return paths
def _setup_fixed_mount_tree(base: pathlib.Path) -> dict[str, pathlib.Path]:
if base.exists():
shutil.rmtree(base)
base.mkdir(parents=True)
paths: dict[str, pathlib.Path] = {}
for key in VOLUME_MAP:
host_path = base / f"{key.upper()}_NETALERTX_TEST"
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],
read_only: set[str] | None = None,
skip: set[str] | None = None,
) -> list[tuple[str, str, bool]]:
bindings: list[tuple[str, str, bool]] = []
for key, target in VOLUME_MAP.items():
if skip and key in skip:
continue
bindings.append((str(paths[key]), target, key in read_only if read_only else False))
return bindings
def _chown_root(host_path: pathlib.Path) -> None:
_chown_path(host_path, 0, 0)
def _chown_netalertx(host_path: pathlib.Path) -> None:
_chown_path(host_path, 20211, 20211)
def _run_container(
label: str,
volumes: list[tuple[str, str, bool]] | None = None,
*,
env: dict[str, str] | None = None,
user: str | None = None,
drop_caps: list[str] | None = None,
network_mode: str | None = "host",
extra_args: list[str] | None = None,
volume_specs: list[str] | None = None,
sleep_seconds: float = GRACE_SECONDS,
) -> subprocess.CompletedProcess[str]:
name = f"netalertx-test-{label}-{uuid.uuid4().hex[:8]}".lower()
cmd: list[str] = ["docker", "run", "--rm", "--name", name]
if network_mode:
cmd.extend(["--network", network_mode])
cmd.extend(["--userns", "host"])
if user:
cmd.extend(["--user", user])
if drop_caps:
for cap in drop_caps:
cmd.extend(["--cap-drop", cap])
else:
for cap in DEFAULT_CAPS:
cmd.extend(["--cap-add", cap])
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])
if volume_specs:
for spec in volume_specs:
cmd.extend(["-v", spec])
# Diagnostic wrapper: list ownership and perms of mounted targets inside
# the container before running the real entrypoint. This helps debug
# permission failures by capturing the container's view of the host mounts.
mounts_ls = """
echo "--- MOUNT PERMS (container view) ---";
ls -ldn \
"""
for _, target, _ in volumes or []:
mounts_ls += f" {target}"
mounts_ls += " || true; echo '--- END MOUNTS ---'; \n"
script = (
mounts_ls
+ f"sh /entrypoint.sh & pid=$!; "
+ f"sleep {sleep_seconds}; "
+ "if kill -0 $pid >/dev/null 2>&1; then kill -TERM $pid >/dev/null 2>&1 || true; fi; "
+ "wait $pid; code=$?; if [ $code -eq 143 ]; then exit 0; fi; exit $code"
)
cmd.extend(["--entrypoint", "/bin/sh", IMAGE, "-c", script])
return subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=sleep_seconds + 30,
check=False,
)
def _assert_contains(output: str, snippet: str) -> None:
import re
stripped = re.sub(r'\x1b\[[0-9;]*m', '', output)
assert snippet in stripped, f"Expected to find '{snippet}' in container output.\nGot:\n{stripped}"
def _setup_zero_perm_dir(paths: dict[str, pathlib.Path], key: str) -> None:
"""Set up a directory with files and zero permissions for testing."""
if key in ["app_db", "app_config"]:
# Files already exist from _setup_mount_tree seeding
pass
else:
# Create a dummy file for other directories
(paths[key] / "dummy.txt").write_text("dummy")
# Chmod all files in the directory to 000
for f in paths[key].iterdir():
f.chmod(0)
# Chmod the directory itself to 000
paths[key].chmod(0)
def _restore_zero_perm_dir(paths: dict[str, pathlib.Path], key: str) -> None:
"""Restore permissions after zero perm test."""
# Chmod directory back to 700
paths[key].chmod(0o700)
# Chmod files back to appropriate permissions
for f in paths[key].iterdir():
if f.name in ["app.db", "app.conf"]:
f.chmod(0o600)
else:
f.chmod(0o644)
def test_first_run_creates_config_and_db(tmp_path: pathlib.Path) -> None:
"""Test that containers start successfully with proper configuration.
0.1 Missing config/db generation: First run creates default app.conf and app.db
This test validates that on the first run with empty mount directories,
the container automatically generates default configuration and database files.
"""
paths = _setup_mount_tree(tmp_path, "first_run_missing", seed_config=False, seed_db=False)
volumes = _build_volume_args(paths)
# In some CI/devcontainer environments the bind mounts are visible as
# root-owned inside the container due to user namespace or mount behaviour.
# Allow the container to run as root for the initial-seed test so it can
# write default config and build the DB. This keeps the test stable.
result = _run_container("first-run-missing", volumes, user="0:0")
_assert_contains(result.stdout, "Default configuration written to")
_assert_contains(result.stdout, "Building initial database schema")
assert result.returncode == 0
def test_second_run_starts_clean() -> None:
"""Test that containers start successfully with proper configuration.
0.2 After config/db generation: Subsequent runs start cleanly with existing files
This test validates that after initial configuration and database files exist,
the container starts cleanly without regenerating defaults.
"""
base = pathlib.Path("/tmp/NETALERTX_SECOND_RUN_CLEAN_TEST_MOUNT_INTENTIONAL")
paths = _setup_fixed_mount_tree(base)
volumes = _build_volume_args(paths)
try:
shutil.copyfile("/workspaces/NetAlertX/back/app.conf", paths["app_config"] / "app.conf")
shutil.copyfile("/workspaces/NetAlertX/db/app.db", paths["app_db"] / "app.db")
(paths["app_config"] / "app.conf").chmod(0o600)
(paths["app_db"] / "app.db").chmod(0o600)
second = _run_container("second-run", volumes, user="0:0", sleep_seconds=3)
assert "Default configuration written" not in second.stdout
assert "Building initial database schema" not in second.stdout
finally:
shutil.rmtree(base, ignore_errors=True)
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_db"]))
assert result.returncode != 0
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_config"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_log"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_api"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["nginx_conf"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["services_run"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_db"]))
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.stdout, "Write permission denied")
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_config"]))
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.stdout, "Write permission denied")
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_log"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_api"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["services_run"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_db"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_config"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_log"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["app_api"]))
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")
_setup_zero_perm_dir(paths, "nginx_conf")
volumes = _build_volume_args(paths)
try:
result = _run_container("readonly-nginx-conf", volumes)
_assert_contains(result.stdout, "Write permission denied")
_assert_contains(result.stdout, "/services/config/nginx/conf.active")
assert result.returncode != 0
finally:
_restore_zero_perm_dir(paths, "nginx_conf")
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.stdout, "Write permission denied")
_assert_contains(result.stdout, str(VOLUME_MAP["services_run"]))
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/services/config/nginx/conf.active")
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.
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.
Check scripts: check-storage.sh, check-storage-extra.sh
Sample message: "⚠️ ATTENTION: /app/db is not a persistent mount. Your data in this directory..."
"""
paths = _setup_mount_tree(tmp_path, "missing_mount_app_db")
volumes = _build_volume_args(paths, skip={"app_db"})
result = _run_container("missing-mount-app-db", volumes, user="20211:20211")
_assert_contains(result.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/api")
assert result.returncode != 0
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/api")
assert result.returncode != 0
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/api")
assert result.returncode != 0
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/config")
assert result.returncode != 0
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/api")
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.stdout, "Write permission denied")
_assert_contains(result.stdout, "/app/api")
assert result.returncode != 0
def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None:
"""Test missing required capabilities - simulates insufficient container privileges.
5. Missing Required Capabilities: Simulates running without NET_ADMIN, NET_RAW,
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..."
"""
paths = _setup_mount_tree(tmp_path, "missing_caps")
volumes = _build_volume_args(paths)
result = _run_container(
"missing-caps",
volumes,
drop_caps=["ALL"],
)
_assert_contains(result.stdout, "exec /bin/sh: operation not permitted")
assert result.returncode != 0
def test_running_as_root_is_blocked(tmp_path: pathlib.Path) -> None:
"""Test running as root user - simulates insecure container execution.
6. Running as Root User: Simulates running container as root (UID 0) instead of
dedicated netalertx user. Warning about security risks, special permission fix mode.
Expected: Warning about security risks, guidance to use UID 20211.
Check script: check-root.sh
Sample message: "⚠️ ATTENTION: NetAlertX is running as root (UID 0). This defeats every hardening..."
"""
paths = _setup_mount_tree(tmp_path, "run_as_root")
volumes = _build_volume_args(paths)
result = _run_container(
"run-as-root",
volumes,
user="0:0",
)
_assert_contains(result.stdout, "NetAlertX is running as root")
assert result.returncode == 0
def test_running_as_uid_1000_warns(tmp_path: pathlib.Path) -> None:
"""Test running as wrong user - simulates using arbitrary user instead of netalertx.
7. Running as Wrong User: Simulates running as arbitrary user (UID 1000) instead
of netalertx user. Permission errors due to incorrect user context.
Expected: Permission errors, guidance to use correct user.
Check script: check-user-netalertx.sh
Sample message: "⚠️ ATTENTION: NetAlertX is running as UID 1000:1000. Hardened permissions..."
"""
paths = _setup_mount_tree(tmp_path, "run_as_1000")
volumes = _build_volume_args(paths)
result = _run_container(
"run-as-1000",
volumes,
user="1000:1000",
)
assert result.returncode != 0
def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
"""Test missing host networking - simulates running without host network mode.
8. Missing Host Networking: Simulates running without network_mode: host.
Limits ARP scanning capabilities for network discovery.
Expected: Warning about ARP scanning limitations, guidance to use host networking.
Check script: check-network-mode.sh
Sample message: "⚠️ ATTENTION: NetAlertX is not running with --network=host. Bridge networking..."
"""
paths = _setup_mount_tree(tmp_path, "missing_host_net")
volumes = _build_volume_args(paths)
result = _run_container(
"missing-host-network",
volumes,
network_mode=None,
)
assert result.returncode != 0
def test_missing_app_conf_triggers_seed(tmp_path: pathlib.Path) -> None:
"""Test missing configuration file seeding - simulates corrupted/missing app.conf.
9. Missing Configuration File: Simulates corrupted/missing app.conf.
Container automatically regenerates default configuration on startup.
Expected: Automatic regeneration of default configuration.
"""
paths = _setup_mount_tree(tmp_path, "missing_app_conf")
(paths["app_config"] / "app.conf").unlink()
volumes = _build_volume_args(paths)
result = _run_container("missing-app-conf", volumes, user="0:0")
_assert_contains(result.stdout, "Default configuration written to")
assert result.returncode == 0
def test_missing_app_db_triggers_seed(tmp_path: pathlib.Path) -> None:
"""Test missing database file seeding - simulates corrupted/missing app.db.
10. Missing Database File: Simulates corrupted/missing app.db.
Container automatically creates initial database schema on startup.
Expected: Automatic creation of initial database schema.
"""
paths = _setup_mount_tree(tmp_path, "missing_app_db")
(paths["app_db"] / "app.db").unlink()
volumes = _build_volume_args(paths)
result = _run_container("missing-app-db", volumes, user="0:0")
_assert_contains(result.stdout, "Building initial database schema")
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.
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.
Check scripts: check-storage.sh, check-storage-extra.sh
Sample message: "⚠️ ATTENTION: /app/config is not a persistent mount. Your data in this directory..."
"""
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.stdout, "Read permission denied")
_assert_contains(result.stdout, "/app/config")
assert result.returncode != 0
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.
"""
paths = _setup_mount_tree(tmp_path, "tmpfs_db")
volumes = _build_volume_args(paths, skip={"app_db"})
extra = ["--mount", "type=tmpfs,destination=/app/db"]
result = _run_container(
"tmpfs-db",
volumes,
extra_args=extra,
)
_assert_contains(result.stdout, "Read permission denied")
_assert_contains(result.stdout, "/app/db")
assert result.returncode != 0