From 6326af6b4b1eeec306d9390c22f021d8b5a5e453 Mon Sep 17 00:00:00 2001 From: Ben Limmer Date: Wed, 13 May 2026 13:51:40 -0600 Subject: [PATCH] ci: add zizmor for GitHub Actions security scanning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces zizmor static analysis for .github/. Findings upload to GitHub code scanning (Security tab), and the workflow's SARIF format keeps the job's exit at 0 regardless of findings. The audit is gated on changes to .github/** or the root zizmor.yml so we don't burn the shared 5,000/hr GITHUB_TOKEN budget on every PR. Also address the findings on existing workflows: - Fix template-injection in the fetch-release-secrets composite action by routing inputs.parameter-path through env-var indirection. - Pin 13 actions to commit SHAs across ci.yml, release-sdk.yml, release-plz.yml, deploy-website.yml, fetch-release-secrets, and zizmor.yml. dtolnay/rust-toolchain@stable keeps its branch ref (the action's documented "stable channel" usage) with inline zizmor: ignore[unpinned-uses] comments. Important: SHAs come from the commit those tags point at, not the annotated-tag object SHA. Annotated tags are accepted by the runner but trigger a worst-case API loop in zizmor's impostor-commit audit (per zizmorcore/zizmor#1997 — ~3,500 calls for github/codeql-action alone). Use `gh api repos/X/git/refs/tags/Y` then dereference via `gh api repos/X/git/tags/` when .object.type is "tag". - Bump aws-actions/configure-aws-credentials v6.1.0 -> v6.1.1. - Add persist-credentials: false to all checkouts in ci.yml, release-sdk.yml, and deploy-website.yml. - Add top-level permissions: contents: read to ci.yml and release-plz.yml. deploy-website.yml's workflow-level pages: write / id-token: write move down to the deploy job only. - Add 3-day dependabot cooldown across all ecosystems with inline zizmor: ignore[dependabot-cooldown] (matches planbridge: fast bumps for low-risk updates; security advisories bypass cooldown anyway). - Exclude .github/workflows/release.yml from audit. It is generated by cargo-dist, so any fixes there get clobbered on `dist generate`. - The mise-action and setup-node steps in release-sdk.yml carry inline zizmor: ignore[cache-poisoning] with a rationale comment. GitHub Actions cache scoping prevents PR-scope caches from propagating up to tag/main builds, so poisoning this workflow's cache requires a merged PR (at which point caching is the least of the problems). The build-time win is worth keeping. Heads up: GitHub code scanning must be enabled (Settings -> Code security) for the SARIF upload step to succeed on first run. --- .../actions/fetch-release-secrets/action.yml | 6 +- .github/dependabot.yml | 8 +++ .github/workflows/ci.yml | 59 ++++++++++------ .github/workflows/deploy-website.yml | 17 +++-- .github/workflows/release-plz.yml | 23 ++++--- .github/workflows/release-sdk.yml | 13 +++- .github/workflows/zizmor.yml | 67 +++++++++++++++++++ zizmor.yml | 1 + 8 files changed, 150 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/zizmor.yml create mode 100644 zizmor.yml diff --git a/.github/actions/fetch-release-secrets/action.yml b/.github/actions/fetch-release-secrets/action.yml index d486548e8..b75bd0c99 100644 --- a/.github/actions/fetch-release-secrets/action.yml +++ b/.github/actions/fetch-release-secrets/action.yml @@ -18,17 +18,19 @@ runs: using: 'composite' steps: - name: Configure AWS Credentials - uses: aws-actions/configure-aws-credentials@v6.1.0 + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: role-to-assume: ${{ inputs.role-to-assume }} aws-region: ${{ inputs.aws-region }} - name: Fetch secrets from SSM shell: bash + env: + PARAMETER_PATH: ${{ inputs.parameter-path }} run: | set -euo pipefail - PARAM_PATH="${{ inputs.parameter-path }}" + PARAM_PATH="$PARAMETER_PATH" PARAMS=$(aws ssm get-parameters-by-path \ --path "$PARAM_PATH" \ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 57574beb5..cb12cd01e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,8 @@ updates: schedule: interval: "weekly" day: "monday" + cooldown: + default-days: 3 # zizmor: ignore[dependabot-cooldown] open-pull-requests-limit: 10 commit-message: prefix: "chore(deps):" @@ -19,6 +21,8 @@ updates: schedule: interval: "weekly" day: "monday" + cooldown: + default-days: 3 # zizmor: ignore[dependabot-cooldown] commit-message: prefix: "chore(deps):" ignore: @@ -32,6 +36,8 @@ updates: schedule: interval: "weekly" day: "monday" + cooldown: + default-days: 3 # zizmor: ignore[dependabot-cooldown] commit-message: prefix: "chore(deps):" @@ -40,6 +46,8 @@ updates: schedule: interval: "weekly" day: "monday" + cooldown: + default-days: 3 # zizmor: ignore[dependabot-cooldown] commit-message: prefix: "chore(deps):" groups: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf3a7263d..1b54fb811 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,23 +6,28 @@ on: - main pull_request: +permissions: + contents: read + jobs: fmt: name: Formatting runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] with: components: rustfmt, clippy, rust-analyzer - name: Install TypeScript language server run: npm install -g typescript typescript-language-server - - uses: jdx/mise-action@v4 - - uses: Swatinem/rust-cache@v2 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Build aether-lspd run: cargo build -p aether-lspd - name: Check formatting @@ -33,17 +38,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] with: components: rustfmt, clippy, rust-analyzer - name: Install TypeScript language server run: npm install -g typescript typescript-language-server - - uses: jdx/mise-action@v4 - - uses: Swatinem/rust-cache@v2 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Build aether-lspd run: cargo build -p aether-lspd - name: Clippy check @@ -54,17 +61,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] with: components: rustfmt, clippy, rust-analyzer - name: Install TypeScript language server run: npm install -g typescript typescript-language-server - - uses: jdx/mise-action@v4 - - uses: Swatinem/rust-cache@v2 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Build aether-lspd run: cargo build -p aether-lspd - name: Run tests @@ -75,13 +84,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - - uses: jdx/mise-action@v4 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Check formatting @@ -98,17 +109,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] with: components: rustfmt, clippy, rust-analyzer - name: Install TypeScript language server run: npm install -g typescript typescript-language-server - - uses: jdx/mise-action@v4 - - uses: Swatinem/rust-cache@v2 + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Build aether-lspd run: cargo build -p aether-lspd - name: Check documentation @@ -119,8 +132,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: jdx/mise-action@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Check formatting diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index 278fc288b..1a2823c71 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -15,8 +15,6 @@ on: permissions: contents: read - pages: write - id-token: write concurrency: group: github-pages @@ -28,16 +26,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v6 - - uses: jdx/mise-action@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 - name: Install dependencies run: pnpm install --frozen-lockfile - name: Build website run: pnpm website:build - name: Configure GitHub Pages - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Upload GitHub Pages artifact - uses: actions/upload-pages-artifact@v5 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: website/dist @@ -45,10 +45,13 @@ jobs: name: Deploy website needs: build runs-on: ubuntu-latest + permissions: + pages: write + id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index d75cb3a90..332733807 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -4,6 +4,9 @@ on: push: branches: [main] +permissions: + contents: read + jobs: release-plz-release: name: Release-plz release @@ -16,14 +19,14 @@ jobs: steps: - name: Generate ContextBridge app token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ secrets.CB_PR_AUTOMATION_APP_ID }} private-key: ${{ secrets.CB_PR_AUTOMATION_APP_PRIVATE_KEY }} owner: contextbridge repositories: aether - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -31,10 +34,10 @@ jobs: - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Run release-plz - uses: release-plz/action@v0.5.129 + uses: release-plz/action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129 with: command: release env: @@ -53,14 +56,14 @@ jobs: steps: - name: Generate ContextBridge app token id: app-token - uses: actions/create-github-app-token@v3 + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 with: client-id: ${{ secrets.CB_PR_AUTOMATION_APP_ID }} private-key: ${{ secrets.CB_PR_AUTOMATION_APP_PRIVATE_KEY }} owner: contextbridge repositories: aether - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 persist-credentials: false @@ -68,10 +71,10 @@ jobs: - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + uses: dtolnay/rust-toolchain@stable # zizmor: ignore[unpinned-uses] + - uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 - name: Run release-plz - uses: release-plz/action@v0.5.129 + uses: release-plz/action@064f4d1e36c843611ddf013be726beaa4ad804db # v0.5.129 with: command: release-pr env: diff --git a/.github/workflows/release-sdk.yml b/.github/workflows/release-sdk.yml index 257ddd9a1..509613754 100644 --- a/.github/workflows/release-sdk.yml +++ b/.github/workflows/release-sdk.yml @@ -18,15 +18,22 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev - - uses: jdx/mise-action@v4 + # zizmor cache-poisoning fires here because this is a publishing + # workflow with cache-enabled actions. The threat is bounded: + # GitHub Actions cache scoping prevents PR-scope caches from + # propagating up to tag builds, so an attacker would need a merged + # PR to poison the cache that this workflow reads. + - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1 # zizmor: ignore[cache-poisoning] - name: Setup npm registry - uses: actions/setup-node@v6 + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 # zizmor: ignore[cache-poisoning] with: node-version-file: .tool-versions registry-url: "https://registry.npmjs.org" diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000..8c751689d --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,67 @@ +name: zizmor + +on: + pull_request: + push: + branches: [main] + +permissions: {} + +concurrency: + group: zizmor-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: read # zizmor reads workflow run metadata for online audits + security-events: write # upload SARIF to GitHub code scanning + steps: + # Audit only when something zizmor cares about changed. Keeps the job + # a required check that always reports a status, while skipping the + # API-heavy audit (and SARIF upload) on unrelated PRs. Trigger-level + # `paths:` would leave the check stuck in Pending and block merges. + - name: Detect workflow changes + id: changes + uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + filters: | + github: + - '.github/**' + - 'zizmor.yml' + + - name: Checkout + if: steps.changes.outputs.github == 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install uv + if: steps.changes.outputs.github == 'true' + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + + # release.yml is generated by cargo-dist; pin/permission fixes there + # get clobbered on every `dist generate`, so we exclude it from audit. + - name: Run zizmor + if: steps.changes.outputs.github == 'true' + shell: bash + run: | + set -euo pipefail + mapfile -t inputs < <( + find .github -type f \( -name '*.yml' -o -name '*.yaml' \) \ + ! -path '.github/workflows/release.yml' + ) + uvx zizmor --format=sarif "${inputs[@]}" > zizmor.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload zizmor SARIF to code scanning + if: steps.changes.outputs.github == 'true' + uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + with: + sarif_file: zizmor.sarif + category: zizmor diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 000000000..550e33a7c --- /dev/null +++ b/zizmor.yml @@ -0,0 +1 @@ +rules: {}