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
354 lines
12 KiB
Python
354 lines
12 KiB
Python
"""
|
|
Behavioral tests for security/module_whitelist module.
|
|
|
|
Tests the whitelist validation logic, security error handling,
|
|
and module/class name filtering.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
class TestAllowedModulePaths:
|
|
"""Tests for ALLOWED_MODULE_PATHS constant."""
|
|
|
|
def test_is_frozenset(self):
|
|
"""ALLOWED_MODULE_PATHS is a frozenset."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert isinstance(ALLOWED_MODULE_PATHS, frozenset)
|
|
|
|
def test_is_non_empty(self):
|
|
"""ALLOWED_MODULE_PATHS is non-empty."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert len(ALLOWED_MODULE_PATHS) > 0
|
|
|
|
def test_all_paths_start_with_dot(self):
|
|
"""All paths start with '.' (relative imports only)."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
for path in ALLOWED_MODULE_PATHS:
|
|
assert path.startswith("."), (
|
|
f"Path {path!r} does not start with '.'"
|
|
)
|
|
|
|
def test_all_paths_are_strings(self):
|
|
"""All paths are strings."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
for path in ALLOWED_MODULE_PATHS:
|
|
assert isinstance(path, str)
|
|
|
|
def test_contains_brave_engine(self):
|
|
"""Contains brave search engine module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert ".engines.search_engine_brave" in ALLOWED_MODULE_PATHS
|
|
|
|
def test_contains_searxng_engine(self):
|
|
"""Contains SearXNG search engine module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert ".engines.search_engine_searxng" in ALLOWED_MODULE_PATHS
|
|
|
|
def test_contains_base_engine(self):
|
|
"""Contains search_engine_base module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert ".search_engine_base" in ALLOWED_MODULE_PATHS
|
|
|
|
def test_does_not_contain_os(self):
|
|
"""Does not contain 'os' module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert "os" not in ALLOWED_MODULE_PATHS
|
|
|
|
def test_does_not_contain_subprocess(self):
|
|
"""Does not contain 'subprocess' module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
)
|
|
|
|
assert "subprocess" not in ALLOWED_MODULE_PATHS
|
|
|
|
def test_legacy_alias_matches(self):
|
|
"""ALLOWED_MODULES is the same as ALLOWED_MODULE_PATHS."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_MODULE_PATHS,
|
|
ALLOWED_MODULES,
|
|
)
|
|
|
|
assert ALLOWED_MODULES is ALLOWED_MODULE_PATHS
|
|
|
|
|
|
class TestAllowedClassNames:
|
|
"""Tests for ALLOWED_CLASS_NAMES constant."""
|
|
|
|
def test_is_frozenset(self):
|
|
"""ALLOWED_CLASS_NAMES is a frozenset."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
assert isinstance(ALLOWED_CLASS_NAMES, frozenset)
|
|
|
|
def test_is_non_empty(self):
|
|
"""ALLOWED_CLASS_NAMES is non-empty."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
assert len(ALLOWED_CLASS_NAMES) > 0
|
|
|
|
def test_all_names_are_strings(self):
|
|
"""All class names are strings."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
for name in ALLOWED_CLASS_NAMES:
|
|
assert isinstance(name, str)
|
|
|
|
def test_contains_brave_search_engine(self):
|
|
"""Contains BraveSearchEngine class."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
assert "BraveSearchEngine" in ALLOWED_CLASS_NAMES
|
|
|
|
def test_contains_base_search_engine(self):
|
|
"""Contains BaseSearchEngine class."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
assert "BaseSearchEngine" in ALLOWED_CLASS_NAMES
|
|
|
|
def test_does_not_contain_generic_names(self):
|
|
"""Does not contain generic dangerous class names."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
ALLOWED_CLASS_NAMES,
|
|
)
|
|
|
|
assert "Popen" not in ALLOWED_CLASS_NAMES
|
|
assert "system" not in ALLOWED_CLASS_NAMES
|
|
|
|
|
|
class TestSecurityError:
|
|
"""Tests for SecurityError exception."""
|
|
|
|
def test_is_exception_subclass(self):
|
|
"""SecurityError is an Exception subclass."""
|
|
from local_deep_research.security.module_whitelist import SecurityError
|
|
|
|
assert issubclass(SecurityError, Exception)
|
|
|
|
def test_can_be_raised_and_caught(self):
|
|
"""SecurityError can be raised and caught."""
|
|
from local_deep_research.security.module_whitelist import SecurityError
|
|
|
|
with pytest.raises(SecurityError):
|
|
raise SecurityError("test")
|
|
|
|
def test_message_preserved(self):
|
|
"""Error message is preserved."""
|
|
from local_deep_research.security.module_whitelist import SecurityError
|
|
|
|
err = SecurityError("blocked import")
|
|
assert str(err) == "blocked import"
|
|
|
|
def test_legacy_alias(self):
|
|
"""ModuleNotAllowedError is same as SecurityError."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
SecurityError,
|
|
ModuleNotAllowedError,
|
|
)
|
|
|
|
assert ModuleNotAllowedError is SecurityError
|
|
|
|
|
|
class TestValidateModuleImport:
|
|
"""Tests for validate_module_import function."""
|
|
|
|
def test_valid_module_and_class(self):
|
|
"""Returns True for whitelisted module and class."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(
|
|
".engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|
|
assert result is True
|
|
|
|
def test_invalid_module_path(self):
|
|
"""Returns False for non-whitelisted module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(
|
|
".engines.evil_module", "BraveSearchEngine"
|
|
)
|
|
assert result is False
|
|
|
|
def test_invalid_class_name(self):
|
|
"""Returns False for non-whitelisted class."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(
|
|
".engines.search_engine_brave", "EvilClass"
|
|
)
|
|
assert result is False
|
|
|
|
def test_both_invalid(self):
|
|
"""Returns False when both module and class are invalid."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(".evil.module", "EvilClass")
|
|
assert result is False
|
|
|
|
def test_empty_module_path(self):
|
|
"""Returns False for empty module path."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import("", "BraveSearchEngine")
|
|
assert result is False
|
|
|
|
def test_empty_class_name(self):
|
|
"""Returns False for empty class name."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(".engines.search_engine_brave", "")
|
|
assert result is False
|
|
|
|
def test_none_module_path(self):
|
|
"""Returns False for None module path."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(None, "BraveSearchEngine")
|
|
assert result is False
|
|
|
|
def test_none_class_name(self):
|
|
"""Returns False for None class name."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(".engines.search_engine_brave", None)
|
|
assert result is False
|
|
|
|
def test_rejects_absolute_import_os(self):
|
|
"""Rejects absolute import of 'os' module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import("os", "system")
|
|
assert result is False
|
|
|
|
def test_rejects_absolute_import_subprocess(self):
|
|
"""Rejects absolute import of 'subprocess' module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import("subprocess", "Popen")
|
|
assert result is False
|
|
|
|
def test_rejects_non_relative_whitelisted_path(self):
|
|
"""Rejects non-relative path even if it looks similar to whitelisted one."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
validate_module_import,
|
|
)
|
|
|
|
result = validate_module_import(
|
|
"engines.search_engine_brave", "BraveSearchEngine"
|
|
)
|
|
assert result is False
|
|
|
|
|
|
class TestGetSafeModuleClass:
|
|
"""Tests for get_safe_module_class function security validation."""
|
|
|
|
def test_raises_for_non_whitelisted_module(self):
|
|
"""Raises SecurityError for non-whitelisted module."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
get_safe_module_class,
|
|
SecurityError,
|
|
)
|
|
|
|
with pytest.raises(SecurityError) as exc_info:
|
|
get_safe_module_class(".evil.module", "EvilClass")
|
|
assert "not in the security whitelist" in str(exc_info.value)
|
|
|
|
def test_raises_for_non_whitelisted_class(self):
|
|
"""Raises SecurityError for non-whitelisted class name."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
get_safe_module_class,
|
|
SecurityError,
|
|
)
|
|
|
|
with pytest.raises(SecurityError) as exc_info:
|
|
get_safe_module_class(".engines.search_engine_brave", "EvilClass")
|
|
assert "not in the security whitelist" in str(exc_info.value)
|
|
|
|
def test_raises_for_absolute_import(self):
|
|
"""Raises SecurityError for absolute import path."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
get_safe_module_class,
|
|
SecurityError,
|
|
)
|
|
|
|
with pytest.raises(SecurityError):
|
|
get_safe_module_class("os", "system")
|
|
|
|
def test_error_message_includes_module_path(self):
|
|
"""Error message includes the rejected module path."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
get_safe_module_class,
|
|
SecurityError,
|
|
)
|
|
|
|
with pytest.raises(SecurityError) as exc_info:
|
|
get_safe_module_class(".evil.module", "EvilClass")
|
|
assert ".evil.module" in str(exc_info.value)
|
|
|
|
def test_error_message_includes_class_name(self):
|
|
"""Error message includes the rejected class name."""
|
|
from local_deep_research.security.module_whitelist import (
|
|
get_safe_module_class,
|
|
SecurityError,
|
|
)
|
|
|
|
with pytest.raises(SecurityError) as exc_info:
|
|
get_safe_module_class(".evil.module", "EvilClass")
|
|
assert "EvilClass" in str(exc_info.value)
|