From 46bb67450029778cbab8e227b7064f39f07e1b79 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Tue, 12 May 2026 16:00:12 -0400 Subject: [PATCH] chore(ci): add zizmor workflow for github actions security analysis (#471) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds [zizmor](https://github.com/zizmorcore/zizmor) to audit GitHub Actions workflows for security issues. Runs on push to main and on PRs that change `.github/workflows/**`. Fails CI on any finding. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- > [!NOTE] > **Medium Risk** > Mostly CI/workflow hardening, but it also changes release automation (`postversion.sh`) and workflow permissions/credentials behavior, which could break tagging/publishing if misconfigured. > > **Overview** > Adds a new `zizmor` workflow that runs on PRs/pushes touching `.github/workflows/**` to security-audit workflows. > > Hardens existing workflows by defaulting to least-privilege `permissions`, setting `actions/checkout` to `persist-credentials: false`, and adjusting related behavior (e.g., `scripts/postversion.sh` now runs `gh auth setup-git` so `git push` still works; `ci.yml` disables `mise-action` caching; `test.yml` avoids interpolating `steps.bad.outcome` inside a shell string by passing it via env). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d878aee510daaa5a948092861d1408f6120073eb. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/workflows/check-dist.yml | 2 ++ .github/workflows/ci.yml | 7 +++++++ .github/workflows/codeql-analysis.yml | 2 ++ .github/workflows/release-plz.yml | 1 + .github/workflows/release.yml | 11 ++++++++--- .github/workflows/test-redacted-env.yml | 5 +++++ .github/workflows/test.yml | 19 ++++++++++++++++++- .github/workflows/zizmor.yml | 22 ++++++++++++++++++++++ scripts/postversion.sh | 5 +++++ 9 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml index 4c49be4..14ede83 100644 --- a/.github/workflows/check-dist.yml +++ b/.github/workflows/check-dist.yml @@ -32,6 +32,8 @@ jobs: - name: Checkout id: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b94ceb2..e7f78fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ on: - main - 'releases/*' +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: true @@ -20,6 +23,8 @@ jobs: - name: Checkout id: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false # `mise.toml` pins both Node and aube; mise-action installs # whatever's listed there. Reads `package-lock.json` @@ -28,6 +33,8 @@ jobs: # npm-flat (rollup's `--configPlugin` resolution # requires this). - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 + with: + cache: false - name: Install Dependencies id: aube-ci diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf9e725..a8f13c6 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -35,6 +35,8 @@ jobs: - name: Checkout id: checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Initialize CodeQL id: initialize diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 6fbb4d3..f9fff0c 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -26,6 +26,7 @@ jobs: fetch-depth: 0 submodules: recursive token: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }} + persist-credentials: false - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 - run: mise run release-plz env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6dab23e..e6ba3d2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,19 +5,21 @@ on: types: [closed] branches: [main] -permissions: - contents: write +permissions: {} jobs: release: if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release') runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Setup mise uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 @@ -30,10 +32,13 @@ jobs: enhance-release: needs: [release] runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: fetch-depth: 0 + persist-credentials: false - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 - name: Enhance release notes with communique run: | @@ -41,4 +46,4 @@ jobs: communique generate "$TAG_NAME" --github-release env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }} diff --git a/.github/workflows/test-redacted-env.yml b/.github/workflows/test-redacted-env.yml index a7cbd84..03599cb 100644 --- a/.github/workflows/test-redacted-env.yml +++ b/.github/workflows/test-redacted-env.yml @@ -7,12 +7,17 @@ on: branches: [main] workflow_dispatch: +permissions: + contents: read + jobs: test-redacted-env: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Create test mise config with sensitive values run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 21c2cf3..ca6ccc6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,9 @@ on: # rebuild any PRs and main branch changes - main workflow_dispatch: +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref_name }} cancel-in-progress: true @@ -17,6 +20,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4 - run: aube install - run: aubr all @@ -43,6 +48,8 @@ jobs: if: ${{ matrix.requirements }} run: ${{ matrix.requirements }} - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup mise uses: ./ with: @@ -65,6 +72,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup mise uses: ./ with: @@ -90,6 +99,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup mise id: bad uses: ./ @@ -107,14 +118,18 @@ jobs: if: ${{ steps.bad.outcome == 'failure' }} - name: not failed as expected run: | - echo "Expected failure but the job was ${{ steps.bad.outcome }}" + echo "Expected failure but the job was ${STEPS_BAD_OUTCOME}" exit 1 if: ${{ steps.bad.outcome != 'failure' }} + env: + STEPS_BAD_OUTCOME: ${{ steps.bad.outcome }} custom_cache_key: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup mise with custom cache key uses: ./ with: @@ -132,6 +147,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false - name: Setup mise from mise.jdx.dev uses: ./ with: diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..90ad35f --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,22 @@ +name: zizmor +on: + push: + branches: [main] + paths: ['.github/workflows/**'] + pull_request: + paths: ['.github/workflows/**'] + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + advanced-security: false diff --git a/scripts/postversion.sh b/scripts/postversion.sh index 7a57ee4..253e80f 100755 --- a/scripts/postversion.sh +++ b/scripts/postversion.sh @@ -4,6 +4,11 @@ set -euxo pipefail VERSION=$(jq -r .version package.json) MAJOR_VERSION=$(echo "$VERSION" | cut -d. -f1) +# Configure git to use gh's credential helper. The checkout step uses +# persist-credentials: false (per zizmor's artipacked audit), so the +# token isn't written to .git/config and raw `git push` would 403. +gh auth setup-git + # create the version tag (allow it to fail if it already exists) git tag "v$VERSION" || echo "Tag v$VERSION already exists locally"