import contextlib import io import json import os from pathlib import Path import sys import tempfile import unittest from unittest import mock ROOT = Path(__file__).resolve().parents[1] APP_DIR = ROOT / 'app' sys.path.insert(0, str(APP_DIR)) import docker_depth_experiment as depth import docker_depth_operator as operator from runtime_security import PrivateFileError, ensure_private_directory POLICY_SHA256 = 'b' * 64 SECRET_SENTINEL = 'operator-secret-sentinel' def experiment_config(*, enabled=False, secret_query=False): queries = [f'operator-query-{index:02d}' for index in range(61)] if secret_query: queries[17] = SECRET_SENTINEL return { 'global': { 'database_url': ( f'postgresql://scanner:{SECRET_SENTINEL}@127.0.0.1:5432/truf' ), 'sync_file_queues': False, }, 'sources': {'dockerhub': { 'mode': 'search', 'require_digest': True, 'queries': queries, 'pages': 30, 'per_page': 100, 'docker_platform_candidate_tags': 20, 'docker_images_per_repository': 3, 'docker_depth_experiment': { 'experiment_key': 'operator-test-v1', 'enabled': enabled, 'queries': queries, 'repositories_per_query': 10, 'shallow_images_per_repository': 1, 'deep_repositories_per_query': 1, 'deep_images_per_repository': 10, 'target_limit': 1200, 'selector_version': depth.DOCKER_DEPTH_SELECTOR_VERSION, }, }}, } def validated_experiment(*, enabled=False, secret_query=False): return depth.validate_docker_depth_config( experiment_config(enabled=enabled, secret_query=secret_query), managed_postgres=True, final_cutover=True, ) def cohort_manifest(experiment): candidates = {} for query_ordinal, query in enumerate(experiment.queries): candidates[query] = [{ 'repository_queue_id': query_ordinal * 100 + rank, 'eligibility_page_id': 10000 + query_ordinal * 100 + rank, 'best_search_rank': rank, 'valid_distinct_graph_count': rank, } for rank in range(1, 11)] return depth.build_docker_depth_cohort_manifest( experiment, POLICY_SHA256, candidates, ) class _Result: def __init__(self, row=None): self.row = row def fetchone(self): return self.row class _StatusConnection: is_postgres = True def __init__(self): self.statements = [] self.commits = 0 def execute(self, sql, params=None): self.statements.append(str(sql).strip()) if 'FROM docker_depth_experiments' in str(sql): return _Result(None) raise AssertionError(f'unexpected status SQL: {sql}') def commit(self): self.commits += 1 class _StatusDB: def __init__(self): self.conn = _StatusConnection() class DockerDepthOperatorTests(unittest.TestCase): def test_status_is_read_only_and_does_not_emit_queries_or_secrets(self): validated = validated_experiment(secret_query=True) db = _StatusDB() @contextlib.contextmanager def opened(*_args, **_kwargs): yield db coverage = { 'query_count': 61, 'complete_query_count': 61, 'incomplete_query_count': 0, 'required_repository_count': 610, 'eligible_repository_slots': 610, 'coverage_sha256': 'c' * 64, } stdout = io.StringIO() with mock.patch.object(operator, 'load_config', return_value=validated.config), \ mock.patch.object(operator, 'operator_database', side_effect=opened), \ mock.patch.object( operator, 'summarize_docker_depth_fresh_coverage', return_value=coverage, ), contextlib.redirect_stdout(stdout): self.assertEqual(operator.main(['--config', 'private.yaml', '--status']), 0) report = json.loads(stdout.getvalue()) self.assertEqual(report['action'], 'status') self.assertEqual(report['fresh_coverage'], coverage) self.assertNotIn(SECRET_SENTINEL, stdout.getvalue()) self.assertTrue(db.conn.statements) self.assertTrue(all(statement.upper().startswith('SELECT') for statement in db.conn.statements)) def test_failure_output_redacts_raw_exception_text(self): validated = validated_experiment(secret_query=True) stdout = io.StringIO() with mock.patch.object(operator, 'load_config', return_value=validated.config), \ mock.patch.object( operator, 'operator_database', side_effect=RuntimeError(SECRET_SENTINEL), ), contextlib.redirect_stdout(stdout), self.assertRaises(SystemExit) as raised: operator.main(['--status']) self.assertEqual(stdout.getvalue(), '') self.assertNotIn(SECRET_SENTINEL, str(raised.exception)) def test_private_manifest_publish_is_exclusive_and_exact_replay_is_safe(self): manifest = cohort_manifest(validated_experiment().experiment) with tempfile.TemporaryDirectory() as directory: ensure_private_directory(directory, reject_reparse=True) path = os.path.join(directory, 'cohort.json') published, created = operator._publish_manifest(path, manifest) replayed, replay_created = operator._publish_manifest(path, manifest) self.assertEqual((published, created), (path, True)) self.assertEqual((replayed, replay_created), (path, False)) changed = dict(manifest) changed['target_limit'] = 1199 with self.assertRaisesRegex(RuntimeError, 'different reviewed manifest'): operator._publish_manifest(path, changed) def test_private_manifest_paths_reject_untrusted_parent_and_reparse(self): with mock.patch.object( operator, 'require_private_directory', side_effect=PrivateFileError('wrong owner'), ): with self.assertRaises(PrivateFileError): operator._manifest_path('untrusted/cohort.json', existing=False) with mock.patch.object( operator, 'reject_reparse_components', side_effect=PrivateFileError('reparse'), ): with self.assertRaises(PrivateFileError): operator._manifest_path('linked/cohort.json', existing=False) def test_approval_mismatch_fails_before_database_access(self): experiment = validated_experiment().experiment manifest = cohort_manifest(experiment) with tempfile.TemporaryDirectory() as directory: ensure_private_directory(directory, reject_reparse=True) path = os.path.join(directory, 'cohort.json') operator._publish_manifest(path, manifest) with self.assertRaisesRegex(ValueError, 'approval hash conflicts'): operator._read_approved_manifest( path, depth.validate_docker_depth_cohort_manifest, experiment, POLICY_SHA256, 'f' * 64, ) def test_phase_config_gates_and_semantic_hash_are_stable(self): disabled = validated_experiment(enabled=False).experiment enabled = validated_experiment(enabled=True).experiment self.assertEqual(disabled.config_hash, enabled.config_hash) operator._require_action_config_state(disabled, 'apply-hold') operator._require_action_config_state(enabled, 'apply-reactivation') operator._require_action_config_state(enabled, 'apply-resolver-disposition') operator._require_action_config_state(enabled, 'apply-resolver-refund') with self.assertRaisesRegex(RuntimeError, 'requires disabled config'): operator._require_action_config_state(enabled, 'generate-cohort') with self.assertRaisesRegex(RuntimeError, 'requires enabled config'): operator._require_action_config_state(disabled, 'generate-reactivation') def test_sources_running_refusal_precedes_identity_and_database(self): dsn = 'postgresql://scanner@127.0.0.1:5432/truf' with mock.patch.object(operator, 'preflight_lifecycle_paths'), \ mock.patch.object(operator, 'load_postgres_environment'), \ mock.patch.object(operator, 'canonical_database_url', return_value=dsn), \ mock.patch.object(operator, 'is_postgres_url', return_value=True), \ mock.patch.object( operator, 'ClusterAuthorityLock', return_value=contextlib.nullcontext(), ), mock.patch.object( operator, 'require_local_sources_stopped', side_effect=RuntimeError('sources active'), ), mock.patch.object(operator, 'verify_cluster_identity') as identity, \ mock.patch.object(operator, 'ScannerDB') as scanner_db: with self.assertRaisesRegex(RuntimeError, 'sources active'): with operator.operator_database('config.yaml', {}, read_only=True): pass identity.assert_not_called() scanner_db.assert_not_called() def test_database_preflight_enforces_identity_guard_schema_and_cutover(self): dsn = 'postgresql://scanner@127.0.0.1:5432/truf' identity = { 'database': 'truf', 'user': 'scanner', 'port': 5432, 'data_directory': r'D:\private\postgres', 'system_identifier': '123', } connection = mock.Mock() database = mock.Mock(enabled=True, conn=connection) events = [] def mark(name): return lambda *_args, **_kwargs: events.append(name) with mock.patch.object( operator, 'preflight_lifecycle_paths', side_effect=mark('preflight'), ), mock.patch.object( operator, 'load_postgres_environment', side_effect=mark('environment'), ), mock.patch.object( operator, 'canonical_database_url', return_value=dsn, ), mock.patch.object( operator, 'is_postgres_url', return_value=True, ), mock.patch.object( operator, 'ClusterAuthorityLock', return_value=contextlib.nullcontext(), ), mock.patch.object( operator, 'require_local_sources_stopped', side_effect=mark('sources'), ), mock.patch.object( operator, 'verify_cluster_identity', side_effect=lambda *_args, **_kwargs: events.append('identity') or identity, ), mock.patch.object( operator, 'canonical_postgres_url', return_value=dsn, ), mock.patch.object( operator, 'ScannerDB', return_value=database, ), mock.patch.object( operator, '_verify_online_cluster_identity', side_effect=mark('online'), ), mock.patch.object( operator, 'postgres_migration_guard', return_value=contextlib.nullcontext(), ) as guard: with operator.operator_database('config.yaml', {}, read_only=True) as opened: self.assertIs(opened, database) self.assertEqual( events, ['preflight', 'environment', 'sources', 'identity', 'online'], ) guard.assert_called_once_with(database) database.require_runtime_safety_schema.assert_called_once_with() database.require_final_cutover.assert_called_once_with() connection.execute.assert_called_once_with( 'SET default_transaction_read_only = on' ) database.close.assert_called_once_with() def test_apply_cli_requires_all_explicit_approval_switches(self): for argv in ( ['--apply-cohort-manifest', 'cohort.json'], ['--apply-cohort-manifest', 'cohort.json', '--apply'], [ '--apply-cohort-manifest', 'cohort.json', '--apply', '--sources-stopped', '--approve-sha256', 'not-a-hash', ], ): with self.subTest(argv=argv), contextlib.redirect_stderr(io.StringIO()), \ self.assertRaises(SystemExit): operator.parse_args(argv) args, action = operator.parse_args([ '--apply-resolver-refund-manifest', 'refund.json', '--apply', '--sources-stopped', '--approve-sha256', 'a' * 64, ]) self.assertEqual(action, 'apply-resolver-refund') self.assertEqual(args.apply_resolver_refund_manifest, 'refund.json') args, action = operator.parse_args([ '--apply-resolver-disposition-manifest', 'disposition.json', '--apply', '--sources-stopped', '--approve-sha256', 'b' * 64, ]) self.assertEqual(action, 'apply-resolver-disposition') self.assertEqual( args.apply_resolver_disposition_manifest, 'disposition.json', ) if __name__ == '__main__': unittest.main()