From 20665e6725d9728f80f67a7a16fd59f985373f3d Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Mon, 3 Aug 2026 11:07:25 +0300 Subject: [PATCH 1/2] SBOM generation - initial implementation --- .config/sbom-tool/dotnet-tools.json | 13 ++++ .github/workflows/sbom.yml | 107 ++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 .config/sbom-tool/dotnet-tools.json create mode 100644 .github/workflows/sbom.yml diff --git a/.config/sbom-tool/dotnet-tools.json b/.config/sbom-tool/dotnet-tools.json new file mode 100644 index 0000000..eabcff8 --- /dev/null +++ b/.config/sbom-tool/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "microsoft.sbom.dotnettool": { + "version": "4.1.5", + "commands": [ + "sbom-tool" + ], + "rollForward": true + } + } +} diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..bcf10ad --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,107 @@ +name: Generate SBOM + +# Runs when the 'generate sbom' label is added to a PR, or when a release is published. +# Uses the sbom-tool CLI (version pinned in .config/sbom-tool/dotnet-tools.json) +# so the NuGet package is never modified and no SBOM is embedded in it. +on: + pull_request: + types: [labeled] + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: sbom-${{ github.event_name == 'release' && github.ref_name || github.event.pull_request.number }} + cancel-in-progress: true + +env: + BUILD_CONFIGURATION: Release + DOTNET_VERSION: '9.x' + PACKAGE_VERSION: ${{ github.event_name == 'release' && github.ref_name || format('0.0.0-pr.{0}', github.event.pull_request.number) }} + +jobs: + sbom: + if: github.event_name == 'release' || github.event.label.name == 'generate sbom' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Pack NuGet package + run: > + dotnet pack Infragistics.QueryBuilder.Executor.csproj + --configuration ${{ env.BUILD_CONFIGURATION }} + -p:Version=${{ env.PACKAGE_VERSION }} + -o ./artifacts + + # Dedicated nested manifest keeps sbom-tool out of the root 'dotnet tool restore' used by build-and-publish.yml + - name: Restore sbom-tool (pinned) + run: dotnet tool restore --tool-manifest .config/sbom-tool/dotnet-tools.json + + # -b: the shipped artifact (nupkg) gets listed with its hash in the SBOM's files section + # -bc: dependency detection scans the repo root (csproj) + - name: Generate SBOM + working-directory: .config/sbom-tool + run: > + dotnet tool run sbom-tool -- generate + -b ${{ github.workspace }}/artifacts + -bc ${{ github.workspace }} + -pn Infragistics.QueryBuilder.Executor + -pv ${{ env.PACKAGE_VERSION }} + -ps "Infragistics Inc." + -nsb http://spdx.org/spdxdocs/Infragistics.QueryBuilder.Executor + -V Information + + - name: Verify SBOM + run: | + set -euo pipefail + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json + test -s artifacts/_manifest/spdx_2.2/manifest.spdx.json.sha256 + echo "SBOM generated successfully." + + - name: Upload NuGet package + uses: actions/upload-artifact@v7 + with: + name: nupkg + path: artifacts/*.nupkg + retention-days: 1 + if-no-files-found: error + + - name: Upload SBOM files + uses: actions/upload-artifact@v7 + with: + name: sbom-spdx_2.2 + path: artifacts/_manifest/spdx_2.2 + retention-days: 1 + if-no-files-found: error + + attach-to-release: + if: github.event_name == 'release' + needs: sbom + runs-on: ubuntu-latest + permissions: + contents: write # required to upload release assets + + steps: + - name: Download SBOM artifact + uses: actions/download-artifact@v8 + with: + name: sbom-spdx_2.2 + path: spdx_2.2 + + - name: Attach SBOM to release + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + asset="Infragistics.QueryBuilder.Executor.${TAG}.spdx_2.2.zip" + (cd spdx_2.2 && zip -r "../${asset}" .) + gh release upload "$TAG" "$asset" --clobber -R "${{ github.repository }}" From ff059f4d4f31c33a2af316840e42e6f6c0c5b26d Mon Sep 17 00:00:00 2001 From: Borislav Traykov Date: Tue, 4 Aug 2026 15:19:59 +0300 Subject: [PATCH 2/2] Use pinned SHAs for all github actions instead of release versions - for improved security (we hope) --- .github/workflows/sbom.yml | 10 +-- .github/workflows/signing.yml | 151 ---------------------------------- 2 files changed, 5 insertions(+), 156 deletions(-) delete mode 100644 .github/workflows/signing.yml diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index bcf10ad..a69a297 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -27,10 +27,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5.4.0 with: dotnet-version: ${{ env.DOTNET_VERSION }} @@ -67,7 +67,7 @@ jobs: echo "SBOM generated successfully." - name: Upload NuGet package - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: nupkg path: artifacts/*.nupkg @@ -75,7 +75,7 @@ jobs: if-no-files-found: error - name: Upload SBOM files - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: sbom-spdx_2.2 path: artifacts/_manifest/spdx_2.2 @@ -91,7 +91,7 @@ jobs: steps: - name: Download SBOM artifact - uses: actions/download-artifact@v8 + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 with: name: sbom-spdx_2.2 path: spdx_2.2 diff --git a/.github/workflows/signing.yml b/.github/workflows/signing.yml deleted file mode 100644 index 5d266bf..0000000 --- a/.github/workflows/signing.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Build and Publish - -on: - workflow_dispatch: - -env: - BUILD_CONFIGURATION: Release - DOTNET_VERSION: '9.x' - -jobs: - build-sign-publish: - runs-on: windows-latest - environment: nuget-org-publish - permissions: - id-token: write - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Get version from tag - id: version - shell: pwsh - run: | - $version = "${{ github.ref_name }}" - $version = "99.99.99" - Write-Host "Version: $version" - echo "version=$version" >> $env:GITHUB_OUTPUT - - - name: Build - run: | - dotnet build Infragistics.QueryBuilder.Executor.csproj ` - -c ${{ env.BUILD_CONFIGURATION }} ` - /p:Version=${{ steps.version.outputs.version }} - - - name: Setup Code Signing Certificate - run: | - Write-Host "Setting up code signing certificate from GitHub secrets..." - - # Create certificate file from secret (base64 encoded) - $certBytes = [Convert]::FromBase64String("${{ secrets.SIGNING_CERTIFICATE_2023_2026 }}") - [System.IO.File]::WriteAllBytes("${{ runner.temp }}\certificate.pfx", $certBytes) - Write-Host "Certificate written to: $certPath" - shell: pwsh - - - name: Sign all DLL files - if: false # This step will never run - shell: pwsh - env: - CERT_PASS: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} - TIMESTAMP_URL: ${{ vars.SIGNING_CERTIFICATE_TIMESTAMP_URL }} - run: | - $dllFolder = "${{ github.workspace }}\bin\${{ env.BUILD_CONFIGURATION }}\net9.0" - $certPath = "${{ runner.temp }}\certificate.pfx" - Write-Host "Signing DLLs in folder: $dllFolder" - - # Find the latest signtool.exe - Write-Host "##[section]Starting search for signtool.exe at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')" - - $signtoolPath = $null - $searchPaths = @( - "C:\Program Files (x86)\Windows Kits\10\bin\*\x64\signtool.exe", - "C:\Program Files (x86)\Windows Kits\10\bin\*\x86\signtool.exe", - "C:\Program Files (x86)\Microsoft SDKs\Windows\*\bin\*\signtool.exe", - "C:\Program Files (x86)\Microsoft SDKs\Windows\*\bin\signtool.exe" - ) - - foreach ($searchPath in $searchPaths) { - $foundPaths = Get-ChildItem -Path $searchPath -ErrorAction SilentlyContinue | Sort-Object -Property FullName -Descending - if ($foundPaths) { - $signtoolPath = $foundPaths[0].FullName - break - } - } - - if (-not $signtoolPath) { - Write-Error "signtool.exe not found in any of the well-known locations" - exit 1 - } - - Write-Host "##[section]Found signtool.exe at $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss.fff')" - Write-Host "Using signtool at: $signtoolPath" - - $dllFiles = Get-ChildItem -Path $dllFolder -Filter *.dll -Recurse - foreach ($dll in $dllFiles) { - Write-Host "Signing $($dll.FullName)..." - & $signtoolPath sign /f $certPath /p $env:CERT_PASS /tr $env:TIMESTAMP_URL /td sha256 /fd sha256 $dll.FullName - - if ($LASTEXITCODE -ne 0) { - Write-Error "Signing failed for $($dll.FullName)" - exit 1 - } - } - - - name: Pack NuGet package - shell: pwsh - run: | - $packageOutputDir = "${{ github.workspace }}\nupkg" - $packageVersion = "${{ steps.version.outputs.version }}" - - Write-Host "Packing project from existing build output..." - dotnet pack ./Infragistics.QueryBuilder.Executor.csproj ` - --no-build ` - --configuration ${{ env.BUILD_CONFIGURATION }} ` - -p:PackageVersion=$packageVersion ` - -o $packageOutputDir - - if ($LASTEXITCODE -ne 0) { - Write-Error "dotnet pack failed" - exit 1 - } - - - name: Sign NuGet package - shell: pwsh - env: - SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.SIGNING_CERTIFICATE_PASSWORD }} - SIGNING_CERTIFICATE_TIMESTAMP_URL: ${{ vars.SIGNING_CERTIFICATE_TIMESTAMP_URL }} - run: | - $certPath = "${{ runner.temp }}\certificate.pfx" - $nupkgPath = "${{ github.workspace }}\nupkg\*.nupkg" - - dotnet nuget sign $nupkgPath ` - --certificate-path $certPath ` - --certificate-password "$env:SIGNING_CERTIFICATE_PASSWORD" ` - --timestamper "$env:SIGNING_CERTIFICATE_TIMESTAMP_URL" ` - --overwrite - - - - name: NuGet login (OIDC Trusted Publishing) - uses: nuget/login@v1 - id: nuget-login - with: - user: ${{ secrets.NUGET_ORG_USER }} - - - name: Clean up certificate - if: always() - shell: pwsh - run: | - $certPath = "${{ runner.temp }}\certificate.pfx" - if (Test-Path $certPath) { - Remove-Item $certPath -Force - Write-Host "Certificate cleaned up" - }