"""Actual scripts in private PID/network namespaces and tmpfs; no remote writes.""" from pathlib import Path import hashlib import importlib.util import json import os import shutil import subprocess import sys import time def sha(raw): return hashlib.sha256(raw).hexdigest() def run(argv, data=None, success=True): result = subprocess.run(argv, input=data, capture_output=True, timeout=180) if (result.returncode == 0) != success: # Inputs are public or disposable synthetic material. Do not print raw diagnostics. phases = [] for line in result.stderr.splitlines(): try: value = json.loads(line) if isinstance(value, dict) and value.get('result') == 'unverified': phases.append(value.get('probe_phase', 'unknown')) except (ValueError, UnicodeError): pass raise AssertionError('unexpected-script-status:' + str(result.returncode) + ';phase=' + ','.join(phases)) return result def inside(case): c = json.loads(Path('/fixtures/control.json').read_text()) old, new = Path(c['old_directory']), Path(c['new_directory']) old.mkdir(parents=True) for name, src in [('receive-secret-scan', '/old-adapter'), ('gitleaks', '/provider')]: shutil.copyfile(src, old / name); (old / name).chmod(0o775) new.mkdir() for name, src in [('receive-secret-scan', '/new-adapter'), ('gitleaks', '/provider')]: shutil.copyfile(src, new / name); (new / name).chmod(0o775) def guard(command, success=True): return run(command, success=success) def clean_new(): shutil.rmtree(new) def stage(success=True, payload=None): return run(['sh', '/scripts/stage.sh', str(new), str(old), c['candidate_sha256'], c['provider_sha256'], 'offline-fixture'], data=Path('/new-adapter').read_bytes() if payload is None else payload, success=success) if case == 'old-single-guard-counterexample': guard(c['single_command']) assert all((new / n).stat().st_mode & 0o777 == 0o755 for n in ['receive-secret-scan', 'gitleaks']) assert all((old / n).stat().st_mode & 0o777 == 0o775 for n in ['receive-secret-scan', 'gitleaks']) elif case == 'both-version-guard': guard(c['combined_command']) assert all((p / n).stat().st_mode & 0o777 == 0o755 for p in [old, new] for n in ['receive-secret-scan', 'gitleaks']) elif case in ['guard-old-drift', 'guard-new-drift', 'guard-symlink', 'guard-unexpected-mode']: if case == 'guard-old-drift': (old / 'gitleaks').write_bytes(b'changed') if case == 'guard-new-drift': (new / 'receive-secret-scan').write_bytes(b'changed') if case == 'guard-symlink': (new / 'gitleaks').unlink(); (new / 'gitleaks').symlink_to(old / 'gitleaks') if case == 'guard-unexpected-mode': (new / 'gitleaks').chmod(0o777) guard(c['combined_command'], success=False) elif case == 'stage-roundtrip': clean_new(); stage(); stage() assert sha((new / 'receive-secret-scan').read_bytes()) == c['candidate_sha256'] assert not list(new.parent.glob(new.name + '.staging-*')) elif case == 'stage-bad-hash-cleanup': clean_new(); stage(success=False, payload=b'incomplete-transfer') assert not new.exists() and not list(new.parent.glob(new.name + '.staging-*')) elif case == 'stage-existing-unowned': (new / 'unrelated').write_bytes(b'preserve-me') stage(success=False) assert (new / 'unrelated').read_bytes() == b'preserve-me' elif case == 'stage-symlink-refused': clean_new(); new.symlink_to(old) stage(success=False) assert (old / 'receive-secret-scan').stat().st_mode & 0o777 == 0o775 elif case.startswith('hooks-'): area = Path('/tmp/control'); area.mkdir() before, after = b'#!/bin/sh\nexit 0\n', b'#!/bin/sh\nexit 2\n' (area / 'before').write_bytes(before); (area / 'after').write_bytes(after) paths = [Path('/data/git/gitea-repositories/test/' + name + '.git/hooks/pre-receive.d/10-custodian-secret-scan') for name in ['first', 'second']] for path in paths: path.parent.mkdir(parents=True); path.write_bytes(before); path.chmod(0o775) (path.parent / 'protected').write_bytes(b'unchanged authorization') (area / 'paths').write_text(''.join(str(p) + '\n' for p in paths)) def hooks(direction, success=True): return run(['sh', '/scripts/hooks.sh', str(area), direction, sha(before), sha(after)], success=success) if case == 'hooks-roundtrip': hooks('apply') assert all(p.read_bytes() == after and p.stat().st_mode & 0o777 == 0o775 for p in paths) hooks('rollback') assert all(p.read_bytes() == before and p.stat().st_mode & 0o777 == 0o775 for p in paths) elif case == 'hooks-refuse-complete-scope-before-writing': paths[1].write_bytes(b'unexpected owner change'); hooks('apply', success=False) assert paths[0].read_bytes() == before assert paths[1].read_bytes() == b'unexpected owner change' elif case == 'hooks-partial-failure-recovers': paths[1].parent.chmod(0o555); hooks('apply', success=False) assert all(p.read_bytes() == before for p in paths) assert all(p.stat().st_mode & 0o777 == 0o775 for p in paths) elif case == 'hooks-symlink-refused': paths[1].unlink(); paths[1].symlink_to(paths[0]); hooks('apply', success=False) assert paths[0].read_bytes() == before elif case == 'hooks-rollback-unknown-drift-retained': hooks('apply'); paths[1].write_bytes(b'unknown-drift'); hooks('rollback', success=False) assert paths[0].read_bytes() == after and paths[1].read_bytes() == b'unknown-drift' assert all((p.parent / 'protected').read_bytes() == b'unchanged authorization' for p in paths) assert not list(Path('/data').rglob('.custodian-*')) elif case == 'real-receive-and-quarantine': guard(c['combined_command']) result = json.loads(run(['sh', '/scripts/probe.sh', str(new), '/dispatcher']).stdout) assert all(value is True for value in result.values()) assert not list(Path('/dev/shm').iterdir()) else: raise ValueError('unknown-case') print(json.dumps({'case': case, 'passed': True, 'native_acceptance': False})) def outside(): import argparse parser = argparse.ArgumentParser(); parser.add_argument('--attempt', required=True) args = parser.parse_args() if not args.attempt.isalnum(): raise ValueError('unsafe-attempt') here = Path(__file__).resolve().parent receipt = here / ('filesystem-' + args.attempt + '.json') with receipt.open('x') as f: f.write('{"result":"unverified"}\n') cfg = json.loads((here / 'inputs.json').read_bytes()) fixtures = Path('/tmp/sync-scanner-native-fixtures'); fixtures.mkdir(exist_ok=True) guard = cfg['new_guard_command'].copy() guard[2] = guard[2][len(cfg['guard_command'][2]) + 1:] control = {'old_directory': cfg['old_dir'], 'new_directory': cfg['new_dir'], 'candidate_sha256': cfg['new_adapter_sha256'], 'provider_sha256': cfg['scanner_sha256'], 'combined_command': guard, 'single_command': guard} (fixtures / 'control.json').write_text(json.dumps(control)) (fixtures / 'passwd').write_text('gitea:x:1000:1000:Offline fixture:/tmp:/bin/sh\n') (fixtures / 'group').write_text('gitea:x:1000:\n') binaries = {'old-adapter': Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'), 'new-adapter': Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'), 'provider': Path('/home/worsch/.local/lib/repo-manager/gitleaks/8.30.1/gitleaks'), 'dispatcher': here / 'fixture-dispatcher.sh'} assert sha(binaries['new-adapter'].read_bytes()) == cfg['new_adapter_sha256'] assert sha(binaries['provider'].read_bytes()) == cfg['scanner_sha256'] prefix = ['bwrap', '--unshare-net', '--unshare-pid', '--die-with-parent', '--new-session'] for path in ['/usr', '/lib', '/lib64']: prefix += ['--ro-bind', path, path] prefix += ['--symlink', 'usr/bin', '/bin', '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/dev/shm', '--tmpfs', '/tmp', '--tmpfs', '/data', '--ro-bind', str(here), '/scripts', '--ro-bind', str(fixtures), '/fixtures', '--dir', '/etc', '--ro-bind', str(fixtures / 'passwd'), '/etc/passwd', '--ro-bind', str(fixtures / 'group'), '/etc/group'] for name, path in binaries.items(): prefix += ['--ro-bind', str(path), '/' + name] prefix += ['--clearenv', '--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'HOME', '/tmp', '--chdir', '/', '--', 'python3', '/scripts/test_filesystem.py', '--inside'] cases = ['guard-new-drift', 'guard-symlink', 'guard-unexpected-mode', 'stage-roundtrip', 'stage-bad-hash-cleanup', 'stage-existing-unowned', 'stage-symlink-refused', 'hooks-roundtrip', 'hooks-refuse-complete-scope-before-writing', 'hooks-partial-failure-recovers', 'hooks-symlink-refused', 'hooks-rollback-unknown-drift-retained', 'real-receive-and-quarantine'] results = [] try: for case in cases: started = time.monotonic(); completed = subprocess.run(prefix + [case], capture_output=True, timeout=200) failure = [line for line in completed.stderr.decode(errors='replace').splitlines() if line.startswith('AssertionError: unexpected-script-status:')] results.append({'case': case, 'returncode': completed.returncode, 'seconds': round(time.monotonic()-started, 2), **({'failure': failure[-1]} if failure else {})}) receipt.write_text(json.dumps({'result': 'unverified', 'cases': results}, indent=2) + '\n') if completed.returncode: raise ValueError('offline-case-failed:' + case) receipt.write_text(json.dumps({'result': 'passed', 'cases': results, 'native_acceptance': False, 'real_remote_refs_changed': False, 'scope': 'Exact new guard, stage/hook scripts, real candidate/Gitleaks receive probes; original guard bytes unchanged.'}, indent=2) + '\n') finally: print(json.dumps({'receipt': str(receipt), 'passed': sum(x['returncode'] == 0 for x in results), 'total': len(cases)})) if __name__ == '__main__': if len(sys.argv) == 3 and sys.argv[1] == '--inside': inside(sys.argv[2]) else: outside()