Skip to content

chore: add zizmor security audit; harden existing workflow permissions#5

Merged
timzsu merged 4 commits into
mainfrom
zsu/zizmor-ci
Apr 30, 2026
Merged

chore: add zizmor security audit; harden existing workflow permissions#5
timzsu merged 4 commits into
mainfrom
zsu/zizmor-ci

Conversation

@timzsu
Copy link
Copy Markdown
Collaborator

@timzsu timzsu commented Apr 30, 2026

Purpose

Add zizmor to CI — a static auditor for GitHub Actions workflows. Lives in .github/workflows/security.yml, so future security tools (gitleaks, bandit, pip-audit) land as additional jobs in the same file.

Changes

  • .github/workflows/security.yml — new workflow with a zizmor job. Runs uvx --from zizmor==1.24.1 zizmor --persona regular --format github .github/workflows on every PR and on push to main. Pinned to 1.24.1 because unpinned uvx zizmor resolved to a pre-1.x version in CI that didn't accept --format github.
  • Existing six workflows — close zizmor findings: top-level permissions: contents: read, with: persist-credentials: false on every checkout, every uses: pinned to its content SHA with a # vN comment.

Pinned actions:

  • actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
  • astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7

No suppressions: no .github/zizmor.yml, no per-rule disables.

Design

SHA pinning was the actual recommendation behind the unpinned-uses audit; pin rather than suppress. The # vN comment after each SHA preserves readability for reviewers. The zizmorcore/zizmor-action was rejected — it requires Docker-in-Docker, which the self-hosted runner container doesn't provide.

Test Plan

uvx --from zizmor==1.24.1 zizmor --persona regular .github/workflows

Test Result

No findings to report. Good job! (22 suppressed)
exit=0

The 22 suppressed entries are zizmor's own built-in default-disabled audits at the regular persona; no project-side suppression.


Pre-submission Checklist
  • I have read CONTRIBUTING.md (or AGENTS.md if no CONTRIBUTING.md).
  • I have run uv run pre-commit run --all-files and fixed any issues.
  • I have added or updated tests covering my changes (if applicable).
  • I have verified that the test suite passes locally.
  • If this is a breaking change, I have prefixed the PR title with [BREAKING] and described migration steps above.

@timzsu timzsu changed the base branch from main to zsu/ci-concurrency April 30, 2026 09:08
@timzsu timzsu force-pushed the zsu/zizmor-ci branch 2 times, most recently from 77b0784 to c1f68c5 Compare April 30, 2026 10:21
@timzsu timzsu requested a review from kaiitunnz April 30, 2026 10:38
Copy link
Copy Markdown
Collaborator

@kaiitunnz kaiitunnz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Base automatically changed from zsu/ci-concurrency to main April 30, 2026 13:12
timzsu and others added 4 commits April 30, 2026 13:15
Adds a ``zizmor`` GitHub Actions workflow that audits ``.github/workflows``
on every PR that touches workflow / action definitions, with results
surfaced as inline annotations (no GHAS / SARIF). Per-rule overrides
live in ``.github/zizmor.yml``; ``unpinned-uses`` is disabled there
pending a dedicated SHA-pin sweep.

To make the new audit pass on the existing workflow set, the six
existing workflows gain:

* a top-level ``permissions: contents: read`` block (closes
  ``excessive-permissions`` finding — default token permissions were
  much broader than these read-only jobs need)
* ``with: persist-credentials: false`` on every ``actions/checkout``
  step (closes ``artipacked`` — prevents the runner from caching the
  GitHub App token in ``.git/config``, which would persist across PR
  runs on a self-hosted runner)

Local zizmor run after the changes: ``No findings to report``
(23 suppressed are the unpinned-uses ones disabled in
``.github/zizmor.yml``).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
The previous commit ran zizmor through ``zizmorcore/zizmor-action``,
which spawns ``docker run`` — the self-hosted runner container has no
Docker daemon access (no Docker-in-Docker), so the action exited 125
("Cannot connect to the Docker daemon"). Switch the workflow to
``uvx zizmor --persona regular --format github`` so the audit runs
in the same Python environment that ``setup-uv`` already provisions.

While replacing the action, also pin every remaining action to its
content SHA:

* ``actions/checkout@v6`` →
  ``actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6``
* ``astral-sh/setup-uv@v7`` →
  ``astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7``

This closes the ``unpinned-uses`` audit zizmor was suppressing via the
``.github/zizmor.yml`` config — that suppression file is now removed,
since there are no rules left to disable. The version comment after
each SHA stays so a human can read what was pinned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Renames ``zizmor.yml`` to ``security.yml`` and updates the workflow's
``name:`` to ``security``. The file now reads as the home for every
security-CI job, so adding gitleaks / bandit / pip-audit later means a
new ``jobs.<name>:`` block, not a new file.

The ``Run zizmor`` step now pins ``zizmor==1.24.1`` via
``uvx --from zizmor==1.24.1 zizmor``. Without the pin, the CI's uvx was
resolving to a pre-1.x zizmor whose ``--format`` argument only accepted
``[plain, json, sarif]`` (no ``github``), so the step exited 2 with
``invalid value 'github' for '--format'``. Pinning to 1.24.1 (the
version verified locally) locks the CLI surface.

The ``paths:`` filter that scoped the workflow to ``.github/workflows``
changes is dropped — the renamed file runs every PR, which is the right
trigger now that the security audit has a broader scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
Bumps the zizmor invocation in ``security.yml`` from
``--persona regular`` to ``--persona pedantic`` (the strictest level
whose findings are actually addressable for this project) and closes
every finding pedantic surfaces:

* ``anonymous-definition`` (7×, info) — every existing job lacks a
  human-readable ``name:`` field. Adds one to each.
* ``template-injection`` (2×, low) — ``check-signoff`` interpolated
  ``${{ github.event.pull_request.{base,head}.sha }}`` directly into
  a shell ``run:`` block. Moved to an ``env:`` mapping so the values
  pass through as ordinary shell env vars and never reach
  template-expansion in the script body.

Why pedantic, not auditor: ``auditor`` adds 7 ``self-hosted-runner``
warnings — one per job — flagging the use of self-hosted infra at all.
That's an architectural choice for this project, not a bug. Pedantic
catches every actionable issue without that noise.

After this commit, ``zizmor --persona pedantic .github/workflows``
reports ``No findings to report``. The 7 still-suppressed entries are
auditor-only audits, not project-side suppressions; ``.github/zizmor.yml``
remains absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Zhengyuan Su <su.zhengyuan@u.nus.edu>
@timzsu timzsu merged commit 69ae9e9 into main Apr 30, 2026
7 checks passed
@timzsu timzsu deleted the zsu/zizmor-ci branch April 30, 2026 13:21
@timzsu timzsu linked an issue Apr 30, 2026 that may be closed by this pull request
7 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants