From 0c0137f81090872231f74f1a5d3e17d9dabac386 Mon Sep 17 00:00:00 2001 From: nbuckwalt Date: Thu, 23 Jul 2026 16:41:48 -0400 Subject: [PATCH 1/2] ci: add SBOM generation as a reusable workflow Adds sbom.yml (workflow_call) invoked from pipeline.yml after build-image and build-helm-chart. Generates CycloneDX + SPDX for the container image, CycloneDX for the source tree (post dotnet restore), and CycloneDX for the Helm chart tgz. All four SBOMs are published as GitHub Release assets. Drivers: customer procurement requirements and EO 14028 / EU CRA compliance. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pipeline.yml | 20 ++++++++- .github/workflows/sbom.yml | 75 ++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sbom.yml diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index 5b40e75e..f6942ef8 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -204,6 +204,18 @@ jobs: path: | manifests/helm/dist/output.yaml retention-days: 7 + # + # SBOM Stage + # + generate-sbom: + needs: + - build-image + - build-helm-chart + if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} + uses: ./.github/workflows/sbom.yml + with: + image-digest: ${{ needs.build-image.outputs.digest }} + secrets: inherit test-image: runs-on: ${{ matrix.runner }} needs: @@ -408,6 +420,7 @@ jobs: - generate-version - build-image - release-internal + - generate-sbom permissions: contents: write packages: write @@ -462,6 +475,11 @@ jobs: with: name: helm-schema path: ./artifacts/schema + - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + id: download-sbom + with: + name: sbom + path: ./artifacts/sbom - name: Publish uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 with: @@ -476,7 +494,7 @@ jobs: quay.io/contrast/agent-operator:${{ env.BUILD_VERSION }} quay.io/contrast/agent-operator@${{ needs.build-image.outputs.digest }} ``` - artifacts: "${{ steps.download-manifests.outputs.download-path }}/*.yaml,${{ steps.download-schema.outputs.download-path }}/*.json" + artifacts: "${{ steps.download-manifests.outputs.download-path }}/*.yaml,${{ steps.download-schema.outputs.download-path }}/*.json,${{ steps.download-sbom.outputs.download-path }}/*.json" token: ${{ secrets.GITHUB_TOKEN }} allowUpdates: true immutableCreate: true diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 00000000..984f6795 --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,75 @@ +# Contrast Security, Inc licenses this file to you under the Apache 2.0 License. +# See the LICENSE file in the project root for more information. + +name: "Generate SBOMs" + +on: + workflow_call: + inputs: + image-digest: + description: "Container image digest from build-image" + required: true + type: string + +jobs: + generate-sbom: + runs-on: ubuntu-latest + permissions: + packages: read + env: + IMAGE: ghcr.io/contrast-security-oss/agent-operator/operator@${{ inputs.image-digest }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Login (GitHub) + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Setup .NET SDK + uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 + with: + dotnet-version: 10.0.x + - name: Install Syft + run: | + curl -sSfL "https://raw.githubusercontent.com/anchore/syft/v1.49.0/install.sh" | sh -s -- -b /usr/local/bin v1.49.0 + shell: bash + - name: Restore NuGet Packages + run: dotnet restore + shell: bash + - name: Generate Source SBOM + run: | + set -xe + syft dir:. \ + --name agent-operator \ + -o cyclonedx-json=sbom-source.cdx.json + shell: bash + - name: Generate Image SBOMs + run: | + set -xe + syft "$IMAGE" \ + -o cyclonedx-json=sbom-image.cdx.json \ + -o spdx-json=sbom-image.spdx.json + shell: bash + - name: Download Helm Chart + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: helm-chart + path: ./helm-dist + - name: Generate Helm Chart SBOM + run: | + set -xe + CHART=$(ls ./helm-dist/*.tgz | head -1) + syft "$CHART" \ + -o cyclonedx-json=sbom-helm.cdx.json + shell: bash + - name: Upload SBOMs + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: sbom + path: | + sbom-source.cdx.json + sbom-image.cdx.json + sbom-image.spdx.json + sbom-helm.cdx.json + retention-days: 7 From 8db40b59abd6eb053d3e425095de117ad9fa9d76 Mon Sep 17 00:00:00 2001 From: nbuckwalt Date: Fri, 24 Jul 2026 12:19:40 -0400 Subject: [PATCH 2/2] ci(sbom): address review findings - release-public: require generate-sbom.result == 'success' so a skipped SBOM job (dependabot actor, etc.) cannot proceed to download a non-existent artifact and block the release - setup-dotnet: add cache: 'nuget' to avoid re-downloading the full NuGet closure on every run - Install Syft: replace curl|sh with anchore/sbom-action/download-syft (SHA-pinned composite action) for verified binary installation - Generate Source SBOM: exclude .git/.github from the scan and add a comment noting project.assets.json provides transitive NuGet deps - Generate Helm Chart SBOM: replace fragile ls|head -1 with a bash array glob and an explicit existence check for a clearer failure message Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pipeline.yml | 2 +- .github/workflows/sbom.yml | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index f6942ef8..9f4b6e7f 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -426,7 +426,7 @@ jobs: packages: write env: BUILD_VERSION: ${{ needs.generate-version.outputs.version }} - if: ${{ needs.generate-version.outputs.version != '0.0.1' }} + if: ${{ needs.generate-version.outputs.version != '0.0.1' && needs.generate-sbom.result == 'success' }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Login (GitHub) diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index 984f6795..d8e8fa23 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -30,18 +30,22 @@ jobs: uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0 with: dotnet-version: 10.0.x + cache: 'nuget' - name: Install Syft - run: | - curl -sSfL "https://raw.githubusercontent.com/anchore/syft/v1.49.0/install.sh" | sh -s -- -b /usr/local/bin v1.49.0 - shell: bash + uses: anchore/sbom-action/download-syft@43a17d6e7add2b5535efe4dcae9952337c479a93 # v0.20.11 + with: + syft-version: v1.49.0 - name: Restore NuGet Packages run: dotnet restore shell: bash - name: Generate Source SBOM run: | set -xe + # dotnet restore above produces obj/project.assets.json; syft reads it for transitive deps syft dir:. \ --name agent-operator \ + --exclude './.git' \ + --exclude './.github' \ -o cyclonedx-json=sbom-source.cdx.json shell: bash - name: Generate Image SBOMs @@ -59,8 +63,9 @@ jobs: - name: Generate Helm Chart SBOM run: | set -xe - CHART=$(ls ./helm-dist/*.tgz | head -1) - syft "$CHART" \ + CHART=(./helm-dist/*.tgz) + [[ -f "${CHART[0]}" ]] || { echo "No .tgz found in ./helm-dist" >&2; exit 1; } + syft "${CHART[0]}" \ -o cyclonedx-json=sbom-helm.cdx.json shell: bash - name: Upload SBOMs