Files
truf-server/tests/test_scan_execution.py
T
2026-09-30 20:30:56 +03:00

905 lines
43 KiB
Python

import base64
import copy
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import time
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
APP = ROOT / 'app'
sys.path.insert(0, str(APP))
import scan_execution
import scanner
import scanner_db
from lifecycle_authority import LifecycleAuthorityError
from parity_helpers import (
configured_trufflehog, native_streamed_command, normalized_bundle_evidence,
)
from result_bundle import BundleReservation, FORMAT_VERSION, ResultBundleReader
from worker_contracts import WorkerPhase, validate_phase_transition
def reservation(platform='github'):
return BundleReservation(
reservation_id=7, reservation_token='request-token', bundle_id='a' * 32,
scan_event_id='b' * 32, queue_id=11, claim_lease_token='lease-token',
declared_bytes=1024, ready_path='ready/aa/' + 'a' * 32 + '.trb',
source=platform, platform=platform, query='fixture',
target='https://example.invalid/repo',
normalized_target='https://example.invalid/repo',
)
def scan_policy():
return {
'drop_detectors': ['generic'],
'strict_git_provider_token_filter': True,
'trufflehog_stdout_max_mb': 2,
'trufflehog_stderr_max_mb': 1,
'result_bundle_max_event_bytes': 64 * 1024 * 1024,
'trufflehog_max_findings_per_target': 20000,
'trufflehog_job_memory_limit_bytes': 0,
'trufflehog_windows_job_cpu_weight': 0,
'trufflehog_windows_memory_priority': 0,
'trufflehog_diagnostic_max_lines': 100,
'trufflehog_diagnostic_max_line_chars': 1000,
'trufflehog_diagnostic_max_line_bytes': 1000,
'trufflehog_diagnostic_max_errors': 10,
'trufflehog_diagnostic_max_warnings': 10,
'trufflehog_diagnostic_max_unclassified': 5,
}
def source_reservation(platform, target, bundle_id='a' * 32, reservation_id=7):
return BundleReservation(
reservation_id=reservation_id, reservation_token='request-token',
bundle_id=bundle_id, scan_event_id='b' * 32, queue_id=11,
claim_lease_token='lease-token', declared_bytes=1024 * 1024,
ready_path=f'ready/{bundle_id[:2]}/{bundle_id}.trb',
source=platform, platform=platform, query='fixture', target=target,
normalized_target=scanner.normalize_target(target, platform),
)
def noop_git_plan():
head = 'a' * 40
return {
'version': 1, 'provider': 'github',
'repo_url': 'https://github.com/Owner/Repo.git',
'repo_path': 'Owner/Repo', 'branch': 'Feature/Main',
'ref': 'refs/heads/Feature/Main', 'head_sha': head,
'ref_source': 'explicit', 'base_sha': head, 'mode': 'noop',
'baseline_depth': 100,
}
class ScanExecutionTests(unittest.TestCase):
def test_compatibility_requires_exact_policy_and_platform(self):
value = scan_execution.ScanCompatibility(
scan_execution.PROTOCOL_VERSION, FORMAT_VERSION, 'windows-x86_64',
'a' * 64, 'b' * 64, 'c' * 64,
)
self.assertIs(scan_execution.validate_scan_compatibility(value, value), value)
for field in ('platform_tag', 'code_manifest_sha256', 'effective_config_sha256',
'detector_policy_sha256'):
changed = dict(value.as_dict())
changed[field] = 'linux-x86_64' if field == 'platform_tag' else 'd' * 64
with self.subTest(field=field), self.assertRaises(scan_execution.ScanExecutionError):
scan_execution.validate_scan_compatibility(value, changed)
def test_scan_kwargs_reject_unknown_commands_and_unbounded_timeout(self):
self.assertEqual(
scan_execution.validate_scan_kwargs('github', {'timeout_sec': 60, 'git_plan': {}}),
{'timeout_sec': 60.0, 'git_plan': {}},
)
for value in ({'timeout_sec': 60, 'command': ['calc']}, {'timeout_sec': 0}):
with self.assertRaises(scan_execution.ScanExecutionError):
scan_execution.validate_scan_kwargs('github', value)
def test_remote_assignment_deadlines_are_exact_and_immutable(self):
reservation_value = {
'remote_issued_at': '2026-09-17T00:00:00+00:00',
'remote_expires_at': '2026-09-18T00:00:00+00:00',
}
deadlines = {
'target_scan_timeout_seconds': 60,
'result_upload_body_timeout_seconds': 1800,
'assignment_ttl_seconds': 86400,
'assignment_issued_at': reservation_value['remote_issued_at'],
'assignment_deadline_at': reservation_value['remote_expires_at'],
}
self.assertEqual(
scan_execution._normalize_remote_assignment_deadlines(
deadlines, reservation_value, {'timeout_sec': 60.0},
),
deadlines,
)
invalid = []
extra = copy.deepcopy(deadlines)
extra['unknown'] = 1
invalid.append((extra, reservation_value, {'timeout_sec': 60.0}))
boolean = copy.deepcopy(deadlines)
boolean['assignment_ttl_seconds'] = True
invalid.append((boolean, reservation_value, {'timeout_sec': 60.0}))
wrong_scan = copy.deepcopy(deadlines)
wrong_scan['target_scan_timeout_seconds'] = 61
invalid.append((wrong_scan, reservation_value, {'timeout_sec': 60.0}))
wrong_interval = copy.deepcopy(deadlines)
wrong_interval['assignment_ttl_seconds'] = 86399
invalid.append((wrong_interval, reservation_value, {'timeout_sec': 60.0}))
changed_reservation = copy.deepcopy(reservation_value)
changed_reservation['remote_expires_at'] = '2026-09-18T00:00:01+00:00'
invalid.append((deadlines, changed_reservation, {'timeout_sec': 60.0}))
noncanonical = copy.deepcopy(deadlines)
noncanonical['assignment_issued_at'] = '2026-09-17T00:00:00Z'
noncanonical_reservation = copy.deepcopy(reservation_value)
noncanonical_reservation['remote_issued_at'] = noncanonical[
'assignment_issued_at'
]
invalid.append((noncanonical, noncanonical_reservation, {'timeout_sec': 60.0}))
for value, reserved, scan_kwargs in invalid:
with self.subTest(value=value), self.assertRaises(
scan_execution.ScanExecutionError,
):
scan_execution._normalize_remote_assignment_deadlines(
value, reserved, scan_kwargs,
)
def test_every_remote_scan_policy_field_changes_effective_identity(self):
kwargs = {'timeout_sec': 60, 'trufflehog_config': '@package/detector_policy'}
event = {'timeout_sec': 60.0, 'trufflehog_config': '@package/detector_policy'}
limits = {'candidate_max_items': 20, 'candidate_max_bytes': 4096}
policy = scan_policy()
original, _ = scan_execution.remote_execution_identity(
'github', kwargs, event, {}, limits, policy,
)
for name, value in policy.items():
changed = dict(policy)
if name == 'drop_detectors':
changed[name] = ['other']
elif isinstance(value, bool):
changed[name] = not value
else:
changed[name] = value + 1
with self.subTest(name=name):
updated, _ = scan_execution.remote_execution_identity(
'github', kwargs, event, {}, limits, changed,
)
self.assertNotEqual(updated, original)
def test_remote_scan_policy_overrides_inherited_process_settings(self):
findings = [
{'DetectorName': 'Generic', 'Raw': 'fixture'},
{'DetectorName': 'GitHub', 'Raw': 'not-a-token', 'Verified': False},
]
with mock.patch.dict(os.environ, {
'TRUFFLEHOG_STDOUT_MAX_MB': '999',
'TRUFFLEHOG_STDERR_MAX_MB': '999',
'TRUFFLEHOG_MAX_FINDINGS_PER_TARGET': '999999',
}):
with scanner.client_scan_execution_policy(scan_policy()):
kept, dropped, _ = scanner.filter_dropped_detectors(findings)
self.assertEqual(dropped, 1)
kept, noisy = scanner.filter_noisy_findings(kept)
self.assertEqual((kept, noisy), ([], 1))
self.assertEqual(
scanner.command_output_limits(), (2 * 1024 * 1024, 1024 * 1024),
)
self.assertEqual(scanner._trufflehog_diagnostic_limits(), {
'lines': 100, 'line_chars': 1000, 'line_bytes': 1000,
'errors': 10, 'warnings': 10, 'unclassified': 5,
})
def test_queue_disposition_preserves_existing_retry_classes(self):
policy = scan_execution.QueueDispositionPolicy(
target_retry_max_attempts=3, target_retry_base_delay_sec=10,
target_retry_max_delay_sec=100, target_timeout_retry_delay_sec=60,
soft_skip_reasons=('no_ci_runs',),
)
cases = (
({'errors': []}, 1, 'done', False),
({'errors': ['bad'], 'retryable': False}, 1, 'failed', False),
({'errors': ['timeout'], 'error_class': 'timeout'}, 1, 'deferred', False),
({'errors': ['source'], 'source_failure': True, 'retryable': True}, 3,
'deferred', True),
({'errors': [], 'skipped': 'no_ci_runs'}, 1, 'deferred', True),
)
for result, attempts, status, reset in cases:
with self.subTest(status=status, result=result):
disposition = scan_execution.queue_disposition_for_result(
result, 'github_actions', attempts, policy,
)
self.assertEqual(disposition['queue_status'], status)
self.assertEqual(disposition['reset_attempts'], reset)
def test_planned_execution_reuses_scanner_and_canonical_bundle_staging(self):
result = {'findings': [], 'errors': [], 'target': 'wrong', 'scan_type': 'wrong'}
staged = object()
with mock.patch.object(scan_execution, 'scan_target_result', return_value=result) as scan, \
mock.patch.object(scan_execution, 'stage_result_bundle', return_value=staged) as stage:
actual = scan_execution.execute_planned_result_in_scope(
reservation(), '/private/bundles', {'timeout_sec': 60}, {'detectors': 'OpenAI'},
scan_execution.QueueDispositionPolicy(), attempts=1,
scan_meta_defaults={'assignment': {'mode': 'remote'}},
)
self.assertIs(actual, staged)
scan.assert_called_once_with(
'https://example.invalid/repo', 'github', 'b' * 32, {'timeout_sec': 60.0},
)
args = stage.call_args.args
self.assertEqual(args[0]['target'], reservation().target)
self.assertEqual(args[0]['scan_type'], 'github')
self.assertEqual(args[0]['scan_meta']['assignment']['mode'], 'remote')
self.assertEqual(args[4]['queue_status'], 'done')
def test_runner_phase_callback_tracks_real_execution_boundaries_and_cleanup_counts(self):
phases = []
def scan(*_args, **_kwargs):
scanner.emit_client_scan_phase('scanning', {'records_seen': 3})
scanner.emit_client_scan_phase('filtering', {'records_seen': 3})
return {'findings': [], 'errors': []}
with mock.patch.object(
scan_execution, 'scan_slot_scope', return_value=mock.MagicMock(),
), mock.patch.object(
scan_execution, 'scan_target_result', side_effect=scan,
), mock.patch.object(
scan_execution, 'cleanup_assignment_work_dir',
return_value={'enumerated': 2, 'removed': 2, 'retained': 0},
), mock.patch.object(
scan_execution, 'stage_result_bundle', return_value=object(),
):
scan_execution.execute_planned_claim(
reservation(), '/private/bundles', {'timeout_sec': 60}, {},
scan_execution.QueueDispositionPolicy(), scan_policy(), attempts=1,
phase_callback=lambda phase, progress=None: phases.append(
(phase, dict(progress or {})),
),
)
self.assertEqual([item[0] for item in phases], [
'waiting_permit', 'scanning', 'filtering',
'cleaning', 'cleaning', 'bundling',
])
self.assertEqual(phases[-2][1]['removed'], 2)
self.assertEqual(phases[1][1], {'records_seen': 3})
self.assertEqual(phases[0][1], {'boundary': 'scan_slot_scope'})
def test_provider_callbacks_expose_only_observable_or_integrated_boundaries(self):
def streamed_output():
output = mock.MagicMock()
output.returncode = 0
output.stderr_lines.return_value = iter(())
output.stdout_lines.return_value = iter(())
context = mock.MagicMock()
context.__enter__.return_value = output
context.__exit__.return_value = False
return context
docker_target = 'docker.io/library/alpine@sha256:' + ('a' * 64)
docker_phases = []
manifest = scanner._client_scan_manifest.set({'executables': {}})
direct = scanner._client_remote_execution_kind.set('docker_direct_v1')
try:
with scanner.client_scan_phase_events(
lambda phase, progress=None: docker_phases.append(
(phase, dict(progress or {})),
)
), mock.patch.object(
scanner, 'get_trufflehog_cmd', return_value='trufflehog',
), mock.patch.object(
scanner, 'run_command_streamed', return_value=streamed_output(),
):
scanner.scan_docker_image(docker_target, timeout_sec=60)
finally:
scanner._client_remote_execution_kind.reset(direct)
scanner._client_scan_manifest.reset(manifest)
self.assertEqual(docker_phases[0], (
'scanning', {'integrated_operation': 'docker_pull_and_scan'},
))
self.assertNotIn('downloading', [item[0] for item in docker_phases])
hf_phases = []
direct = scanner._client_remote_execution_kind.set('huggingface_space_v1')
try:
with scanner.client_scan_phase_events(
lambda phase, progress=None: hf_phases.append(
(phase, dict(progress or {})),
)
), mock.patch.object(
scanner, 'get_trufflehog_cmd', return_value='trufflehog',
), mock.patch.object(
scanner, 'run_command_streamed', return_value=streamed_output(),
):
scanner.scan_huggingface_space('Example/Public-Space', timeout_sec=60)
finally:
scanner._client_remote_execution_kind.reset(direct)
self.assertEqual(hf_phases[0], (
'scanning', {'integrated_operation': 'huggingface_clone_and_scan'},
))
self.assertNotIn('cloning', [item[0] for item in hf_phases])
resolving = []
with scanner.client_scan_phase_events(
lambda phase, progress=None: resolving.append((phase, dict(progress or {}))),
), mock.patch.object(
scanner, 'recent_commit_boundary',
return_value={'skip': True, 'reason': 'fixture'},
):
scanner.scan_git_repo('https://example.invalid/repo', timeout_sec=60)
self.assertEqual(resolving[0][0], 'resolving')
self.assertEqual(resolving[0][1]['operation'], 'recent_commit_boundary')
docker_resolution = []
with scanner.client_scan_phase_events(
lambda phase, progress=None: docker_resolution.append(
(phase, dict(progress or {}), time.monotonic()),
)
), mock.patch.object(
scanner, 'resolve_docker_content_manifest',
side_effect=scanner.DockerContentScanError(
'fixture_resolution', 'fixture resolution stopped',
),
):
scanner._recover_docker_image_contents(
docker_target, time.monotonic() + 60, None, None, 0,
None, None, False, None, anonymous_public_client=True,
)
self.assertEqual(docker_resolution[0][0], 'resolving')
self.assertEqual(
docker_resolution[0][1]['operation'],
'docker_manifest_resolution_recovery',
)
self.assertEqual(
validate_phase_transition(
WorkerPhase.SCANNING, WorkerPhase(docker_resolution[0][0]),
),
WorkerPhase.RESOLVING,
)
def test_native_git_checkout_recovery_emits_real_cloning_boundary(self):
plan = {**noop_git_plan(), 'mode': 'baseline'}
phases = []
outputs = []
for returncode in (1, 0, 0):
output = mock.MagicMock()
output.returncode = returncode
output.stderr_lines.return_value = iter(())
output.stdout_lines.return_value = iter(())
context = mock.MagicMock()
context.__enter__.return_value = output
context.__exit__.return_value = False
outputs.append(context)
diagnostics = [0]
def apply(result, *_args, **_kwargs):
diagnostics[0] += 1
if diagnostics[0] == 1:
result['errors'] = ['checkout failed']
with tempfile.TemporaryDirectory() as temporary, \
scanner.client_scan_phase_events(
lambda phase, progress=None: phases.append(
(phase, dict(progress or {}), time.monotonic()),
),
), mock.patch.object(
scanner, 'get_trufflehog_cmd', return_value='trufflehog',
), mock.patch.object(
scanner, 'get_git_cmd', return_value='git',
), mock.patch.object(
scanner, 'run_command_streamed', side_effect=outputs,
), mock.patch.object(
scanner, 'apply_trufflehog_diagnostics', side_effect=apply,
), mock.patch.object(
scanner, 'append_trufflehog_findings',
), mock.patch.object(
scanner, 'git_checkout_recovery_allowed', return_value=True,
), mock.patch.object(
scanner, 'create_command_work_dir', return_value=temporary,
), mock.patch.object(
scanner, 'cleanup_command_work_dir',
):
scanner.scan_exact_git_plan(
plan['repo_url'], plan, 'f' * 64, 60,
None, None, False, None, None, False,
)
names = [item[0] for item in phases]
self.assertEqual(names[:3], ['scanning', 'cloning', 'scanning'])
for previous, current in zip(names, names[1:]):
validate_phase_transition(WorkerPhase(previous), WorkerPhase(current))
measured = [
later[2] - earlier[2]
for earlier, later in zip(phases, phases[1:])
]
self.assertTrue(measured)
self.assertTrue(all(duration >= 0 for duration in measured))
def test_docker_direct_claim_executes_bound_target_and_stages_bundle(self):
target = 'docker.io/library/alpine@sha256:' + ('a' * 64)
claim = BundleReservation(
reservation_id=17, reservation_token='docker-request-token',
bundle_id='d' * 32, scan_event_id='e' * 32, queue_id=23,
claim_lease_token='docker-lease-token', declared_bytes=1024 * 1024,
ready_path='ready/dd/' + ('d' * 32) + '.trb',
source='dockerhub', platform='docker', query='fixture',
target=target, normalized_target=scanner.normalize_target(target, 'docker'),
)
validated = {
'reservation': claim,
'planning_kind': 'docker_direct_v1',
'execution_target': target,
}
options = {'timeout_sec': 60.0}
manifest_token = scanner._client_scan_manifest.set({'executables': {}})
try:
with tempfile.TemporaryDirectory() as temp_dir:
bundle_root = os.path.join(temp_dir, 'bundles')
scanner.ensure_private_directory(bundle_root, reject_reparse=True)
with mock.patch.object(
scan_execution, 'scan_slot_scope', return_value=mock.MagicMock(),
), mock.patch.object(
scanner, 'scan_docker_image',
return_value={'findings': [], 'errors': [], 'scan_meta': {}},
) as docker_scan, mock.patch.object(
scanner.docker_token_manager, 'get_next_config',
side_effect=AssertionError('direct Docker cannot select server credentials'),
):
staged = scan_execution.execute_protocol2_remote_claim(
validated, bundle_root, options, options,
scan_execution.QueueDispositionPolicy(), scan_policy(), attempts=1,
)
docker_scan.assert_called_once()
self.assertEqual(docker_scan.call_args.args[0], target)
self.assertIsNone(docker_scan.call_args.kwargs['config_dir'])
reader = ResultBundleReader(
os.path.join(bundle_root, staged.relative_path),
)
metadata = reader.metadata()
self.assertEqual(metadata['target'], target)
self.assertEqual(metadata['scan_type'], 'docker')
encoded = json.dumps(metadata, sort_keys=True)
for forbidden in (
'docker_layer_work', 'docker_registry_auth',
'git_scan_plan', 'git_scan_execution',
):
self.assertNotIn(forbidden, encoded)
finally:
scanner._client_scan_manifest.reset(manifest_token)
self.assertIsNone(scanner._client_remote_execution_kind.get())
def test_huggingface_direct_claim_is_tokenless_and_stages_bundle(self):
target = 'ExampleOrg/Public-Space'
claim = BundleReservation(
reservation_id=18, reservation_token='hf-request-token',
bundle_id='f' * 32, scan_event_id='1' * 32, queue_id=24,
claim_lease_token='hf-lease-token', declared_bytes=1024 * 1024,
ready_path='ready/ff/' + ('f' * 32) + '.trb',
source='huggingface', platform='huggingface', query='fixture',
target=target,
normalized_target=scanner.normalize_target(target, 'huggingface'),
)
validated = {
'reservation': claim,
'planning_kind': 'huggingface_space_v1',
'execution_target': target,
}
options = {'timeout_sec': 60.0}
manifest_token = scanner._client_scan_manifest.set({'executables': {}})
try:
with tempfile.TemporaryDirectory() as temp_dir:
bundle_root = os.path.join(temp_dir, 'bundles')
scanner.ensure_private_directory(bundle_root, reject_reparse=True)
with mock.patch.object(
scan_execution, 'scan_slot_scope', return_value=mock.MagicMock(),
), mock.patch.object(
scanner, 'scan_huggingface_space',
return_value={'findings': [], 'errors': [], 'scan_meta': {}},
) as hf_scan:
staged = scan_execution.execute_protocol2_remote_claim(
validated, bundle_root, options, options,
scan_execution.QueueDispositionPolicy(), scan_policy(), attempts=1,
)
hf_scan.assert_called_once()
self.assertEqual(hf_scan.call_args.args[0], target)
self.assertIsNone(hf_scan.call_args.kwargs.get('token'))
reader = ResultBundleReader(
os.path.join(bundle_root, staged.relative_path),
)
metadata = reader.metadata()
self.assertEqual(metadata['target'], target)
self.assertEqual(metadata['scan_type'], 'huggingface')
encoded = json.dumps(metadata, sort_keys=True)
for forbidden in (
'huggingface_token', 'hf_token', 'authorization',
'docker_layer_work', 'docker_registry_auth',
'git_scan_plan', 'git_scan_execution',
):
self.assertNotIn(forbidden, encoded.lower())
finally:
scanner._client_scan_manifest.reset(manifest_token)
self.assertIsNone(scanner._client_remote_execution_kind.get())
def test_exact_git_noop_local_and_db_free_bundles_have_identical_coverage(self):
plan = noop_git_plan()
claim = source_reservation('github', plan['repo_url'])
kwargs = {'timeout_sec': 60, 'git_plan': plan}
queue_policy = scan_execution.QueueDispositionPolicy()
with tempfile.TemporaryDirectory() as temp_dir:
local_root = os.path.join(temp_dir, 'local')
remote_root = os.path.join(temp_dir, 'remote')
scanner.ensure_private_directory(local_root, reject_reparse=True)
scanner.ensure_private_directory(remote_root, reject_reparse=True)
with mock.patch.object(
scanner, 'run_command_streamed',
side_effect=AssertionError('noop Git plan must not launch a scanner'),
):
local_result = scanner.scan_target_result(
claim.target, claim.platform, claim.scan_event_id, kwargs,
)
local = scan_execution.stage_scan_result_in_scope(
local_result, claim, local_root, {}, queue_policy, attempts=1,
)
remote = scan_execution.execute_planned_result_in_scope(
claim, remote_root, kwargs, {}, queue_policy, attempts=1,
)
local_metadata = ResultBundleReader(
os.path.join(local_root, local.relative_path),
).metadata()
remote_metadata = ResultBundleReader(
os.path.join(remote_root, remote.relative_path),
).metadata()
self.assertEqual(local.queue_status, remote.queue_status)
for name in ('git_scan_plan', 'git_scan_execution'):
self.assertEqual(local_metadata[name], remote_metadata[name])
self.assertEqual(
local_metadata['scan_meta']['exact_git_scope'],
remote_metadata['scan_meta']['exact_git_scope'],
)
encoded_plan = scanner_db.canonical_git_scan_plan_bytes(plan)
stored = {
'git_scan_plan_json': encoded_plan.decode('ascii'),
'git_scan_plan_sha256': hashlib.sha256(encoded_plan).hexdigest(),
}
for metadata in (local_metadata, remote_metadata):
self.assertEqual(
scanner_db.matching_git_coverage(
stored, metadata, metadata['queue_status'], metadata['error_count'],
),
(True, plan['ref'], plan['head_sha']),
)
@unittest.skipUnless(
configured_trufflehog() and shutil.which('git'),
'configured TruffleHog and Git executables are required',
)
def test_native_exact_git_local_and_db_free_bundles_are_equivalent(self):
executable = configured_trufflehog()
git_executable = shutil.which('git')
xai_before = 'xai-' + hashlib.sha512(b'xai-before-parity').hexdigest()[:48]
xai_after = 'xai-' + hashlib.sha512(b'xai-after-parity').hexdigest()[:48]
zai_before = 'zai-' + base64.urlsafe_b64encode(
hashlib.sha512(b'zai-before-parity').digest()
).decode('ascii')[:48]
zai_after = 'zai-' + base64.urlsafe_b64encode(
hashlib.sha512(b'zai-after-parity').digest()
).decode('ascii')[:48]
fixtures = (
('xai-before.env', f'XAI_API_KEY={xai_before}\n'),
('xai-after.env', f'{xai_after} api.x.ai\n'),
('zai-before.env', f'ZAI_API_KEY={zai_before}\n'),
('zai-after.env', f'{zai_after} api.z.ai\n'),
)
policy = str(APP / 'trufflehog-custom-detectors.yaml')
repo_url = 'https://gitlab.com/Fixture/Parity.git'
with tempfile.TemporaryDirectory() as temp_dir:
source = Path(temp_dir) / 'source'
source.mkdir()
git_env = os.environ.copy()
git_env.update({
'GIT_CONFIG_NOSYSTEM': '1', 'GIT_CONFIG_GLOBAL': os.devnull,
'GIT_TERMINAL_PROMPT': '0', 'GIT_ALLOW_PROTOCOL': 'file',
})
def git(*args):
completed = subprocess.run(
[git_executable, '-c', 'user.name=Parity Fixture', '-c',
'user.email=parity@example.invalid', '-c', 'commit.gpgsign=false',
*args],
cwd=source, env=git_env, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=False, timeout=30,
)
self.assertEqual(
completed.returncode, 0,
completed.stderr.decode('utf-8', errors='replace'),
)
return completed.stdout.decode('ascii').strip()
git('init', '--quiet', '--initial-branch=main', '--template=')
for name, content in fixtures:
(source / name).write_text(content, encoding='ascii', newline='\n')
git('add', '--', name)
git('commit', '--quiet', '-m', name)
head = git('rev-parse', 'HEAD')
plan = {
'version': 1, 'provider': 'gitlab', 'repo_url': repo_url,
'repo_path': 'Fixture/Parity', 'branch': 'main',
'ref': 'refs/heads/main', 'head_sha': head, 'base_sha': None,
'mode': 'baseline', 'baseline_depth': len(fixtures),
'ref_source': 'explicit',
}
claim = source_reservation('gitlab', repo_url)
kwargs = {
'timeout_sec': 60, 'git_plan': plan, 'no_verification': True,
'trufflehog_config': policy,
'external_trufflehog_lifecycle': True,
}
local_root = os.path.join(temp_dir, 'local')
remote_root = os.path.join(temp_dir, 'remote')
scanner.ensure_private_directory(local_root, reject_reparse=True)
scanner.ensure_private_directory(remote_root, reject_reparse=True)
execution_env = dict(git_env)
execution_env.update({
'GIT_CONFIG_COUNT': '1',
'GIT_CONFIG_KEY_0': f'url.{source.as_uri()}.insteadOf',
'GIT_CONFIG_VALUE_0': repo_url,
})
with mock.patch.dict(os.environ, execution_env, clear=True), \
mock.patch.object(
scanner, 'get_trufflehog_cmd', return_value=str(executable),
), mock.patch.object(
scanner, 'run_command_streamed',
side_effect=native_streamed_command,
):
local_result = scanner.scan_target_result(
claim.target, claim.platform, claim.scan_event_id, kwargs,
)
local = scan_execution.stage_scan_result_in_scope(
local_result, claim, local_root, {},
scan_execution.QueueDispositionPolicy(), attempts=1,
)
remote = scan_execution.execute_planned_result_in_scope(
claim, remote_root, kwargs, {},
scan_execution.QueueDispositionPolicy(), attempts=1,
)
local_evidence = normalized_bundle_evidence(
os.path.join(local_root, local.relative_path),
)
remote_evidence = normalized_bundle_evidence(
os.path.join(remote_root, remote.relative_path),
)
self.assertEqual(local_evidence, remote_evidence)
self.assertEqual(local.queue_status, 'done')
self.assertEqual(local.queue_status, remote.queue_status)
findings = local_evidence['findings']
self.assertEqual(
{item.get('DetectorName') for item in findings}, {'Xai', 'ZaiGLM'},
)
self.assertEqual(
{item.get('ExtraData', {}).get('name') for item in findings},
{'Xai', 'XaiContextAfter', 'ZaiGLM', 'ZaiGLMContextAfter'},
)
self.assertEqual(
{item.get('service') for item in local_evidence['candidates']},
{'xai', 'zai'},
)
encoded_plan = scanner_db.canonical_git_scan_plan_bytes(plan)
stored = {
'git_scan_plan_json': encoded_plan.decode('ascii'),
'git_scan_plan_sha256': hashlib.sha256(encoded_plan).hexdigest(),
}
metadata = local_evidence['metadata']
self.assertEqual(
scanner_db.matching_git_coverage(
stored, metadata, metadata['queue_status'], metadata['error_count'],
),
(True, plan['ref'], plan['head_sha']),
)
@unittest.skipUnless(
configured_trufflehog(), 'configured TruffleHog executable is required',
)
def test_native_postman_local_and_db_free_bundles_are_equivalent(self):
executable = configured_trufflehog()
gemini_key = 'AIza' + ('A' * 35)
azure_key = 'a' * 32
endpoint = 'fixture.openai.azure.com'
content = json.dumps({
'values': [
{'key': 'GEMINI_API_KEY', 'value': gemini_key},
{'key': 'AZURE_OPENAI_KEY', 'value': azure_key},
{'url': f'https://{endpoint}/openai/deployments/demo'},
],
}, sort_keys=True).encode('utf-8')
with tempfile.TemporaryDirectory() as temp_dir:
cache_root = os.path.join(temp_dir, 'cache')
work_root = os.path.join(temp_dir, 'work')
local_root = os.path.join(temp_dir, 'local')
remote_root = os.path.join(temp_dir, 'remote')
for path in (cache_root, work_root, local_root, remote_root):
scanner.ensure_private_directory(path, reject_reparse=True)
with mock.patch.multiple(
scanner.scan_config, postman_cache_dir=cache_root, runtime_dir=temp_dir,
postman_cache_min_free_bytes=0, min_free_gb=0,
):
cache_path, digest, size = scanner.write_postman_cache(
content, kind='collection', cache_dir=cache_root,
)
postman = {
'source': 'fixture', 'repo': 'Owner/Repo',
'path': 'collection.json', 'kind': 'collection',
'cache_path': cache_path, 'sha256': digest, 'size': size,
}
target = json.dumps(postman, sort_keys=True)
claim = source_reservation('postman', target)
kwargs = {
'timeout_sec': 60, 'max_artifact_size_mb': 1,
'no_verification': True,
}
with mock.patch.object(
scanner, 'get_work_dir', return_value=work_root,
), mock.patch.object(
scanner, 'require_scanner_runtime_initialized', return_value=None,
), mock.patch.object(
scanner, 'get_trufflehog_cmd', return_value=str(executable),
), mock.patch.object(
scanner, 'run_command_streamed', side_effect=native_streamed_command,
):
result = scanner.scan_target_result(
target, 'postman', claim.scan_event_id, kwargs,
)
self.assertTrue(
result.get('structured_keycheck_pending'),
result.get('warnings') or result.get('errors'),
)
local = scan_execution.stage_scan_result_in_scope(
result, claim, local_root, {},
scan_execution.QueueDispositionPolicy(), attempts=1,
)
remote = scan_execution.execute_planned_result_in_scope(
claim, remote_root, kwargs, {},
scan_execution.QueueDispositionPolicy(), attempts=1,
)
local_evidence = normalized_bundle_evidence(
os.path.join(local_root, local.relative_path),
)
remote_evidence = normalized_bundle_evidence(
os.path.join(remote_root, remote.relative_path),
)
self.assertEqual(local_evidence, remote_evidence)
self.assertEqual(local.queue_status, remote.queue_status)
self.assertEqual(local.queue_status, 'done')
candidates = local_evidence['candidates']
self.assertEqual({item['service'] for item in candidates}, {'gemini', 'azure'})
self.assertTrue(all(
item['candidate_kind'] == 'structured_postman' for item in candidates
))
origin = f'fixture:Owner/Repo:collection.json:{digest}'
expected = {
'gemini': (gemini_key, gemini_key, 'GoogleAIStudio'),
'azure': (
f'{endpoint}:{azure_key}', f'{azure_key}:{endpoint}', 'AzureOpenAI',
),
}
for item in candidates:
probe, raw, detector = expected[item['service']]
self.assertEqual(item['metadata']['origin'], origin)
self.assertEqual(item['metadata']['detector_name'], detector)
self.assertTrue(item['metadata']['structured_origin'].startswith(f'{origin}:$'))
self.assertEqual(item['attribution']['origin'], item['metadata']['structured_origin'])
self.assertEqual(
item['provider_key_hash'], hashlib.sha256(probe.encode('utf-8')).hexdigest(),
)
self.assertEqual(
item['secret_hash'], hashlib.sha256(raw.encode('utf-8')).hexdigest(),
)
self.assertEqual(item['credential_hash'], hashlib.sha256(
f"truf-credential-v2|{item['service']}|{probe}".encode('utf-8')
).hexdigest())
encoded = json.dumps(item, sort_keys=True)
for forbidden in ('status', 'status_group', 'checked_at', 'result_source'):
self.assertNotIn(f'"{forbidden}"', encoded)
self.assertEqual(len(candidates), 2)
def test_structured_postman_candidate_staging_rejects_size_or_hash_drift(self):
content = json.dumps({'token': 'AIza' + ('A' * 35)}).encode('utf-8')
with tempfile.TemporaryDirectory() as temp_dir:
cache_root = os.path.join(temp_dir, 'cache')
scanner.ensure_private_directory(cache_root, reject_reparse=True)
with mock.patch.multiple(
scanner.scan_config, postman_cache_dir=cache_root, runtime_dir=temp_dir,
postman_cache_min_free_bytes=0,
):
cache_path, digest, size = scanner.write_postman_cache(
content, cache_dir=cache_root,
)
for index, drift in enumerate(('size', 'hash'), start=1):
with self.subTest(drift=drift):
bundle_root = os.path.join(temp_dir, f'bundles-{index}')
scanner.ensure_private_directory(bundle_root, reject_reparse=True)
declared_digest = 'c' * 64 if drift == 'hash' else digest
target = f'postman:sha256:{declared_digest}'
claim = source_reservation(
'postman', target, bundle_id=str(index) * 32,
reservation_id=index,
)
result = {
'scan_event_id': claim.scan_event_id, 'target': target,
'scan_type': 'postman', 'findings': [], 'errors': [],
'structured_keycheck_pending': True,
'postman': {
'source': 'fixture', 'cache_path': cache_path,
'sha256': declared_digest, 'size': size,
},
'bytes': size + 1 if drift == 'size' else size,
'postman_max_artifact_size_mb': 1,
}
staged = scan_execution.stage_scan_result_in_scope(
result, claim, bundle_root, {},
scan_execution.QueueDispositionPolicy(), attempts=1,
)
reader = ResultBundleReader(
os.path.join(bundle_root, staged.relative_path),
)
self.assertEqual(list(reader.iter_candidates()), [])
metadata = reader.metadata()
self.assertTrue(metadata['degraded'])
self.assertTrue(any(
'structured keycheck extraction failed' in warning.lower()
for warning in metadata['warnings']
))
def test_client_manifest_authorizes_only_manifested_tools_and_policy(self):
manifest = {
'executables': {
'trufflehog': {'path': os.path.abspath('trufflehog'), 'sha256': 'a' * 64},
'git': {'path': os.path.abspath('git'), 'sha256': 'b' * 64},
},
'assets': {},
}
with mock.patch.object(scanner, 'verify_code_manifest', return_value=manifest), \
mock.patch.object(scanner, 'resolve_manifest_executable',
return_value=manifest['executables']['trufflehog']['path']):
with scanner.client_scan_launch_authority({}, expected_sha256='c' * 64):
self.assertEqual(scanner.get_git_cmd(), manifest['executables']['git']['path'])
metadata = scanner.require_trufflehog_launch_authority(
[manifest['executables']['trufflehog']['path'], 'filesystem', '/fixture'],
)
self.assertEqual(metadata['authority'], 'remote-worker')
with mock.patch.object(scanner.os.path, 'isabs', return_value=True):
metadata = scanner.require_git_clone_launch_authority([
manifest['executables']['git']['path'], 'clone', '--no-checkout',
'--no-recurse-submodules', '--', 'https://example.invalid/repo',
os.path.abspath('checkout'),
])
self.assertEqual(metadata['authority'], 'remote-worker')
with self.assertRaises(LifecycleAuthorityError):
scanner.require_trufflehog_launch_authority(
[manifest['executables']['trufflehog']['path'], 'filesystem', '/fixture'],
)
if __name__ == '__main__':
unittest.main()