Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,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.
Expand Down
4 changes: 3 additions & 1 deletion docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
110 changes: 110 additions & 0 deletions docs/guides/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
---
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. The first six
months of 2026 had 4.5x the malitoius package volume of _all_ of 2025[^1].

[^1]: <https://phoenix.security/accelerating-supply-chain-attacks-npm-pypi-vsx-ai-enabled-2026/>

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,
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. 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@<SHA>`. 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.

:::{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
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
rev: "v1.26.1"
hooks:
- id: zizmor
```

:::
:::{tab-item} GitHub Actions

```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
```

The [`zizmorcore/zizmor-action`](https://github.com/zizmorcore/zizmor-action)
GitHub Action can upload its findings to GitHub's code scanning dashboard.

:::
::::

You can silence individual findings with `# zizmor: ignore[rule]` comments, or
collect them in a [`zizmor.yml`](https://docs.zizmor.sh/configuration/) config
file.
1 change: 1 addition & 0 deletions docs/myst.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def pc_bump(session: nox.Session) -> None:
versions = {}
pages = [
Path("docs/guides/style.md"),
Path("docs/guides/security.md"),
Path("{{cookiecutter.project_name}}/.pre-commit-config.yaml"),
Path(".pre-commit-config.yaml"),
]
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 55 additions & 0 deletions src/sp_repo_review/checks/security.py
Original file line number Diff line number Diff line change
@@ -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__()}
3 changes: 3 additions & 0 deletions src/sp_repo_review/families.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
41 changes: 41 additions & 0 deletions tests/test_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
2 changes: 2 additions & 0 deletions {{cookiecutter.project_name}}/.github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ updates:
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
actions:
patterns:
Expand Down
12 changes: 10 additions & 2 deletions {{cookiecutter.project_name}}/.github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ on:
branches:
- main

permissions: {}

concurrency:
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
cancel-in-progress: true
Expand All @@ -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 %}

Expand All @@ -45,6 +50,8 @@ jobs:
{%- if cookiecutter.__type == "compiled" %}
needs: [lint]
{%- endif %}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
Expand All @@ -57,8 +64,9 @@ jobs:

steps:
- uses: actions/checkout@v7
{%- if cookiecutter.vcs %}
with:
persist-credentials: false
{%- if cookiecutter.vcs %}
fetch-depth: 0
{%- endif %}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ on:
types:
- published

permissions: {}

concurrency:
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
cancel-in-progress: true
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
paths:
- .github/workflows/cd.yml

permissions: {}

concurrency:
group: {% raw %}${{ github.workflow }}-${{ github.ref }}{% endraw %}
cancel-in-progress: true
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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

Expand Down
Loading
Loading