Skip to content
Open
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
7 changes: 7 additions & 0 deletions .github/sbom-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"max_added": 50,
"max_removed": 50,
"deny_licenses": [],
"require_licenses": true,
"deny_duplicates": true
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
name: Generate and upload SBOMs
name: Generate SBOMs

# Runs after OCI images are built, or on direct pushes that touch image sources.
#
# Prerequisites (one-time setup):
# 1. Add SBOMIFY_TOKEN secret to the repository (Settings → Secrets → Actions).
# Generate the token at https://app.sbomify.com.
# 2. Create one sbomify component per image and replace the placeholder
# sbomify_component_id values in the matrix below with the real IDs.
# Generates and enriches CycloneDX SBOMs for all Nix-built OCI images.
# Enriched SBOMs are uploaded as artifacts for downstream workflows
# (quality gate on PRs, upload to sbomify on merge).

on:
workflow_run:
workflows: ["Build and publish OCI images"]
types: [completed]
pull_request:
branches: [main]
paths:
- images/**
- deployments/sbomify/**
- flake.nix
- flake.lock
- bin/patch-sbom-root
push:
branches:
- main
branches: [main]
paths:
- images/**
- deployments/sbomify/**
- flake.nix
- flake.lock
- bin/patch-sbom-root
workflow_dispatch:

permissions:
actions: write
contents: read

jobs:
generate-and-upload:
# Skip if triggered by a failed workflow_run
if: >-
github.event_name != 'workflow_run' ||
github.event.workflow_run.conclusion == 'success'
generate:
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
Expand All @@ -41,34 +37,42 @@ jobs:
- name: postgres
sbom_package: postgres-sbom
nixpkg: postgresql_17
license: PostgreSQL
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rather get the license data from the nix package / derivation that we're making for the image?

Ideally by lining the image license to the license of its primary package; in the same way the we do for version

sbomify_component_id: M8rixM6mMEPe
- name: redis
sbom_package: redis-sbom
nixpkg: redis
license: AGPL-3.0-only
sbomify_component_id: ABBCcw2YiYrG
- name: minio
sbom_package: minio-sbom
nixpkg: minio
license: AGPL-3.0-or-later
sbomify_component_id: PLACEHOLDER_MINIO
- name: minio-client
sbom_package: minio-client-sbom
nixpkg: minio-client
license: Apache-2.0
sbomify_component_id: PLACEHOLDER_MINIO_CLIENT
- name: sbomify-app
sbom_package: sbomify-app-sbom
nixpkg: python313
license: Apache-2.0
sbomify_component_id: VP42I4XQgpDE
- name: sbomify-keycloak
sbom_package: sbomify-keycloak-sbom
nixpkg: keycloak
license: Apache-2.0
sbomify_component_id: N4agQD8pvej8
- name: sbomify-caddy-dev
sbom_package: sbomify-caddy-dev-sbom
nixpkg: caddy
license: Apache-2.0
sbomify_component_id: zYDq6NtrOBuo
- name: sbomify-minio-init
sbom_package: sbomify-minio-init-sbom
nixpkg: minio-client
license: Apache-2.0
sbomify_component_id: PLACEHOLDER_SBOMIFY_MINIO_INIT

steps:
Expand All @@ -90,7 +94,6 @@ jobs:
id: version
run: |
if [[ "${{ matrix.image.name }}" == sbomify-* ]]; then
# sbomify images use the pinned sbomify source version
VERSION=$(grep 'github:sbomify/sbomify/' flake.nix | grep -oP '/v\K[0-9.]+')
else
VERSION=$(nix eval --raw nixpkgs#${{ matrix.image.nixpkg }}.version)
Expand All @@ -102,7 +105,19 @@ jobs:
nix build .#${{ matrix.image.sbom_package }}
cp -L result ${{ matrix.image.name }}.cdx.json

- name: Generate and Upload SBOM to sbomify
- name: Patch root component metadata
run: |
VERSION="${{ steps.version.outputs.upstream }}"
SBOM="${{ matrix.image.name }}.cdx.json"

bin/patch-sbom-root \
--name "wellmaintained/packages/${{ matrix.image.name }}-image" \
--version "$VERSION" \
--purl "pkg:docker/wellmaintained/packages/${{ matrix.image.name }}@${VERSION}" \
--license "${{ matrix.image.license }}" \
< "$SBOM" > "${SBOM}.tmp" && mv "${SBOM}.tmp" "$SBOM"
Comment on lines +113 to +118
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same argument here - can we it get the data we need for this patch from the nix expression used to describe the image?


- name: Enrich SBOM
if: ${{ !startsWith(matrix.image.sbomify_component_id, 'PLACEHOLDER') }}
uses: sbomify/sbomify-action@master
continue-on-error: true
Expand All @@ -115,12 +130,17 @@ jobs:
SBOM_FORMAT: cyclonedx
AUGMENT: true
ENRICH: true
UPLOAD: true
UPLOAD: false
OUTPUT_FILE: ${{ matrix.image.name }}.enriched.cdx.json

- name: Upload enriched SBOM as artifact
- name: Fall back to raw SBOM if enrichment skipped
run: |
if [ ! -f "${{ matrix.image.name }}.enriched.cdx.json" ]; then
cp "${{ matrix.image.name }}.cdx.json" "${{ matrix.image.name }}.enriched.cdx.json"
fi

- name: Upload enriched SBOM
uses: actions/upload-artifact@v6
with:
name: sbom-${{ matrix.image.name }}
path: ${{ matrix.image.name }}.enriched.cdx.json
if-no-files-found: warn
174 changes: 174 additions & 0 deletions .github/workflows/sbom-quality-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
name: SBOM quality gate

# Scores the PR's SBOMs and compares against the baseline from main.
# Blocks merging if any image's quality score regresses.
#
# Requires the "Generate SBOMs" workflow to have run first on this PR
# (it produces the enriched SBOM artifacts we score here).

on:
workflow_run:
workflows: ["Generate SBOMs"]
types: [completed]

permissions:
actions: read
contents: read
pull-requests: write

jobs:
score:
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: blacksmith-4vcpu-ubuntu-2404
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a cheaper blacksmith image type we can use for these jobs where we aren't doing compilation or image building

strategy:
fail-fast: false
matrix:
image:
- name: postgres
- name: redis
- name: minio
- name: minio-client
- name: sbomify-app
- name: sbomify-keycloak
- name: sbomify-caddy-dev
- name: sbomify-minio-init

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Install sbomqs
run: |
curl -sSfL https://github.com/interlynk-io/sbomqs/releases/latest/download/sbomqs-linux-amd64 \
-o /usr/local/bin/sbomqs
chmod +x /usr/local/bin/sbomqs

- name: Install sbomlyze
run: |
curl -sSfL https://raw.githubusercontent.com/rezmoss/sbomlyze/main/install.sh | sh
mv ./sbomlyze /usr/local/bin/sbomlyze
Comment on lines +42 to +51
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rather make custom nix packages for these so we can pin to specific versions


- name: Download PR SBOM
uses: actions/download-artifact@v4
with:
name: sbom-${{ matrix.image.name }}
path: current/
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Score current SBOM
run: |
mkdir -p results
SBOM=$(ls current/*.cdx.json | head -1)
bin/sbom-score --image "${{ matrix.image.name }}" "$SBOM" \
> results/score-${{ matrix.image.name }}.json
env:
PATH: /usr/local/bin:$PATH

- name: Fetch baseline SBOM
id: baseline
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
# Find the latest successful Generate SBOMs run on main
RUN_ID=$(gh run list \
--workflow="Generate SBOMs" \
--branch=main \
--status=success \
--limit=1 \
--json databaseId \
--jq '.[0].databaseId')

if [ -z "$RUN_ID" ] || [ "$RUN_ID" = "null" ]; then
echo "has_baseline=false" >> "$GITHUB_OUTPUT"
exit 0
fi

gh run download "$RUN_ID" \
--name "sbom-${{ matrix.image.name }}" \
--dir baseline/ || {
echo "has_baseline=false" >> "$GITHUB_OUTPUT"
exit 0
}

echo "has_baseline=true" >> "$GITHUB_OUTPUT"

- name: Score baseline SBOM
if: steps.baseline.outputs.has_baseline == 'true'
run: |
SBOM=$(ls baseline/*.cdx.json | head -1)
bin/sbom-score --image "${{ matrix.image.name }}" "$SBOM" \
> results/baseline-${{ matrix.image.name }}.json

- name: Compare SBOMs
if: steps.baseline.outputs.has_baseline == 'true'
run: |
BASELINE=$(ls baseline/*.cdx.json | head -1)
CURRENT=$(ls current/*.cdx.json | head -1)
bin/sbom-compare \
--baseline "$BASELINE" \
--current "$CURRENT" \
--image "${{ matrix.image.name }}" \
--policy .github/sbom-policy.json \
> results/compare-${{ matrix.image.name }}.json

- name: Upload results
uses: actions/upload-artifact@v6
with:
name: sbom-qg-${{ matrix.image.name }}
path: results/

report:
needs: score
if: always() && needs.score.result != 'skipped'
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
pull-requests: write
actions: read

steps:
- name: Checkout
uses: actions/checkout@v5

- name: Download all results
uses: actions/download-artifact@v4
with:
pattern: sbom-qg-*
merge-multiple: true
path: results/

- name: Generate report
id: report
run: |
bin/sbom-report --scores-dir results/ > pr-comment.md 2>report-stderr.txt || {
cat report-stderr.txt >&2
echo "failed=true" >> "$GITHUB_OUTPUT"
}

- name: Resolve PR number
id: pr
env:
GH_TOKEN: ${{ github.token }}
run: |
# The workflow_run event doesn't directly give us the PR number.
# Look it up from the head branch.
HEAD_BRANCH="${{ github.event.workflow_run.head_branch }}"
PR_NUMBER=$(gh pr list --head "$HEAD_BRANCH" --json number --jq '.[0].number')
echo "number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"

- name: Post PR comment
if: steps.pr.outputs.number
uses: marocchino/sticky-pull-request-comment@v2
with:
header: sbom-quality-gate
number: ${{ steps.pr.outputs.number }}
path: pr-comment.md

- name: Fail on regression
if: steps.report.outputs.failed == 'true'
run: |
echo "SBOM quality regression detected. See PR comment for details."
exit 1
59 changes: 59 additions & 0 deletions .github/workflows/sbom-upload.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Upload SBOMs to sbomify

# Uploads enriched SBOMs to sbomify after they are generated on main.
# Only runs when the Generate SBOMs workflow completes successfully on main.

on:
workflow_run:
workflows: ["Generate SBOMs"]
types: [completed]
branches: [main]

permissions:
actions: read
contents: read

jobs:
upload:
if: github.event.workflow_run.conclusion == 'success'
runs-on: blacksmith-4vcpu-ubuntu-2404
strategy:
fail-fast: false
matrix:
image:
- name: postgres
sbomify_component_id: M8rixM6mMEPe
- name: redis
sbomify_component_id: ABBCcw2YiYrG
- name: minio
sbomify_component_id: PLACEHOLDER_MINIO
- name: minio-client
sbomify_component_id: PLACEHOLDER_MINIO_CLIENT
- name: sbomify-app
sbomify_component_id: VP42I4XQgpDE
- name: sbomify-keycloak
sbomify_component_id: N4agQD8pvej8
- name: sbomify-caddy-dev
sbomify_component_id: zYDq6NtrOBuo
- name: sbomify-minio-init
sbomify_component_id: PLACEHOLDER_SBOMIFY_MINIO_INIT

steps:
- name: Download enriched SBOM
if: ${{ !startsWith(matrix.image.sbomify_component_id, 'PLACEHOLDER') }}
uses: actions/download-artifact@v4
with:
name: sbom-${{ matrix.image.name }}
path: sbom/
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Upload to sbomify
if: ${{ !startsWith(matrix.image.sbomify_component_id, 'PLACEHOLDER') }}
uses: sbomify/sbomify-action@master
env:
TOKEN: ${{ secrets.SBOMIFY_TOKEN }}
COMPONENT_ID: ${{ matrix.image.sbomify_component_id }}
SBOM_FILE: sbom/${{ matrix.image.name }}.enriched.cdx.json
SBOM_FORMAT: cyclonedx
UPLOAD: true
Loading
Loading