Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

495 lines
18 KiB
Python

"""
Extended tests for the module whitelist security system.
Tests the whitelist constants, validation functions, error hierarchy,
and security-critical edge cases in local_deep_research.security.module_whitelist.
"""
import pytest
from unittest.mock import MagicMock, patch
from local_deep_research.security.module_whitelist import (
ALLOWED_MODULE_PATHS,
ALLOWED_CLASS_NAMES,
ALLOWED_MODULES,
SecurityError,
ModuleNotAllowedError,
validate_module_import,
get_safe_module_class,
)
# ---------------------------------------------------------------------------
# ALLOWED_MODULE_PATHS
# ---------------------------------------------------------------------------
class TestAllowedModulePathsExtended:
"""Extended tests for the ALLOWED_MODULE_PATHS constant."""
def test_is_frozenset(self):
"""ALLOWED_MODULE_PATHS must be a frozenset (immutable)."""
assert isinstance(ALLOWED_MODULE_PATHS, frozenset)
def test_contains_brave_engine(self):
"""Whitelist contains the Brave search engine module."""
assert ".engines.search_engine_brave" in ALLOWED_MODULE_PATHS
def test_contains_arxiv_engine(self):
"""Whitelist contains the ArXiv search engine module."""
assert ".engines.search_engine_arxiv" in ALLOWED_MODULE_PATHS
def test_all_entries_start_with_dot(self):
"""Every entry must start with '.' enforcing relative-import-only policy."""
for path in ALLOWED_MODULE_PATHS:
assert path.startswith("."), (
f"Path {path!r} violates the relative-import-only policy"
)
def test_does_not_contain_os(self):
"""Whitelist must never contain the 'os' module."""
assert "os" not in ALLOWED_MODULE_PATHS
def test_does_not_contain_subprocess(self):
"""Whitelist must never contain the 'subprocess' module."""
assert "subprocess" not in ALLOWED_MODULE_PATHS
def test_does_not_contain_sys(self):
"""Whitelist must never contain the 'sys' module."""
assert "sys" not in ALLOWED_MODULE_PATHS
def test_no_dangerous_stdlib_paths(self):
"""Whitelist must not contain any well-known dangerous stdlib modules."""
dangerous = {
"os",
"sys",
"subprocess",
"shutil",
"ctypes",
"socket",
"pickle",
"shelve",
"code",
"codeop",
"compileall",
"importlib",
"builtins",
"io",
"signal",
}
for module in dangerous:
assert module not in ALLOWED_MODULE_PATHS, (
f"Dangerous module {module!r} must not appear in whitelist"
)
def test_immutability(self):
"""frozenset must refuse mutation attempts."""
with pytest.raises(AttributeError):
ALLOWED_MODULE_PATHS.add("os") # type: ignore[attr-defined]
with pytest.raises(AttributeError):
ALLOWED_MODULE_PATHS.discard( # type: ignore[attr-defined]
".engines.search_engine_brave"
)
# ---------------------------------------------------------------------------
# ALLOWED_CLASS_NAMES
# ---------------------------------------------------------------------------
class TestAllowedClassNamesExtended:
"""Extended tests for the ALLOWED_CLASS_NAMES constant."""
def test_is_frozenset(self):
"""ALLOWED_CLASS_NAMES must be a frozenset (immutable)."""
assert isinstance(ALLOWED_CLASS_NAMES, frozenset)
def test_contains_brave_search_engine(self):
"""Whitelist contains the BraveSearchEngine class."""
assert "BraveSearchEngine" in ALLOWED_CLASS_NAMES
def test_contains_arxiv_search_engine(self):
"""Whitelist contains the ArXivSearchEngine class."""
assert "ArXivSearchEngine" in ALLOWED_CLASS_NAMES
def test_does_not_contain_system(self):
"""Whitelist must not contain 'system' (os.system target)."""
assert "system" not in ALLOWED_CLASS_NAMES
def test_does_not_contain_exec(self):
"""Whitelist must not contain 'exec' (code execution target)."""
assert "exec" not in ALLOWED_CLASS_NAMES
def test_does_not_contain_eval(self):
"""Whitelist must not contain 'eval' (code execution target)."""
assert "eval" not in ALLOWED_CLASS_NAMES
def test_no_dangerous_class_names(self):
"""Whitelist must not contain any well-known dangerous class/function names."""
dangerous = {
"system",
"exec",
"eval",
"Popen",
"call",
"run",
"loads",
"load",
"CDLL",
"interact",
"__import__",
"import_module",
"compile",
}
for name in dangerous:
assert name not in ALLOWED_CLASS_NAMES, (
f"Dangerous name {name!r} must not appear in class whitelist"
)
def test_immutability(self):
"""frozenset must refuse mutation attempts."""
with pytest.raises(AttributeError):
ALLOWED_CLASS_NAMES.add("system") # type: ignore[attr-defined]
# ---------------------------------------------------------------------------
# ALLOWED_MODULES (legacy alias)
# ---------------------------------------------------------------------------
class TestAllowedModulesAlias:
"""Tests for the ALLOWED_MODULES legacy alias."""
def test_is_same_object_as_allowed_module_paths(self):
"""ALLOWED_MODULES must be the exact same object as ALLOWED_MODULE_PATHS."""
assert ALLOWED_MODULES is ALLOWED_MODULE_PATHS
# ---------------------------------------------------------------------------
# SecurityError and ModuleNotAllowedError
# ---------------------------------------------------------------------------
class TestSecurityErrorHierarchy:
"""Tests for the SecurityError and ModuleNotAllowedError exception classes."""
def test_security_error_is_exception_subclass(self):
"""SecurityError must be a subclass of Exception."""
assert issubclass(SecurityError, Exception)
def test_module_not_allowed_error_is_security_error(self):
"""ModuleNotAllowedError must be the same class as SecurityError (legacy alias)."""
assert ModuleNotAllowedError is SecurityError
def test_security_error_is_catchable_as_exception(self):
"""SecurityError instances must be catchable as plain Exception."""
with pytest.raises(Exception):
raise SecurityError("test")
def test_module_not_allowed_error_is_catchable_as_security_error(self):
"""ModuleNotAllowedError instances must be catchable as SecurityError."""
with pytest.raises(SecurityError):
raise ModuleNotAllowedError("test")
def test_security_error_preserves_message(self):
"""SecurityError must preserve its error message."""
err = SecurityError("blocked import attempt")
assert str(err) == "blocked import attempt"
# ---------------------------------------------------------------------------
# validate_module_import
# ---------------------------------------------------------------------------
class TestValidateModuleImportExtended:
"""Extended tests for the validate_module_import function."""
# --- happy-path ---
def test_valid_module_and_valid_class_returns_true(self):
"""Returns True when both module and class are whitelisted."""
assert (
validate_module_import(
".engines.search_engine_brave", "BraveSearchEngine"
)
is True
)
# --- single-invalid dimension ---
def test_invalid_module_valid_class_returns_false(self):
"""Returns False when only the module is not whitelisted."""
assert (
validate_module_import(
".engines.nonexistent_engine", "BraveSearchEngine"
)
is False
)
def test_valid_module_invalid_class_returns_false(self):
"""Returns False when only the class is not whitelisted."""
assert (
validate_module_import(".engines.search_engine_brave", "EvilClass")
is False
)
# --- both-invalid dimension ---
def test_both_invalid_returns_false(self):
"""Returns False when neither module nor class is whitelisted."""
assert validate_module_import(".evil.module", "EvilClass") is False
# --- empty / None inputs ---
def test_empty_module_path_returns_false(self):
"""Returns False for an empty string module_path."""
assert validate_module_import("", "BraveSearchEngine") is False
def test_empty_class_name_returns_false(self):
"""Returns False for an empty string class_name."""
assert (
validate_module_import(".engines.search_engine_brave", "") is False
)
def test_none_module_path_returns_false(self):
"""Returns False when module_path is None."""
assert validate_module_import(None, "BraveSearchEngine") is False
def test_none_class_name_returns_false(self):
"""Returns False when class_name is None."""
assert (
validate_module_import(".engines.search_engine_brave", None)
is False
)
def test_both_none_returns_false(self):
"""Returns False when both arguments are None."""
assert validate_module_import(None, None) is False
# --- CRITICAL SECURITY: non-relative paths ---
def test_non_relative_os_returns_false(self):
"""CRITICAL: bare 'os' module must be rejected as non-relative path."""
assert validate_module_import("os", "system") is False
def test_absolute_fully_qualified_path_returns_false(self):
"""Absolute fully-qualified module path must be rejected."""
assert (
validate_module_import(
"local_deep_research.web_search_engines.engines.search_engine_brave",
"BraveSearchEngine",
)
is False
)
def test_double_dot_prefix_returns_false(self):
"""Path starting with '..' must be rejected (not in whitelist)."""
assert validate_module_import("..os", "system") is False
def test_triple_dot_prefix_returns_false(self):
"""Path starting with '...' must be rejected."""
assert validate_module_import("...os", "system") is False
def test_non_relative_subprocess_returns_false(self):
"""Bare 'subprocess' module must be rejected."""
assert validate_module_import("subprocess", "Popen") is False
def test_non_relative_sys_returns_false(self):
"""Bare 'sys' module must be rejected."""
assert validate_module_import("sys", "exit") is False
# --- path traversal attempts ---
def test_path_traversal_slash(self):
"""Path traversal using forward slashes must be rejected."""
assert validate_module_import(".engines/../../../os", "system") is False
def test_path_traversal_backslash(self):
"""Path traversal using backslashes must be rejected."""
assert (
validate_module_import(".engines\\..\\..\\subprocess", "Popen")
is False
)
# --- case sensitivity ---
def test_case_sensitive_module_path(self):
"""Module paths are case-sensitive -- upper-cased variant must be rejected."""
assert (
validate_module_import(
".ENGINES.SEARCH_ENGINE_BRAVE", "BraveSearchEngine"
)
is False
)
def test_case_sensitive_class_name(self):
"""Class names are case-sensitive -- lower-cased variant must be rejected."""
assert (
validate_module_import(
".engines.search_engine_brave", "bravesearchengine"
)
is False
)
# --- whitespace / special chars ---
def test_whitespace_module_path_returns_false(self):
"""A whitespace-only module_path must be rejected (not starting with '.')."""
assert validate_module_import(" ", "BraveSearchEngine") is False
def test_module_path_with_trailing_space_returns_false(self):
"""Module path with trailing space must not match whitelist entry."""
assert (
validate_module_import(
".engines.search_engine_brave ", "BraveSearchEngine"
)
is False
)
def test_class_name_with_trailing_space_returns_false(self):
"""Class name with trailing space must not match whitelist entry."""
assert (
validate_module_import(
".engines.search_engine_brave", "BraveSearchEngine "
)
is False
)
# --- injection-style inputs ---
def test_semicolon_injection_in_module_path(self):
"""Semicolon-based injection attempt in module path must be rejected."""
assert validate_module_import(".engines; import os;", "system") is False
def test_newline_injection_in_module_path(self):
"""Newline injection attempt in module path must be rejected."""
assert validate_module_import(".engines\nimport os", "system") is False
# ---------------------------------------------------------------------------
# get_safe_module_class
# ---------------------------------------------------------------------------
class TestGetSafeModuleClassExtended:
"""Extended tests for the get_safe_module_class function."""
def test_raises_security_error_for_non_whitelisted_module(self):
"""Raises SecurityError when the module path is not whitelisted."""
with pytest.raises(SecurityError):
get_safe_module_class(".evil.module", "BraveSearchEngine")
def test_raises_security_error_for_non_whitelisted_class(self):
"""Raises SecurityError when the class name is not whitelisted."""
with pytest.raises(SecurityError):
get_safe_module_class(
".engines.search_engine_brave", "MaliciousClass"
)
def test_raises_security_error_for_absolute_os(self):
"""Raises SecurityError for absolute path 'os'."""
with pytest.raises(SecurityError):
get_safe_module_class("os", "system")
def test_raises_security_error_for_subprocess(self):
"""Raises SecurityError for absolute path 'subprocess'."""
with pytest.raises(SecurityError):
get_safe_module_class("subprocess", "Popen")
def test_error_message_contains_module_path(self):
"""The SecurityError message must include the rejected module path."""
with pytest.raises(SecurityError, match=r"\.evil\.module"):
get_safe_module_class(".evil.module", "EvilClass")
def test_error_message_contains_class_name(self):
"""The SecurityError message must include the rejected class name."""
with pytest.raises(SecurityError, match="EvilClass"):
get_safe_module_class(".evil.module", "EvilClass")
def test_error_message_mentions_whitelist(self):
"""The SecurityError message must mention 'whitelist' for clarity."""
with pytest.raises(SecurityError, match="whitelist"):
get_safe_module_class("os", "system")
# --- default package for relative imports ---
def test_default_package_used_for_relative_import(self):
"""When no package kwarg is given, the default package is used for relative imports."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
) as mock_import:
mock_module = MagicMock()
mock_module.FakeEngine = type("FakeEngine", (), {})
mock_import.return_value = mock_module
get_safe_module_class(".engines.fake", "FakeEngine")
mock_import.assert_called_once_with(
".engines.fake",
package="local_deep_research.web_search_engines",
)
# --- custom package parameter ---
def test_custom_package_parameter_is_used(self):
"""When a custom package kwarg is given, it overrides the default."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
) as mock_import:
mock_module = MagicMock()
mock_module.FakeEngine = type("FakeEngine", (), {})
mock_import.return_value = mock_module
get_safe_module_class(
".engines.fake",
"FakeEngine",
package="my.custom.package",
)
mock_import.assert_called_once_with(
".engines.fake",
package="my.custom.package",
)
# --- downstream exception propagation ---
def test_module_not_found_error_propagates(self):
"""ModuleNotFoundError propagates when a whitelisted module cannot be found."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
side_effect=ModuleNotFoundError("No module named 'fake'"),
):
with pytest.raises(ModuleNotFoundError):
get_safe_module_class(".engines.fake_module", "FakeEngine")
def test_attribute_error_propagates(self):
"""AttributeError propagates when the class is missing from the module."""
with patch(
"local_deep_research.security.module_whitelist.validate_module_import",
return_value=True,
):
mock_module = MagicMock(spec=[])
with patch(
"local_deep_research.security.module_whitelist.importlib.import_module",
return_value=mock_module,
):
with pytest.raises(AttributeError):
get_safe_module_class(
".engines.search_engine_brave",
"NonExistentClass",
)