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
237 lines
7.5 KiB
Python
237 lines
7.5 KiB
Python
"""High-value tests for security/module_whitelist.py - Module Import Security."""
|
|
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
get_safe_module_class,
|
|
ALLOWED_MODULE_PATHS,
|
|
ALLOWED_CLASS_NAMES,
|
|
ALLOWED_MODULES,
|
|
SecurityError,
|
|
ModuleNotAllowedError,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ALLOWED_MODULE_PATHS constraints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAllowedModulePaths:
|
|
"""Whitelist structure validation."""
|
|
|
|
def test_is_frozenset(self):
|
|
assert isinstance(ALLOWED_MODULE_PATHS, frozenset)
|
|
|
|
def test_all_start_with_dot(self):
|
|
for path in ALLOWED_MODULE_PATHS:
|
|
assert path.startswith("."), (
|
|
f"Module path {path!r} must start with '.'"
|
|
)
|
|
|
|
def test_legacy_alias_same_object(self):
|
|
assert ALLOWED_MODULES is ALLOWED_MODULE_PATHS
|
|
|
|
def test_known_engine_paths_present(self):
|
|
expected = [
|
|
".engines.search_engine_brave",
|
|
".engines.search_engine_ddg",
|
|
".engines.search_engine_arxiv",
|
|
".engines.search_engine_wikipedia",
|
|
".engines.search_engine_tavily",
|
|
]
|
|
for path in expected:
|
|
assert path in ALLOWED_MODULE_PATHS
|
|
|
|
def test_not_empty(self):
|
|
assert len(ALLOWED_MODULE_PATHS) > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ALLOWED_CLASS_NAMES constraints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestAllowedClassNames:
|
|
"""Class name whitelist validation."""
|
|
|
|
def test_is_frozenset(self):
|
|
assert isinstance(ALLOWED_CLASS_NAMES, frozenset)
|
|
|
|
def test_known_classes_present(self):
|
|
expected = [
|
|
"BraveSearchEngine",
|
|
"DuckDuckGoSearchEngine",
|
|
"ArXivSearchEngine",
|
|
"WikipediaSearchEngine",
|
|
"TavilySearchEngine",
|
|
"BaseSearchEngine",
|
|
]
|
|
for cls_name in expected:
|
|
assert cls_name in ALLOWED_CLASS_NAMES
|
|
|
|
def test_not_empty(self):
|
|
assert len(ALLOWED_CLASS_NAMES) > 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# SecurityError / ModuleNotAllowedError
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSecurityError:
|
|
def test_is_exception(self):
|
|
assert issubclass(SecurityError, Exception)
|
|
|
|
def test_alias_is_same_class(self):
|
|
assert ModuleNotAllowedError is SecurityError
|
|
|
|
def test_can_raise_and_catch(self):
|
|
with pytest.raises(SecurityError):
|
|
raise SecurityError("blocked")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_module_import()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestValidateModuleImport:
|
|
"""Module + class pair validation."""
|
|
|
|
def test_valid_pair_accepted(self):
|
|
assert (
|
|
validate_module_import(
|
|
".engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|
|
is True
|
|
)
|
|
|
|
def test_non_relative_path_rejected(self):
|
|
assert validate_module_import("os", "system") is False
|
|
|
|
def test_absolute_dotted_path_rejected(self):
|
|
assert (
|
|
validate_module_import(
|
|
"local_deep_research.web_search_engines.engines.search_engine_brave",
|
|
"BraveSearchEngine",
|
|
)
|
|
is False
|
|
)
|
|
|
|
def test_empty_module_rejected(self):
|
|
assert validate_module_import("", "BraveSearchEngine") is False
|
|
|
|
def test_empty_class_rejected(self):
|
|
assert (
|
|
validate_module_import(".engines.search_engine_brave", "") is False
|
|
)
|
|
|
|
def test_both_empty_rejected(self):
|
|
assert validate_module_import("", "") is False
|
|
|
|
def test_unlisted_module_rejected(self):
|
|
assert (
|
|
validate_module_import(
|
|
".engines.search_engine_evil", "BraveSearchEngine"
|
|
)
|
|
is False
|
|
)
|
|
|
|
def test_unlisted_class_rejected(self):
|
|
assert (
|
|
validate_module_import(".engines.search_engine_brave", "EvilEngine")
|
|
is False
|
|
)
|
|
|
|
def test_both_unlisted_rejected(self):
|
|
assert validate_module_import(".evil_module", "EvilClass") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_safe_module_class()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestGetSafeModuleClass:
|
|
"""Safe dynamic import."""
|
|
|
|
def test_invalid_module_raises_security_error(self):
|
|
with pytest.raises(
|
|
SecurityError, match="not in the security whitelist"
|
|
):
|
|
get_safe_module_class("os", "system")
|
|
|
|
def test_invalid_class_raises_security_error(self):
|
|
with pytest.raises(
|
|
SecurityError, match="not in the security whitelist"
|
|
):
|
|
get_safe_module_class(".engines.search_engine_brave", "EvilClass")
|
|
|
|
def test_whitelisted_class_non_whitelisted_module_rejected(self):
|
|
"""Whitelisted class name with non-whitelisted module still raises."""
|
|
with pytest.raises(
|
|
SecurityError, match="not in the security whitelist"
|
|
):
|
|
get_safe_module_class(
|
|
".engines.search_engine_evil", "BraveSearchEngine"
|
|
)
|
|
|
|
@patch(
|
|
"local_deep_research.security.module_whitelist.importlib.import_module"
|
|
)
|
|
def test_valid_pair_imports_and_returns_class(self, mock_import):
|
|
mock_module = MagicMock()
|
|
mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {})
|
|
mock_import.return_value = mock_module
|
|
|
|
cls = get_safe_module_class(
|
|
".engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|
|
assert cls is mock_module.BraveSearchEngine
|
|
mock_import.assert_called_once_with(
|
|
".engines.search_engine_brave",
|
|
package="local_deep_research.web_search_engines",
|
|
)
|
|
|
|
@patch(
|
|
"local_deep_research.security.module_whitelist.importlib.import_module"
|
|
)
|
|
def test_custom_package_used(self, mock_import):
|
|
mock_module = MagicMock()
|
|
mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {})
|
|
mock_import.return_value = mock_module
|
|
|
|
get_safe_module_class(
|
|
".engines.search_engine_brave",
|
|
"BraveSearchEngine",
|
|
package="custom.package",
|
|
)
|
|
mock_import.assert_called_once_with(
|
|
".engines.search_engine_brave", package="custom.package"
|
|
)
|
|
|
|
@patch(
|
|
"local_deep_research.security.module_whitelist.importlib.import_module"
|
|
)
|
|
def test_module_not_found_propagates(self, mock_import):
|
|
mock_import.side_effect = ModuleNotFoundError("No module")
|
|
with pytest.raises(ModuleNotFoundError):
|
|
get_safe_module_class(
|
|
".engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|
|
|
|
@patch(
|
|
"local_deep_research.security.module_whitelist.importlib.import_module"
|
|
)
|
|
def test_attribute_error_propagates(self, mock_import):
|
|
mock_module = MagicMock(spec=[]) # no attributes
|
|
mock_import.return_value = mock_module
|
|
with pytest.raises(AttributeError):
|
|
get_safe_module_class(
|
|
".engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|