SBOMs (Software Bills of Materials)

ingredients

As explained earlier in this chapter, SBOMs are basically the artifact "ingredients list", listing all contained packages and components, as well as license information.

Creating SBOMs as part of the build process is not only a good security practice (for all the reasons outlined in the introduction to this chapter) - as per regulations, they are also becoming mandatory for selling software to government agencies.

Since we are naturally following our own recommendations, Red Hat provides SBOMs for all Red Hat artifacts here as outlined in this article.

The delivery method (all "dumped" into a huge S3 bucket that is made available to customers) is still beta. The security team is working on easier methods for our customer’s consumption needs of Red Hat SBOMs.

Creating SBOMs

With that said - when we create artifacts, we also want to create SBOMs of these artifacts, typically as part of our CI toolchain.

Just as a side note - talking to customers, we know that many customers are already creating SBOMs in their toolchain. BUT the majority of customers is just creating SBOMs to "tick a regulatory box" - they are not leveraging the valuable information contained in them. They just file it away somewhere, "just in case we need it".

With tools such as Trusted Profile Analyzer (TPA), we can use that information, identifying the "blast radius" if a vulnerability is identified and informing remediation efforts.

However, TPA itself doesn’t create SBOMs and as long as the SBOMs are in SPDX or CycloneDX format will consume them, regardless how they were created.

TPA 2.1 accepts CycloneDX versions 1.3, 1.4, 1.5 and 1.6, and System Package Data Exchange (SPDX) versions 2.2, and 2.3 in .json format

syft

As an Open Source solution for customers who don’t have any means of creating SBOMs today, we recommend using syft:

Syft is an open-source CLI tool and library that generates Software Bill of Materials (SBOMs) from container images, filesystems, and archives. It provides detailed visibility into packages, dependencies, and components within software artifacts.

Origin & Development Syft was created and is sponsored by Anchore, a software supply chain security company, and is released under the Apache 2.0 License. The tool was designed to address the growing need for automated SBOM generation in cloud-native environments.

Key Capabilities - Supports multiple container image formats (OCI, Docker, Singularity) - Generates SBOMs in multiple standard formats including SPDX, CycloneDX, and Syft’s native format - Deep inspection capabilities that can identify dependencies nested multiple levels deep

Using syft

To understand what syft is doing, simply run syft $IMAGE and it will download the image from the registry and catalogue it.

(First, we’re checking that the $IMAGE variable for our example image still exists - also, the copy-image-to-quay.sh script creates a dockerconfig to login to Quay)

cd /workspace/l3-enablement-helpers/security-concepts
if [ -z "${IMAGE}" ]; then
    source ./copy-image-to-quay.sh
fi
echo "\$IMAGE is: $IMAGE"
syft $IMAGE

You can see below that syft not only parsed the image and its contents, but also the java application that was packaged inside it, as it identified the smallrye-* components that were used as dependencies.

syft $IMAGE
 ✔ Parsed image                                                                                                    sha256:771d0be00ec2b488e35925f2a9bfe27aa013afaedc40950f9c54430ef524a5c4
 ✔ Cataloged contents                                                                                                     5c50daa8cf06e7c36854343ccc31a99aecc10167d391f2a1d3cc048b63bd29ee
   ├── ✔ Packages                        [414 packages]
   ├── ✔ Executables                     [826 executables]
   ├── ✔ File digests                    [6,179 files]
   └── ✔ File metadata                   [6,179 locations]
NAME                                        VERSION                                          TYPE
alsa-lib                                    1.2.5-4.el8                                      rpm
aopalliance                                 1.0                                              java-archive
aopalliance                                 1.0-20.module+el8.3.0+6804+157bd82e              rpm
[...]
smallrye-mutiny-vertx-auth-common           2.24.1                                           java-archive
smallrye-mutiny-vertx-bridge-common         2.24.1                                           java-archive
smallrye-mutiny-vertx-core                  2.24.1                                           java-archive
smallrye-mutiny-vertx-runtime               2.24.1                                           java-archive
smallrye-mutiny-vertx-uri-template          2.24.1                                           java-archive
[...]
xorg-x11-fonts-Type1                        7.5-19.el8                                       rpm
xz-libs                                     5.2.4-3.el8                                      rpm
zlib                                        1.2.11-17.el8                                    rpm

To get a full report in CycloneDX or SPDX format, we have to specify the format and direct it to a file:

syft $IMAGE --enrich all --output cyclonedx-json=demo-image-sbom-syft.json

