the-custodian/docs/changes/CUST-WP-0080/verify_forge_template.py

96 lines
4.6 KiB
Python

#!/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()