Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 79 additions & 7 deletions .github/workflows/sbom-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,14 @@ jobs:
echo "lockfile_path=" >> $GITHUB_OUTPUT
fi
PRODUCT_ID=$(yq -r '.sbomify.product_id // ""' "$CONFIG")
BUNDLE_PRODUCT_ID=$(yq -r '.sbomify.bundle_product_id // ""' "$CONFIG")
echo "product_id=${PRODUCT_ID}" >> $GITHUB_OUTPUT
if [[ -n "$PRODUCT_ID" && -n "$VERSION" ]]; then
echo "product_release=[\"${PRODUCT_ID}:${VERSION}\"]" >> $GITHUB_OUTPUT
RELEASE_ARRAY="\"${PRODUCT_ID}:${VERSION}\""
if [[ -n "$BUNDLE_PRODUCT_ID" ]]; then
RELEASE_ARRAY="${RELEASE_ARRAY},\"${BUNDLE_PRODUCT_ID}:${VERSION}\""
fi
echo "product_release=[${RELEASE_ARRAY}]" >> $GITHUB_OUTPUT
fi

# Build PURL for TEA dedup lookup
Expand Down Expand Up @@ -162,11 +168,25 @@ jobs:
|| (steps.config.outputs.source_type != 'github_release' && steps.config.outputs.source_type != 'lockfile')
run: ./scripts/fetch-sbom.sh "${{ inputs.app }}"

- name: Read image digest
id: digest
if: >-
steps.config.outputs.source_type == 'docker'
|| steps.config.outputs.source_type == 'chainguard'
run: |
if [[ ! -f image-digest.txt ]]; then
echo "::error::image-digest.txt not found"
exit 1
fi
digest=$(cat image-digest.txt)
echo "image_digest=${digest}" >> "$GITHUB_OUTPUT"
echo "image_digest_safe=${digest//:/-}" >> "$GITHUB_OUTPUT"

- name: Upload input artifact
if: always() && hashFiles('sbom.json') != ''
uses: actions/upload-artifact@v4
with:
name: sbom-${{ inputs.app }}-${{ steps.config.outputs.version }}
name: sbom-${{ inputs.app }}-${{ steps.digest.outputs.image_digest_safe || steps.config.outputs.version }}
path: sbom.json

- name: Upload lockfile artifact
Expand All @@ -184,7 +204,7 @@ jobs:
TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
COMPONENT_ID: ${{ steps.config.outputs.component_id }}
COMPONENT_NAME: ${{ steps.config.outputs.component_name }}
COMPONENT_VERSION: ${{ steps.config.outputs.version }}
COMPONENT_VERSION: ${{ steps.digest.outputs.image_digest || steps.config.outputs.version }}
SBOM_FILE: sbom.json
OUTPUT_FILE: sbom-output.json
AUGMENT: true
Expand Down Expand Up @@ -233,17 +253,52 @@ jobs:
echo "should_upload=false" >> "$GITHUB_OUTPUT"
fi

- name: Check sbomify for existing digest
id: sbomify-check
if: >-
steps.config.outputs.component_id != '' && !inputs.dry_run
&& (steps.config.outputs.source_type == 'docker'
|| steps.config.outputs.source_type == 'chainguard')
&& steps.config.outputs.product_id != ''
run: |
source scripts/lib/common.sh
source scripts/lib/sbomify-api.sh
if sbomify_digest_exists \
"${{ steps.config.outputs.product_id }}" \
"${{ steps.config.outputs.version }}" \
"${{ steps.config.outputs.component_id }}" \
"${{ steps.digest.outputs.image_digest }}"; then
echo "Digest already exists, skipping upload"
echo "should_upload=false" >> "$GITHUB_OUTPUT"
else
echo "New digest, will upload"
echo "should_upload=true" >> "$GITHUB_OUTPUT"
fi
env:
SBOMIFY_TOKEN: ${{ secrets.SBOMIFY_TOKEN }}

