Skip to content

Audit workflows with zizmor, and fix 13 of the 15 findings it reports today - #179

Merged
rocklambros merged 2 commits into
integrationfrom
ci/zizmor
Sep 22, 2026
Merged

rocklambros merged 2 commits into
integrationfrom
ci/zizmor

Conversation

@rocklambros

@rocklambros rocklambros commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

CodeQL default setup was turned on for this repository on 2026-09-21 and covers the Actions language. This adds zizmor, which the OWASP GitHub Actions Security Cheat Sheet recommends beside CodeQL for defense in depth. zizmor reads each workflow's logic and checks what CodeQL does not: hash pins no tag points to, version comments that disagree with their pin, App tokens broader than the job needs, and credentials a checkout leaves in .git/config.

Run against integration before this change with online audits on, zizmor 1.30.1 reported 15 findings. This clears 13. The other two are github-app findings on board-reconcile.yml, whose App token takes the installation's full grant instead of naming its repository and permissions. Token scopes are not changing right now, so those two stay open in code scanning, where they record the deferred decision instead of disappearing behind a suppression.

  • pr-intake.yml, 1 high. zizmor flags every pull_request_target. This one checks out no code and reads the pull request body as data through an environment variable, which the workflow's header already explains. It carries an inline zizmor: ignore that points at that explanation.
  • Eleven checkouts across nine workflows, 11 low. Each left the job's token in .git/config. None of those jobs pushes with git. They call gh with GH_TOKEN or touch no network at all, and sync_version.yml hands its push to create-pull-request, which configures its own token. All eleven now set persist-credentials: false. The checkout in validate-owasp-metadata.yaml also gains the # v7.0.1 comment every other pin carries.
  • reference-implementation.yml, 1 medium. The setup-bun pin 735343b is v2.0.2, and its comment said v2.0.1. Only the comment changes. Dependabot's ci: bump oven-sh/setup-bun from 2.0.2 to 2.2.0 #83 already treats it as 2.0.2.

The new workflow runs zizmor from a zizmor dependency group in uv.lock, pinned with hashes. No new action enters the Actions allowlist, and Dependabot's uv ecosystem moves zizmor under the existing 7-day cooldown. The group is not a default group, so uv sync --locked in the deploy and test jobs never installs it. A default sync against this lockfile would uninstall it.

It runs on pull requests and pushes that touch .github/, an action definition, pyproject.toml, or uv.lock, and weekly, because the online audits compare pins against advisories published after the pin. Findings upload as SARIF under the category zizmor, and the job stays green on findings so code scanning tracks them and a ruleset can gate on them later. A tool or upload failure still fails the job. A pull request from a fork cannot upload with its read-only token, so it gets annotations instead. upload-sarif is pinned at v4.38.0 because v4.38.1 was three days old, inside the cooldown this repository applies to its dependencies.

Open pull requests #162 and #169 add workflows that zizmor will flag once they land, for unset persist-credentials and the same # v2.0.1 setup-bun comment.

Verified locally: zizmor 1.30.1 with online audits reports only the two deferred github-app findings, with 1 ignored and 26 low-confidence findings suppressed by the default persona. The workflow's own command produces valid SARIF 2.1.0. actionlint is clean. uv lock --check passes under uv 0.9.9, the version CI pins, and the lockfile diff adds only zizmor. uv run pytest gives 278 passed, 1 skipped.

Signed-off-by: Rock Lambros rock@rockcyber.com

CodeQL default setup was turned on for this repository on 2026-09-21 and covers the Actions language. This adds zizmor, which the [OWASP GitHub Actions Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GitHub_Actions_Security_Cheat_Sheet.html) recommends beside CodeQL for defense in depth. zizmor reads each workflow's logic and checks what CodeQL does not: hash pins no tag points to, version comments that disagree with their pin, App tokens broader than the job needs, and credentials a checkout leaves in `.git/config`.

Run against `integration` before this change with online audits on, zizmor 1.30.1 reported 15 findings. This clears all of them, so the Security tab starts clean.

- **`board-reconcile.yml`, 2 high.** The App installation token inherited the installation's whole grant and every repository it covers. It now names this repository and the four permissions the reconciler uses. The installation is that narrow today, but its settings live outside this repository and were over-provisioned once already, with `repository_projects: write` removed on 2026-09-21. With the grant stated in the workflow, a wider installation still mints a narrow token. The repository is named literally because a scheduled run carries no repository in its event payload.
- **`pr-intake.yml`, 1 high.** zizmor flags every `pull_request_target`. This one checks out no code and reads the pull request body as data through an environment variable, which the workflow's header already explains. It carries an inline `zizmor: ignore` that points at that explanation.
- **Eleven checkouts across nine workflows, 11 low.** Each left the job's token in `.git/config`. None of those jobs pushes with git. They call `gh` with `GH_TOKEN` or touch no network at all, and `sync_version.yml` hands its push to `create-pull-request`, which configures its own token. All eleven now set `persist-credentials: false`. The checkout in `validate-owasp-metadata.yaml` also gains the `# v7.0.1` comment every other pin carries.
- **`reference-implementation.yml`, 1 medium.** The setup-bun pin `735343b` is v2.0.2, and its comment said v2.0.1. Only the comment changes. Dependabot's #83 already treats it as 2.0.2.

