the-custodian/docs/changes/2026-11-01-scanner-json-digest/native-r2/test_filesystem.py
codex 26d4efd280
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
Reject generated-only commits / generated-commit-guard (push) Successful in 14s
Prepare exact guarded JSON-digest scanner rollout from founder preflight
2026-11-02 01:25:37 +01:00

164 lines
10 KiB
Python

"""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()