mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-03-30 23:03:03 -07:00
Handle more edge cases; more clear warnings
This commit is contained in:
@@ -14,6 +14,8 @@ services:
|
||||
- ALL
|
||||
cap_add:
|
||||
- CHOWN
|
||||
- SETGID
|
||||
- SETUID
|
||||
- NET_ADMIN
|
||||
- NET_RAW
|
||||
- NET_BIND_SERVICE
|
||||
@@ -31,12 +33,31 @@ services:
|
||||
source: test_netalertx_data
|
||||
target: /data
|
||||
read_only: false
|
||||
- type: tmpfs
|
||||
target: /tmp/log
|
||||
tmpfs:
|
||||
size: 64m
|
||||
mode: 1777
|
||||
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||
- type: tmpfs
|
||||
target: /tmp/api
|
||||
tmpfs:
|
||||
size: 64m
|
||||
mode: 1777
|
||||
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||
- type: tmpfs
|
||||
target: /tmp/run
|
||||
tmpfs:
|
||||
size: 64m
|
||||
mode: 1777
|
||||
options: noexec,nosuid,nodev,async,noatime,nodiratime
|
||||
- type: volume
|
||||
source: test_system_services_active_config
|
||||
target: /tmp/nginx/active-config
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
# Ensure /tmp is a writable tmpfs for the app user; mode 1777 to support su-exec drop.
|
||||
- /tmp:uid=20211,gid=20211,mode=1777,noexec,nosuid,nodev,size=64m
|
||||
volumes:
|
||||
test_netalertx_data:
|
||||
test_system_services_active_config:
|
||||
@@ -8,7 +8,6 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
image: netalertx-test
|
||||
container_name: netalertx-test-mount-data_noread
|
||||
user: "20211:20211"
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
@@ -38,7 +37,7 @@ services:
|
||||
read_only: false
|
||||
|
||||
tmpfs:
|
||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
- "/tmp:mode=1777,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
|
||||
volumes:
|
||||
test_netalertx_data:
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
read_only: false
|
||||
|
||||
tmpfs:
|
||||
- "/tmp:mode=1700,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
- "/tmp:mode=1700,uid=20211,gid=20211,rw,noexec,nosuid,nodev,async,noatime,nodiratime"
|
||||
|
||||
volumes:
|
||||
test_netalertx_data:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -476,6 +476,7 @@ def test_root_then_user_20211_transition() -> None:
|
||||
"transition-root",
|
||||
volumes=None,
|
||||
volume_specs=[f"{volume}:/data"],
|
||||
env={"NETALERTX_CHECK_ONLY": "1"},
|
||||
sleep_seconds=8,
|
||||
)
|
||||
assert init_result.returncode == 0
|
||||
@@ -493,6 +494,7 @@ def test_root_then_user_20211_transition() -> None:
|
||||
)
|
||||
|
||||
combined_output = (user_result.output or "") + (user_result.stderr or "")
|
||||
print(combined_output) # DO NOT REMOVE OR MODIFY - MANDATORY LOGGING FOR DEBUGGING & CI.
|
||||
assert user_result.returncode == 0, combined_output
|
||||
assert "permission denied" not in combined_output.lower()
|
||||
assert "configuration issues detected" not in combined_output.lower()
|
||||
@@ -886,37 +888,6 @@ def test_missing_capabilities_triggers_warning(tmp_path: pathlib.Path) -> None:
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Sample message: "NetAlertX is running as ROOT"
|
||||
"""
|
||||
paths = _setup_mount_tree(tmp_path, "run_as_root")
|
||||
volumes = _build_volume_args_for_keys(paths, {"data", "nginx_conf"})
|
||||
result = _run_container(
|
||||
"run-as-root",
|
||||
volumes,
|
||||
user="0",
|
||||
)
|
||||
_assert_contains(result, "NetAlertX is running as ROOT", result.args)
|
||||
_assert_contains_any(
|
||||
result,
|
||||
[
|
||||
"Permissions fixed for read-write paths.",
|
||||
"Permissions prepared for PUID=",
|
||||
"Permissions prepared",
|
||||
],
|
||||
result.args,
|
||||
)
|
||||
assert (
|
||||
result.returncode == 0
|
||||
) # container warns but continues running, then terminated by test framework
|
||||
|
||||
|
||||
def test_missing_host_network_warns(tmp_path: pathlib.Path) -> None:
|
||||
# No output assertion, just returncode check
|
||||
"""Test missing host networking - simulates running without host network mode.
|
||||
@@ -1386,19 +1357,7 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
||||
keys = {"data", "app_db", "app_config", "app_log", "app_api", "services_run", "nginx_conf"}
|
||||
volumes = _build_volume_args_for_keys(paths, keys)
|
||||
|
||||
# Case 1: Running as non-root (default) - Should fail to write
|
||||
# We disable host network/userns to avoid potential hangs in devcontainer environment
|
||||
result = _run_container(
|
||||
"restrictive-perms-user",
|
||||
volumes,
|
||||
user="20211:20211",
|
||||
sleep_seconds=5,
|
||||
network_mode=None,
|
||||
userns_mode=None
|
||||
)
|
||||
assert result.returncode != 0 or "Permission denied" in result.output or "Unable to write" in result.output
|
||||
|
||||
# Case 2: Running as root - Should trigger the fix script
|
||||
# Run as root by default to exercise permission-fix path explicitly.
|
||||
result_root = _run_container(
|
||||
"restrictive-perms-root",
|
||||
volumes,
|
||||
@@ -1408,17 +1367,17 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
||||
userns_mode=None
|
||||
)
|
||||
|
||||
# Ensure root-based startup succeeds without permission errors before verification.
|
||||
assert result_root.returncode == 0
|
||||
assert "permission denied" not in result_root.output.lower()
|
||||
assert "unable to write" not in result_root.output.lower()
|
||||
|
||||
_assert_contains(result_root, "NetAlertX is running as ROOT", result_root.args)
|
||||
_assert_contains_any(
|
||||
result_root,
|
||||
["Permissions fixed for read-write paths", "Permissions prepared for PUID=", "Permissions prepared"],
|
||||
result_root.args,
|
||||
)
|
||||
|
||||
check_cmd = [
|
||||
"docker", "run", "--rm",
|
||||
"--entrypoint", "/bin/sh",
|
||||
"--user", "20211:20211",
|
||||
"--user", "0:0",
|
||||
IMAGE,
|
||||
"-c", "ls -ldn /data/db && touch /data/db/test_write_after_fix"
|
||||
]
|
||||
@@ -1433,6 +1392,13 @@ def test_restrictive_permissions_handling(tmp_path: pathlib.Path) -> None:
|
||||
timeout=SUBPROCESS_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
# MANDATORY LOGGING: capture the follow-up verification command output for CI debugging.
|
||||
print("\n--- PERM FIX CHECK CMD ---\n", " ".join(check_cmd), "\n--- END CHECK CMD ---\n")
|
||||
print("--- PERM FIX CHECK STDOUT ---")
|
||||
print(check_result.stdout or "<no stdout>")
|
||||
print("--- PERM FIX CHECK STDERR ---")
|
||||
print(check_result.stderr or "<no stderr>")
|
||||
|
||||
if check_result.returncode != 0:
|
||||
print(f"Check command failed. Cmd: {check_cmd}")
|
||||
print(f"Stderr: {check_result.stderr}")
|
||||
|
||||
@@ -696,29 +696,60 @@ def test_custom_port_with_unwritable_nginx_config_compose() -> None:
|
||||
compose_file = CONFIG_DIR / "mount-tests" / "docker-compose.mount-test.active_config_unwritable.yml"
|
||||
http_port = _select_custom_ports()
|
||||
graphql_port = _select_custom_ports({http_port})
|
||||
LAST_PORT_SUCCESSES.pop(http_port, None)
|
||||
project_name = "netalertx-custom-port"
|
||||
|
||||
def _wait_for_unwritable_failure() -> None:
|
||||
deadline = time.time() + 45
|
||||
while time.time() < deadline:
|
||||
ps_cmd = [
|
||||
"docker",
|
||||
"compose",
|
||||
"-f",
|
||||
str(compose_file),
|
||||
"-p",
|
||||
project_name,
|
||||
"ps",
|
||||
"--format",
|
||||
"{{.Name}} {{.State}}",
|
||||
]
|
||||
ps_proc = subprocess.run(
|
||||
ps_cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
ps_output = (ps_proc.stdout or "") + (ps_proc.stderr or "")
|
||||
print("[unwritable-nginx ps poll]", ps_output.strip() or "<no output>")
|
||||
if "exited" in ps_output.lower() or "dead" in ps_output.lower():
|
||||
return
|
||||
time.sleep(2)
|
||||
raise TimeoutError("netalertx-custom-port container did not exit within 45 seconds")
|
||||
|
||||
result = _run_docker_compose(
|
||||
compose_file,
|
||||
"netalertx-custom-port",
|
||||
project_name,
|
||||
env_vars={
|
||||
"PORT": str(http_port),
|
||||
"GRAPHQL_PORT": str(graphql_port),
|
||||
"NETALERTX_CHECK_ONLY": "1",
|
||||
# Run full startup to validate nginx config generation on tmpfs.
|
||||
"NETALERTX_CHECK_ONLY": "0",
|
||||
},
|
||||
timeout=60,
|
||||
detached=False,
|
||||
timeout=8,
|
||||
detached=True,
|
||||
post_up=_wait_for_unwritable_failure,
|
||||
)
|
||||
|
||||
# MANDATORY LOGGING - DO NOT REMOVE (see file header for reasoning)
|
||||
print("\n[compose output]", result.output)
|
||||
|
||||
full_output = (result.output or "") + (result.stdout or "") + (result.stderr or "")
|
||||
full_output = ANSI_ESCAPE.sub("", result.output)
|
||||
lowered_output = full_output.lower()
|
||||
print("\n[compose output unwritable-nginx]", full_output)
|
||||
|
||||
assert "unable to write" in lowered_output or "nginx" in lowered_output or "chown" in lowered_output
|
||||
assert "chown" in lowered_output or "permission" in lowered_output
|
||||
# The container may succeed (with warnings) or fail depending on the chown behavior
|
||||
# The important thing is that the warnings are shown
|
||||
assert "missing-capabilities" in lowered_output or "permission" in lowered_output
|
||||
# Container should exit due to inability to write nginx config and custom port.
|
||||
assert result.returncode == 1
|
||||
assert "unable to write to /tmp/nginx/active-config/netalertx.conf" in lowered_output
|
||||
assert "mv: can't create '/tmp/nginx/active-config/nginx.conf'" in lowered_output
|
||||
|
||||
|
||||
def test_host_network_compose(tmp_path: pathlib.Path) -> None:
|
||||
@@ -791,7 +822,7 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
default_result = _run_docker_compose(
|
||||
default_compose_file,
|
||||
default_project,
|
||||
timeout=60,
|
||||
timeout=8,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(default_ports),
|
||||
)
|
||||
@@ -847,7 +878,7 @@ def test_normal_startup_no_warnings_compose(tmp_path: pathlib.Path) -> None:
|
||||
custom_result = _run_docker_compose(
|
||||
custom_compose_file,
|
||||
custom_project,
|
||||
timeout=60,
|
||||
timeout=8,
|
||||
detached=True,
|
||||
post_up=_make_port_check_hook(custom_ports),
|
||||
)
|
||||
|
||||
@@ -354,22 +354,22 @@ def create_test_scenarios() -> List[TestScenario]:
|
||||
# These are intentionally not part of the full matrix to avoid runtime bloat.
|
||||
scenarios.extend(
|
||||
[
|
||||
TestScenario(
|
||||
TestScenario( # Will no longer fail due to the root-entrypoint fix
|
||||
name="data_noread",
|
||||
path_var="NETALERTX_DATA",
|
||||
container_path="/data",
|
||||
is_persistent=True,
|
||||
docker_compose="docker-compose.mount-test.data_noread.yml",
|
||||
expected_issues=["table_issues", "warning_message"],
|
||||
expected_issues=[""],
|
||||
expected_exit_code=0,
|
||||
),
|
||||
TestScenario(
|
||||
TestScenario( # Will no longer fail due to the root-entrypoint fix
|
||||
name="db_noread",
|
||||
path_var="NETALERTX_DB",
|
||||
container_path="/data/db",
|
||||
is_persistent=True,
|
||||
docker_compose="docker-compose.mount-test.db_noread.yml",
|
||||
expected_issues=["table_issues", "warning_message"],
|
||||
expected_issues=[],
|
||||
expected_exit_code=0,
|
||||
),
|
||||
TestScenario(
|
||||
@@ -437,6 +437,18 @@ def validate_scenario_table_output(output: str, test_scenario: TestScenario) ->
|
||||
"""Validate the diagnostic table for scenarios that should report issues."""
|
||||
|
||||
if not test_scenario.expected_issues:
|
||||
if test_scenario.name in ("data_noread", "db_noread"):
|
||||
# Cannot fix chmod 0300 (write-only) when running as user; expect R=❌, W=✅, dataloss=✅
|
||||
assert_table_row(
|
||||
output,
|
||||
test_scenario.container_path,
|
||||
readable=False,
|
||||
writeable=True,
|
||||
mount=True,
|
||||
ramdisk=None,
|
||||
performance=None,
|
||||
dataloss=True,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -663,8 +675,10 @@ def test_mount_diagnostic(netalertx_test_image, test_scenario):
|
||||
# Always surface diagnostic output for visibility
|
||||
print("\n[diagnostic output from startup logs]\n", diagnostic_output)
|
||||
|
||||
# Always validate the table output, even when expected_issues is empty.
|
||||
validate_scenario_table_output(diagnostic_output, test_scenario)
|
||||
|
||||
if test_scenario.expected_issues:
|
||||
validate_scenario_table_output(diagnostic_output, test_scenario)
|
||||
assert_has_troubleshooting_url(diagnostic_output)
|
||||
assert "⚠️" in diagnostic_output, (
|
||||
f"Issue scenario {test_scenario.name} should include a warning symbol in startup logs"
|
||||
|
||||
Reference in New Issue
Block a user