import ast import ntpath import os from pathlib import Path import posixpath import shutil import subprocess import sys import tempfile from types import SimpleNamespace import unittest from unittest import mock ROOT = Path(__file__).resolve().parents[1] STAGING_REFUSAL = ( 'Docker development copy: runtime control is disabled until container isolation is ready. ' 'See DOCKER_MIGRATION.md.' ) CONTAINER_REFUSAL = ( 'Docker development copy: runtime control is disabled outside the prepared container. ' 'See DOCKER_MIGRATION.md.' ) sys.path.insert(0, str(ROOT / 'app')) import paths def isolated_child_environment(temporary): environment = {name: os.environ[name] for name in ('SystemRoot', 'WINDIR', 'COMSPEC', 'SystemDrive') if name in os.environ} environment.update({name: str(temporary) for name in ('TEMP', 'TMP', 'TMPDIR')}) environment.update(PYTHONDONTWRITEBYTECODE='1', PYTEST_DISABLE_PLUGIN_AUTOLOAD='1', PYTEST_ADDOPTS='', PYTEST_PLUGINS='') return environment class PortablePathTests(unittest.TestCase): def setUp(self): self.environment = {} self.operating_system = SimpleNamespace( name='posix', path=posixpath, getenv=self.environment.get, getcwd=lambda: '/unrelated-working-directory', makedirs=mock.Mock(side_effect=AssertionError('path resolution must not write')), ) self.enterContext(mock.patch.multiple( paths, os=self.operating_system, APP_DIR='/opt/truf/app', CANONICAL_ROOT='/opt/truf', )) def test_defaults_are_checkout_local_and_independent_of_cwd(self): resolved = paths.default_project_paths() self.assertEqual(resolved['root_dir'], '/opt/truf') self.assertEqual(resolved['project_dir'], '/opt/truf/app') self.assertEqual(resolved['result_bundle_dir'], '/opt/truf/runtime/result_bundles') self.assertEqual(paths.resolve_postgres_data_dir(), '/opt/truf/runtime/postgres/data') self.operating_system.makedirs.assert_not_called() def test_generated_paths_use_posix_separators(self): config = paths.apply_path_config({}) for key, value in config['global'].items(): if key in {'database_url', 'dashboard_db_url', 'trufflehog_path'}: continue with self.subTest(key=key): self.assertTrue(posixpath.isabs(value)) self.assertNotIn('\\', value) for value in config['supervisor'].values(): self.assertTrue(posixpath.isabs(value)) self.assertNotIn('\\', value) def test_environment_defaults_and_explicit_config_precedence(self): self.environment.update({ 'SCANNER_ROOT_DIR': '/environment/root', 'SCANNER_PROJECT_DIR': '/environment/app', 'SCANNER_RUNTIME_DIR': '/environment/runtime', 'SCANNER_RESULT_BUNDLE_DIR': '/environment/bundles', 'TRUFFLEHOG_WORK_DIR': '/environment/work', }) resolved = paths.resolve_project_paths() self.assertEqual(resolved['log_dir'], '/environment/runtime/logs') self.assertEqual(resolved['result_bundle_dir'], '/environment/bundles') self.assertEqual(resolved['work_dir'], '/environment/work') explicit = { 'root_dir': '/configured/root', 'project_dir': '/configured/app', 'runtime_dir': '/configured/runtime', 'result_bundle_dir': '/configured/bundles', 'work_dir': '/configured/work', } resolved = paths.resolve_project_paths(explicit) for key, value in explicit.items(): self.assertEqual(resolved[key], value) def test_explicit_config_directory_and_relative_paths_are_preserved(self): resolved = paths.resolve_project_paths( {'root_dir': '..', 'project_dir': '.', 'work_dir': 'scratch'}, '/srv/settings/config.linux.yaml', ) self.assertEqual(resolved['root_dir'], '/srv') self.assertEqual(resolved['project_dir'], '/srv/settings') self.assertEqual(resolved['work_dir'], '/srv/settings/scratch') def test_windows_paths_are_rejected_before_command_or_relative_resolution(self): for value in ( r'D:\truf', 'D:/truf', 'C:trufflehog', r'\\server\share', '//server/share', r'\\?\C:\tools', r'\rooted', r'runtime\logs', ): with self.subTest(value=value), self.assertRaisesRegex(paths.PathResolutionError, 'Windows path'): paths.resolve_path(value, base_dir='/opt/truf/app', allow_command=True) with self.assertRaises(paths.PathResolutionError): paths.resolve_path('{external}', {'external': r'S:\scanner-work'}) for base in (r'D:\truf', 'D:/truf', r'\\server\share', r'runtime\state'): with self.subTest(base=base): with self.assertRaisesRegex(paths.PathResolutionError, 'Windows base path'): paths.resolve_path('logs', base_dir=base) for key in ('project_dir', 'config_dir'): with self.assertRaises(paths.PathResolutionError): paths.resolve_path('logs', {key: base}) self.environment['SCANNER_ROOT_DIR'] = r'D:\truf' with self.assertRaises(paths.PathResolutionError): paths.resolve_project_paths() def test_windows_path_behavior_remains_available_on_windows(self): self.operating_system.name = 'nt' self.operating_system.path = ntpath self.assertEqual( paths.resolve_path('{root_dir}/runtime', {'root_dir': r'E:\development'}), r'E:\development\runtime', ) with mock.patch.object(ntpath, 'exists', return_value=True): self.assertEqual(paths.default_trufflehog_path(), paths.DEFAULT_TRUFFLEHOG) def test_linux_does_not_probe_the_windows_trufflehog_fallback(self): with mock.patch.object(posixpath, 'exists', side_effect=AssertionError('Windows binary probe')): self.assertEqual(paths.default_trufflehog_path(), 'trufflehog') self.assertEqual(paths.resolve_path('trufflehog', allow_command=True), 'trufflehog') def test_required_paths_and_unknown_placeholders_still_fail(self): for value in (None, '', ' '): with self.subTest(value=value), self.assertRaises(paths.PathResolutionError): paths.resolve_path(value, required=True) self.assertIsNone(paths.resolve_path(None)) with self.assertRaisesRegex(paths.PathResolutionError, 'Unknown path placeholder'): paths.resolve_path('{missing}/file') def test_managed_database_urls_keep_precedence_and_are_not_filesystem_paths(self): managed = 'postgresql://fixture:fixture%5Cvalue@127.0.0.1:5432/fixture' self.environment['TRUF_MANAGED_POSTGRES_DSN'] = managed resolved = paths.resolve_project_paths({ 'database_url': 'postgresql://other.invalid/other', 'dashboard_db_url': 'postgresql://other.invalid/dashboard', }) self.assertEqual(resolved['database_url'], managed) self.assertEqual(resolved['dashboard_db_url'], managed) def test_linux_profile_resolves_without_windows_storage(self): import yaml config = yaml.safe_load((ROOT / 'app' / 'config.linux.yaml').read_text(encoding='utf-8')) resolved = paths.apply_path_config(config, '/opt/truf/app/config.linux.yaml') expected = { 'root_dir': '/opt/truf', 'project_dir': '/opt/truf/app', 'runtime_dir': '/data/runtime-linux', 'postgres_data_dir': '/data/postgres-linux', 'postgres_bin_dir': '/usr/lib/postgresql/16/bin', 'result_bundle_dir': '/data/scanner-result-bundles', 'work_dir': '/data/scanner-work', 'control_dir': '/run/truf/control', 'secrets_file': '/data/config/secrets.yaml', } for key, value in expected.items(): self.assertEqual(resolved['global'][key], value) for key in ('summary_tsv', 'summary_json', 'alive_summary_tsv'): value = paths.resolve_optional_path(resolved['keychecks'][key], resolved['global']) self.assertTrue(value.startswith('/data/runtime-linux/keychecks/')) self.assertEqual(resolved['supervisor']['control_host'], '127.0.0.1') self.assertEqual(resolved['supervisor']['control_dir'], '/run/truf/control') self.assertEqual(resolved['supervisor']['instance_file'], '/run/truf/control/supervisor.instance.json') self.assertEqual(resolved['global']['max_active_scans'], 1) self.assertEqual(resolved['global']['opportunistic_scan_slots'], 0) self.assertFalse(resolved['supervisor']['dashboard']['enabled']) self.operating_system.makedirs.assert_not_called() class DockerStagingTests(unittest.TestCase): def test_native_checkout_default_is_derived_from_this_copy(self): self.assertEqual(Path(paths.CANONICAL_ROOT), ROOT) self.assertEqual(Path(paths.APP_DIR), ROOT / 'app') self.assertFalse((ROOT / 'app' / 'config.yaml').exists()) def test_python_entrypoints_refuse_before_application_imports(self): for name in ('runtime_bootstrap.py', 'supervisor.py', 'postgres_runtime.py'): entrypoint = ROOT / 'app' / name canonical = '/opt/truf/app/' + name expected = ast.parse( "import sys\nimport os\nif __name__ == '__main__':\n" " if sys.platform != 'linux' or not os.path.isfile('/.dockerenv') " f'or os.path.abspath(__file__) != {canonical!r}:\n' f' raise SystemExit({CONTAINER_REFUSAL!r})\n' ' import runpy\n' " runpy.run_path('/opt/truf/app/container_runtime.py')['require_container']()\n" ).body tree = ast.parse(entrypoint.read_text(encoding='utf-8')) statements = tree.body if (isinstance(statements[0], ast.Expr) and isinstance(statements[0].value, ast.Constant) and isinstance(statements[0].value.value, str)): statements = statements[1:] with self.subTest(entrypoint=name): self.assertEqual( [ast.dump(node) for node in statements[:3]], [ast.dump(node) for node in expected], ) # Execute only the AST-proved guard, never the operational body. guard = compile(ast.Module(body=[statements[2]], type_ignores=[]), str(entrypoint), 'exec') for platform, dockerenv, filename in ( ('win32', True, canonical), ('linux', False, canonical), ('linux', True, '/tmp/' + name), ('linux', True, canonical), ): require = mock.Mock() runpy = SimpleNamespace(run_path=mock.Mock(return_value={'require_container': require})) importer = mock.Mock(return_value=runpy) namespace = { '__name__': '__main__', '__file__': filename, 'sys': SimpleNamespace(platform=platform), 'os': SimpleNamespace(path=SimpleNamespace( isfile=lambda path: dockerenv, abspath=lambda path: filename, )), '__builtins__': {'SystemExit': SystemExit, '__import__': importer}, } if platform == 'linux' and dockerenv and filename == canonical: exec(guard, namespace) runpy.run_path.assert_called_once_with('/opt/truf/app/container_runtime.py') require.assert_called_once_with() self.assertEqual([call.args[0] for call in importer.call_args_list], ['runpy']) require.side_effect = RuntimeError('unsafe container layout') with self.assertRaisesRegex(RuntimeError, 'unsafe container layout'): exec(guard, namespace) else: with self.assertRaisesRegex(SystemExit, 'disabled outside the prepared container'): exec(guard, namespace) importer.assert_not_called() require.assert_not_called() # Windows is positively a host refusal case. In the Linux image, # keep the full AST/mock coverage above without invoking its CLI. if sys.platform == 'win32': with tempfile.TemporaryDirectory(prefix='container-cli-refusal-') as temporary: completed = subprocess.run( [sys.executable, '-I', '-S', '-B', str(entrypoint), '--stop-background'], cwd=temporary, env=isolated_child_environment(temporary), stdin=subprocess.DEVNULL, capture_output=True, text=True, timeout=15, ) self.assertEqual(completed.returncode, 1) self.assertIn(CONTAINER_REFUSAL, completed.stderr) self.assertNotIn('Traceback', completed.stderr) @unittest.skipUnless(os.name == 'nt', 'Windows PowerShell parser required') def test_all_copied_powershell_tools_have_static_refusals(self): powershell = shutil.which('powershell.exe') self.assertIsNotNone(powershell) entrypoints = sorted(ROOT.glob('*.ps1')) self.assertTrue(entrypoints) # Never execute operational PowerShell scripts to test their safety guard. check = r""" $ErrorActionPreference = 'Stop' foreach ($file in @(__FILES__)) { $errors = $null $tokens = $null $tree = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors) if ($errors.Count -ne 0) { throw 'PowerShell syntax error' } if ($tree.BeginBlock -or $tree.ProcessBlock -or $tree.DynamicParamBlock) { throw 'Unexpected startup block' } $statements = @($tree.EndBlock.Statements) if ($statements.Count -lt 2) { throw 'Missing startup refusal' } if ($statements[0].Extent.Text -cne '$ErrorActionPreference = ''Stop''') { throw 'Unexpected startup statement' } if ($statements[1] -isnot [System.Management.Automation.Language.ThrowStatementAst]) { throw 'Missing startup throw' } if ($statements[1].Extent.Text -cne __THROW__) { throw 'Unexpected refusal expression' } foreach ($parameter in @($tree.ParamBlock.Parameters)) { if ($parameter.DefaultValue -and $parameter.DefaultValue -isnot [System.Management.Automation.Language.ConstantExpressionAst] -and $parameter.DefaultValue -isnot [System.Management.Automation.Language.StringConstantExpressionAst]) { throw 'Nonliteral parameter default before refusal' } } $attributes = $tree.FindAll({ param($node) $node -is [System.Management.Automation.Language.AttributeAst] -or $node -is [System.Management.Automation.Language.TypeConstraintAst] }, $true) foreach ($attribute in $attributes) { if ($attribute.Extent.StartOffset -lt $statements[1].Extent.StartOffset -and $attribute.TypeName.FullName -notin @('CmdletBinding', 'ValidateRange', 'string', 'int', 'switch')) { throw 'Unexpected parameter attribute before refusal' } } } """ files = ','.join("'" + str(path).replace("'", "''") + "'" for path in entrypoints) expected_throw = "throw '" + STAGING_REFUSAL + "'" check = check.replace('__FILES__', files).replace('__THROW__', "'" + expected_throw.replace("'", "''") + "'") with tempfile.TemporaryDirectory(prefix='powershell-ast-') as temporary: completed = subprocess.run( [powershell, '-NoProfile', '-Command', check], cwd=temporary, env=isolated_child_environment(temporary), stdin=subprocess.DEVNULL, capture_output=True, timeout=15, ) self.assertEqual(completed.returncode, 0, completed.stderr) def test_docker_context_exceptions_are_explicit_source_files_only(self): rules = [line.strip() for line in (ROOT / '.dockerignore').read_text(encoding='ascii').splitlines() if line.strip() and not line.startswith('#')] self.assertEqual(rules[0], '**') allowed = {line[1:] for line in rules if line.startswith('!')} for relative in allowed: with self.subTest(relative=relative): self.assertNotRegex(relative, r'[*?\[\\]') self.assertFalse(relative.endswith('/')) self.assertNotIn('..', Path(relative).parts) if relative != 'Dockerfile': self.assertTrue((ROOT / relative).is_file()) for source in (ROOT / 'app').rglob('*.py'): self.assertIn(source.relative_to(ROOT).as_posix(), allowed) for forbidden in ( '.env.postgres', 'app/secrets.yaml', 'app/secrets.yaml.bak', 'app/keycheckers/gemini/gem.txt', 'app/scanner.db', 'app/credentials.json', 'runtime/postgres/data/PG_VERSION', 'runtime/results/found_secrets.jsonl', '.git/config', '.opencode/package.json', ): self.assertNotIn(forbidden, allowed) if __name__ == '__main__': unittest.main()