We have added --enrich all here. Syft can query e.g. maven central for additional information, such as license information for packages. Especially when it comes to security, "more is better" - so the more information we can get, the better.

We will upload and manage SBOMs in Module 8: Trusted Software Supply Chain with TPA but if you’d like to take a quick look, we’ll have to format it (jq) and make it accessible (less).

HINT: Exit less with q or Q

cat demo-image-sbom-syft.json | jq | less

roxctl

roxctl is a command-line interface (CLI) for running commands on Red Hat Advanced Cluster Security for Kubernetes (RHACS), a comprehensive Kubernetes-native security platform. While roxctl supports various security operations, version 4.7 introduced SBOM generation capabilities as a Technology Preview feature.

From Red Hat Advanced Cluster Security 4.7, users can generate SBOMs from the command line or through the user interface. SBOMs can be generated from Scanner V4 image scans via the UI, CLI (roxctl image sbom), and API (/api/v1/images/sbom). Currently, only scans executed via Central are supported, with delegated scanning planned for future releases.

Context

The SBOM generation feature was introduced in response to the growing importance of SBOMs for supply chain security, especially in light of the NIST Executive Order from 2021. The roxctl tool integrates into CI/CD pipelines for automated security scanning, policy enforcement, and now SBOM generation alongside RHACS’s broader security capabilities including vulnerability management, runtime threat detection, and compliance checking.

Architecture

The major difference between standalone SBOM generators and roxctl is that the scan / cataloging of components does not happen in the task execution environment (in our case, the terminal pod, in a real CI the task pod) - but on the ACS Cluster. No image is pulled locally for inspection.

The roxctl CLI is just an interface that tells the cluster what to do.

As a consequence, everything that roxctl does could also be done via the REST API. If you’re interested how that works, you can checkout the setup-acs-integration.sh in the terminal home directory:

less ~/setup-acs-integration.sh

HINT: Exit less with q or Q

This script creates a "Registry Integration" in ACS. It basically tells ACS which authentication method and credentials to use when accessing a specific registry. During the terminal pod startup, we’re checking that the integration exists and if not, create it - all via the REST API.

acs integrations

But we’re digressing from the topic at hand…​ 🤷

Using roxctl

Creating an SBOM using roxctl is as simple as roxctl image sbom. The image subcommand allows us to

  • Scan an image for vulnerabilities (roxctl image scan)

  • Scan and verify policy violations in the ACS list of policies (roxctl image check)

  • Generate an SBOM (roxctl image sbom)

Try them all out, play with the results in different formats and check roxctl image --help as well as the help for the sub-sub-commands such as roxctl image scan --help

To generate an SBOM, just pass in the image you’re interested in.

(First, we’re checking that the $IMAGE variable for our example image still exists)

cd /workspace/l3-enablement-helpers/security-concepts
if [ -z "${IMAGE}" ]; then
    source ./copy-image-to-quay.sh
fi
echo "\$IMAGE is: $IMAGE"
# using jq for readability and less since it's large
roxctl image sbom --image=$IMAGE | jq | less

HINT: Exit less with q or Q

So, as you can see at the beginning of the file, roxctl generates an SPDX (not CycloneDX) SBOM - which is fine, since both formats are very well established

