7.1 Record the promoted image’s SBOM

Track 6 put a signed application digest into production. Now connect that digest to its software inventory in Red Hat Trusted Profile Analyzer (TPA). Work in the Showroom terminal. You will compare the saved CycloneDX SBOM with production, upload it, and download the stored document to verify the same identity and dependency version.

Why this control exists

A software bill of materials (SBOM) lists the components discovered in an artifact. A system of record makes that inventory available after the build finishes. The useful question is “What is in the image we promoted?” An inventory from a different build cannot answer it, even when both images use the same tag.

TPA stores SBOMs and advisory information. In this module you establish the inventory and its image identity. Module 7.2 adds the workshop library advisory and compares its fixed version with the inventory. An uploaded SBOM alone does not establish that a vulnerability is fixed.

Compare the promoted digest with the saved CycloneDX root then upload and retrieve that same inventory from Trusted Profile Analyzer

Learning objectives

By the end of this module, you will be able to:

  • Compare the production image digest with the root component of a saved CycloneDX SBOM.

  • Obtain a workshop OIDC token and upload that inventory to TPA.

  • Retrieve the stored SBOM and verify its digest and Commons Lang version.

  • Explain the difference between an inventory and an advisory about one of its components.

Prepare the evidence

Use the final SBOM saved with the image you signed in Track 5 and promoted in Track 6. Module 4.4 originally saved it under ~/lightwell-build-evidence/build.*; later builds may have produced a replacement. ConfigMap lab-sbom-cyclonedx in the build namespace holds only the most recently persisted inventory. The completed Syft Task’s temporary workspace is gone, so use your saved file or that ConfigMap rather than oc cp from the finished pod.

Run the blocks in order in the same terminal. Stop on a failed command or comparison. You need oc, curl, jq, and access to the production clone at /home/lab-user/lightwell-workspace/gitops-prod.

Exercise

Match production to the saved image

Fetch the production branch and inspect its published values.yaml. Its digest must match the saved signed image and the live Deployment. The Argo CD source and revision must still match the production repository and fetched commit, with Synced and Healthy status, as in module 6.2.

mkdir -p ~/lightwell-compliance-evidence
EVIDENCE="$HOME/lightwell-compliance-evidence"
BUILD_NS="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_build_namespace}')"
PROD_NS="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_prod_namespace}')"
APP="$(oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_prod_argocd_app}')"
IMAGE="$(cat /home/lab-user/lightwell-signing-evidence/image.txt)"
DIGEST="${IMAGE##*@}"
printf '%s\n' "$DIGEST" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo 'Missing signed image digest'; exit 1; }
cd /home/lab-user/lightwell-workspace/gitops-prod
git fetch origin "$(git branch --show-current)"
git show FETCH_HEAD:values.yaml
git rev-parse FETCH_HEAD | tee "$EVIDENCE/prod-commit.txt"
oc -n gitea get configmap lab-gitea -o jsonpath='{.data.student_prod_gitops_repo_url}{"\n"}'
oc -n openshift-gitops get application "$APP" \
  -o jsonpath='repo={.spec.source.repoURL}{"\n"}revision={.status.sync.revision}{"\n"}sync={.status.sync.status}{"\n"}health={.status.health.status}{"\n"}'
PROD_IMAGE="$(oc -n "$PROD_NS" get deploy spring-boot-lw-poc -o jsonpath='{.spec.template.spec.containers[0].image}')"
test "${PROD_IMAGE##*@}" = "$DIGEST" || { echo 'Production differs from the signed image'; exit 1; }
printf '%s\n' "$PROD_IMAGE" | tee "$EVIDENCE/promoted-image.txt"

Select and inspect the matching SBOM

Copy your saved inventory to the evidence directory. Replace the path below with the actual saved file. If it is unavailable, extract the ConfigMap using the alternative block, then perform the same comparison. Do not rebuild merely to recover a file: a new build may have a new digest.

cp /path/to/saved/sbom.cyclonedx.json "$EVIDENCE/sbom.cyclonedx.json"
Alternative: recover the current persisted inventory
oc -n "$BUILD_NS" get configmap lab-sbom-cyclonedx \
  -o jsonpath='{.data.sbom\.cyclonedx\.json}' > "$EVIDENCE/sbom.cyclonedx.json"

Run only one of those copy methods. Success below requires CycloneDX, a nonempty inventory, and a root component version equal to the promoted digest. This workshop’s Syft output records the manifest digest in metadata.component.version. Merely finding the digest somewhere in a library entry is insufficient.

jq -e --arg digest "$DIGEST" '
  .bomFormat == "CycloneDX" and
  .metadata.component.version == $digest and
  (.components | length > 0)
' "$EVIDENCE/sbom.cyclonedx.json"
jq -e '[.components[] | select(.name == "commons-lang3") |
  {name, version, purl}] | select(length > 0)' "$EVIDENCE/sbom.cyclonedx.json"

Compare the printed version with the committed application POM and Track 2’s selected version, 3.14.0.rhlw-00001. A digest mismatch means you selected the wrong inventory; recover the matching saved file before continuing. Do not change the SBOM’s digest field to make it pass.

