96 lines
4.6 KiB
Python
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()
|