Skip to content

Commit

Permalink
Merge pull request #3 from RedHatProductSecurity/container-examples
Browse files Browse the repository at this point in the history
Add container image sbom example generator
  • Loading branch information
mprpic authored Aug 1, 2024
2 parents ae32979 + 3d6b475 commit 151fcf9
Show file tree
Hide file tree
Showing 11 changed files with 13,852 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .github/workflows/schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ jobs:
- name: check schema
run: |
sudo apt-get install -y python3-jsonschema
for example in $GITHUB_WORKSPACE/sbom/examples/rpm/*.json; do
for example in $GITHUB_WORKSPACE/sbom/examples/*/*.json; do
jsonschema $GITHUB_WORKSPACE/sbom/spdx-2.3-schema.json -i $example
done
226 changes: 226 additions & 0 deletions sbom/examples/container_image/from_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import json
import itertools
import sys

import requests

# These container images (identified by their NVR) are known to contain only RPM packages and no
# other content type.
RPM_CONTAINER_IMAGES = [
"ubi9-micro-container-9.4-6.1716471860",
"kernel-module-management-operator-container-1.1.2-25", # Contains openssl-3.0.7-18.el9_2
]

catalog_url = "https://catalog.redhat.com/api/containers/v1/"
nvr_api = catalog_url + "images/nvr/"
rpm_manifest_api = catalog_url + "images/id/{catalog_image_id}/rpm-manifest"

rpm_sbom_url = "https://access.redhat.com/security/data/sbom/v1/rpm/"


def get_image_data(image_nvr):
response = requests.get(nvr_api + image_nvr)
response.raise_for_status()
# This is a paged response, but we're assuming there are not 100+ images for a single
# container image NVR.
return sorted(response.json()["data"], key=lambda image: image["_id"])


def get_rpms(image_id):
response = requests.get(rpm_manifest_api.format(catalog_image_id=image_id))
response.raise_for_status()
return sorted(response.json()["rpms"], key=lambda rpm: rpm["nvra"])


def create_sbom(doc_id, image_id, root_package, packages, rel_type):
relationships = [
{
"spdxElementId": f"SPDXRef-DOCUMENT-{doc_id}",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": root_package["SPDXID"],
}
]
for pkg in packages:
relationships.append(
{
"spdxElementId": root_package["SPDXID"],
"relationshipType": rel_type,
"relatedSpdxElement": pkg["SPDXID"],
}
)

spdx = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": f"SPDXRef-DOCUMENT-{doc_id}",
"creationInfo": {
"created": "2006-08-14T02:34:56-06:00",
"creators": [
"Tool: example SPDX document only",
],
},
"name": image_id,
"packages": [root_package] + packages,
"relationships": relationships,
}

with open(f"{image_id}.spdx.json", "w") as fp:
json.dump(spdx, fp, indent=2)


def generate_sboms_for_image(image_nvr):
# Split to e.g. "ubi9-micro-container" and "9.4-6.1716471860"
image_nvr_name, *image_nvr_version = image_nvr.rsplit("-", maxsplit=2)
image_nvr_version = "-".join(image_nvr_version)

image_index_pkg = None
per_arch_images = []
doc_id_generator = itertools.count(1) # Reserve 0 for the image list SBOM.

for image in get_image_data(image_nvr):
packages = []

catalog_image_id = image["_id"]
image_digest = image["image_id"]
content_sets = image["content_sets"]

# A container image may be available through more than one repo; collect all repos,
# registries they are available from, and the most specific tag for each repo image.
repos = set()
image_index_digest = ""
for repo in image["repositories"]:
repo_url = f"{repo['registry']}/{repo['repository']}"
tags = list(
sorted(
[t for t in repo["tags"] if t["name"] != "latest"],
# Sort by the length of the tag, ignoring "latest"; this is a very dumb
# heuristic to find the most specific tag for a particular image. From tags
# such as "9.4", "latest", and "9.4-6.1716471860", it will select the last one.
key=lambda x: len(x["name"]),
reverse=True,
)
)
if not tags:
print(f"ERROR: no usable tag found for image ID: {catalog_image_id}")
sys.exit(1)
repo_name = repo["repository"].split("/")[-1]
repos.add((repo_name, repo_url, tags[0]["name"]))
image_index_digest = repo["manifest_list_digest"].lstrip("sha256:")

if not repos or not image_index_digest:
print("ERROR: No repos or image index digest found for image ID: {catalog_image_id}")
sys.exit(1)

# Get license information from labels if it is set
image_license = "NOASSERTION"
for label in image["parsed_data"]["labels"]:
if label["name"].lower() == "license":
image_license = label["value"]

# Create an index image object, but since all arch-specific images are descendents of one
# and the same index image, we only have to create it once. Its SBOM is created at the
# end after we collect information about all arch-specific images.
if not image_index_pkg:
image_index_pkg = {
"SPDXID": "SPDXRef-image-index",
"name": image_nvr_name,
"versionInfo": image_nvr_version,
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": image_license,
"externalRefs": [],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": image_index_digest,
}
],
}
for name, repo_url, tag in sorted(repos):
purl = (
f"pkg:oci/{name}@sha256%3A{image_index_digest}?"
f"repository_url={repo_url}&tag={tag}"
)
ref = {
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": purl,
}
image_index_pkg["externalRefs"].append(ref)