The new workflow runs zizmor from a `zizmor` dependency group in `uv.lock`, pinned with hashes. No new action enters the Actions allowlist, and Dependabot's uv ecosystem moves zizmor under the existing 7-day cooldown. The group is not a default group, so `uv sync --locked` in the deploy and test jobs never installs it. A default sync against this lockfile would uninstall it.

It runs on pull requests and pushes that touch `.github/`, an action definition, `pyproject.toml`, or `uv.lock`, and weekly, because the online audits compare pins against advisories published after the pin. Findings upload as SARIF under the category `zizmor`, and the job stays green on findings so code scanning tracks them and a ruleset can gate on them later. A tool or upload failure still fails the job. A pull request from a fork cannot upload with its read-only token, so it gets annotations instead. `upload-sarif` is pinned at v4.38.0 because v4.38.1 was three days old, inside the cooldown this repository applies to its dependencies.

One change cannot be verified before merge. The `board-automation` environment releases its key only to `integration`, so a dry-run dispatch from this branch cannot mint a token. After merge, dispatch `Reconcile project board` on `integration` with apply off. A wrong scope fails the token request loudly, and the next nightly run is the one at risk, so the dry run belongs on the same day.

Open pull requests #162 and #169 add workflows that zizmor will flag once they land, for unset `persist-credentials` and the same `# v2.0.1` setup-bun comment.

Verified locally: zizmor 1.30.1 with online audits reports no findings, with 1 ignored and 26 low-confidence findings suppressed by the default persona. The workflow's own command produces SARIF 2.1.0 with zero results. `actionlint` is clean. `uv lock --check` passes under uv 0.9.9, the version CI pins, and the lockfile diff adds only zizmor. `uv run pytest` gives 278 passed, 1 skipped.

Signed-off-by: Rock Lambros <rock@rockcyber.com>
@github-actions

This comment has been minimized.

@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@rocklambros rocklambros removed the status:needs-triage Not yet triaged. Applied by the issue forms label Sep 21, 2026
Token scopes are not changing in this pull request. zizmor's two github-app findings on board-reconcile.yml stay open in code scanning, where they record the deferred decision instead of disappearing behind a suppression.

Signed-off-by: rocklambros <rock@rockcyber.com>
@Lrd0036 Lrd0036 mentioned this pull request Sep 21, 2026
10 tasks
rocklambros added a commit that referenced this pull request Sep 22, 2026
v0.1.2 reached `main` with #59 on 2026-09-09 and stayed untagged until today, when it was tagged by hand on `7743826`, the commit that first carried it. Tagging was a manual step with no owner, so nothing noticed the gap.

`Tag release` makes it automatic. A push to `main` that changes `version.txt` is the release event, and the workflow tags that commit `v<version>` as an annotated tag, which matches how v0.1.1 and v0.1.2 are shaped. It exits quietly when the tag already exists, so reruns and unrelated pushes are safe. A manual dispatch from any branch other than `main` is skipped, since it would tag a commit `main` never published.

It also refuses to tag when `version.txt`, `pyproject.toml`, and `uv.lock` disagree. The release version lives in those three files and no other check keeps them in step. A tag is the point where a mismatch becomes permanent, so the run fails with all three values in the error.

`version.txt` is attacker-controlled by anyone with write access and becomes a git ref here, so it passes the same semver check `sync_version.yml` uses before it reaches `git tag`. No `${{ }}` expression appears inside `run:`. The job holds `contents: write` and nothing else, and the only action is `actions/checkout` at the SHA every other workflow pins.

The workflow lives on `main` only after the next promotion, so the first release it tags is the next version bump that reaches `main` after that.

Tested against a local bare remote with the step's own script, extracted from the YAML. An existing version exits 0 with "already exists". A new version in all three files creates an annotated tag and a rerun is a no-op. A stale `uv.lock` fails with the three values. An invalid version fails before touching git. `actionlint` is clean and `uv run pytest` gives 278 passed, 1 skipped.

The checkout keeps its credentials because the tag push authenticates with them. zizmor's `artipacked` audit flags that, and the finding is suppressed inline with the reason: the job uploads no artifacts, which is the leak the audit exists to catch. With #179 merged, zizmor reports nothing on this workflow.

CONTRIBUTING.md's Release Process section now says tagging is automatic.

Signed-off-by: Rock Lambros <rock@rockcyber.com>
@rocklambros
rocklambros merged commit 6b25dd8 into integration Sep 22, 2026
11 checks passed
@rocklambros
rocklambros deleted the ci/zizmor branch September 22, 2026 03:21
@github-project-automation github-project-automation Bot moved this from Needs triage to Done in ACS Project Tracker Sep 22, 2026
@rocklambros rocklambros changed the title Audit workflows with zizmor, after fixing everything it finds today Audit workflows with zizmor, and fix 13 of the 15 findings it reports today Sep 22, 2026
President added a commit to disciplinedware/agent-control-standard that referenced this pull request Sep 23, 2026
…comment in the Go workflow

zizmor, added in GenAI-Security-Project#179, reports both once this workflow lands. Nothing in the
job pushes with git, so the checkout sets persist-credentials to false. The
setup-bun pin 735343b is v2.0.2, and only its comment changes.

Signed-off-by: Konstantin Trunin <konstantin@trunin.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants