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

@@ -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