-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feat: Add script for image index SBOM creation.
- Loading branch information
1 parent
ca08290
commit 2b4fe8d
Showing
11 changed files
with
609 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
sbom-utility-scripts/scripts/index-image-sbom-script/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# SBOM for Image Index | ||
|
||
This script builds SPDX2.3 SBOM for image index. | ||
|
||
## Usage | ||
|
||
**List of arguments:** | ||
|
||
- `--image-index-url` / `-u` | ||
- Must be in the format `repository/image:tag` | ||
- Example value `quay.io/mkosiarc_rhtap/single-container-app:f2566ab` | ||
- `--image-index-digest` / `-d` | ||
- Must be in the format `algorithm:hexvalue` | ||
- Example value `sha256:8f99627e843e931846855c5d899901bf093f5093e613a92745696a26b5420941` | ||
- `--inspect-input-file` / `-i` | ||
- Path to a file containing a json output of `buildah manifest inspect` command | ||
- File contents MUST be a valid JSON | ||
- See example in `sample_data/inspect.json` | ||
- `--output-path` / `-o` | ||
- Path where the SBOM should be written | ||
- If omitted, SBOM is returned to STDOUT | ||
|
||
## Behavior | ||
|
||
This script creates an SBOM with externalRefs using both | ||
PURLs from child digest and from index digest with `arch` qualifier. | ||
|
||
## Example | ||
|
||
To closely replicate the [example image index](https://github.com/RedHatProductSecurity/security-data-guidelines/blob/main/sbom/examples/container_image/build/ubi9-micro-container-9.4-6.1716471860.spdx.json), | ||
you can use the following command: | ||
|
||
```bash | ||
python3 index_image_sbom_script.py \ | ||
-u registry.redhat.io/ubi-micro:9.4-6.1716471860 \ | ||
-d sha256:1c8483e0fda0e990175eb9855a5f15e0910d2038dd397d9e2b357630f0321e6d \ | ||
-i sample_data/inspect.json | ||
``` | ||
|
||
To generate a different output, create a json document using `buildah manifest inspect <manifest_pullspec>` | ||
and supply this file as the `-i` argument. |
198 changes: 198 additions & 0 deletions
198
sbom-utility-scripts/scripts/index-image-sbom-script/index_image_sbom_script.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
#!/usr/bin/env python3 | ||
import argparse | ||
import hashlib | ||
import json | ||
from datetime import datetime, timezone | ||
from pathlib import Path | ||
from typing import Optional, Any | ||
from dataclasses import dataclass | ||
from uuid import uuid4 | ||
|
||
from packageurl import PackageURL | ||
|
||
|
||
@dataclass | ||
class Image: | ||
repository: str | ||
name: str | ||
digest: str | ||
tag: str | ||
arch: Optional[str] | ||
|
||
@staticmethod | ||
def from_image_index_url_and_digest( | ||
image_url_and_tag: str, | ||
image_digest: str, | ||
arch: Optional[str] = None, | ||
) -> "Image": | ||
|
||
repository, tag = image_url_and_tag.rsplit(":", 1) | ||
_, name = repository.rsplit("/", 1) | ||
return Image( | ||
repository=repository, | ||
name=name, | ||
digest=image_digest, | ||
tag=tag, | ||
arch=arch, | ||
) | ||
|
||
@property | ||
def digest_algo(self) -> str: | ||
algo, _ = self.digest.split(":") | ||
return algo.upper() | ||
|
||
@property | ||
def digest_hex_val(self) -> str: | ||
_, val = self.digest.split(":") | ||
return val | ||
|
||
def purls(self, index_digest: Optional[str] = None) -> list[str]: | ||
ans = [] | ||
if index_digest and self.arch: | ||
ans.append( | ||
PackageURL( | ||
type="oci", | ||
name=self.name, | ||
version=index_digest, | ||
qualifiers={"arch": self.arch, "repository_url": self.repository}, | ||
).to_string() | ||
) | ||
ans.append( | ||
PackageURL( | ||
type="oci", name=self.name, version=self.digest, qualifiers={"repository_url": self.repository} | ||
).to_string() | ||
) | ||
return ans | ||
|
||
def propose_spdx_id(self) -> str: | ||
purl_hex_digest = hashlib.sha256(self.purls()[0].encode()).hexdigest() | ||
return f"SPDXRef-image-{self.name}-{purl_hex_digest}" | ||
|
||
|
||
def create_package(image: Image, spdxid: Optional[str] = None, image_index_digest: Optional[str] = None) -> dict: | ||
return { | ||
"SPDXID": image.propose_spdx_id() if not spdxid else spdxid, | ||
"name": image.name if not image.arch else f"{image.name}_{image.arch}", | ||
"versionInfo": image.tag, | ||
"supplier": "NOASSERTION", | ||
"downloadLocation": "NOASSERTION", | ||
"licenseDeclared": "NOASSERTION", | ||
"externalRefs": [ | ||
{ | ||
"referenceCategory": "PACKAGE-MANAGER", | ||
"referenceType": "purl", | ||
"referenceLocator": purl, | ||
} | ||
for purl in image.purls(image_index_digest) | ||
], | ||
"checksums": [ | ||
{ | ||
"algorithm": image.digest_algo, | ||
"checksumValue": image.digest_hex_val, | ||
} | ||
], | ||
} | ||
|
||
|
||
def get_relationship(spdxid: str, related_spdxid: str): | ||
return { | ||
"spdxElementId": spdxid, | ||
"relationshipType": "VARIANT_OF", | ||
"relatedSpdxElement": related_spdxid, | ||
} | ||
|
||
|
||
def create_sbom( | ||
image_index_url: str, | ||
image_index_digest: str, | ||
inspect_input: dict[str, Any], | ||
) -> dict: | ||
if inspect_input["mediaType"] != "application/vnd.oci.image.index.v1+json": | ||
raise ValueError("Invalid input file detected, requires `buildah manifest inspect` json.") | ||
|
||
image_index_obj = Image.from_image_index_url_and_digest(image_index_url, image_index_digest) | ||
sbom_name = f"{image_index_obj.name}-{image_index_obj.tag}" | ||
|
||
packages = [create_package(image_index_obj, "SPDXRef-image-index")] | ||
relationships = [ | ||
{ | ||
"spdxElementId": "SPDXRef-DOCUMENT", | ||
"relationshipType": "DESCRIBES", | ||
"relatedSpdxElement": "SPDXRef-image-index", | ||
} | ||
] | ||
|
||
for manifest in inspect_input["manifests"]: | ||
if manifest["mediaType"] != "application/vnd.oci.image.manifest.v1+json": | ||
continue | ||
|
||
arch_image = Image( | ||
arch=manifest.get("platform", {}).get("architecture"), | ||
name=image_index_obj.name, | ||
digest=manifest.get("digest"), | ||
tag=image_index_obj.tag, | ||
repository=image_index_obj.repository, | ||
) | ||
packages.append(create_package(arch_image, image_index_digest=image_index_obj.digest)) | ||
relationships.append(get_relationship(arch_image.propose_spdx_id(), "SPDXRef-image-index")) | ||
|
||
sbom = { | ||
"spdxVersion": "SPDX-2.3", | ||
"dataLicense": "CC0-1.0", | ||
"documentNamespace": f"https://konflux-ci.dev/spdxdocs/{sbom_name}-{uuid4()}", | ||
"SPDXID": "SPDXRef-DOCUMENT", | ||
"creationInfo": { | ||
"created": datetime.now(timezone.utc).isoformat(timespec="seconds"), | ||
"creators": ["Tool: Konflux"], | ||
"licenseListVersion": "3.25", | ||
}, | ||
"name": sbom_name, | ||
"packages": packages, | ||
"relationships": relationships, | ||
} | ||
return sbom | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser(description="Create an image index SBOM.") | ||
parser.add_argument( | ||
"--image-index-url", | ||
"-u", | ||
type=str, | ||
help="Image index URL in the format 'repository/image:tag'.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--image-index-digest", | ||
"-d", | ||
type=str, | ||
help="Image index digest in the format 'algorithm:digest'.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--inspect-input-file", | ||
"-i", | ||
type=Path, | ||
help="Inspect json file produced by image index inspection.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--output-path", | ||
"-o", | ||
type=str, | ||
help="Path to save the output SBOM in JSON format.", | ||
) | ||
args = parser.parse_args() | ||
with open(args.inspect_input_file, "r") as inp_file: | ||
inspect_input = json.load(inp_file) | ||
|
||
sbom = create_sbom(args.image_index_url, args.image_index_digest, inspect_input) | ||
if args.output_path: | ||
with open(args.output_path, "w") as fp: | ||
json.dump(sbom, fp, indent=4) | ||
else: | ||
print(json.dumps(sbom, indent=4)) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
2 changes: 2 additions & 0 deletions
2
sbom-utility-scripts/scripts/index-image-sbom-script/requirements-test.in
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
pytest | ||
pytest-mock |
18 changes: 18 additions & 0 deletions
18
sbom-utility-scripts/scripts/index-image-sbom-script/requirements-test.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# | ||
# This file is autogenerated by pip-compile with Python 3.12 | ||
# by the following command: | ||
# | ||
# pip-compile requirements-test.in | ||
# | ||
iniconfig==2.0.0 | ||
# via pytest | ||
packaging==24.1 | ||
# via pytest | ||
pluggy==1.5.0 | ||
# via pytest | ||
pytest==8.3.3 | ||
# via | ||
# -r requirements-test.in | ||
# pytest-mock | ||
pytest-mock==3.14.0 | ||
# via -r requirements-test.in |
1 change: 1 addition & 0 deletions
1
sbom-utility-scripts/scripts/index-image-sbom-script/requirements.in
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
packageurl-python==0.15.0 |
8 changes: 8 additions & 0 deletions
8
sbom-utility-scripts/scripts/index-image-sbom-script/requirements.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
# | ||
# This file is autogenerated by pip-compile with Python 3.12 | ||
# by the following command: | ||
# | ||
# pip-compile requirements.in | ||
# | ||
packageurl-python==0.15.0 | ||
# via -r requirements.in |
42 changes: 42 additions & 0 deletions
42
sbom-utility-scripts/scripts/index-image-sbom-script/sample_data/inspect.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
{ | ||
"schemaVersion": 2, | ||
"mediaType": "application/vnd.oci.image.index.v1+json", | ||
"manifests": [ | ||
{ | ||
"mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
"digest": "sha256:f08722139c4da653b870272a192fac700960a3315baa1f79f83a4712a436d4", | ||
"size": 201, | ||
"platform": { | ||
"architecture": "ppc64le", | ||
"os": "linux" | ||
} | ||
}, | ||
{ | ||
"mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
"digest": "sha256:c9e70f4174747c6b53d253e879177c52731cc4bdc5fe9c6a2555412d849a952", | ||
"size": 201, | ||
"platform": { | ||
"architecture": "s390x", | ||
"os": "linux" | ||
} | ||
}, | ||
{ | ||
"mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
"digest": "sha256:629211996680a3143cd42dc99e19ad2dbb2c3c8a4fa9568607e9d96da8deaf01", | ||
"size": 201, | ||
"platform": { | ||
"architecture": "arm64", | ||
"os": "linux" | ||
} | ||
}, | ||
{ | ||
"mediaType": "application/vnd.oci.image.manifest.v1+json", | ||
"digest": "sha256:13fd2a0116a76eaa274fee20c86eef4dfba9f311784e8fb7d7f5fc38b32f3ef", | ||
"size": 201, | ||
"platform": { | ||
"architecture": "amd64", | ||
"os": "linux" | ||
} | ||
} | ||
] | ||
} |
Oops, something went wrong.