import hashlib import hmac import json import os import shutil import socket import stat from db_backend import parse_postgres_url from process_identity import verify_retained_process from runtime_security import ( canonical_path, private_file_ready, read_private_json, reject_reparse_components, require_trusted_native_executable, sha256_file, ) CODE_MANIFEST_SCHEMA = 5 APPLICATION_IMPORT_SUFFIXES = ('.py', '.pyw', '.pyc', '.pyd') CONTROL_SCHEMA = 1 DISCOVERY_PRODUCER_ROLE = 'discovery-producer' DISCOVERY_PRODUCER_SOURCES = ('gitlab', 'dockerhub', 'huggingface') PHASE_INACTIVE = 'INACTIVE' PHASE_ACTIVATING = 'ACTIVATING' PHASE_ACTIVE = 'ACTIVE' PHASE_STOPPING = 'STOPPING' PHASE_FAILED_HOLD = 'FAILED_HOLD' LIFECYCLE_PHASES = { PHASE_INACTIVE, PHASE_ACTIVATING, PHASE_ACTIVE, PHASE_STOPPING, PHASE_FAILED_HOLD, } # These files collectively decide process ownership, database authority, and # what data may be launched or persisted by the supervisor. CODE_AUTHORITY_FILES = ( 'owned_process.py', 'supervisor.py', 'supervisor_instance.py', 'console_runner.py', 'scanner.py', 'docker_shadow.py', 'keycheck_runner.py', 'dashboard.py', 'postgres_runtime.py', 'process_identity.py', 'runtime_security.py', 'scanner_db.py', 'db_backend.py', 'result_spool.py', 'janitor.py', 'result_bundle.py', 'result_ingester.py', 'jsonl_projector.py', 'admin_api.py', 'worker_api.py', 'worker_assignment.py', 'worker_package.py', 'scan_execution.py', 'keycheck_candidates.py', 'paths.py', 'target_identity.py', 'lifecycle_authority.py', 'audit_github_tokens.py', 'sync_alive_github_tokens.py', 'child_bootstrap.py', 'runtime_bootstrap.py', 'runtime_document.py', 'capacity_model.py', 'runtime_document_io.py', 'managed_files.py', 'host_agent_client.py', 'host_agent_protocol.py', 'host_agent_reconcile.py', 'host_agent_server.py', 'host_agent_apply.py', 'host_agent_lifecycle.py', 'host_agent_runtime.py', 'host_agent_state.py', 'worker_contracts.py', 'worker_assignment_runner.py', ) REMOTE_WORKER_CODE_AUTHORITY_FILES = ( 'db_backend.py', 'docker_depth_experiment.py', 'janitor.py', 'keycheck_candidates.py', 'lifecycle_authority.py', 'owned_process.py', 'paths.py', 'process_identity.py', 'query_policy.py', 'remote_worker_bootstrap.py', 'remote_worker_client.py', 'result_bundle.py', 'result_spool.py', 'runtime_security.py', 'scan_execution.py', 'scanner.py', 'scanner_db.py', 'supervisor_instance.py', 'target_identity.py', 'worker_contracts.py', 'worker_assignment_runner.py', 'worker_cli.py', 'worker_local_state.py', 'worker_supervisor.py', 'worker_package.py', ) EXTERNAL_CODE_AUTHORITY_FILES = ( '../runtime/check-openrouter-keys.ps1', '../start_core_runtime.ps1', '../start_runtime.ps1', '../stop_runtime.ps1', ) if os.name == 'nt' else () # The launchers execute before a manifest can be captured. First-launch trust # therefore requires offline ACL hardening; manifests only detect later drift. TRUFFLEHOG_MANIFEST_NAME = 'trufflehog' GIT_MANIFEST_NAME = 'git' CHILD_INSTANCE_FILE_ENV = 'TRUF_SUPERVISOR_INSTANCE_FILE' CHILD_INSTANCE_ID_ENV = 'TRUF_SUPERVISOR_INSTANCE_ID' CHILD_TOKEN_ENV = 'TRUF_SUPERVISOR_TOKEN' CHILD_CONFIG_HASH_ENV = 'TRUF_SUPERVISOR_CONFIG_SHA256' CHILD_SCRIPT_HASH_ENV = 'TRUF_SUPERVISOR_SHA256' CHILD_MANIFEST_HASH_ENV = 'TRUF_SUPERVISOR_CODE_MANIFEST_SHA256' CHILD_DSN_HASH_ENV = 'TRUF_SUPERVISOR_DSN_SHA256' CHILD_KIND_ENV = 'TRUF_SUPERVISOR_CHILD_KIND' PRIVATE_CHILD_ENV_KEYS = ( CHILD_INSTANCE_FILE_ENV, CHILD_INSTANCE_ID_ENV, CHILD_TOKEN_ENV, CHILD_CONFIG_HASH_ENV, CHILD_SCRIPT_HASH_ENV, CHILD_MANIFEST_HASH_ENV, CHILD_DSN_HASH_ENV, CHILD_KIND_ENV, 'SCANNER_SUPERVISED', 'TRUF_MANAGED_POSTGRES_DSN', 'SCANNER_DB_URL', 'DATABASE_URL', 'SCANNER_DASHBOARD_DB_URL', 'KEYCHECK_DB_URL', 'TRUF_DASHBOARD_CANONICAL_LAUNCH', 'TRUF_DASHBOARD_HOST', ) _LIBPQ_PRIVATE_ENV_KEYS = frozenset({ 'PGPASSWORD', 'PGUSER', 'PGDATABASE', 'PGHOST', 'PGHOSTADDR', 'PGPORT', 'PGSERVICE', 'PGSERVICEFILE', 'PGPASSFILE', 'PGOPTIONS', 'PGSSLMODE', 'PGSSLKEY', 'PGSSLCERT', 'PGSSLROOTCERT', }) _PRIVATE_EXTERNAL_ENV_KEYS = frozenset(PRIVATE_CHILD_ENV_KEYS) | _LIBPQ_PRIVATE_ENV_KEYS class LifecycleAuthorityError(ValueError): pass def _manifest_payload(manifest): return json.dumps( manifest, ensure_ascii=True, sort_keys=True, separators=(',', ':'), ).encode('utf-8') def code_manifest_sha256(manifest): return hashlib.sha256(_manifest_payload(manifest)).hexdigest() def _is_reparse_point(path): details = os.lstat(path) if stat.S_ISLNK(details.st_mode): return True attributes = getattr(details, 'st_file_attributes', 0) reparse_attribute = getattr(stat, 'FILE_ATTRIBUTE_REPARSE_POINT', 0) return bool(attributes & reparse_attribute) or getattr(os.path, 'isjunction', lambda _path: False)(path) def _application_root(app_dir=None): candidate = os.path.abspath(os.fspath(app_dir or os.path.dirname(os.path.abspath(__file__)))) try: details = os.lstat(candidate) if _is_reparse_point(candidate): raise LifecycleAuthorityError(f'application root reparse point is forbidden: {candidate}') except OSError as exc: raise LifecycleAuthorityError(f'application root is unavailable: {candidate}') from exc if not stat.S_ISDIR(details.st_mode): raise LifecycleAuthorityError(f'application root is not a directory: {candidate}') return canonical_path(candidate) def _external_authority_expected_path(root, name): if name not in EXTERNAL_CODE_AUTHORITY_FILES: raise LifecycleAuthorityError(f'code authority path is not an allowed external: {name}') project_root = os.path.normcase(os.path.abspath(os.path.dirname(root))) candidate = os.path.normcase(os.path.abspath(os.path.join(root, *name.split('/')))) try: contained = candidate != project_root and os.path.commonpath((project_root, candidate)) == project_root except ValueError: contained = False if not contained: raise LifecycleAuthorityError(f'external code authority path escapes the project root: {name}') return candidate def _require_external_authority_file(root, name): path = _external_authority_expected_path(root, name) try: reject_reparse_components(path) except OSError as exc: raise LifecycleAuthorityError(f'external code authority reparse point is forbidden: {name}') from exc try: details = os.stat(path, follow_symlinks=False) except OSError as exc: raise LifecycleAuthorityError(f'required external code authority file is absent: {name}') from exc if not stat.S_ISREG(details.st_mode): raise LifecycleAuthorityError(f'external code authority file is not regular: {name}') if canonical_path(path) != path: raise LifecycleAuthorityError(f'external code authority path is not exact: {name}') return path def _application_code_files(root): """Return the exact importable application code surface.""" names = [] def raise_walk_error(exc): raise LifecycleAuthorityError(f'unable to inspect the application root: {exc}') from exc for current, directories, files in os.walk(root, followlinks=False, onerror=raise_walk_error): for name in directories: candidate = os.path.join(current, name) try: linked = _is_reparse_point(candidate) except OSError as exc: raise LifecycleAuthorityError(f'unable to inspect application directory: {candidate}') from exc if linked: relative = os.path.relpath(candidate, root).replace(os.sep, '/') raise LifecycleAuthorityError(f'application directory reparse point is forbidden: {relative}') relative_current = os.path.relpath(current, root) in_cache = any(part.lower() == '__pycache__' for part in relative_current.split(os.sep)) suffixes = ('.pyc',) if in_cache else APPLICATION_IMPORT_SUFFIXES for name in files: source_path = os.path.abspath(os.path.join(current, name)) try: linked = _is_reparse_point(source_path) except OSError as exc: raise LifecycleAuthorityError(f'unable to inspect application file: {source_path}') from exc if linked: relative = os.path.relpath(source_path, root).replace(os.sep, '/') raise LifecycleAuthorityError(f'application file reparse point is forbidden: {relative}') if not name.lower().endswith(suffixes): continue path = canonical_path(source_path) try: if os.path.commonpath((root, path)) != root: raise LifecycleAuthorityError(f'code authority path escapes the application root: {path}') except ValueError as exc: raise LifecycleAuthorityError(f'code authority path escapes the application root: {path}') from exc names.append(os.path.relpath(source_path, root).replace(os.sep, '/')) return sorted(names) def code_authority_file_names(app_dir=None, existing_only=False): root = _application_root(app_dir) names = list(_application_code_files(root)) for name in (*CODE_AUTHORITY_FILES, *EXTERNAL_CODE_AUTHORITY_FILES): path = canonical_path(os.path.join(root, name)) if not existing_only or os.path.isfile(path): names.append(name) return tuple(dict.fromkeys(names)) def resolve_manifest_executable(value, *, name=TRUFFLEHOG_MANIFEST_NAME, app_dir=None): if name not in {TRUFFLEHOG_MANIFEST_NAME, GIT_MANIFEST_NAME}: raise LifecycleAuthorityError('unsupported manifested executable') label = 'TruffleHog' if name == TRUFFLEHOG_MANIFEST_NAME else 'Git' text = str(value or '').strip() if not text: if name == GIT_MANIFEST_NAME: text = 'git' if os.name == 'nt': private_git = os.path.join(os.path.dirname(_application_root(app_dir)), 'runtime', 'git', 'cmd', 'git.exe') try: reject_reparse_components(private_git) except OSError as exc: raise LifecycleAuthorityError('project-private Git path contains a reparse point') from exc text = private_git if os.path.lexists(private_git) else text else: from paths import default_trufflehog_path text = default_trufflehog_path() candidate = shutil.which(text) if not os.path.isabs(text) and not any(sep in text for sep in ('/', '\\')) else text if not candidate: raise LifecycleAuthorityError(f'configured {label} executable is unavailable: {text}') if os.name != 'nt': if not os.path.isabs(candidate) or candidate != os.path.normpath(candidate): raise LifecycleAuthorityError(f'configured {label} executable path must be exact and absolute: {candidate}') try: reject_reparse_components(candidate) except (OSError, ValueError) as exc: raise LifecycleAuthorityError(f'configured {label} executable path is unsafe: {candidate}') from exc path = canonical_path(candidate) if not os.path.isfile(path): raise LifecycleAuthorityError(f'configured {label} executable is not a regular file: {path}') return path def manifest_authority_paths( app_dir=None, trufflehog_path=None, policy_paths=None, existing_only=False, *, git_path=None, include_executables=True, ): """List authority paths; exclude executables when applying private-file policy.""" root = _application_root(app_dir) paths = [] names = list(code_authority_file_names(root, existing_only=existing_only)) # Required externals may never disappear from read-only preflight or an # offline hardening plan, even when optional paths use existing_only. names.extend(name for name in EXTERNAL_CODE_AUTHORITY_FILES if name not in names) for name in names: if name in EXTERNAL_CODE_AUTHORITY_FILES: paths.append(_require_external_authority_file(root, name)) continue path = canonical_path(os.path.join(root, *name.split('/'))) if os.path.isfile(path): paths.append(path) elif not existing_only: raise LifecycleAuthorityError(f'code authority file is absent or outside the application root: {name}') if include_executables: if trufflehog_path or not existing_only: try: paths.append(resolve_manifest_executable(trufflehog_path)) except LifecycleAuthorityError: if not existing_only or trufflehog_path: raise # Git is required even in preflight/offline hardening's existing-only mode. paths.append(resolve_manifest_executable(git_path, name=GIT_MANIFEST_NAME, app_dir=root)) for value in policy_paths or (): if not value: continue path = canonical_path(value) if os.path.isfile(path): paths.append(path) elif not existing_only: raise LifecycleAuthorityError(f'configured policy authority file is unavailable: {path}') return tuple(dict.fromkeys(paths)) def build_code_manifest( app_dir=None, trufflehog_path=None, policy_paths=None, *, git_path=None, include_trufflehog=True, ): root = _application_root(app_dir) files = {} for name in code_authority_file_names(root): path = ( _require_external_authority_file(root, name) if name in EXTERNAL_CODE_AUTHORITY_FILES else canonical_path(os.path.join(root, *name.split('/'))) ) try: contained = os.path.commonpath((root, path)) == root except ValueError: contained = False explicitly_external = ( name in EXTERNAL_CODE_AUTHORITY_FILES and path == _external_authority_expected_path(root, name) ) if (not contained and not explicitly_external) or not os.path.isfile(path): raise LifecycleAuthorityError(f'code authority file is absent or outside the application root: {name}') files[name] = {'path': path, 'sha256': sha256_file(path)} git_executable = resolve_manifest_executable(git_path, name=GIT_MANIFEST_NAME, app_dir=root) executables = { GIT_MANIFEST_NAME: {'path': git_executable, 'sha256': sha256_file(git_executable)}, } if include_trufflehog: executable = resolve_manifest_executable(trufflehog_path) executables[TRUFFLEHOG_MANIFEST_NAME] = { 'path': executable, 'sha256': sha256_file(executable), } assets = {} for value in policy_paths or (): if not value: continue path = canonical_path(value) if not os.path.isfile(path): raise LifecycleAuthorityError(f'configured policy authority file is unavailable: {path}') assets[path] = {'path': path, 'sha256': sha256_file(path)} return { 'schema': CODE_MANIFEST_SCHEMA, 'root': root, 'files': files, 'executables': executables, 'assets': assets, } def normalize_code_manifest(manifest, *, required_names=None, external_names=None): if not isinstance(manifest, dict) or manifest.get('schema') != CODE_MANIFEST_SCHEMA: raise LifecycleAuthorityError('unsupported code authority manifest schema') root_value = manifest.get('root') or '' root = _application_root(root_value) if root_value else '' values = manifest.get('files') expected_names = set(values) if isinstance(values, dict) else set() external_names = set( EXTERNAL_CODE_AUTHORITY_FILES if external_names is None else external_names ) if not external_names <= set(EXTERNAL_CODE_AUTHORITY_FILES): raise LifecycleAuthorityError('code authority manifest has unsupported external files') required_names = set(CODE_AUTHORITY_FILES if required_names is None else required_names) required_names.update(external_names) if not root or not isinstance(values, dict) or not required_names.issubset(expected_names): raise LifecycleAuthorityError('code authority manifest has an incomplete file set') files = {} for name in sorted(expected_names): value = values.get(name) if not isinstance(value, dict): raise LifecycleAuthorityError(f'invalid code authority entry: {name}') if name in external_names: path = os.path.normcase(os.path.abspath(os.fspath(value.get('path') or ''))) expected_path = _external_authority_expected_path(root, name) else: path = canonical_path(value.get('path') or '') expected_path = canonical_path(os.path.join(root, *name.split('/'))) try: contained = os.path.commonpath((root, expected_path)) == root except ValueError: contained = False if not contained: raise LifecycleAuthorityError(f'code authority path is not an allowed external: {name}') digest = str(value.get('sha256') or '') if path != expected_path: raise LifecycleAuthorityError(f'code authority path mismatch: {name}') if len(digest) != 64 or any(ch not in '0123456789abcdef' for ch in digest): raise LifecycleAuthorityError(f'invalid code authority digest: {name}') files[name] = {'path': path, 'sha256': digest} executables = manifest.get('executables') executable_names = set(executables) if isinstance(executables, dict) else set() if executable_names not in ( {GIT_MANIFEST_NAME}, {TRUFFLEHOG_MANIFEST_NAME, GIT_MANIFEST_NAME}, ): raise LifecycleAuthorityError('code authority manifest has an incomplete executable set') normalized_executables = {} for name, label in ((TRUFFLEHOG_MANIFEST_NAME, 'TruffleHog'), (GIT_MANIFEST_NAME, 'Git')): if name not in executables: continue executable = executables[name] if not isinstance(executable, dict): raise LifecycleAuthorityError(f'invalid {label} authority entry') raw_path = executable.get('path') executable_digest = str(executable.get('sha256') or '') if not isinstance(raw_path, str) or not os.path.isabs(raw_path) or '\x00' in raw_path or len(executable_digest) != 64 or any(ch not in '0123456789abcdef' for ch in executable_digest): raise LifecycleAuthorityError(f'invalid {label} authority identity') executable_path = canonical_path(raw_path) if os.name != 'nt' and executable_path != raw_path: raise LifecycleAuthorityError(f'{label} authority path is not exact') normalized_executables[name] = {'path': executable_path, 'sha256': executable_digest} assets_value = manifest.get('assets') if not isinstance(assets_value, dict): raise LifecycleAuthorityError('code authority manifest has an invalid asset set') assets = {} for name in sorted(assets_value): value = assets_value[name] if not isinstance(value, dict): raise LifecycleAuthorityError(f'invalid policy authority entry: {name}') path = canonical_path(value.get('path') or '') digest = str(value.get('sha256') or '') if name != path or not os.path.isabs(path) or len(digest) != 64 or any(ch not in '0123456789abcdef' for ch in digest): raise LifecycleAuthorityError(f'invalid policy authority identity: {name}') assets[name] = {'path': path, 'sha256': digest} return { 'schema': CODE_MANIFEST_SCHEMA, 'root': root, 'files': files, 'executables': normalized_executables, 'assets': assets, } def verify_code_manifest( manifest, expected_sha256=None, require_private_acl=False, *, required_names=None, external_names=None, ): normalized = normalize_code_manifest( manifest, required_names=required_names, external_names=external_names, ) digest = code_manifest_sha256(normalized) if expected_sha256 and not hmac.compare_digest(digest, str(expected_sha256)): raise LifecycleAuthorityError('code authority manifest digest mismatch') for name in (EXTERNAL_CODE_AUTHORITY_FILES if external_names is None else external_names): if normalized['files'][name]['path'] != _require_external_authority_file(normalized['root'], name): raise LifecycleAuthorityError(f'code authority path mismatch: {name}') current_code = set(_application_code_files(normalized['root'])) manifested_code = { name for name, value in normalized['files'].items() if name.lower().endswith(APPLICATION_IMPORT_SUFFIXES) and os.path.commonpath((normalized['root'], value['path'])) == normalized['root'] } if current_code != manifested_code: added = sorted(current_code - manifested_code) removed = sorted(manifested_code - current_code) detail = added[0] if added else removed[0] if removed else 'unknown' raise LifecycleAuthorityError(f'application code authority file set drifted: {detail}') entries = [(name, value, False) for name, value in normalized['files'].items()] + [ (f'executable:{name}', value, True) for name, value in normalized['executables'].items() ] + [(f'asset:{name}', value, False) for name, value in normalized['assets'].items()] for name, value, native in entries: if require_private_acl: if native and os.name != 'nt': try: require_trusted_native_executable(value['path']) except (OSError, ValueError) as exc: raise LifecycleAuthorityError(f'code authority executable is not trusted: {name}: {exc}') from exc elif not private_file_ready(value['path']): raise LifecycleAuthorityError(f'code authority ACL is not exact-private: {name}') try: current = sha256_file(value['path']) except OSError as exc: raise LifecycleAuthorityError(f'unable to verify code authority file: {name}') from exc if not hmac.compare_digest(current, value['sha256']): raise LifecycleAuthorityError(f'code authority drifted: {name}') return normalized def dsn_sha256(dsn): value = str(dsn or '') return hashlib.sha256(value.encode('utf-8')).hexdigest() if value else '' def supervised_child_environment(metadata, canonical_dsn, child_kind): return { 'SCANNER_SUPERVISED': '1', CHILD_INSTANCE_FILE_ENV: str(metadata['instance_file']), CHILD_INSTANCE_ID_ENV: str(metadata['instance_id']), CHILD_TOKEN_ENV: str(metadata['token']), CHILD_CONFIG_HASH_ENV: str(metadata['config_sha256']), CHILD_SCRIPT_HASH_ENV: str(metadata['supervisor_sha256']), CHILD_MANIFEST_HASH_ENV: str(metadata['code_manifest_sha256']), CHILD_DSN_HASH_ENV: str(metadata.get('canonical_dsn_sha256') or ''), CHILD_KIND_ENV: str(child_kind), 'TRUF_MANAGED_POSTGRES_DSN': str(canonical_dsn or ''), } def strip_supervisor_credentials(env): for key in list(env): normalized = str(key).upper() if normalized.startswith('TRUF_POSTGRES_') or normalized in _PRIVATE_EXTERNAL_ENV_KEYS: env.pop(key, None) return env def _send_handshake(metadata, timeout=3): control = metadata.get('control') or {} request = { 'schema': CONTROL_SCHEMA, 'instance_id': metadata['instance_id'], 'token': metadata['token'], 'action': 'handshake', } payload = json.dumps(request, ensure_ascii=True, separators=(',', ':')).encode('utf-8') + b'\n' with socket.create_connection((control.get('host'), int(control.get('port') or 0)), timeout=timeout) as sock: sock.settimeout(timeout) sock.sendall(payload) sock.shutdown(socket.SHUT_WR) chunks = [] total = 0 while True: chunk = sock.recv(65536) if not chunk: break total += len(chunk) if total > 1024 * 1024: raise LifecycleAuthorityError('supervisor handshake response is too large') chunks.append(chunk) try: response = json.loads(b''.join(chunks).decode('utf-8')) except (UnicodeDecodeError, json.JSONDecodeError) as exc: raise LifecycleAuthorityError('invalid supervisor handshake response') from exc if ( not isinstance(response, dict) or response.get('schema') != CONTROL_SCHEMA or response.get('instance_id') != metadata['instance_id'] or response.get('ok') is not True or not isinstance(response.get('result'), dict) ): raise LifecycleAuthorityError('authenticated supervisor handshake failed') return response['result'] def _send_handshake_with_timeout_retry(metadata, timeout_retries=0): retries = min(1, max(0, int(timeout_retries or 0))) for attempt in range(retries + 1): try: return _send_handshake(metadata) except TimeoutError: if attempt >= retries: raise def verify_supervisor_command_line(arguments, supervisor_path, config_path): """Verify the unique script binding and config value retained by the OS.""" arguments = [str(argument) for argument in arguments] options = { '--runtime-bootstrap-entrypoint': [], '--config': [], } option_value_indices = set() for index, argument in enumerate(arguments): for option in options: if argument == option: value = arguments[index + 1] if index + 1 < len(arguments) else '' options[option].append(value) if index + 1 < len(arguments): option_value_indices.add(index + 1) elif argument.startswith(option + '='): options[option].append(argument.split('=', 1)[1]) expected_supervisor = canonical_path(supervisor_path) direct_bindings = [] for index, argument in enumerate(arguments): if index in option_value_indices or not argument or argument.startswith('-'): continue try: if canonical_path(argument) == expected_supervisor: direct_bindings.append(argument) except (OSError, TypeError, ValueError): continue bindings = options['--runtime-bootstrap-entrypoint'] + direct_bindings if len(bindings) != 1: raise LifecycleAuthorityError('supervisor command line must contain exactly one bound supervisor script') try: binding_matches = canonical_path(bindings[0]) == expected_supervisor except (OSError, TypeError, ValueError): binding_matches = False if not binding_matches: raise LifecycleAuthorityError('supervisor command line bound supervisor script mismatch') configs = options['--config'] if len(configs) != 1: raise LifecycleAuthorityError('supervisor command line must contain exactly one bound config argument') try: config_matches = canonical_path(configs[0]) == canonical_path(config_path) except (OSError, TypeError, ValueError): config_matches = False if not config_matches: raise LifecycleAuthorityError('supervisor command line bound config argument mismatch') def _verify_supervisor_process(metadata): process = verify_retained_process( metadata['pid'], metadata['process_creation_time'], metadata['executable'], ) try: arguments = process.command_line() verify_supervisor_command_line( arguments, metadata['supervisor_path'], metadata['config_path'], ) finally: process.close() def require_active_supervisor_child( config_path=None, child_kind=None, require_dsn=True, handshake_timeout_retries=0, ): """Authenticate a mutating child before it reads application inputs.""" instance_file = os.getenv(CHILD_INSTANCE_FILE_ENV) or '' inherited_id = os.getenv(CHILD_INSTANCE_ID_ENV) or '' inherited_token = os.getenv(CHILD_TOKEN_ENV) or '' inherited_config_hash = os.getenv(CHILD_CONFIG_HASH_ENV) or '' inherited_script_hash = os.getenv(CHILD_SCRIPT_HASH_ENV) or '' inherited_manifest_hash = os.getenv(CHILD_MANIFEST_HASH_ENV) or '' inherited_dsn_hash = os.getenv(CHILD_DSN_HASH_ENV) or '' inherited_kind = os.getenv(CHILD_KIND_ENV) or '' if not all((instance_file, inherited_id, inherited_token, inherited_config_hash, inherited_script_hash, inherited_manifest_hash)): raise LifecycleAuthorityError('direct mutation is retired; use an authenticated active supervisor command') if child_kind and inherited_kind != str(child_kind): raise LifecycleAuthorityError('supervised child kind does not match the requested mutation entrypoint') # Imported lazily to avoid a module cycle while supervisor metadata support # itself imports the manifest helpers above. from supervisor_instance import load_instance_metadata try: metadata = load_instance_metadata(instance_file) except (OSError, ValueError) as exc: raise LifecycleAuthorityError('private supervisor instance metadata is unavailable') from exc if not hmac.compare_digest(metadata['instance_id'], inherited_id): raise LifecycleAuthorityError('supervisor child instance identity mismatch') if not hmac.compare_digest(metadata['token'], inherited_token): raise LifecycleAuthorityError('supervisor child credential mismatch') if metadata.get('activation_state') != PHASE_ACTIVE: raise LifecycleAuthorityError('supervisor is not ACTIVE; mutation is refused') if canonical_path(instance_file) != canonical_path(metadata.get('instance_file') or instance_file): raise LifecycleAuthorityError('supervisor child instance path mismatch') if config_path and canonical_path(config_path) != metadata['config_path']: raise LifecycleAuthorityError('supervisor child config path mismatch') expected_pairs = ( ('config_sha256', inherited_config_hash), ('supervisor_sha256', inherited_script_hash), ('code_manifest_sha256', inherited_manifest_hash), ) for key, inherited in expected_pairs: if not hmac.compare_digest(str(metadata.get(key) or ''), inherited): raise LifecycleAuthorityError(f'supervisor child {key} authority mismatch') if not hmac.compare_digest(sha256_file(metadata['config_path']), metadata['config_sha256']): raise LifecycleAuthorityError('supervisor config authority drifted') verify_code_manifest( metadata['code_manifest'], metadata['code_manifest_sha256'], require_private_acl=True, ) _verify_supervisor_process(metadata) dsn = os.getenv('TRUF_MANAGED_POSTGRES_DSN') or '' if require_dsn: try: parsed = parse_postgres_url(dsn) except ValueError as exc: raise LifecycleAuthorityError('a canonical managed PostgreSQL DSN is required') from exc if parsed['host'] != '127.0.0.1': raise LifecycleAuthorityError('managed PostgreSQL DSN is not loopback-bound') actual_dsn_hash = dsn_sha256(dsn) if not inherited_dsn_hash or not hmac.compare_digest(actual_dsn_hash, inherited_dsn_hash): raise LifecycleAuthorityError('managed PostgreSQL DSN authority mismatch') if not hmac.compare_digest(str(metadata.get('canonical_dsn_sha256') or ''), inherited_dsn_hash): raise LifecycleAuthorityError('private metadata PostgreSQL DSN authority mismatch') for key in ('SCANNER_DB_URL', 'DATABASE_URL'): if not hmac.compare_digest(str(os.getenv(key) or ''), dsn): raise LifecycleAuthorityError(f'{key} does not match the managed PostgreSQL DSN') if any(key.upper().startswith('PG') for key in os.environ): raise LifecycleAuthorityError('libpq PG* environment overrides are forbidden for managed children') handshake = _send_handshake_with_timeout_retry(metadata, handshake_timeout_retries) if handshake.get('instance_id') != metadata['instance_id'] or handshake.get('activation_state') != PHASE_ACTIVE: raise LifecycleAuthorityError('supervisor handshake did not confirm ACTIVE authority') for key, value in expected_pairs: if not hmac.compare_digest(str(handshake.get(key) or ''), value): raise LifecycleAuthorityError(f'supervisor handshake {key} mismatch') if require_dsn and not hmac.compare_digest(str(handshake.get('canonical_dsn_sha256') or ''), inherited_dsn_hash): raise LifecycleAuthorityError('supervisor handshake PostgreSQL DSN authority mismatch') return metadata