the-custodian/docs/changes/2026-11-01-scanner-json-digest/native-r4/rollout.py

194 lines
11 KiB
Python

"""Founder-run r4: portable hook migration, exact mode restoration, existing CM.
Original r2/r3 code and failed receipts are preserved. Same scanner binary,
profile, deployment fields and six owner-source candidates. Acknowledgement
binds reviewed bytes; it is not a credential or runtime approval service.
"""
from pathlib import Path
import argparse
import base64
import copy
import hashlib
import importlib.util
import json
import re
import shlex
import subprocess
HERE = Path(__file__).resolve().parent
ORIGINAL = HERE.parent / 'native-r2'
OUTPUT = Path('/tmp/forge-scanner-rollout-r4-receipt.json')
R2 = 'd22b1f555af96c203b7d68efd25e80815ce18ca4e3bf0dd3ab96b6f699d50237'
def sha(raw): return hashlib.sha256(raw).hexdigest()
def ensure(ok, reason):
if not ok: raise RuntimeError(reason)
def support():
spec = importlib.util.spec_from_file_location('original_rollout', ORIGINAL / 'rollout.py')
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
ensure(m.verify_packet() == R2, 'original-reviewed-packet-drift')
m.RECEIPT = OUTPUT
return m
def verify_packet():
raw = (HERE / 'packet.json').read_bytes(); p = json.loads(raw)
for name, digest in p['files'].items():
ensure(Path(name).name == name and sha((HERE / name).read_bytes()) == digest, 'r4-packet-drift')
support()
return sha(raw)
def operation(args):
if 'exec' in args:
for source, label in [(HERE/'runtime_self_test.sh','runtime-tool-self-test'), (HERE/'hooks.sh','hook-migration'), (HERE/'restore_modes.sh','hook-mode-restoration'),
(ORIGINAL/'stage.sh','artifact-staging'), (ORIGINAL/'probe.sh','candidate-receive-probe'),
(ORIGINAL/'rollback-probe.sh','old-policy-receive-probe')]:
if source.read_text() in args: return label
if any(isinstance(a,str) and 'custodian-replace-' in a for a in args): return 'hook-migration'
if any(isinstance(a,str) and 'opened-inode' in a for a in args): return 'hook-mode-restoration'
if any(isinstance(a,str) and 'for repo in /data/git/gitea-repositories/' in a for a in args): return 'hook-inventory'
return 'public-artifact-or-template-check'
for verb in ('get','patch'):
if verb in args: return verb + ':' + args[args.index(verb)+1]
return 'reviewed-kubernetes-operation'
def make_runner(m):
class Rollout(m.Rollout):
def k(self, *args, data=None, timeout=60):
label = operation(args)
try: q = subprocess.run(['kubectl',*args], input=data, capture_output=True, timeout=timeout)
except subprocess.TimeoutExpired:
self.event('command-failed', operation=label, category='timeout')
raise RuntimeError('kubernetes-timeout:' + label)
if q.returncode:
category = 'remote-check-nonzero' if b'command terminated with exit code' in q.stderr else 'command-failed'
for needle,name in [(b'Forbidden','forbidden'),(b'Unauthorized','unauthorized'),(b'NotFound','not-found')]:
if needle in q.stderr: category=name
self.event('command-failed', operation=label, category=category)
raise RuntimeError('kubernetes-command-failed:' + label)
return q.stdout
def existing_cm(self):
cm = json.loads(self.k('-n','forgejo','get','configmap',self.c['new_configmap'],'-o','json'))
wanted = json.loads((ORIGINAL/'configmap.json').read_bytes())
ensure(cm.get('immutable') is True and cm.get('data') == wanted['data'] and
cm['metadata']['name'] == self.c['new_configmap'] and cm['metadata']['namespace'] == 'forgejo', 'existing-public-configmap-drift')
uid = cm['metadata']['uid']
ensure(not hasattr(self,'cm_uid') or self.cm_uid == uid, 'existing-configmap-identity-drift')
self.cm_uid = uid
def dry_run(self,d):
self.existing_cm()
patch = m.patch_for(d,self.c)
self.k('-n','forgejo','patch','deployment','forge','--type=json','--dry-run=server','-p',json.dumps(patch),'-o','name')
diff = subprocess.run(['kubectl','diff','-f','-'], input=(ORIGINAL/'configmap.json').read_bytes(), capture_output=True, timeout=60)
ensure(diff.returncode in (0,1),'client-diff-failed; raw diagnostics withheld')
self.event('server-dry-run-passed', reused_immutable_configmap=self.c['new_configmap'], configmap_uid=self.cm_uid,
client_configmap_diff_exit=diff.returncode, deployment_changes=2, strategy='Recreate', resources_unchanged=True)
def preflight(self):
result = super().preflight()
for files in self.receipt['before_hooks'].values():
for f in files.values():
ensure(f['uid'] == 1000 and f['gid'] == 1000 and f['mode'] in ('755','775'),'unsupported-hook-custody-or-mode')
return result
def migrate(self,pod,direction):
c=self.c
before=self.inventory(pod,{c['old_hook_sha256'],c['new_hook_sha256']})
baseline=self.receipt['before_hooks']
reference=copy.deepcopy(before)
records=[]
expected=c['old_hook_sha256'] if direction=='rollback' else c['new_hook_sha256']
for repo,files in baseline.items():
ensure(repo in before and set(before[repo]) == set(files),'hook-set-drift')
for name,f in files.items():
now=before[repo][name]
ensure((now['uid'],now['gid'])==(f['uid'],f['gid']),'hook-owner-drift')
ensure(now['mode']==f['mode'] or (f['mode']=='755' and now['mode']=='775'),'unexpected-hook-mode-drift')
if name!=m.HOOK: ensure(now['sha256']==f['sha256'],'protected-hook-content-drift')
reference[repo][name]=dict(f)
digest=expected if name==m.HOOK else f['sha256']
records.append(f['mode']+'\t'+digest+'\t'+repo+'/hooks/'+name+'\n')
paths=''.join(repo+'/hooks/'+m.HOOK+'\n' for repo,files in sorted(before.items())
if direction=='rollback' or files[m.HOOK]['sha256']==c['old_hook_sha256'])
# One private tmpfs control area; public hooks/paths/modes only.
wrapper='''set +vx
set -eu
umask 077
test "$(stat -f -c %t /dev/shm)" = 1021994
area=$(mktemp -d /dev/shm/forge-r4-hooks-XXXXXXXX)
cleanup() { code=$?; trap - EXIT; rm -f -- "$area/before" "$area/after" "$area/paths" "$area/modes"; rmdir "$area" || code=70; exit "$code"; }
trap cleanup EXIT
'''
for name in ('before','after'):
b64=base64.b64encode((ORIGINAL/(name+'-hook')).read_bytes()).decode()
wrapper+='printf %s '+shlex.quote(b64)+' | base64 -d > "$area/'+name+'"\n'
# Stream records through stdin; never put a large inventory in argv.
wrapper+=': > "$area/paths"\nwhile IFS= read -r line; do\n test "$line" = __FORGE_R4_MODE_RECORDS__ && break\n printf "%s\\n" "$line" >> "$area/paths"\ndone\ncat > "$area/modes"\n'
if paths:
wrapper+='sh -c '+shlex.quote((HERE/'hooks.sh').read_text())+' sh "$area" "$1" "$2" "$3"\n'
wrapper+='sh -c '+shlex.quote((HERE/'restore_modes.sh').read_text())+' sh "$area/modes"\n'
# shell output contains two JSON lines; only report vetted counters.
raw=self.shell(pod,wrapper,(direction,c['old_hook_sha256'],c['new_hook_sha256']),data=(paths+'__FORGE_R4_MODE_RECORDS__\n'+''.join(records)).encode(),timeout=180)
counters=[json.loads(line) for line in raw.splitlines() if line]
after=self.inventory(pod,{expected})
m.protected_equal(reference,after)
self.event('hooks-'+direction,repositories=len(after),portable_modes=True,baseline_modes_restored=True,
group_write_additions_reversed=sum(x.get('group_write_additions_reversed',0) for x in counters))
return after
def apply(self,binary):
d,pod,before=self.preflight()
raw=binary.read_bytes();ensure(sha(raw)==self.c['new_adapter_sha256'],'candidate-binary-drift')
tools=json.loads(self.shell(pod,(HERE/'runtime_self_test.sh').read_text()))
ensure(all(tools.get(k) for k in ('numeric_modes','descriptor_mode_restore','atomic_rename','temporary_test_removed')),'runtime-tool-self-test-failed')
self.event('runtime-tool-self-test-passed',checks=tools)
self.receipt['native_mutation']=True;self.event('staging-started')
try:
self.shell(pod,(ORIGINAL/'stage.sh').read_text(),(self.c['new_dir'],self.c['old_dir'],self.c['new_adapter_sha256'],self.c['scanner_sha256'],'SYNC-SCANNER-JSON-DIGEST-20261101-R2'),data=raw)
self.shell(pod,self.c['new_guard_command'][2],('check',))
self.probe(pod,self.c['new_dir'],before)
self.existing_cm()
self.migrate(pod,'apply')
patch=m.patch_for(self.deployment(),self.c)
self.k('-n','forgejo','patch','deployment','forge','--type=json','-p',json.dumps(patch),'-o','name')
self.event('deployment-patched')
pod=self.ready_pod(self.deployment())
after=self.migrate(pod,'apply')
m.protected_equal(before,after)
self.shell(pod,self.c['new_guard_command'][2],('check',))
mounted=self.shell(pod,'cat /opt/forge-secret-template/hooks/pre-receive.d/10-custodian-secret-scan')
ensure(sha(mounted)==self.c['new_hook_sha256'],'mounted-template-drift')
self.probe(pod,self.c['new_dir'],after)
self.event('activation-verified',repositories=len(after),hook_sha256=self.c['new_hook_sha256'],baseline_hook_metadata_preserved=True)
except Exception as exc:
self.event('activation-failed',reason=str(exc) if isinstance(exc,RuntimeError) else type(exc).__name__)
try:self.rollback(before)
except Exception as recovery:self.event('rollback-unverified',reason=str(recovery) if isinstance(recovery,RuntimeError) else type(recovery).__name__)
raise
return Rollout
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--approved-digest',required=True)
p.add_argument('--binary',type=Path,default=Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate'));args=p.parse_args()
digest=verify_packet();ensure(args.approved_digest==digest,'exact-founder-acknowledgement-required')
m=support();c=json.loads((ORIGINAL/'inputs.json').read_bytes());runner=make_runner(m)(c)
runner.receipt.update(revision=4,packet_sha256=digest,reused_r2_packet_sha256=R2,mode='apply')
with OUTPUT.open('x') as stream:stream.write(json.dumps(runner.receipt)+'\n')
runner.apply(args.binary)
if __name__=='__main__':
try:main()
except Exception as exc:
print(json.dumps({'result':'refused-or-unverified','reason':str(exc) if isinstance(exc,RuntimeError) else type(exc).__name__}))
raise SystemExit(1)