From 9248bd161d2216f9944ce62486bc4af1d290b20b Mon Sep 17 00:00:00 2001 From: chgl Date: Mon, 22 Aug 2022 19:27:00 +0200 Subject: [PATCH 1/2] feat: support tags and digest simultaneously With this change, Connaisseur now supports the use of tags and digests simultaneously. The signature is still validated based on the digest, but the human readable aspect of the tag isn't lost. --- connaisseur/flask_application.py | 3 +- connaisseur/image.py | 106 ++++++++++-------- .../validators/cosign/cosign_validator.py | 5 +- connaisseur/validators/notaryv1/key_store.py | 3 +- .../validators/notaryv1/notaryv1_validator.py | 64 ++++++----- connaisseur/validators/notaryv1/trust_data.py | 3 +- tests/integration/cases.yaml | 11 ++ tests/test_flask_application.py | 12 +- tests/test_image.py | 90 ++++++++------- .../notaryv1/test_notaryv1_validator.py | 35 +++--- 10 files changed, 181 insertions(+), 151 deletions(-) diff --git a/connaisseur/flask_application.py b/connaisseur/flask_application.py index c12886707..2b2d56d03 100644 --- a/connaisseur/flask_application.py +++ b/connaisseur/flask_application.py @@ -6,6 +6,7 @@ from flask import Flask, jsonify, request from prometheus_flask_exporter import PrometheusMetrics, NO_PREFIX +import connaisseur.constants as const from connaisseur.admission_request import AdmissionRequest from connaisseur.alert import send_alerts from connaisseur.config import Config @@ -190,5 +191,5 @@ async def __validate_image(type_index, image, admission_request): msg = f'successful verification of image "{original_image}"' logging.info(__create_logging_msg(msg, **logging_context)) if trusted_digest: - image.set_digest(trusted_digest) + image.digest, image.digest_algo = trusted_digest, const.SHA256 return admission_request.wl_object.get_json_patch(image, type_, index) diff --git a/connaisseur/image.py b/connaisseur/image.py index 67732faed..11fa36818 100644 --- a/connaisseur/image.py +++ b/connaisseur/image.py @@ -1,6 +1,7 @@ import re from typing import Optional +import connaisseur.constants as const from connaisseur.exceptions import InvalidImageFormatError @@ -26,62 +27,71 @@ class Image: name: str tag: Optional[str] digest: Optional[str] + digest_algo: Optional[str] - def __init__(self, image: str): - separator = r"[-._:@+]|--" - alphanum = r"[A-Za-z0-9]+" - component = f"{alphanum}(?:(?:{separator}){alphanum})*" - ref = f"^{component}(?:/{component})*$" + def __init__(self, image: str): # pylint: disable=too-many-locals - # e.g. :v1, :3.7-alpine, @sha256:3e7a89... - tag_re = r"(?:(?:@sha256:([a-f0-9]{64}))|(?:\:([\w.-]+)))" - - match = re.search(ref, image) - if not match: - msg = "{image} is not a valid image reference." - raise InvalidImageFormatError(message=msg, image=image) - - name_tag = image.split("/")[-1] - search = re.search(tag_re, name_tag) - self.digest, self.tag = search.groups() if search else (None, "latest") - self.name = name_tag.removesuffix(":" + str(self.tag)).removesuffix( - "@sha256:" + str(self.digest) + # implements https://github.com/distribution/distribution/blob/main/reference/regexp.go + digest_hex = r"[0-9a-fA-F]{32,}" + digest_algorithm_component = r"[A-Za-z][A-Za-z0-9]*" + digest_algorithm_separator = r"[+._-]" + digest_algorithm = ( + rf"{digest_algorithm_component}(?:{digest_algorithm_separator}" + rf"{digest_algorithm_component})*" ) - - first_comp = image.removesuffix(name_tag).split("/")[0] - self.registry = ( - first_comp - if re.search(r"[.:]", first_comp) - or first_comp == "localhost" - or any(ele.isupper() for ele in first_comp) - else "docker.io" - ) - self.repository = ( - image.removesuffix(name_tag).removeprefix(self.registry) - ).strip("/") or ("library" if self.registry == "docker.io" else "") - - if (self.repository + self.name).lower() != self.repository + self.name: + digest = rf"{digest_algorithm}:{digest_hex}" + tag = r"[\w][\w.-]{0,127}" + separator = r"[_.]|__|[-]*" + alpha_numeric = r"[a-z0-9]+" + path_component = rf"{alpha_numeric}(?:{separator}{alpha_numeric})*" + port = r"[0-9]+" + domain_component = r"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])" + domain_name = rf"{domain_component}(?:\.{domain_component})*" + ipv6 = r"\[(?:[a-fA-F0-9:]+)\]" + host = rf"(?:{domain_name}|{ipv6})" + domain = rf"{host}(?::{port})?" + name = rf"(?:{domain}/)?{path_component}(?:/{path_component})*" + reference = rf"^(?P{name})(?::(?P{tag}))?(?:@(?P{digest}))?$" + + match = re.search(reference, image) + if (not match) or (len(match.group("name")) > 255): msg = "{image} is not a valid image reference." raise InvalidImageFormatError(message=msg, image=image) - def set_digest(self, digest): - """ - Set the digest to the given `digest`. - """ - self.digest = digest - - def has_digest(self) -> bool: - """ - Return `True` if the image has a digest, `False` otherwise. - """ - return self.digest is not None + name, tag, digest = match.groups() + components = name.split("/") + self.name = components[-1] + self.digest_algo, self.digest = digest.split(":") if digest else (None, None) + self.tag = tag or ("latest" if not self.digest else None) + + if self.digest_algo and self.digest_algo != const.SHA256: + raise InvalidImageFormatError( + message="A digest algorithm of {digest_algo} is not supported. Use sha256 instead.", + digest_algo=self.digest_algo, + ) + + registry_repo = components[:-1] + try: + registry = registry_repo[0] + self.registry = ( + registry + if re.search(r"[.:]", registry) + or registry == "localhost" + or any(ele.isupper() for ele in registry) + else "docker.io" + ) + self.repository = "/".join(registry_repo).removeprefix( + f"{self.registry}" + ).removeprefix("/") or ("library" if self.registry == "docker.io" else None) + except IndexError: + self.registry = "docker.io" + self.repository = "library" def __str__(self): - repo_reg = "".join( - f"{item}/" for item in [self.registry, self.repository] if item - ) - tag = f":{self.tag}" if not self.digest else f"@sha256:{self.digest}" - return f"{repo_reg}{self.name}{tag}" + repo_reg = "/".join(item for item in [self.registry, self.repository] if item) + tag = f":{self.tag}" if self.tag else "" + digest = f"@{self.digest_algo}:{self.digest}" if self.digest else "" + return f"{repo_reg}/{self.name}{tag}{digest}" def __eq__(self, other): return str(self) == str(other) diff --git a/connaisseur/validators/cosign/cosign_validator.py b/connaisseur/validators/cosign/cosign_validator.py index 468e9d94c..e577ba995 100644 --- a/connaisseur/validators/cosign/cosign_validator.py +++ b/connaisseur/validators/cosign/cosign_validator.py @@ -7,6 +7,7 @@ from concurrent.futures import ThreadPoolExecutor +import connaisseur.constants as const from connaisseur.exceptions import ( CosignError, CosignTimeout, @@ -162,7 +163,7 @@ def __get_cosign_validated_digests(self, image: str, trust_root: dict): digest = sig_data["critical"]["image"].get( "docker-manifest-digest", "" ) - if re.match(r"sha256:[0-9A-Fa-f]{64}", digest) is None: + if re.match(rf"{const.SHA256}:[0-9A-Fa-f]{{64}}", digest) is None: msg = "Digest '{digest}' does not match expected digest pattern." raise InvalidFormatException(message=msg, digest=digest) except Exception as err: @@ -180,7 +181,7 @@ def __get_cosign_validated_digests(self, image: str, trust_root: dict): trust_root=trust_root["name"], ) from err # remove prefix 'sha256' - digests.append(digest.removeprefix("sha256:")) + digests.append(digest.removeprefix(f"{const.SHA256}:")) except json.JSONDecodeError: logging.info("non-json signature data from Cosign: %s", sig) pass diff --git a/connaisseur/validators/notaryv1/key_store.py b/connaisseur/validators/notaryv1/key_store.py index 4cb99e188..74c535bc7 100644 --- a/connaisseur/validators/notaryv1/key_store.py +++ b/connaisseur/validators/notaryv1/key_store.py @@ -1,5 +1,6 @@ from connaisseur.exceptions import NotFoundException from connaisseur.trust_root import TrustRoot +import connaisseur.constants as const class KeyStore: @@ -68,7 +69,7 @@ def update(self, trust_data): self.hashes.setdefault( role, ( - hashes[role].get("hashes", {}).get("sha256"), + hashes[role].get("hashes", {}).get(const.SHA256), hashes[role].get("length", 0), ), ) diff --git a/connaisseur/validators/notaryv1/notaryv1_validator.py b/connaisseur/validators/notaryv1/notaryv1_validator.py index 373422e95..04dbb3bc1 100644 --- a/connaisseur/validators/notaryv1/notaryv1_validator.py +++ b/connaisseur/validators/notaryv1/notaryv1_validator.py @@ -3,10 +3,12 @@ import datetime as dt import logging +import connaisseur.constants as const from connaisseur.exceptions import ( AmbiguousDigestError, InsufficientTrustDataError, NotFoundException, + ValidationError, ) from connaisseur.image import Image from connaisseur.trust_root import TrustRoot @@ -47,15 +49,12 @@ async def validate( image, req_delegations, root_key ) - # search for digests or tag, depending on given image - search_image_targets = ( - NotaryV1Validator.__search_image_targets_for_digest - if image.has_digest() - else NotaryV1Validator.__search_image_targets_for_tag - ) - # filter out the searched for digests, if present + # search for digests in given targets digests = list( - map(lambda x: search_image_targets(x, image), signed_image_targets) + map( + lambda x: NotaryV1Validator.__search_image_targets(x, image), + signed_image_targets, + ) ) # in case certain delegations are needed, `signed_image_targets` should only @@ -224,29 +223,36 @@ async def __process_chain_of_trust( return image_targets @staticmethod - def __search_image_targets_for_digest(trust_data: dict, image: Image): - """ - Search in the `trust_data` for a signed digest, given an `image` with - digest. - """ - image_digest = base64.b64encode(bytes.fromhex(image.digest)).decode("utf-8") - if image_digest in {data["hashes"]["sha256"] for data in trust_data.values()}: - return image.digest - + def __search_image_targets(trust_data: dict, image: Image): + if image.tag: + if image.tag not in trust_data: + return None + + base64_digest = trust_data[image.tag]["hashes"][const.SHA256] + digest = base64.b64decode(base64_digest).hex() + + # if both tag and digest are given + if image.digest: + # validate if the digest in the trust_data found by the tag, + # matches the digest requested by the image reference + if digest == image.digest: + return digest + else: + raise ValidationError( + message="Image tag and digest do not match.", + tag=image.tag, + tag_digest=digest, + digest=image.digest, + ) + # if only the tag is given + return digest + # if only the digest is given + elif image.digest: + digest = base64.b64encode(bytes.fromhex(image.digest)).decode("utf-8") + if digest in {data["hashes"][const.SHA256] for data in trust_data.values()}: + return image.digest return None - @staticmethod - def __search_image_targets_for_tag(trust_data: dict, image: Image): - """ - Search in the `trust_data` for a digest, given an `image` with tag. - """ - image_tag = image.tag - if image_tag not in trust_data: - return None - - base64_digest = trust_data[image_tag]["hashes"]["sha256"] - return base64.b64decode(base64_digest).hex() - async def __update_with_delegation_trust_data( self, trust_data, delegations, key_store, image ): diff --git a/connaisseur/validators/notaryv1/trust_data.py b/connaisseur/validators/notaryv1/trust_data.py index 0e08b07b3..d8fa0e3c7 100644 --- a/connaisseur/validators/notaryv1/trust_data.py +++ b/connaisseur/validators/notaryv1/trust_data.py @@ -7,6 +7,7 @@ import pytz from dateutil import parser +import connaisseur.constants as const from connaisseur.exceptions import ( InvalidTrustDataFormatError, NoSuchClassError, @@ -189,7 +190,7 @@ def get_tags(self): def get_digest(self, tag: str): try: - return self.signed.get("targets", {})[tag]["hashes"]["sha256"] + return self.signed.get("targets", {})[tag]["hashes"][const.SHA256] except KeyError as err: msg = "Unable to find digest for tag {tag}." raise NotFoundException(message=msg, tag=tag) from err diff --git a/tests/integration/cases.yaml b/tests/integration/cases.yaml index 177b04156..8d08393ad 100644 --- a/tests/integration/cases.yaml +++ b/tests/integration/cases.yaml @@ -56,6 +56,13 @@ test_cases: namespace: default expected_msg: Unable to find signed digest for image docker.io/securesystemsengineering/testimage:unsigned. expected_result: INVALID + - id: rstd + txt: Testing signed image with tag and digest... + type: deploy + ref: securesystemsengineering/testimage:signed@sha256:c5327b291d702719a26c6cf8cc93f72e7902df46547106a9930feda2c002a4a7 + namespace: default + expected_msg: pod/pod-rstd created + expected_result: VALID cosign: - id: cu txt: Testing unsigned cosign image... @@ -78,6 +85,10 @@ test_cases: namespace: default expected_msg: pod/pod-cs created expected_result: null + - id: cstd + txt: Testing signed cosign image with tag and digest... + type: deploy + ref: securesystemsengineering/testimage:co-signed@sha256:c5327b291d702719a26c6cf8cc93f72e7902df46547106a9930feda2c002a4a7 multi-cosigned: - id: mc-u txt: Testing multi-cosigned image `threshold` => undefined, not reached... diff --git a/tests/test_flask_application.py b/tests/test_flask_application.py index 7a699a0a4..0c258dc09 100644 --- a/tests/test_flask_application.py +++ b/tests/test_flask_application.py @@ -154,12 +154,12 @@ def test_create_logging_msg(msg, kwargs, out): "status": {"code": 202}, "patchType": "JSONPatch", "patch": ( - "W3sib3AiOiAicmVwbGFjZSIsICJwYXRoIjogI" - "i9zcGVjL3RlbXBsYXRlL3NwZWMvY29udGFpbmVycy8wL2lt" - "YWdlIiwgInZhbHVlIjogImRvY2tlci5pby9zZWN1cmVzeXN" - "0ZW1zZW5naW5lZXJpbmcvYWxpY2UtaW1hZ2VAc2hhMjU2Om" - "FjOTA0YzliMTkxZDE0ZmFmNTRiNzk1MmYyNjUwYTRiYjIxY" - "zIwMWJmMzQxMzEzODhiODUxZThjZTk5MmE2NTIifV0=" + "W3sib3AiOiAicmVwbGFjZSIsICJwYXRoIjogIi9zcGVjL3RlbXBs" + "YXRlL3NwZWMvY29udGFpbmVycy8wL2ltYWdlIiwgInZhbHVlIjog" + "ImRvY2tlci5pby9zZWN1cmVzeXN0ZW1zZW5naW5lZXJpbmcvYWxp" + "Y2UtaW1hZ2U6dGVzdEBzaGEyNTY6YWM5MDRjOWIxOTFkMTRmYWY1" + "NGI3OTUyZjI2NTBhNGJiMjFjMjAxYmYzNDEzMTM4OGI4NTFlOGNl" + "OTkyYTY1MiJ9XQ==" ), }, }, diff --git a/tests/test_image.py b/tests/test_image.py index 6fddbb576..c6c2a9ec3 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -42,7 +42,7 @@ "image", "tag", None, - "", + None, "registry.io", fix.no_exc(), ), @@ -94,16 +94,7 @@ "master-node:5000", fix.no_exc(), ), - ("Test/test:v1", "test", "v1", None, "", "Test", fix.no_exc()), - ( - "docker.io/Library/image:tag", - "image", - "tag", - None, - None, - "docker.io", - pytest.raises(exc.InvalidImageFormatError), - ), + ("Test/test:v1", "test", "v1", None, None, "Test", fix.no_exc()), ( "docker.io/library/image:Tag", "image", @@ -113,6 +104,38 @@ "docker.io", fix.no_exc(), ), + ( + "ghcr.io/repo/test/image-with-tag-and-digest:v1.2.3@sha256:f8816ada742348e1adfcec5c2a180b675bf6e4a294e0feb68bd70179451e1242", + "image-with-tag-and-digest", + "v1.2.3", + "f8816ada742348e1adfcec5c2a180b675bf6e4a294e0feb68bd70179451e1242", + "repo/test", + "ghcr.io", + fix.no_exc(), + ), + ( + "image@sha:859b5aada817b3eb53410222e8fc232cf126c9e598390ae61895eb96f52ae46d", + None, + None, + None, + None, + None, + pytest.raises(exc.InvalidImageFormatError, match=r".*is not supported.*"), + ), + ( + ( + "what/a/long/path/to/an/image/that/is/quite/unnecessarily/long/if/you/ask/me/this/shouldnt" + "/be/as/long/as/this/is/but/in/order/to/test/this/i/see/no/other/way/that/using/such/a" + "ridiculous/long/name/that/may/or/may/not/make/it/into/the/book/from/this/guiness/person/" + "the/one/with/the/beer/you/know/image:tag" + ), + None, + None, + None, + None, + None, + pytest.raises(exc.InvalidImageFormatError, match=r".*not a valid.*"), + ), ], ) def test_image( @@ -127,41 +150,6 @@ def test_image( assert i.registry == registry -@pytest.mark.parametrize( - "image, tag, digest", - [ - ( - "image:tag", - "tag", - "859b5aada817b3eb53410222e8fc232cf126c9e598390ae61895eb96f52ae46d", - ) - ], -) -def test_set_digest(image: str, tag: str, digest: str): - i = img.Image(image) - i.set_digest(digest) - assert i.digest == digest - assert i.tag == tag - - -@pytest.mark.parametrize( - "image, digest", - [ - ("image:tag", False), - ( - ( - "image@sha256:859b5aada817b3eb53410222e8f" - "c232cf126c9e598390ae61895eb96f52ae46d" - ), - True, - ), - ], -) -def test_has_digest(image: str, digest: bool): - i = img.Image(image) - assert i.has_digest() == digest - - @pytest.mark.parametrize( "image, str_image", [ @@ -183,6 +171,16 @@ def test_has_digest(image: str, digest: bool): ), ), ("path/image", "docker.io/path/image:latest"), + ( + ( + "ghcr.io/repo/test/image-with-tag-and-digest:v1.2.3" + "@sha256:859b5aada817b3eb53410222e8fc232cf126c9e598390ae61895eb96f52ae46d" + ), + ( + "ghcr.io/repo/test/image-with-tag-and-digest:v1.2.3" + "@sha256:859b5aada817b3eb53410222e8fc232cf126c9e598390ae61895eb96f52ae46d" + ), + ), ], ) def test_str(image: str, str_image: str): diff --git a/tests/validators/notaryv1/test_notaryv1_validator.py b/tests/validators/notaryv1/test_notaryv1_validator.py index 004071afb..dc95cad32 100644 --- a/tests/validators/notaryv1/test_notaryv1_validator.py +++ b/tests/validators/notaryv1/test_notaryv1_validator.py @@ -85,6 +85,22 @@ def test_init(m_notary, val_config): match=r"Unable to find signed digest for image.*'image': '[^']*securesystemsengineering/alice-image:missingtag", ), ), + ( + "securesystemsengineering/alice-image:test@sha256:ac904c9b191d14faf54b7952f2650a4bb21c201bf34131388b851e8ce992a652", + None, + [], + "ac904c9b191d14faf54b7952f2650a4bb21c201bf34131388b851e8ce992a652", + fix.no_exc(), + ), + ( + "securesystemsengineering/alice-image:test@sha256:13333333333333333333333333333337", + None, + [], + "", + pytest.raises( + exc.ValidationError, match=r".*tag and digest do not match.*" + ), + ), ], ) async def test_validate( @@ -358,21 +374,6 @@ async def test_process_chain_of_trust( ), None, ), - ], -) -def test_search_image_targets_for_digest(sample_nv1, image: str, digest: str): - data = fix.get_td("sample_releases")["signed"]["targets"] - assert ( - sample_nv1._NotaryV1Validator__search_image_targets_for_digest( - data, Image(image) - ) - == digest - ) - - -@pytest.mark.parametrize( - "image, digest", - [ ( "image:v1", "1388abc7a12532836c3a81bdb0087409b15208f5aeba7a87aedcfd56d637c145", @@ -384,10 +385,10 @@ def test_search_image_targets_for_digest(sample_nv1, image: str, digest: str): ("image:v3", None), ], ) -def test_search_image_targets_for_tag(sample_nv1, image: str, digest: str): +def test_search_image_targets_for_digest(sample_nv1, image: str, digest: str): data = fix.get_td("sample_releases")["signed"]["targets"] assert ( - sample_nv1._NotaryV1Validator__search_image_targets_for_tag(data, Image(image)) + sample_nv1._NotaryV1Validator__search_image_targets(data, Image(image)) == digest ) From 420fb365767a5371200ec6c3189ee5c58d8cb737 Mon Sep 17 00:00:00 2001 From: Philipp Belitz Date: Fri, 23 Dec 2022 12:06:12 +0100 Subject: [PATCH 2/2] refactor: isort for package import --- connaisseur/__main__.py | 3 +-- connaisseur/alert.py | 4 ++-- connaisseur/config.py | 1 + connaisseur/constants.py | 1 + connaisseur/flask_application.py | 3 +-- connaisseur/kube_api.py | 1 + connaisseur/util.py | 2 +- .../validators/cosign/cosign_validator.py | 10 ++++++---- connaisseur/validators/notaryv1/key_store.py | 2 +- connaisseur/validators/notaryv1/trust_data.py | 2 +- connaisseur/workload_object.py | 1 - scripts/changelogger.py | 3 ++- setup.py | 2 +- tests/conftest.py | 17 +++++++++-------- tests/integration/alerting/app/alert_checker.py | 3 ++- tests/test_admission_request.py | 3 ++- tests/test_alert.py | 8 +++++--- tests/test_config.py | 4 +++- tests/test_exceptions.py | 2 ++ tests/test_flask_application.py | 5 ++++- tests/test_image.py | 6 ++++-- tests/test_kube_api.py | 4 +++- tests/test_logging_wrapper.py | 5 +++-- tests/test_trust_root.py | 3 ++- tests/test_util.py | 6 ++++-- tests/test_workload_object.py | 5 +++-- .../validators/cosign/test_cosign_validator.py | 11 +++++++---- tests/validators/notaryv1/test_keystore.py | 9 ++++++--- tests/validators/notaryv1/test_notary.py | 15 +++++++++------ .../notaryv1/test_notaryv1_validator.py | 9 ++++++--- tests/validators/notaryv1/test_trust_data.py | 11 +++++++---- tests/validators/notaryv1/test_tuf_role.py | 6 ++++-- .../notaryv2/test_notaryv2_validator.py | 4 +++- .../validators/static/test_static_validator.py | 6 ++++-- tests/validators/test_validators.py | 6 ++++-- 35 files changed, 115 insertions(+), 68 deletions(-) create mode 100644 connaisseur/constants.py diff --git a/connaisseur/__main__.py b/connaisseur/__main__.py index 8782e8cf6..cc582aa56 100644 --- a/connaisseur/__main__.py +++ b/connaisseur/__main__.py @@ -5,13 +5,12 @@ from logging.config import dictConfig from cheroot.server import HTTPServer -from cheroot.wsgi import Server from cheroot.ssl.builtin import BuiltinSSLAdapter +from cheroot.wsgi import Server from connaisseur.flask_application import APP from connaisseur.logging_wrapper import ConnaisseurLoggingWrapper - if __name__ == "__main__": LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO") diff --git a/connaisseur/alert.py b/connaisseur/alert.py index 4094f51ad..0d17333fa 100644 --- a/connaisseur/alert.py +++ b/connaisseur/alert.py @@ -7,14 +7,14 @@ import requests from jinja2 import StrictUndefined, Template -from connaisseur.util import safe_json_open, validate_schema +from connaisseur.admission_request import AdmissionRequest from connaisseur.exceptions import ( AlertSendingError, ConfigurationError, InvalidConfigurationFormatError, InvalidImageFormatError, ) -from connaisseur.admission_request import AdmissionRequest +from connaisseur.util import safe_json_open, validate_schema class AlertingConfiguration: diff --git a/connaisseur/config.py b/connaisseur/config.py index 83ecc25c6..b8454f7e4 100644 --- a/connaisseur/config.py +++ b/connaisseur/config.py @@ -1,6 +1,7 @@ import collections import fnmatch import os + import yaml from connaisseur.exceptions import ( diff --git a/connaisseur/constants.py b/connaisseur/constants.py new file mode 100644 index 000000000..946c32aee --- /dev/null +++ b/connaisseur/constants.py @@ -0,0 +1 @@ +SHA256 = "sha256" diff --git a/connaisseur/flask_application.py b/connaisseur/flask_application.py index 2b2d56d03..3877aa20a 100644 --- a/connaisseur/flask_application.py +++ b/connaisseur/flask_application.py @@ -4,7 +4,7 @@ import traceback from flask import Flask, jsonify, request -from prometheus_flask_exporter import PrometheusMetrics, NO_PREFIX +from prometheus_flask_exporter import NO_PREFIX, PrometheusMetrics import connaisseur.constants as const from connaisseur.admission_request import AdmissionRequest @@ -17,7 +17,6 @@ ) from connaisseur.util import get_admission_review - APP = Flask(__name__) """ Flask application that admits the request send to the k8s cluster, validates it and diff --git a/connaisseur/kube_api.py b/connaisseur/kube_api.py index a799f06bc..43f1c03f2 100644 --- a/connaisseur/kube_api.py +++ b/connaisseur/kube_api.py @@ -1,4 +1,5 @@ import os + import requests diff --git a/connaisseur/util.py b/connaisseur/util.py index 014157f8e..07b59fd49 100644 --- a/connaisseur/util.py +++ b/connaisseur/util.py @@ -5,7 +5,7 @@ from typing import Optional import yaml -from jsonschema import FormatChecker, validate, ValidationError +from jsonschema import FormatChecker, ValidationError, validate from connaisseur.exceptions import PathTraversalError diff --git a/connaisseur/validators/cosign/cosign_validator.py b/connaisseur/validators/cosign/cosign_validator.py index e577ba995..2d2b37367 100644 --- a/connaisseur/validators/cosign/cosign_validator.py +++ b/connaisseur/validators/cosign/cosign_validator.py @@ -4,21 +4,20 @@ import os import re import subprocess # nosec - from concurrent.futures import ThreadPoolExecutor import connaisseur.constants as const from connaisseur.exceptions import ( CosignError, CosignTimeout, - NotFoundException, InvalidFormatException, + NotFoundException, UnexpectedCosignData, ValidationError, WrongKeyError, ) from connaisseur.image import Image -from connaisseur.trust_root import KMSKey, TrustRoot, ECDSAKey +from connaisseur.trust_root import ECDSAKey, KMSKey, TrustRoot from connaisseur.util import safe_path_func # nosec from connaisseur.validators.interface import ValidatorInterface @@ -163,7 +162,10 @@ def __get_cosign_validated_digests(self, image: str, trust_root: dict): digest = sig_data["critical"]["image"].get( "docker-manifest-digest", "" ) - if re.match(rf"{const.SHA256}:[0-9A-Fa-f]{{64}}", digest) is None: + if ( + re.match(rf"{const.SHA256}:[0-9A-Fa-f]{{64}}", digest) + is None + ): msg = "Digest '{digest}' does not match expected digest pattern." raise InvalidFormatException(message=msg, digest=digest) except Exception as err: diff --git a/connaisseur/validators/notaryv1/key_store.py b/connaisseur/validators/notaryv1/key_store.py index 74c535bc7..18d6ccc8b 100644 --- a/connaisseur/validators/notaryv1/key_store.py +++ b/connaisseur/validators/notaryv1/key_store.py @@ -1,6 +1,6 @@ +import connaisseur.constants as const from connaisseur.exceptions import NotFoundException from connaisseur.trust_root import TrustRoot -import connaisseur.constants as const class KeyStore: diff --git a/connaisseur/validators/notaryv1/trust_data.py b/connaisseur/validators/notaryv1/trust_data.py index d8fa0e3c7..a83f2a76c 100644 --- a/connaisseur/validators/notaryv1/trust_data.py +++ b/connaisseur/validators/notaryv1/trust_data.py @@ -15,7 +15,7 @@ ValidationError, WrongKeyError, ) -from connaisseur.trust_root import TrustRoot, ECDSAKey +from connaisseur.trust_root import ECDSAKey, TrustRoot from connaisseur.util import validate_schema from connaisseur.validators.notaryv1.key_store import KeyStore diff --git a/connaisseur/workload_object.py b/connaisseur/workload_object.py index 9e35d73da..286f155e0 100644 --- a/connaisseur/workload_object.py +++ b/connaisseur/workload_object.py @@ -2,7 +2,6 @@ from connaisseur.exceptions import ParentNotFoundError, UnknownAPIVersionError from connaisseur.image import Image - SUPPORTED_API_VERSIONS = { "Pod": ["v1"], "Deployment": ["apps/v1", "apps/v1beta1", "apps/v1beta2"], diff --git a/scripts/changelogger.py b/scripts/changelogger.py index 169766af7..9cce646ff 100644 --- a/scripts/changelogger.py +++ b/scripts/changelogger.py @@ -1,10 +1,11 @@ import argparse import base64 -import requests import subprocess import sys import time +import requests + sep = "@@__CHGLOG__@@" delim = "@@__CHGLOG_DELIMITER__@@" ha = "%H" diff --git a/setup.py b/setup.py index cacf9878a..69e6af81f 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,3 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup setup(name="connaisseur", packages=find_packages()) diff --git a/tests/conftest.py b/tests/conftest.py index bfd9e21cb..c6b11bca7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,22 @@ +import json import os import re -import json +from contextlib import contextmanager + import pytest import requests from aioresponses import CallbackResult -import connaisseur.kube_api -import connaisseur.config as co + import connaisseur.admission_request as admreq import connaisseur.alert as alert -from connaisseur.trust_root import TrustRoot -import connaisseur.validators.notaryv1.trust_data as td +import connaisseur.config as co +import connaisseur.kube_api +import connaisseur.util as util import connaisseur.validators.notaryv1.key_store as ks import connaisseur.validators.notaryv1.notary as no import connaisseur.validators.notaryv1.notaryv1_validator as nv1 -import connaisseur.util as util -from contextlib import contextmanager - +import connaisseur.validators.notaryv1.trust_data as td +from connaisseur.trust_root import TrustRoot """ This file is used for sharing fixtures across all other test files. diff --git a/tests/integration/alerting/app/alert_checker.py b/tests/integration/alerting/app/alert_checker.py index c41e30413..a961fe1ce 100644 --- a/tests/integration/alerting/app/alert_checker.py +++ b/tests/integration/alerting/app/alert_checker.py @@ -1,6 +1,7 @@ -from flask import Flask, request import json +from flask import Flask, request + APP = Flask(__name__) endpoint_hits = {} diff --git a/tests/test_admission_request.py b/tests/test_admission_request.py index 0c917597e..49799f495 100644 --- a/tests/test_admission_request.py +++ b/tests/test_admission_request.py @@ -1,8 +1,9 @@ import pytest -from . import conftest as fix + import connaisseur.admission_request as admreq import connaisseur.exceptions as exc +from . import conftest as fix static_adm_req = [ { diff --git a/tests/test_alert.py b/tests/test_alert.py index e3ec661aa..43de5e41a 100644 --- a/tests/test_alert.py +++ b/tests/test_alert.py @@ -1,12 +1,14 @@ -import pytest -from datetime import datetime, timedelta import json +from datetime import datetime, timedelta + +import pytest -from . import conftest as fix import connaisseur.alert as alert from connaisseur.admission_request import AdmissionRequest from connaisseur.exceptions import AlertSendingError, ConfigurationError +from . import conftest as fix + with open( "tests/data/sample_admission_requests/ad_request_deployments.json", "r" ) as readfile: diff --git a/tests/test_config.py b/tests/test_config.py index 5947aa03b..fa09cc307 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,12 @@ import pytest -from . import conftest as fix + import connaisseur.config as co import connaisseur.exceptions as exc import connaisseur.validators as vals from connaisseur.image import Image +from . import conftest as fix + @pytest.fixture(autouse=True) def mock_config_path(monkeypatch): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a99cef574..8a6843e60 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,5 +1,7 @@ import os + import pytest + import connaisseur.exceptions as exc diff --git a/tests/test_flask_application.py b/tests/test_flask_application.py index 0c258dc09..748770484 100644 --- a/tests/test_flask_application.py +++ b/tests/test_flask_application.py @@ -1,7 +1,8 @@ import re + import pytest from aioresponses import aioresponses -from . import conftest as fix + import connaisseur.alert as alert import connaisseur.config as co import connaisseur.exceptions as exc @@ -9,6 +10,8 @@ from connaisseur.image import Image from connaisseur.validators.static.static_validator import StaticValidator +from . import conftest as fix + @pytest.fixture(autouse=True) def m_config(monkeypatch, sample_nv1): diff --git a/tests/test_image.py b/tests/test_image.py index c6c2a9ec3..7b213bd09 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -1,7 +1,9 @@ import pytest -from . import conftest as fix -import connaisseur.image as img + import connaisseur.exceptions as exc +import connaisseur.image as img + +from . import conftest as fix @pytest.mark.parametrize( diff --git a/tests/test_kube_api.py b/tests/test_kube_api.py index a8b3e74e8..85d5d96dd 100644 --- a/tests/test_kube_api.py +++ b/tests/test_kube_api.py @@ -1,7 +1,9 @@ import pytest -from . import conftest as fix + import connaisseur.kube_api as k_api +from . import conftest as fix + @pytest.mark.parametrize( "url, response", diff --git a/tests/test_logging_wrapper.py b/tests/test_logging_wrapper.py index 6e20961fb..a70a9b1e9 100644 --- a/tests/test_logging_wrapper.py +++ b/tests/test_logging_wrapper.py @@ -1,10 +1,11 @@ -import pytest import time -from . import conftest as fix +import pytest import connaisseur.logging_wrapper as lw +from . import conftest as fix + @pytest.fixture def mock_time(monkeypatch): diff --git a/tests/test_trust_root.py b/tests/test_trust_root.py index 5ccd838f1..a38444607 100644 --- a/tests/test_trust_root.py +++ b/tests/test_trust_root.py @@ -1,8 +1,9 @@ import pytest -from . import conftest as fix + import connaisseur.exceptions as exc import connaisseur.trust_root as trust_root +from . import conftest as fix sample_ecdsa = "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOXYta5TgdCwXTCnLU09W5T4M4r9f\nQQrqJuADP6U7g5r9ICgPSmZuRHP/1AYUfOQW3baveKsT969EfELKj1lfCA==\n-----END PUBLIC KEY-----" sample_rsa = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs5pC7R5OTSTUMJHUniPk\nrLfmGDAUxZtRlvIE+pGPCD6cUXH22advkK87xwpupjxdVYuKTFnWHUIyFJwjI3vu\nsievezcAr0E/xxyeo49tWog9kFoooK3qmXjpETC8OpvNROZ0K3qhlm9PZkGo3gSJ\n/B4rMU/d+jkCI8eiUPpdVQOczdBoD5nzQAF1mfmffWGsbKY+d8/l77Vset0GXExR\nzUtnglMhREyHNpDeQUg5OEn+kuGLlTzIxpIF+MlbzP3+xmNEzH2iafr0ae2g5kX2\n880priXpxG8GXW2ybZmPvchclnvFu4ZfZcM10FpgYJFvR/9iofFeAka9u5z6VZcc\nmQIDAQAB\n-----END PUBLIC KEY-----" diff --git a/tests/test_util.py b/tests/test_util.py index 3267c3fd7..66a61f466 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -1,7 +1,9 @@ import pytest -from . import conftest as fix -import connaisseur.util as ut + import connaisseur.exceptions as exc +import connaisseur.util as ut + +from . import conftest as fix @pytest.mark.parametrize( diff --git a/tests/test_workload_object.py b/tests/test_workload_object.py index 53116905d..a766be35e 100644 --- a/tests/test_workload_object.py +++ b/tests/test_workload_object.py @@ -1,9 +1,10 @@ import pytest -from . import conftest as fix -import connaisseur.workload_object as wl + import connaisseur.exceptions as exc +import connaisseur.workload_object as wl from connaisseur.image import Image +from . import conftest as fix static_k8s = [ { diff --git a/tests/validators/cosign/test_cosign_validator.py b/tests/validators/cosign/test_cosign_validator.py index f9cb8e204..862b7efc2 100644 --- a/tests/validators/cosign/test_cosign_validator.py +++ b/tests/validators/cosign/test_cosign_validator.py @@ -1,12 +1,15 @@ +import subprocess + import pytest import pytest_subprocess -import subprocess -from ... import conftest as fix -from connaisseur.image import Image -import connaisseur.validators.cosign.cosign_validator as co + import connaisseur.exceptions as exc +import connaisseur.validators.cosign.cosign_validator as co +from connaisseur.image import Image from connaisseur.trust_root import TrustRoot +from ... import conftest as fix + example_key = ( "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6uuXb" "ZhEfTYb4Mnb/LdrtXKTIIbzNBp8mwriocbaxXxzqu" diff --git a/tests/validators/notaryv1/test_keystore.py b/tests/validators/notaryv1/test_keystore.py index 269c4c7b6..6744a3ba4 100644 --- a/tests/validators/notaryv1/test_keystore.py +++ b/tests/validators/notaryv1/test_keystore.py @@ -1,10 +1,13 @@ import base64 -from connaisseur.trust_root import TrustRoot + import pytest -from ... import conftest as fix + +import connaisseur.exceptions as exc import connaisseur.validators.notaryv1.key_store as ks +from connaisseur.trust_root import TrustRoot from connaisseur.validators.notaryv1.trust_data import TrustData -import connaisseur.exceptions as exc + +from ... import conftest as fix sample_key = ( "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtR5kwrDK22SyCu" diff --git a/tests/validators/notaryv1/test_notary.py b/tests/validators/notaryv1/test_notary.py index b6ce5f71d..6142ed49b 100644 --- a/tests/validators/notaryv1/test_notary.py +++ b/tests/validators/notaryv1/test_notary.py @@ -1,15 +1,18 @@ -from requests.models import HTTPError -import yaml -import pytest import re -from aioresponses import aioresponses + +import pytest +import yaml from aiohttp.client_exceptions import ClientResponseError -from ... import conftest as fix -import connaisseur.validators.notaryv1.notary as notary +from aioresponses import aioresponses +from requests.models import HTTPError + import connaisseur.exceptions as exc import connaisseur.util +import connaisseur.validators.notaryv1.notary as notary from connaisseur.image import Image +from ... import conftest as fix + @pytest.fixture def sample_notaries(): diff --git a/tests/validators/notaryv1/test_notaryv1_validator.py b/tests/validators/notaryv1/test_notaryv1_validator.py index dc95cad32..8e97cb5ab 100644 --- a/tests/validators/notaryv1/test_notaryv1_validator.py +++ b/tests/validators/notaryv1/test_notaryv1_validator.py @@ -1,12 +1,15 @@ import os import re -from connaisseur.trust_root import TrustRoot + import pytest from aioresponses import aioresponses -from ... import conftest as fix + +import connaisseur.exceptions as exc import connaisseur.validators.notaryv1.notaryv1_validator as nv1 from connaisseur.image import Image -import connaisseur.exceptions as exc +from connaisseur.trust_root import TrustRoot + +from ... import conftest as fix @pytest.mark.parametrize( diff --git a/tests/validators/notaryv1/test_trust_data.py b/tests/validators/notaryv1/test_trust_data.py index 355b39ab9..637df3967 100644 --- a/tests/validators/notaryv1/test_trust_data.py +++ b/tests/validators/notaryv1/test_trust_data.py @@ -1,12 +1,15 @@ -import pytest +import datetime as dt import json + +import pytest import pytz -import datetime as dt -from ... import conftest as fix -import connaisseur.validators.notaryv1.trust_data as td + import connaisseur.exceptions as exc +import connaisseur.validators.notaryv1.trust_data as td from connaisseur.trust_root import TrustRoot +from ... import conftest as fix + pub_root_keys = { "2cd463575a31cb3184320e889e82fb1f9e3bbebee2ae42b2f825b0c8a734e798": { "keytype": "ecdsa-x509", diff --git a/tests/validators/notaryv1/test_tuf_role.py b/tests/validators/notaryv1/test_tuf_role.py index dffcc83d4..3890abb63 100644 --- a/tests/validators/notaryv1/test_tuf_role.py +++ b/tests/validators/notaryv1/test_tuf_role.py @@ -1,7 +1,9 @@ import pytest -from ... import conftest as fix -import connaisseur.validators.notaryv1.tuf_role as tuf + import connaisseur.exceptions as exc +import connaisseur.validators.notaryv1.tuf_role as tuf + +from ... import conftest as fix @pytest.mark.parametrize( diff --git a/tests/validators/notaryv2/test_notaryv2_validator.py b/tests/validators/notaryv2/test_notaryv2_validator.py index db403b454..8e011fff2 100644 --- a/tests/validators/notaryv2/test_notaryv2_validator.py +++ b/tests/validators/notaryv2/test_notaryv2_validator.py @@ -1,7 +1,9 @@ import pytest -from ... import conftest as fix + import connaisseur.validators.notaryv2.notaryv2_validator as nv2 +from ... import conftest as fix + @pytest.mark.parametrize("", []) def test_init(): diff --git a/tests/validators/static/test_static_validator.py b/tests/validators/static/test_static_validator.py index 34d1855de..ffa1dc517 100644 --- a/tests/validators/static/test_static_validator.py +++ b/tests/validators/static/test_static_validator.py @@ -1,9 +1,11 @@ import pytest -from ... import conftest as fix -import connaisseur.validators.static.static_validator as st + import connaisseur.exceptions as exc +import connaisseur.validators.static.static_validator as st from connaisseur.image import Image +from ... import conftest as fix + @pytest.mark.parametrize("name, approve", [("sample", True), ("sample", False)]) def test_init(name, approve): diff --git a/tests/validators/test_validators.py b/tests/validators/test_validators.py index 07143b799..61575033f 100644 --- a/tests/validators/test_validators.py +++ b/tests/validators/test_validators.py @@ -1,7 +1,9 @@ import pytest -from .. import conftest as fix -import connaisseur.validators.validator as val + import connaisseur.exceptions as exc +import connaisseur.validators.validator as val + +from .. import conftest as fix @pytest.mark.parametrize(