103 lines
5.7 KiB
Python
103 lines
5.7 KiB
Python
"""Hash selected public image files without pulling, installing or running it."""
|
|
from pathlib import Path, PurePosixPath
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import re
|
|
import tarfile
|
|
import time
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
BASE = Path(__file__).resolve().parent
|
|
HOST = 'https://forgejo.coulomb.social'
|
|
IMAGE = '/v2/coulomb/state-hub/'
|
|
DIGEST = 'sha256:c802690ee1811b3d77df4077936f9cdced1385514e8673fcbc9232b84d645015'
|
|
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 response:
|
|
raw = response.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 0 < len(token) < 60000
|
|
raw = get(IMAGE + 'manifests/' + DIGEST, 1048576, token,
|
|
'application/vnd.docker.distribution.manifest.v2+json')
|
|
assert 'sha256:' + hashlib.sha256(raw).hexdigest() == DIGEST
|
|
manifest = json.loads(raw)
|
|
assert manifest['schemaVersion'] == 2 and len(manifest['layers']) <= 80
|
|
assert sum(x['size'] for x in manifest['layers']) <= 500 * 1024**2
|
|
files, packages, layers = {}, {}, []
|
|
for layer in manifest['layers']:
|
|
pin = layer['digest']
|
|
assert re.fullmatch('sha256:[0-9a-f]{64}', pin) and 0 <= layer['size'] <= 160 * 1024**2
|
|
blob = get(IMAGE + 'blobs/' + pin, layer['size'], token)
|
|
assert len(blob) == layer['size'] and 'sha256:' + hashlib.sha256(blob).hexdigest() == pin
|
|
with tarfile.open(fileobj=io.BytesIO(blob), mode='r|*') as archive:
|
|
count = 0
|
|
for entry in archive:
|
|
count += 1
|
|
assert count <= 100000 and time.monotonic() < DEADLINE
|
|
path = entry.name.removeprefix('./')
|
|
assert not path.startswith('/') and '..' not in PurePosixPath(path).parts
|
|
parent, _, basename = path.rpartition('/')
|
|
# Apply OCI deletions to the selected metadata view.
|
|
if basename.startswith('.wh.'):
|
|
erased = parent + '/' + basename[4:]
|
|
for key in list(files):
|
|
if key == erased or key.startswith(erased + '/'):
|
|
del files[key]
|
|
for key in list(packages):
|
|
if key == erased or key.startswith(erased + '/'):
|
|
del packages[key]
|
|
continue
|
|
code = (path == 'app/api/services/historical_projection.py'
|
|
or path.startswith('app/migrations/versions/') and path.endswith('.py')
|
|
or '/site-packages/hub_core/' in path and path.endswith('.py')
|
|
or path.startswith('tmp/hub-core/hub_core/') and path.endswith('.py')
|
|
or path == 'tmp/.ci-build-id')
|
|
metadata = '/site-packages/' in path and path.endswith('.dist-info/METADATA')
|
|
if not code and not metadata:
|
|
continue
|
|
assert entry.isfile() and entry.size <= 2 * 1024**2
|
|
stream = archive.extractfile(entry)
|
|
content = stream.read(2 * 1024**2 + 1)
|
|
assert len(content) == entry.size
|
|
if code:
|
|
files[path] = 'sha256:' + hashlib.sha256(content).hexdigest()
|
|
if metadata:
|
|
text = content.decode('utf-8')
|
|
name = re.findall(r'^Name: ([A-Za-z0-9_.-]+)$', text, re.M)
|
|
version = re.findall(r'^Version: ([A-Za-z0-9_.+!-]+)$', text, re.M)
|
|
assert len(name) == len(version) == 1
|
|
packages[path] = {'name': name[0], 'version': version[0],
|
|
'metadata_sha256': 'sha256:' + hashlib.sha256(content).hexdigest()}
|
|
layers.append({'digest': pin, 'compressed_bytes': len(blob), 'archive_entries': count})
|
|
del blob
|
|
expected = 'sha256:' + hashlib.sha256(Path('/home/worsch/state-hub/api/services/historical_projection.py').read_bytes()).hexdigest()
|
|
assert files.get('app/api/services/historical_projection.py') == expected
|
|
assert files.get('tmp/.ci-build-id') == 'sha256:' + hashlib.sha256(b'51bc70d\n').hexdigest()
|
|
migration = 'app/migrations/versions/c4d5e6f7a8b9_inbox_feed_revision.py'
|
|
assert files.get(migration) == 'sha256:48f1730037be99b5623e878d7f20a06bec243ce27317fde81c82e16006d7b3ce'
|
|
del auth, token
|
|
result = {'schema': 'statehub.public-image-inspection.v1', 'status': 'verified_selected_image_files',
|
|
'source_revision': '51bc70d1ff8f25450a8650d8e50caf053093a02e', 'image_digest': DIGEST,
|
|
'anonymous_protocol_only': True, 'token_retained': False, 'image_executed': False,
|
|
'image_installed_or_daemon_pulled': False, 'layers': layers, 'files': files,
|
|
'python_package_metadata': packages,
|
|
'limitations': ['Selected code and package metadata inventory; no full application acceptance or hermetic build provenance.',
|
|
'Floating dependency origins remain unknown; observed bytes and versions are bound to this immutable image digest.']}
|
|
(BASE / 'statehub-public-image-inspection.json').write_text(json.dumps(result, indent=2) + '\n')
|
|
print(json.dumps({'status': result['status'], 'layers': len(layers), 'selected_files': len(files), 'packages': len(packages)}))
|