Skip to content

Commit

Permalink
Add a script for sbom enrichment
Browse files Browse the repository at this point in the history
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
Allda committed Nov 13, 2024
1 parent c34cbcd commit 4d2bbe1
Show file tree
Hide file tree
Showing 7 changed files with 553 additions and 1 deletion.
8 changes: 7 additions & 1 deletion sbom-utility-scripts/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ COPY scripts/base-images-sbom-script/app/requirements.txt /scripts/base-images-s
COPY scripts/index-image-sbom-script/requirements.txt /scripts/index-image-sbom-script-requirements.txt
COPY scripts/index-image-sbom-script/index_image_sbom_script.py /scripts

RUN pip3 install -r base-images-sbom-script-requirements.txt -r index-image-sbom-script-requirements.txt
COPY scripts/add-image-reference-script/add_image_reference.py /scripts
COPY scripts/add-image-reference-script/requirements.txt /scripts/add-image-reference-requirements.txt

RUN pip3 install --no-cache-dir \
-r base-images-sbom-script-requirements.txt \
-r index-image-sbom-script-requirements.txt \
-r add-image-reference-requirements.txt
95 changes: 95 additions & 0 deletions sbom-utility-scripts/scripts/add-image-reference-script/README.md
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"
}
]
},
...
]
```
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()
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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packageurl-python==0.15.0
Loading

0 comments on commit 4d2bbe1

Please sign in to comment.