From 425808e2c79d6dacdc2bc6546c93eea8575f208c Mon Sep 17 00:00:00 2001 From: mkosiarc Date: Tue, 13 Aug 2024 12:35:12 +0200 Subject: [PATCH] Create sbom-utility-scripts image This image contains multiple python scripts that are used in the buildah tasks. This is done so that multiple steps in buildah tasks can be merged. The image contains scripts from the following stages: merge-syft-sboms merge-cachi2-sbom create-purl-sbom create-base-images-sbom NOTE: the "merge_syft_sbom" from the "merge-cachi2-sbom" stage was taken from https://github.com/containerbuildsystem/cachi2/blob/main/utils/merge_syft_sbom.py and renamed to "merge_cachi2_sboms.py". It will be removed from the cachi2 repo and image. STONEBLD-2608 Signed-off-by: mkosiarc --- ...l => build-sbom-utility-scripts-image.yml} | 22 +- base-images-sbom-script/Dockerfile | 7 - sbom-utility-scripts/Dockerfile | 11 + .../base-images-sbom-script}/README.md | 0 .../app/base_images_sbom_script.py | 0 .../app/requirements-test.in | 0 .../app/requirements-test.txt | 0 .../app/requirements.in | 0 .../app/requirements.txt | 0 .../app/test_base_images_sbom_script.py | 0 .../base-images-sbom-script}/app/tox.ini | 0 .../scripts/create_purl_sbom.py | 10 + .../merge_cachi2_sboms.py | 180 ++++ .../requirements-test.in | 1 + .../requirements-test.txt | 22 + .../test_data/cachi2.bom.json | 227 +++++ .../test_data/merged.bom.json | 327 +++++++ .../test_data/syft.bom.json | 903 ++++++++++++++++++ .../test_merge_cachi2_sboms.py | 133 +++ .../scripts/merge-cachi2-sboms-script/tox.ini | 15 + .../scripts/merge_syft_sboms.py | 27 + 21 files changed, 1870 insertions(+), 15 deletions(-) rename .github/workflows/{build-base-images-sbom-script-image.yml => build-sbom-utility-scripts-image.yml} (65%) delete mode 100644 base-images-sbom-script/Dockerfile create mode 100644 sbom-utility-scripts/Dockerfile rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/README.md (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/base_images_sbom_script.py (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/requirements-test.in (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/requirements-test.txt (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/requirements.in (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/requirements.txt (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/test_base_images_sbom_script.py (100%) rename {base-images-sbom-script => sbom-utility-scripts/scripts/base-images-sbom-script}/app/tox.ini (100%) create mode 100644 sbom-utility-scripts/scripts/create_purl_sbom.py create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/merge_cachi2_sboms.py create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.in create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.txt create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/cachi2.bom.json create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/merged.bom.json create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/syft.bom.json create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_merge_cachi2_sboms.py create mode 100644 sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini create mode 100644 sbom-utility-scripts/scripts/merge_syft_sboms.py diff --git a/.github/workflows/build-base-images-sbom-script-image.yml b/.github/workflows/build-sbom-utility-scripts-image.yml similarity index 65% rename from .github/workflows/build-base-images-sbom-script-image.yml rename to .github/workflows/build-sbom-utility-scripts-image.yml index 6094543..7f16a4d 100644 --- a/.github/workflows/build-base-images-sbom-script-image.yml +++ b/.github/workflows/build-sbom-utility-scripts-image.yml @@ -1,22 +1,22 @@ -name: Build base images sbom script image +name: Build sbom utility scripts image on: push: branches: - main paths: - - base-images-sbom-script/** + - sbom-utility-scripts/** pull_request: branches: - main paths: - - base-images-sbom-script/** + - sbom-utility-scripts/** env: REGISTRY: quay.io/redhat-appstudio - IMAGE_NAME: base-images-sbom-script + IMAGE_NAME: sbom-utility-scripts-image jobs: build: @@ -31,10 +31,16 @@ jobs: with: python-version: 3.11 - - name: Run tox checks + - name: Run tox checks for base-images-sbom-script run: | python3 -m pip install tox - cd ./base-images-sbom-script/app/ + cd ./sbom-utility-scripts/scripts/base-images-sbom-script/app/ + tox + + - name: Run tox checks for merge-cachi2-sboms-script + run: | + python3 -m pip install tox + cd ./sbom-utility-scripts/scripts/merge-cachi2-sboms-script/ tox - name: Build Image @@ -43,9 +49,9 @@ jobs: with: image: ${{ env.IMAGE_NAME }} tags: ${{ github.sha }} - context: ./base-images-sbom-script + context: ./sbom-utility-scripts containerfiles: | - ./base-images-sbom-script/Dockerfile + ./sbom-utility-scripts/Dockerfile - name: Push to Quay if: github.event_name == 'push' # don't push image from PR diff --git a/base-images-sbom-script/Dockerfile b/base-images-sbom-script/Dockerfile deleted file mode 100644 index ace75d1..0000000 --- a/base-images-sbom-script/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM registry.access.redhat.com/ubi9/python-39:1-192.1722518946@sha256:0176b477075984d5a502253f951d2502f0763c551275f9585ac515b9f241d73d - -WORKDIR /app -COPY app/requirements.txt /app -COPY app/base_images_sbom_script.py /app - -RUN pip3 install -r requirements.txt diff --git a/sbom-utility-scripts/Dockerfile b/sbom-utility-scripts/Dockerfile new file mode 100644 index 0000000..1c92d24 --- /dev/null +++ b/sbom-utility-scripts/Dockerfile @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi9/python-39:1-192.1722518946@sha256:0176b477075984d5a502253f951d2502f0763c551275f9585ac515b9f241d73d + +WORKDIR /scripts + +COPY scripts/merge_syft_sboms.py /scripts +COPY scripts/merge-cachi2-sboms-script/merge_cachi2_sboms.py /scripts +COPY scripts/create_purl_sbom.py /scripts +COPY scripts/base-images-sbom-script/app/base_images_sbom_script.py /scripts +COPY scripts/base-images-sbom-script/app/requirements.txt /scripts + +RUN pip3 install -r requirements.txt diff --git a/base-images-sbom-script/README.md b/sbom-utility-scripts/scripts/base-images-sbom-script/README.md similarity index 100% rename from base-images-sbom-script/README.md rename to sbom-utility-scripts/scripts/base-images-sbom-script/README.md diff --git a/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 similarity index 100% rename from base-images-sbom-script/app/base_images_sbom_script.py rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/base_images_sbom_script.py diff --git a/base-images-sbom-script/app/requirements-test.in b/sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements-test.in similarity index 100% rename from base-images-sbom-script/app/requirements-test.in rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements-test.in diff --git a/base-images-sbom-script/app/requirements-test.txt b/sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements-test.txt similarity index 100% rename from base-images-sbom-script/app/requirements-test.txt rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements-test.txt diff --git a/base-images-sbom-script/app/requirements.in b/sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements.in similarity index 100% rename from base-images-sbom-script/app/requirements.in rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements.in diff --git a/base-images-sbom-script/app/requirements.txt b/sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements.txt similarity index 100% rename from base-images-sbom-script/app/requirements.txt rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/requirements.txt diff --git a/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 similarity index 100% rename from base-images-sbom-script/app/test_base_images_sbom_script.py rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/test_base_images_sbom_script.py diff --git a/base-images-sbom-script/app/tox.ini b/sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini similarity index 100% rename from base-images-sbom-script/app/tox.ini rename to sbom-utility-scripts/scripts/base-images-sbom-script/app/tox.ini diff --git a/sbom-utility-scripts/scripts/create_purl_sbom.py b/sbom-utility-scripts/scripts/create_purl_sbom.py new file mode 100644 index 0000000..bb22db5 --- /dev/null +++ b/sbom-utility-scripts/scripts/create_purl_sbom.py @@ -0,0 +1,10 @@ +import json + +with open("./sbom-cyclonedx.json") as f: + cyclonedx_sbom = json.load(f) + +purls = [{"purl": component["purl"]} for component in cyclonedx_sbom.get("components", []) if "purl" in component] +purl_content = {"image_contents": {"dependencies": purls}} + +with open("sbom-purl.json", "w") as output_file: + json.dump(purl_content, output_file, indent=4) diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/merge_cachi2_sboms.py b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/merge_cachi2_sboms.py new file mode 100644 index 0000000..3473862 --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/merge_cachi2_sboms.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +import json +from argparse import ArgumentParser +from typing import Any, Callable +from urllib.parse import quote_plus, urlsplit + + +def _is_syft_local_golang_component(component: dict) -> bool: + """ + Check if a Syft Golang reported component is a local replacement. + + Local replacements are reported in a very different way by Cachi2, which is why the same + reports by Syft should be removed. + """ + return component.get("purl", "").startswith("pkg:golang") and ( + component.get("name", "").startswith(".") or component.get("version", "") == "(devel)" + ) + + +def _is_cachi2_non_registry_dependency(component: dict) -> bool: + """ + Check if Cachi2 component was fetched from a VCS or a direct file location. + + Cachi2 reports non-registry components in a different way from Syft, so the reports from + Syft need to be removed. + + Unfortunately, there's no way to determine which components are non-registry by looking + at the Syft report alone. This function is meant to create a list of non-registry components + from Cachi2's SBOM, then remove the corresponding ones reported by Syft for the merged SBOM. + + Note that this function is only applicable for PyPI or NPM components. + """ + purl = component.get("purl", "") + + return (purl.startswith("pkg:pypi") or purl.startswith("pkg:npm")) and ( + "vcs_url=" in purl or "download_url=" in purl + ) + + +def _unique_key_cachi2(component: dict) -> str: + """ + Create a unique key from Cachi2 reported components. + + This is done by taking a purl and removing any qualifiers and subpaths. + + See https://github.com/package-url/purl-spec/tree/master#purl for more info on purls. + """ + url = urlsplit(component["purl"]) + return url.scheme + ":" + url.path + + +def _unique_key_syft(component: dict) -> str: + """ + Create a unique key for Syft reported components. + + This is done by taking a lowercase namespace/name, and URL encoding the version. + + Syft does not set any qualifier for NPM, Pip or Golang, so there's no need to remove them + as done in _unique_key_cachi2. + + If a Syft component lacks a purl (e.g. type OS), we'll use its name and version instead. + """ + if "purl" not in component: + return component.get("name", "") + "@" + component.get("version", "") + + if "@" in component["purl"]: + name, version = component["purl"].split("@") + + if name.startswith("pkg:pypi"): + name = name.lower() + + if name.startswith("pkg:golang"): + version = quote_plus(version) + + return f"{name}@{version}" + else: + return component["purl"] + + +def _get_syft_component_filter(cachi_sbom_components: list[dict[str, Any]]) -> Callable: + """ + Get a function that filters out Syft components for the merged SBOM. + + This function currently considers a Syft component as a duplicate/removable if: + - it has the same key as a Cachi2 component + - it is a local Golang replacement + - is a non-registry component also reported by Cachi2 + + Note that for the last bullet, we can only rely on the Pip dependency's name to find a + duplicate. This is because Cachi2 does not report a non-PyPI Pip dependency's version. + + Even though multiple versions of a same dependency can be available in the same project, + we are removing all Syft instances by name only because Cachi2 will report them correctly, + given that it scans all the source code properly and the image is built hermetically. + """ + cachi2_non_registry_components = [ + component["name"] for component in cachi_sbom_components if _is_cachi2_non_registry_dependency(component) + ] + + cachi2_indexed_components = {_unique_key_cachi2(component): component for component in cachi_sbom_components} + + def is_duplicate_non_registry_component(component: dict[str, Any]) -> bool: + return component["name"] in cachi2_non_registry_components + + def component_is_duplicated(component: dict[str, Any]) -> bool: + key = _unique_key_syft(component) + + return ( + _is_syft_local_golang_component(component) + or is_duplicate_non_registry_component(component) + or key in cachi2_indexed_components.keys() + ) + + return component_is_duplicated + + +def _merge_tools_metadata(syft_sbom: dict[Any, Any], cachi2_sbom: dict[Any, Any]) -> None: + """Merge the content of tools in the metadata section of the SBOM. + + With CycloneDX 1.5, a new format for specifying tools was introduced, and the format from 1.4 + was marked as deprecated. + + This function aims to support both formats in the Syft SBOM. We're assuming the Cachi2 SBOM + was generated with the same version as this script, and it will be in the older format. + """ + syft_tools = syft_sbom["metadata"]["tools"] + cachi2_tools = cachi2_sbom["metadata"]["tools"] + + if isinstance(syft_tools, dict): + components = [] + + for t in cachi2_tools: + components.append( + { + "author": t["vendor"], + "name": t["name"], + "type": "application", + } + ) + + syft_tools["components"].extend(components) + elif isinstance(syft_tools, list): + syft_tools.extend(cachi2_tools) + else: + raise RuntimeError( + "The .metadata.tools JSON key is in an unexpected format. " + f"Expected dict or list, got {type(syft_tools)}." + ) + + +def merge_sboms(cachi2_sbom_path: str, syft_sbom_path: str) -> str: + """Merge Cachi2 components into the Syft SBOM while removing duplicates.""" + with open(cachi2_sbom_path) as file: + cachi2_sbom = json.load(file) + + with open(syft_sbom_path) as file: + syft_sbom = json.load(file) + + is_duplicate_component = _get_syft_component_filter(cachi2_sbom["components"]) + + filtered_syft_components = [c for c in syft_sbom.get("components", []) if not is_duplicate_component(c)] + + syft_sbom["components"] = filtered_syft_components + cachi2_sbom["components"] + + _merge_tools_metadata(syft_sbom, cachi2_sbom) + + return json.dumps(syft_sbom, indent=2) + + +if __name__ == "__main__": + parser = ArgumentParser() + + parser.add_argument("cachi2_sbom_path") + parser.add_argument("syft_sbom_path") + + args = parser.parse_args() + + merged_sbom = merge_sboms(args.cachi2_sbom_path, args.syft_sbom_path) + + print(merged_sbom) diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.in b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.in new file mode 100644 index 0000000..e079f8a --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.in @@ -0,0 +1 @@ +pytest diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.txt b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.txt new file mode 100644 index 0000000..18bd5a4 --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/requirements-test.txt @@ -0,0 +1,22 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --generate-hashes --output-file=requirements-test.txt requirements-test.in +# +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +packaging==24.1 \ + --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \ + --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124 + # via pytest +pluggy==1.5.0 \ + --hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \ + --hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669 + # via pytest +pytest==8.3.2 \ + --hash=sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5 \ + --hash=sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce + # via -r requirements-test.in diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/cachi2.bom.json b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/cachi2.bom.json new file mode 100644 index 0000000..846f639 --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/cachi2.bom.json @@ -0,0 +1,227 @@ +{ + "bomFormat": "CycloneDX", + "components": [ + { + "name": "aiowsgi", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "0.8", + "purl": "pkg:pypi/aiowsgi@0.8", + "type": "library" + }, + { + "name": "appr", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:pypi/appr?checksum=sha256:ee6a0a38bed8cff46a562ed3620bc453141a02262ab0c8dd055824af2829ee5c&download_url=https://github.com/quay/appr/archive/37ff9a487a54ad41b59855ecd76ee092fe206a84.zip", + "type": "library" + }, + { + "name": "archive/tar", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:golang/archive/tar?type=package", + "type": "library" + }, + { + "name": "cachi2", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "0.0.1", + "purl": "pkg:pypi/cachi2@0.0.1?vcs_url=git%2Bssh://git%40github.com/containerbuildsystem/cachi2%40fc0d6079c2dc9b2a491c0848e550ad3509986110", + "type": "library" + }, + { + "name": "cachito-npm-without-deps", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:npm/cachito-npm-without-deps?vcs_url=git%2Bhttps://github.com/cachito-testing/cachito-npm-without-deps.git%402f0ce1d7b1f8b35572d919428b965285a69583f6", + "type": "library" + }, + { + "name": "code.gitea.io/sdk/gitea", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.15.1", + "purl": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1?type=module", + "type": "library" + }, + { + "name": "code.gitea.io/sdk/gitea", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.15.1", + "purl": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1?type=package", + "type": "library" + }, + { + "name": "fecha", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:npm/fecha?checksum=sha512:8ae71e98d68e38e1f6e4c629187684dd85e4dc96647c7219b1dd189598ea52865e947f0ad94a7001fa8fb5eccf58467fe34ad10066e831af3374120134604bd5&download_url=https://github.com/taylorhakes/fecha/archive/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz", + "type": "library" + }, + { + "name": "github.com/docker/cli/cli/config", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v23.0.0-rc.3+incompatible", + "purl": "pkg:golang/github.com/docker/cli/cli/config@v23.0.0-rc.3%2Bincompatible?type=package", + "type": "library" + }, + { + "name": "github.com/docker/cli", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v23.0.0-rc.3+incompatible", + "purl": "pkg:golang/github.com/docker/cli@v23.0.0-rc.3%2Bincompatible?type=module", + "type": "library" + }, + { + "name": "knative.dev/pkg/metrics", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230125083639-408ad0773f47", + "purl": "pkg:golang/knative.dev/pkg/metrics@v0.0.0-20230125083639-408ad0773f47?type=package", + "type": "library" + }, + { + "name": "knative.dev/pkg", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230125083639-408ad0773f47", + "purl": "pkg:golang/knative.dev/pkg@v0.0.0-20230125083639-408ad0773f47?type=module", + "type": "library" + }, + { + "name": "github.com/redhat-appstudio/build-service", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230503110830-d1a9e858489d", + "purl": "pkg:golang/github.com/redhat-appstudio/build-service@v0.0.0-20230503110830-d1a9e858489d?type=module", + "type": "library" + }, + { + "name": "github.com/redhat-appstudio/build-service", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230503110830-d1a9e858489d", + "purl": "pkg:golang/github.com/redhat-appstudio/build-service@v0.0.0-20230503110830-d1a9e858489d?type=package", + "type": "library" + }, + { + "name": "github.com/cachito-testing/gomod-pandemonium/terminaltor", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v1.0.0", + "purl": "pkg:golang/github.com/cachito-testing/gomod-pandemonium/terminaltor@v1.0.0?type=module", + "type": "library" + }, + { + "name": "github.com/cachito-testing/gomod-pandemonium/terminaltor", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v1.0.0", + "purl": "pkg:golang/github.com/cachito-testing/gomod-pandemonium/terminaltor@v1.0.0?type=package", + "type": "library" + }, + { + "name": "PyYAML", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "6.0", + "purl": "pkg:pypi/pyyaml@6.0", + "type": "library" + }, + { + "name": "test_package_cachi2", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "1.0.0", + "purl": "pkg:pypi/test-package-cachi2@1.0.0?vcs_url=git%2Bssh://git%40github.com/brunoapimentel/pip-e2e-test.git%40294df352deed835cf703ae8a799926418ae5fd3b", + "type": "library" + } + ], + "metadata": { + "tools": [ + { + "vendor": "red hat", + "name": "cachi2" + } + ] + }, + "specVersion": "1.4", + "version": 1 +} diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/merged.bom.json b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/merged.bom.json new file mode 100644 index 0000000..dc876ce --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/merged.bom.json @@ -0,0 +1,327 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:4370d1ba-7643-4579-8313-bc715da2fa90", + "version": 1, + "metadata": { + "timestamp": "2023-05-03T18:19:41Z", + "tools": { + "components": [ + { + "type": "application", + "author": "anchore", + "name": "syft", + "version": "0.100.0" + }, + { + "type": "application", + "author": "red hat", + "name": "cachi2" + } + ] + }, + "component": { + "bom-ref": "6b8edfe5f2756e0", + "type": "file", + "name": "/var/lib/containers/storage/vfs/dir/517aef0ffe20db360d19aa475dbbfbe03f452f53403881a31f9a475c83af788b" + } + }, + "components": [ + { + "bom-ref": "pkg:rpm/rhel/bash@4.4.20-4.el8_6?arch=x86_64&upstream=bash-4.4.20-4.el8_6.src.rpm&distro=rhel-8.7&package-id=5b17560161ffa050", + "type": "library", + "publisher": "Red Hat, Inc.", + "name": "bash", + "version": "4.4.20-4.el8_6", + "cpe": "cpe:2.3:a:redhat:bash:4.4.20-4.el8_6:*:*:*:*:*:*:*", + "purl": "pkg:rpm/rhel/bash@4.4.20-4.el8_6?arch=x86_64&upstream=bash-4.4.20-4.el8_6.src.rpm&distro=rhel-8.7", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "rpmdb-cataloger" + }, + { + "name": "syft:package:metadataType", + "value": "RpmdbMetadata" + }, + { + "name": "syft:package:type", + "value": "rpm" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:bash:bash:4.4.20-4.el8_6:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "var/lib/rpm/Packages" + }, + { + "name": "syft:metadata:release", + "value": "4.el8_6" + }, + { + "name": "syft:metadata:size", + "value": "6861444" + }, + { + "name": "syft:metadata:sourceRpm", + "value": "bash-4.4.20-4.el8_6.src.rpm" + } + ] + }, + { + "type": "operating-system", + "name": "rhel", + "version": "8.7", + "description": "Red Hat Enterprise Linux 8.7 (Ootpa)", + "cpe": "cpe:/o:redhat:enterprise_linux:8::baseos", + "swid": { + "tagId": "rhel", + "name": "rhel", + "version": "8.7" + }, + "externalReferences": [ + { + "url": "https://bugzilla.redhat.com/", + "type": "issue-tracker" + }, + { + "url": "https://www.redhat.com/", + "type": "website" + } + ], + "properties": [ + { + "name": "syft:distro:id", + "value": "rhel" + }, + { + "name": "syft:distro:idLike:0", + "value": "fedora" + }, + { + "name": "syft:distro:prettyName", + "value": "Red Hat Enterprise Linux 8.7 (Ootpa)" + }, + { + "name": "syft:distro:versionID", + "value": "8.7" + } + ] + }, + { + "name": "aiowsgi", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "0.8", + "purl": "pkg:pypi/aiowsgi@0.8", + "type": "library" + }, + { + "name": "appr", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:pypi/appr?checksum=sha256:ee6a0a38bed8cff46a562ed3620bc453141a02262ab0c8dd055824af2829ee5c&download_url=https://github.com/quay/appr/archive/37ff9a487a54ad41b59855ecd76ee092fe206a84.zip", + "type": "library" + }, + { + "name": "archive/tar", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:golang/archive/tar?type=package", + "type": "library" + }, + { + "name": "cachi2", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "0.0.1", + "purl": "pkg:pypi/cachi2@0.0.1?vcs_url=git%2Bssh://git%40github.com/containerbuildsystem/cachi2%40fc0d6079c2dc9b2a491c0848e550ad3509986110", + "type": "library" + }, + { + "name": "cachito-npm-without-deps", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:npm/cachito-npm-without-deps?vcs_url=git%2Bhttps://github.com/cachito-testing/cachito-npm-without-deps.git%402f0ce1d7b1f8b35572d919428b965285a69583f6", + "type": "library" + }, + { + "name": "code.gitea.io/sdk/gitea", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.15.1", + "purl": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1?type=module", + "type": "library" + }, + { + "name": "code.gitea.io/sdk/gitea", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.15.1", + "purl": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1?type=package", + "type": "library" + }, + { + "name": "fecha", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "purl": "pkg:npm/fecha?checksum=sha512:8ae71e98d68e38e1f6e4c629187684dd85e4dc96647c7219b1dd189598ea52865e947f0ad94a7001fa8fb5eccf58467fe34ad10066e831af3374120134604bd5&download_url=https://github.com/taylorhakes/fecha/archive/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz", + "type": "library" + }, + { + "name": "github.com/docker/cli/cli/config", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v23.0.0-rc.3+incompatible", + "purl": "pkg:golang/github.com/docker/cli/cli/config@v23.0.0-rc.3%2Bincompatible?type=package", + "type": "library" + }, + { + "name": "github.com/docker/cli", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v23.0.0-rc.3+incompatible", + "purl": "pkg:golang/github.com/docker/cli@v23.0.0-rc.3%2Bincompatible?type=module", + "type": "library" + }, + { + "name": "knative.dev/pkg/metrics", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230125083639-408ad0773f47", + "purl": "pkg:golang/knative.dev/pkg/metrics@v0.0.0-20230125083639-408ad0773f47?type=package", + "type": "library" + }, + { + "name": "knative.dev/pkg", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230125083639-408ad0773f47", + "purl": "pkg:golang/knative.dev/pkg@v0.0.0-20230125083639-408ad0773f47?type=module", + "type": "library" + }, + { + "name": "github.com/redhat-appstudio/build-service", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230503110830-d1a9e858489d", + "purl": "pkg:golang/github.com/redhat-appstudio/build-service@v0.0.0-20230503110830-d1a9e858489d?type=module", + "type": "library" + }, + { + "name": "github.com/redhat-appstudio/build-service", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v0.0.0-20230503110830-d1a9e858489d", + "purl": "pkg:golang/github.com/redhat-appstudio/build-service@v0.0.0-20230503110830-d1a9e858489d?type=package", + "type": "library" + }, + { + "name": "github.com/cachito-testing/gomod-pandemonium/terminaltor", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v1.0.0", + "purl": "pkg:golang/github.com/cachito-testing/gomod-pandemonium/terminaltor@v1.0.0?type=module", + "type": "library" + }, + { + "name": "github.com/cachito-testing/gomod-pandemonium/terminaltor", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "v1.0.0", + "purl": "pkg:golang/github.com/cachito-testing/gomod-pandemonium/terminaltor@v1.0.0?type=package", + "type": "library" + }, + { + "name": "PyYAML", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "6.0", + "purl": "pkg:pypi/pyyaml@6.0", + "type": "library" + }, + { + "name": "test_package_cachi2", + "properties": [ + { + "name": "cachi2:found_by", + "value": "cachi2" + } + ], + "version": "1.0.0", + "purl": "pkg:pypi/test-package-cachi2@1.0.0?vcs_url=git%2Bssh://git%40github.com/brunoapimentel/pip-e2e-test.git%40294df352deed835cf703ae8a799926418ae5fd3b", + "type": "library" + } + ] +} diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/syft.bom.json b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/syft.bom.json new file mode 100644 index 0000000..6bf35bc --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_data/syft.bom.json @@ -0,0 +1,903 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:4370d1ba-7643-4579-8313-bc715da2fa90", + "version": 1, + "metadata": { + "timestamp": "2023-05-03T18:19:41Z", + "tools": { + "components": [ + { + "type": "application", + "author": "anchore", + "name": "syft", + "version": "0.100.0" + } + ] + }, + "component": { + "bom-ref": "6b8edfe5f2756e0", + "type": "file", + "name": "/var/lib/containers/storage/vfs/dir/517aef0ffe20db360d19aa475dbbfbe03f452f53403881a31f9a475c83af788b" + } + }, + "components": [ + { + "bom-ref": "pkg:pypi/aiowsgi@0.8?package-id=3038521054a801b2", + "type": "library", + "author": "Gael Pasgrimaud ", + "name": "aiowsgi", + "version": "0.8", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "cpe": "cpe:2.3:a:gael_pasgrimaud:python-aiowsgi:0.8:*:*:*:*:*:*:*", + "purl": "pkg:pypi/aiowsgi@0.8", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "python-package-cataloger" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:gael_pasgrimaud:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-aiowsgi:python-aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-aiowsgi:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_aiowsgi:python-aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_aiowsgi:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:gael_pasgrimaud:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:aiowsgi:python-aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:aiowsgi:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-aiowsgi:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_aiowsgi:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python-aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:gael:python-aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:gael:python_aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:aiowsgi:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:gael:aiowsgi:0.8:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/lib/python3.9/site-packages/aiowsgi-0.8.dist-info/METADATA" + }, + { + "name": "syft:location:1:path", + "value": "opt/app-root/lib/python3.9/site-packages/aiowsgi-0.8.dist-info/METADATA" + }, + { + "name": "syft:location:2:path", + "value": "opt/app-root/lib/python3.9/site-packages/aiowsgi-0.8.dist-info/RECORD" + }, + { + "name": "syft:location:3:path", + "value": "opt/app-root/lib/python3.9/site-packages/aiowsgi-0.8.dist-info/top_level.txt" + } + ] + }, + { + "bom-ref": "pkg:pypi/appr@0.7.4?package-id=86c12ef7e3e40483", + "type": "library", + "author": "Antoine Legrand <2t.antoine@gmail.com>", + "name": "appr", + "version": "0.7.4", + "cpe": "cpe:2.3:a:antoine_legrand:python-appr:0.7.4:*:*:*:*:*:*:*", + "purl": "pkg:pypi/appr@0.7.4", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "python-package-cataloger" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:antoine_legrand:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-appr:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-appr:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_appr:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_appr:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t-antoine:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t-antoine:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t_antoine:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t_antoine:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:antoine_legrand:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:appr:python-appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:appr:python_appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-appr:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_appr:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t-antoine:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:2t_antoine:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:appr:appr:0.7.4:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/lib/python3.9/site-packages/appr-0.7.4-py3.9.egg-info/PKG-INFO" + }, + { + "name": "syft:location:1:path", + "value": "opt/app-root/lib/python3.9/site-packages/appr-0.7.4-py3.9.egg-info/PKG-INFO" + }, + { + "name": "syft:location:2:path", + "value": "opt/app-root/lib/python3.9/site-packages/appr-0.7.4-py3.9.egg-info/top_level.txt" + } + ] + }, + { + "bom-ref": "pkg:rpm/rhel/bash@4.4.20-4.el8_6?arch=x86_64&upstream=bash-4.4.20-4.el8_6.src.rpm&distro=rhel-8.7&package-id=5b17560161ffa050", + "type": "library", + "publisher": "Red Hat, Inc.", + "name": "bash", + "version": "4.4.20-4.el8_6", + "cpe": "cpe:2.3:a:redhat:bash:4.4.20-4.el8_6:*:*:*:*:*:*:*", + "purl": "pkg:rpm/rhel/bash@4.4.20-4.el8_6?arch=x86_64&upstream=bash-4.4.20-4.el8_6.src.rpm&distro=rhel-8.7", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "rpmdb-cataloger" + }, + { + "name": "syft:package:metadataType", + "value": "RpmdbMetadata" + }, + { + "name": "syft:package:type", + "value": "rpm" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:bash:bash:4.4.20-4.el8_6:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "var/lib/rpm/Packages" + }, + { + "name": "syft:metadata:release", + "value": "4.el8_6" + }, + { + "name": "syft:metadata:size", + "value": "6861444" + }, + { + "name": "syft:metadata:sourceRpm", + "value": "bash-4.4.20-4.el8_6.src.rpm" + } + ] + }, + { + "bom-ref": "pkg:pypi/cachi2@0.0.post1+gdfd2180.d20230704?package-id=d6b54ce4d7c02efb", + "type": "library", + "name": "cachi2", + "version": "0.0.post1+gdfd2180.d20230704", + "cpe": "cpe:2.3:a:python-cachi2:python-cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*", + "purl": "pkg:pypi/cachi2@0.0.post1+gdfd2180.d20230704", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "python-package-cataloger" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-cachi2:python_cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_cachi2:python-cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_cachi2:python_cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachi2:python-cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachi2:python_cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-cachi2:cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python-cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python_cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_cachi2:cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachi2:cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:cachi2:0.0.post1\\+gdfd2180.d20230704:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "src/cachi2.egg-info/PKG-INFO" + }, + { + "name": "syft:location:1:path", + "value": "src/cachi2.egg-info/top_level.txt" + } + ] + }, + { + "bom-ref": "pkg:npm/cachito-npm-without-deps%40git+https:/github.com/cachito-testing/cachito-npm-without-deps.git%232f0ce1d7b1f8b35572d919428b965285a69583f6?package-id=f381e97e072d545c", + "type": "library", + "name": "cachito-npm-without-deps", + "version": "git+https://github.com/cachito-testing/cachito-npm-without-deps.git#2f0ce1d7b1f8b35572d919428b965285a69583f6", + "cpe": "cpe:2.3:a:cachito-npm-without-deps:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*", + "purl": "pkg:npm/cachito-npm-without-deps@git+https://github.com/cachito-testing/cachito-npm-without-deps.git%232f0ce1d7b1f8b35572d919428b965285a69583f6", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "javascript-lock-cataloger" + }, + { + "name": "syft:package:language", + "value": "javascript" + }, + { + "name": "syft:package:type", + "value": "npm" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito-npm-without-deps:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm_without_deps:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm_without_deps:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito-npm-without:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito-npm-without:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm_without:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm_without:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito-npm:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito-npm:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito_npm:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:cachito:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:*:cachito-npm-without-deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:*:cachito_npm_without_deps:git\\+https\\:\\/\\/github.com\\/cachito-testing\\/cachito-npm-without-deps.git\\#2f0ce1d7b1f8b35572d919428b965285a69583f6:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/src/package-lock.json" + } + ] + }, + { + "bom-ref": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1?package-id=bd0f7d1b7175ede9", + "type": "library", + "name": "code.gitea.io/sdk/gitea", + "version": "v0.15.1", + "cpe": "cpe:2.3:a:sdk:gitea:v0.15.1:*:*:*:*:*:*:*", + "purl": "pkg:golang/code.gitea.io/sdk/gitea@v0.15.1", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-binary-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangBinMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:location:0:path", + "value": "manager" + }, + { + "name": "syft:metadata:architecture", + "value": "amd64" + }, + { + "name": "syft:metadata:goCompiledVersion", + "value": "go1.18.4" + }, + { + "name": "syft:metadata:h1Digest", + "value": "h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=" + } + ] + }, + { + "bom-ref": "pkg:npm/fecha%40https:/github.com/taylorhakes/fecha/archive/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz?package-id=b989ad78c681a9ab", + "type": "library", + "name": "fecha", + "version": "https://github.com/taylorhakes/fecha/archive/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz", + "cpe": "cpe:2.3:a:fecha:fecha:https\\:\\/\\/github.com\\/taylorhakes\\/fecha\\/archive\\/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz:*:*:*:*:*:*:*", + "purl": "pkg:npm/fecha@https://github.com/taylorhakes/fecha/archive/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "javascript-lock-cataloger" + }, + { + "name": "syft:package:language", + "value": "javascript" + }, + { + "name": "syft:package:type", + "value": "npm" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:*:fecha:https\\:\\/\\/github.com\\/taylorhakes\\/fecha\\/archive\\/91680e4db1415fea33eac878cfd889c80a7b55c7.tar.gz:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/src/package-lock.json" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/docker/cli@v23.0.0-rc.3+incompatible?package-id=53a1b0df25cf6062", + "type": "library", + "name": "github.com/docker/cli", + "version": "v23.0.0-rc.3+incompatible", + "cpe": "cpe:2.3:a:docker:cli:v23.0.0-rc.3\\+incompatible:*:*:*:*:*:*:*", + "purl": "pkg:golang/github.com/docker/cli@v23.0.0-rc.3+incompatible", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-binary-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangBinMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:location:0:path", + "value": "manager" + }, + { + "name": "syft:metadata:architecture", + "value": "amd64" + }, + { + "name": "syft:metadata:goCompiledVersion", + "value": "go1.18.4" + }, + { + "name": "syft:metadata:h1Digest", + "value": "h1:OPrcUDrpApVrVZsZByISt51zID7HT0VxDKa/onvUzOo=" + } + ] + }, + { + "bom-ref": "pkg:golang/knative.dev/pkg@v0.0.0-20230125083639-408ad0773f47?package-id=d90b5a02ef9f5ceb", + "type": "library", + "name": "knative.dev/pkg", + "version": "v0.0.0-20230125083639-408ad0773f47", + "purl": "pkg:golang/knative.dev/pkg@v0.0.0-20230125083639-408ad0773f47", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-binary-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangBinMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:location:0:path", + "value": "manager" + }, + { + "name": "syft:metadata:architecture", + "value": "amd64" + }, + { + "name": "syft:metadata:goCompiledVersion", + "value": "go1.18.4" + }, + { + "name": "syft:metadata:h1Digest", + "value": "h1:zlRO7wXOHVYgKvsC3nIaYGqeQGlLJL8EIUY30Rh37Is=" + } + ] + }, + { + "bom-ref": "pkg:golang/./terminaltor?package-id=ead05afcd869908f", + "type": "library", + "name": "./terminaltor", + "purl": "pkg:golang/./terminaltor", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-mod-file-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/src/go.mod" + } + ] + }, + { + "bom-ref": "pkg:golang/./terminaltor@(devel)?package-id=f75768aa294abccc", + "type": "library", + "name": "./terminaltor", + "version": "(devel)", + "purl": "pkg:golang/./terminaltor@(devel)", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-binary-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangBinMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:location:0:path", + "value": "opt/app-root/src/main" + }, + { + "name": "syft:metadata:architecture", + "value": "amd64" + }, + { + "name": "syft:metadata:goCompiledVersion", + "value": "go1.18.9" + }, + { + "name": "syft:metadata:mainModule", + "value": "github.com/cachito-testing/gomod-pandemonium" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/redhat-appstudio/build-service@(devel)?package-id=2d30ce4decf63ac", + "type": "library", + "name": "github.com/redhat-appstudio/build-service", + "version": "(devel)", + "cpe": "cpe:2.3:a:redhat-appstudio:build-service:\\(devel\\):*:*:*:*:*:*:*", + "purl": "pkg:golang/github.com/redhat-appstudio/build-service@(devel)", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "go-module-binary-cataloger" + }, + { + "name": "syft:package:language", + "value": "go" + }, + { + "name": "syft:package:metadataType", + "value": "GolangBinMetadata" + }, + { + "name": "syft:package:type", + "value": "go-module" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:redhat-appstudio:build_service:\\(devel\\):*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:redhat_appstudio:build-service:\\(devel\\):*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:redhat_appstudio:build_service:\\(devel\\):*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:redhat:build-service:\\(devel\\):*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:redhat:build_service:\\(devel\\):*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "manager" + }, + { + "name": "syft:metadata:architecture", + "value": "amd64" + }, + { + "name": "syft:metadata:goCompiledVersion", + "value": "go1.18.4" + } + ] + }, + { + "bom-ref": "pkg:pypi/pyyaml@6.0?package-id=377756039bc8cd3d", + "type": "library", + "author": "Kirill Simonov ", + "name": "PyYAML", + "version": "6.0", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "cpe": "cpe:2.3:a:kirill_simonov:python-PyYAML:6.0:*:*:*:*:*:*:*", + "purl": "pkg:pypi/PyYAML@6.0", + "properties": [ + { + "name": "syft:package:foundBy", + "value": "python-package-cataloger" + }, + { + "name": "syft:package:language", + "value": "python" + }, + { + "name": "syft:package:metadataType", + "value": "PythonPackageMetadata" + }, + { + "name": "syft:package:type", + "value": "python" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:kirill_simonov:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-PyYAML:python-PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-PyYAML:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_PyYAML:python-PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_PyYAML:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:kirill_simonov:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:PyYAML:python-PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:PyYAML:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python-PyYAML:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python-PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python_PyYAML:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:xi:python-PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:xi:python_PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:PyYAML:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:python:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:cpe23", + "value": "cpe:2.3:a:xi:PyYAML:6.0:*:*:*:*:*:*:*" + }, + { + "name": "syft:location:0:path", + "value": "usr/local/lib64/python3.11/site-packages/PyYAML-6.0.dist-info/METADATA" + }, + { + "name": "syft:location:1:path", + "value": "usr/local/lib64/python3.11/site-packages/PyYAML-6.0.dist-info/RECORD" + }, + { + "name": "syft:location:2:path", + "value": "usr/local/lib64/python3.11/site-packages/PyYAML-6.0.dist-info/top_level.txt" + } + ] + }, + { + "type": "operating-system", + "name": "rhel", + "version": "8.7", + "description": "Red Hat Enterprise Linux 8.7 (Ootpa)", + "cpe": "cpe:/o:redhat:enterprise_linux:8::baseos", + "swid": { + "tagId": "rhel", + "name": "rhel", + "version": "8.7" + }, + "externalReferences": [ + { + "url": "https://bugzilla.redhat.com/", + "type": "issue-tracker" + }, + { + "url": "https://www.redhat.com/", + "type": "website" + } + ], + "properties": [ + { + "name": "syft:distro:id", + "value": "rhel" + }, + { + "name": "syft:distro:idLike:0", + "value": "fedora" + }, + { + "name": "syft:distro:prettyName", + "value": "Red Hat Enterprise Linux 8.7 (Ootpa)" + }, + { + "name": "syft:distro:versionID", + "value": "8.7" + } + ] + } + ] +} diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_merge_cachi2_sboms.py b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_merge_cachi2_sboms.py new file mode 100644 index 0000000..b88c945 --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/test_merge_cachi2_sboms.py @@ -0,0 +1,133 @@ +import json +from pathlib import Path +from typing import Any + +import pytest + +from merge_cachi2_sboms import merge_sboms + +TOOLS_METADATA = { + "syft-cyclonedx-1.4": { + "name": "syft", + "vendor": "anchore", + "version": "0.47.0", + }, + "syft-cyclonedx-1.5": { + "type": "application", + "author": "anchore", + "name": "syft", + "version": "0.100.0", + }, + "cachi2-cyclonedx-1.4": { + "name": "cachi2", + "vendor": "red hat", + }, + "cachi2-cyclonedx-1.5": { + "type": "application", + "author": "red hat", + "name": "cachi2", + }, +} + + +@pytest.fixture +def data_dir() -> Path: + """Path to the directory for storing unit test data.""" + return Path(__file__).parent / "test_data" + + +def test_merge_sboms(data_dir: Path) -> None: + result = merge_sboms(f"{data_dir}/cachi2.bom.json", f"{data_dir}/syft.bom.json") + + with open(f"{data_dir}/merged.bom.json") as file: + expected_sbom = json.load(file) + + assert json.loads(result) == expected_sbom + + +@pytest.mark.parametrize( + "syft_tools_metadata, expected_result", + [ + ( + [TOOLS_METADATA["syft-cyclonedx-1.4"]], + [ + TOOLS_METADATA["syft-cyclonedx-1.4"], + TOOLS_METADATA["cachi2-cyclonedx-1.4"], + ], + ), + ( + { + "components": [TOOLS_METADATA["syft-cyclonedx-1.5"]], + }, + { + "components": [ + TOOLS_METADATA["syft-cyclonedx-1.5"], + TOOLS_METADATA["cachi2-cyclonedx-1.5"], + ], + }, + ), + ], +) +def test_merging_tools_metadata(syft_tools_metadata: str, expected_result: Any, tmpdir: Path) -> None: + syft_sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": { + "tools": syft_tools_metadata, + }, + "components": [], + } + + cachi2_sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "metadata": { + "tools": [TOOLS_METADATA["cachi2-cyclonedx-1.4"]], + }, + "components": [], + } + + syft_sbom_path = f"{tmpdir}/syft.bom.json" + cachi2_sbom_path = f"{tmpdir}/cachi2.bom.json" + + with open(syft_sbom_path, "w") as file: + json.dump(syft_sbom, file) + + with open(cachi2_sbom_path, "w") as file: + json.dump(cachi2_sbom, file) + + result = merge_sboms(cachi2_sbom_path, syft_sbom_path) + + assert json.loads(result)["metadata"]["tools"] == expected_result + + +def test_invalid_tools_format(tmpdir: Path) -> None: + syft_sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "metadata": { + "tools": "invalid", + }, + "components": [], + } + + cachi2_sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "metadata": { + "tools": [TOOLS_METADATA["cachi2-cyclonedx-1.4"]], + }, + "components": [], + } + + syft_sbom_path = f"{tmpdir}/syft.bom.json" + cachi2_sbom_path = f"{tmpdir}/cachi2.bom.json" + + with open(syft_sbom_path, "w") as file: + json.dump(syft_sbom, file) + + with open(cachi2_sbom_path, "w") as file: + json.dump(cachi2_sbom, file) + + with pytest.raises(RuntimeError): + merge_sboms(cachi2_sbom_path, syft_sbom_path) diff --git a/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini new file mode 100644 index 0000000..db6ddad --- /dev/null +++ b/sbom-utility-scripts/scripts/merge-cachi2-sboms-script/tox.ini @@ -0,0 +1,15 @@ +[tox] +env_list = flake8,black,test + +[testenv:test] +deps = -r requirements-test.txt +commands = pytest test_merge_cachi2_sboms.py + +[testenv:flake8] +deps = flake8 +commands = flake8 --max-line-length 120 merge_cachi2_sboms.py test_merge_cachi2_sboms.py + +[testenv:black] +deps = black +commands = black --line-length 120 --check --diff . + diff --git a/sbom-utility-scripts/scripts/merge_syft_sboms.py b/sbom-utility-scripts/scripts/merge_syft_sboms.py new file mode 100644 index 0000000..62c2185 --- /dev/null +++ b/sbom-utility-scripts/scripts/merge_syft_sboms.py @@ -0,0 +1,27 @@ +import json + +# load SBOMs +with open("./sbom-image.json") as f: + image_sbom = json.load(f) + +with open("./sbom-source.json") as f: + source_sbom = json.load(f) + +# fetch unique components from available SBOMs +def get_identifier(component): + return component["name"] + '@' + component.get("version", "") + +image_sbom_components = image_sbom.setdefault("components", []) +existing_components = [get_identifier(component) for component in image_sbom_components] + +source_sbom_components = source_sbom.get("components", []) +for component in source_sbom_components: + if get_identifier(component) not in existing_components: + image_sbom_components.append(component) + existing_components.append(get_identifier(component)) + +image_sbom_components.sort(key=lambda c: get_identifier(c)) + +# write the CycloneDX unified SBOM +with open("./sbom-cyclonedx.json", "w") as f: + json.dump(image_sbom, f, indent=4)