import hashlib import json import os from pathlib import Path import subprocess import sys import tempfile from types import SimpleNamespace import unittest from unittest import mock ROOT = Path(__file__).resolve().parents[1] APP_DIR = ROOT / 'app' sys.path.insert(0, str(APP_DIR)) import console_runner import scanner from scanner_db import ( RuntimeSafetySchemaError, ScanEventConflictError, ScannerDB, canonical_git_scan_plan_bytes, matching_git_coverage, validate_git_resolution, ) def api_response(payload=None, status=200, headers=None, raw=None): body = raw if raw is not None else json.dumps(payload).encode('utf-8') response = mock.Mock() response.status_code = status response.headers = dict(headers or {}) response.headers.setdefault('Content-Length', str(len(body))) response.encoding = 'utf-8' response.text = body.decode('utf-8', errors='replace') response.json.return_value = payload response.iter_content.return_value = [body] return response def git_plan(mode='baseline', provider='github'): head = 'a' * 40 base = None if mode == 'baseline' else head if mode == 'noop' else 'b' * 40 host = f'{provider}.com' return { 'version': 1, 'provider': provider, 'repo_url': f'https://{host}/Owner/Repo.git', 'repo_path': 'Owner/Repo', 'branch': 'Feature/Main', 'ref': 'refs/heads/Feature/Main', 'head_sha': head, 'ref_source': 'explicit', 'base_sha': base, 'mode': mode, 'baseline_depth': 100, } class GitRefResolutionTests(unittest.TestCase): def test_github_default_and_explicit_refs_are_resolved_exactly(self): token = 'sentinel-github-token' head = 'a' * 40 default_responses = [ api_response({'default_branch': 'Main'}), api_response({'ref': 'refs/heads/Main', 'object': {'type': 'commit', 'sha': head}}), ] with mock.patch.object(scanner, 'api_request', side_effect=default_responses) as request: resolved = scanner.resolve_git_scan_target( 'https://github.com/Owner/Repo.git', 'github', token, ) self.assertEqual(resolved['ref'], 'refs/heads/Main') self.assertEqual(resolved['head_sha'], head) self.assertEqual(resolved['ref_source'], 'provider_default') self.assertEqual(request.call_count, 2) self.assertEqual( request.call_args_list[1].args[1], 'https://api.github.com/repos/Owner/Repo/git/ref/heads%2FMain', ) for call in request.call_args_list: self.assertEqual(call.kwargs['headers']['Authorization'], f'Bearer {token}') self.assertIs(call.kwargs['allow_redirects'], False) self.assertIs(call.kwargs['stream'], True) self.assertNotIn(token, json.dumps(resolved, sort_keys=True)) explicit_target = json.dumps({ 'url': 'https://github.com/Owner/Repo.git', 'branch': 'Feature/X', 'ref': 'refs/heads/Feature/X', 'head_sha': 'f' * 40, }) response = api_response({ 'ref': 'refs/heads/Feature/X', 'object': {'type': 'commit', 'sha': 'c' * 40}, }) with mock.patch.object(scanner, 'api_request', return_value=response) as request: resolved = scanner.resolve_git_scan_target(explicit_target, 'github', token) self.assertEqual(request.call_count, 1) self.assertTrue(request.call_args.args[1].endswith('/git/ref/heads%2FFeature%2FX')) self.assertEqual(resolved['head_sha'], 'c' * 40) self.assertEqual(resolved['ref_source'], 'explicit') def test_gitlab_default_and_slash_branch_are_encoded_and_validated(self): head = 'd' * 40 responses = [ api_response({'default_branch': 'release/Next'}), api_response({'name': 'release/Next', 'commit': {'id': head}}), ] with mock.patch.object(scanner, 'api_request', side_effect=responses) as request: resolved = scanner.resolve_git_scan_target( 'https://gitlab.com/Group/Sub/Repo.git', 'gitlab', 'gitlab-token', ) self.assertEqual(resolved['repo_path'], 'Group/Sub/Repo') self.assertEqual(resolved['ref'], 'refs/heads/release/Next') self.assertEqual(request.call_count, 2) self.assertEqual( request.call_args_list[0].args[1], 'https://gitlab.com/api/v4/projects/Group%2FSub%2FRepo', ) self.assertTrue(request.call_args_list[1].args[1].endswith('/release%2FNext')) self.assertEqual( request.call_args_list[0].kwargs['headers']['PRIVATE-TOKEN'], 'gitlab-token', ) def test_unsafe_targets_and_refs_fail_before_any_authenticated_request(self): invalid = [ ('https://user:secret@github.com/Owner/Repo.git', 'github'), ('https://github.com/Owner/Repo.git?token=secret', 'github'), ('https://github.com/Owner/Repo.git#fragment', 'github'), ('https://gitlab.com/Owner/Repo.git', 'github'), ('https://github.com/Owner%2FRepo.git', 'github'), (json.dumps({ 'url': 'https://github.com/Owner/Repo.git', 'branch': 'main', 'ref': 'refs/heads/other', }), 'github'), ] with mock.patch.object(scanner, 'api_request') as request: for target, provider in invalid: with self.subTest(target=target), self.assertRaises(ValueError): scanner.resolve_git_scan_target(target, provider, 'must-not-be-sent') request.assert_not_called() def test_malformed_oversized_redirected_and_mismatched_payloads_fail_closed(self): cases = [ api_response(raw=b'{not-json'), api_response({'default_branch': 'main'}, headers={'Content-Length': '2000'}), api_response({'default_branch': 'main'}, status=302, headers={'Location': 'https://evil.test'}), ] for response in cases: with self.subTest(status=response.status_code), \ mock.patch.object(scanner, 'api_request', return_value=response), \ self.assertRaises(scanner.ApiRequestError): scanner.resolve_github_ref_head( 'Owner/Repo', max_response_bytes=100, ) response.close.assert_called() mismatched = api_response({ 'ref': 'refs/heads/other', 'object': {'type': 'tag', 'sha': 'e' * 40}, }) with mock.patch.object(scanner, 'api_request', return_value=mismatched), \ self.assertRaisesRegex(scanner.ApiRequestError, 'mismatched'): scanner.resolve_github_ref_head('Owner/Repo', ref_hint='main') def test_provider_error_keeps_rate_limit_category_and_redacts_token(self): token = 'sentinel-rate-limit-token' response = api_response( {'message': f'API rate limit exceeded for {token}'}, status=429, headers={'X-RateLimit-Reset': '1800000000'}, ) with mock.patch.object(scanner, 'api_request', return_value=response), \ self.assertRaises(scanner.RateLimitError) as raised: scanner.resolve_github_ref_head('Owner/Repo', token, ref_hint='main') self.assertEqual(raised.exception.category, 'rate_limit') self.assertNotIn(token, str(raised.exception)) class ExactGitExecutionTests(unittest.TestCase): @staticmethod def run_scan(plan, **kwargs): return scanner.scan_git_repo( plan['repo_url'], provider=plan['provider'], git_plan=plan, token='sentinel-scan-token', max_depth=7, max_commit_age_days=1, **kwargs, ) def test_noop_records_exact_scope_without_launching_command(self): plan = git_plan('noop') with mock.patch.object(scanner, 'run_command_streamed') as run: result = self.run_scan(plan) run.assert_not_called() self.assertEqual(result['errors'], []) self.assertEqual(result['git_scan_execution']['mode'], 'noop') self.assertTrue(result['git_scan_execution']['success']) self.assertEqual(result['scan_meta']['exact_git_scope']['head_sha'], plan['head_sha']) def test_baseline_is_sha_pinned_and_depth_bounded(self): plan = git_plan('baseline') output = scanner.streamed_output_from_text('', '{"msg":"finished scanning"}', 0) with mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \ mock.patch.object(scanner, 'run_command_streamed', return_value=output) as run: result = self.run_scan(plan) command, _, environment = run.call_args.args self.assertEqual(command[command.index('--branch') + 1], plan['head_sha']) self.assertEqual(command[command.index('--max-depth') + 1], '100') self.assertNotIn('--since-commit', command) self.assertNotIn('sentinel-scan-token', command) self.assertEqual(environment['TRUF_GIT_TOKEN'], 'sentinel-scan-token') self.assertTrue(result['git_scan_execution']['success']) def test_delta_uses_only_pinned_head_and_covered_base(self): plan = git_plan('delta') output = scanner.streamed_output_from_text('', '{"msg":"finished scanning"}', 0) with mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \ mock.patch.object(scanner, 'run_command_streamed', return_value=output) as run: result = self.run_scan(plan) command = run.call_args.args[0] self.assertEqual(command[command.index('--branch') + 1], plan['head_sha']) self.assertEqual(command[command.index('--since-commit') + 1], plan['base_sha']) self.assertNotIn('--max-depth', command) self.assertEqual(result['git_scan_execution']['mode'], 'delta') def test_unavailable_delta_base_falls_back_to_pinned_bounded_baseline(self): plan = git_plan('delta') outputs = [ scanner.streamed_output_from_text('', 'fatal: bad object', 1), scanner.streamed_output_from_text('', '{"msg":"finished scanning"}', 0), ] with mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \ mock.patch.object(scanner, 'git_delta_base_unavailable', return_value=True), \ mock.patch.object(scanner, 'run_command_streamed', side_effect=outputs) as run: result = self.run_scan(plan) self.assertEqual(run.call_count, 2) delta_command = run.call_args_list[0].args[0] reset_command = run.call_args_list[1].args[0] self.assertIn('--since-commit', delta_command) self.assertNotIn('--max-depth', delta_command) self.assertNotIn('--since-commit', reset_command) self.assertEqual(reset_command[reset_command.index('--max-depth') + 1], '100') self.assertEqual(result['git_scan_execution']['mode'], 'baseline_reset') self.assertTrue(result['git_scan_execution']['continuity_reset']) self.assertTrue(result['git_scan_execution']['success']) class GitCoverageGateTests(unittest.TestCase): @staticmethod def reservation(plan): encoded = canonical_git_scan_plan_bytes(plan) return { 'git_scan_plan_json': encoded.decode('ascii'), 'git_scan_plan_sha256': hashlib.sha256(encoded).hexdigest(), } @staticmethod def metadata(plan, execution_mode=None, **overrides): encoded = canonical_git_scan_plan_bytes(plan) execution = { 'mode': execution_mode or plan['mode'], 'pinned': True, 'success': True, 'continuity_reset': False, 'plan_sha256': hashlib.sha256(encoded).hexdigest(), } execution.update(overrides) return {'git_scan_plan': plan, 'git_scan_execution': execution} def test_matching_successful_modes_advance_only_to_bound_head(self): for mode, execution_mode, reset in ( ('baseline', 'baseline', False), ('delta', 'delta', False), ('delta', 'baseline_reset', True), ('noop', 'noop', False), ): with self.subTest(mode=mode, execution_mode=execution_mode): plan = git_plan(mode) metadata = self.metadata( plan, execution_mode, continuity_reset=reset, ) self.assertEqual( matching_git_coverage(self.reservation(plan), metadata, 'done', 0), (True, plan['ref'], plan['head_sha']), ) def test_failed_deferred_or_unpinned_execution_never_advances(self): plan = git_plan('delta') reservation = self.reservation(plan) metadata = self.metadata(plan) self.assertEqual( matching_git_coverage(reservation, metadata, 'deferred', 1), (False, None, None), ) metadata['git_scan_execution']['pinned'] = False self.assertEqual( matching_git_coverage(reservation, metadata, 'done', 0), (False, None, None), ) def test_plan_mismatch_and_corrupt_storage_fail_closed(self): plan = git_plan('baseline') reservation = self.reservation(plan) changed = dict(plan, head_sha='f' * 40) with self.assertRaises(ScanEventConflictError): matching_git_coverage( reservation, self.metadata(changed), 'done', 0, ) with self.assertRaises(RuntimeSafetySchemaError): matching_git_coverage({ 'git_scan_plan_json': '\N{SNOWMAN}', 'git_scan_plan_sha256': '0' * 64, }, {}, 'done', 0) def test_resolution_repository_url_must_match_canonical_path(self): resolved = {key: value for key, value in git_plan().items() if key in { 'provider', 'repo_url', 'repo_path', 'branch', 'ref', 'head_sha', 'ref_source', }} validate_git_resolution(resolved) with self.assertRaisesRegex(ValueError, 'canonical repository'): validate_git_resolution(dict(resolved, repo_url='https://github.com/Other/Repo.git')) class ExactGitWiringTests(unittest.TestCase): def test_core_source_config_enables_bounded_exact_planning(self): config = console_runner.load_config(str(APP_DIR / 'config.yaml')) for source in ('github', 'gitlab'): args = console_runner.build_args_from_source_config( source, config['sources'][source], config['global'], 'fixture', ) self.assertTrue(args.exact_git_planning_enabled) self.assertEqual(args.git_baseline_depth, 100) self.assertEqual(args.git_ref_resolution_attempts, 2) self.assertEqual(args.git_ref_resolution_timeout_sec, 10) self.assertEqual(args.git_ref_resolution_max_bytes, 1 << 20) self.assertEqual(args.admission_resolution_attempts, 300) self.assertEqual(args.admission_resolution_seconds, 300) self.assertEqual(args.admission_resolution_retry_delay_sec, 1) def test_post_claim_helper_resolves_then_binds_with_dedicated_connection(self): args = SimpleNamespace( platform='github', git_ref_resolution_attempts=2, git_ref_resolution_timeout_sec=9, git_ref_resolution_max_bytes=4096, git_baseline_depth=77, ) claim = { 'target': 'https://github.com/Owner/Repo.git', 'reservation_id': 12, 'claim_lease_token': 'lease-token', } resolved = {key: value for key, value in git_plan().items() if key in { 'provider', 'repo_url', 'repo_path', 'branch', 'ref', 'head_sha', 'ref_source', }} bound = git_plan() planning_db = mock.Mock(enabled=True) planning_db.bind_git_scan_plan.return_value = bound with mock.patch.object(console_runner, 'resolve_git_scan_target', return_value=resolved) as resolve, \ mock.patch.object(console_runner, 'ScannerDB', return_value=planning_db) as db_class: result = console_runner.resolve_and_bind_git_claim( args, 'postgresql://fixture', 'github', claim, {'token': 'sentinel-token'}, ) self.assertIs(result, bound) resolve.assert_called_once_with( claim['target'], 'github', 'sentinel-token', request_attempts=2, timeout_sec=9.0, max_response_bytes=4096, ) db_class.assert_called_once_with(db_url='postgresql://fixture', initialize=False) planning_db.bind_git_scan_plan.assert_called_once_with( 12, 'lease-token', resolved, 77, ) planning_db.close.assert_called_once_with() def test_resolver_failures_have_durable_queue_policy_without_token_leakage(self): args = SimpleNamespace(platform='github') claim = { 'target': 'https://github.com/Owner/Repo.git', 'scan_event_id': 'event-id', } token = 'sentinel-resolution-token' cases = [ ( scanner.RateLimitError( 'github', f'HTTP 404 for {token}', category='not_found', retryable=False, auth_related=False, ), False, False, 'not_found', 'not_found', ), ( scanner.RateLimitError( 'github', f'rate limited {token}', category='rate_limit', retryable=True, auth_related=True, ), True, True, 'source_auth', 'rate_limit', ), ( scanner.ApiRequestError(f'transport failed with {token}'), True, True, 'remote_transient', 'remote_transient', ), ] for error, source_failure, retryable, error_class, category in cases: with self.subTest(category=category): failure = console_runner._GitResolutionFailure(error) result = console_runner.git_resolution_failure_result( args, claim, {'token': token}, failure, ) self.assertEqual(result['source_failure'], source_failure) self.assertEqual(result['retryable'], retryable) self.assertEqual(result['error_class'], error_class) self.assertEqual( result['scan_meta']['exact_git_resolution']['category'], category, ) self.assertNotIn(token, json.dumps(result, sort_keys=True)) invalid = console_runner.git_resolution_failure_result( args, claim, {'token': token}, console_runner._GitResolutionFailure( ValueError('unsafe target'), invalid_target=True, ), ) self.assertFalse(invalid['source_failure']) self.assertFalse(invalid['retryable']) self.assertEqual(invalid['error_class'], 'invalid_target') def test_only_resolver_failures_are_wrapped_before_plan_binding(self): args = SimpleNamespace( platform='github', git_ref_resolution_attempts=2, git_ref_resolution_timeout_sec=9, git_ref_resolution_max_bytes=4096, ) claim = { 'target': 'https://github.com/Owner/Repo.git', 'reservation_id': 12, 'claim_lease_token': 'lease-token', } rate_limit = scanner.RateLimitError( 'github', 'limited', category='rate_limit', retryable=True, auth_related=True, ) with mock.patch.object(console_runner, 'resolve_git_scan_target', side_effect=rate_limit), \ mock.patch.object(console_runner, 'ScannerDB') as db_class, \ self.assertRaises(console_runner._GitResolutionFailure) as raised: console_runner.resolve_and_bind_git_claim( args, 'postgresql://fixture', 'github', claim, {'token': 'token'}, ) self.assertIs(raised.exception.error, rate_limit) db_class.assert_not_called() invalid_claim = dict(claim, target='https://user:secret@github.com/Owner/Repo.git') with mock.patch.object(console_runner, 'resolve_git_scan_target') as resolve, \ self.assertRaises(console_runner._GitResolutionFailure) as raised: console_runner.resolve_and_bind_git_claim( args, 'postgresql://fixture', 'github', invalid_claim, {'token': 'token'}, ) self.assertTrue(raised.exception.invalid_target) resolve.assert_not_called() def test_bind_fence_failure_is_not_downgraded_to_a_target_result(self): args = SimpleNamespace( platform='github', git_ref_resolution_attempts=2, git_ref_resolution_timeout_sec=9, git_ref_resolution_max_bytes=4096, git_baseline_depth=77, ) claim = { 'target': 'https://github.com/Owner/Repo.git', 'reservation_id': 12, 'claim_lease_token': 'lease-token', } resolved = {key: value for key, value in git_plan().items() if key in { 'provider', 'repo_url', 'repo_path', 'branch', 'ref', 'head_sha', 'ref_source', }} planning_db = mock.Mock(enabled=True) planning_db.bind_git_scan_plan.side_effect = ScanEventConflictError('stale fence') with mock.patch.object(console_runner, 'resolve_git_scan_target', return_value=resolved), \ mock.patch.object(console_runner, 'ScannerDB', return_value=planning_db), \ self.assertRaisesRegex(ScanEventConflictError, 'stale fence'): console_runner.resolve_and_bind_git_claim( args, 'postgresql://fixture', 'github', claim, {'token': 'token'}, ) planning_db.close.assert_called_once_with() def test_expected_resolver_failures_are_staged_instead_of_infrastructure_holds(self): class Connection: is_postgres = True class DB: url = 'postgresql://fixture' last_error = '' conn = Connection() @staticmethod def require_runtime_safety_schema(): return True @staticmethod def require_final_cutover(): return True @staticmethod def has_claimable_targets_v2(*_args, **_kwargs): return True @staticmethod def finish_source_cycle(*_args, **_kwargs): return True class Lease: def release(self): return None args = SimpleNamespace( platform='github', workers=1, max_targets=1, timeout=10, target_retry_max_attempts=3, target_retry_base_delay_sec=60, target_retry_max_delay_sec=3600, refresh_registry=False, target_claim_order='oldest', result_bundle_min_free_bytes=0, result_bundle_max_event_bytes=1024 * 1024, projection_backlog_max_items=10, projection_backlog_max_bytes=8 * 1024 * 1024, projection_backlog_headroom_bytes=2 * 1024 * 1024, result_spool_wait_sec=0.01, exact_git_planning_enabled=True, ) claim = { 'target': 'https://github.com/Owner/Repo.git', 'reservation_id': 12, 'reservation_token': 'reservation-token', 'bundle_id': 'bundle-id', 'scan_event_id': 'event-id', 'claim_lease_token': 'lease-token', 'ready_relative_path': 'ready/bundle', 'attempts': 1, } identity = SimpleNamespace(as_dict=lambda: { 'pid': 1, 'creation_time': 'fixture', 'executable': 'python', }) cases = [ ( scanner.RateLimitError( 'github', 'missing', category='not_found', retryable=False, auth_related=False, ), 'failed', 0, ), ( scanner.RateLimitError( 'github', 'limited', category='rate_limit', retryable=True, auth_related=True, ), 'deferred', 1, ), ] for error, expected_queue_status, expected_source_failures in cases: captured = {} def stage(result, _claim, _root, _options, disposition, **_kwargs): captured['result'] = result captured['disposition'] = disposition return scanner.StagedResult( target=result['target'], scan_event_id='event-id', bundle_id='bundle-id', reservation_id=12, scan_event_hash='hash', actual_bytes=1, relative_path='ready/bundle', frame_count=1, finding_count=0, error_count=1, candidate_count=0, queue_status=disposition['queue_status'], source_failure=bool(result.get('source_failure')), source_failure_category=str(result.get('source_failure_category') or ''), source_failure_auth_related=bool(result.get('source_failure_auth_related')), first_error=str(result['errors'][0]), ) notification = mock.Mock(enabled=True) notification.mark_result_bundle_ready.return_value = True with self.subTest(category=error.category), tempfile.TemporaryDirectory() as temp_dir, \ mock.patch.dict(os.environ, {'TRUF_SUPERVISOR_INSTANCE_ID': 'fixture'}), \ mock.patch.object(scanner.scan_config, 'max_active_scans', 1), \ mock.patch.object(console_runner, 'require_private_directory', return_value=temp_dir), \ mock.patch.object(console_runner, 'current_process_identity', return_value=identity), \ mock.patch.object(console_runner, 'queue_files_for_args', return_value=(None, None)), \ mock.patch.object(console_runner, 'prepare_scan_options', return_value={'token': 'token'}), \ mock.patch.object(console_runner, 'acquire_scan_slot', return_value=Lease()), \ mock.patch.object(console_runner, 'scan_slot_scope', return_value=mock.MagicMock()), \ mock.patch.object(console_runner, 'ensure_bundle_reservation_paths'), \ mock.patch.object(console_runner, 'reserve_v2_admission_with_recovery', return_value= console_runner.V2AdmissionOutcome(claim, False)), \ mock.patch.object(console_runner, 'resolve_and_bind_git_claim', side_effect= console_runner._GitResolutionFailure(error)), \ mock.patch.object(console_runner, 'scan_target_result') as scan, \ mock.patch.object(console_runner, 'stage_result_bundle', side_effect=stage), \ mock.patch.object(console_runner, 'ScannerDB', return_value=notification), \ mock.patch.object(console_runner, 'supervised_spool_stop_requested', return_value=False): metrics = console_runner.run_cycle_v2(args, DB(), 1, 2, 'github') self.assertEqual(captured['disposition']['queue_status'], expected_queue_status) self.assertEqual(metrics['staged_count'], 1) self.assertEqual(metrics['source_failure_count'], expected_source_failures) scan.assert_not_called() def test_fresh_schema_contains_plan_and_coverage_columns(self): with tempfile.TemporaryDirectory() as temp_dir: db = ScannerDB(db_path=os.path.join(temp_dir, 'scanner.db'), db_url='') try: reservations = { row['name'] for row in db.conn.execute( 'PRAGMA table_info(result_reservations)' ).fetchall() } queue = { row['name'] for row in db.conn.execute( 'PRAGMA table_info(target_queue)' ).fetchall() } finally: db.close() self.assertTrue({'git_scan_plan_json', 'git_scan_plan_sha256'} <= reservations) self.assertTrue({'covered_ref', 'covered_head'} <= queue) class InstalledTruffleHogContractTests(unittest.TestCase): def test_installed_binary_accepts_commit_sha_as_branch(self): executable = Path(r'C:\Tools\trufflehog.exe') if not executable.is_file(): self.skipTest('configured TruffleHog binary is not installed') with tempfile.TemporaryDirectory(dir=ROOT / 'tmp') as temp_dir: repo = Path(temp_dir) / 'repo' repo.mkdir() home = Path(temp_dir) / 'home' home.mkdir() env = {key: os.environ[key] for key in ( 'PATH', 'SystemRoot', 'WINDIR', 'COMSPEC', 'PATHEXT', ) if key in os.environ} env.update( TMP=temp_dir, TEMP=temp_dir, TMPDIR=temp_dir, HOME=str(home), USERPROFILE=str(home), GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL=os.devnull, GIT_TERMINAL_PROMPT='0', GIT_ALLOW_PROTOCOL='file', HTTP_PROXY='http://127.0.0.1:9', HTTPS_PROXY='http://127.0.0.1:9', ALL_PROXY='http://127.0.0.1:9', ) git = ['git', '-c', f'core.hooksPath={home}', '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.test', '-C', str(repo)] subprocess.run(git + ['init', '-q', f'--template={home}'], env=env, check=True) revisions = [] for number in range(3): (repo / 'README.md').write_text(f'exact revision fixture {number}\n', encoding='ascii') subprocess.run(git + ['add', 'README.md'], env=env, check=True) subprocess.run(git + ['commit', '-q', '-m', 'fixture'], env=env, check=True) revisions.append(subprocess.check_output(git + ['rev-parse', 'HEAD'], env=env, text=True).strip()) uri = 'file://' + repo.as_posix() if os.name == 'nt' else repo.as_uri() for label, head, options, success in ( ('baseline', revisions[1], ['--max-depth', '2'], True), ('delta', revisions[1], ['--since-commit', revisions[0]], True), ('missing_head', 'f' * 40, ['--max-depth', '2'], False), ('missing_base', revisions[1], ['--since-commit', 'f' * 40], False), ): with self.subTest(case=label): completed = subprocess.run( [str(executable), 'git', uri, '--json', '--no-update', '--no-verification', '--local-dev', '--concurrency', '1', '--branch', head, *options], cwd=temp_dir, env=env, capture_output=True, text=True, timeout=120, ) result = scanner.apply_trufflehog_diagnostics( {'errors': []}, completed.stderr, completed.returncode, 'git', require_completion=True, ) if not success: self.assertGreater(len(result['errors']), 0) self.assertIn('unable to resolve commit: object not found', completed.stderr) continue self.assertEqual(completed.returncode, 0) self.assertEqual(len(result['errors']), 0) self.assertTrue(result['scan_meta']['trufflehog_finished']) messages = [json.loads(line) for line in completed.stderr.splitlines() if line.strip().startswith('{')] finished = [message for message in messages if message.get('msg') == 'finished scanning'] self.assertTrue(any(message.get('chunks', 0) > 0 and message.get('bytes', 0) > 0 for message in finished)) if __name__ == '__main__': unittest.main()