spdx_image_id = f"SPDXRef-{image_nvr_name}-{image['architecture']}"
image_pkg = {
"SPDXID": spdx_image_id,
"name": f"{image_nvr_name}_{image['architecture']}",
"versionInfo": image_nvr_version,
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": image_license,
"externalRefs": [],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": image_digest,
}
],
}
for name, repo_url, tag in sorted(repos):
purl = (
f"pkg:oci/{name}@sha256%3A{image_index_digest}?"
f"arch={image['architecture']}&repository_url={repo_url}&tag={tag}"
)
ref = {
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": purl,
}
image_pkg["externalRefs"].append(ref)
per_arch_images.append(image_pkg)

for rpm in get_rpms(catalog_image_id):
rpm_purl = (
f"pkg:rpm/redhat/{rpm['name']}@{rpm['version']}-{rpm['release']}?"
# We don't have a way to find out which content set (RPM repo) an RPM came from,
# so we arbitrarily choose one here (assuming we have this mapping via RPM
# lockfiles or other means eventually).
f"arch={rpm['architecture']}&repository_id={content_sets[0]}"
)
spdx_rpm_id = f"SPDXRef-{rpm['architecture']}-{rpm['name']}"
rpm_pkg = {
"SPDXID": spdx_rpm_id,
"name": rpm["name"],
"versionInfo": rpm["version"],
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION", # Unset on purpose; refer to RPM SBOM
"packageFileName": rpm["nvra"] + ".rpm",
"licenseDeclared": "NOASSERTION", # Unset on purpose; refer to RPM SBOM
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": rpm_purl,
},
],
# We don't have data on a checksum for binary RPMs included in images; should we?
}
packages.append(rpm_pkg)

create_sbom(
doc_id=next(doc_id_generator),
image_id=f"{image_nvr}_" f"{image['architecture']}",
root_package=image_pkg,
packages=packages,
rel_type="CONTAINS",
)

create_sbom(
doc_id=0,
image_id=image_nvr,
root_package=image_index_pkg,
packages=per_arch_images,
rel_type="DESCENDANT_OF",
)


for rpm_image in RPM_CONTAINER_IMAGES:
generate_sboms_for_image(rpm_image)
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT-0",
"creationInfo": {
"created": "2006-08-14T02:34:56-06:00",
"creators": [
"Tool: example SPDX document only"
]
},
"name": "kernel-module-management-operator-container-1.1.2-25",
"packages": [
{
"SPDXID": "SPDXRef-image-index",
"name": "kernel-module-management-operator-container",
"versionInfo": "1.1.2-25",
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "Apache License 2.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/kernel-module-management-rhel9-operator@sha256%3Ad845f0bd93dad56c92c47e8c116a11a0cc5924c0b99aed912b4f8b54178efa98?repository_url=registry.access.redhat.com/kmm/kernel-module-management-rhel9-operator&tag=1.1.2-25"
}
],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "d845f0bd93dad56c92c47e8c116a11a0cc5924c0b99aed912b4f8b54178efa98"
}
]
},
{
"SPDXID": "SPDXRef-kernel-module-management-operator-container-amd64",
"name": "kernel-module-management-operator-container_amd64",
"versionInfo": "1.1.2-25",
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "Apache License 2.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/kernel-module-management-rhel9-operator@sha256%3Ad845f0bd93dad56c92c47e8c116a11a0cc5924c0b99aed912b4f8b54178efa98?arch=amd64&repository_url=registry.access.redhat.com/kmm/kernel-module-management-rhel9-operator&tag=1.1.2-25"
}
],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "sha256:ed976a0ba418a498b05a56cb05afa0cb36a65e750771f3840b12e9bae3afb22d"
}
]
},
{
"SPDXID": "SPDXRef-kernel-module-management-operator-container-arm64",
"name": "kernel-module-management-operator-container_arm64",
"versionInfo": "1.1.2-25",
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "Apache License 2.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/kernel-module-management-rhel9-operator@sha256%3Ad845f0bd93dad56c92c47e8c116a11a0cc5924c0b99aed912b4f8b54178efa98?arch=arm64&repository_url=registry.access.redhat.com/kmm/kernel-module-management-rhel9-operator&tag=1.1.2-25"
}
],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "sha256:9e74a91f532b7550ee909c6ce1636122982a5c5e32859c40c3bfe68231d31100"
}
]
},
{
"SPDXID": "SPDXRef-kernel-module-management-operator-container-ppc64le",
"name": "kernel-module-management-operator-container_ppc64le",
"versionInfo": "1.1.2-25",
"supplier": "Organization: Red Hat",
"downloadLocation": "NOASSERTION",
"licenseDeclared": "Apache License 2.0",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:oci/kernel-module-management-rhel9-operator@sha256%3Ad845f0bd93dad56c92c47e8c116a11a0cc5924c0b99aed912b4f8b54178efa98?arch=ppc64le&repository_url=registry.access.redhat.com/kmm/kernel-module-management-rhel9-operator&tag=1.1.2-25"
}
],
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "sha256:32a9929e6f11dfefb7a339d6767d1050cec2b24d397856cbd9b46a1bbc3f8827"
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT-0",
"relationshipType": "DESCRIBES",
"relatedSpdxElement": "SPDXRef-image-index"
},
{
"spdxElementId": "SPDXRef-image-index",
"relationshipType": "DESCENDANT_OF",
"relatedSpdxElement": "SPDXRef-kernel-module-management-operator-container-amd64"
},
{
"spdxElementId": "SPDXRef-image-index",
"relationshipType": "DESCENDANT_OF",
"relatedSpdxElement": "SPDXRef-kernel-module-management-operator-container-arm64"
},
{
"spdxElementId": "SPDXRef-image-index",
"relationshipType": "DESCENDANT_OF",
"relatedSpdxElement": "SPDXRef-kernel-module-management-operator-container-ppc64le"
}
]
}
Loading

0 comments on commit 151fcf9

Please sign in to comment.