344 lines
12 KiB
Python
344 lines
12 KiB
Python
import ast
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_DIR = ROOT / 'app'
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
from runtime_security import ensure_private_directory, harden_private_file
|
|
|
|
|
|
DIRECT_ENTRYPOINTS = {
|
|
'audit_github_tokens.py',
|
|
'child_bootstrap.py',
|
|
'console_runner.py',
|
|
'container_runtime.py',
|
|
'dashboard.py',
|
|
'docker_depth_operator.py',
|
|
'docker_depth_report.py',
|
|
'docker_shadow.py',
|
|
'keycheck_accounting_smoke.py',
|
|
'keycheck_runner.py',
|
|
'janitor.py',
|
|
'jsonl_projector.py',
|
|
'result_ingester.py',
|
|
'keycheckers/anthropic/anthropicKeycheck.py',
|
|
'keycheckers/aws/awsKeycheck.py',
|
|
'keycheckers/azure/azureKeycheck.py',
|
|
'keycheckers/deepseek/deepseekKeycheck.py',
|
|
'keycheckers/dockerhub/dockerhubKeycheck.py',
|
|
'keycheckers/gcp/gcpKeycheck.py',
|
|
'keycheckers/gemini/geminiKeycheck.py',
|
|
'keycheckers/github/githubKeycheck.py',
|
|
'keycheckers/gitlab/gitlabKeycheck.py',
|
|
'keycheckers/groq/groqKeycheck.py',
|
|
'keycheckers/huggingface/huggingfaceKeycheck.py',
|
|
'keycheckers/kimi/kimiKeycheck.py',
|
|
'keycheckers/openai/Keycheck.py',
|
|
'keycheckers/openrouter/OpenrouterKeycheck.py',
|
|
'keycheckers/provider_resolver/providerResolverKeycheck.py',
|
|
'keycheckers/qwen/qwenKeycheck.py',
|
|
'keycheckers/replicate/replicateKeycheck.py',
|
|
'keycheckers/xai/xaiKeycheck.py',
|
|
'keycheckers/zai/zaiKeycheck.py',
|
|
'migrate_layout.py',
|
|
'migrate_observability_db.py',
|
|
'migrate_runtime_safety.py',
|
|
'optimize_dashboard_db.py',
|
|
'owned_process.py',
|
|
'postgres_runtime.py',
|
|
'remote_worker_bootstrap.py',
|
|
'remote_worker_client.py',
|
|
'runtime_bootstrap.py',
|
|
'scanner_error_policy_smoke.py',
|
|
'supervisor.py',
|
|
'sync_alive_github_tokens.py',
|
|
'worker_api.py',
|
|
'worker_cli.py',
|
|
'worker_package_builder.py',
|
|
}
|
|
|
|
CORE_ENTRYPOINTS = {
|
|
'child_bootstrap.py',
|
|
'console_runner.py',
|
|
'container_runtime.py',
|
|
'dashboard.py',
|
|
'docker_shadow.py',
|
|
'keycheck_runner.py',
|
|
'janitor.py',
|
|
'jsonl_projector.py',
|
|
'result_ingester.py',
|
|
'owned_process.py',
|
|
'postgres_runtime.py',
|
|
'remote_worker_bootstrap.py',
|
|
'remote_worker_client.py',
|
|
'runtime_bootstrap.py',
|
|
'supervisor.py',
|
|
'worker_api.py',
|
|
'worker_cli.py',
|
|
}
|
|
|
|
|
|
def _is_main_name(node):
|
|
return isinstance(node, ast.Name) and node.id == '__name__'
|
|
|
|
|
|
def _is_main_value(node):
|
|
return isinstance(node, ast.Constant) and node.value == '__main__'
|
|
|
|
|
|
def _has_main_guard(tree):
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, ast.Compare) or len(node.ops) != 1 or not isinstance(node.ops[0], ast.Eq):
|
|
continue
|
|
if len(node.comparators) != 1:
|
|
continue
|
|
right = node.comparators[0]
|
|
if (_is_main_name(node.left) and _is_main_value(right)) or (
|
|
_is_main_value(node.left) and _is_main_name(right)
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _bytecode_assignment_line(tree):
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Constant) or node.value.value is not True:
|
|
continue
|
|
for target in node.targets:
|
|
if (
|
|
isinstance(target, ast.Attribute)
|
|
and target.attr == 'dont_write_bytecode'
|
|
and isinstance(target.value, ast.Name)
|
|
and target.value.id == 'sys'
|
|
):
|
|
return node.lineno
|
|
return None
|
|
|
|
|
|
def _local_import_lines(tree, local_names):
|
|
lines = []
|
|
for node in tree.body:
|
|
if isinstance(node, ast.Import):
|
|
if any(alias.name.partition('.')[0] in local_names for alias in node.names):
|
|
lines.append(node.lineno)
|
|
elif isinstance(node, ast.ImportFrom):
|
|
root = (node.module or '').partition('.')[0]
|
|
if node.level or root in local_names:
|
|
lines.append(node.lineno)
|
|
return lines
|
|
|
|
|
|
def _has_fail_closed_check(tree, assignment_line):
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.If) or node.lineno <= assignment_line:
|
|
continue
|
|
checks_policy = any(
|
|
isinstance(item, ast.Attribute)
|
|
and item.attr == 'dont_write_bytecode'
|
|
and isinstance(item.value, ast.Name)
|
|
and item.value.id == 'sys'
|
|
for item in ast.walk(node.test)
|
|
)
|
|
if checks_policy and any(isinstance(item, ast.Raise) for item in ast.walk(node)):
|
|
return True
|
|
return False
|
|
|
|
|
|
class DirectEntrypointPolicyTests(unittest.TestCase):
|
|
def test_inventory_sets_bytecode_policy_before_local_imports(self):
|
|
parsed = {}
|
|
discovered = set()
|
|
for path in APP_DIR.rglob('*.py'):
|
|
tree = ast.parse(path.read_text(encoding='utf-8'))
|
|
relative = path.relative_to(APP_DIR).as_posix()
|
|
parsed[relative] = tree
|
|
if _has_main_guard(tree):
|
|
discovered.add(relative)
|
|
|
|
self.assertEqual(discovered, DIRECT_ENTRYPOINTS)
|
|
local_names = {path.stem for path in APP_DIR.rglob('*.py')}
|
|
local_names.update(path.name for path in APP_DIR.iterdir() if path.is_dir())
|
|
for relative in sorted(discovered):
|
|
with self.subTest(entrypoint=relative):
|
|
tree = parsed[relative]
|
|
assignment_line = _bytecode_assignment_line(tree)
|
|
self.assertIsNotNone(assignment_line)
|
|
local_imports = _local_import_lines(tree, local_names)
|
|
if local_imports:
|
|
self.assertLess(assignment_line, min(local_imports))
|
|
if relative in CORE_ENTRYPOINTS:
|
|
self.assertTrue(_has_fail_closed_check(tree, assignment_line))
|
|
|
|
def test_supported_direct_commands_create_no_application_bytecode_without_dash_b(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
root = Path(temp_dir)
|
|
app_dir = root / 'app'
|
|
shutil.copytree(
|
|
APP_DIR,
|
|
app_dir,
|
|
ignore=shutil.ignore_patterns('__pycache__', '*.pyc'),
|
|
)
|
|
authority_runtime = root / 'runtime'
|
|
ensure_private_directory(str(authority_runtime), reject_reparse=True)
|
|
authority_files = [authority_runtime / 'check-openrouter-keys.ps1']
|
|
shutil.copy2(ROOT / 'runtime' / 'check-openrouter-keys.ps1', authority_files[0])
|
|
git_dir = authority_runtime / 'git' / 'cmd'
|
|
ensure_private_directory(str(git_dir), reject_reparse=True)
|
|
git_executable = git_dir / 'git.exe'
|
|
git_executable.write_bytes(b'fixture Git executable')
|
|
authority_files.append(git_executable)
|
|
for name in ('start_runtime.ps1', 'stop_runtime.ps1'):
|
|
target = root / name
|
|
shutil.copy2(ROOT / name, target)
|
|
authority_files.append(target)
|
|
for path in authority_files:
|
|
harden_private_file(str(path))
|
|
|
|
data_dir = root / 'fixture-data'
|
|
postgres_dir = data_dir / 'postgres'
|
|
ensure_private_directory(str(data_dir), reject_reparse=True)
|
|
ensure_private_directory(str(postgres_dir), reject_reparse=True)
|
|
executable = data_dir / ('trufflehog.exe' if os.name == 'nt' else 'trufflehog')
|
|
executable.write_bytes(b'fixture')
|
|
harden_private_file(str(executable))
|
|
state_file = data_dir / 'runner-state.json'
|
|
state_file.write_text('{"version": 1, "sources": {}}\n', encoding='ascii')
|
|
harden_private_file(str(state_file))
|
|
|
|
common_directory = str(data_dir)
|
|
bundle_directory = data_dir / 'bundles'
|
|
for path in (
|
|
bundle_directory,
|
|
bundle_directory / 'tmp',
|
|
bundle_directory / 'ready',
|
|
bundle_directory / 'quarantine',
|
|
):
|
|
ensure_private_directory(str(path), reject_reparse=True)
|
|
config = {
|
|
'global': {
|
|
'root_dir': common_directory,
|
|
'project_dir': common_directory,
|
|
'runtime_dir': common_directory,
|
|
'result_spool_dir': common_directory,
|
|
'result_bundle_dir': str(bundle_directory),
|
|
'results_dir': common_directory,
|
|
'queue_dir': common_directory,
|
|
'state_dir': common_directory,
|
|
'log_dir': common_directory,
|
|
'control_dir': common_directory,
|
|
'keycheck_dir': common_directory,
|
|
'postman_cache_dir': common_directory,
|
|
'gharchive_cache_dir': common_directory,
|
|
'work_dir': common_directory,
|
|
'state_file': str(state_file),
|
|
'trufflehog_path': str(executable),
|
|
},
|
|
'sources': {},
|
|
}
|
|
config_path = root / 'read-only-config.yaml'
|
|
config_path.write_text(json.dumps(config), encoding='ascii')
|
|
harden_private_file(str(config_path))
|
|
|
|
environment = os.environ.copy()
|
|
for key in list(environment):
|
|
normalized = key.upper()
|
|
if normalized.startswith('TRUF_SUPERVISOR_') or normalized in {
|
|
'SCANNER_SUPERVISED',
|
|
'TRUF_MANAGED_POSTGRES_DSN',
|
|
'SCANNER_DB_URL',
|
|
'DATABASE_URL',
|
|
'SCANNER_DASHBOARD_DB_URL',
|
|
'KEYCHECK_DB_URL',
|
|
'PYTHONDONTWRITEBYTECODE',
|
|
'PYTHONPYCACHEPREFIX',
|
|
'PYTHONPATH',
|
|
}:
|
|
environment.pop(key, None)
|
|
|
|
commands = (
|
|
(
|
|
'migration help',
|
|
[sys.executable, str(app_dir / 'migrate_runtime_safety.py'), '--help'],
|
|
0,
|
|
'usage:',
|
|
),
|
|
(
|
|
'keycheck print plan',
|
|
[
|
|
sys.executable,
|
|
str(app_dir / 'keycheck_runner.py'),
|
|
'--config',
|
|
str(config_path),
|
|
'--service',
|
|
'github',
|
|
'--no-summary',
|
|
'--print-plan',
|
|
],
|
|
0,
|
|
'mode: run-keychecks',
|
|
),
|
|
(
|
|
'console show state',
|
|
[
|
|
sys.executable,
|
|
str(app_dir / 'console_runner.py'),
|
|
'--config',
|
|
str(config_path),
|
|
'--show-state',
|
|
],
|
|
0,
|
|
'State file:',
|
|
),
|
|
(
|
|
'provider authority rejection',
|
|
[
|
|
sys.executable,
|
|
str(app_dir / 'keycheckers' / 'github' / 'githubKeycheck.py'),
|
|
'--input',
|
|
str(root / 'missing.jsonl'),
|
|
],
|
|
1,
|
|
'direct mutation is retired',
|
|
),
|
|
)
|
|
|
|
for name, command, expected_code, expected_output in commands:
|
|
with self.subTest(command=name):
|
|
self.assertNotIn('-B', command)
|
|
completed = subprocess.run(
|
|
command,
|
|
cwd=root,
|
|
env=environment,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
)
|
|
if expected_code == 0:
|
|
self.assertEqual(completed.returncode, 0, completed.stdout)
|
|
else:
|
|
self.assertNotEqual(completed.returncode, 0, completed.stdout)
|
|
self.assertIn(expected_output, completed.stdout)
|
|
self.assertEqual(list(app_dir.rglob('*.pyc')), [])
|
|
self.assertEqual(
|
|
[path for path in app_dir.rglob('__pycache__') if path.is_dir()],
|
|
[],
|
|
)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|