This repository has been archived by the owner on Oct 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 284
+ Docker image integrity verifier #4392
Merged
Merged
Changes from 4 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
b70a9ec
+ Docker image integrity verifier
shadeofblue e0e76e0
+ README.md for the dockerhub image verification script
shadeofblue 9f3d56d
lint...
shadeofblue 894fae5
fix readme
shadeofblue 8556f7a
resolution of the dispute with @Krigpl
shadeofblue e8d3b5c
fix readme once again
shadeofblue 9ffc01d
+ add `requests` to the specifically-required files in `requirements-…
shadeofblue 8518a98
@Krigpl ... that better? ;p
shadeofblue File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# repository tag hash | ||
# 0.19 | ||
golemfactory/base 1.2 5727c396b963192c842f7d31935f165b63e6d78bda68105a6479083248bd79bf | ||
golemfactory/blender 1.4 c50af45ebd96a712553d01d214602b132e66f434f95e597ea741ef57011cebcd | ||
golemfactory/blender_nvgpu 1.1 69f736764a908237b2f121c31e464365049601cd2a32949fc59efaa1f481cc47 | ||
golemfactory/image_metrics 1.8.1 9995485a80b80a0e2f541034c780d0104f6bec6169abecbd4e4a718f849d8f68 | ||
golemfactory/nvgpu 1.1 ec9f9441927ddadf0fa5d954cc8812f955b706c84bf79a11e8be4d82eae06958 | ||
|
||
# 0.20 | ||
golemfactory/base 1.5 93c72af33f5eefaf325f594f0f46237cb07c25bbc3a1283ae91eb70761dcd035 | ||
golemfactory/blender 1.10 9d857c19e136e084edae95ba6982bb168f411e414ade50215d639f0c907df398 | ||
golemfactory/blender_nvgpu 1.4 ff84d6f5a84557eb6f2535b5bdb2caa3d4e720c96f960f934274869d7cc3aa63 | ||
golemfactory/blender_verifier 1.5 705f94c0e6944d792ac4c47330c443a0905e03c98a183ab6e8775b3717508628 | ||
golemfactory/dummy 1.2 60ba63d94c08ceebe67d8af6325fe37928d5178f0f7d340a195df5cf8d042d4b | ||
golemfactory/glambda 1.4 2417d0fcde4a90d69b78a5552920beac8cca8d68283eace7c68ea231ea623b7b | ||
golemfactory/nvgpu 1.4 7344c68586f06e61a1adae738d95d7dcd37306c6936c21ea06437326ba32b5f0 | ||
golemfactory/wasm 0.3.0 fea1d5c524044bd889ebea906db49a4345cce78b2c7ab2f8c4ef4e71ffbebbb4 | ||
|
||
# develop | ||
golemfactory/base 1.5 93c72af33f5eefaf325f594f0f46237cb07c25bbc3a1283ae91eb70761dcd035 | ||
golemfactory/blender 1.10 9d857c19e136e084edae95ba6982bb168f411e414ade50215d639f0c907df398 | ||
golemfactory/blender_nvgpu 1.4 ff84d6f5a84557eb6f2535b5bdb2caa3d4e720c96f960f934274869d7cc3aa63 | ||
golemfactory/blender_verifier 1.5 705f94c0e6944d792ac4c47330c443a0905e03c98a183ab6e8775b3717508628 | ||
golemfactory/dummy 1.2 60ba63d94c08ceebe67d8af6325fe37928d5178f0f7d340a195df5cf8d042d4b | ||
golemfactory/glambda 1.4 2417d0fcde4a90d69b78a5552920beac8cca8d68283eace7c68ea231ea623b7b | ||
golemfactory/nvgpu 1.4 7344c68586f06e61a1adae738d95d7dcd37306c6936c21ea06437326ba32b5f0 | ||
golemfactory/wasm 0.3.0 fea1d5c524044bd889ebea906db49a4345cce78b2c7ab2f8c4ef4e71ffbebbb4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
This scripts verifies the integrity of Golem's Docker hub images. | ||
|
||
In order to do that, a registry of docker images required by Golem is defined in | ||
`apps/image_integrity.ini` that has a format of: | ||
|
||
``` | ||
golemfactory/image_name 1.0 sha256-hash-of-the-image | ||
``` | ||
|
||
The registry holds entries that are valid for the latest stable releases | ||
plus those used currently in develop. All entries must be consistent (the | ||
script verifies if there are no duplicates with differing sha256 hashes and | ||
will raise a `ConfigurationError` if it encounters a conflict). | ||
|
||
To run verification, just launch the script: | ||
|
||
`./scripts/docker_integrity/verify.py` | ||
|
||
The script will run through all images listed in the registry and will produce | ||
a consistent report. | ||
|
||
If all images are found intact, it will exit normally, with an exit code of `0`. | ||
|
||
Should it encounter hash mismatches, it will produce a failure report and an | ||
exit code of `1`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,186 @@ | ||
#!/usr/bin/env python | ||
import json | ||
import pathlib | ||
import re | ||
import requests | ||
from requests.status_codes import codes as http_codes | ||
import sys | ||
import typing | ||
|
||
DOCKERHUB_URI = 'https://registry.hub.docker.com/v2/' | ||
REPOSITORY_ROOT = 'golemfactory' | ||
IMAGES_FILE = pathlib.Path(__file__).parents[2] / 'apps/image_integrity.ini' | ||
|
||
|
||
class COLORS(object): | ||
RESET = '\033[0m' | ||
RED = '\033[1;31m' | ||
GREEN = '\033[1;32m' | ||
|
||
|
||
class AuthenticationError(Exception): | ||
pass | ||
|
||
|
||
class ConfigurationError(Exception): | ||
pass | ||
|
||
|
||
class CommunicationError(Exception): | ||
pass | ||
|
||
|
||
def get_images() -> dict: | ||
images: dict = {} | ||
shadeofblue marked this conversation as resolved.
Show resolved
Hide resolved
|
||
with open(IMAGES_FILE) as f: | ||
for l in f: | ||
m = re.match( | ||
r"(?P<repo>[\w._/]+)\s+(?P<tag>[\w.]+)\s+(?P<hash>\w+)?$", l) | ||
|
||
if not m: | ||
continue | ||
|
||
m_repo = m.group('repo') | ||
m_tag = m.group('tag') | ||
|
||
repo = images.setdefault(m_repo, {}) | ||
|
||
if m_tag in repo and m.group('hash') != repo.get(m_tag): | ||
raise ConfigurationError( | ||
f"{m_repo}:{m_tag} has a conflicting hash: " | ||
f"'{m.group('hash')}' vs '{repo.get(m_tag)}' " | ||
f"defined in '{IMAGES_FILE}'." | ||
) | ||
else: | ||
repo[m.group('tag')] = m.group('hash') | ||
|
||
return images | ||
|
||
|
||
def authenticate(repository: str): | ||
r = requests.get(DOCKERHUB_URI) | ||
if not r.status_code == http_codes.UNAUTHORIZED: | ||
raise AuthenticationError( | ||
f"Unexpected status code: {r.status_code} " | ||
f"while retrieving: {DOCKERHUB_URI}" | ||
) | ||
auth_properties = { | ||
g[0]: g[1] | ||
for g in re.findall( | ||
r"(\w+)=\"(.+?)\"", r.headers.get('Www-Authenticate', '') | ||
) | ||
} | ||
realm = auth_properties.get('realm') | ||
if not realm: | ||
raise AuthenticationError( | ||
f"Could not find expected auth header in: {r.headers}" | ||
) | ||
auth_r = requests.get( # type:ignore | ||
realm, | ||
params={ | ||
'service': auth_properties.get('service'), | ||
'scope': f'repository:{repository}:pull', | ||
} | ||
) | ||
if not auth_r.status_code == http_codes.OK: | ||
raise AuthenticationError( | ||
f"Could not access: {realm}" | ||
) | ||
try: | ||
token = auth_r.json().get('token') | ||
return { | ||
'Authorization': f'Bearer {token}', | ||
'Accept': 'application/vnd.docker.distribution.manifest.v2+json' | ||
} | ||
except json.decoder.JSONDecodeError: | ||
raise AuthenticationError( | ||
f"Auth token not found in {auth_r.text}, retrieved from {realm}." | ||
) | ||
|
||
|
||
def get_manifest(token: dict, repository: str, tag: str): | ||
r = requests.get( | ||
DOCKERHUB_URI + f'{repository}/manifests/{tag}', | ||
headers=token | ||
) | ||
try: | ||
manifest = r.json() | ||
if not isinstance(manifest, dict): | ||
raise CommunicationError( | ||
f"Expected a dictionary, got {type(manifest)}: {manifest} " | ||
f"for {repository}:{tag}" | ||
) | ||
except json.JSONDecodeError as e: | ||
raise CommunicationError( | ||
f"Failed to retrieve the correct manifest for {repository}:{tag}, " | ||
f"got {r.status_code} - {r.text}" | ||
) from e | ||
|
||
return manifest | ||
|
||
|
||
def get_info(repository: str, tag: str): | ||
r = requests.get(DOCKERHUB_URI + f'repositories/{repository}/tags/{tag}/') | ||
try: | ||
info = r.json() | ||
if not isinstance(info, dict): | ||
raise CommunicationError( | ||
f"Expected a dictionary, got {type(info)}: {info} " | ||
f"for {repository}:{tag}" | ||
) | ||
except json.JSONDecodeError as e: | ||
raise CommunicationError( | ||
f"Failed to retrieve image info for {repository}:{tag}, " | ||
f"got {r.status_code} - {r.text}" | ||
) from e | ||
|
||
return info | ||
|
||
|
||
def verify_images() -> typing.Tuple[int, int]: | ||
cnt_images = 0 | ||
cnt_failures = 0 | ||
for repository, tags in get_images().items(): | ||
token = authenticate(repository) | ||
for tag, img_hash in tags.items(): | ||
cnt_images += 1 | ||
manifest = get_manifest(token, repository, tag) | ||
manifest_hash = manifest.get('config', {}).get('digest', '')[7:] | ||
if img_hash != manifest_hash: | ||
last_updated = get_info(repository, tag).get('last_updated') | ||
print( | ||
f'{repository}:{tag}: ' | ||
f'{COLORS.RED}hash differs ' | ||
f'(expected:{img_hash}, received:{manifest_hash}).' | ||
f'{COLORS.RESET}' | ||
f' Last updated: {last_updated}' | ||
) | ||
cnt_failures += 1 | ||
else: | ||
print( | ||
f'{repository}:{tag}: {COLORS.GREEN}\u2713{COLORS.RESET}' | ||
) | ||
|
||
return cnt_images, cnt_failures | ||
|
||
|
||
def run_verification(): | ||
|
||
cnt_images, cnt_failures = verify_images() | ||
|
||
if cnt_failures: | ||
print( | ||
f'{COLORS.RED}{cnt_failures} out of {cnt_images} images ' | ||
f'had modified hashes!{COLORS.RESET}' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ugh... it's more complicated than that ;p There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Krigpl anyway, updated |
||
) | ||
sys.exit(1) | ||
|
||
print( | ||
f'{COLORS.GREEN}All {cnt_images} images successfully verified :)' | ||
f'{COLORS.RESET}' | ||
) | ||
sys.exit(0) | ||
|
||
|
||
print("Verifying Golem Docker image integrity...") | ||
run_verification() |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file may easily get desynchronized with the real state of the world (i.e. publishing a new image without adding a new tag to this file). Also these sections seem unnecessary and repetitive.
What I would suggest is to put the hashes into the already existing
images.ini
file.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Krigpl
images.ini
defines only the images needed for the current branch, here we have a chance to verify severalI thought about it -> we could add a check later to verify that no references are present in
images.ini
that are not reflected inimage_integrity.ini
but that seems secondary to the most basic requirementThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Krigpl addressed ...