From 1d0e79e71d107ef35394e366ee370bc7259087c8 Mon Sep 17 00:00:00 2001 From: Douglas Eichelberger Date: Wed, 19 Aug 2026 15:15:45 -0700 Subject: [PATCH] Pin all GitHub Actions to commits and add the zizmor linter Follow-up to #51. Denis raised two things on that PR: whether `taiki-e/install-action` is trustworthy, and whether we should pin actions by SHA and run zizmor like other rubyatscale repos do. This does the pinning, adds the linter, and clears every finding it reports. ## zizmor `.github/workflows/zizmor.yml` is copied verbatim from shared-config, which is also what visualize_packs uses. It is a copied file rather than a `uses:` of a reusable workflow because shared-config's zizmor.yml has no `workflow_call` trigger, unlike its codeql.yml. It defaults to `advanced-security: true`, which runs zizmor in SARIF mode. zizmor exits 0 in that mode even when it has findings, so this reports into the Security tab and cannot turn the build red on its own. ## Pinning Every action in the repo is now pinned to a commit, with the version in a trailing comment: - `audit.yml`: `install-action` to v2.86.3 and `checkout` to v7.0.1. `checkout` was on v5, two majors behind the v7.0.1 that shared-config standardizes on. - `ci.yml`: three `checkout@v2` uses, also now v7.0.1. - `release.yml`: all 17 uses, via `[dist.github-action-commits]` in dist-workspace.toml. That file is generated by dist, so editing it directly does not survive the next `dist generate`; the config is the supported knob for this and landed in dist 0.29.0, and we pin 0.30.3. Each action stays on the major version dist 0.30.3 already emitted, so this is a pure pin with no behavior change. `dist generate --check` passes, and the regenerated diff touches nothing but the pinned refs. ## Hardening beyond pinning - `persist-credentials: false` on every checkout, so the job's token is not left behind in `.git/config` (zizmor's artipacked audit). - `permissions: {}` at the top of audit.yml, leaving the job to opt into `contents: read`. - `fallback: none` on install-action. Its default falls back to cargo-binstall, which it passes a token to, and can reach `cargo install` -- the exact path whose compile failure #51 fixed. This keeps it from quietly coming back if a download fails. On install-action itself: it verifies SHA256 checksums by default and attestations where upstream publishes them, and applies a dependency cooldown, none of which the `actions-rs/audit-check` it replaced did. ## What is ignored, and why `.github/zizmor.yml` ignores three audits, scoped to release.yml only: excessive-permissions, template-injection, unpinned-images. All ten are structural to dist's template with no config knob, and dist would overwrite inline `# zizmor: ignore` comments. They are file-scoped rather than line-scoped because regeneration shifts line numbers. Reasoning for each is in the file. Net: 50 findings before, 26 after the pins and hardening, 0 with the documented ignores. `cargo test` passes. --- .github/workflows/audit.yml | 17 ++++++++++++----- .github/workflows/ci.yml | 12 +++++++++--- .github/workflows/release.yml | 34 +++++++++++++++++----------------- .github/workflows/zizmor.yml | 23 +++++++++++++++++++++++ .github/zizmor.yml | 33 +++++++++++++++++++++++++++++++++ dist-workspace.toml | 11 +++++++++++ 6 files changed, 105 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/zizmor.yml create mode 100644 .github/zizmor.yml diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 680dc11..1cced08 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -9,17 +9,24 @@ on: - '**/Cargo.lock' pull_request: +permissions: {} + jobs: audit: permissions: contents: read runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - # Installs a prebuilt cargo-audit from the upstream GitHub releases. - # `actions-rs/audit-check` built it with `cargo install` instead, which - # broke once cargo-audit's dependencies outran the runner's toolchain. - - uses: taiki-e/install-action@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # Installs a prebuilt cargo-audit from the upstream GitHub releases and + # verifies its SHA256 checksum. `actions-rs/audit-check` built it with + # `cargo install`, which broke once cargo-audit's dependencies outran the + # runner's toolchain. `fallback: none` keeps that compile path from + # silently coming back if the download ever fails. + - uses: taiki-e/install-action@5b4d68e2e660441203ab128a23676f1e4faf1532 # v2.86.3 with: tool: cargo-audit + fallback: none - run: cargo audit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f77e08b..6515afb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run cargo check run: cargo check @@ -35,7 +37,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run cargo test with backtrace run: cargo test -- --nocapture @@ -48,7 +52,9 @@ jobs: RUSTFLAGS: '-Dwarnings' steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Run cargo fmt run: cargo fmt --all -- --check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ea7152..78ccf1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -56,7 +56,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false submodules: recursive @@ -66,7 +66,7 @@ jobs: shell: bash run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: cargo-dist-cache path: ~/.cargo/bin/dist @@ -82,7 +82,7 @@ jobs: cat plan-dist-manifest.json echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json @@ -120,7 +120,7 @@ jobs: - name: enable windows longpaths run: | git config --global core.longpaths true - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false submodules: recursive @@ -135,7 +135,7 @@ jobs: run: ${{ matrix.install_dist.run }} # Get the dist-manifest - name: Fetch local artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -149,7 +149,7 @@ jobs: dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json echo "dist ran successfully" - name: Attest - uses: actions/attest-build-provenance@v2 + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be with: subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*" - id: cargo-dist @@ -166,7 +166,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: artifacts-build-local-${{ join(matrix.targets, '_') }} path: | @@ -183,19 +183,19 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -213,7 +213,7 @@ jobs: cp dist-manifest.json "$BUILD_MANIFEST_NAME" - name: "Upload artifacts" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: artifacts-build-global path: | @@ -233,19 +233,19 @@ jobs: outputs: val: ${{ steps.host.outputs.manifest }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false submodules: recursive - name: Install cached dist - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: name: cargo-dist-cache path: ~/.cargo/bin/ - run: chmod +x ~/.cargo/bin/dist # Fetch artifacts from scratch-storage - name: Fetch artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: target/distrib/ @@ -258,14 +258,14 @@ jobs: cat dist-manifest.json echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" - name: "Upload dist-manifest.json" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: # Overwrite the previous copy name: artifacts-dist-manifest path: dist-manifest.json # Create a GitHub Release while uploading all files to it - name: "Download GitHub Artifacts" - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 with: pattern: artifacts-* path: artifacts @@ -298,7 +298,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 with: persist-credentials: false submodules: recursive diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..e911917 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,23 @@ +name: GitHub Actions Security Analysis + +on: + push: + branches: [main] + pull_request: + branches: ["**"] + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + actions: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Run zizmor + uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..19c0fdd --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,33 @@ +# Configuration for the zizmor GitHub Actions security linter. +# See https://docs.zizmor.sh/configuration/ +# +# Everything ignored below is in release.yml, which is autogenerated by dist +# (cargo-dist) from dist-workspace.toml -- see the header comment in that file. +# Editing release.yml by hand does not work: the next `dist generate` overwrites +# it. Where dist offers a knob we use it (the actions in release.yml are pinned +# to commits via [dist.github-action-commits] in dist-workspace.toml), and what +# is left below is inherent to dist's template and has no such knob. +# +# These are deliberately file-scoped rather than line-scoped, because line +# numbers shift whenever release.yml is regenerated. +rules: + excessive-permissions: + ignore: + # dist declares `contents: write` at the workflow level because the + # announce job creates the GitHub Release. Narrowing it per-job is + # dist's call, not ours. + - release.yml + template-injection: + ignore: + # dist interpolates `github.ref_name`, `needs.plan.outputs.tag*` and its + # `matrix.*` build parameters straight into run blocks. The tag-derived + # values only expand on `push:` of a tag, which requires push access, and + # expand to '' on pull_request. The matrix values come from dist's own + # plan output, not from user input. + - release.yml + unpinned-images: + ignore: + # `container: ${{ matrix.container && matrix.container.image || null }}`. + # This repo configures no custom container runners, so matrix.container is + # always null and no image is ever pulled. + - release.yml diff --git a/dist-workspace.toml b/dist-workspace.toml index a719571..138620e 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -24,3 +24,14 @@ install-path = "CARGO_HOME" install-updater = false # Link build artifacts to the process which created it github-attestations = true + +# Pin the GitHub Actions that dist generates into release.yml to exact commits +# rather than floating tags, so a compromised or retagged upstream release can't +# silently change what runs. Keep these on the same major version that dist +# 0.30.3 generates; bump the SHA and the trailing version comment together. +# Docs: https://axodotdev.github.io/cargo-dist/book/ci/customizing.html#pinned-actions-commits +[dist.github-action-commits] +"actions/checkout" = "11d5960a326750d5838078e36cf38b85af677262" # v4.4.0 +"actions/upload-artifact" = "ea165f8d65b6e75b540449e92b4886f43607fa02" # v4.6.2 +"actions/download-artifact" = "d3f86a106a0bac45b974a628896c90dbdf5c8093" # v4.3.0 +"actions/attest-build-provenance" = "e8998f949152b193b063cb0ec769d69d929409be" # v2.4.0