{
  "spdxVersion": "SPDX-2.3",
  "dataLicense": "CC0-1.0",
  "SPDXID": "SPDXRef-DOCUMENT",
  "name": "sha256:5c50daa8cf06e7c36854343ccc31a99aecc10167d391f2a1d3cc048b63bd29ee",
  "documentNamespace": "https://quay-v9q9c.apps.cluster-v9q9c.dynamic.redhatworkshops.io/l3-students/l3-rhads-demoimage-2fe8c138-572d-44e6-90fa-43b5d0353ca8",
  "comment": "Tech Preview - generated for 'quay-v9q9c.apps.cluster-v9q9c.dynamic.redhatworkshops.io/l3-students/l3-rhads-demoimage:latest'",
  "creationInfo": {
    "creators": [
      "Tool: Claircore-v1.5.38",
      "Tool: scanner-v4-matcher-4.8.5"
    ],
    "created": "2025-10-31T13:04:06Z"
  },
  "packages": [
    {
      "name": "ubi8-minimal-container",
      "SPDXID": "SPDXRef-Package-374",
      "versionInfo": "8.5-240.1648458092",
      "packageFileName": "root/buildinfo/Dockerfile-ubi8-minimal-8.5-240.1648458092",
      "downloadLocation": "NOASSERTION",
      "filesAnalyzed": true,
      "primaryPackagePurpose": "SOURCE"
    },

To save it, we just use standard Linux mechanisms:

roxctl image sbom --image=$IMAGE > demo-image-sbom-roxctl.json

OWASP CycloneDX maven plugin

For sake of completeness we’re also mentioning the CycloneDX maven plugin here, since it has been around since 2017 and was one of the first SBOM generators.

However, based on its nature (a maven plugin) it can only generate SBOMs for Java artifacts. It obviously can not generate an SBOM for the container image that the Java Application will run in - nor for the application server (in case of .war or .ear files)

The CycloneDX Maven plugin generates CycloneDX Software Bill of Materials (SBOM) containing the aggregate of all direct and transitive dependencies of a project. It provides three main goals: makeBom (creates a BOM for each Maven module with its dependencies), makeAggregateBom (creates an aggregate BOM at build root with dependencies from the whole multi-modules build), and makePackageBom (creates a BOM for each Maven module with war or ear packaging).

Origin & Development

The plugin is copyright OWASP Foundation and is released under the Apache 2.0 license. The project was incepted in 2017 and has been actively maintained with regular updates,hosted on GitHub under the CycloneDX organization.

Key Plugin Features

By default, the BOM(s) will be attached as additional artifacts with cyclonedx classifier (can be customized by setting cyclonedx.classifier) and xml or json extension during a Maven install or deploy. The plugin offers extensive configuration options including:

  • Multiple output formats (XML, JSON, or both)

  • Configurable CycloneDX schema versions (currently supporting up to 1.6)

  • Scope filtering (compile, provided, runtime, system, test scopes)

  • Component exclusion capabilities by groupId or artifactId

  • License information inclusion options

  • Serial number generation for BOM tracking

Here is an example configuration that will generate a sbom.json in the project’s target directory during the maven package phase:

<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>my-web-app</artifactId>
    <version>1.0.0</version>
    <packaging>war</packaging>

    <build>
        <plugins>
            <plugin>
                <groupId>org.cyclonedx</groupId>
                <artifactId>cyclonedx-maven-plugin</artifactId>
                <version>2.8.0</version>
                <configuration>
                    <!-- Project type -->
                    <projectType>application</projectType>

                    <!-- CycloneDX schema version -->
                    <schemaVersion>1.6</schemaVersion>

                    <!-- Include serial number for tracking -->
                    <includeBomSerialNumber>true</includeBomSerialNumber>

                    <!-- Dependency scopes to include -->
                    <includeCompileScope>true</includeCompileScope>
                    <includeProvidedScope>true</includeProvidedScope>
                    <includeRuntimeScope>true</includeRuntimeScope>
                    <includeSystemScope>true</includeSystemScope>
                    <includeTestScope>false</includeTestScope>

                    <!-- License details -->
                    <includeLicenseText>true</includeLicenseText>

                    <!-- Output format (xml, json, or all) -->
                    <outputFormat>json</outputFormat>

                    <!-- Output file name -->
                    <outputName>sbom</outputName>

                    <!-- Output directory -->
                    <outputDirectory>${project.build.directory}</outputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>makePackageBom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Attesting SBOMs

As we have discussed in the previous exercise, an SBOM is an acceptable, even predefined attestation type that cosign attest accepts and ec validate can parse and work with:

The pre-defined predicate types are:

  • slsaprovenance - SLSA Provenance v0.1

  • slsaprovenance02 - SLSA Provenance v0.2

  • slsaprovenance1 - SLSA Provenance v1.0

  • link - in-toto link attestation

  • spdx - SPDX SBOM format

  • spdxjson - SPDX SBOM in JSON format

  • cyclonedx - CycloneDX SBOM format

  • vuln - Vulnerability scan attestation

  • openvex - OpenVEX attestation

  • custom - Custom attestation (default)

We have created SBOMs of our image above, so naturally we will attest (= sign & attach) them to this image:

(First, making sure that we still have the $IMAGE variable set and the $SIGSTORE_ID_TOKEN hasn’t expired)

cd /workspace/l3-enablement-helpers/security-concepts
if [ -z "${IMAGE}" ]; then
    source ./copy-image-to-quay.sh
fi
echo "\$IMAGE is: $IMAGE"
source ./get-access-token.sh

With these set again, attesting the SBOMs we have created earlier is as simple as

cosign attest --predicate demo-image-sbom-syft.json --type cyclonedx $IMAGE
cosign attest --predicate demo-image-sbom-roxctl.json --type spdxjson $IMAGE

A cosign tree will now show us the SBOM attestation associated with this image:

cosign tree $IMAGE
cosign tree $IMAGE
📦 Supply Chain Security Related artifacts for an image: quay-v9q9c.apps.cluster-v9q9c.dynamic.redhatworkshops.io/l3-students/l3-rhads-demoimage:latest
└── 💾 Attestations for an image tag: quay-v9q9c.apps.cluster-v9q9c.dynamic.redhatworkshops.io/l3-students/l3-rhads-demoimage:sha256-5c50daa8cf06e7c36854343ccc31a99aecc10167d391f2a1d3cc048b63bd29ee.att
   ├── 🍒 sha256:6e1bd0b49e31237c28d14b0e90f69c532defd47685a7ea1e5920e63924adf378
   ├── 🍒 sha256:2c9aaacbdcad589845dfd37f830e878673e41aaa79a5623d23ff152c69fdb5b9
   ├── 🍒 sha256:b37ce919cda4683964533680c5eec92bedc9df10a4e5f4236d68f084a70fa872
   └── 🍒 sha256:72af2fa19513e226e73e70c567bf3d8b763524f0607e09357301dadb1ddb432c
└── 🔐 Signatures for an image tag: quay-v9q9c.apps.cluster-v9q9c.dynamic.redhatworkshops.io/l3-students/l3-rhads-demoimage:sha256-5c50daa8cf06e7c36854343ccc31a99aecc10167d391f2a1d3cc048b63bd29ee.sig
   └── 🍒 sha256:853aa60efb46da00ef2fe35f9343f68c269be888ed3202274df55ff275a780c5

Just from the cosign tree we can just tell that we now have a new attestation, but not what type it is.

The same image in {quay_url}/repository/l3-students/l3-rhads-demoimage?tab=tags[Quay^,window="quay"] just shows us one attestation tag (which contains all of them).

quay signed image attestations

Given the size of the attestation, you can guess that it contains considerable payload (instead of just a bit of text that we attested earlier) - but we can only see it when using cosign download attestation or ec validate image (when using policy rules with the latter)

So, with the knowledge from the previous chapter, we know that a simple cosign download attestation will just give us the in-toto "envelope" with the "Statement" as base64 encoded payload.

If we want to browse what we have, we need to go one level deeper, using jq and base64 --decode:

HINT: Exit less with q or Q

cosign download attestation $IMAGE | jq -r '.payload' | base64 -d | jq | less

Instead of scrolling back and forth with less in a huge list of attestations, we can also check what attestation types (predicateTypes) we have and then only download and inspect the ones we’re interested in:

cosign download attestation $IMAGE | jq -r '.payload' | base64 -d | jq -r '.predicateType' | sort -u
cosign download attestation $IMAGE | jq -r '.payload' | base64 -d | jq -r '.predicateType' | sort -u
https://cyclonedx.org/bom
https://example.com/predicate/v1
https://slsa.dev/provenance/v0.2
https://spdx.dev/Document
cosign download attestation $IMAGE --predicate-type "https://cyclonedx.org/bom" | jq -r '.payload' | base64 -d | jq | less

Simple Signature and Integrity Verification

In the previous chapter (and to more extent in Module 9: Attestations, Tekton Chains & Enterprise Contract with TAS) we used Enterprise Contract / Conforma for the basic verification.

If we simply want to check the integrity and certificate claims (OIDC Identity and OIDC Issuer) without applying policies, we can use cosign verify-attestation:

So, for our image, to verify an attestation (or more than one if we have more of the same type) we would issue

  • using the cosign built-in predicate type

cosign verify-attestation --type spdxjson --certificate-identity pipeline-auth@demo.redhat.com --certificate-oidc-issuer $SIGSTORE_OIDC_ISSUER $IMAGE | jq -r '.payload' | base64 -d | jq | less

or

  • using the explicit predicate type

cosign verify-attestation --type https://spdx.dev/Document --certificate-identity pipeline-auth@demo.redhat.com --certificate-oidc-issuer $SIGSTORE_OIDC_ISSUER $IMAGE | jq -r '.payload' | base64 -d | jq | less
We again added jq to get to the base64 encoded payload (our attestation / SBOM in question and piped it to less since the SBOMs are large). Exit less with q or Q