#!/usr/bin/env python3 """Verify the proposed file mount, encoded configuration and rollback in isolation.""" import argparse import hashlib import importlib.util import json from pathlib import Path import subprocess import tempfile BASE = Path(__file__).with_name('verify_forge_lifecycle.py') BASE_SHA256 = '88bbf4235086b7ffc40ca4d7df7fafa1d67e2e1b41d2f0055ec82421ed4840cb' if hashlib.sha256(BASE.read_bytes()).hexdigest() != BASE_SHA256: raise SystemExit('Lifecycle helper revision changed.') spec = importlib.util.spec_from_file_location('lifecycle', BASE) base = importlib.util.module_from_spec(spec) spec.loader.exec_module(base) SETTING = 'GITEA__GIT_0X2E_CONFIG__INIT_0X2E_TEMPLATEDIR' TEMPLATE = '/opt/forge-secret-template' MOUNT = TEMPLATE + '/hooks/pre-receive.d/10-custodian-secret-scan' class Probe(base.Probe): def __init__(self, receipt, hook_file): super().__init__(receipt) self.hook_file = hook_file self.evidence['scope'] = ('Isolated exact-image instance with SQLite and no network. ' 'One read-only mount of the public hook fixture simulates a Kubernetes file subPath. ' 'No production data, configuration or credentials used; production Kubernetes acceptance remains separate.') def run(self, argv, *args, **kwargs): if argv[:2] == ['docker', 'run']: argv = list(argv) index = argv.index(base.IMAGE) argv[index:index] = ['--mount', f'type=bind,src={self.hook_file},dst={MOUNT},readonly'] return super().run(argv, *args, **kwargs) def set_template(self, value): self.execute('env', SETTING + '=' + value, 'environment-to-ini', '--config', base.CONFIG, '--work-path', base.ROOT, '--out', base.CONFIG) self.run(['docker', 'restart', self.cid]) self.start_web() selected = self.execute('git', 'config', '--file', base.ROOT + '/git-home/.gitconfig', '--get', 'init.templateDir').stdout.decode().strip() self.check('encoded_setting_selected_' + ('default' if value != TEMPLATE else 'template'), selected == value) def execute(self, *args, **kwargs): if args[:5] == ('git', 'config', '--file', base.ROOT + '/git-home/.gitconfig', 'init.templateDir'): self.set_template(TEMPLATE) result = super().execute('sh', '-c', 'test -f "$1" && test ! -L "$1" && test -x "$1"', 'sh', MOUNT) self.check('mounted_hook_regular_executable', result.returncode == 0) return result return super().execute(*args, **kwargs) def run_probe(self, artifact_dir): super().run_probe(artifact_dir) self.evidence['result'] = 'unverified' self.phase = 'rollback-default-template' self.set_template('/usr/share/git-core/templates') status, _ = self.api('POST', '/api/v1/user/repos', {'name': 'rollback', 'private': True, 'auto_init': True}) self.check('rollback_new_repository_created', status == 201) absent = self.execute('test', '-e', self.profile('rollback'), allowed=(0, 1)) self.check('rollback_restores_default_creation_behavior', absent.returncode == 1) for repo in ['coverage', 'templated']: digest = self.execute('sha256sum', self.profile(repo)).stdout.split()[0].decode() self.check('rollback_preserves_existing_' + repo + '_hook', digest == hashlib.sha256(self.hook_file.read_bytes()).hexdigest()) self.evidence['result'] = 'verified' def close(self): super().close() self.evidence['base_executor_sha256'] = BASE_SHA256 self.evidence['executor_sha256'] = hashlib.sha256(Path(__file__).read_bytes()).hexdigest() self.receipt.write_text(json.dumps(self.evidence, indent=2) + '\n') def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--artifact-dir', type=Path, required=True) p.add_argument('--receipt', type=Path, required=True) args = p.parse_args() with tempfile.TemporaryDirectory(prefix='forge-template-fixture-') as temporary: hook = Path(temporary) / 'hook' hook.write_bytes(Path(__file__).with_name('receive-profile-hook').read_bytes()) hook.chmod(0o555) probe = Probe(args.receipt, hook) try: probe.run_probe(args.artifact_dir) except (ValueError, OSError, KeyError, subprocess.SubprocessError) as error: probe.evidence['failure'] = probe.phase + ':' + type(error).__name__ raise SystemExit('Template acceptance incomplete at phase ' + probe.phase) finally: probe.close() if __name__ == '__main__': main()