From c1c47f746ed8d751ae9a55fd97a23cb274713f43 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 13:50:41 +0200 Subject: [PATCH] Harden the CI workflow Actions pinned to full commits, no token left in the checkout, no binaries from a fork's pull request among the artifacts, the version passed through the environment, an audit of the NuGet packages, time limits, one release at a time with checksums and never over another commit's release, and Dependabot for the actions and packages. --- .github/dependabot.yml | 14 ++++++++ .github/workflows/dotnet.yml | 67 +++++++++++++++++++++++++++++++----- 2 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..64ca4fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Pull requests for new versions of the pinned actions and of the NuGet packages, +# so a fix for a vulnerability reaches the build without anyone watching for it. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: nuget + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 1514026..fb83e2f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -13,6 +13,9 @@ on: workflow_dispatch: +# Read-only unless a job says otherwise; only the release writes. Actions are +# pinned to full commits - a tag can be moved under us, a commit cannot - and +# checkout leaves no token behind for the pull request code that runs next. permissions: contents: read @@ -40,13 +43,16 @@ jobs: os: ubuntu-22.04 runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup .NET 9 - uses: actions/setup-dotnet@v6 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 9.0.x @@ -56,21 +62,36 @@ jobs: "${{ env.TEST_PROJECT_PATH }}" -c Release + # A known vulnerability in a NuGet package, direct or pulled in, fails the run. + - name: Audit packages + if: runner.os == 'Linux' + shell: bash + run: | + set -euo pipefail + + dotnet list StarterNG.sln package --vulnerable --include-transitive 2>&1 | tee audit.txt + if grep -q "has the following vulnerable packages" audit.txt; then + echo "::error::Vulnerable NuGet packages, listed above" + exit 1 + fi + # Major and minor are set by hand in the project file; the patch is the number # of commits, so every build of main is higher than the one before and no bot # has to commit a bump back to the branch. version: name: Version runs-on: ubuntu-latest + timeout-minutes: 5 outputs: number: ${{ steps.number.outputs.number }} steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Number this build id: number @@ -112,13 +133,16 @@ jobs: package: artifacts/StarterNG-linux-x64.tar.gz runs-on: ${{ matrix.os }} + timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup .NET 9 - uses: actions/setup-dotnet@v6 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 with: dotnet-version: 9.0.x @@ -137,6 +161,9 @@ jobs: -r "${{ matrix.rid }}" - name: Publish NativeAOT + shell: bash + env: + VERSION: ${{ needs.version.outputs.number }} run: > dotnet publish "${{ env.PROJECT_PATH }}" @@ -146,7 +173,7 @@ jobs: --no-restore -p:PublishAot=true -p:PublishTrimmed=true - -p:Version=${{ needs.version.outputs.number }} + -p:Version="$VERSION" -o "publish/${{ matrix.rid }}" - name: Package Windows build @@ -170,8 +197,11 @@ jobs: -czf "${{ matrix.package }}" \ . + # A fork's pull request is built, to prove it builds, but its binaries are not + # handed out under the repository's name. - name: Upload artifact - uses: actions/upload-artifact@v7 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ matrix.artifact }} path: ${{ matrix.package }} @@ -190,6 +220,12 @@ jobs: needs: [publish, version] if: github.event_name == 'push' runs-on: ubuntu-latest + timeout-minutes: 10 + + # Two pushes in a row release one after the other, not over each other. + concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false permissions: contents: write @@ -200,12 +236,16 @@ jobs: # Windows .zip a file - unpacked, its contents would end up in the release # instead of the package. - name: Collect the packages - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: dist merge-multiple: true skip-decompress: true + - name: Checksum the packages + working-directory: dist + run: sha256sum StarterNG-windows-x64.zip StarterNG-linux-x64.tar.gz | tee SHA256SUMS.txt + - name: Publish the release if: github.ref_name == 'main' env: @@ -217,8 +257,15 @@ jobs: ls -l dist - # A re-run of the same commit finds its release already there. + # A re-run of the same commit finds its release already there and refreshes + # it; a release of the same number for another commit - main rewritten - is + # left alone rather than given packages that are not its own. if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then + tagged=$(gh api "repos/$GITHUB_REPOSITORY/commits/$TAG" --jq .sha) + if [ "$tagged" != "$GITHUB_SHA" ]; then + echo "::error::$TAG already belongs to $tagged, not to $GITHUB_SHA" + exit 1 + fi gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber exit 0 fi @@ -226,6 +273,7 @@ jobs: gh release create "$TAG" \ dist/StarterNG-windows-x64.zip \ dist/StarterNG-linux-x64.tar.gz \ + dist/SHA256SUMS.txt \ --repo "$GITHUB_REPOSITORY" \ --target "$GITHUB_SHA" \ --title "StarterNG $VERSION" \ @@ -248,6 +296,7 @@ jobs: gh release create staging \ dist/StarterNG-windows-x64.zip \ dist/StarterNG-linux-x64.tar.gz \ + dist/SHA256SUMS.txt \ --repo "$GITHUB_REPOSITORY" \ --target "$GITHUB_SHA" \ --prerelease \