From 2d12281d63bfde06977e49fa2c740ede496dfb20 Mon Sep 17 00:00:00 2001 From: Daniel Kreling Date: Tue, 30 Jun 2026 18:59:14 -0300 Subject: [PATCH 1/2] konflux: generate real SPDX SBOM for dm-verity image Replace the empty placeholder SBOM with a real SPDX 2.3 SBOM that lists all RPM packages installed in the QCOW2 guest and references the osc-podvm-payload container by digest. The build now captures the RPM inventory via rpm -qa inside the guest, extracts it with virt-copy-out, and transfers it back to the Tekton pod. The sbom-generate step constructs valid SPDX JSON with package entries and PURL references for each RPM and the payload container. The SBOM format was aligned against real SBOMs from osc-caa (syft) and osc-podvm-payload (mobster) to match field structure, PURL conventions, license fields, and checksums. Ref: KATA-5098 Signed-off-by: Daniel Kreling Co-Authored-By: Claude Opus 4.6 --- .tekton/build-dm-verity-image-debug.yaml | 159 ++++++++++++++++-- task/README.md | 9 +- .../0.1/build-dm-verity-image.yaml | 159 ++++++++++++++++-- 3 files changed, 300 insertions(+), 27 deletions(-) diff --git a/.tekton/build-dm-verity-image-debug.yaml b/.tekton/build-dm-verity-image-debug.yaml index 5f5db98e..24e8967f 100644 --- a/.tekton/build-dm-verity-image-debug.yaml +++ b/.tekton/build-dm-verity-image-debug.yaml @@ -175,6 +175,9 @@ spec: -v $BUILD_DIR/activation-key/:/activation-key/:Z \ -t builder -f $BUILD_DIR/source/konflux/Dockerfile $BUILD_DIR/source + # Extract payload container digest from Dockerfile for SBOM + grep 'osc-podvm-payload@sha256:' $BUILD_DIR/source/konflux/Dockerfile | head -1 | grep -o 'sha256:[a-f0-9]*' > output/payload-digest.txt || true + time sudo podman run -d --authfile=$BUILD_DIR/.docker/config.json --rm -it --privileged --pull=newer \ -v ${BUILD_DIR}:/workspace \ -v $(pwd)/output:/output \ @@ -221,6 +224,9 @@ spec: "$BUILDAH_IMAGE" \ /script-push.sh $DISK $MEASUREMENTS + # Copy SBOM data to BUILD_DIR for transfer back to Tekton pod + cp output/rpm-manifest.txt output/payload-digest.txt $BUILD_DIR/ 2>/dev/null || true + REMOTESSHEOF cat >>scripts/script-disk-mods.sh <>scripts/podvm-setup.sh <<'REMOTESSHEOF' @@ -341,6 +350,9 @@ spec: # networking fix firewall-offline-cmd --zone=public --add-port=15150/tcp + + # Capture installed RPM inventory for SBOM generation + rpm -qa --queryformat '%{NAME}|%{EPOCH}|%{VERSION}|%{RELEASE}|%{ARCH}|%{SOURCERPM}\n' | sort > /tmp/rpm-manifest.txt REMOTESSHEOF cat >>scripts/script-verity.sh <<'REMOTESSHEOF' @@ -652,6 +664,10 @@ spec: ssh -v $SSH_ARGS "$SSH_HOST" $BUILD_DIR/scripts/script-build.sh rsync -ra "$SSH_HOST:$BUILD_DIR/tekton-results/*" "/tekton/results/" + + # Retrieve SBOM data from remote host + rsync "$SSH_HOST:$BUILD_DIR/rpm-manifest.txt" "/var/workdir/" 2>/dev/null || true + rsync "$SSH_HOST:$BUILD_DIR/payload-digest.txt" "/var/workdir/" 2>/dev/null || true volumeMounts: - mountPath: /ssh name: ssh @@ -664,21 +680,140 @@ spec: #!/bin/bash set -euo pipefail - OCI_COPY_FILE_PATH=empty-oci-copy.yaml - - # This generates an empty SBOM. Fix this in the future to generate one with real contents - echo "artifacts: []" > $OCI_COPY_FILE_PATH - IMAGE_URL=$(cat "$(results.IMAGE_URL.path)") IMAGE_DIGEST=$(cat "$(results.IMAGE_DIGEST.path)") - mobster generate \ - --output /var/workdir/sbom.json \ - oci-artifact \ - --oci-copy-yaml "$OCI_COPY_FILE_PATH" \ - --image-pullspec "$IMAGE_URL" \ - --image-digest "$IMAGE_DIGEST" \ - --sbom-type "$SBOM_TYPE" + RPM_MANIFEST="/var/workdir/rpm-manifest.txt" + PAYLOAD_DIGEST_FILE="/var/workdir/payload-digest.txt" + SBOM_FILE="/var/workdir/sbom.json" + + IMAGE_PULLSPEC="${IMAGE_URL}@${IMAGE_DIGEST}" + CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "1970-01-01T00:00:00Z") + + { + printf '{\n' + printf ' "spdxVersion": "SPDX-2.3",\n' + printf ' "dataLicense": "CC0-1.0",\n' + printf ' "SPDXID": "SPDXRef-DOCUMENT",\n' + printf ' "name": "%s",\n' "$IMAGE_PULLSPEC" + printf ' "documentNamespace": "https://konflux-ci.dev/spdxdocs/%s",\n' "$IMAGE_PULLSPEC" + printf ' "creationInfo": {\n' + printf ' "created": "%s",\n' "$CREATED" + printf ' "creators": ["Organization: Red Hat", "Tool: coco-podvm-scripts-sbom-generator"],\n' + printf ' "licenseListVersion": "3.25"\n' + printf ' },\n' + + printf ' "packages": [\n' + + printf ' {\n' + printf ' "SPDXID": "SPDXRef-image",\n' + printf ' "name": "osc-dm-verity-image",\n' + printf ' "versionInfo": "%s",\n' "$IMAGE_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${IMAGE_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-dm-verity-image@%s?repository_url=%s"\n' "$IMAGE_DIGEST" "$IMAGE_URL" + printf ' }]\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + PURL="pkg:rpm/redhat/${NAME}@${VERSION}-${RELEASE}?arch=${ARCH}&distro=rhel-10.0" + if [ "$EPOCH" != "(none)" ] && [ -n "$EPOCH" ]; then + PURL="${PURL}&epoch=${EPOCH}" + fi + if [ -n "$SOURCERPM" ] && [ "$SOURCERPM" != "(none)" ]; then + PURL="${PURL}&upstream=${SOURCERPM}" + fi + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "name": "%s",\n' "$NAME" + printf ' "versionInfo": "%s-%s",\n' "$VERSION" "$RELEASE" + printf ' "supplier": "Organization: Red Hat, Inc.",\n' + printf ' "originator": "Organization: Red Hat, Inc.",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "licenseDeclared": "NOASSERTION",\n' + printf ' "copyrightText": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "%s"\n' "$PURL" + printf ' }]\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-payload",\n' + printf ' "name": "osc-podvm-payload",\n' + printf ' "versionInfo": "%s",\n' "$PAYLOAD_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${PAYLOAD_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-podvm-payload@%s?repository_url=quay.io/redhat-user-workloads/ose-osc-tenant"\n' "$PAYLOAD_DIGEST" + printf ' }]\n' + printf ' }' + fi + fi + + printf '\n ],\n' + + printf ' "relationships": [\n' + printf ' {\n' + printf ' "spdxElementId": "SPDXRef-DOCUMENT",\n' + printf ' "relatedSpdxElement": "SPDXRef-image",\n' + printf ' "relationshipType": "DESCRIBES"\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-payload",\n' + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + fi + fi + + printf '\n ]\n' + printf '}\n' + } > "$SBOM_FILE" + + echo "Generated SBOM with $RPM_COUNT RPM packages at $SBOM_FILE" - name: upload-sbom image: quay.io/konflux-ci/appstudio-utils:1610c1fc4cfc9c9053dbefc1146904a4df6659ef@sha256:90ac97b811073cb99a23232c15a08082b586c702b85da6200cf54ef505e3c50c diff --git a/task/README.md b/task/README.md index d6000baf..cc8f710b 100644 --- a/task/README.md +++ b/task/README.md @@ -31,7 +31,7 @@ At a high level, the task is doing the following: and verify checksum. 2. Copy sources and required files to the remote SSH build host and run a script remotely to build the QCOW image. -3. Generate a minimal SBOM for the published image. +3. Generate an SBOM for the published image. Additional steps and code exist in the task to share context with the other tasks in our CI (running before or after it). We won't detail them in this document. @@ -104,8 +104,11 @@ them to build our image. ### step "sbom-generate" -This step is required for Konflux CI, but we are currently doing a very simple -(empty) SBOM. We need to improve this. +This step generates an SPDX 2.3 SBOM listing all RPM packages installed in the +QCOW2 guest image and a reference to the `osc-podvm-payload` container (by +digest). The RPM inventory is captured during the build via `rpm -qa` inside +the guest, extracted with `virt-copy-out`, and transferred back to the Tekton +pod via rsync. ## Build and publish the task diff --git a/task/build-dm-verity-image/0.1/build-dm-verity-image.yaml b/task/build-dm-verity-image/0.1/build-dm-verity-image.yaml index cfa0b728..e5a362c2 100644 --- a/task/build-dm-verity-image/0.1/build-dm-verity-image.yaml +++ b/task/build-dm-verity-image/0.1/build-dm-verity-image.yaml @@ -175,6 +175,9 @@ spec: -v $BUILD_DIR/activation-key/:/activation-key/:Z \ -t builder -f $BUILD_DIR/source/konflux/Dockerfile $BUILD_DIR/source + # Extract payload container digest from Dockerfile for SBOM + grep 'osc-podvm-payload@sha256:' $BUILD_DIR/source/konflux/Dockerfile | head -1 | grep -o 'sha256:[a-f0-9]*' > output/payload-digest.txt || true + time sudo podman run -d --authfile=$BUILD_DIR/.docker/config.json --rm -it --privileged --pull=newer \ -v ${BUILD_DIR}:/workspace \ -v $(pwd)/output:/output \ @@ -221,6 +224,9 @@ spec: "$BUILDAH_IMAGE" \ /script-push.sh $DISK $MEASUREMENTS + # Copy SBOM data to BUILD_DIR for transfer back to Tekton pod + cp output/rpm-manifest.txt output/payload-digest.txt $BUILD_DIR/ 2>/dev/null || true + REMOTESSHEOF cat >>scripts/script-disk-mods.sh <>scripts/podvm-setup.sh <<'REMOTESSHEOF' @@ -341,6 +350,9 @@ spec: # networking fix firewall-offline-cmd --zone=public --add-port=15150/tcp + + # Capture installed RPM inventory for SBOM generation + rpm -qa --queryformat '%{NAME}|%{EPOCH}|%{VERSION}|%{RELEASE}|%{ARCH}|%{SOURCERPM}\n' | sort > /tmp/rpm-manifest.txt REMOTESSHEOF cat >>scripts/script-verity.sh <<'REMOTESSHEOF' @@ -652,6 +664,10 @@ spec: ssh -v $SSH_ARGS "$SSH_HOST" $BUILD_DIR/scripts/script-build.sh rsync -ra "$SSH_HOST:$BUILD_DIR/tekton-results/*" "/tekton/results/" + + # Retrieve SBOM data from remote host + rsync "$SSH_HOST:$BUILD_DIR/rpm-manifest.txt" "/var/workdir/" 2>/dev/null || true + rsync "$SSH_HOST:$BUILD_DIR/payload-digest.txt" "/var/workdir/" 2>/dev/null || true volumeMounts: - mountPath: /ssh name: ssh @@ -664,21 +680,140 @@ spec: #!/bin/bash set -euo pipefail - OCI_COPY_FILE_PATH=empty-oci-copy.yaml - - # This generates an empty SBOM. Fix this in the future to generate one with real contents - echo "artifacts: []" > $OCI_COPY_FILE_PATH - IMAGE_URL=$(cat "$(results.IMAGE_URL.path)") IMAGE_DIGEST=$(cat "$(results.IMAGE_DIGEST.path)") - mobster generate \ - --output /var/workdir/sbom.json \ - oci-artifact \ - --oci-copy-yaml "$OCI_COPY_FILE_PATH" \ - --image-pullspec "$IMAGE_URL" \ - --image-digest "$IMAGE_DIGEST" \ - --sbom-type "$SBOM_TYPE" + RPM_MANIFEST="/var/workdir/rpm-manifest.txt" + PAYLOAD_DIGEST_FILE="/var/workdir/payload-digest.txt" + SBOM_FILE="/var/workdir/sbom.json" + + IMAGE_PULLSPEC="${IMAGE_URL}@${IMAGE_DIGEST}" + CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "1970-01-01T00:00:00Z") + + { + printf '{\n' + printf ' "spdxVersion": "SPDX-2.3",\n' + printf ' "dataLicense": "CC0-1.0",\n' + printf ' "SPDXID": "SPDXRef-DOCUMENT",\n' + printf ' "name": "%s",\n' "$IMAGE_PULLSPEC" + printf ' "documentNamespace": "https://konflux-ci.dev/spdxdocs/%s",\n' "$IMAGE_PULLSPEC" + printf ' "creationInfo": {\n' + printf ' "created": "%s",\n' "$CREATED" + printf ' "creators": ["Organization: Red Hat", "Tool: coco-podvm-scripts-sbom-generator"],\n' + printf ' "licenseListVersion": "3.25"\n' + printf ' },\n' + + printf ' "packages": [\n' + + printf ' {\n' + printf ' "SPDXID": "SPDXRef-image",\n' + printf ' "name": "osc-dm-verity-image",\n' + printf ' "versionInfo": "%s",\n' "$IMAGE_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${IMAGE_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-dm-verity-image@%s?repository_url=%s"\n' "$IMAGE_DIGEST" "$IMAGE_URL" + printf ' }]\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + PURL="pkg:rpm/redhat/${NAME}@${VERSION}-${RELEASE}?arch=${ARCH}&distro=rhel-10.0" + if [ "$EPOCH" != "(none)" ] && [ -n "$EPOCH" ]; then + PURL="${PURL}&epoch=${EPOCH}" + fi + if [ -n "$SOURCERPM" ] && [ "$SOURCERPM" != "(none)" ]; then + PURL="${PURL}&upstream=${SOURCERPM}" + fi + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "name": "%s",\n' "$NAME" + printf ' "versionInfo": "%s-%s",\n' "$VERSION" "$RELEASE" + printf ' "supplier": "Organization: Red Hat, Inc.",\n' + printf ' "originator": "Organization: Red Hat, Inc.",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "licenseDeclared": "NOASSERTION",\n' + printf ' "copyrightText": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "%s"\n' "$PURL" + printf ' }]\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-payload",\n' + printf ' "name": "osc-podvm-payload",\n' + printf ' "versionInfo": "%s",\n' "$PAYLOAD_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${PAYLOAD_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-podvm-payload@%s?repository_url=quay.io/redhat-user-workloads/ose-osc-tenant"\n' "$PAYLOAD_DIGEST" + printf ' }]\n' + printf ' }' + fi + fi + + printf '\n ],\n' + + printf ' "relationships": [\n' + printf ' {\n' + printf ' "spdxElementId": "SPDXRef-DOCUMENT",\n' + printf ' "relatedSpdxElement": "SPDXRef-image",\n' + printf ' "relationshipType": "DESCRIBES"\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-payload",\n' + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + fi + fi + + printf '\n ]\n' + printf '}\n' + } > "$SBOM_FILE" + + echo "Generated SBOM with $RPM_COUNT RPM packages at $SBOM_FILE" - name: upload-sbom image: quay.io/konflux-ci/appstudio-utils:1610c1fc4cfc9c9053dbefc1146904a4df6659ef@sha256:90ac97b811073cb99a23232c15a08082b586c702b85da6200cf54ef505e3c50c From abdd7a23910b86e231d102a8bba3982c238190ed Mon Sep 17 00:00:00 2001 From: Daniel Kreling Date: Wed, 1 Jul 2026 14:35:30 -0300 Subject: [PATCH 2/2] test: use local task to verify SBOM generation DO NOT MERGE - temporary commit to test the SBOM changes by embedding the task directly instead of referencing the quay.io bundle. Signed-off-by: Daniel Kreling Co-Authored-By: Claude Opus 4.6 --- .tekton/build-dm-verity-image.yaml | 847 +++++++++++++++++++++++++++++ .tekton/build-pipeline.yaml | 9 +- 2 files changed, 848 insertions(+), 8 deletions(-) create mode 100644 .tekton/build-dm-verity-image.yaml diff --git a/.tekton/build-dm-verity-image.yaml b/.tekton/build-dm-verity-image.yaml new file mode 100644 index 00000000..e5a362c2 --- /dev/null +++ b/.tekton/build-dm-verity-image.yaml @@ -0,0 +1,847 @@ +apiVersion: tekton.dev/v1 +kind: Task +metadata: + annotations: + tekton.dev/pipelines.minVersion: 0.12.1 + tekton.dev/tags: image-build, konflux + labels: + app.kubernetes.io/version: "0.1" + build.appstudio.redhat.com/build_type: disk-image + name: build-dm-verity-image +spec: + description: "Build disk images for sandboxed containers" + params: + - name: OUTPUT_IMAGE + type: string + description: The output manifest list that points to the OCI artifact of the zipped image + - name: SOURCE_ARTIFACT + type: string + - default: activation-key + description: Name of secret which contains subscription activation key + name: ACTIVATION_KEY + type: string + - name: RHEL_IMAGE_CHECKSUM + type: string + description: The checksum to use for downloading the RHEL image + default: e15cb333529c332e76e4b1b946efe3515c99f996546675aec18e8effdf2540a5 + - name: KERNEL_VERSION + type: string + description: The kernel version that the stock kernel included with the RHEL image will be updated to + default: 6.12.0-211.16.1.el10_2 + - name: NVIDIA_DRIVER_VERSION + type: string + description: The NVIDIA driver version to install + default: 595.58.03 + - default: redhat-api-secret + description: Name of secret which contains the offline token for the Red Hat API + name: REDHAT_OFFLINE_TOKEN_SECRET + type: string + - name: LABELS + type: string + description: A comma-separated list of key=value pairs to add as labels to the built image + results: + - description: Digest of the manifest list just built + name: IMAGE_DIGEST + - description: Image repository where the built manifest list was pushed + name: IMAGE_URL + - description: Image reference (IMAGE_URL + IMAGE_DIGEST) + name: IMAGE_REFERENCE + - name: SBOM_BLOB_URL + stepTemplate: + env: + - name: OUTPUT_IMAGE + value: $(params.OUTPUT_IMAGE) + - name: ACTIVATION_KEY + value: $(params.ACTIVATION_KEY) + - name: KERNEL_VERSION + value: $(params.KERNEL_VERSION) + - name: NVIDIA_DRIVER_VERSION + value: $(params.NVIDIA_DRIVER_VERSION) + - name: BUILDAH_IMAGE + value: 'registry.access.redhat.com/ubi9/buildah:9.5-1739778322' + - name: SBOM_TYPE + value: spdx + - name: LABELS + value: $(params.LABELS) + volumeMounts: + - mountPath: "/var/workdir" + name: workdir + - mountPath: "/var/lib/containers/storage" + name: varlibcontainers + steps: + - name: use-trusted-artifact + image: quay.io/konflux-ci/build-trusted-artifacts:latest@sha256:4689f88dd253bd1feebf57f1a76a5a751880f739000719cd662bbdc76990a7fd + args: + - use + - $(params.SOURCE_ARTIFACT)=/var/workdir/source + - name: download-rhel-image + image: quay.io/konflux-ci/hermeto:0.29.0@sha256:f577e0399953471df7a9826c1550aef83d28e8b35f76dd65a193441822b629ee + env: + - name: REDHAT_OFFLINE_TOKEN + valueFrom: + secretKeyRef: + name: $(params.REDHAT_OFFLINE_TOKEN_SECRET) + key: offline-token + - name: RHEL_IMAGE_CHECKSUM + value: $(params.RHEL_IMAGE_CHECKSUM) + script: |- + #!/bin/bash + set -euo pipefail + + # Check that required variables are populated + if [ -z "${REDHAT_OFFLINE_TOKEN:-}" ]; then + echo "Error: REDHAT_OFFLINE_TOKEN is not set" + exit 1 + fi + + if [ -z "${RHEL_IMAGE_CHECKSUM:-}" ]; then + echo "Error: RHEL_IMAGE_CHECKSUM is not set" + exit 1 + fi + + echo "Using RHEL image checksum: ${RHEL_IMAGE_CHECKSUM}" + + alias curl="curl --retry 5" + token_api_url=https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token + download_api_url=https://api.access.redhat.com/management/v1/images/${RHEL_IMAGE_CHECKSUM}/download + token=$(curl "${token_api_url}" \ + -d grant_type=refresh_token -d client_id=rhsm-api -d refresh_token="${REDHAT_OFFLINE_TOKEN}" | jq --raw-output .access_token) + download_url=$(curl -X 'GET' "${download_api_url}" \ + -H "Authorization: Bearer ${token}" -H 'accept: application/json' | jq --raw-output .body.href) + filepath="/var/workdir/rhel.iso" + curl -X GET "${download_url}" -H "Authorization: Bearer ${token}" --output "${filepath}" + echo "${RHEL_IMAGE_CHECKSUM}" "${filepath}" | sha256sum --check + - name: build + image: quay.io/konflux-ci/buildah-task:latest@sha256:b82d465a06c926882d02b721cf8a8476048711332749f39926a01089cf85a3f9 + computeResources: + limits: + memory: 512Mi + requests: + cpu: 250m + memory: 128Mi + script: |- + #!/bin/bash + set -o verbose + set -eu + set -x + + mkdir -p ~/.ssh + if [ -e "/ssh/error" ]; then + #no server could be provisioned + cat /ssh/error + exit 1 + elif [ -e "/ssh/otp" ]; then + curl --cacert /ssh/otp-ca -XPOST -d @/ssh/otp $(cat /ssh/otp-server) >~/.ssh/id_rsa + echo "" >> ~/.ssh/id_rsa + else + cp /ssh/id_rsa ~/.ssh + fi + chmod 0400 ~/.ssh/id_rsa + export SSH_HOST=$(cat /ssh/host) + export BUILD_DIR=$(cat /ssh/user-dir) + export SSH_ARGS="-o StrictHostKeyChecking=no -o ServerAliveInterval=60" + mkdir -p scripts + echo "$BUILD_DIR" + ssh -v $SSH_ARGS "$SSH_HOST" mkdir -p "$BUILD_DIR/workspaces" "$BUILD_DIR/scripts" "$BUILD_DIR/tmp" "$BUILD_DIR/tekton-results" "$BUILD_DIR/activation-key" + + rsync -ra "$HOME/.docker/" "$SSH_HOST:$BUILD_DIR/.docker/" + rsync -ra /activation-key/ "$SSH_HOST:$BUILD_DIR/activation-key/" + + rsync -ra "/var/workdir/rhel.iso" "$SSH_HOST:$BUILD_DIR/" + rsync -ra "/var/workdir/source" "$SSH_HOST:$BUILD_DIR/" + + # this unquoted heredoc allows expansions for the image name + cat >scripts/script-build.sh <>scripts/script-build.sh <<'REMOTESSHEOF' + + mkdir -p output/qcow2/ + echo "RUNNING BUILD" + + # Define paths + DISK=/output/qcow2/disk.qcow2 + MEASUREMENTS=/output/qcow2/measurements.json + + time sudo podman build --authfile=$BUILD_DIR/.docker/config.json \ + -v $BUILD_DIR/activation-key/:/activation-key/:Z \ + -t builder -f $BUILD_DIR/source/konflux/Dockerfile $BUILD_DIR/source + + # Extract payload container digest from Dockerfile for SBOM + grep 'osc-podvm-payload@sha256:' $BUILD_DIR/source/konflux/Dockerfile | head -1 | grep -o 'sha256:[a-f0-9]*' > output/payload-digest.txt || true + + time sudo podman run -d --authfile=$BUILD_DIR/.docker/config.json --rm -it --privileged --pull=newer \ + -v ${BUILD_DIR}:/workspace \ + -v $(pwd)/output:/output \ + -v /lib/modules:/lib/modules:ro,Z \ + -v $BUILD_DIR/activation-key/:/activation-key/:Z \ + --user 0 \ + --security-opt=apparmor=unconfined \ + --security-opt=seccomp=unconfined \ + --mount type=bind,source=/dev,target=/dev \ + --mount type=bind,source=/run/udev,target=/run/udev \ + --env=LIBGUESTFS_BACKEND=direct \ + builder + + time sudo podman exec --latest bash -c 'until virsh list --all >/dev/null 2>&1; do sleep 2; done' + + time sudo podman exec -t --latest virt-install --virt-type qemu --cpu host-model --os-variant rhel10.0 --arch x86_64 --boot uefi --name disk --memory 8192 --location /workspace/rhel.iso --disk path=$DISK,format=qcow2,bus=scsi,size=7 --initrd-inject=/workspace/source/helpers/rhel10-dm-root.ks --nographics --extra-args 'console=ttyS0 inst.ks=file:/rhel10-dm-root.ks' --transient + + # disk modifications in registered system + time sudo podman exec -t --latest virt-customize \ + --copy-in /activation-key:/tmp/ \ + --run-command "subscription-manager register --org \$(cat /tmp/activation-key/org) --activationkey \$(cat /tmp/activation-key/activationkey)" \ + --run /workspace/scripts/script-disk-mods.sh \ + --run-command "subscription-manager unregister" \ + --run-command "rm -rf /tmp/activation-key" \ + -a $DISK + + time sudo podman exec -t --latest /workspace/scripts/script-podvm-maker.sh + + # debug varient is being created at this point + + # Apply dm-verity + time sudo podman exec -t --latest /workspace/scripts/script-verity.sh $DISK + + # Measure disk + time sudo podman exec -t --latest /workspace/scripts/podvm-measure.sh $DISK $MEASUREMENTS + + # Push image + time sudo podman run --authfile=$BUILD_DIR/.docker/config.json --rm -it --privileged --pull=newer --security-opt label=type:unconfined_t \ + -v $BUILD_DIR/.docker:/.docker \ + -v $(pwd)/output:/output \ + -v /var/lib/containers/storage:/var/lib/containers/storage \ + -v $BUILD_DIR/scripts/script-push.sh:/script-push.sh \ + -v $BUILD_DIR/tekton-results:/tekton-results \ + "$BUILDAH_IMAGE" \ + /script-push.sh $DISK $MEASUREMENTS + + # Copy SBOM data to BUILD_DIR for transfer back to Tekton pod + cp output/rpm-manifest.txt output/payload-digest.txt $BUILD_DIR/ 2>/dev/null || true + + REMOTESSHEOF + + cat >>scripts/script-disk-mods.sh < /boot/efi/EFI/redhat/BOOTX64.CSV + + echo "removing previous kernel pkgs:" \$(rpm -qa "kernel*" | grep -Ev "${KERNEL_VERSION}|^kernel-uki") + rpm -qa "kernel*" | grep -Ev "${KERNEL_VERSION}|^kernel-uki" | xargs -r rpm -e --nodeps + + # TODO: check if this still needed when we switch to using NVIDIA attestation RPMs. + dnf install -y xmlsec1 xmlsec1-openssl + + ##### NVIDIA DRIVERS + if [ -n "${NVIDIA_DRIVER_VERSION}" ]; then + subscription-manager repos --enable=rhel-10-for-x86_64-supplementary-rpms + subscription-manager repos --enable=rhel-10-for-x86_64-extensions-rpms + dnf install -y --setopt=install_weak_deps=False nvidia-driver-${NVIDIA_DRIVER_VERSION} \ + nvidia-driver-cuda-${NVIDIA_DRIVER_VERSION} \ + nvidia-driver-libs-${NVIDIA_DRIVER_VERSION} \ + nvidia-persistenced-${NVIDIA_DRIVER_VERSION} \ + kmod-nvidia-open-${NVIDIA_DRIVER_VERSION}-${KERNEL_VERSION%.el*} \ + nvidia-container-toolkit + dnf clean all + + echo -e "blacklist nouveau\nblacklist nova_core" > /etc/modprobe.d/blacklist_nv_alt.conf + sed -i 's/^#no-cgroups = false/no-cgroups = true/' /etc/nvidia-container-runtime/config.toml + + cat << 'EOF' > /usr/local/bin/generate-nvidia-cdi.sh + #!/bin/bash + + # Check if NVIDIA GPU is present + if ! lspci | grep -i nvidia > /dev/null 2>&1; then + echo "No NVIDIA GPU detected, skipping NVIDIA setup" | tee /var/log/nvidia-cdi-gen.log + exit 0 + fi + + # Load drivers + nvidia-ctk -d system create-device-nodes --control-devices --load-kernel-modules + + nvidia-persistenced + + # Set confidential compute to ready state (non-fatal if unsupported) + if nvidia-smi conf-compute -srs 1 2>/dev/null; then + echo "Confidential Compute enabled" | tee -a /var/log/nvidia-cdi-gen.log + else + echo "Could not set Confidential Compute GPUs to Ready State" | tee -a /var/log/nvidia-cdi-gen.log + fi + + # Generate NVIDIA CDI configuration + nvidia-ctk cdi generate --output=/var/run/cdi/nvidia.yaml >> /var/log/nvidia-cdi-gen.log 2>&1 || exit 1 + EOF + chmod 755 /usr/local/bin/generate-nvidia-cdi.sh + + cat <<'EOF' > /etc/systemd/system/nvidia-cdi.service + [Unit] + Description=Generate NVIDIA CDI Configuration + Before=kata-agent.service + + [Service] + Type=oneshot + ExecStart=/usr/local/bin/generate-nvidia-cdi.sh + RemainAfterExit=true + + [Install] + WantedBy=multi-user.target + EOF + chmod 644 /etc/systemd/system/nvidia-cdi.service + ln -s /etc/systemd/system/nvidia-cdi.service /etc/systemd/system/multi-user.target.wants/nvidia-cdi.service + fi + + REMOTESSHEOF + + cat >>scripts/script-podvm-maker.sh <<'REMOTESSHEOF' + #!/bin/bash + set -ex + + DISK=/output/qcow2/disk.qcow2 + + root_tar_dir=/workspace/source/konflux/podvm-root + root_tar_file=${root_tar_dir}/podvm-root.tar.gz + pushd ${root_tar_dir} + tar -czvf ${root_tar_file} * + popd + + virt-customize \ + --copy-in /payload/podvm-binaries.tar.gz:/tmp/ \ + --copy-in /payload/pause-bundle.tar.gz:/tmp/ \ + --copy-in ${root_tar_file}:/tmp/ \ + --run /workspace/scripts/podvm-setup.sh \ + -a $DISK + + # Extract RPM manifest from guest for SBOM generation + virt-copy-out -a $DISK /tmp/rpm-manifest.txt /output/ + REMOTESSHEOF + + cat >>scripts/podvm-setup.sh <<'REMOTESSHEOF' + #!/bin/bash + set -ex + + tar -xzvf /tmp/podvm-binaries.tar.gz -C / + tar -xzvf /tmp/pause-bundle.tar.gz -C / + tar -xzvf /tmp/podvm-root.tar.gz -C / + + dnf remove -y cloud-init WALinuxAgent + + # fixes a failure of the podns@netns service, paths differ due to Selinux equivalency rules + semanage fcontext -a -t bin_t /usr/bin/ip && restorecon -v /usr/sbin/ip + + # this will allow /run/issue and /run/issue.d to take precedence + mv /etc/issue.d /usr/lib/issue.d || true + rm -f /etc/issue.net + rm -f /etc/issue + + systemctl enable afterburn-checkin.service + systemctl enable luks-scratch.service + systemctl enable gen-issue.service + + # networking fix + firewall-offline-cmd --zone=public --add-port=15150/tcp + + # Capture installed RPM inventory for SBOM generation + rpm -qa --queryformat '%{NAME}|%{EPOCH}|%{VERSION}|%{RELEASE}|%{ARCH}|%{SOURCERPM}\n' | sort > /tmp/rpm-manifest.txt + REMOTESSHEOF + + cat >>scripts/script-verity.sh <<'REMOTESSHEOF' + #!/bin/bash + set -ex + + # Accept disk path as first argument + if [ -z "$1" ]; then + echo "Error: Disk path not provided" + echo "Usage: $0 " + exit 1 + fi + + DISK="$1" + + ADDON_SBAT="sbat,1,SBAT Version,sbat,1,https://github.com/rhboot/shim/blob/main/SBAT.md + coco-podvm-uki-addon,1,Red Hat,coco-podvm-uki-addon,1,mailto:secalert@redhat.com" + + NBD_DEVICE=/dev/nbd0 + EFI_PN=nbd0p1 + ROOT_PN=nbd0p2 + + LUKS_MINIMAL_SPACE_MB=2500 + VERITY_MAX_SPACE_MB=512 + DISK_FORMAT=qcow2 + + # Resize the disk + MB=$((1024 * 1024)) + current_size=$(qemu-img info -f $DISK_FORMAT --output json $DISK | jq '."virtual-size"') + # new_size=$((current_size * 110 / 100)) # increase 10% for verity - obsolete + luks_min_space=$((LUKS_MINIMAL_SPACE_MB * MB)) + verity_max_space=$((current_size * 7 / 100)) + new_size=$((current_size + luks_min_space + verity_max_space)) + rounded_size=$(((new_size + MB - 1) / MB * MB)) + echo "Current disk size: $current_size" + echo "New disk size: $rounded_size" + qemu-img resize "$DISK" -f $DISK_FORMAT "${rounded_size}" + + temp_dir=$(mktemp -d) + cd $temp_dir + + temp_mount=$temp_dir/mnt + mkdir $temp_mount + + modprobe nbd + qemu-nbd -c $NBD_DEVICE -f $DISK_FORMAT $DISK + udevadm settle + sleep 2 + + # Debug + fdisk -l $NBD_DEVICE + + # call_fsck + fs_type=$(blkid -o value -s TYPE /dev/$ROOT_PN) + fsck.$fs_type -p /dev/$ROOT_PN + + # update the values in the conf files + sed -i "s|{INSERT_HERE}|$current_size|g" /workspace/source/konflux/verity-definitions/root.conf + sed -i "s|{INSERT_HERE}|$verity_max_space|g" /workspace/source/konflux/verity-definitions/verity.conf + + # apply_dmverity + # create config files and folders for systemd-repart and UKI + systemd-repart $NBD_DEVICE --dry-run=no --definitions=/workspace/source/konflux/verity-definitions/ --no-pager --json=pretty | jq -r '.[] | select(.type == "root-x86-64-verity") | .roothash' > roothash.txt + RH=$(cat roothash.txt) + + partprobe $NBD_DEVICE + # Allow udev events to settle again after partprobe + udevadm settle + sleep 1 # Optional small sleep just in case + + if [ "$RH" == "TBD" ]; then + echo "roothash is TBD, something went wrong. Make sure the image you are using doesn't have a /verity partition already!" + echo "Exiting." + exit 1 + fi + + # create_uki_addon + UKI_FOLDER=$temp_mount/EFI/Linux + ADDON_NAME=verity.addon.efi + mount /dev/$EFI_PN $temp_mount + efi_files=($UKI_FOLDER/*.efi) + # Check if any EFI files exist + if [[ ${#efi_files[@]} -eq 0 || ! -f "${efi_files[0]}" ]]; then + echo "Error: No .efi files found in $UKI_FOLDER" + exit 1 + fi + + # If multiple files, pick the most recent one + if [[ ${#efi_files[@]} -gt 1 ]]; then + echo "Found ${#efi_files[@]} EFI files: ${efi_files[@]}" + echo "" + echo "Current EFI fallback value (/boot/efi/EFI/redhat/BOOTX64.CSV):" + cat mnt/EFI/redhat/BOOTX64.CSV + echo "" + echo "Selecting the most recently modified UKI..." + UKI_NAME=$(ls -t "${efi_files[@]}" | head -1) + else + UKI_NAME=${efi_files[0]} + fi + + echo "Using UKI: $UKI_NAME" + mkdir -p "$UKI_NAME.extra.d" + cd $UKI_NAME.extra.d + rm -f $ADDON_NAME + + if [[ -n "$SB_PRIVATE_KEY" && -n "$SB_CERTIFICATE" ]]; then + ADDON_OPTIONS="--secureboot-private-key=$SB_PRIVATE_KEY --secureboot-certificate=$SB_CERTIFICATE" + echo "Signing addon with $SB_PRIVATE_KEY and $SB_CERTIFICATE" + fi + /usr/lib/systemd/ukify build --cmdline="roothash=$RH systemd.volatile=overlay" --output=$ADDON_NAME --sbat="$ADDON_SBAT" $ADDON_OPTIONS + echo "Created UKI addon $UKI_NAME.extra.d/$ADDON_NAME" + /usr/lib/systemd/ukify inspect $ADDON_NAME + cd + umount $temp_mount + + # Debug + fdisk -l $NBD_DEVICE + + # umount + qemu-nbd --disconnect $NBD_DEVICE + REMOTESSHEOF + + # script-push.sh script is intended run _inside_ podman on the ssh host and requires sudo + # this unquoted heredoc allows expansions for the image name + cat >scripts/script-push.sh <>scripts/script-push.sh <<'REMOTESSHEOF' + # This script pushes a PodVM disk image to a container registry + # Arguments: + # $1: DISK_PATH - Path to the qcow2 disk image + # $2: MEASUREMENTS_PATH - Path to the measurements JSON file + + if [ -z "$1" ] || [ -z "$2" ]; then + echo "Error: Disk path and measurements path not provided" + echo "Usage: $0 [suffix]" + exit 1 + fi + + DISK_PATH="$1" + MEASUREMENTS_PATH="$2" + + dnf -y install buildah skopeo pigz jq + + image_name="dm-verity-image" + buildah_container=$(buildah from scratch) + buildah add $buildah_container "$DISK_PATH" image/podvm.qcow2 + buildah add $buildah_container "$MEASUREMENTS_PATH" image/measurements.json + # Get the list of labels from the LABELS env variable and apply them to the image + IFS=',' read -ra LABEL_ARRAY <<< "$LABELS" + for label in "${LABEL_ARRAY[@]}"; do + # Remove leading and trailing whitespace from the label + label=$(echo "$label" | xargs) + buildah config --label "$label" $buildah_container + done + + # Add PCR values as OCI manifest annotations + ANNOTATION_ARGS=() + if [ -f "$MEASUREMENTS_PATH" ]; then + # Extract hash algorithm and PCR values from measurements.json + alg=$(jq -r '.measurements | keys[0]' "$MEASUREMENTS_PATH") + + # Add hash algorithm as separate annotation + ANNOTATION_ARGS+=(--annotation "io.github.confidential-containers.measurements.algorithm=${alg}") + + # Add each PCR value as a separate annotation + for pcr in $(jq -r ".measurements.${alg} | keys[]" "$MEASUREMENTS_PATH"); do + pcr_value=$(jq -r ".measurements.${alg}.${pcr}" "$MEASUREMENTS_PATH") + ANNOTATION_ARGS+=(--annotation "io.github.confidential-containers.measurements.${pcr}=${pcr_value}") + done + fi + + buildah commit "${ANNOTATION_ARGS[@]}" $buildah_container $image_name + buildah push --digestfile image-digest --authfile /.docker/config.json --retry 10 --all $image_name $OUTPUT_IMAGE + + MANIFEST_DIGEST=$(cat image-digest) + + # Finally, record all that in our results + echo -n "$OUTPUT_IMAGE" | tee /tekton-results/IMAGE_URL + echo -n $MANIFEST_DIGEST | tee /tekton-results/IMAGE_DIGEST + # Saving also these two output in one unique variable. This task is using a matrix reference. + # Unfortunately it seems that in Tekton, when using a matrix, each task run is executed in isolation, + # and result values can't be dynamically constructed or reused across matrix combinations. + # In order to prevent that, we are preparing in the task itself what we'll call as `IMAGE_REFERENCE` + # so that we can reference that safely in the pipeline. + IMAGE_URL_CLEANED=$(echo -n "$OUTPUT_IMAGE" | tr -d '\n') + echo -n "${IMAGE_URL_CLEANED}@${MANIFEST_DIGEST}" | tee /tekton-results/IMAGE_REFERENCE + REMOTESSHEOF + + cat >scripts/podvm-measure.sh <<'REMOTESSHEOF' + #!/bin/bash + set -euo pipefail + + # This script measures TPM PCR values from a VM disk image + # Arguments: + # $1: DISK - Path to the qcow2 disk image to boot and measure + # $2: OUTPUT_FILE - Path where to write the measurements JSON file + + # Accept disk path and output file as arguments + if [ -z "$1" ] || [ -z "$2" ]; then + echo "Error: Disk path and output file not provided" + echo "Usage: $0 " + exit 1 + fi + + DISK="$1" + OUTPUT_FILE="$2" + + launch_guest() { + echo "Launching QEMU guest..." + cp /usr/share/edk2/ovmf/OVMF_CODE.fd . + + /usr/libexec/qemu-kvm \ + -machine type=q35,accel=tcg,smm=off \ + -m 2048 \ + -cpu max \ + -drive file=./OVMF_CODE.fd,format=raw,if=pflash \ + -drive file="$DISK",format=qcow2 \ + -chardev socket,id=chrtpm,path=./vtpm/swtpm.sock \ + -tpmdev emulator,id=tpm0,chardev=chrtpm \ + -device tpm-tis,tpmdev=tpm0 \ + -nographic \ + -serial file:serial.log \ + -monitor telnet::45454,server,nowait + } + + # Subcommand: stop + stop_guest() { + echo "Stopping QEMU guest..." + echo q | nc localhost 45454 + } + + # Subcommand: swtpm + setup_swtpm() { + echo "Setting up software TPM..." + mkdir -p vtpm + swtpm socket \ + --tpmstate dir=./vtpm \ + --ctrl type=unixio,path=./vtpm/swtpm.sock \ + --tpm2 + } + + # Subcommand: wait + wait_for_guest() { + echo "Waiting for guest to boot..." + timeout=1200 + elapsed=0 + touch serial.log + while ! grep -q "login:" serial.log; do + sleep 2 + elapsed="$((elapsed + 2))" + if [ "$elapsed" -ge "$timeout" ]; then + echo "Guest failed to boot within ${timeout}s." >&2 + exit 1 + fi + done + echo "Guest booted correctly!" + } + + # Subcommand: scrape + scrape_logs() { + # Extract the PCR block from the log. + + # This assumes the block starts with "Detected vTPM PCR values:" + # and ends when a non-indented line appears. + pcr_block=$(sed -n '/Detected vTPM PCR values:/,/^[^[:space:]]/p' serial.log) + + # detect line "sha***:" + alg=$(echo "$pcr_block" | grep -E "^[[:space:]]*sha.*:" | awk -F':' '{print $1}' | tr -d '[:space:]') + echo -n "{\"measurements\":{\"${alg}\":{" + # Now filter for PCRs 3, 9, 11 and 12. + echo "$pcr_block" | grep -E "^[[:space:]]*(3|9|11|12)[[:space:]]*:" | while read -r line; do + # Use awk to split the line at the colon. + index=$(echo "$line" | awk -F':' '{print $1}' | tr -d '[:space:]') + value=$(echo "$line" | awk -F':' '{print tolower($2)}' | tr -d '[:space:]') + echo -n "${need_sep:+,}\"pcr$(printf "%02d" "$index")\":\"$value\"" + need_sep=1 + done + echo "}}}" + } + + # Main command dispatch + main() { + setup_swtpm & + launch_guest & + wait_for_guest + scrape_logs > "$OUTPUT_FILE" + stop_guest + jq -e . "$OUTPUT_FILE" + + rm -f serial.log + rm -rf vtpm/ + } + + main "$@" + REMOTESSHEOF + + # make scripts executable and sync them to the cloud VM. + chmod +x scripts/*.sh + rsync -ra scripts "$SSH_HOST:$BUILD_DIR" + rsync -ra /var/workdir/source/ "$SSH_HOST:$BUILD_DIR/workspaces/source/" + + ssh -v $SSH_ARGS "$SSH_HOST" $BUILD_DIR/scripts/script-build.sh + rsync -ra "$SSH_HOST:$BUILD_DIR/tekton-results/*" "/tekton/results/" + + # Retrieve SBOM data from remote host + rsync "$SSH_HOST:$BUILD_DIR/rpm-manifest.txt" "/var/workdir/" 2>/dev/null || true + rsync "$SSH_HOST:$BUILD_DIR/payload-digest.txt" "/var/workdir/" 2>/dev/null || true + volumeMounts: + - mountPath: /ssh + name: ssh + readOnly: true + - mountPath: /activation-key + name: activation-key + - name: sbom-generate + image: quay.io/konflux-ci/mobster@sha256:45298b363ff4b96a084bf77a627b3e23471dcfb821eab55a3fa49a60f0ac43f3 + script: | + #!/bin/bash + set -euo pipefail + + IMAGE_URL=$(cat "$(results.IMAGE_URL.path)") + IMAGE_DIGEST=$(cat "$(results.IMAGE_DIGEST.path)") + + RPM_MANIFEST="/var/workdir/rpm-manifest.txt" + PAYLOAD_DIGEST_FILE="/var/workdir/payload-digest.txt" + SBOM_FILE="/var/workdir/sbom.json" + + IMAGE_PULLSPEC="${IMAGE_URL}@${IMAGE_DIGEST}" + CREATED=$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || echo "1970-01-01T00:00:00Z") + + { + printf '{\n' + printf ' "spdxVersion": "SPDX-2.3",\n' + printf ' "dataLicense": "CC0-1.0",\n' + printf ' "SPDXID": "SPDXRef-DOCUMENT",\n' + printf ' "name": "%s",\n' "$IMAGE_PULLSPEC" + printf ' "documentNamespace": "https://konflux-ci.dev/spdxdocs/%s",\n' "$IMAGE_PULLSPEC" + printf ' "creationInfo": {\n' + printf ' "created": "%s",\n' "$CREATED" + printf ' "creators": ["Organization: Red Hat", "Tool: coco-podvm-scripts-sbom-generator"],\n' + printf ' "licenseListVersion": "3.25"\n' + printf ' },\n' + + printf ' "packages": [\n' + + printf ' {\n' + printf ' "SPDXID": "SPDXRef-image",\n' + printf ' "name": "osc-dm-verity-image",\n' + printf ' "versionInfo": "%s",\n' "$IMAGE_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${IMAGE_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-dm-verity-image@%s?repository_url=%s"\n' "$IMAGE_DIGEST" "$IMAGE_URL" + printf ' }]\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + PURL="pkg:rpm/redhat/${NAME}@${VERSION}-${RELEASE}?arch=${ARCH}&distro=rhel-10.0" + if [ "$EPOCH" != "(none)" ] && [ -n "$EPOCH" ]; then + PURL="${PURL}&epoch=${EPOCH}" + fi + if [ -n "$SOURCERPM" ] && [ "$SOURCERPM" != "(none)" ]; then + PURL="${PURL}&upstream=${SOURCERPM}" + fi + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "name": "%s",\n' "$NAME" + printf ' "versionInfo": "%s-%s",\n' "$VERSION" "$RELEASE" + printf ' "supplier": "Organization: Red Hat, Inc.",\n' + printf ' "originator": "Organization: Red Hat, Inc.",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "licenseDeclared": "NOASSERTION",\n' + printf ' "copyrightText": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "%s"\n' "$PURL" + printf ' }]\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "SPDXID": "SPDXRef-payload",\n' + printf ' "name": "osc-podvm-payload",\n' + printf ' "versionInfo": "%s",\n' "$PAYLOAD_DIGEST" + printf ' "supplier": "Organization: Red Hat",\n' + printf ' "downloadLocation": "NOASSERTION",\n' + printf ' "licenseConcluded": "NOASSERTION",\n' + printf ' "filesAnalyzed": false,\n' + printf ' "checksums": [{"algorithm": "SHA256", "checksumValue": "%s"}],\n' "${PAYLOAD_DIGEST#sha256:}" + printf ' "externalRefs": [{\n' + printf ' "referenceCategory": "PACKAGE-MANAGER",\n' + printf ' "referenceType": "purl",\n' + printf ' "referenceLocator": "pkg:oci/osc-podvm-payload@%s?repository_url=quay.io/redhat-user-workloads/ose-osc-tenant"\n' "$PAYLOAD_DIGEST" + printf ' }]\n' + printf ' }' + fi + fi + + printf '\n ],\n' + + printf ' "relationships": [\n' + printf ' {\n' + printf ' "spdxElementId": "SPDXRef-DOCUMENT",\n' + printf ' "relatedSpdxElement": "SPDXRef-image",\n' + printf ' "relationshipType": "DESCRIBES"\n' + printf ' }' + + RPM_COUNT=0 + if [ -f "$RPM_MANIFEST" ]; then + while IFS='|' read -r NAME EPOCH VERSION RELEASE ARCH SOURCERPM; do + [ -z "$NAME" ] && continue + RPM_COUNT=$((RPM_COUNT + 1)) + SAFE_NAME=$(echo "$NAME" | tr -c 'a-zA-Z0-9.-' '-') + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-rpm-%s-%d",\n' "$SAFE_NAME" "$RPM_COUNT" + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + done < "$RPM_MANIFEST" + fi + + if [ -f "$PAYLOAD_DIGEST_FILE" ]; then + PAYLOAD_DIGEST=$(tr -d '[:space:]' < "$PAYLOAD_DIGEST_FILE") + if [ -n "$PAYLOAD_DIGEST" ]; then + printf ',\n {\n' + printf ' "spdxElementId": "SPDXRef-image",\n' + printf ' "relatedSpdxElement": "SPDXRef-payload",\n' + printf ' "relationshipType": "CONTAINS"\n' + printf ' }' + fi + fi + + printf '\n ]\n' + printf '}\n' + } > "$SBOM_FILE" + + echo "Generated SBOM with $RPM_COUNT RPM packages at $SBOM_FILE" + + - name: upload-sbom + image: quay.io/konflux-ci/appstudio-utils:1610c1fc4cfc9c9053dbefc1146904a4df6659ef@sha256:90ac97b811073cb99a23232c15a08082b586c702b85da6200cf54ef505e3c50c + workingDir: /var/workdir + script: | + # Pre-select the correct credentials to work around cosign not supporting the containers-auth.json spec + mkdir -p /tmp/auth && select-oci-auth "$(cat "$(results.IMAGE_REFERENCE.path)")" > /tmp/auth/config.json + DOCKER_CONFIG=/tmp/auth cosign attach sbom --sbom sbom.json --type "$SBOM_TYPE" "$(cat "$(results.IMAGE_REFERENCE.path)")" + - name: report-sbom-url + image: quay.io/konflux-ci/yq:latest@sha256:15d0238843d954ee78c9c190705eb8b36f6e52c31434183c37d99a80841a635a + script: | + #!/bin/bash + REPO=${OUTPUT_IMAGE%:*} + echo "Found that ${REPO} is the repository for ${OUTPUT_IMAGE}" + SBOM_DIGEST=$(sha256sum sbom.json | awk '{ print $1 }') + echo "Found that ${SBOM_DIGEST} is the SBOM digest" + echo -n "${REPO}@sha256:${SBOM_DIGEST}" | tee $(results.SBOM_BLOB_URL.path) + workingDir: /var/workdir + volumes: + - emptyDir: {} + name: workdir + - emptyDir: {} + name: varlibcontainers + - name: ssh + secret: + optional: false + secretName: multi-platform-ssh-$(context.taskRun.name) + - name: activation-key + secret: + optional: true + secretName: $(params.ACTIVATION_KEY) diff --git a/.tekton/build-pipeline.yaml b/.tekton/build-pipeline.yaml index a8719d0d..ed837115 100644 --- a/.tekton/build-pipeline.yaml +++ b/.tekton/build-pipeline.yaml @@ -188,14 +188,7 @@ spec: runAfter: - prefetch-dependencies taskRef: - resolver: bundles - params: - - name: name - value: build-dm-verity-image - - name: bundle - value: quay.io/konflux-ci/ose-osc-tenant/build-dm-verity-image-task:0.1@sha256:c5e1b8c3f5640ff98d0771dc4a1d5d86ca6059882b0e0188a0b484b4c2874adc - - name: kind - value: task + name: build-dm-verity-image - name: build-image-index params: - name: IMAGE