Initial server source import

This commit is contained in:
sashatrask
2026-09-30 20:30:56 +03:00
commit 170dd941b9
498 changed files with 261563 additions and 0 deletions
+666
View File
@@ -0,0 +1,666 @@
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()