#!/usr/bin/env python3 """Admit an exact private inventory, add hooks, and verify vetoed SSH pushes.""" import argparse import datetime import hashlib import importlib.util import json from pathlib import Path import re import shlex HERE=Path(__file__).parent s=importlib.util.spec_from_file_location('template',HERE/'activate_forge_template.py') t=importlib.util.module_from_spec(s) s.loader.exec_module(t) def repo_path(row): if row['owner']!='coulomb' or not re.fullmatch(r'[a-z0-9-]+',row['name']): raise ValueError('scope-identity-refused') return '/data/git/gitea-repositories/coulomb/'+row['name']+'.git' def inspect(pod,rows,expected): commands=['set -euo pipefail'] for row in rows: commands += ['repo='+shlex.quote(repo_path(row)), 'printf "REPO '+str(row['id'])+'\\n"', '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)"', 'find "$repo/hooks" -maxdepth 2 -type f ! -name "*.sample" -exec sha256sum {} \\;', 'test "$(git --git-dir="$repo" rev-parse --is-shallow-repository)" = false', 'test "$(git --git-dir="$repo" rev-parse --show-object-format)" = sha1', 'config=$(git --git-dir="$repo" config --local --includes --name-only --list)', 'if printf "%s\\n" "$config" | grep -Eiq "(^extensions.partialclone$|^remote\\..*\\.(promisor|partialclonefilter)$)"; then exit 21; fi', 'if git --git-dir="$repo" rev-parse --verify HEAD >/dev/null 2>&1; then git --git-dir="$repo" ls-tree -r --format="%(objectmode)" HEAD | awk \'$0=="160000" {bad=1} END {exit bad}\'; fi'] raw=t.native.execute(pod,'bash','-s',payload=('\n'.join(commands)+'\n').encode(),timeout=180).decode() found={}; current=None; by_id={r['id']:r for r in rows} for line in raw.splitlines(): if line.startswith('REPO '): current=int(line[5:]);found[current]={} else: digest,path=line.split(' ',1);prefix=repo_path(by_id[current])+'/hooks/' if not path.startswith(prefix) or not re.fullmatch('[a-f0-9]{64}',digest):raise ValueError('unexpected-hook-output') found[current][path[len(prefix):]]=digest if set(found)!=set(by_id) or any(hooks!=expected for hooks in found.values()): raise ValueError('hook-inventory-changed') def install(pod,path,content,digest): # Temporary executable is outside the dispatch directory. Hard-link creation # atomically exposes a complete file and refuses any existing destination. script='''set -eu umask 077 target=$1 temporary=$2 test ! -e "$target" && test ! -L "$target" test ! -e "$temporary" && test ! -L "$temporary" set -C cat > "$temporary" test "$(sha256sum "$temporary" | cut -d ' ' -f 1)" = "$3" chmod 755 "$temporary" ln "$temporary" "$target" ''' temporary=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name t.native.execute(pod,'sh','-c',script,'sh',path,temporary,digest,payload=content) def remove_owned(pod,path,digest,require_marker=False): marker=path.rsplit('/hooks/pre-receive.d/',1)[0]+'/.custodian-expand-'+Path(path).name t.native.execute(pod,'sh','-c', 'set -eu; if test -e "$1" || test -L "$1"; then test -f "$1" && test ! -L "$1"; ' 'test "$(sha256sum "$1" | cut -d " " -f 1)" = "$2"; ' 'if test "$4" = yes; then test "$1" -ef "$3"; fi; rm -- "$1"; fi; ' 'if test -e "$3" || test -L "$3"; then test -f "$3" && test ! -L "$3"; ' 'test "$(sha256sum "$3" | cut -d " " -f 1)" = "$2"; rm -- "$3"; fi', 'sh',path,digest,marker,'yes' if require_marker else 'no') def rollback(pod,rows,original,proposal,require_marker=True): for row in rows: for name,digest in [(original['hook_name'],original['hook_sha256']), (proposal['veto_name'],proposal['veto_sha256'])]: remove_owned(pod,repo_path(row)+'/hooks/pre-receive.d/'+name,digest,require_marker) inspect(pod,rows,original['existing_hook_hashes']) def main(): parser=argparse.ArgumentParser(description=__doc__) parser.add_argument('--proposal',type=Path,required=True) parser.add_argument('--inventory',type=Path,required=True) parser.add_argument('--receipt',type=Path,required=True) mode=parser.add_mutually_exclusive_group() mode.add_argument('--apply',action='store_true') mode.add_argument('--rollback',action='store_true') args=parser.parse_args() p=json.loads(args.proposal.read_text()) for file,digest in p['executor_hashes'].items(): if hashlib.sha256((HERE/file).read_bytes()).hexdigest()!=digest:raise ValueError('executor-changed') if hashlib.sha256(args.inventory.read_bytes()).hexdigest()!=p['inventory_sha256']:raise ValueError('scope-changed') inventory=json.loads(args.inventory.read_text());by_id={r['id']:r for r in inventory['repositories']} rows=[by_id[i] for i in p['target_ids']] if len(rows)!=p['target_count'] or len(set(p['target_ids']))!=len(rows):raise ValueError('scope-count-changed') if any(r['profile']!='builtin-only' or not r['head_compatible'] for r in rows):raise ValueError('inadmissible-scope') original=json.loads((HERE/'receive-hook-proposal.json').read_text()) pod=t.target_pod(t.deployment(),original) if pod['pod_uid']!=p['pod_uid']:raise ValueError('pod-changed') t.check_artifacts(pod) for a in original['artifacts'][:2]: if t.native.execute(pod,'stat','-c','%a %u %g',original['version_directory']+'/'+a['name']).decode().strip()!='755 1000 1000': raise ValueError('artifact-custody-changed') if args.rollback: receipt=json.loads(args.receipt.read_text()) if receipt['proposal_sha256']!=hashlib.sha256(args.proposal.read_bytes()).hexdigest():raise ValueError('receipt-changed') rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p, require_marker=receipt['result']!='verified') receipt['rollback']='verified-original-profiles-restored' args.receipt.write_text(json.dumps(receipt,indent=2)+'\n');return inspect(pod,rows,original['existing_hook_hashes']) fixture=Path(p['fixture_key']) if hashlib.sha256(fixture.read_bytes()).hexdigest()!=p['fixture_sha256']:raise ValueError('fixture-changed') hook=(HERE/'receive-profile-hook').read_bytes();veto=(HERE/'receive-probe-veto-expansion').read_bytes() assert hashlib.sha256(hook).hexdigest()==original['hook_sha256'] assert hashlib.sha256(veto).hexdigest()==p['veto_sha256'] receipt={'proposal_id':p['proposal_id'],'proposal_sha256':hashlib.sha256(args.proposal.read_bytes()).hexdigest(), 'pod_uid':pod['pod_uid'],'result':'preflight-verified','target_count':len(rows),'attempted_ids':[], 'verified_ids':[],'at':datetime.datetime.now(datetime.timezone.utc).isoformat()} def save():args.receipt.write_text(json.dumps(receipt,indent=2)+'\n') save() if not args.apply:print(json.dumps(receipt));return try: receipt['result']='applying';save() for row in rows: receipt['attempted_ids'].append(row['id']);save() path=repo_path(row)+'/hooks/pre-receive.d/' # No sample ref can be accepted during its probe, even if scanning fails. install(pod,path+p['veto_name'],veto,p['veto_sha256']) install(pod,path+original['hook_name'],hook,original['hook_sha256']) proof=dict(pod,repositories=[row['name']],fixture_key=str(fixture),fixture_sha256=p['fixture_sha256'], probe_ref=p['probe_ref'],proposal_id=p['proposal_id']) t.activation.verify_pushes(t.native,proof) remove_owned(pod,path+p['veto_name'],p['veto_sha256'],require_marker=True) expected=dict(original['existing_hook_hashes']);expected['pre-receive.d/'+original['hook_name']]=original['hook_sha256'] inspect(pod,[row],expected) receipt['verified_ids'].append(row['id']);save() if len(receipt['verified_ids'])%5==0:print('Verified scanner profiles:',len(receipt['verified_ids']),'of',len(rows),flush=True) inspect(pod,rows,expected) receipt.update(result='verified',clean_scanner_and_synthetic_refusal_under_veto=True, no_probe_refs_or_quarantined_commits=True,vetoes_removed=True, original_dispatchers_preserved=True) save() except Exception as error: receipt.update(result='failed',failure=type(error).__name__);save() try: rollback(pod,[by_id[i] for i in receipt['attempted_ids']],original,p) receipt['rollback']='verified-original-profiles-restored' except Exception as recovery: receipt['rollback']='unverified:'+type(recovery).__name__ save();raise SystemExit('Expansion failed; inspect value-free receipt and rollback status.') # After successful acceptance, receipt-bound rollback can use exact hashes. # Cleanup failure leaves only inert ownership links outside the hook directory. for row in rows: target=repo_path(row)+'/hooks/pre-receive.d/'+original['hook_name'] marker=repo_path(row)+'/.custodian-expand-'+original['hook_name'] t.native.execute(pod,'sh','-c','set -eu; test "$1" -ef "$2"; rm -- "$2"','sh',target,marker) receipt['ownership_markers_removed']=True;save() print('All exact target profiles verified.',flush=True) if __name__=='__main__':main()