diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index b3dc20b..378e312 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -27,6 +27,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -63,6 +67,10 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 @@ -123,6 +131,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Deploy to Pages id: deployment diff --git a/.github/workflows/monitor-pages.yml b/.github/workflows/monitor-pages.yml index bc59140..8006c21 100644 --- a/.github/workflows/monitor-pages.yml +++ b/.github/workflows/monitor-pages.yml @@ -22,6 +22,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # A scheduled workflow runs from the default branch. Once the default moves to # integration, checking out the default here would verify integration's schema # set against a site that publishes from main, failing every six hours between diff --git a/.github/workflows/open-promotion.yml b/.github/workflows/open-promotion.yml index a82d419..236d6ff 100644 --- a/.github/workflows/open-promotion.yml +++ b/.github/workflows/open-promotion.yml @@ -23,6 +23,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false fetch-depth: 0 - name: Open the promotion pull request if integration is ahead diff --git a/.github/workflows/pr-base-guard.yml b/.github/workflows/pr-base-guard.yml index b3107f3..9b37cb2 100644 --- a/.github/workflows/pr-base-guard.yml +++ b/.github/workflows/pr-base-guard.yml @@ -24,6 +24,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # Both endpoints of the diff have to be present to compute a merge base. fetch-depth: 0 diff --git a/.github/workflows/pr-intake.yml b/.github/workflows/pr-intake.yml index 71d2574..c21b76b 100644 --- a/.github/workflows/pr-intake.yml +++ b/.github/workflows/pr-intake.yml @@ -9,7 +9,8 @@ name: PR intake on: - pull_request_target: + # zizmor flags every pull_request_target. The paragraph above is why this one is safe. + pull_request_target: # zizmor: ignore[dangerous-triggers] types: [opened, edited, reopened] permissions: {} diff --git a/.github/workflows/reference-implementation.yml b/.github/workflows/reference-implementation.yml index 4a93bc1..424d2a9 100644 --- a/.github/workflows/reference-implementation.yml +++ b/.github/workflows/reference-implementation.yml @@ -42,13 +42,17 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false # Pinned to an exact version rather than a moving major. The bridge in # packages/agt-bridge/src/opa-path.ts works around a Bun defect measured on a # specific version, so the runtime version is part of this tree's contract # rather than an implementation detail. - name: Install Bun - uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.1 + uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # v2.0.2 with: bun-version: "1.3.11" diff --git a/.github/workflows/scope-review.yml b/.github/workflows/scope-review.yml index 047044d..a063d8a 100644 --- a/.github/workflows/scope-review.yml +++ b/.github/workflows/scope-review.yml @@ -20,6 +20,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Open a review issue if the date has passed env: diff --git a/.github/workflows/sync-integration.yml b/.github/workflows/sync-integration.yml index cb4a1f3..2ce0b06 100644 --- a/.github/workflows/sync-integration.yml +++ b/.github/workflows/sync-integration.yml @@ -26,6 +26,9 @@ jobs: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false fetch-depth: 0 - name: Open or update the sync pull request diff --git a/.github/workflows/sync_version.yml b/.github/workflows/sync_version.yml index fe29a0f..a31f170 100644 --- a/.github/workflows/sync_version.yml +++ b/.github/workflows/sync_version.yml @@ -27,6 +27,10 @@ jobs: steps: - name: Check out the repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # create-pull-request sets up its own token for the push, so nothing needs + # the checkout's token left behind in .git/config. + persist-credentials: false - name: Install uv uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 diff --git a/.github/workflows/validate-owasp-metadata.yaml b/.github/workflows/validate-owasp-metadata.yaml index 1aeef36..66dc2eb 100644 --- a/.github/workflows/validate-owasp-metadata.yaml +++ b/.github/workflows/validate-owasp-metadata.yaml @@ -21,7 +21,11 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false - name: Validate metadata file uses: owasp/nest-schema/.github/actions/validate@a733198b4a942eb12d3ee8629cd9e0d409b1b2b9 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..259aa72 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,90 @@ +# Audits every workflow, composite action, and dependabot.yml with zizmor. +# +# CodeQL's actions analysis covers the common injection paths. zizmor is the defense in +# depth the OWASP GitHub Actions Security Cheat Sheet recommends beside it, and it checks +# what CodeQL does not: hash pins no tag points to, version comments that disagree with +# their pin, App tokens broader than the workflow needs, and credentials a checkout +# leaves behind in .git/config. +# +# Findings go to the Security tab instead of failing this job. Code scanning tracks them +# across runs, and a ruleset can gate merges on them. A tool or upload failure still +# fails the job, so the scan cannot skip itself quietly. +name: zizmor + +on: + pull_request: + paths: + - ".github/**" + - "**/action.yml" + - "**/action.yaml" + - "pyproject.toml" + - "uv.lock" + push: + branches: ["main", "integration"] + paths: + - ".github/**" + - "**/action.yml" + - "**/action.yaml" + - "pyproject.toml" + - "uv.lock" + schedule: + # Weekly even when no workflow changed, because the online audits compare pinned + # actions against advisories that are published after the pin. + - cron: "0 6 * * 1" + workflow_dispatch: + +# Deny by default. The one job below grants itself only what it needs. +permissions: {} + +concurrency: + group: zizmor-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + zizmor: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # upload the SARIF report to code scanning + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # Nothing here pushes with git, so the checkout's token has no reason to + # stay behind in .git/config. + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + with: + version: "0.9.9" + + # zizmor runs from its own dependency group in uv.lock, so its version is pinned + # with hashes and --locked refuses a lockfile that drifted from pyproject.toml. + - name: Audit workflows and actions + env: + # The online audits read the public API: impostor commits, version comments, + # and known-vulnerable actions. The job's read-only token is all they use. + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + uv run --locked --only-group zizmor \ + zizmor --no-progress --no-exit-codes --format sarif . > zizmor.sarif + + - name: Upload the report to code scanning + # A pull request from a fork runs with a read-only token and cannot upload. + # Its findings print as annotations in the next step instead. + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 + with: + sarif_file: zizmor.sarif + category: zizmor + + - name: Annotate a pull request from a fork + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + uv run --locked --only-group zizmor \ + zizmor --no-progress --no-exit-codes --format github . diff --git a/pyproject.toml b/pyproject.toml index 1ad3b3f..453e9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,10 @@ dependencies = [ "mike>=1.2.0", "mkdocs-material>=9.6.14", "pymdown-extensions>= [dependency-groups] dev = [ "pytest>=8.0", "jsonschema>=4.25.0",] +# Workflow security analysis, run by .github/workflows/zizmor.yml. A named group rather +# than dev, so the docs build and the test gate never install it. The lockfile pins the +# version with hashes, and Dependabot moves it under the usual cooldown. +zizmor = [ "zizmor>=1.30.1",] [tool.pytest.ini_options] # The deploy workflow runs a bare `uv run pytest -v` as the gate the site build diff --git a/uv.lock b/uv.lock index db2ef13..8cbcf92 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,9 @@ dev = [ { name = "jsonschema" }, { name = "pytest" }, ] +zizmor = [ + { name = "zizmor" }, +] [package.metadata] requires-dist = [ @@ -30,6 +33,7 @@ dev = [ { name = "jsonschema", specifier = ">=4.25.0" }, { name = "pytest", specifier = ">=8.0" }, ] +zizmor = [{ name = "zizmor", specifier = ">=1.30.1" }] [[package]] name = "attrs" @@ -798,3 +802,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, ] + +[[package]] +name = "zizmor" +version = "1.30.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/c5/3d3dc61c0b64c082e1f704f5cf44f6a38153adbccf8787dd06a1a0ed14e0/zizmor-1.30.1.tar.gz", hash = "sha256:cbb7e5cb2fec471066d5c326bcaa69ed3eefdcf6a76d7ad38335b924acf86c07", size = 575745, upload-time = "2026-09-09T05:27:21.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a1/a877361a6c6ae13fb0ff4ed3c9d681ccddb9c838c5c3ca8f163a0fcfded9/zizmor-1.30.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17fda74e15fe41a6aa354ea4cf71fc7ff3cd025cda6584bb6d7466b3f1d00fe6", size = 9148491, upload-time = "2026-09-09T05:26:58.842Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/ea44a8f466b9a080dc0634497f6c9ee63dd586a94f5e2cd743e06c4df140/zizmor-1.30.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f9eb092f089e35fa9fb3b70aeec0a19eca7dacf2ab161f1d50b18351895f085c", size = 8743712, upload-time = "2026-09-09T05:27:01.342Z" }, + { url = "https://files.pythonhosted.org/packages/37/9c/43764a6e9eb5ad5a332202afc3e63622e3380669ecf7079b6b0de892e474/zizmor-1.30.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:92906f448365672ebb657fb50d36f997290a3aaba8cac723214741ea9054845d", size = 9009843, upload-time = "2026-09-09T05:27:03.522Z" }, + { url = "https://files.pythonhosted.org/packages/cf/2d/96c9c7b37e46e2b092e5daac3c6e9d4966ed7b58845f3f04950043d89c34/zizmor-1.30.1-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:5e8a3d54c3d9d51307371f4d1d49f8f9e56de00205c6cc660c6846a95ad4e135", size = 8620204, upload-time = "2026-09-09T05:27:06.001Z" }, + { url = "https://files.pythonhosted.org/packages/63/55/1900b53d34dcebc207cf3ceb7957b3748e8bb40958f062e3853ab72f2395/zizmor-1.30.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:eee12266b793cb87ad4a7e3af2e72404f8a63e3de5eb099b80bf7b1cfd232a8e", size = 9467893, upload-time = "2026-09-09T05:27:08.07Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/d998bbdebf1f40e0d3dd027f94bceb961065640b29bc82bf5ffa9266fdb9/zizmor-1.30.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:15679642e8c4f825ba3f22537c698b1bbbad19ead23640f62ec45ebbe7abc803", size = 9038587, upload-time = "2026-09-09T05:27:10.226Z" }, + { url = "https://files.pythonhosted.org/packages/e0/16/0512afae720fd4b7e22bbb6f5bdd24fad49d6b1b92cd34ac71e0d8076109/zizmor-1.30.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf63099a27ef3bf1329dbab44bb9d1027124855c71bd0eca864d5c8e1c5c0db1", size = 8583701, upload-time = "2026-09-09T05:27:12.73Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ff/72efacfb79177835609fe712e9d543a0e5a05d41e3774e907f0b610c8619/zizmor-1.30.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4a5ef2fa4fbd2984f794cfc2f95790bd03fa0bae801487ed550e044675874e60", size = 9558806, upload-time = "2026-09-09T05:27:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/1746121c819319ad7e9d0dfaa96de38dc52d4ac5f6485472b11362f3c6ba/zizmor-1.30.1-py3-none-win32.whl", hash = "sha256:42512bb4a1bce4c787d221d1572ff6b691fa52a3d129b2910be8afaf860c5871", size = 7773805, upload-time = "2026-09-09T05:27:17.218Z" }, + { url = "https://files.pythonhosted.org/packages/db/fe/2889fcbcf7728dd5e013bae7cf5c4e634d981a9e26d55e1da6ab9006404f/zizmor-1.30.1-py3-none-win_amd64.whl", hash = "sha256:06e039fae8e185e2a1a8b002f2d95a6f5c3b548048bdd79e004dcb501fcd0868", size = 8925696, upload-time = "2026-09-09T05:27:19.159Z" }, +]