#!/usr/bin/env python3 """Activate exact reviewed additive Forgejo hook profile and verify SSH refusals. Synthetic pushes are always vetoed by a temporary independent hook. Existing Forgejo hooks are hash-checked and preserved. No real credential is read and no synthetic ref is retained. Native frontend/HTTP/new-repo coverage stays separate. """ import argparse import hashlib import importlib.util import json import os from pathlib import Path import re import shlex import subprocess import tempfile def sha(path): return hashlib.sha256(Path(path).read_bytes()).hexdigest() def helper(proposal): path = Path(__file__).with_name('verify_receive_native.py') if sha(__file__) != proposal['executor_sha256'] or sha(path) != proposal['native_helper_sha256']: raise ValueError('executor-revision-changed') spec = importlib.util.spec_from_file_location('native_receive_support', path) module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) return module def shell(native, proposal, script, payload=None, timeout=60): return native.execute(proposal, 'bash', '-c', 'set -euo pipefail\n' + script, payload=payload, timeout=timeout) def repo_script(proposal, body): names = proposal['repositories'] if len(names) != 22 or len(set(names)) != len(names) or any(not re.fullmatch(r'[a-z0-9-]+', n) for n in names): raise ValueError('scope-refused') return 'for name in ' + ' '.join(names) + '; do\nrepo=/data/git/gitea-repositories/coulomb/$name.git\n' + body + '\ndone\n' def validate(proposal): for name in [proposal['hook_name'], proposal['veto_name']]: if not re.fullmatch(r'[a-z0-9-]+', name): raise ValueError('unsafe-hook-name') if not re.fullmatch(r'/data/security/receive-secret-scan/[a-z0-9-]+', proposal['version_directory']): raise ValueError('unsafe-version-directory') if not re.fullmatch(r'refs/heads/custodian-g07-probe-[a-z0-9-]+', proposal['probe_ref']): raise ValueError('unsafe-probe-ref') def inspect(native, proposal, applied=False, sentinel=False): validate(proposal) prefix = proposal['hook_name']; veto = proposal['veto_name'] expected = dict(proposal['existing_hook_hashes']) if applied: expected['pre-receive.d/' + prefix] = proposal['hook_sha256'] if sentinel: expected['pre-receive.d/' + veto] = proposal['veto_sha256'] body = '''test -d "$repo" && test ! -L "$repo" && test ! -L "$repo/hooks" && test ! -L "$repo/hooks/pre-receive.d" test -z "$(find "$repo/hooks" -maxdepth 2 -type l -print -quit)" printf 'REPO %s\n' "$name" find "$repo/hooks" -maxdepth 2 -type f ! -name '*.sample' -exec sha256sum {} \\; ''' if applied: body += 'test -x "$repo/hooks/pre-receive.d/' + proposal['hook_name'] + '"\n' if sentinel: body += 'test -x "$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '"\n' raw = shell(native, proposal, repo_script(proposal, body)).decode() rows = {}; current = None for line in raw.splitlines(): if line.startswith('REPO '): current = line[5:]; rows[current] = {} else: digest, name = line.split(' ', 1) if not re.fullmatch(r'[a-f0-9]{64}', digest): raise ValueError('invalid-hook-digest') prefix_path = '/data/git/gitea-repositories/coulomb/' + current + '.git/hooks/' if not name.startswith(prefix_path): raise ValueError('unexpected-hook-path') rows[current][name[len(prefix_path):]] = digest if set(rows) != set(proposal['repositories']) or any(v != expected for v in rows.values()): raise ValueError('hook-inventory-changed') return rows def rollback(native, proposal): validate(proposal) for name, digest in [(proposal['hook_name'], proposal['hook_sha256']), (proposal['veto_name'], proposal['veto_sha256'])]: body = 'path="$repo/hooks/pre-receive.d/' + name + '"\n' body += 'if test -e "$path"; then test ! -L "$path"; test "$(sha256sum "$path" | cut -d" " -f1)" = ' + shlex.quote(digest) + '; fi\n' shell(native, proposal, repo_script(proposal, body)) for name in [proposal['hook_name'], proposal['veto_name']]: shell(native, proposal, repo_script(proposal, 'rm -f -- "$repo/hooks/pre-receive.d/' + name + '"')) inspect(native, proposal) def git(directory, args, payload=None, accepted=(0,)): env = dict(os.environ) for key in list(env): if key.startswith('GIT_'): env.pop(key) env.update(GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_TERMINAL_PROMPT='0', GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=10') r = subprocess.run(['/usr/bin/git', *args], cwd=directory, env=env, input=payload, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120) if r.returncode not in accepted: raise ValueError('git-probe-command-failed') return r def verify_pushes(native, proposal): fixture = Path(proposal['fixture_key']) if sha(fixture) != proposal['fixture_sha256']: raise ValueError('fixture-changed') rows = [] with tempfile.TemporaryDirectory(prefix='forge-receive-push-proof-') as temporary: directory = Path(temporary) git(directory, ['init', '-q', '-b', 'main']) git(directory, ['config', 'core.hooksPath', '/dev/null']) git(directory, ['config', 'user.name', 'Custodian disposable detector probe']) git(directory, ['config', 'user.email', 'probe@example.test']) (directory / 'source.txt').write_text('Disposable detector probe; intended to be rejected by a temporary veto.\n') git(directory, ['add', 'source.txt']); git(directory, ['commit', '-qm', proposal['proposal_id'] + ' clean probe']) clean = git(directory, ['rev-parse', 'HEAD']).stdout.decode().strip() (directory / 'fixture.txt').write_bytes(fixture.read_bytes()) git(directory, ['add', 'fixture.txt']); git(directory, ['commit', '-qm', proposal['proposal_id'] + ' synthetic detector fixture']) blocked = git(directory, ['rev-parse', 'HEAD']).stdout.decode().strip() for number, name in enumerate(proposal['repositories']): remote = 'forgejo-remote:coulomb/' + name + '.git' if git(directory, ['ls-remote', remote, proposal['probe_ref']]).stdout.strip(): raise ValueError('probe-ref-already-exists') row = {'repo': name} for oid, status in [(clean, 'clean'), (blocked, 'blocked')]: r = git(directory, ['push', '--porcelain', remote, oid + ':' + proposal['probe_ref']], accepted=(0, 1)) output = r.stdout + r.stderr if r.returncode == 0: # Only our newly created ref may be removed, with a lease. git(directory, ['push', '--force-with-lease=' + proposal['probe_ref'] + ':' + oid, remote, ':' + proposal['probe_ref']]) raise ValueError('independent-veto-failed-ref-removed') if fixture.read_bytes().strip() in output: raise ValueError('synthetic-content-disclosed') found = [] for line in output.splitlines(): match = re.search(rb'\{"status":.*\}', line) if match: try: found.append(json.loads(match.group())) except ValueError: raise ValueError('malformed-control-receipt') if len(found) != 1 or found[0].get('status') != status or b'CUSTODIAN_G07_PROBE_VETO' not in output: raise ValueError('native-push-proof-incomplete') row[status + '_scanner_result'] = status if git(directory, ['ls-remote', remote, proposal['probe_ref']]).stdout.strip(): raise ValueError('unexpected-probe-ref-retained') raw = native.execute(proposal, '/usr/bin/git', '--git-dir=/data/git/gitea-repositories/coulomb/' + name + '.git', 'cat-file', '--batch-check', payload=(blocked + '\n').encode()).decode().strip() if raw != blocked + ' missing': raise ValueError('quarantine-object-retained') row.update(temporary_veto_observed=True, probe_ref_absent=True, quarantined_commit_absent=True) rows.append(row) if (number + 1) % 5 == 0: print('Verified SSH refusal probes:', number + 1, 'of', len(proposal['repositories']), flush=True) return rows def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--proposal', type=Path, required=True); p.add_argument('--receipt', type=Path, required=True) modes = p.add_mutually_exclusive_group() modes.add_argument('--apply', action='store_true'); modes.add_argument('--rollback', action='store_true') args = p.parse_args(); proposal = json.loads(args.proposal.read_text()); native = helper(proposal) # Reuse only the read-only target/artifact preflight with its own pinned revision. validate(proposal) support = dict(proposal); support['executor_sha256'] = proposal['native_helper_sha256'] if args.rollback: support['artifacts'] = [] observation = native.preflight(support) if args.rollback: rollback(native, proposal); print('Prior hook inventory restored; admitted tools remain inert.'); return inspect(native, proposal) ref = shlex.quote(proposal['probe_ref']) shell(native, proposal, repo_script(proposal, 'test -z "$(git --git-dir="$repo" for-each-ref --format=\'%(refname)\' ' + ref + ')"')) if not args.apply: print(json.dumps({'status': 'preflight-verified', 'repositories': len(proposal['repositories']), **observation})); return temporary = '/tmp/' + proposal['directory_name']; version = proposal['version_directory'] receipt = {'proposal_id': proposal['proposal_id'], 'proposal_sha256': sha(args.proposal), 'result': 'unverified', 'observation': observation, 'phase': 'stage-tools', 'no_live_credentials_used': True, 'executed_revision': proposal['revision']} created = changed = False try: native.execute(proposal, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" "$2" > "$1/.probe-owner"', 'sh', temporary, proposal['proposal_id']); created = True native.execute(proposal, 'tar', '-xmf', '-', '-C', temporary, payload=native.archive(proposal), timeout=90) lines = ['cd ' + shlex.quote(temporary), 'sha256sum -c SHA256SUMS >/dev/null 2>&1'] for parent in ['/data/security', '/data/security/receive-secret-scan', version]: lines.append('test ! -L ' + shlex.quote(parent)) lines.append('mkdir -p ' + shlex.quote(version)) for artifact in proposal['artifacts']: if artifact['name'] not in ['receive-secret-scan', 'gitleaks']: continue target = version + '/' + artifact['name']; quoted = shlex.quote(target) lines.append('if test -e ' + quoted + '; then test ! -L ' + quoted + '; test "$(sha256sum ' + quoted + ' | cut -d" " -f1)" = ' + shlex.quote(artifact['sha256']) + '; else cp ' + shlex.quote(artifact['name']) + ' ' + quoted + '; fi') lines.append('chmod 755 ' + quoted) shell(native, proposal, '\n'.join(lines)) receipt['phase'] = 'install-hooks'; changed = True body = '' for field, source in [('veto_name', 'probe-veto'), ('hook_name', 'profile-hook')]: dest = '"$repo/hooks/pre-receive.d/' + proposal[field] + '"' body += 'test ! -e ' + dest + ' && test ! -L ' + dest + '\n' body += 'staged=$(mktemp \"$repo/hooks/.custodian-install-XXXXXX\")\n' body += 'cp ' + shlex.quote(temporary + '/' + source) + ' \"$staged\"\nchmod 755 \"$staged\"\nmv -n \"$staged\" ' + dest + '\n' body += 'test ! -e \"$staged\"\n' shell(native, proposal, repo_script(proposal, body)) inspect(native, proposal, applied=True, sentinel=True) receipt['phase'] = 'verify-veto' body = 'set +e\noutput=$(printf "%040d %040d %s\\n" 0 1 ' + ref + ' | "$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '" 2>&1)\ncode=$?\nset -e\ntest "$code" = 1 && test "$output" = CUSTODIAN_G07_PROBE_VETO' shell(native, proposal, repo_script(proposal, body)) receipt['phase'] = 'verify-ssh-pushes' receipt['repositories'] = verify_pushes(native, proposal) receipt['phase'] = 'remove-temporary-veto' body = 'path="$repo/hooks/pre-receive.d/' + proposal['veto_name'] + '"\ntest "$(sha256sum "$path" | cut -d" " -f1)" = ' + shlex.quote(proposal['veto_sha256']) + '\nrm -- "$path"' shell(native, proposal, repo_script(proposal, body)) inspect(native, proposal, applied=True) native.execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz') receipt.update(result='verified', phase='active', temporary_veto_removed=True, health_after=True, verification_limit='Synthetic pushes deliberately vetoed; scanner clean/detection results and quarantine absence verified. Normal clean acceptance needs the subsequent authorized source push; HTTP/frontend/lifecycle/outside-scope remain separate.') except BaseException: if changed: try: rollback(native, proposal); receipt['rollback'] = 'prior hook inventory restored' except BaseException: receipt['rollback'] = 'unverified; inspect exact target hashes' raise finally: if created: try: native.execute(proposal, 'sh', '-c', 'test "$(cat "$1/.probe-owner")" = "$2" && rm -rf -- "$1" && test ! -e "$1"', 'sh', temporary, proposal['proposal_id']) receipt['temporary_directory_removed'] = True except BaseException: receipt['temporary_directory_removed'] = False; receipt['result'] = 'unverified' args.receipt.write_text(json.dumps(receipt, indent=2) + '\n') if receipt['result'] != 'verified': raise ValueError('cleanup-incomplete') print('Receive-path profile active for 22 repositories; SSH detector/veto/quarantine proofs verified.') if __name__ == '__main__': try: main() except (ValueError, OSError, KeyError, subprocess.SubprocessError): raise SystemExit('Activation refused or incomplete; inspect value-free receipt. Raw diagnostics suppressed.')