- name: Resolve upload decision
id: upload-decision
if: steps.config.outputs.component_id != '' && !inputs.dry_run
run: |
src="${{ steps.config.outputs.source_type }}"
if [[ "$src" == "docker" || "$src" == "chainguard" ]]; then
echo "should_upload=${{ steps.sbomify-check.outputs.should_upload || 'true' }}" >> "$GITHUB_OUTPUT"
else
echo "should_upload=${{ steps.tea-check.outputs.should_upload || 'true' }}" >> "$GITHUB_OUTPUT"
fi

# Phase 3: Upload only if SBOM is new
- name: Upload SBOM (from existing SBOM)
if: >-
steps.config.outputs.component_id != '' && steps.config.outputs.source_type != 'lockfile'
&& !inputs.dry_run && steps.tea-check.outputs.should_upload == 'true'
&& !inputs.dry_run && steps.upload-decision.outputs.should_upload == 'true'
uses: sbomify/sbomify-action@master
env:
TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
COMPONENT_ID: ${{ steps.config.outputs.component_id }}
COMPONENT_NAME: ${{ steps.config.outputs.component_name }}
COMPONENT_VERSION: ${{ steps.config.outputs.version }}
COMPONENT_VERSION: ${{ steps.digest.outputs.image_digest || steps.config.outputs.version }}
SBOM_FILE: sbom-output.json
OUTPUT_FILE: sbom-final.json
UPLOAD: true
Expand All @@ -252,7 +307,7 @@ jobs:
- name: Upload SBOM (from lockfile)
if: >-
steps.config.outputs.component_id != '' && steps.config.outputs.source_type == 'lockfile'
&& !inputs.dry_run && steps.tea-check.outputs.should_upload == 'true'
&& !inputs.dry_run && steps.upload-decision.outputs.should_upload == 'true'
uses: sbomify/sbomify-action@master
env:
TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
Expand All @@ -264,6 +319,23 @@ jobs:
UPLOAD: true
PRODUCT_RELEASE: ${{ steps.config.outputs.product_release }}

- name: Cleanup old release artifacts
if: >-
steps.config.outputs.product_id != '' && !inputs.dry_run
&& steps.upload-decision.outputs.should_upload == 'true'
&& (steps.config.outputs.source_type == 'docker'
|| steps.config.outputs.source_type == 'chainguard')
run: |
source scripts/lib/common.sh
source scripts/lib/sbomify-api.sh
sbomify_cleanup_old_artifacts \
"${{ steps.config.outputs.product_id }}" \
"${{ steps.config.outputs.version }}" \
"${{ steps.config.outputs.component_id }}" \
"${{ steps.digest.outputs.image_digest }}"
env:
SBOMIFY_TOKEN: ${{ secrets.SBOMIFY_TOKEN }}

- name: Upload output artifact
if: always()
uses: actions/upload-artifact@v4
Expand All @@ -274,7 +346,7 @@ jobs:
- name: Attest SBOM provenance
if: >-
steps.config.outputs.component_id != '' && !inputs.dry_run
&& steps.tea-check.outputs.should_upload == 'true'
&& steps.upload-decision.outputs.should_upload == 'true'
uses: actions/attest-build-provenance@v3
with:
subject-path: sbom-output.json
3 changes: 3 additions & 0 deletions apps/.template/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,7 @@ sbomify:
# Optional: Product ID for release tagging (if this component is part of a product)
# When set, the SBOM will be tagged with "product_id:version"
# product_id: "your-product-id"
# Optional: Bundle product ID (if this component also belongs to a multi-component bundle)
# When set, the SBOM will be tagged with both product releases
# bundle_product_id: "your-bundle-product-id"

3 changes: 2 additions & 1 deletion apps/dependency-track-frontend/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ source:
sbomify:
component_id: "UL9G0VKuqRiI"
component_name: "Dependency Track Frontend"
product_id: "MoJ2O8FemjBc"
product_id: "UMJjUDpLYCTG"
bundle_product_id: "MoJ2O8FemjBc"

3 changes: 2 additions & 1 deletion apps/dependency-track/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ source:
sbomify:
component_id: "hZSsrKGgp1ZP"
component_name: "Dependency Track API Server"
product_id: "MoJ2O8FemjBc"
product_id: "rcTz13LAeJhO"
bundle_product_id: "MoJ2O8FemjBc"

