the-custodian/docs/changes/netkingdom-target-2026-10-17/inspect_guard_image.py

59 lines
5.4 KiB
Python

"""Resolve and inspect public guard image bytes; no pull, install or execution."""
from pathlib import Path,PurePosixPath
import argparse,hashlib,io,json,re,tarfile,time,urllib.request,urllib.parse
p=argparse.ArgumentParser();p.add_argument('--commit',required=True);a=p.parse_args();assert re.fullmatch('[0-9a-f]{40}',a.commit)
here=Path(__file__).resolve().parent;owner=Path('/home/worsch/state-hub');short=a.commit[:7]
host='https://forgejo.coulomb.social';base='/v2/coulomb/state-hub/';deadline=time.monotonic()+180
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self,*args,**kwargs):raise RuntimeError('redirect-refused')
opener=urllib.request.build_opener(urllib.request.ProxyHandler({}),NoRedirect)
def get(path,limit,token=None,accept=None):
assert time.monotonic()<deadline
headers={}
if token:headers['Authorization']='Bearer '+token
if accept:headers['Accept']=accept
with opener.open(urllib.request.Request(host+path,headers=headers),timeout=30) as r:raw=r.read(limit+1)
assert len(raw)<=limit
return raw
query=urllib.parse.urlencode({'service':'container_registry','scope':'repository:coulomb/state-hub:pull'})
auth=json.loads(get('/v2/token?'+query,65536));token=auth.get('token',auth.get('access_token'));assert isinstance(token,str) and len(token)<60000
raw=get(base+'manifests/main-'+short,1048576,token,'application/vnd.docker.distribution.manifest.v2+json');digest='sha256:'+hashlib.sha256(raw).hexdigest()
assert get(base+'manifests/'+digest,1048576,token,'application/vnd.docker.distribution.manifest.v2+json')==raw
m=json.loads(raw);assert m['schemaVersion']==2 and len(m['layers'])<=80 and sum(v['size'] for v in m['layers'])<=500*1024**2
selected=['api/services/historical_projection.py','api/routers/workstreams.py','scripts/consistency_check.py','api/services/forge_projection.py','migrations/versions/c4d5e6f7a8b9_inbox_feed_revision.py']
expected={'app/'+rel:'sha256:'+hashlib.sha256((owner/rel).read_bytes()).hexdigest() for rel in selected}
expected['tmp/.ci-build-id']='sha256:'+hashlib.sha256((short+'\n').encode()).hexdigest()
files={};packages={};layers=[]
for layer in m['layers']:
pin=layer['digest'];assert re.fullmatch('sha256:[0-9a-f]{64}',pin) and 0<=layer['size']<=160*1024**2
blob=get(base+'blobs/'+pin,layer['size'],token);assert len(blob)==layer['size'] and 'sha256:'+hashlib.sha256(blob).hexdigest()==pin
layer_files={};layer_packages={};whiteouts=[]
with tarfile.open(fileobj=io.BytesIO(blob),mode='r|*') as tar:
count=0
for entry in tar:
count+=1;assert count<=100000 and time.monotonic()<deadline
path=entry.name.removeprefix('./');assert not path.startswith('/') and '..' not in PurePosixPath(path).parts
parent,_,name=path.rpartition('/')
if name.startswith('.wh.'):
whiteouts.append((parent,name));continue
code=path in expected or '/site-packages/hub_core/' in path and path.endswith('.py') or path.startswith('tmp/hub-core/hub_core/') and path.endswith('.py')
meta='/site-packages/' in path and path.endswith('.dist-info/METADATA')
if not code and not meta:continue
assert entry.isfile() and entry.size<=2*1024**2;data=tar.extractfile(entry).read(2*1024**2+1);assert len(data)==entry.size
if code:layer_files[path]='sha256:'+hashlib.sha256(data).hexdigest()
if meta:
text=data.decode();names=re.findall(r'^Name: ([A-Za-z0-9_.-]+)$',text,re.M);versions=re.findall(r'^Version: ([A-Za-z0-9_.+!-]+)$',text,re.M);assert len(names)==len(versions)==1
layer_packages[path]={'name':names[0],'version':versions[0],'metadata_sha256':'sha256:'+hashlib.sha256(data).hexdigest()}
for parent,name in whiteouts:
erased=str(PurePosixPath(parent)/name[4:])
for view in [files,packages]:
for key in list(view):
opaque=name=='.wh..wh..opq' and (not parent or key.startswith(parent+'/'))
if opaque or key==erased or key.startswith(erased+'/'):del view[key]
files.update(layer_files);packages.update(layer_packages)
layers.append({'digest':pin,'compressed_bytes':len(blob),'archive_entries':count,'whiteouts':len(whiteouts)});del blob
assert all(files.get(k)==v for k,v in expected.items()),'selected-source-or-build-id-mismatch'
old=json.loads((here/'statehub-public-image-inspection.json').read_text());old_packages=old['python_package_metadata'];old_hub={k:v for k,v in old['files'].items() if '/hub_core/' in k};new_hub={k:v for k,v in files.items() if '/hub_core/' in k}
r={'schema':'statehub.guard-image-inspection.v1','status':'verified-selected-public-image-bytes','source_commit':a.commit,'tag':'main-'+short,'image_digest':digest,'selected_source_hashes':expected,'observed_files':files,'layers':layers,'python_package_metadata':packages,'package_metadata_equal_to_deployed_image':packages==old_packages,'hub_core_source_hashes_equal_to_deployed_image':new_hub==old_hub,'image_executed':False,'image_installed_or_daemon_pulled':False,'anonymous_protocol_only':True,'token_retained':False,'limitations':['Selected code hashes and package inventory do not prove full application acceptance or hermetic provenance.','Floating dependency origins remain unknown; observed byte changes require explicit review.','New API behavior is not deployed or natively accepted.']}
del token,auth
(here/('guard-image-'+short+'-inspection.json')).write_text(json.dumps(r,indent=2)+'\n');print(json.dumps({k:r[k] for k in ['status','source_commit','image_digest','package_metadata_equal_to_deployed_image','hub_core_source_hashes_equal_to_deployed_image']}))