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

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()