Skip to content
Open
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
58 changes: 58 additions & 0 deletions docs/guides/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
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.

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

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.
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Configuration for zizmor (https://docs.zizmor.sh)
rules:
unpinned-uses:
config:
# Feel free to switch to hash pinning, then this can be removed.
policies:
"*": ref-pin
8 changes: 8 additions & 0 deletions {{cookiecutter.project_name}}/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.26.1"
hooks:
- id: zizmor
{%- endif %}
Loading