diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..b380328 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: CodeQL + +# Static analysis for this repository. Generated projects get an equivalent +# workflow of their own; this is the template repository applying its own +# advice to itself. +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "29 10 * * 0" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze Python + runs-on: ubuntu-latest + permissions: + security-events: write + packages: read + actions: read + contents: read + steps: + - uses: actions/checkout@v7.0.1 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: python + build-mode: none + queries: security-and-quality + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:python" diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..811bced --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,43 @@ +# Security Policy + +## Scope + +This repository is a project template. It ships GitHub Actions workflows, +dependency declarations and configuration that are copied into other people's +projects, so a problem here propagates to everything generated from it. + +Reports about the template itself are in scope: a workflow that leaks +credentials, an over-permissioned token, a dependency pinned to something +malicious, or a generated default that is unsafe. + +## Supported versions + +The latest tag receives fixes. Older tags do not. + +## Reporting a vulnerability + +Please **do not** open a public issue. + +Report privately through +[GitHub Security Advisories](https://github.com/leynier/python-template/security/advisories/new), +or by email to . + +Please include: + +- What the problem is and what an attacker could do with it. +- Which file or generated output is affected. +- Steps to reproduce, ideally the `copier` answers that produce the bad output. + +You can expect an initial response within 7 days. + +## Security posture of this repository + +- GitHub Actions are pinned to release tags, and + [zizmor](https://github.com/zizmorcore/zizmor) audits every workflow on + change. The pinning policy is declared in `zizmor.yml`. +- Workflows declare least-privilege `permissions:`. +- Dependabot has a cooldown before adopting newly published releases. +- CodeQL and OpenSSF Scorecard run against this repository. + +Generated projects get the same treatment, plus PyPI Trusted Publishing with +Sigstore attestations instead of long-lived API tokens. diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja index fe3057e..32c1309 100644 --- a/template/pyproject.toml.jinja +++ b/template/pyproject.toml.jinja @@ -46,7 +46,7 @@ Documentation = "https://{{ github_username }}.github.io/{{ github_repo }}" {% endif -%} [build-system] -requires = ["uv_build>=0.12,<0.13"] +requires = ["uv_build>=0.12.1,<0.13"] build-backend = "uv_build" [tool.uv.build-backend] diff --git a/tests/test_repo_practices_what_it_preaches.py b/tests/test_repo_practices_what_it_preaches.py new file mode 100644 index 0000000..08f16f9 --- /dev/null +++ b/tests/test_repo_practices_what_it_preaches.py @@ -0,0 +1,45 @@ +"""The template repository must apply its own advice to itself. + +OpenSSF Scorecard flagged this repository for a missing security policy and for +having no static analysis, while the template happily handed both to every +project it generated. These tests stop that drift from coming back. +""" + +import pathlib + +import pytest + +REPO = pathlib.Path(__file__).resolve().parent.parent + + +@pytest.mark.parametrize( + "path", + [ + "SECURITY.md", + "changelog.md", + "readme.md", + "LICENSE", + "zizmor.yml", + ], +) +def test_repository_has_the_files_it_generates(path: str) -> None: + assert (REPO / path).is_file(), f"{path} is missing from the template repository" + + +@pytest.mark.parametrize("workflow", ["ci.yml", "codeql.yml", "scorecard.yml"]) +def test_repository_runs_the_workflows_it_generates(workflow: str) -> None: + assert (REPO / ".github" / "workflows" / workflow).is_file() + + +def test_required_status_check_exists() -> None: + """Branch protection requires a check named "CI"; something must produce it. + + Renaming or dropping this job would block every future merge, which is + exactly what happened with the job name inherited from 2021. + """ + import yaml + + ci = yaml.safe_load((REPO / ".github" / "workflows" / "ci.yml").read_text()) + names = {job.get("name") for job in ci["jobs"].values()} + + assert "CI" in names, f"no job produces the required 'CI' check; found {names}"