diff --git a/.github/workflows/generate_sbom.yml b/.github/workflows/generate_sbom.yml new file mode 100644 index 00000000..69c1c81e --- /dev/null +++ b/.github/workflows/generate_sbom.yml @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: Copyright 2025 Siemens AG +# +# SPDX-License-Identifier: Apache-2.0 + +# Generates a CycloneDX SBOM from the locked dependencies and publishes it so it +# can always be fetched for analysis: on push to main the SBOM is committed back +# to the repository, and on a published release it is attached as a release asset. + +name: Generate SBOM + +# Triggered when the dependencies (pyproject.toml) or this workflow change, on +# every published release, and manually. +on: + push: + branches: + - main + paths: + - pyproject.toml + - .github/workflows/generate_sbom.yml + pull_request: + paths: + - pyproject.toml + - .github/workflows/generate_sbom.yml + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +# Serialize runs per ref so two commits to main can't race their git push. +# Releases and PRs use separate groups; a run is never cancelled mid push. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + # Generate a docker image URL where all the characters are lower-case, for situations where an external contributor + # has a username that begins with a capital letter. This is a limitation of GHCR, which only allows lower-case + # characters. Subsequent jobs use this URL. + prepare_env: + runs-on: ubuntu-24.04 + outputs: + image_url: ${{ steps.setenv.outputs.image_url }} + steps: + - name: Set lowercase image name + id: setenv + # Pass via env, not inline ${{ }}, so the value can't be parsed as shell. + env: + REPO_OWNER: ${{ github.repository_owner }} + run: | + OWNER_LC=$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]') + echo "image_url=ghcr.io/${OWNER_LC}/cmp-test-dev:latest" >> "$GITHUB_OUTPUT" + + generate_sbom: + needs: prepare_env + name: Generate CycloneDX SBOM + runs-on: ubuntu-24.04 + container: + image: ${{ needs.prepare_env.outputs.image_url }} + steps: + - uses: actions/checkout@v6.0.3 + with: + # Read-only job: don't leave the token in git config for later steps. + persist-credentials: false + + - name: Generate SBOM in CycloneDX 1.5 format + # The container image already resolves and locks the dependencies at build + # time (data/dockerfiles/Dockerfile.dev: `WORKDIR /app` + `uv sync`, which + # writes /app/uv.lock). The checked-out workspace has no committed uv.lock, + # so point --project at /app to reuse the image's lockfile. Output still + # goes to the workspace so the upload-artifact step can find it. + run: uv --project /app export --format cyclonedx1.5 --locked -o "$GITHUB_WORKSPACE/sbom.cdx.json" + + - name: Upload SBOM artifact + uses: actions/upload-artifact@v7.0.1 + with: + name: sbom-cdx + path: ${{ github.workspace }}/sbom.cdx.json + retention-days: 30 + + # Only on direct pushes to main, never from PRs/forks (which get a read-only + # token), so the committed-back SBOM always reflects merged dependencies. + commit_sbom: + needs: generate_sbom + name: Commit SBOM to main + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v6.0.3 + with: + ref: main + + - name: Download SBOM artifact + uses: actions/download-artifact@v8.0.1 + with: + name: sbom-cdx + path: . + + - name: Commit and push updated SBOM + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add sbom.cdx.json + if git diff --staged --quiet; then + echo "SBOM unchanged, nothing to commit" + else + # [skip ci] keeps the bot commit from re-triggering CI. + git commit -m "chore: update SBOM [skip ci]" + # Rebase in case main advanced since checkout; only this job edits + # the SBOM, so the rebase can't conflict. + git pull --rebase origin main + git push + fi + + # Only on published releases; attaches the SBOM to that release's assets. + release_sbom: + needs: generate_sbom + name: Attach SBOM to release + if: github.event_name == 'release' + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Download SBOM artifact + uses: actions/download-artifact@v8.0.1 + with: + name: sbom-cdx + path: . + + - name: Upload SBOM to GitHub release + # Pass the tag via env, not inline ${{ }}, to avoid shell injection; + # --clobber lets re-runs replace the asset instead of failing. + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.event.release.tag_name }} + run: gh release upload "$TAG" sbom.cdx.json --clobber --repo "$GITHUB_REPOSITORY" diff --git a/Makefile b/Makefile index 5f0dbb55..f2e322d7 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ help: @echo ' verify - Run a bunch of checks, to see if there are any obvious deficiencies in the code ' @echo ' verifyformat - Check formatting only ' @echo ' show-outdated - Show outdated packages (depth 1)' + @echo ' sbom - Generate Software Bill of Materials (SBOM) in CycloneDX 1.5 format' @echo ' start-mock-ca - Start the mock CA server, so that it can listens to requests ' @echo ' test-mock-ca - Run the test against the mock CA server ' @echo ' test-mock-ca-verbose - Run all tests against the mock CA server ' @@ -108,6 +109,12 @@ verifyformat: show-outdated: uv tree --outdated --depth 1 +sbom: + uv sync --all-extras + uv lock --locked + uv export --format cyclonedx1.5 --locked -o sbom.cdx.json + @echo "SBOM generated: sbom.cdx.json" + dryrun: robot --dryrun --pythonpath=./ --variable environment:$(env) tests tests_pq_and_hybrid tests_mock_ca diff --git a/REUSE.toml b/REUSE.toml index 5f69520b..844bfae7 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -15,6 +15,7 @@ path = [ "scripts/**", "pq_logic/fips/frodokem.py", "pyrightconfig.json", "VERSION", "client_tests/certs/**", + "sbom.cdx.json" ] precedence = "override" SPDX-FileCopyrightText = "Copyright 2024 Siemens AG"