Skip to content

Commit 9cae530

Browse files
committed
feat: add zizmor for GitHub Actions security
Add zizmor (GitHub Actions static analysis) across the three concerns: - repo-review: new GH106 check (uses the zizmor pre-commit hook or the zizmor-action), with tests and regenerated README. - guide: new "Linting your workflows" section in gha_basic.md; the page is now bumped by the pc_bump nox session. - cookiecutter: add the zizmor pre-commit hook and a .github/zizmor.yml (GitHub-CI projects only) that relaxes unpinned-uses to ref-pin so the template stays maintainable via Dependabot. The generated workflows are made zizmor-clean (top-level permissions, persist-credentials: false, enable-cache: false on the wheel build). GH106 is ignored for this repo's own workflows for now. Assisted-by: ClaudeCode:claude-opus-4.8
1 parent 917daa6 commit 9cae530

11 files changed

Lines changed: 144 additions & 2 deletions

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1
355355
- [`GH103`](https://learn.scientific-python.org/development/guides/gha-basic#GH103): At least one workflow with manual dispatch trigger
356356
- [`GH104`](https://learn.scientific-python.org/development/guides/gha-wheels#GH104): Use unique names for upload-artifact
357357
- [`GH105`](https://learn.scientific-python.org/development/guides/gha-basic#GH105): Use Trusted Publishing instead of token-based publishing on PyPI
358+
- [`GH106`](https://learn.scientific-python.org/development/guides/gha-basic#GH106): Use zizmor to check the GitHub Actions
358359
- [`GH200`](https://learn.scientific-python.org/development/guides/gha-basic#GH200): Maintained by Dependabot
359360
- [`GH210`](https://learn.scientific-python.org/development/guides/gha-basic#GH210): Maintains the GitHub action versions with Dependabot
360361
- [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions

docs/pages/guides/gha_basic.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,27 @@ run a manual check, like check-manifest, then you can keep it but just use
9191
this one check. You can also use `needs: lint` in your other jobs to keep them
9292
from running if the lint check does not pass.
9393

94+
### Linting your workflows
95+
96+
{rr}`GH106` GitHub Actions workflows are a common source of security issues,
97+
such as script injection from untrusted input, overly broad token permissions,
98+
and credentials accidentally persisted by `actions/checkout`.
99+
[zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your
100+
workflows for these problems. The easiest way to run it is as a pre-commit hook:
101+
102+
```yaml
103+
- repo: https://github.com/zizmorcore/zizmor-pre-commit
104+
rev: "v1.25.2"
105+
hooks:
106+
- id: zizmor
107+
```
108+
109+
You can silence individual findings with `# zizmor: ignore[rule]` comments, or
110+
collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config
111+
file. If you'd rather keep it out of pre-commit, zizmor also ships the
112+
[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action)
113+
GitHub Action, which can upload results to GitHub's code scanning dashboard.
114+
94115
### Unit tests
95116

96117
Implementing unit tests is also easy. Since you should be following best

noxfile.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def pc_bump(session: nox.Session) -> None:
426426
versions = {}
427427
pages = [
428428
Path("docs/pages/guides/style.md"),
429+
Path("docs/pages/guides/gha_basic.md"),
429430
Path("{{cookiecutter.project_name}}/.pre-commit-config.yaml"),
430431
Path(".pre-commit-config.yaml"),
431432
]

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ ignore = [
204204

205205
[tool.repo-review.ignore]
206206
RTD103 = "Using Ruby instead of Python for docs"
207+
GH106 = "zizmor adoption for this repo's own workflows is tracked separately"
207208

208209
[tool.typos.default.extend-words]
209210
nd = "nd"

src/sp_repo_review/checks/github.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,45 @@ def check(workflows: dict[str, Any]) -> str:
192192
return "\n".join(errors)
193193

194194

195+
class GH106(GitHub):
196+
"Use zizmor to check the GitHub Actions"
197+
198+
requires = {"GH100"}
199+
url = mk_url("gha-basic")
200+
201+
@staticmethod
202+
def check(precommit: dict[str, Any], workflows: dict[str, Any]) -> bool:
203+
"""
204+
Projects with GitHub Actions should statically analyze their workflows
205+
with [zizmor](https://docs.zizmor.sh), which catches common security
206+
issues such as template injection, excessive permissions, and
207+
credential persistence. The simplest way is to add the pre-commit hook:
208+
209+
```yaml
210+
- repo: https://github.com/zizmorcore/zizmor-pre-commit
211+
rev: v1.25.2
212+
hooks:
213+
- id: zizmor
214+
```
215+
216+
You can also run it as the `zizmorcore/zizmor-action` GitHub Action.
217+
"""
218+
for repo_item in precommit.get("repos", []):
219+
if (
220+
repo_item.get("repo", "").lower()
221+
== "https://github.com/zizmorcore/zizmor-pre-commit"
222+
):
223+
return True
224+
for workflow in workflows.values():
225+
for job in workflow.get("jobs", {}).values():
226+
if not isinstance(job, dict):
227+
continue
228+
for step in job.get("steps", []):
229+
if step.get("uses", "").startswith("zizmorcore/zizmor-action"):
230+
return True
231+
return False
232+
233+
195234
class GH200(GitHub):
196235
"Maintained by Dependabot"
197236

tests/test_github.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,45 @@ def test_gh105_token_based_upload() -> None:
180180
assert "Token-based publishing" in res.err_msg
181181

182182

183+
def test_gh106_precommit() -> None:
184+
precommit = yaml.safe_load(
185+
"""
186+
repos:
187+
- repo: https://github.com/zizmorcore/zizmor-pre-commit
188+
rev: v1.22.0
189+
hooks:
190+
- id: zizmor
191+
"""
192+
)
193+
assert compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result
194+
195+
196+
def test_gh106_action() -> None:
197+
workflows = yaml.safe_load(
198+
"""
199+
zizmor:
200+
jobs:
201+
zizmor:
202+
steps:
203+
- uses: zizmorcore/zizmor-action@v0.5.6
204+
"""
205+
)
206+
assert compute_check("GH106", precommit={}, workflows=workflows).result
207+
208+
209+
def test_gh106_missing() -> None:
210+
precommit = yaml.safe_load(
211+
"""
212+
repos:
213+
- repo: https://github.com/astral-sh/ruff-pre-commit
214+
rev: v0.15.16
215+
hooks:
216+
- id: ruff-check
217+
"""
218+
)
219+
assert not compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result
220+
221+
183222
def test_gh200() -> None:
184223
dependabot = yaml.safe_load(
185224
"""

{{cookiecutter.project_name}}/.github/workflows/ci.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ on:
77
branches:
88
- main
99

10+
permissions: {}
11+
1012
concurrency:
1113
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
1214
cancel-in-progress: true
@@ -21,10 +23,13 @@ jobs:
2123
lint:
2224
name: Format
2325
runs-on: ubuntu-latest
26+
permissions:
27+
contents: read
2428
steps:
2529
- uses: actions/checkout@v6
26-
{%- if cookiecutter.vcs %}
2730
with:
31+
persist-credentials: false
32+
{%- if cookiecutter.vcs %}
2833
fetch-depth: 0
2934
{%- endif %}
3035

@@ -45,6 +50,8 @@ jobs:
4550
{%- if cookiecutter.__type == "compiled" %}
4651
needs: [lint]
4752
{%- endif %}
53+
permissions:
54+
contents: read
4855
strategy:
4956
fail-fast: false
5057
matrix:
@@ -57,8 +64,9 @@ jobs:
5764

5865
steps:
5966
- uses: actions/checkout@v6
60-
{%- if cookiecutter.vcs %}
6167
with:
68+
persist-credentials: false
69+
{%- if cookiecutter.vcs %}
6270
fetch-depth: 0
6371
{%- endif %}
6472

{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type!='compiled' %}cd.yml{% endif %}

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010
types:
1111
- published
1212

13+
permissions: {}
14+
1315
concurrency:
1416
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
1517
cancel-in-progress: true
@@ -23,10 +25,13 @@ jobs:
2325
dist:
2426
name: Distribution build
2527
runs-on: ubuntu-latest
28+
permissions:
29+
contents: read
2630

2731
steps:
2832
- uses: actions/checkout@v6
2933
with:
34+
persist-credentials: false
3035
fetch-depth: 0
3136

3237
- uses: hynek/build-and-inspect-python-package@v2

{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type=='compiled' %}cd.yml{% endif %}

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
paths:
1010
- .github/workflows/cd.yml
1111

12+
permissions: {}
13+
1214
concurrency:
1315
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
1416
cancel-in-progress: true
@@ -22,9 +24,12 @@ jobs:
2224
make_sdist:
2325
name: Make SDist
2426
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
2529
steps:
2630
- uses: actions/checkout@v6
2731
with:
32+
persist-credentials: false
2833
fetch-depth: 0
2934

3035
- name: Build SDist
@@ -38,6 +43,8 @@ jobs:
3843
build_wheels:
3944
name: {% raw %}Wheel on ${{ matrix.os }}{% endraw %}
4045
runs-on: {% raw %}${{ matrix.os }}{% endraw %}
46+
permissions:
47+
contents: read
4148
strategy:
4249
fail-fast: false
4350
matrix:
@@ -52,9 +59,13 @@ jobs:
5259
steps:
5360
- uses: actions/checkout@v6
5461
with:
62+
persist-credentials: false
5563
fetch-depth: 0
5664

5765
- uses: astral-sh/setup-uv@v8.2.0
66+
with:
67+
# Disable caching to avoid poisoning published wheels
68+
enable-cache: false
5869

5970
- uses: pypa/cibuildwheel@v4.0
6071

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Configuration for zizmor (https://docs.zizmor.sh)
2+
rules:
3+
unpinned-uses:
4+
config:
5+
# Actions are kept up to date with Dependabot, so a ref (tag) pin is
6+
# sufficient; hash pinning is not required.
7+
policies:
8+
"*": ref-pin

0 commit comments

Comments
 (0)