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 c12886707..3877aa20a 100644 --- a/connaisseur/flask_application.py +++ b/connaisseur/flask_application.py @@ -4,8 +4,9 @@ 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 from connaisseur.alert import send_alerts from connaisseur.config import Config @@ -16,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 @@ -190,5 +190,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/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 468e9d94c..2d2b37367 100644 --- a/connaisseur/validators/cosign/cosign_validator.py +++ b/connaisseur/validators/cosign/cosign_validator.py @@ -4,20 +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 @@ -162,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(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 +183,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..18d6ccc8b 100644 --- a/connaisseur/validators/notaryv1/key_store.py +++ b/connaisseur/validators/notaryv1/key_store.py @@ -1,3 +1,4 @@ +import connaisseur.constants as const from connaisseur.exceptions import NotFoundException from connaisseur.trust_root import TrustRoot @@ -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..a83f2a76c 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, @@ -14,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 @@ -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/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/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_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 7a699a0a4..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): @@ -154,12 +157,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..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( @@ -42,7 +44,7 @@ "image", "tag", None, - "", + None, "registry.io", fix.no_exc(), ), @@ -94,16 +96,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 +106,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 +152,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 +173,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/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 004071afb..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( @@ -85,6 +88,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 +377,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 +388,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 ) 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(