62 changes: 62 additions & 0 deletions scripts/lib/sbomify-api.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# sbomify-api.sh - Shared utilities for sbomify API calls
#
# Usage: source this file in other scripts (after common.sh)
# source "$(dirname "${BASH_SOURCE[0]}")/lib/sbomify-api.sh"
#
# Requires: SBOMIFY_TOKEN, curl, jq

set -euo pipefail

SBOMIFY_API="${SBOMIFY_API_URL:-https://app.sbomify.com}"

# Check if a release artifact already has this digest version
# Usage: sbomify_digest_exists <product_id> <tag_version> <component_id> <digest>
# Returns: 0 if exists, 1 if not
sbomify_digest_exists() {
local product_id="$1" tag_version="$2" component_id="$3" digest="$4"

# Find the release
local releases release_id
releases=$(curl -fsSL -H "Authorization: Bearer ${SBOMIFY_TOKEN}" \
"${SBOMIFY_API}/api/v1/releases?product_id=${product_id}&version=${tag_version}")
release_id=$(echo "$releases" | jq -r --arg v "$tag_version" \
'.items[] | select(.version == $v) | .id' | head -1)

[[ -z "$release_id" ]] && return 1 # No release → digest doesn't exist

# Check artifacts for matching digest
local artifacts
artifacts=$(curl -fsSL -H "Authorization: Bearer ${SBOMIFY_TOKEN}" \
"${SBOMIFY_API}/api/v1/releases/${release_id}/artifacts?mode=existing")
echo "$artifacts" | jq -e --arg cid "$component_id" --arg d "$digest" \
'.items[] | select(.component_id == $cid and .sbom_version == $d)' > /dev/null 2>&1
}

# Remove old artifacts for a component from a release (keep only current digest)
# Usage: sbomify_cleanup_old_artifacts <product_id> <tag_version> <component_id> <current_digest>
sbomify_cleanup_old_artifacts() {
local product_id="$1" tag_version="$2" component_id="$3" current_digest="$4"

# Find the release
local releases release_id
releases=$(curl -fsSL -H "Authorization: Bearer ${SBOMIFY_TOKEN}" \
"${SBOMIFY_API}/api/v1/releases?product_id=${product_id}&version=${tag_version}")
release_id=$(echo "$releases" | jq -r --arg v "$tag_version" \
'.items[] | select(.version == $v) | .id' | head -1)

[[ -z "$release_id" ]] && return 0 # No release → nothing to clean

# Find old artifacts for this component with different digest
local old_ids
old_ids=$(curl -fsSL -H "Authorization: Bearer ${SBOMIFY_TOKEN}" \
"${SBOMIFY_API}/api/v1/releases/${release_id}/artifacts?mode=existing" | \
jq -r --arg cid "$component_id" --arg d "$current_digest" \
'.items[] | select(.component_id == $cid and .sbom_version != $d) | .id')

for artifact_id in $old_ids; do
log_info "Removing old artifact ${artifact_id} from release ${release_id}"
curl -fsSL -X DELETE -H "Authorization: Bearer ${SBOMIFY_TOKEN}" \
"${SBOMIFY_API}/api/v1/releases/${release_id}/artifacts/${artifact_id}"
done
}
5 changes: 5 additions & 0 deletions scripts/sources/chainguard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ platform=$(get_config "$app" ".source.platform" "linux/amd64")
image_ref="${registry}/${image}:${version}"

log_info "Downloading attestation: $image_ref"

image_digest=$(crane digest --platform "$platform" "$image_ref")
echo "$image_digest" > image-digest.txt
log_info "Image digest: $image_digest"

cosign download attestation \
--platform "$platform" \
--predicate-type="https://spdx.dev/Document" \
Expand Down
1 change: 1 addition & 0 deletions scripts/sources/docker-attestation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ image_digest=$(echo "$index" | jq -r --arg os "$plat_os" --arg arch "$plat_arch"
die "No image found for platform $platform"

log_debug "Image digest: $image_digest"
echo "$image_digest" > image-digest.txt

# Find the attestation manifest that references this image
sbom_digest=$(echo "$index" | jq -r --arg ref "$image_digest" '
Expand Down