From ea0143a02b0361a6200ec00a2b945bdee86838ae Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 9 Jun 2026 12:31:00 -0400 Subject: [PATCH 01/12] 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 --- README.md | 1 + docs/guides/gha_basic.md | 21 ++++++++++ noxfile.py | 1 + pyproject.toml | 1 + src/sp_repo_review/checks/github.py | 39 +++++++++++++++++++ tests/test_github.py | 39 +++++++++++++++++++ .../.github/workflows/ci.yml | 12 +++++- ...ter.__type!='compiled' %}cd.yml{% endif %} | 5 +++ ...ter.__type=='compiled' %}cd.yml{% endif %} | 11 ++++++ ...r.__ci == 'github' %}zizmor.yml{% endif %} | 8 ++++ .../.pre-commit-config.yaml | 8 ++++ 11 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 {{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} diff --git a/README.md b/README.md index 944a7790..45fb35d0 100644 --- a/README.md +++ b/README.md @@ -355,6 +355,7 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`GH103`](https://learn.scientific-python.org/development/guides/gha-basic#GH103): At least one workflow with manual dispatch trigger - [`GH104`](https://learn.scientific-python.org/development/guides/gha-wheels#GH104): Use unique names for upload-artifact - [`GH105`](https://learn.scientific-python.org/development/guides/gha-basic#GH105): Use Trusted Publishing instead of token-based publishing on PyPI +- [`GH106`](https://learn.scientific-python.org/development/guides/gha-basic#GH106): Use zizmor to check the GitHub Actions - [`GH200`](https://learn.scientific-python.org/development/guides/gha-basic#GH200): Maintained by Dependabot - [`GH210`](https://learn.scientific-python.org/development/guides/gha-basic#GH210): Maintains the GitHub action versions with Dependabot - [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions diff --git a/docs/guides/gha_basic.md b/docs/guides/gha_basic.md index f1f0f8af..49bc4477 100644 --- a/docs/guides/gha_basic.md +++ b/docs/guides/gha_basic.md @@ -90,6 +90,27 @@ run a manual check, like check-manifest, then you can keep it but just use this one check. You can also use `needs: lint` in your other jobs to keep them from running if the lint check does not pass. +## Linting your workflows + +{rr}`GH106` GitHub Actions workflows are a common source of security issues, +such as script injection from untrusted input, overly broad token permissions, +and credentials accidentally persisted by `actions/checkout`. +[zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your +workflows for these problems. The easiest way to run it is as a pre-commit hook: + +```yaml +- repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: "v1.25.2" + hooks: + - id: zizmor +``` + +You can silence individual findings with `# zizmor: ignore[rule]` comments, or +collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config +file. If you'd rather keep it out of pre-commit, zizmor also ships the +[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) +GitHub Action, which can upload results to GitHub's code scanning dashboard. + ## Unit tests Implementing unit tests is also easy. Since you should be following best diff --git a/noxfile.py b/noxfile.py index 0e100000..11767467 100755 --- a/noxfile.py +++ b/noxfile.py @@ -426,6 +426,7 @@ def pc_bump(session: nox.Session) -> None: versions = {} pages = [ Path("docs/guides/style.md"), + Path("docs/guides/gha_basic.md"), Path("{{cookiecutter.project_name}}/.pre-commit-config.yaml"), Path(".pre-commit-config.yaml"), ] diff --git a/pyproject.toml b/pyproject.toml index 9fd7904a..320ea26c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -204,6 +204,7 @@ ignore = [ [tool.repo-review.ignore] RTD103 = "Using Ruby instead of Python for docs" +GH106 = "zizmor adoption for this repo's own workflows is tracked separately" [tool.typos.default.extend-words] nd = "nd" diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index 1547b238..8f893034 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -192,6 +192,45 @@ def check(workflows: dict[str, Any]) -> str: return "\n".join(errors) +class GH106(GitHub): + "Use zizmor to check the GitHub Actions" + + requires = {"GH100"} + url = mk_url("gha-basic") + + @staticmethod + def check(precommit: dict[str, Any], workflows: dict[str, Any]) -> bool: + """ + Projects with GitHub Actions should statically analyze their workflows + with [zizmor](https://docs.zizmor.sh), which catches common security + issues such as template injection, excessive permissions, and + credential persistence. The simplest way is to add the pre-commit hook: + + ```yaml + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.25.2 + hooks: + - id: zizmor + ``` + + You can also run it as the `zizmorcore/zizmor-action` GitHub Action. + """ + for repo_item in precommit.get("repos", []): + if ( + repo_item.get("repo", "").lower() + == "https://github.com/zizmorcore/zizmor-pre-commit" + ): + return True + for workflow in workflows.values(): + for job in workflow.get("jobs", {}).values(): + if not isinstance(job, dict): + continue + for step in job.get("steps", []): + if step.get("uses", "").startswith("zizmorcore/zizmor-action"): + return True + return False + + class GH200(GitHub): "Maintained by Dependabot" diff --git a/tests/test_github.py b/tests/test_github.py index 99e3a347..6dcdabf8 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -180,6 +180,45 @@ def test_gh105_token_based_upload() -> None: assert "Token-based publishing" in res.err_msg +def test_gh106_precommit() -> None: + precommit = yaml.safe_load( + """ + repos: + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.22.0 + hooks: + - id: zizmor + """ + ) + assert compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result + + +def test_gh106_action() -> None: + workflows = yaml.safe_load( + """ + zizmor: + jobs: + zizmor: + steps: + - uses: zizmorcore/zizmor-action@v0.5.6 + """ + ) + assert compute_check("GH106", precommit={}, workflows=workflows).result + + +def test_gh106_missing() -> None: + precommit = yaml.safe_load( + """ + repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.16 + hooks: + - id: ruff-check + """ + ) + assert not compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result + + def test_gh200() -> None: dependabot = yaml.safe_load( """ diff --git a/{{cookiecutter.project_name}}/.github/workflows/ci.yml b/{{cookiecutter.project_name}}/.github/workflows/ci.yml index 59e969f7..32dc428d 100644 --- a/{{cookiecutter.project_name}}/.github/workflows/ci.yml +++ b/{{cookiecutter.project_name}}/.github/workflows/ci.yml @@ -7,6 +7,8 @@ on: branches: - main +permissions: {} + concurrency: group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} cancel-in-progress: true @@ -21,10 +23,13 @@ jobs: lint: name: Format runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v7 - {%- if cookiecutter.vcs %} with: + persist-credentials: false + {%- if cookiecutter.vcs %} fetch-depth: 0 {%- endif %} @@ -45,6 +50,8 @@ jobs: {%- if cookiecutter.__type == "compiled" %} needs: [lint] {%- endif %} + permissions: + contents: read strategy: fail-fast: false matrix: @@ -57,8 +64,9 @@ jobs: steps: - uses: actions/checkout@v7 - {%- if cookiecutter.vcs %} with: + persist-credentials: false + {%- if cookiecutter.vcs %} fetch-depth: 0 {%- endif %} diff --git a/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type!='compiled' %}cd.yml{% endif %} b/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type!='compiled' %}cd.yml{% endif %} index 20766cb3..9d33754e 100644 --- a/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type!='compiled' %}cd.yml{% endif %} +++ b/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type!='compiled' %}cd.yml{% endif %} @@ -10,6 +10,8 @@ on: types: - published +permissions: {} + concurrency: group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} cancel-in-progress: true @@ -23,10 +25,13 @@ jobs: dist: name: Distribution build runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v7 with: + persist-credentials: false fetch-depth: 0 - uses: hynek/build-and-inspect-python-package@v2 diff --git a/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type=='compiled' %}cd.yml{% endif %} b/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type=='compiled' %}cd.yml{% endif %} index 83b3f783..1385ce05 100644 --- a/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type=='compiled' %}cd.yml{% endif %} +++ b/{{cookiecutter.project_name}}/.github/workflows/{% if cookiecutter.__type=='compiled' %}cd.yml{% endif %} @@ -9,6 +9,8 @@ on: paths: - .github/workflows/cd.yml +permissions: {} + concurrency: group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %} cancel-in-progress: true @@ -22,9 +24,12 @@ jobs: make_sdist: name: Make SDist runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: actions/checkout@v7 with: + persist-credentials: false fetch-depth: 0 - name: Build SDist @@ -38,6 +43,8 @@ jobs: build_wheels: name: {% raw %}Wheel on ${{ matrix.os }}{% endraw %} runs-on: {% raw %}${{ matrix.os }}{% endraw %} + permissions: + contents: read strategy: fail-fast: false matrix: @@ -52,9 +59,13 @@ jobs: steps: - uses: actions/checkout@v7 with: + persist-credentials: false fetch-depth: 0 - uses: astral-sh/setup-uv@v8.2.0 + with: + # Disable caching to avoid poisoning published wheels + enable-cache: false - uses: pypa/cibuildwheel@v4.1 diff --git a/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} b/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} new file mode 100644 index 00000000..5aae9aaf --- /dev/null +++ b/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} @@ -0,0 +1,8 @@ +# Configuration for zizmor (https://docs.zizmor.sh) +rules: + unpinned-uses: + config: + # Actions are kept up to date with Dependabot, so a ref (tag) pin is + # sufficient; hash pinning is not required. + policies: + "*": ref-pin diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/{{cookiecutter.project_name}}/.pre-commit-config.yaml index 30181c5e..e567f558 100644 --- a/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -114,3 +114,11 @@ repos: - id: check-gitlab-ci {%- endif %} - id: check-readthedocs + +{%- if cookiecutter.__ci == "github" %} + + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: "v1.25.2" + hooks: + - id: zizmor +{%- endif %} From 32e1785599e3be5d345cabe6e9c0d6e558fb2f0c Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 9 Jun 2026 20:57:04 -0400 Subject: [PATCH 02/12] fix: add cooldown to template dependabot.yml for zizmor compliance Assisted-by: ClaudeCode:claude-sonnet-4-6 --- {{cookiecutter.project_name}}/.github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/{{cookiecutter.project_name}}/.github/dependabot.yml b/{{cookiecutter.project_name}}/.github/dependabot.yml index 6c4b3695..01703d62 100644 --- a/{{cookiecutter.project_name}}/.github/dependabot.yml +++ b/{{cookiecutter.project_name}}/.github/dependabot.yml @@ -5,6 +5,8 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 groups: actions: patterns: From 4e7a7eae656c89a81c4ce6860778c76dee8358c1 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 10 Jun 2026 00:00:13 -0400 Subject: [PATCH 03/12] chore: now we pass zizmor Signed-off-by: Henry Schreiner --- docs/guides/gha_basic.md | 2 +- pyproject.toml | 1 - src/sp_repo_review/checks/github.py | 2 +- ...{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} | 3 +-- {{cookiecutter.project_name}}/.pre-commit-config.yaml | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/guides/gha_basic.md b/docs/guides/gha_basic.md index 49bc4477..4c072749 100644 --- a/docs/guides/gha_basic.md +++ b/docs/guides/gha_basic.md @@ -100,7 +100,7 @@ workflows for these problems. The easiest way to run it is as a pre-commit hook: ```yaml - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: "v1.25.2" + rev: "v1.26.1" hooks: - id: zizmor ``` diff --git a/pyproject.toml b/pyproject.toml index 320ea26c..9fd7904a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -204,7 +204,6 @@ ignore = [ [tool.repo-review.ignore] RTD103 = "Using Ruby instead of Python for docs" -GH106 = "zizmor adoption for this repo's own workflows is tracked separately" [tool.typos.default.extend-words] nd = "nd" diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index 8f893034..3f80d4e3 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -208,7 +208,7 @@ def check(precommit: dict[str, Any], workflows: dict[str, Any]) -> bool: ```yaml - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.25.2 + rev: v1.26.1 hooks: - id: zizmor ``` diff --git a/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} b/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} index 5aae9aaf..ab94a2af 100644 --- a/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} +++ b/{{cookiecutter.project_name}}/.github/{% if cookiecutter.__ci == 'github' %}zizmor.yml{% endif %} @@ -2,7 +2,6 @@ rules: unpinned-uses: config: - # Actions are kept up to date with Dependabot, so a ref (tag) pin is - # sufficient; hash pinning is not required. + # Feel free to switch to hash pinning, then this can be removed. policies: "*": ref-pin diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/{{cookiecutter.project_name}}/.pre-commit-config.yaml index e567f558..e729bbf3 100644 --- a/{{cookiecutter.project_name}}/.pre-commit-config.yaml +++ b/{{cookiecutter.project_name}}/.pre-commit-config.yaml @@ -118,7 +118,7 @@ repos: {%- if cookiecutter.__ci == "github" %} - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: "v1.25.2" + rev: "v1.26.1" hooks: - id: zizmor {%- endif %} From 22685f3ed955c721e7fb020f9948f49422d5cbfe Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 23 Jun 2026 14:16:22 -0400 Subject: [PATCH 04/12] refactor: move zizmor check to a new Security family and page Begin implementing the Security page (#808). The zizmor check moves out of the GitHub Actions family into a dedicated Security family (GH106 -> SEC001), and the "Linting your workflows" guide section moves into a new docs/guides/security.md page, which is intended to grow with more security topics (pre-commit hash pinning, lock files, cooldowns, pip/uv audit, SBOMs). Assisted-by: ClaudeCode:claude-opus-4.8 --- README.md | 5 ++- docs/guides/gha_basic.md | 21 ---------- docs/guides/index.md | 4 +- docs/guides/security.md | 30 +++++++++++++++ docs/myst.yml | 1 + noxfile.py | 2 +- pyproject.toml | 1 + src/sp_repo_review/checks/github.py | 39 ------------------- src/sp_repo_review/checks/security.py | 55 +++++++++++++++++++++++++++ src/sp_repo_review/families.py | 3 ++ tests/test_github.py | 39 ------------------- tests/test_security.py | 43 +++++++++++++++++++++ 12 files changed, 141 insertions(+), 102 deletions(-) create mode 100644 docs/guides/security.md create mode 100644 src/sp_repo_review/checks/security.py create mode 100644 tests/test_security.py diff --git a/README.md b/README.md index 45fb35d0..854cf543 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,6 @@ for family, grp in itertools.groupby(collected.checks.items(), key=lambda x: x[1 - [`GH103`](https://learn.scientific-python.org/development/guides/gha-basic#GH103): At least one workflow with manual dispatch trigger - [`GH104`](https://learn.scientific-python.org/development/guides/gha-wheels#GH104): Use unique names for upload-artifact - [`GH105`](https://learn.scientific-python.org/development/guides/gha-basic#GH105): Use Trusted Publishing instead of token-based publishing on PyPI -- [`GH106`](https://learn.scientific-python.org/development/guides/gha-basic#GH106): Use zizmor to check the GitHub Actions - [`GH200`](https://learn.scientific-python.org/development/guides/gha-basic#GH200): Maintained by Dependabot - [`GH210`](https://learn.scientific-python.org/development/guides/gha-basic#GH210): Maintains the GitHub action versions with Dependabot - [`GH211`](https://learn.scientific-python.org/development/guides/gha-basic#GH211): Do not pin core actions as major versions @@ -421,6 +420,10 @@ Will not show up if no `.readthedocs.yml`/`.readthedocs.yaml` file is present. - `RF201`: Avoid using deprecated config settings - `RF202`: Use (new) lint config section +### Security + +- [`SEC001`](https://learn.scientific-python.org/development/guides/security#SEC001): Use zizmor to check the GitHub Actions + ### Setuptools Config Will not show up if no `setup.cfg` file is present. diff --git a/docs/guides/gha_basic.md b/docs/guides/gha_basic.md index 4c072749..f1f0f8af 100644 --- a/docs/guides/gha_basic.md +++ b/docs/guides/gha_basic.md @@ -90,27 +90,6 @@ run a manual check, like check-manifest, then you can keep it but just use this one check. You can also use `needs: lint` in your other jobs to keep them from running if the lint check does not pass. -## Linting your workflows - -{rr}`GH106` GitHub Actions workflows are a common source of security issues, -such as script injection from untrusted input, overly broad token permissions, -and credentials accidentally persisted by `actions/checkout`. -[zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your -workflows for these problems. The easiest way to run it is as a pre-commit hook: - -```yaml -- repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: "v1.26.1" - hooks: - - id: zizmor -``` - -You can silence individual findings with `# zizmor: ignore[rule]` comments, or -collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config -file. If you'd rather keep it out of pre-commit, zizmor also ships the -[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) -GitHub Action, which can upload results to GitHub's code scanning dashboard. - ## Unit tests Implementing unit tests is also easy. Since you should be following best diff --git a/docs/guides/index.md b/docs/guides/index.md index c3ecc900..f49187ec 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -18,7 +18,8 @@ A section on CI follows, with a [general setup guide][gha_basic], and then two choices for using CI to distribute your package, one for [pure Python][gha_pure], and one for [compiled extensions][gha_wheels]. You can read about setting up good tests on the [pytest page][pytest], with -[coverage][]. There's also a page on setting up [docs][], as well. +[coverage][]. There's also a page on setting up [docs][], as well as a page on +[security][] best practices. :::{tip} New project template Once you have completed the guidelines, there is a @@ -45,6 +46,7 @@ WebAssembly! All checks point to a linked badge in the guide. [gha_basic]: guides/gha-basic [gha_pure]: guides/gha-pure [gha_wheels]: guides/gha-wheels +[security]: guides/security [pytest]: guides/pytest [right in the guide]: guides/repo-review diff --git a/docs/guides/security.md b/docs/guides/security.md new file mode 100644 index 00000000..10eee58b --- /dev/null +++ b/docs/guides/security.md @@ -0,0 +1,30 @@ +--- +short_title: Security +--- + +# Security + +Supply-chain and CI security are increasingly important for scientific Python +projects. This page collects recommendations for keeping your repository and its +automation secure. It is a work in progress; expect it to grow over time. + +## GitHub Actions + +{rr}`SEC001` GitHub Actions workflows are a common source of security issues, +such as script injection from untrusted input, overly broad token permissions, +and credentials accidentally persisted by `actions/checkout`. +[zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your +workflows for these problems. The easiest way to run it is as a pre-commit hook: + +```yaml +- repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: "v1.26.1" + hooks: + - id: zizmor +``` + +You can silence individual findings with `# zizmor: ignore[rule]` comments, or +collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config +file. If you'd rather keep it out of pre-commit, zizmor also ships the +[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) +GitHub Action, which can upload results to GitHub's code scanning dashboard. diff --git a/docs/myst.yml b/docs/myst.yml index 0a470973..ed00f642 100644 --- a/docs/myst.yml +++ b/docs/myst.yml @@ -30,6 +30,7 @@ project: - file: guides/gha_basic.md - file: guides/gha_pure.md - file: guides/gha_wheels.md + - file: guides/security.md - file: guides/tasks.md - file: principles/index.md children: diff --git a/noxfile.py b/noxfile.py index 11767467..34e4f958 100755 --- a/noxfile.py +++ b/noxfile.py @@ -426,7 +426,7 @@ def pc_bump(session: nox.Session) -> None: versions = {} pages = [ Path("docs/guides/style.md"), - Path("docs/guides/gha_basic.md"), + Path("docs/guides/security.md"), Path("{{cookiecutter.project_name}}/.pre-commit-config.yaml"), Path(".pre-commit-config.yaml"), ] diff --git a/pyproject.toml b/pyproject.toml index 9fd7904a..99c9ec74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,6 +68,7 @@ precommit = "sp_repo_review.checks.precommit:repo_review_checks" ruff = "sp_repo_review.checks.ruff:repo_review_checks" mypy = "sp_repo_review.checks.mypy:repo_review_checks" github = "sp_repo_review.checks.github:repo_review_checks" +security = "sp_repo_review.checks.security:repo_review_checks" readthedocs = "sp_repo_review.checks.readthedocs:repo_review_checks" setupcfg = "sp_repo_review.checks.setupcfg:repo_review_checks" noxfile = "sp_repo_review.checks.noxfile:repo_review_checks" diff --git a/src/sp_repo_review/checks/github.py b/src/sp_repo_review/checks/github.py index 3f80d4e3..1547b238 100644 --- a/src/sp_repo_review/checks/github.py +++ b/src/sp_repo_review/checks/github.py @@ -192,45 +192,6 @@ def check(workflows: dict[str, Any]) -> str: return "\n".join(errors) -class GH106(GitHub): - "Use zizmor to check the GitHub Actions" - - requires = {"GH100"} - url = mk_url("gha-basic") - - @staticmethod - def check(precommit: dict[str, Any], workflows: dict[str, Any]) -> bool: - """ - Projects with GitHub Actions should statically analyze their workflows - with [zizmor](https://docs.zizmor.sh), which catches common security - issues such as template injection, excessive permissions, and - credential persistence. The simplest way is to add the pre-commit hook: - - ```yaml - - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.26.1 - hooks: - - id: zizmor - ``` - - You can also run it as the `zizmorcore/zizmor-action` GitHub Action. - """ - for repo_item in precommit.get("repos", []): - if ( - repo_item.get("repo", "").lower() - == "https://github.com/zizmorcore/zizmor-pre-commit" - ): - return True - for workflow in workflows.values(): - for job in workflow.get("jobs", {}).values(): - if not isinstance(job, dict): - continue - for step in job.get("steps", []): - if step.get("uses", "").startswith("zizmorcore/zizmor-action"): - return True - return False - - class GH200(GitHub): "Maintained by Dependabot" diff --git a/src/sp_repo_review/checks/security.py b/src/sp_repo_review/checks/security.py new file mode 100644 index 00000000..b11de41c --- /dev/null +++ b/src/sp_repo_review/checks/security.py @@ -0,0 +1,55 @@ +# SEC: Security +## SEC0xx: GitHub Actions security + +from __future__ import annotations + +from typing import Any + +from . import mk_url + + +class Security: + family = "security" + + +class SEC001(Security): + "Use zizmor to check the GitHub Actions" + + requires = {"GH100"} + url = mk_url("security") + + @staticmethod + def check(precommit: dict[str, Any], workflows: dict[str, Any]) -> bool: + """ + Projects with GitHub Actions should statically analyze their workflows + with [zizmor](https://docs.zizmor.sh), which catches common security + issues such as template injection, excessive permissions, and + credential persistence. The simplest way is to add the pre-commit hook: + + ```yaml + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.26.1 + hooks: + - id: zizmor + ``` + + You can also run it as the `zizmorcore/zizmor-action` GitHub Action. + """ + for repo_item in precommit.get("repos", []): + if ( + repo_item.get("repo", "").lower() + == "https://github.com/zizmorcore/zizmor-pre-commit" + ): + return True + for workflow in workflows.values(): + for job in workflow.get("jobs", {}).values(): + if not isinstance(job, dict): + continue + for step in job.get("steps", []): + if step.get("uses", "").startswith("zizmorcore/zizmor-action"): + return True + return False + + +def repo_review_checks() -> dict[str, Security]: + return {p.__name__: p() for p in Security.__subclasses__()} diff --git a/src/sp_repo_review/families.py b/src/sp_repo_review/families.py index b5297cbe..2dbbf5c1 100644 --- a/src/sp_repo_review/families.py +++ b/src/sp_repo_review/families.py @@ -114,6 +114,9 @@ def get_families( "github": Family( name="GitHub Actions", ), + "security": Family( + name="Security", + ), "pre-commit": Family( name="Pre-commit", readme_note="Will not show up if using lefthook instead of pre-commit/prek.", diff --git a/tests/test_github.py b/tests/test_github.py index 6dcdabf8..99e3a347 100644 --- a/tests/test_github.py +++ b/tests/test_github.py @@ -180,45 +180,6 @@ def test_gh105_token_based_upload() -> None: assert "Token-based publishing" in res.err_msg -def test_gh106_precommit() -> None: - precommit = yaml.safe_load( - """ - repos: - - repo: https://github.com/zizmorcore/zizmor-pre-commit - rev: v1.22.0 - hooks: - - id: zizmor - """ - ) - assert compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result - - -def test_gh106_action() -> None: - workflows = yaml.safe_load( - """ - zizmor: - jobs: - zizmor: - steps: - - uses: zizmorcore/zizmor-action@v0.5.6 - """ - ) - assert compute_check("GH106", precommit={}, workflows=workflows).result - - -def test_gh106_missing() -> None: - precommit = yaml.safe_load( - """ - repos: - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.16 - hooks: - - id: ruff-check - """ - ) - assert not compute_check("GH106", precommit=precommit, workflows={"ci": {}}).result - - def test_gh200() -> None: dependabot = yaml.safe_load( """ diff --git a/tests/test_security.py b/tests/test_security.py new file mode 100644 index 00000000..c38caa25 --- /dev/null +++ b/tests/test_security.py @@ -0,0 +1,43 @@ +import yaml +from repo_review.testing import compute_check + + +def test_sec001_precommit() -> None: + precommit = yaml.safe_load( + """ + repos: + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: v1.22.0 + hooks: + - id: zizmor + """ + ) + assert compute_check("SEC001", precommit=precommit, workflows={"ci": {}}).result + + +def test_sec001_action() -> None: + workflows = yaml.safe_load( + """ + zizmor: + jobs: + zizmor: + steps: + - uses: zizmorcore/zizmor-action@v0.5.6 + """ + ) + assert compute_check("SEC001", precommit={}, workflows=workflows).result + + +def test_sec001_missing() -> None: + precommit = yaml.safe_load( + """ + repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.16 + hooks: + - id: ruff-check + """ + ) + assert not compute_check( + "SEC001", precommit=precommit, workflows={"ci": {}} + ).result From 50f7c608d499d146bbfa951dd1e76f73a50a3a58 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:16:44 +0000 Subject: [PATCH 05/12] style: pre-commit fixes --- tests/test_security.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_security.py b/tests/test_security.py index c38caa25..63f5bc98 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -38,6 +38,4 @@ def test_sec001_missing() -> None: - id: ruff-check """ ) - assert not compute_check( - "SEC001", precommit=precommit, workflows={"ci": {}} - ).result + assert not compute_check("SEC001", precommit=precommit, workflows={"ci": {}}).result From 727eb89182acdedbeb1b55e2514a0f65ab7962b6 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 23 Jun 2026 14:51:13 -0400 Subject: [PATCH 06/12] docs: update text a bit Signed-off-by: Henry Schreiner --- docs/guides/security.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index 10eee58b..7f8e9a3e 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -5,8 +5,10 @@ short_title: Security # Security Supply-chain and CI security are increasingly important for scientific Python -projects. This page collects recommendations for keeping your repository and its -automation secure. It is a work in progress; expect it to grow over time. +projects; new attacks are targetting smaller packages than ever before thanks +to the ease with which exploits can be found and utalized with AI. This page +collects recommendations for keeping your repository and its automation secure. +This is a work in progress; expect it to grow over time. ## GitHub Actions From 3de169d39d4ac1f9fbd52c2e699dac02336f0a26 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:59:43 +0000 Subject: [PATCH 07/12] style: pre-commit fixes --- docs/guides/security.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index 7f8e9a3e..fd769a8f 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -5,8 +5,8 @@ short_title: Security # Security Supply-chain and CI security are increasingly important for scientific Python -projects; new attacks are targetting smaller packages than ever before thanks -to the ease with which exploits can be found and utalized with AI. This page +projects; new attacks are targeting smaller packages than ever before thanks +to the ease with which exploits can be found and utilized with AI. This page collects recommendations for keeping your repository and its automation secure. This is a work in progress; expect it to grow over time. From 0313ccdc2b3255e6057141af524459f4f95773de Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Tue, 23 Jun 2026 15:34:43 -0400 Subject: [PATCH 08/12] docs: show zizmor GitHub Action as a full workflow example Expand the zizmor-action mention into a complete workflow block, matching the pre-commit example on the security page. Assisted-by: ClaudeCode:claude-opus-4.8 --- docs/guides/security.md | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index fd769a8f..56ea890f 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -25,8 +25,34 @@ workflows for these problems. The easiest way to run it is as a pre-commit hook: - id: zizmor ``` +If you'd rather keep it out of pre-commit, zizmor also ships the +[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) +GitHub Action, which can upload its findings to GitHub's code scanning +dashboard: + +```yaml +name: zizmor + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - uses: zizmorcore/zizmor-action@v0.5.7 +``` + You can silence individual findings with `# zizmor: ignore[rule]` comments, or collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config -file. If you'd rather keep it out of pre-commit, zizmor also ships the -[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) -GitHub Action, which can upload results to GitHub's code scanning dashboard. +file. From 8b26517261355d177f5eba7661d07852774a4c74 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 24 Jun 2026 10:26:06 -0400 Subject: [PATCH 09/12] docs: rewrite the security page Signed-off-by: Henry Schreiner --- docs/guides/security.md | 50 +++++++++++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index 56ea890f..e71989b7 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -5,18 +5,54 @@ short_title: Security # Security Supply-chain and CI security are increasingly important for scientific Python -projects; new attacks are targeting smaller packages than ever before thanks -to the ease with which exploits can be found and utilized with AI. This page -collects recommendations for keeping your repository and its automation secure. -This is a work in progress; expect it to grow over time. +projects; new attacks are targeting smaller packages than ever before thanks to +the ease with which exploits can be found and utilized with AI. The first six +months of 2026 had 4.5x the malitoius package volume of _all_ of 2025[^1]. + +[^1]: + +Most of these attacks strung together smaller vulerabilties into something +exploitable, often in CI like GitHub Actions. Once in, the attacks upload +malitious packages that spread the attack via PyPI or NPM. + +This page has recommendations for keeping your repository and its automation +secure. This will never be complete, but even a few small steps can make your +code much more secure. ## GitHub Actions {rr}`SEC001` GitHub Actions workflows are a common source of security issues, -such as script injection from untrusted input, overly broad token permissions, -and credentials accidentally persisted by `actions/checkout`. +due to how commonly it is used, and it's original design being focused on ease +of use and convenience. + +Common security problems: + +* Action moving references, like `@v1`, or tags, like `@v1.0.1`, can be pushed + if an attacker comprimizes the action repository you are using. If you use + full 40 character SHA's, these cannot be modified. (Official actions are + likely okay, but important for third party actions). There's even a GitHub + setting to require this. +* Action SHA references can be added by a fork. If you make a fork of + `actions/checkout`, you can reference _your_ SHA via + `actions/checkout@`. Only accept SHAs you have verified or a tool (like + dependabot) produce. If you use Zizmor, it can also verify that an SHA matches + a tag, tags cannot be pulled from a fork. +* Caching is dangerous. Attackers can poison an unrelated cache. Avoid caching + in your release jobs. +* `pull_request_target` is really dangerous. Attackers can use it to poison + caches, for example. +* Tighten default permissions. A job should not have permissions to do anything + it doesn't need. Set the default in settings to read-only, then explicitly + grant required permissions. +* Don't build code in your release job. The release job should do _as little as + possible_. +* Use trusted publishing. There's no long-lived token to steal. + +### Zizmor + [zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your -workflows for these problems. The easiest way to run it is as a pre-commit hook: +workflows for common problems, including many of the ones above. You can run it +is as a pre-commit hook: ```yaml - repo: https://github.com/zizmorcore/zizmor-pre-commit From 30bc96c423b16dccea3e00de33e7da00d680d7c4 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 24 Jun 2026 10:38:55 -0400 Subject: [PATCH 10/12] docs: use tabs for pre-commit vs GHA zizmor options Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/guides/security.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index e71989b7..52b57028 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -51,8 +51,10 @@ Common security problems: ### Zizmor [zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your -workflows for common problems, including many of the ones above. You can run it -is as a pre-commit hook: +workflows for common problems, including many of the ones above. You can run it as a pre-commit hook or as a GitHub Action: + +::::{tab-set} +:::{tab-item} pre-commit ```yaml - repo: https://github.com/zizmorcore/zizmor-pre-commit @@ -61,10 +63,11 @@ is as a pre-commit hook: - id: zizmor ``` -If you'd rather keep it out of pre-commit, zizmor also ships the -[`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) -GitHub Action, which can upload its findings to GitHub's code scanning -dashboard: +::: +:::{tab-item} GitHub Actions + +The [`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) +GitHub Action can upload its findings to GitHub's code scanning dashboard: ```yaml name: zizmor @@ -89,6 +92,9 @@ jobs: - uses: zizmorcore/zizmor-action@v0.5.7 ``` +::: +:::: + You can silence individual findings with `# zizmor: ignore[rule]` comments, or collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config file. From aba7c8925f19f3b0335328421b15551e0d8462ef Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 24 Jun 2026 11:26:11 -0400 Subject: [PATCH 11/12] style: fix line length in security.md Assisted-by: ClaudeCode:claude-sonnet-4-6 --- docs/guides/security.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index 52b57028..b90235c6 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -51,7 +51,8 @@ Common security problems: ### Zizmor [zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your -workflows for common problems, including many of the ones above. You can run it as a pre-commit hook or as a GitHub Action: +workflows for common problems, including many of the ones above. You can run +it as a pre-commit hook or as a GitHub Action: ::::{tab-set} :::{tab-item} pre-commit From 28ffa8df92a6e397971ad766d7b6d59addce9588 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 24 Jun 2026 14:18:47 -0400 Subject: [PATCH 12/12] docs: minor touchup Signed-off-by: Henry Schreiner --- docs/guides/security.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/guides/security.md b/docs/guides/security.md index b90235c6..d51e7eff 100644 --- a/docs/guides/security.md +++ b/docs/guides/security.md @@ -31,7 +31,8 @@ Common security problems: if an attacker comprimizes the action repository you are using. If you use full 40 character SHA's, these cannot be modified. (Official actions are likely okay, but important for third party actions). There's even a GitHub - setting to require this. + setting to require this. It's conventional to include the tag as a trailing + comment. * Action SHA references can be added by a fork. If you make a fork of `actions/checkout`, you can reference _your_ SHA via `actions/checkout@`. Only accept SHAs you have verified or a tool (like @@ -48,6 +49,14 @@ Common security problems: possible_. * Use trusted publishing. There's no long-lived token to steal. +:::{note} + +This guide and cookiecutter does _not_ use SHA pinning to make it easier to +read and maintain. You can convert to SHA with tools like +[`npx actions-up`](https://github.com/azat-io/actions-up). + +::: + ### Zizmor [zizmor](https://docs.zizmor.sh) is a static analysis tool that audits your @@ -67,9 +76,6 @@ it as a pre-commit hook or as a GitHub Action: ::: :::{tab-item} GitHub Actions -The [`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) -GitHub Action can upload its findings to GitHub's code scanning dashboard: - ```yaml name: zizmor @@ -93,6 +99,9 @@ jobs: - uses: zizmorcore/zizmor-action@v0.5.7 ``` +The [`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action) +GitHub Action can upload its findings to GitHub's code scanning dashboard. + ::: ::::