Authenticate and upload the inventory

Read the analyzer URL and OIDC issuer from the claim. The password and CLI secret stay in shell variables; do not save them in your evidence or Git. The lab commands use -k for the workshop Route certificates. Customer automation should trust its configured CA.

RHTPA_URL="$(oc -n trusted-profile-analyzer get configmap rhtpa-ingestion-info -o jsonpath='{.data.server_url}')"
OIDC_ISSUER="$(oc -n sso get configmap lab-keycloak -o jsonpath='{.data.oidc_issuer}')"
TPA_USER="$(oc -n sso get configmap lab-keycloak -o jsonpath='{.data.workshop_username}')"
TPA_PASS="$(oc -n sso get configmap lab-keycloak -o jsonpath='{.data.workshop_password}')"
CLI_SECRET="$(oc -n trusted-profile-analyzer get secret tpa-realm-cli-clients -o jsonpath='{.data.cli}' | base64 -d)"
TOKEN="$(curl -fkSs --max-time 30 "${OIDC_ISSUER%/}/protocol/openid-connect/token" \
  --data-urlencode grant_type=password --data-urlencode client_id=cli \
  --data-urlencode client_secret="$CLI_SECRET" \
  --data-urlencode username="$TPA_USER" --data-urlencode password="$TPA_PASS" \
  --data-urlencode scope=openid | jq -er '.access_token | select(type == "string" and length > 0)')"
unset TPA_PASS CLI_SECRET
test -n "$TOKEN" || { echo 'Authentication failed'; exit 1; }
curl -fkSs --max-time 60 -X POST -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @"$EVIDENCE/sbom.cyclonedx.json" \
  -o "$EVIDENCE/app-upload.json" -w 'SBOM upload HTTP %{http_code}\n' \
  "${RHTPA_URL%/}/api/v3/sbom"
jq . "$EVIDENCE/app-upload.json"

Expect an HTTP success response. A successful upload is only the first observation; retrieve the document next. If authentication expires during either module, rerun the token block above through test -n, without repeating the upload unnecessarily.

Retrieve and compare the stored document

List TPA’s SBOMs and select entries whose described_by version matches the promoted digest. The listing is paginated. If no match appears, increment OFFSET by 50 and repeat the request and filter until you find the document or reach an empty items list. Allow ingestion to finish before retrying a recent upload.

OFFSET=0
curl -fkSs --max-time 30 -H "Authorization: Bearer $TOKEN" \
  "${RHTPA_URL%/}/api/v3/sbom?limit=50&offset=$OFFSET" \
  -o "$EVIDENCE/sbom-list.json"
jq --arg digest "$DIGEST" '.items[] |
  select(any(.described_by[]?; .version == $digest)) |
  {id, described_by, labels}' "$EVIDENCE/sbom-list.json"

Copy the matching id into the prompt below. The download must pass the same root-digest test and show Commons Lang at the selected version. If several entries match, inspect their labels and choose the CycloneDX document.

read -r -p 'Matching TPA SBOM id: ' SBOM_ID
SBOM_ID_ENCODED="$(printf '%s' "$SBOM_ID" | jq -sRr @uri)"
curl -fkSs --max-time 30 -H "Authorization: Bearer $TOKEN" \
  "${RHTPA_URL%/}/api/v3/sbom/$SBOM_ID_ENCODED/download" \
  -o "$EVIDENCE/tpa-app-sbom.json"
jq -e --arg digest "$DIGEST" '
  .bomFormat == "CycloneDX" and .metadata.component.version == $digest and
  (.components | length > 0)
' "$EVIDENCE/tpa-app-sbom.json"
jq -e '[.components[] | select(.name == "commons-lang3") |
  {name, version, purl}] | select(length > 0)' "$EVIDENCE/tpa-app-sbom.json"
printf '%s\n' "$SBOM_ID" > "$EVIDENCE/tpa-app-sbom-id.txt"

You can also open $RHTPA_URL, sign in to the tpa realm, and find this document in the SBOM list. Use the document ID and digest from your terminal evidence to identify it; a similarly named library SBOM is a separate document.

Review

Compare the fetched production commit, saved signed digest, live Deployment image, and downloaded CycloneDX root. All must identify the promoted artifact. Confirm that both local and retrieved inventories show the expected Commons Lang version. These terminal observations replace the module validation Job and report token.

If any identity differs, stop and resolve the mismatch before interpreting an advisory. Explain why an HTTP success response alone is insufficient and why the library SBOM in the next module cannot replace this application inventory.

Carry the result into module 7.2

Keep ~/lightwell-compliance-evidence, including the promoted image reference, production commit, document ID, and retrieved SBOM. Continue in the same terminal, or restore EVIDENCE, DIGEST, and the authentication variables before the next exercise.

Python inventories may use SPDX rather than CycloneDX. The same identity comparison applies. Red Hat Dependency Analytics is the developer-side workflow described in RHDA shift-left; this exercise records the built application in TPA.

Key takeaways

  • The promoted digest connects production to its inventory.

  • Retrieving the stored document verifies more than a successful upload.

  • Advisory interpretation starts after the artifact and its components are identified.