-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The script is responsible for expanding a image sbom by adding a reference pointing to the image into a SBOM content. The script recognize an input format and adds necessary data into the list of components or packages. The script also sets a name of the SBOM based on a pullspec. JIRA: ISV-5411, ISV-5320 Signed-off-by: Ales Raszka <araszka@redhat.com>
- Loading branch information
Showing
7 changed files
with
553 additions
and
1 deletion.
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
95 changes: 95 additions & 0 deletions
95
sbom-utility-scripts/scripts/add-image-reference-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,95 @@ | ||
# Add image reference script | ||
|
||
The script aims to enrich the SBOM file with additional information about the images used in the build process. | ||
Based on a input SBOM type, the script updates certain fields with the image reference information. This is needed | ||
to provide a complete SBOM file that can be used for further analysis. | ||
|
||
## Usage | ||
|
||
```bash | ||
python add_image_reference.py \ | ||
--input-file ./input-sbom.json \ | ||
--output-path ./updated-sbom.json \ | ||
--image-url quay.io/foo/bar/ubi8:1.1 \ | ||
--image-digest sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc \ | ||
--arch amd64 | ||
``` | ||
The script stores the updated SBOM in the output path provided. | ||
|
||
## List of updates | ||
### SPDX | ||
The script updates the following fields in the SPDX SBOM: | ||
- `packages` - the script adds a new package with the image reference information | ||
- `relationships` - the script adds a new relationship between the package and the image reference | ||
- `name` - the script adds the image reference as a name | ||
|
||
#### Example | ||
```json | ||
{ | ||
... | ||
"name": "quay.io/foo/bar/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc", | ||
"packages": [ | ||
{ | ||
"SPDXID": "SPDXRef-image", | ||
"name": "ubi8", | ||
"versionInfo": "1.1", | ||
"downloadLocation": "NOASSERTION", | ||
"licenseConcluded": "NOASSERTION", | ||
"supplier": "NOASSERTION", | ||
"externalRefs": [ | ||
{ | ||
"referenceLocator": "pkg:oci/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc?repository_url=quay.io/foo/bar/ubi8", | ||
"referenceType": "purl", | ||
"referenceCategory": "PACKAGE-MANAGER" | ||
} | ||
], | ||
"checksums": [ | ||
{ | ||
"algorithm": "SHA256", | ||
"checksumValue": "011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc" | ||
} | ||
] | ||
}, | ||
], | ||
"relationships": [ | ||
{ | ||
"spdxElementId": "SPDXRef-DOCUMENT", | ||
"relationshipType": "DESCRIBES", | ||
"relatedSpdxElement": "SPDXRef-image" | ||
}, | ||
] | ||
} | ||
|
||
``` | ||
|
||
### CycloneDX | ||
- `components` - the script adds a new component with the image reference information | ||
- `metadata.component.purl` - the script adds the image reference as a purl | ||
- `metadata.component.name` - the script adds the image reference as a name | ||
|
||
#### Example | ||
```json | ||
"metadata": { | ||
"component": { | ||
"name": "quay.io/foo/bar/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc", | ||
"purl": "pkg:oci/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc?arch=amd64&repository_url=quay.io/foo/bar/ubi8" | ||
} | ||
}, | ||
"components": [ | ||
{ | ||
"type": "container", | ||
"bom-ref": "pkg:oci/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc?arch=amd64&repository_url=quay.io/foo/bar/ubi8", | ||
"name": "ubi8", | ||
"purl": "pkg:oci/ubi8@sha256:011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc?arch=amd64&repository_url=quay.io/foo/bar/ubi8", | ||
"version": "1.1", | ||
"publisher": "Red Hat, Inc.", | ||
"hashes": [ | ||
{ | ||
"alg": "SHA-256", | ||
"content": "011ff0cd8f34588d3eca86da97619f7baf99c8cc12e24cc3a7f337873c8d36cc" | ||
} | ||
] | ||
}, | ||
... | ||
] | ||
``` |
279 changes: 279 additions & 0 deletions
279
sbom-utility-scripts/scripts/add-image-reference-script/add_image_reference.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,279 @@ | ||
#!/usr/bin/env python3 | ||
import argparse | ||
import json | ||
from dataclasses import dataclass | ||
from pathlib import Path | ||
from typing import Optional | ||
|
||
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": | ||
""" | ||
Create an instance of the Image class from the image URL and digest. | ||
Args: | ||
image_url_and_tag (str): Image URL in the format 'registry.com/repository/image:tag'. | ||
image_digest (str): Manifest digest of the image. (sha256:digest) | ||
arch (Optional[str], optional): Image architecture. Defaults to None. | ||
Returns: | ||
Image: An instance of the Image class representing the 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_cyclonedx(self) -> str: | ||
""" | ||
Get the digest algorithm used for the image in cyclonedx format. | ||
The output is in uppercase. | ||
Returns: | ||
str: Algorithm used for the digest. | ||
""" | ||
algo, _ = self.digest.split(":") | ||
mapping = {"sha256": "SHA-256", "sha512": "SHA-512"} | ||
return mapping.get(algo, algo.upper()) | ||
|
||
@property | ||
def digest_algo_spdx(self) -> str: | ||
""" | ||
Get the digest algorithm used for the image in SPDX format. | ||
Returns: | ||
str: Algorithm used for the digest in SPDX format. | ||
""" | ||
algo, _ = self.digest.split(":") | ||
return algo.upper() | ||
|
||
@property | ||
def digest_hex_val(self) -> str: | ||
""" | ||
Get the digest value of the image in hexadecimal format. | ||
Returns: | ||
str: Digest value in hexadecimal format. | ||
""" | ||
_, val = self.digest.split(":") | ||
return val | ||
|
||
def purl(self) -> str: | ||
""" | ||
Get the Package URL (PURL) for the image. | ||
Returns: | ||
str: A string representing the PURL for the image. | ||
""" | ||
return PackageURL( | ||
type="oci", | ||
name=self.name, | ||
version=self.digest, | ||
qualifiers={"arch": self.arch, "repository_url": self.repository}, | ||
).to_string() | ||
|
||
|
||
def setup_arg_parser() -> argparse.ArgumentParser: | ||
""" | ||
Setup the argument parser for the script. | ||
Returns: | ||
argparse.ArgumentParser: Argument parser for the script. | ||
""" | ||
parser = argparse.ArgumentParser(description="Add image reference to image SBOM.") | ||
parser.add_argument( | ||
"--image-url", | ||
type=str, | ||
help="Image URL in the format 'registry.com/repository/image:tag'.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--image-digest", | ||
type=str, | ||
help="Image manifest digest in a form sha256:xxxx.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--arch", | ||
type=str, | ||
help="Image architecture.", | ||
) | ||
parser.add_argument( | ||
"--input-file", | ||
"-i", | ||
type=Path, | ||
help="SBOM file in JSON format.", | ||
required=True, | ||
) | ||
parser.add_argument( | ||
"--output-path", | ||
"-o", | ||
type=str, | ||
help="Path to save the output SBOM in JSON format.", | ||
) | ||
return parser | ||
|
||
|
||
def update_component_in_cyclonedx_sbom(sbom: dict, image: Image) -> dict: | ||
""" | ||
Update the CycloneDX SBOM with the image reference. | ||
The reference to the image is added to the SBOM in the form of a component and | ||
purl is added to the metadata. | ||
Args: | ||
sbom (dict): SBOM in JSON format. | ||
image (Image): An instance of the Image class that represents the image. | ||
Returns: | ||
dict: Updated SBOM with the image reference added. | ||
""" | ||
# First, update the metadata with the image purl | ||
sbom["metadata"]["component"]["purl"] = image.purl() | ||
|
||
# Then, add the image component to the components list | ||
image_component = { | ||
"type": "container", | ||
"bom-ref": image.purl(), | ||
"name": image.name, | ||
"purl": image.purl(), | ||
"version": image.tag, | ||
"publisher": "Red Hat, Inc.", | ||
"hashes": [{"alg": image.digest_algo_cyclonedx, "content": image.digest_hex_val}], | ||
} | ||
sbom["components"].insert(0, image_component) | ||
return sbom | ||
|
||
|
||
def update_package_in_spdx_sbom(sbom: dict, image: Image) -> dict: | ||
""" | ||
Update the SPDX SBOM with the image reference. | ||
The reference to the image is added to the SBOM in the form of a package and | ||
appropriate relationships are added to the SBOM. | ||
Args: | ||
sbom (dict): SBOM in JSON format. | ||
image (Image): An instance of the Image class that represents the image. | ||
Returns: | ||
dict: Updated SBOM with the image reference added. | ||
""" | ||
# Add the image package to the packages list | ||
package = { | ||
"SPDXID": "SPDXRef-image", | ||
"name": image.name, | ||
"versionInfo": image.tag, | ||
"downloadLocation": "NOASSERTION", | ||
"licenseConcluded": "NOASSERTION", | ||
"supplier": "NOASSERTION", | ||
"externalRefs": [ | ||
{ | ||
"referenceLocator": image.purl(), | ||
"referenceType": "purl", | ||
"referenceCategory": "PACKAGE-MANAGER", | ||
} | ||
], | ||
"checksums": [{"algorithm": image.digest_algo_spdx, "checksumValue": image.digest_hex_val}], | ||
} | ||
sbom["packages"].insert(0, package) | ||
|
||
# Add the relationship between the image and the package | ||
sbom["relationships"].insert( | ||
0, | ||
{ | ||
"spdxElementId": sbom["SPDXID"], | ||
"relationshipType": "DESCRIBES", | ||
"relatedSpdxElement": package["SPDXID"], | ||
}, | ||
) | ||
return sbom | ||
|
||
|
||
def extend_sbom_with_image_reference(sbom: dict, image: Image) -> dict: | ||
""" | ||
Extend the SBOM with the image reference. | ||
Based on the SBOM format, the image reference is added to the SBOM in | ||
a different way. | ||
Args: | ||
sbom (dict): SBOM in JSON format. | ||
image (Image): An instance of the Image class that represents the image. | ||
Returns: | ||
dict: Updated SBOM with the image reference added. | ||
""" | ||
if sbom.get("bomFormat") == "CycloneDX": | ||
update_component_in_cyclonedx_sbom(sbom, image) | ||
elif "spdxVersion" in sbom: | ||
update_package_in_spdx_sbom(sbom, image) | ||
|
||
return sbom | ||
|
||
|
||
def update_name(sbom: dict, image: Image) -> dict: | ||
""" | ||
Update the SBOM name with the image reference in the format 'repository@digest'. | ||
Args: | ||
sbom (dict): SBOM in JSON format. | ||
image (Image): An instance of the Image class that represents the image. | ||
Returns: | ||
dict: Updated SBOM with the name field updated. | ||
""" | ||
if sbom.get("bomFormat") == "CycloneDX": | ||
sbom["metadata"]["component"]["name"] = f"{image.repository}@{image.digest}" | ||
elif "spdxVersion" in sbom: | ||
sbom["name"] = f"{image.repository}@{image.digest}" | ||
return sbom | ||
|
||
|
||
def main(): | ||
""" | ||
Main function to add image reference to SBOM. | ||
""" | ||
arg_parser = setup_arg_parser() | ||
args = arg_parser.parse_args() | ||
|
||
with open(args.input_file, "r") as inp_file: | ||
sbom = json.load(inp_file) | ||
|
||
image = Image.from_image_index_url_and_digest( | ||
args.image_url, | ||
args.image_digest, | ||
args.arch, | ||
) | ||
|
||
# Update the input SBOM with the image reference and name attributes | ||
sbom = extend_sbom_with_image_reference(sbom, image) | ||
sbom = update_name(sbom, image) | ||
|
||
# Save the updated SBOM to the output file | ||
if args.output_path: | ||
with open(args.output_path, "w") as out_file: | ||
json.dump(sbom, out_file) | ||
|
||
|
||
if __name__ == "__main__": # pragma: no cover | ||
main() |
5 changes: 5 additions & 0 deletions
5
sbom-utility-scripts/scripts/add-image-reference-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,5 @@ | ||
black==24.10.0 | ||
flake8==7.1.1 | ||
pytest-mock==3.14.0 | ||
pytest==8.3.3 | ||
pytest-cov==6.0.0 |
1 change: 1 addition & 0 deletions
1
sbom-utility-scripts/scripts/add-image-reference-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 @@ | ||
packageurl-python==0.15.0 |
Oops, something went wrong.