Files
pyisolate/tests/test_sandbox_detect.py
John Pollock b73366f884 fix: CI compatibility - ruff format, Python 3.10+, bubblewrap
Drop Python 3.9, add bubblewrap to workflows, apply ruff formatting.

Was running `ruff` and not the additional `ruff-format`. Installed pre-commit hooks to prevent any future non-compliant commits.
2026-01-21 00:28:17 -06:00

406 lines
16 KiB
Python

"""Unit tests for sandbox capability detection.
Tests cover:
- Sysctl file reading
- RHEL/Ubuntu restriction detection
- SELinux and hardened kernel checks
- bwrap binary invocation
- Error classification
- Full detection flow
"""
import subprocess
import sys
from unittest.mock import MagicMock, mock_open, patch
import pytest
from pyisolate._internal.sandbox_detect import (
RestrictionModel,
SandboxCapability,
_check_hardened_kernel,
_check_rhel_restriction,
_check_selinux_enforcing,
_check_ubuntu_apparmor_restriction,
_classify_error,
_read_sysctl,
_test_bwrap,
detect_sandbox_capability,
)
class TestSysctlReaders:
"""Test low-level sysctl reading functions."""
def test_read_sysctl_success(self) -> None:
"""Test successful sysctl read."""
m = mock_open(read_data="15000\n")
with patch("builtins.open", m):
assert _read_sysctl("/proc/sys/user/max_user_namespaces") == 15000
def test_read_sysctl_file_missing(self) -> None:
"""Test when sysctl file doesn't exist."""
with patch("builtins.open", side_effect=FileNotFoundError):
assert _read_sysctl("/proc/sys/nonexistent") is None
def test_read_sysctl_permission_denied(self) -> None:
"""Test when sysctl file is not readable."""
with patch("builtins.open", side_effect=PermissionError):
assert _read_sysctl("/proc/sys/restricted") is None
def test_read_sysctl_invalid_value(self) -> None:
"""Test when sysctl contains non-integer."""
m = mock_open(read_data="not_a_number\n")
with patch("builtins.open", m):
assert _read_sysctl("/proc/sys/something") is None
def test_rhel_restriction_detected(self) -> None:
"""Test RHEL sysctl restriction (max_user_namespaces=0)."""
m = mock_open(read_data="0")
with patch("builtins.open", m):
assert _check_rhel_restriction() is True
def test_rhel_restriction_not_present(self) -> None:
"""Test when RHEL restriction is not present."""
m = mock_open(read_data="15000")
with patch("builtins.open", m):
assert _check_rhel_restriction() is False
def test_rhel_restriction_file_missing(self) -> None:
"""Test when sysctl file doesn't exist."""
with patch("builtins.open", side_effect=FileNotFoundError):
assert _check_rhel_restriction() is False
def test_ubuntu_apparmor_detected(self) -> None:
"""Test Ubuntu AppArmor restriction."""
m = mock_open(read_data="1")
with patch("builtins.open", m):
assert _check_ubuntu_apparmor_restriction() is True
def test_ubuntu_apparmor_not_present(self) -> None:
"""Test when Ubuntu AppArmor is not enabled."""
m = mock_open(read_data="0")
with patch("builtins.open", m):
assert _check_ubuntu_apparmor_restriction() is False
def test_ubuntu_apparmor_file_missing(self) -> None:
"""Test when AppArmor sysctl doesn't exist."""
with patch("builtins.open", side_effect=FileNotFoundError):
assert _check_ubuntu_apparmor_restriction() is False
class TestKernelChecks:
"""Test kernel feature detection."""
def test_selinux_enforcing(self) -> None:
"""Test SELinux enforcing detection."""
mock_result = MagicMock()
mock_result.stdout = b"Enforcing\n"
with patch("subprocess.run", return_value=mock_result):
assert _check_selinux_enforcing() is True
def test_selinux_permissive(self) -> None:
"""Test SELinux permissive mode."""
mock_result = MagicMock()
mock_result.stdout = b"Permissive\n"
with patch("subprocess.run", return_value=mock_result):
assert _check_selinux_enforcing() is False
def test_selinux_disabled(self) -> None:
"""Test SELinux disabled mode."""
mock_result = MagicMock()
mock_result.stdout = b"Disabled\n"
with patch("subprocess.run", return_value=mock_result):
assert _check_selinux_enforcing() is False
def test_selinux_not_installed(self) -> None:
"""Test when getenforce command doesn't exist."""
with patch("subprocess.run", side_effect=FileNotFoundError):
assert _check_selinux_enforcing() is False
def test_selinux_timeout(self) -> None:
"""Test when getenforce times out."""
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("getenforce", 5)):
assert _check_selinux_enforcing() is False
def test_hardened_kernel_detected(self) -> None:
"""Test hardened kernel detection."""
m = mock_open(read_data="Linux version 5.15.0-hardened-x86_64")
with patch("builtins.open", m):
assert _check_hardened_kernel() is True
def test_hardened_kernel_not_present(self) -> None:
"""Test standard kernel."""
m = mock_open(read_data="Linux version 5.15.0-generic-x86_64")
with patch("builtins.open", m):
assert _check_hardened_kernel() is False
def test_hardened_kernel_file_missing(self) -> None:
"""Test when /proc/version doesn't exist."""
with patch("builtins.open", side_effect=FileNotFoundError):
assert _check_hardened_kernel() is False
class TestBwrapInvocation:
"""Test bwrap binary invocation and error handling."""
def test_bwrap_test_success(self) -> None:
"""Test successful bwrap invocation."""
mock_result = MagicMock()
mock_result.returncode = 0
with patch("subprocess.run", return_value=mock_result):
success, error = _test_bwrap("/usr/bin/bwrap")
assert success is True
assert error == ""
def test_bwrap_test_uses_unshare_user_try(self) -> None:
"""Test that bwrap test uses --unshare-user-try flag."""
mock_result = MagicMock()
mock_result.returncode = 0
with patch("subprocess.run", return_value=mock_result) as mock_run:
_test_bwrap("/usr/bin/bwrap")
args = mock_run.call_args[0][0]
assert "--unshare-user-try" in args
def test_bwrap_test_failure_permission(self) -> None:
"""Test bwrap failure with permission denied."""
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stderr = b"Permission denied: uid map"
with patch("subprocess.run", return_value=mock_result):
success, error = _test_bwrap("/usr/bin/bwrap")
assert success is False
assert "Permission denied" in error
def test_bwrap_test_timeout(self) -> None:
"""Test bwrap test timeout."""
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("bwrap", 10)):
success, error = _test_bwrap("/usr/bin/bwrap")
assert success is False
assert "timed out" in error.lower()
def test_bwrap_test_exception(self) -> None:
"""Test bwrap test with unexpected exception."""
with patch("subprocess.run", side_effect=Exception("Unexpected error")):
success, error = _test_bwrap("/usr/bin/bwrap")
assert success is False
assert "Unexpected error" in error
class TestErrorClassification:
"""Test error message classification."""
def test_classify_apparmor_error(self) -> None:
"""Test AppArmor error classification."""
with patch(
"pyisolate._internal.sandbox_detect._check_ubuntu_apparmor_restriction",
return_value=True,
):
model = _classify_error("Permission denied: uid map")
assert model == RestrictionModel.UBUNTU_APPARMOR
def test_classify_selinux_error(self) -> None:
"""Test SELinux error classification."""
with (
patch(
"pyisolate._internal.sandbox_detect._check_ubuntu_apparmor_restriction",
return_value=False,
),
patch(
"pyisolate._internal.sandbox_detect._check_selinux_enforcing",
return_value=True,
),
):
model = _classify_error("Permission denied")
assert model == RestrictionModel.SELINUX
def test_classify_rhel_sysctl_error(self) -> None:
"""Test RHEL sysctl error classification."""
model = _classify_error("No space left on device")
assert model == RestrictionModel.RHEL_SYSCTL
def test_classify_rhel_enospc_error(self) -> None:
"""Test RHEL ENOSPC error classification."""
model = _classify_error("ENOSPC")
assert model == RestrictionModel.RHEL_SYSCTL
def test_classify_hardened_kernel_error(self) -> None:
"""Test hardened kernel error classification."""
with patch(
"pyisolate._internal.sandbox_detect._check_hardened_kernel",
return_value=True,
):
model = _classify_error("Operation not permitted")
assert model == RestrictionModel.ARCH_HARDENED
def test_classify_operation_not_permitted_non_hardened(self) -> None:
"""Test operation not permitted on non-hardened kernel."""
with patch(
"pyisolate._internal.sandbox_detect._check_hardened_kernel",
return_value=False,
):
model = _classify_error("Operation not permitted")
assert model == RestrictionModel.UNKNOWN
def test_classify_unknown_error(self) -> None:
"""Test unknown error classification."""
model = _classify_error("Some weird error")
assert model == RestrictionModel.UNKNOWN
def test_classify_permission_denied_neither_apparmor_nor_selinux(self) -> None:
"""Test permission denied when neither AppArmor nor SELinux."""
with (
patch(
"pyisolate._internal.sandbox_detect._check_ubuntu_apparmor_restriction",
return_value=False,
),
patch(
"pyisolate._internal.sandbox_detect._check_selinux_enforcing",
return_value=False,
),
):
model = _classify_error("Permission denied")
assert model == RestrictionModel.UNKNOWN
class TestFullDetection:
"""Integration tests for full detection flow."""
def test_platform_check_non_linux(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that non-Linux platforms return PLATFORM_UNSUPPORTED."""
monkeypatch.setattr(sys, "platform", "darwin")
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.PLATFORM_UNSUPPORTED
assert "darwin" in cap.remediation
def test_platform_check_windows(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that Windows returns PLATFORM_UNSUPPORTED."""
monkeypatch.setattr(sys, "platform", "win32")
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.PLATFORM_UNSUPPORTED
def test_bwrap_missing(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that missing bwrap binary returns BWRAP_MISSING."""
monkeypatch.setattr(sys, "platform", "linux")
with patch("shutil.which", return_value=None):
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.BWRAP_MISSING
assert "bubblewrap" in cap.remediation.lower()
def test_rhel_restriction_blocks(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test RHEL sysctl blocks before bwrap test."""
monkeypatch.setattr(sys, "platform", "linux")
with (
patch("shutil.which", return_value="/usr/bin/bwrap"),
patch(
"pyisolate._internal.sandbox_detect._check_rhel_restriction",
return_value=True,
),
):
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.RHEL_SYSCTL
assert cap.bwrap_path == "/usr/bin/bwrap"
def test_full_success(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test full detection success path."""
monkeypatch.setattr(sys, "platform", "linux")
with (
patch("shutil.which", return_value="/usr/bin/bwrap"),
patch(
"pyisolate._internal.sandbox_detect._check_rhel_restriction",
return_value=False,
),
patch(
"pyisolate._internal.sandbox_detect._test_bwrap",
return_value=(True, ""),
),
):
cap = detect_sandbox_capability()
assert cap.available is True
assert cap.restriction_model == RestrictionModel.NONE
assert cap.bwrap_path == "/usr/bin/bwrap"
assert cap.remediation == ""
def test_ubuntu_apparmor_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test Ubuntu AppArmor detection and remediation."""
monkeypatch.setattr(sys, "platform", "linux")
with (
patch("shutil.which", return_value="/usr/bin/bwrap"),
patch(
"pyisolate._internal.sandbox_detect._check_rhel_restriction",
return_value=False,
),
patch(
"pyisolate._internal.sandbox_detect._test_bwrap",
return_value=(False, "Permission denied: uid map"),
),
patch(
"pyisolate._internal.sandbox_detect._check_ubuntu_apparmor_restriction",
return_value=True,
),
):
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.UBUNTU_APPARMOR
assert "apparmor" in cap.remediation.lower()
assert cap.raw_error == "Permission denied: uid map"
def test_unknown_error_includes_message(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that unknown errors include the raw error in remediation."""
monkeypatch.setattr(sys, "platform", "linux")
with (
patch("shutil.which", return_value="/usr/bin/bwrap"),
patch(
"pyisolate._internal.sandbox_detect._check_rhel_restriction",
return_value=False,
),
patch(
"pyisolate._internal.sandbox_detect._test_bwrap",
return_value=(False, "Some weird unknown error"),
),
patch(
"pyisolate._internal.sandbox_detect._classify_error",
return_value=RestrictionModel.UNKNOWN,
),
):
cap = detect_sandbox_capability()
assert cap.available is False
assert cap.restriction_model == RestrictionModel.UNKNOWN
assert "weird unknown" in cap.remediation
def test_capability_dataclass_fields(self) -> None:
"""Test SandboxCapability dataclass has expected fields."""
cap = SandboxCapability(
available=True,
bwrap_path="/usr/bin/bwrap",
restriction_model=RestrictionModel.NONE,
remediation="",
raw_error=None,
)
assert cap.available is True
assert cap.bwrap_path == "/usr/bin/bwrap"
assert cap.restriction_model == RestrictionModel.NONE
assert cap.remediation == ""
assert cap.raw_error is None
class TestRestrictionModelEnum:
"""Test RestrictionModel enum values."""
def test_all_models_have_remediation(self) -> None:
"""Ensure all restriction models have remediation messages."""
from pyisolate._internal.sandbox_detect import _REMEDIATION_MESSAGES
for model in RestrictionModel:
assert model in _REMEDIATION_MESSAGES, f"Missing remediation for {model}"
def test_none_has_empty_remediation(self) -> None:
"""Test that NONE restriction has empty remediation."""
from pyisolate._internal.sandbox_detect import _REMEDIATION_MESSAGES
assert _REMEDIATION_MESSAGES[RestrictionModel.NONE] == ""