diff --git a/sbom-utility-scripts/Dockerfile b/sbom-utility-scripts/Dockerfile index e9e727a..2b47c6e 100644 --- a/sbom-utility-scripts/Dockerfile +++ b/sbom-utility-scripts/Dockerfile @@ -1,4 +1,4 @@ -FROM registry.access.redhat.com/ubi9/python-39:1-197.1725907694@sha256:278ae38e8f28ccba3cb7cd542f684d739a84f771e418fc8018d07a522205b05c +FROM registry.access.redhat.com/ubi9/python-312:9.5@sha256:88ea2d10c741f169681102b46b16c66d20c94c3cc561edbb6444b0de3a1c81b3 WORKDIR /scripts diff --git a/sbom-utility-scripts/scripts/add-image-reference-script/tox.ini b/sbom-utility-scripts/scripts/add-image-reference-script/tox.ini index a0ff21a..0d6449e 100644 --- a/sbom-utility-scripts/scripts/add-image-reference-script/tox.ini +++ b/sbom-utility-scripts/scripts/add-image-reference-script/tox.ini @@ -2,6 +2,7 @@ env_list = flake8,black,test [testenv:test] +basepython = 3.12 deps = -r requirements-test.txt -r requirements.txt commands = pytest ./ \ diff --git a/sbom-utility-scripts/scripts/base-images-sbom-script/README.md b/sbom-utility-scripts/scripts/base-images-sbom-script/README.md index 72f8eb5..f2fee64 100644 --- a/sbom-utility-scripts/scripts/base-images-sbom-script/README.md +++ b/sbom-utility-scripts/scripts/base-images-sbom-script/README.md @@ -7,10 +7,10 @@ It takes several inputs: 1. path to the sbom file, that will be updated in place with the base image data 2. path to a json file containing parsed Dockerfile via dockerfile-json 3. path to a file containing base images references as used during in the Dockerfile mapped to the full image references -with digests. This mapping is expected to be in the format **|**. -The full image reference with digest is generated from the output of **buildah images --format '{{ .Name }}:{{ .Tag }}@{{ .Digest }}'**. - +with digests. This mapping is expected to be in the format ` `. +The full image reference with digest is generated from the output of `buildah images --format '{{ .Name }}:{{ .Tag }}@{{ .Digest }}'`. +## CycloneDX The base images data will be added in the [formulation attribute](https://cyclonedx.org/docs/1.5/json/#formulation), by appending new item with the **components** array. This will not affect any other items in the formulation array. If the formulation part of the sbom does not exist, it is created. @@ -72,6 +72,78 @@ If a base image is used multiple times, only one component with multiple propert }, ``` +## SPDX + +The script adds the base images to the `.packages` array in the SBOM and adds +` BUILD_TOOL_OF ` relationships to the `.relationships` array. + +Where the CycloneDX SBOM uses `.properties` to indicate whether the image is a "builder" +image or a base image for SPDX we use annotations instead +(see the [relevant section of the ADR][spdx-adr-properties]). + +The result might look something like this: + +``` +{ + // ... + "packages": [ + // ... + { + "SPDXID": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "name": "quay.io/mkosiarc_rhtap/single-container-app", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app" + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": "{\"name\":\"konflux:container:is_builder_image:for_stage\",\"value\":\"0\"}", + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER" + } + ] + }, + { + "SPDXID": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "name": "registry.access.redhat.com/ubi8/ubi", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi" + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": "{\"name\":\"konflux:container:is_builder_image:for_stage\",\"value\":\"1\"}", + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER" + } + ] + } + ], + "relationships": [ + // ... + { + "spdxElementId": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-main" + }, + { + "spdxElementId": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-main" + } + ] +} +``` ## The image The image that is used in the buildah task in Konflux is built by the Github CI and pushed to @@ -84,4 +156,5 @@ during development with: ``` tox -e test ``` - \ No newline at end of file + +[spdx-adr-properties]: https://github.com/konflux-ci/architecture/blob/main/ADR/0044-spdx-support.md#componentproperties diff --git a/sbom-utility-scripts/scripts/base-images-sbom-script/app/base_images_sbom_script.py b/sbom-utility-scripts/scripts/base-images-sbom-script/app/base_images_sbom_script.py index 78479cb..c607be6 100644 --- a/sbom-utility-scripts/scripts/base-images-sbom-script/app/base_images_sbom_script.py +++ b/sbom-utility-scripts/scripts/base-images-sbom-script/app/base_images_sbom_script.py @@ -1,14 +1,44 @@ -import json import argparse +import datetime +import hashlib +import json import pathlib +from typing import Any, Literal, NamedTuple, NewType, TypedDict -from collections import namedtuple from packageurl import PackageURL -ParsedImage = namedtuple("ParsedImage", "repository, digest, name") + +class ParsedImage(NamedTuple): + repository: str + digest: str + name: str + + +class CDXComponent(TypedDict): + """The relevant attributes of a CycloneDX Component.""" + + type: str + name: str + purl: str + properties: list[dict[str, str]] -def parse_image_reference_to_parts(image): +SPDXID = NewType("SPDXID", str) + + +class SPDXPackage(TypedDict): + """The relevant attributes of an SPDX Package (the equivalent of a CycloneDX Component).""" + + # SPDXID, name and downloadLocation are required per the SPDX 2.3 schema + # https://github.com/spdx/spdx-spec/blob/ed8c9b520963b651fce3c86422b387e92e6d9a8f/schemas/spdx-schema.json#L438 + SPDXID: SPDXID + name: str + downloadLocation: str + externalRefs: list[dict[str, str]] + annotations: list[dict[str, str]] + + +def parse_image_reference_to_parts(image: str) -> ParsedImage: """ This function expects that the image is in the expected format as generated from the output of @@ -18,7 +48,7 @@ def parse_image_reference_to_parts(image): :return: ParsedImage (namedTuple): the image parsed into individual parts """ - # example image: registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac # noqa + # example image: registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac # repository_with_tag = registry.access.redhat.com/ubi8/ubi:latest # digest = sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac # repository = registry.access.redhat.com/ubi8/ubi @@ -33,28 +63,28 @@ def parse_image_reference_to_parts(image): return ParsedImage(repository=repository, digest=digest, name=name) -def get_base_images_sbom_components(base_images, base_images_digests): +def get_base_images_sbom_components(base_images: list[str], base_images_digests: dict[str, str]) -> list[CDXComponent]: """ Creates the base images sbom data - :param base_images: (List) - List of base images used during build, in the order they were used. The values here - are the keys in the base_images_digests dict. - For example: - ["registry.access.redhat.com/ubi8/ubi:latest"] - :param base_images_digests: (Dict) - Dict of base images references, where the key is the image reference as - used in the original Dockerfile (The elements of base_images param) - and the values are the full image reference with digests that was - actually used by buildah during build time. - For example: - { - "registry.access.redhat.com/ubi8/ubi:latest": - "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - } - :return: components (List) - List of dict items in which each item contains sbom data about each base image + :param base_images: List of base images used during build, in the order they were used. The values here + are the keys in the base_images_digests dict. + For example: + ["registry.access.redhat.com/ubi8/ubi:latest"] + :param base_images_digests: Dict of base images references, where the key is the image reference as + used in the original Dockerfile (The elements of base_images param) + and the values are the full image reference with digests that was + actually used by buildah during build time. + For example: + { + "registry.access.redhat.com/ubi8/ubi:latest": + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" + } + :return: List of dict items in which each item contains sbom data about each base image """ - components = [] - already_used_base_images = set() + components: list[CDXComponent] = [] + already_used_base_images: set[str] = set() for index, image in enumerate(base_images): # flatpak archive and scratch are not real base images. So we skip them, but @@ -102,7 +132,7 @@ def get_base_images_sbom_components(base_images, base_images_digests): if component["purl"] == purl_str: component["properties"].append(property) else: - component = { + component: CDXComponent = { "type": "container", "name": parsed_image.repository, "purl": purl_str, @@ -114,13 +144,13 @@ def get_base_images_sbom_components(base_images, base_images_digests): return components -def get_base_images_from_dockerfile(parsed_dockerfile): +def get_base_images_from_dockerfile(parsed_dockerfile: dict[str, Any]) -> list[str]: """ Reads the base images from provided parsed dockerfile - :param parsed_dockerfile: (Dict) - Contents of the parsed dockerfile - :return: base_images (List) - List of base images used during build as extracted - from the dockerfile in the order they were used. + :param parsed_dockerfile: Contents of the parsed dockerfile + :return: base_images List of base images used during build as extracted + from the dockerfile in the order they were used. Example: If the Dockerfile looks like @@ -144,7 +174,7 @@ def get_base_images_from_dockerfile(parsed_dockerfile): ] }, """ - base_images = [] + base_images: list[str] = [] # this part of the json is the relevant one that contains the # info about base images @@ -168,7 +198,104 @@ def get_base_images_from_dockerfile(parsed_dockerfile): return base_images -def parse_args(): +def cdx_to_spdx(cdx: CDXComponent, annotation_date: datetime.datetime) -> SPDXPackage: + name = cdx["name"] + purl = cdx["purl"] + # consistent with index_image_sbom_script.py + spdxid = SPDXID(f"SPDXRef-image-{name}-{hashlib.sha256(purl.encode()).hexdigest()}") + + return { + "SPDXID": spdxid, + "name": name, + "downloadLocation": "NOASSERTION", + # https://github.com/konflux-ci/architecture/blob/main/ADR/0044-spdx-support.md#componentpurl + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": purl, + }, + ], + # https://github.com/konflux-ci/architecture/blob/main/ADR/0044-spdx-support.md#componentproperties + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": json.dumps(cdx_property, separators=(",", ":")), + # https://spdx.github.io/spdx-spec/v2.3/annotations/#122-annotation-date-field + "annotationDate": annotation_date.strftime("%Y-%m-%dT%H:%M:%SZ"), + "annotationType": "OTHER", + } + for cdx_property in cdx["properties"] + ], + } + + +def find_spdx_root_package(sbom: dict[str, Any]) -> SPDXID: + """Find the element that's in relationship DESCRIBES . + + If there isn't exactly one such element, raise an error. + """ + doc_spxid = sbom["SPDXID"] + doc_describes = [ + r["relatedSpdxElement"] + for r in sbom.get("relationships", []) + if r["spdxElementId"] == doc_spxid and r["relationshipType"] == "DESCRIBES" + ] + if len(doc_describes) != 1: + raise ValueError( + "Expected to find exactly one DESCRIBES relationship. " + f"Found {len(doc_describes)} ROOTs: {doc_describes}" + ) + return SPDXID(doc_describes[0]) + + +def update_cyclonedx_sbom(sbom: dict[str, Any], base_images: list[CDXComponent]) -> None: + """Update (in-place) a CycloneDX SBOM with a list of base images. + + Add an item containing the base image list into the .formulation section. + + :param base_images: list of base images in CycloneDX format + """ + sbom.setdefault("formulation", []).append({"components": base_images}) + + +def update_spdx_sbom(sbom: dict[str, Any], base_images: list[SPDXPackage]) -> None: + """Update (in-place) an SPDX SBOM with a list of base images. + + Add the base images to the .packages section. + Add BUILD_TOOL_OF relationships to the .relationships section. + + :param base_images: list of base images in SPDX format + """ + root = find_spdx_root_package(sbom) + relationships = [ + { + "spdxElementId": base_image["SPDXID"], + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": root, + } + for base_image in base_images + ] + + sbom.setdefault("packages", []).extend(base_images) + sbom.setdefault("relationships", []).extend(relationships) + + +def detect_sbom_type(sbom: dict[str, Any]) -> Literal["cyclonedx", "spdx"]: + if sbom.get("bomFormat") == "CycloneDX": + return "cyclonedx" + elif sbom.get("spdxVersion"): + return "spdx" + else: + raise ValueError("Unknown SBOM format") + + +def _datetime_utc_now() -> datetime.datetime: + # a mockable datetime.datetime.now (just for tests): + return datetime.datetime.now(datetime.UTC) + + +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description="Updates the sbom file with base images data based on the provided files" ) @@ -193,8 +320,7 @@ def parse_args(): return args -def main(): - +def main() -> None: args = parse_args() with args.parsed_dockerfile.open("r") as f: @@ -205,17 +331,20 @@ def main(): base_images_digests_raw = args.base_images_digests.read_text().splitlines() base_images_digests = dict(item.split() for item in base_images_digests_raw) + base_images_sbom_components = get_base_images_sbom_components(base_images, base_images_digests) + # base_images_sbom_components could be empty, when having just one stage FROM scratch + if not base_images_sbom_components: + return + with args.sbom.open("r") as f: sbom = json.load(f) - base_images_sbom_components = get_base_images_sbom_components(base_images, base_images_digests) - - # base_images_sbom_components could be empty, when having just one stage FROM scratch - if base_images_sbom_components: - if "formulation" in sbom: - sbom["formulation"].append({"components": base_images_sbom_components}) - else: - sbom.update({"formulation": [{"components": base_images_sbom_components}]}) + if detect_sbom_type(sbom) == "cyclonedx": + update_cyclonedx_sbom(sbom, base_images_sbom_components) + else: + annotation_date = _datetime_utc_now() + base_images_spdx = [cdx_to_spdx(c, annotation_date) for c in base_images_sbom_components] + update_spdx_sbom(sbom, base_images_spdx) with args.sbom.open("w") as f: json.dump(sbom, f, indent=4) diff --git a/sbom-utility-scripts/scripts/base-images-sbom-script/app/test_base_images_sbom_script.py b/sbom-utility-scripts/scripts/base-images-sbom-script/app/test_base_images_sbom_script.py index 78077e8..aa4ab08 100644 --- a/sbom-utility-scripts/scripts/base-images-sbom-script/app/test_base_images_sbom_script.py +++ b/sbom-utility-scripts/scripts/base-images-sbom-script/app/test_base_images_sbom_script.py @@ -1,14 +1,18 @@ -import pytest import json - +import datetime +from pathlib import Path +from typing import Any from unittest.mock import MagicMock +import pytest +from pytest_mock import MockerFixture + from base_images_sbom_script import ( - get_base_images_sbom_components, + ParsedImage, get_base_images_from_dockerfile, + get_base_images_sbom_components, main, parse_image_reference_to_parts, - ParsedImage, ) @@ -23,16 +27,14 @@ "scratch", ], { - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", # noqa + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", }, [ { "type": "container", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io" - "/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -43,8 +45,7 @@ { "type": "container", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -61,16 +62,14 @@ "registry.access.redhat.com/ubi8/ubi:latest", ], { - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", # noqa + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", }, [ { "type": "container", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io" - "/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -81,8 +80,7 @@ { "type": "container", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", "properties": [{"name": "konflux:container:is_base_image", "value": "true"}], }, ], @@ -91,14 +89,13 @@ ( ["registry.access.redhat.com/ubi8/ubi:latest"], { - "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", # noqa + "registry.access.redhat.com/ubi8/ubi:latest": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", }, [ { "type": "container", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", "properties": [{"name": "konflux:container:is_base_image", "value": "true"}], }, ], @@ -107,15 +104,13 @@ ( ["quay.io/mkosiarc_rhtap/single-container-app:f2566ab", "scratch"], { - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", }, [ { "type": "container", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io" - "/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -128,28 +123,23 @@ # four builder images, and from scratch in last stage ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", - "quay.io/builder3/builder3:ccccccc@sha256" - ":3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", - "quay.io/builder4/builder4:ddddddd@sha256" - ":4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", "scratch", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa - "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", # noqa - "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -160,8 +150,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -172,8 +161,7 @@ { "type": "container", "name": "quay.io/builder3/builder3", - "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943" - "?repository_url=quay.io/builder3/builder3", + "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943?repository_url=quay.io/builder3/builder3", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -184,8 +172,7 @@ { "type": "container", "name": "quay.io/builder4/builder4", - "purl": "pkg:oci/builder4@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944" - "?repository_url=quay.io/builder4/builder4", + "purl": "pkg:oci/builder4@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944?repository_url=quay.io/builder4/builder4", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -198,30 +185,24 @@ # four builders and one parent image ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", - "quay.io/builder3/builder3:ccccccc@sha256" - ":3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", - "quay.io/builder4/builder4:ddddddd@sha256" - ":4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", - "registry.access.redhat.com/ubi8/ubi:latest@sha256" - ":627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa - "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", # noqa - "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", # noqa - "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -232,8 +213,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -244,8 +224,7 @@ { "type": "container", "name": "quay.io/builder3/builder3", - "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943" - "?repository_url=quay.io/builder3/builder3", + "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943?repository_url=quay.io/builder3/builder3", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -256,8 +235,7 @@ { "type": "container", "name": "quay.io/builder4/builder4", - "purl": "pkg:oci/builder4@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944" - "?repository_url=quay.io/builder4/builder4", + "purl": "pkg:oci/builder4@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944?repository_url=quay.io/builder4/builder4", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -268,8 +246,7 @@ { "type": "container", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", "properties": [{"name": "konflux:container:is_base_image", "value": "true"}], }, ], @@ -277,32 +254,25 @@ # 3 builders and one final base image. builder 1 is reused three times, resulting in multiple properties ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder3/builder3:ccccccc@sha256" - ":3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "registry.access.redhat.com/ubi8/ubi:latest@sha256" - ":627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa - "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", # noqa - "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", # noqa - "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac": "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -321,8 +291,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -333,8 +302,7 @@ { "type": "container", "name": "quay.io/builder3/builder3", - "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943" - "?repository_url=quay.io/builder3/builder3", + "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943?repository_url=quay.io/builder3/builder3", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -359,30 +327,24 @@ # 3 builders and final base image is scratch. builder 1 is reused three times, resulting in multiple properties ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder3/builder3:ccccccc@sha256" - ":3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", "scratch", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa - "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", # noqa - "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943": "quay.io/builder3/builder3:ccccccc@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943", + "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944": "quay.io/builder4/builder4:ddddddd@sha256:4f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420944", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -401,8 +363,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -413,8 +374,7 @@ { "type": "container", "name": "quay.io/builder3/builder3", - "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943" - "?repository_url=quay.io/builder3/builder3", + "purl": "pkg:oci/builder3@sha256:3f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420943?repository_url=quay.io/builder3/builder3", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -427,23 +387,19 @@ # 2 builders and builder 1 is then reused as final base image, resulting in multiple properties ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -458,8 +414,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -472,31 +427,25 @@ # Two images, both reused and several oci-archives and from scratch layers ( [ - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", "scratch", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", "scratch", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", "oci-archive:export/out.ociarchive", - "quay.io/builder2/builder2:bbbbbbb@sha256" - ":2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", "oci-archive:export/out.ociarchive", - "quay.io/builder1/builder1:aaaaaaa@sha256" - ":1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", ], { - "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa - "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", # noqa + "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941": "quay.io/builder1/builder1:aaaaaaa@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942": "quay.io/builder2/builder2:bbbbbbb@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942", }, [ { "type": "container", "name": "quay.io/builder1/builder1", - "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941" - "?repository_url=quay.io/builder1/builder1", + "purl": "pkg:oci/builder1@sha256:1f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/builder1/builder1", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -515,8 +464,7 @@ { "type": "container", "name": "quay.io/builder2/builder2", - "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942" - "?repository_url=quay.io/builder2/builder2", + "purl": "pkg:oci/builder2@sha256:2f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420942?repository_url=quay.io/builder2/builder2", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -537,15 +485,13 @@ "oci-archive:export/out.ociarchive", ], { - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", # noqa + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", }, [ { "type": "container", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io" - "/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", "properties": [ { "name": "konflux:container:is_builder_image:for_stage", @@ -568,300 +514,699 @@ def test_get_base_images_sbom_components(base_images, base_images_digests, expec assert result == expected_result -def test_main_input_sbom_does_not_contain_formulation(tmp_path, mocker): - sbom_file = tmp_path / "sbom.json" - parsed_dockerfile = tmp_path / "parsed_dockerfile.json" - base_images_digests_raw_file = tmp_path / "base_images_digests.txt" - - # minimal input sbom file - sbom_file.write_text( - """{ - "project_name": "MyProject", - "version": "1.0", - "components": [] - }""" - ) - - # one builder images and one base image - parsed_dockerfile.write_text( - """ - { - "Stages": [ - { - "From": { - "Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab" +@pytest.mark.parametrize( + ["input_sbom", "parsed_dockerfile", "base_images_digests_lines", "expect_result"], + [ + pytest.param( + # minimal CycloneDX SBOM + { + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + }, + # one builder images and one base image + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + { + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + "formulation": [ + { + "components": [ + { + "type": "container", + "name": "quay.io/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + "properties": [ + { + "name": "konflux:container:is_builder_image:for_stage", + "value": "0", + } + ], + }, + { + "type": "container", + "name": "registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", + "properties": [ + { + "name": "konflux:container:is_base_image", + "value": "true", + } + ], + }, + ] } - }, - { - "From": { - "Image": "registry.access.redhat.com/ubi8/ubi:latest" + ], + }, + id="cyclonedx-no-formulation", + ), + pytest.param( + # minimal CycloneDX SBOM + { + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + }, + # two builder images, base from scratch + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + {"From": {"Scratch": True}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + { + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + "formulation": [ + { + "components": [ + { + "type": "container", + "name": "quay.io/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + "properties": [ + { + "name": "konflux:container:is_builder_image:for_stage", + "value": "0", + } + ], + }, + { + "type": "container", + "name": "registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", + # Note: the property is "is_builder_image", not "is_base_image". + # There is no base image, the base is from scratch. + "properties": [ + { + "name": "konflux:container:is_builder_image:for_stage", + "value": "1", + } + ], + }, + ] } - } - ] - } - """ - ) - base_images_digests_raw_file.write_text( - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab " - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941\n" - "registry.access.redhat.com/ubi8/ubi:latest " - "registry.access.redhat.com/ubi8/ubi" - ":latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - ) - - # mock the parsed args, to avoid testing parse_args function - mock_args = MagicMock() - mock_args.sbom = sbom_file - mock_args.parsed_dockerfile = parsed_dockerfile - mock_args.base_images_digests = base_images_digests_raw_file - mocker.patch("base_images_sbom_script.parse_args", return_value=mock_args) - - main() - - expected_output = { - "formulation": [ + ], + }, + id="cyclonedx-no-formulation-base-from-scratch", + ), + pytest.param( + # CycloneDX SBOM with .formulation { - "components": [ + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + "formulation": [ { - "type": "container", + "components": [ + { + "type": "library", + "name": "fresh", + "version": "0.5.2", + "purl": "pkg:npm/fresh@0.5.2", + } + ] + } + ], + }, + # two builder images, base from scratch + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + {"From": {"Scratch": True}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + { + "bomFormat": "CycloneDX", + "version": "1.0", + "components": [], + "formulation": [ + { + "components": [ + { + "type": "library", + "name": "fresh", + "version": "0.5.2", + "purl": "pkg:npm/fresh@0.5.2", + } + ] + }, + # The new formulation is appended to the existing formulations + { + "components": [ + { + "type": "container", + "name": "quay.io/mkosiarc_rhtap/single-container-app", + "purl": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + "properties": [ + { + "name": "konflux:container:is_builder_image:for_stage", + "value": "0", + } + ], + }, + { + "type": "container", + "name": "registry.access.redhat.com/ubi8/ubi", + "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", + # Note: the property is "is_builder_image", not "is_base_image". + # There is no base image, the base is from scratch. + "properties": [ + { + "name": "konflux:container:is_builder_image:for_stage", + "value": "1", + } + ], + }, + ] + }, + ], + }, + id="cyclonedx-has-formulation-base-from-scratch", + ), + pytest.param( + # Unknown SBOM format + {}, + # one builder images and one base image + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + ValueError(r"Unknown SBOM format"), + id="unknown-sbom-format", + ), + pytest.param( + # SPDX SBOM with no root package + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + }, + # one builder images and one base image + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + ValueError(r"Found 0 ROOTs: \[\]"), + id="spdx-missing-root", + ), + pytest.param( + # SPDX SBOM with too many roots + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-root1", + "name": "", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-root2", + "name": "", + "downloadLocation": "NOASSERTION", + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-root1", + }, + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-root2", + }, + ], + }, + # one builder images and one base image + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + ValueError(r"Found 2 ROOTs: \['SPDXRef-root1', 'SPDXRef-root2'\]"), + id="spdx-too-many-roots", + ), + pytest.param( + # minimal valid SPDX SBOM + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + ], + }, + # one builder images and one base image + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + ] + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url" - "=quay.io/mkosiarc_rhtap/single-container-app", - "properties": [ + "downloadLocation": "NOASSERTION", + "externalRefs": [ { - "name": "konflux:container:is_builder_image:for_stage", - "value": "0", + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"0"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", } ], }, { - "type": "container", + "SPDXID": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", - "properties": [ + "downloadLocation": "NOASSERTION", + "externalRefs": [ { - "name": "konflux:container:is_base_image", - "value": "true", + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", } ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_base_image","value":"true"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", + } + ], + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", }, + ], + }, + id="spdx-minimal", + ), + pytest.param( + # minimal valid SPDX SBOM + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + ], + }, + # two builder images, base from scratch + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + {"From": {"Scratch": True}}, ] - } - ] - } - - with sbom_file.open("r") as f: - sbom = json.load(f) - - assert "formulation" in sbom - assert expected_output["formulation"] == sbom["formulation"] - - -def test_main_input_sbom_does_not_contain_formulation_and_base_image_from_scratch(tmp_path, mocker): - sbom_file = tmp_path / "sbom.json" - parsed_dockerfile = tmp_path / "parsed_dockerfile.json" - base_images_digests_raw_file = tmp_path / "base_images_digests.txt" - - # minimal input sbom file - sbom_file.write_text( - """{ - "project_name": "MyProject", - "version": "1.0", - "components": [] - }""" - ) - - # two builder images and the last one is from scratch - parsed_dockerfile.write_text( - """ - { - "Stages": [ - { - "From": { - "Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab" - } - }, - { - "From": { - "Image": "registry.access.redhat.com/ubi8/ubi:latest" - } - }, - { - "From": { - "Scratch": true - } - } - ] - } - """ - ) - base_images_digests_raw_file.write_text( - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab " - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941\n" - "registry.access.redhat.com/ubi8/ubi:latest " - "registry.access.redhat.com/ubi8/ubi" - ":latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - ) - - # mock the parsed args, to avoid testing parse_args function - mock_args = MagicMock() - mock_args.sbom = sbom_file - mock_args.parsed_dockerfile = parsed_dockerfile - mock_args.base_images_digests = base_images_digests_raw_file - mocker.patch("base_images_sbom_script.parse_args", return_value=mock_args) - - main() - - expected_output = { - "formulation": [ + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output { - "components": [ + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, { - "type": "container", + "SPDXID": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url" - "=quay.io/mkosiarc_rhtap/single-container-app", - "properties": [ + "downloadLocation": "NOASSERTION", + "externalRefs": [ { - "name": "konflux:container:is_builder_image:for_stage", - "value": "0", + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"0"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", } ], }, { - "type": "container", + "SPDXID": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", - "properties": [ + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", + } + ], + "annotations": [ { - "name": "konflux:container:is_builder_image:for_stage", - "value": "1", + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"1"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", } ], }, - ] - } - ] - } - - with sbom_file.open("r") as f: - sbom = json.load(f) - - assert "formulation" in sbom - assert expected_output["formulation"] == sbom["formulation"] - - -def test_main_input_sbom_contains_formulation(tmp_path, mocker): - sbom_file = tmp_path / "sbom.json" - parsed_dockerfile = tmp_path / "parsed_dockerfile.json" - base_images_digests_raw_file = tmp_path / "base_images_digests.txt" - - # minimal sbom with existing formulation that contains components item - sbom_file.write_text( - """ - { - "project_name": "MyProject", - "version": "1.0", - "components": [], - "formulation": [ + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + ], + }, + id="spdx-minimal-base-from-scratch", + ), + pytest.param( + # SPDX SBOM with some existing packages { - "components": [ + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ { - "type": "library", - "name": "fresh", - "version": "0.5.2", - "purl": "pkg:npm/fresh@0.5.2" - } + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-some-runtime-dep", + "name": "SomeDependency", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-some-builddep", + "name": "BuildTool", + "downloadLocation": "NOASSERTION", + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-my-cool-image", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-some-runtime-dep", + }, + { + "spdxElementId": "SPDXRef-some-builddep", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + ], + }, + # two builder images, base from scratch + { + "Stages": [ + {"From": {"Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab"}}, + {"From": {"Image": "registry.access.redhat.com/ubi8/ubi:latest"}}, + {"From": {"Scratch": True}}, ] - } - ] - } - """ - ) + }, + # base image digests + [ + "quay.io/mkosiarc_rhtap/single-container-app:f2566ab quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941", + "registry.access.redhat.com/ubi8/ubi:latest registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + ], + # expected output + { + "SPDXID": "SPDXRef-Document", + "spdxVersion": "SPDX-2.3", + "name": "MyProject", + "documentNamespace": "http://example.com/uid-1234", + "packages": [ + { + "SPDXID": "SPDXRef-image-my-cool-image", + "name": "MyMainPackage", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-some-runtime-dep", + "name": "SomeDependency", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-some-builddep", + "name": "BuildTool", + "downloadLocation": "NOASSERTION", + }, + { + "SPDXID": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "name": "quay.io/mkosiarc_rhtap/single-container-app", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/single-container-app@sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io/mkosiarc_rhtap/single-container-app", + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"0"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", + } + ], + }, + { + "SPDXID": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "name": "registry.access.redhat.com/ubi8/ubi", + "downloadLocation": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac?repository_url=registry.access.redhat.com/ubi8/ubi", + } + ], + "annotations": [ + { + "annotator": "Tool: konflux:jsonencoded", + "comment": '{"name":"konflux:container:is_builder_image:for_stage","value":"1"}', + "annotationDate": "2024-12-09T12:00:00Z", + "annotationType": "OTHER", + } + ], + }, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Document", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-my-cool-image", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-some-runtime-dep", + }, + { + "spdxElementId": "SPDXRef-some-builddep", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-quay.io/mkosiarc_rhtap/single-container-app-9520a72cbb69edfca5cac88ea2a9e0e09142ec934952b9420d686e77765f002c", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + { + "spdxElementId": "SPDXRef-image-registry.access.redhat.com/ubi8/ubi-0f22256f634f8205fbd9c438c387ccf2d4859250e04104571c93fdb89a62bae1", + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": "SPDXRef-image-my-cool-image", + }, + ], + }, + id="spdx-nonminimal-base-from-scratch", + ), + ], +) +def test_main( + input_sbom: dict[str, Any], + parsed_dockerfile: dict[str, Any], + base_images_digests_lines: list[str], + expect_result: dict[str, Any] | Exception, + tmp_path: Path, + mocker: MockerFixture, +): + sbom_file = tmp_path / "sbom.json" + parsed_dockerfile_file = tmp_path / "parsed_dockerfile.json" + base_images_digests_raw_file = tmp_path / "base_images_digests.txt" - # two builder images and the last one is from scratch - parsed_dockerfile.write_text( - """ - { - "Stages": [ - { - "From": { - "Image": "quay.io/mkosiarc_rhtap/single-container-app:f2566ab" - } - }, - { - "From": { - "Image": "registry.access.redhat.com/ubi8/ubi:latest" - } - }, - { - "From": { - "Scratch": true - } - } - ] - } - """ - ) - base_images_digests_raw_file.write_text( - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab " - "quay.io/mkosiarc_rhtap/single-container-app:f2566ab@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941\n" - "registry.access.redhat.com/ubi8/ubi:latest " - "registry.access.redhat.com/ubi8/ubi" - ":latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - ) + sbom_file.write_text(json.dumps(input_sbom)) + parsed_dockerfile_file.write_text(json.dumps(parsed_dockerfile)) + base_images_digests_raw_file.write_text("\n".join(base_images_digests_lines)) # mock the parsed args, to avoid testing parse_args function mock_args = MagicMock() mock_args.sbom = sbom_file - mock_args.parsed_dockerfile = parsed_dockerfile + mock_args.parsed_dockerfile = parsed_dockerfile_file mock_args.base_images_digests = base_images_digests_raw_file mocker.patch("base_images_sbom_script.parse_args", return_value=mock_args) - main() + # mock datetime.now + mocker.patch( + "base_images_sbom_script._datetime_utc_now", + return_value=datetime.datetime(2024, 12, 9, 12, 0, 0, tzinfo=datetime.UTC), + ) - expected_output = { - "components": [ - { - "type": "container", - "name": "quay.io/mkosiarc_rhtap/single-container-app", - "purl": "pkg:oci/single-container-app@sha256" - ":8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941?repository_url=quay.io" - "/mkosiarc_rhtap/single-container-app", - "properties": [ - { - "name": "konflux:container:is_builder_image:for_stage", - "value": "0", - } - ], - }, - { - "type": "container", - "name": "registry.access.redhat.com/ubi8/ubi", - "purl": "pkg:oci/ubi@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac" - "?repository_url=registry.access.redhat.com/ubi8/ubi", - "properties": [ - { - "name": "konflux:container:is_builder_image:for_stage", - "value": "1", - } - ], - }, - ] - } + if not isinstance(expect_result, Exception): + main() + + with sbom_file.open("r") as f: + sbom = json.load(f) - with sbom_file.open("r") as f: - sbom = json.load(f) + assert sbom == expect_result - # we are appending an item to the formulation key, so it should be at the end - assert expected_output == sbom["formulation"][-1] + else: + with pytest.raises(type(expect_result), match=str(expect_result)): + main() @pytest.mark.parametrize( @@ -869,8 +1214,7 @@ def test_main_input_sbom_contains_formulation(tmp_path, mocker): [ # basic example ( - "registry.access.redhat.com/ubi8/ubi:latest@sha256" - ":627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + "registry.access.redhat.com/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", ParsedImage( repository="registry.access.redhat.com/ubi8/ubi", digest="sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", @@ -879,8 +1223,7 @@ def test_main_input_sbom_contains_formulation(tmp_path, mocker): ), # missing tag ( - "registry.access.redhat.com/ubi8/ubi:@sha256" - ":627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + "registry.access.redhat.com/ubi8/ubi:@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", ParsedImage( repository="registry.access.redhat.com/ubi8/ubi", digest="sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", @@ -889,8 +1232,7 @@ def test_main_input_sbom_contains_formulation(tmp_path, mocker): ), # registry with port ( - "some_registry_with_port:5000/ubi8/ubi:latest@sha256" - ":627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", + "some_registry_with_port:5000/ubi8/ubi:latest@sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", ParsedImage( repository="some_registry_with_port:5000/ubi8/ubi", digest="sha256:627867e53ad6846afba2dfbf5cef1d54c868a9025633ef0afd546278d4654eac", @@ -899,8 +1241,7 @@ def test_main_input_sbom_contains_formulation(tmp_path, mocker): ), # multiple path components ( - "quay.io/redhat-user-workloads/rh-acs-tenant/acs/collector:358b6cfb019e436d1fa61a09fcca04e081e1c993" - "@sha256:8e5d62b32a5bb6d73ca7f54941f00ee8807563ddcb424660894dea85ed1f109d", + "quay.io/redhat-user-workloads/rh-acs-tenant/acs/collector:358b6cfb019e436d1fa61a09fcca04e081e1c993@sha256:8e5d62b32a5bb6d73ca7f54941f00ee8807563ddcb424660894dea85ed1f109d", ParsedImage( repository="quay.io/redhat-user-workloads/rh-acs-tenant/acs/collector", digest="sha256:8e5d62b32a5bb6d73ca7f54941f00ee8807563ddcb424660894dea85ed1f109d", diff --git a/sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini b/sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini index 846a495..d1cc7ae 100644 --- a/sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini +++ b/sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini @@ -2,14 +2,19 @@ env_list = flake8,black,test [testenv:test] +basepython = 3.12 deps = -r requirements-test.txt -r requirements.txt commands = pytest test_base_images_sbom_script.py [testenv:flake8] deps = flake8 -commands = flake8 --max-line-length 120 base_images_sbom_script.py test_base_images_sbom_script.py +commands = flake8 base_images_sbom_script.py test_base_images_sbom_script.py [testenv:black] deps = black commands = black --line-length 120 --check --diff . + +[flake8] +# line-length check is useless since we have auto-formatting +extend-ignore = E501 diff --git a/sbom-utility-scripts/scripts/index-image-sbom-script/tox.ini b/sbom-utility-scripts/scripts/index-image-sbom-script/tox.ini index 3a0a1b5..bb77f80 100644 --- a/sbom-utility-scripts/scripts/index-image-sbom-script/tox.ini +++ b/sbom-utility-scripts/scripts/index-image-sbom-script/tox.ini @@ -2,6 +2,7 @@ env_list = flake8,black,test [testenv:test] +basepython = 3.12 deps = -r requirements-test.txt -r requirements.txt commands = pytest -s -vv test_image_index_sbom_script.py diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini index db6ddad..0c7ec59 100644 --- a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini @@ -2,6 +2,7 @@ env_list = flake8,black,test [testenv:test] +basepython = 3.12 deps = -r requirements-test.txt commands = pytest test_merge_cachi2_sboms.py