145 lines
7 KiB
Python
145 lines
7 KiB
Python
#!/usr/bin/env python3
|
|
"""Exact-binary disposable probe in the admitted live Forgejo container.
|
|
|
|
No production repository, Git hook, credential, account, config or policy writes.
|
|
Default is read-only preflight; --apply creates only the exact new /tmp directory
|
|
and removes it after the probe, guarded by its unique owner marker.
|
|
"""
|
|
import argparse
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import re
|
|
import shlex
|
|
import subprocess
|
|
import tarfile
|
|
from pathlib import Path
|
|
|
|
|
|
def sha(path):
|
|
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
|
|
|
|
|
def command(args, payload=None, timeout=45):
|
|
result = subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10',
|
|
'railiance01', shlex.join(['kubectl', '--request-timeout=15s', *args])],
|
|
input=payload, capture_output=True, timeout=timeout)
|
|
if result.returncode:
|
|
raise ValueError('native-command-refused')
|
|
return result.stdout
|
|
|
|
|
|
def execute(proposal, *args, payload=None, timeout=45):
|
|
return command(['-n', 'forgejo', 'exec', '-i', proposal['pod'], '-c', 'gitea', '--', *args],
|
|
payload, timeout)
|
|
|
|
|
|
def preflight(proposal):
|
|
if not re.fullmatch(r'custodian-receive-native-[a-z0-9-]+', proposal['directory_name']):
|
|
raise ValueError('unsafe-probe-directory')
|
|
if proposal['executor_sha256'] != sha(__file__):
|
|
raise ValueError('executor-revision-changed')
|
|
nodes = json.loads(command(['get', 'nodes', '-o', 'json']))['items']
|
|
if len(nodes) != 1 or not any(a['address'] == '92.205.62.239' for a in nodes[0]['status']['addresses']):
|
|
raise ValueError('target-host-changed')
|
|
pod = json.loads(command(['-n', 'forgejo', 'get', 'pod', proposal['pod'], '-o', 'json']))
|
|
if pod['metadata']['uid'] != proposal['pod_uid']:
|
|
raise ValueError('target-pod-changed')
|
|
if not any(c['type'] == 'Ready' and c['status'] == 'True' for c in pod['status']['conditions']):
|
|
raise ValueError('target-pod-unready')
|
|
image = next(c['image'] for c in pod['spec']['containers'] if c['name'] == 'gitea')
|
|
if image != proposal['image']:
|
|
raise ValueError('target-image-changed')
|
|
image_id = next(c['imageID'] for c in pod['status']['containerStatuses'] if c['name'] == 'gitea')
|
|
if image_id != proposal['image_id']:
|
|
raise ValueError('target-image-digest-changed')
|
|
for item in proposal['artifacts']:
|
|
path = Path(item['source'])
|
|
if path.is_symlink() or not path.is_file() or sha(path) != item['sha256']:
|
|
raise ValueError('artifact-changed')
|
|
if not re.fullmatch(r'[a-zA-Z0-9._-]+', item['name']):
|
|
raise ValueError('artifact-name-refused')
|
|
df = execute(proposal, 'df', '-Pk', '/tmp').decode().splitlines()[-1].split()
|
|
if int(df[3]) < 256 * 1024:
|
|
raise ValueError('temporary-space-insufficient')
|
|
execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz')
|
|
dispatcher = execute(proposal, 'sha256sum', proposal['existing_dispatcher']).decode().split()[0]
|
|
if dispatcher != proposal['dispatcher_sha256']:
|
|
raise ValueError('existing-dispatcher-changed')
|
|
return {'pod_uid': pod['metadata']['uid'], 'image_id': image_id,
|
|
'git_version': execute(proposal, '/usr/bin/git', '--version').decode().strip(),
|
|
'free_tmp_kib': int(df[3]), 'health_before': True}
|
|
|
|
|
|
def archive(proposal):
|
|
data = io.BytesIO()
|
|
checksums = []
|
|
with tarfile.open(fileobj=data, mode='w') as output:
|
|
for item in proposal['artifacts']:
|
|
content = Path(item['source']).read_bytes()
|
|
info = tarfile.TarInfo(item['name']); info.size = len(content); info.mode = item['mode']
|
|
info.uid = info.gid = 1000
|
|
output.addfile(info, io.BytesIO(content))
|
|
checksums.append(item['sha256'] + ' ' + item['name'] + '\n')
|
|
content = ''.join(checksums).encode()
|
|
info = tarfile.TarInfo('SHA256SUMS'); info.size = len(content); info.mode = 0o600
|
|
info.uid = info.gid = 1000
|
|
output.addfile(info, io.BytesIO(content))
|
|
return data.getvalue()
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--proposal', type=Path, required=True)
|
|
parser.add_argument('--receipt', type=Path, required=True)
|
|
parser.add_argument('--apply', action='store_true')
|
|
args = parser.parse_args()
|
|
proposal = json.loads(args.proposal.read_text())
|
|
observation = preflight(proposal)
|
|
if not args.apply:
|
|
print(json.dumps({'status': 'preflight-verified', **observation}))
|
|
return
|
|
directory = '/tmp/' + proposal['directory_name']
|
|
marker = proposal['proposal_id']
|
|
if not re.fullmatch(r'[A-Za-z0-9-]+', marker):
|
|
raise ValueError('unsafe-marker')
|
|
receipt = {'proposal_id': marker, 'proposal_sha256': sha(args.proposal),
|
|
'executed_revision': proposal['revision'], 'result': 'unverified', 'observation': observation,
|
|
'scope': 'exact binaries in disposable bare repositories inside existing Forgejo image; builtin auth stubbed',
|
|
'production_repositories_changed': False, 'live_credentials_used': False}
|
|
created = False
|
|
try:
|
|
execute(proposal, 'sh', '-c', 'umask 077; test ! -e "$1" && mkdir "$1" && printf "%s" "$2" > "$1/.probe-owner"',
|
|
'sh', directory, marker)
|
|
created = True
|
|
execute(proposal, 'tar', '-xmf', '-', '-C', directory, payload=archive(proposal), timeout=90)
|
|
raw = execute(proposal, 'timeout', '-k', '5', '150', 'sh', directory + '/probe.sh', directory, timeout=180)
|
|
result = json.loads(raw)
|
|
keys = ['clean_push', 'intermediate_secret_refused', 'quarantine_discarded',
|
|
'scanner_unavailable_refused', 'clean_after_recovery', 'dispatcher_preserved']
|
|
if not all(result.get(k) is True for k in keys) or result.get('other_hook_invocations') != 4:
|
|
raise ValueError('incomplete-native-proof')
|
|
receipt['verification'] = result
|
|
execute(proposal, 'wget', '-qO', '/dev/null', 'http://127.0.0.1:3000/api/healthz')
|
|
receipt['health_after'] = True
|
|
receipt['result'] = 'verified'
|
|
finally:
|
|
if created:
|
|
try:
|
|
execute(proposal, 'sh', '-c', 'test "$(cat "$1/.probe-owner")" = "$2" && rm -rf -- "$1" && test ! -e "$1"',
|
|
'sh', directory, marker)
|
|
receipt['temporary_directory_removed'] = True
|
|
except (ValueError, OSError, subprocess.SubprocessError):
|
|
receipt['temporary_directory_removed'] = False
|
|
receipt['result'] = 'unverified'
|
|
args.receipt.write_text(json.dumps(receipt, indent=2) + '\n')
|
|
if receipt['result'] != 'verified':
|
|
raise ValueError('verification-or-cleanup-incomplete')
|
|
print('Exact-binary native probe verified; temporary area removed; production hooks unchanged.')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except (ValueError, OSError, KeyError, StopIteration, subprocess.SubprocessError):
|
|
raise SystemExit('Native probe refused or incomplete; inspect value-free receipt. Raw diagnostics suppressed.')
|