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
224 lines
7.0 KiB
Python
Executable File
224 lines
7.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Pre-commit hook to prevent absolute module paths in the codebase.
|
|
|
|
Module path configuration values MUST use relative imports (starting with ".")
|
|
instead of absolute paths (starting with "local_deep_research.").
|
|
The module whitelist security boundary requires relative paths for search engines,
|
|
and absolute paths elsewhere are a packaging anti-pattern.
|
|
|
|
See: src/local_deep_research/security/module_whitelist.py
|
|
"""
|
|
|
|
import ast
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
# The absolute base that must NOT appear as a module path prefix
|
|
ABSOLUTE_BASE = "local_deep_research"
|
|
|
|
# Known legitimate uses of absolute package references that are NOT module_path
|
|
# config values. These are used for importlib.resources, package= kwargs, and
|
|
# normalization constants where the full package name is required by Python APIs.
|
|
LEGITIMATE_ABSOLUTE_REFS = frozenset(
|
|
{
|
|
# importlib.resources.files() requires the full package name
|
|
"local_deep_research.defaults.settings",
|
|
# importlib.import_module() to avoid circular imports
|
|
"local_deep_research.utilities",
|
|
# package= kwarg for relative import resolution
|
|
"local_deep_research.llm.providers",
|
|
# package= kwarg for relative import resolution in module_whitelist.py
|
|
"local_deep_research.web_search_engines",
|
|
}
|
|
)
|
|
|
|
# Directory patterns where absolute paths are allowed (matched as path components)
|
|
ALLOWED_DIR_PATTERNS = {
|
|
"tests", # Tests may verify rejection of absolute paths
|
|
".pre-commit-hooks", # Hook files themselves
|
|
}
|
|
|
|
# Specific file suffixes where absolute paths are allowed
|
|
ALLOWED_FILE_SUFFIXES = {
|
|
# Use os.sep-independent matching via PurePosixPath
|
|
"security/module_whitelist.py", # Defines the security boundary itself
|
|
}
|
|
|
|
# Basename patterns for test files
|
|
ALLOWED_BASENAME_PREFIXES = {"test_"} # test_*.py
|
|
ALLOWED_BASENAME_SUFFIXES = {"_test.py"} # *_test.py
|
|
|
|
|
|
class AbsoluteModulePathChecker(ast.NodeVisitor):
|
|
"""AST visitor to detect absolute module paths in string literals."""
|
|
|
|
def __init__(self, filename: str):
|
|
self.filename = filename
|
|
self.errors = []
|
|
|
|
def visit_Constant(self, node):
|
|
"""Check string constants for absolute module paths."""
|
|
if (
|
|
isinstance(node.value, str)
|
|
and node.value.startswith(ABSOLUTE_BASE + ".")
|
|
and node.value not in LEGITIMATE_ABSOLUTE_REFS
|
|
):
|
|
self.errors.append(
|
|
(
|
|
node.lineno,
|
|
f'Absolute module path "{node.value}" found. '
|
|
f"Use a relative path (starting with '.') instead.",
|
|
)
|
|
)
|
|
self.generic_visit(node)
|
|
|
|
|
|
def _is_file_allowed(filename: str) -> bool:
|
|
"""Check if this file is allowed to contain absolute module paths."""
|
|
p = Path(filename)
|
|
normalized = p.as_posix()
|
|
parts = p.parts
|
|
|
|
# Check directory patterns as path components
|
|
for pattern in ALLOWED_DIR_PATTERNS:
|
|
if pattern in parts:
|
|
return True
|
|
|
|
# Check specific file suffixes
|
|
for suffix in ALLOWED_FILE_SUFFIXES:
|
|
if normalized.endswith(suffix):
|
|
return True
|
|
|
|
# Check basename patterns
|
|
basename = p.name
|
|
for prefix in ALLOWED_BASENAME_PREFIXES:
|
|
if basename.startswith(prefix):
|
|
return True
|
|
for suffix in ALLOWED_BASENAME_SUFFIXES:
|
|
if basename.endswith(suffix):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def check_python_file(filename: str) -> bool:
|
|
"""Check a Python file for absolute module paths in string literals."""
|
|
if _is_file_allowed(filename):
|
|
return True
|
|
|
|
try:
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
except Exception as e:
|
|
print(f"Error reading {filename}: {e}")
|
|
return False
|
|
|
|
try:
|
|
tree = ast.parse(content, filename=filename)
|
|
checker = AbsoluteModulePathChecker(filename)
|
|
checker.visit(tree)
|
|
|
|
if checker.errors:
|
|
print(f"\n{filename}:")
|
|
for line_num, error in checker.errors:
|
|
print(f" Line {line_num}: {error}")
|
|
return False
|
|
|
|
except SyntaxError:
|
|
# Skip files with syntax errors (let other tools handle that)
|
|
pass
|
|
except Exception as e:
|
|
print(f"Error parsing {filename}: {e}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def check_json_file(filename: str) -> bool:
|
|
"""Check a JSON config file for absolute module paths in module_path keys."""
|
|
if _is_file_allowed(filename):
|
|
return True
|
|
|
|
try:
|
|
with open(filename, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
except Exception as e:
|
|
print(f"Error reading {filename}: {e}")
|
|
return False
|
|
|
|
errors = []
|
|
|
|
def _check_value(obj, path=""):
|
|
if isinstance(obj, dict):
|
|
for key, value in obj.items():
|
|
current_path = f"{path}.{key}" if path else key
|
|
# Only check values under module_path keys
|
|
if key in ("module_path", "full_search_module") and isinstance(
|
|
value, str
|
|
):
|
|
if (
|
|
value.startswith(ABSOLUTE_BASE + ".")
|
|
and value not in LEGITIMATE_ABSOLUTE_REFS
|
|
):
|
|
errors.append((current_path, value))
|
|
elif isinstance(value, (dict, list)):
|
|
_check_value(value, current_path)
|
|
elif isinstance(obj, list):
|
|
for i, item in enumerate(obj):
|
|
_check_value(item, f"{path}[{i}]")
|
|
|
|
_check_value(data)
|
|
|
|
if errors:
|
|
print(f"\n{filename}:")
|
|
for key_path, value in errors:
|
|
print(
|
|
f' Key "{key_path}": Absolute module path "{value}" found. '
|
|
f"Use a relative path (starting with '.') instead."
|
|
)
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Check all provided files for absolute module paths."""
|
|
if len(sys.argv) < 2:
|
|
print("Usage: check-absolute-module-paths.py <file1> <file2> ...")
|
|
sys.exit(1)
|
|
|
|
files_to_check = sys.argv[1:]
|
|
has_errors = False
|
|
|
|
for filename in files_to_check:
|
|
if filename.endswith(".py"):
|
|
if not check_python_file(filename):
|
|
has_errors = True
|
|
elif filename.endswith(".json"):
|
|
if not check_json_file(filename):
|
|
has_errors = True
|
|
|
|
if has_errors:
|
|
print("\n" + "=" * 70)
|
|
print("Module Path Security: Absolute module paths detected!")
|
|
print("=" * 70)
|
|
print("\nModule paths must use relative imports (starting with '.')")
|
|
print("instead of absolute paths starting with 'local_deep_research.'.")
|
|
print("\nExamples:")
|
|
print(
|
|
' BAD: "local_deep_research.web_search_engines'
|
|
'.engines.search_engine_local"'
|
|
)
|
|
print(' GOOD: ".engines.search_engine_local"')
|
|
print("\nSee: src/local_deep_research/security/module_whitelist.py")
|
|
sys.exit(1)
|
|
else:
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|