chore(ci): add zizmor GitHub Actions SAST (advisory → code-scanning) - #2365
Conversation
…anning) Scans .github/workflows + both templates/*/.github/workflows (the YAML synced to 9 consumer repos) — an injection/permission bug here replicates fleet-wide. Advisory for now: findings upload to Security -> Code scanning (category "zizmor"); GitHub annotates NEW findings on PR diffs. Baseline is ~94 template-injection + 14 dangerous-triggers + 2 bot-conditions (high confidence), too large to hard-gate without wedging every PR, so gating follows a triage pass (tracking issue to follow). .github/zizmor.yml relaxes unpinned-uses to ref-pin (669 tag-pin findings are a separate SHA-pinning concern, not injection). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
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:
For more information about GitHub Code Scanning, check out the documentation. |
Workflow source neededPR #2365 needs either a linked GitHub issue or one valid non-issue Workflow Source before PR metadata automation can manage it safely. Please do one of:
Once a valid source is present, this warning will not be reposted. |
Automated Status SummaryHead SHA: a5381d3
Coverage Overview
Coverage Trend
Top Coverage Hotspots (lowest coverage)
Low Coverage Files (<50.0%)
Updated automatically; will refresh on subsequent CI/Docker completions. Keepalive checklistScopeNo scope information available Tasks
Acceptance criteria
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
Bugbot Autofix prepared fixes for all 3 issues found in the latest run.
- ✅ Fixed: Fork PR upload fails
- Added continue-on-error to the advisory SARIF upload step so upload failures do not fail fork PR checks.
- ✅ Fixed: Missing actions read permission
- Granted actions: read to the zizmor job for CodeQL SARIF upload metadata access.
- ✅ Fixed: Summary always claims SARIF uploaded
- Updated the summary to report success, failure, or skipped status based on the upload step outcome.
Preview (afea73c7d8)
diff --git a/.github/workflows/health-51-zizmor.yml b/.github/workflows/health-51-zizmor.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/health-51-zizmor.yml
@@ -1,0 +1,95 @@
+name: Health 51 Actions SAST (zizmor)
+
+# Static analysis of the GitHub Actions YAML this repo ships to 9 consumer repos via template
+# sync — a template-injection / dangerous-trigger / over-permissioned bug here replicates
+# fleet-wide. Advisory for now: findings go to Security -> Code scanning (category `zizmor`),
+# and GitHub annotates NEW findings on PR diffs. Flip to a hard gate (drop the step's
+# continue-on-error and add this check to the branch ruleset) once the baseline injection
+# findings are triaged — see the zizmor tracking issue.
+
+on:
+ pull_request:
+ branches: [main]
+ paths:
+ - '.github/workflows/**'
+ - 'templates/consumer-repo/.github/workflows/**'
+ - 'templates/integration-repo/.github/workflows/**'
+ - '.github/zizmor.yml'
+ push:
+ branches: [main]
+ paths:
+ - '.github/workflows/**'
+ - 'templates/consumer-repo/.github/workflows/**'
+ - 'templates/integration-repo/.github/workflows/**'
+ - '.github/zizmor.yml'
+ schedule:
+ - cron: '30 2 * * 1' # weekly, Monday 02:30 UTC (offset from health-50's Sunday run)
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: zizmor-${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ zizmor:
+ name: zizmor Actions static analysis
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ security-events: write # upload SARIF to code-scanning
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ persist-credentials: false # read-only scan; no git creds needed (zizmor: artipacked)
+
+ - name: Run zizmor (advisory)
+ id: run_zizmor
+ continue-on-error: true # existing ~110 injection findings are a tracked baseline;
+ # code-scanning annotates NEW findings on PR diffs. Remove this
+ # to make zizmor a hard gate once the baseline is triaged.
+ env:
+ GH_TOKEN: ${{ github.token }} # online metadata lookups; avoids API rate limits
+ run: |
+ set -uo pipefail
+ # Config in .github/zizmor.yml relaxes unpinned-uses (a separate SHA-pinning concern);
+ # the injection + permission audits are what we care about here.
+ pipx run zizmor \
+ --config .github/zizmor.yml \
+ --format sarif \
+ .github/workflows \
+ templates/consumer-repo/.github/workflows \
+ templates/integration-repo/.github/workflows \
+ > zizmor.sarif
+
+ - name: Upload SARIF to code-scanning
+ id: upload_sarif
+ if: ${{ hashFiles('zizmor.sarif') != '' }}
+ continue-on-error: true # advisory scan; fork PRs may not be allowed to upload SARIF
+ uses: github/codeql-action/upload-sarif@v4
+ with:
+ sarif_file: zizmor.sarif
+ category: zizmor
+
+ - name: Summary
+ if: always()
+ run: |
+ {
+ echo "## zizmor — GitHub Actions SAST"
+ echo "Advisory run; code-scanning upload status: \`${{ steps.upload_sarif.outcome }}\`."
+ if [ "${{ steps.upload_sarif.outcome }}" = "success" ]; then
+ echo "Findings uploaded to **Security → Code scanning** (category \`zizmor\`)."
+ elif [ "${{ steps.upload_sarif.outcome }}" = "failure" ]; then
+ echo "SARIF upload failed; findings were not uploaded to code scanning."
+ elif [ "${{ steps.upload_sarif.outcome }}" = "skipped" ]; then
+ echo "SARIF upload skipped; no \`zizmor.sarif\` file was available to upload."
+ else
+ echo "Findings were not uploaded to code scanning; inspect the upload step for details."
+ fi
+ echo "Covers \`.github/workflows\` + both \`templates/*/.github/workflows\` (the YAML synced to 9 repos)."
+ echo "Hard-gating follows the baseline injection triage — see the zizmor tracking issue."
+ } >> "$GITHUB_STEP_SUMMARY"
diff --git a/.github/zizmor.yml b/.github/zizmor.yml
new file mode 100644
--- /dev/null
+++ b/.github/zizmor.yml
@@ -1,0 +1,16 @@
+# zizmor — GitHub Actions static analysis (https://docs.zizmor.sh).
+#
+# Why this exists: this repo SHIPS workflow YAML to 9 consumer repos via template sync, so a
+# template-injection / dangerous-trigger / over-permissioned bug here replicates fleet-wide.
+# zizmor's injection + permission audits are the high-value signal we want surfaced.
+#
+# unpinned-uses policy: zizmor's default is hash-pin (every `uses:` must be a full commit SHA),
+# which flags ~669 ordinary tag refs in this repo (actions/checkout@v6, codeql-action@v4, …).
+# Fleet-wide SHA pinning is a separate, deliberate supply-chain decision (Dependabot/Renovate
+# territory), NOT an injection concern — so relax to ref-pin here (require a ref; flag only
+# truly-floating `uses:`). Revisit this if/when the fleet adopts SHA pinning.
+rules:
+ unpinned-uses:
+ config:
+ policies:
+ "*": ref-pin
diff --git a/docs/ci/WORKFLOWS.md b/docs/ci/WORKFLOWS.md
--- a/docs/ci/WORKFLOWS.md
+++ b/docs/ci/WORKFLOWS.md
@@ -22,7 +22,7 @@
- **PR checks:** [Gate](../../.github/workflows/pr-00-gate.yml) fans out to the reusable Python CI matrix and Docker smoke tests before its inline `summary` job publishes the commit status and PR comment. The **Gate summary job** keeps that follow-up comment updated with the latest artifacts.
- **Autofix path:** When Gate reports a failure, it dispatches `autofix_gate_failure`; [agents-autofix-dispatcher.yml](../../.github/workflows/agents-autofix-dispatcher.yml) receives that event and routes eligible PRs into [agents-autofix-loop.yml](../../.github/workflows/agents-autofix-loop.yml), while [Reusable 18 Autofix](../../.github/workflows/reusable-18-autofix.yml) remains a direct-call helper for hygiene-fix or patch-artifact callers. The diagram edge from Gate to Autofix Dispatch represents this repository dispatch hop, not a direct call into reusable autofix.
- **Agents control plane:** Successful Gate runs dispatch the [Agents 70 Orchestrator](../../.github/workflows/agents-70-orchestrator.yml), which coordinates the [Codex belt](../../.github/workflows/agents-71-codex-belt-dispatcher.yml) hand-off (dispatcher → worker → conveyor) and runs the built-in keepalive sweep unless the repository-level `keepalive:paused` label or `keepalive_enabled` flag disables it. The orchestrator summary exposes whether the pause label was detected and records the exact label name through the `keepalive_pause_label` output so downstream jobs can echo the control state.
-- **Health checks:** The [Health 4x suite](../../.github/workflows/health-40-repo-selfcheck.yml), [Health 40 Sweep](../../.github/workflows/health-40-sweep.yml), [Health 41](../../.github/workflows/health-41-repo-health.yml), [Health 42](../../.github/workflows/health-42-actionlint.yml), [Health 43](../../.github/workflows/health-43-ci-signature-guard.yml), [Health 44](../../.github/workflows/health-44-gate-branch-protection.yml), [Health 46 Codex Auth Check](../../.github/workflows/health-codex-auth-check.yml), [Health 50 Security Scan](../../.github/workflows/health-50-security-scan.yml), [Health 67 Integration Sync Check](../../.github/workflows/health-67-integration-sync-check.yml), [Health 68 Consumer Sync Drift Check](../../.github/workflows/health-68-consumer-sync-drift.yml), and [Health 70 Validate Sync Manifest](../../.github/workflows/health-70-validate-sync-manifest.yml) workflows provide scheduled drift detection, enforcement snapshots, auth token monitoring, security scanning, and sync drift detection.
+- **Health checks:** The [Health 4x suite](../../.github/workflows/health-40-repo-selfcheck.yml), [Health 40 Sweep](../../.github/workflows/health-40-sweep.yml), [Health 41](../../.github/workflows/health-41-repo-health.yml), [Health 42](../../.github/workflows/health-42-actionlint.yml), [Health 43](../../.github/workflows/health-43-ci-signature-guard.yml), [Health 44](../../.github/workflows/health-44-gate-branch-protection.yml), [Health 46 Codex Auth Check](../../.github/workflows/health-codex-auth-check.yml), [Health 50 Security Scan](../../.github/workflows/health-50-security-scan.yml), [Health 51 Actions SAST](../../.github/workflows/health-51-zizmor.yml), [Health 67 Integration Sync Check](../../.github/workflows/health-67-integration-sync-check.yml), [Health 68 Consumer Sync Drift Check](../../.github/workflows/health-68-consumer-sync-drift.yml), and [Health 70 Validate Sync Manifest](../../.github/workflows/health-70-validate-sync-manifest.yml) workflows provide scheduled drift detection, enforcement snapshots, auth token monitoring, security scanning, and sync drift detection.
Start with the [Workflow System Overview](WORKFLOW_SYSTEM.md) for the
bucket-level summary, the [keep vs retire roster](WORKFLOW_SYSTEM.md#final-topology-keep-vs-retire), and policy checklist. Return
@@ -178,6 +178,7 @@
* [`health-44-gate-branch-protection.yml`](../../.github/workflows/health-44-gate-branch-protection.yml) ensures branch protection stays aligned with Gate expectations.
* [`health-codex-auth-check.yml`](../../.github/workflows/health-codex-auth-check.yml) checks Codex auth token expiration twice daily and creates issues when refresh is needed.
* [`health-50-security-scan.yml`](../../.github/workflows/health-50-security-scan.yml) runs CodeQL security analysis on Python code (push, PR, weekly schedule).
+* [`health-51-zizmor.yml`](../../.github/workflows/health-51-zizmor.yml) runs zizmor static analysis over the shipped GitHub Actions YAML (both template dirs + this repo) and uploads findings to code scanning (push, PR, weekly schedule).
* [`health-67-integration-sync-check.yml`](../../.github/workflows/health-67-integration-sync-check.yml) validates that Workflows-Integration-Tests repo stays in sync with templates (push, `repository_dispatch`, daily schedule).
* [`health-68-consumer-sync-drift.yml`](../../.github/workflows/health-68-consumer-sync-drift.yml) detects drift in registered consumer repos (template/manifest changes, daily schedule, manual dispatch).
* [`health-70-validate-sync-manifest.yml`](../../.github/workflows/health-70-validate-sync-manifest.yml) validates that sync-manifest.yml is complete - ensures all sync-able files are declared (PR, push).
diff --git a/docs/ci/WORKFLOW_SYSTEM.md b/docs/ci/WORKFLOW_SYSTEM.md
--- a/docs/ci/WORKFLOW_SYSTEM.md
+++ b/docs/ci/WORKFLOW_SYSTEM.md
@@ -709,6 +709,7 @@
| **Health 43 CI Signature Guard** (`health-43-ci-signature-guard.yml`, maintenance bucket) | `schedule` (daily) | Verify reusable workflow signature pins. | ⚪ Scheduled | [Health 43 verification](https://github.com/stranske/Workflows/actions/workflows/health-43-ci-signature-guard.yml) |
| **Health 44 Gate Branch Protection** (`health-44-gate-branch-protection.yml`, maintenance bucket) | `pull_request`, `workflow_dispatch`, `workflow_call` | Ensure Gate and Health 45 Agents Guard stay required on the default branch. | ⚪ Scheduled via sweep / manual | [Health 44 enforcement logs](https://github.com/stranske/Workflows/actions/workflows/health-44-gate-branch-protection.yml) |
| **Health 50 Security Scan** (`health-50-security-scan.yml`, maintenance bucket) | `push`, `pull_request`, `schedule` (weekly) | Run CodeQL security analysis on Python code to detect vulnerabilities. | ⚪ Automatic/scheduled | [Security scan runs](https://github.com/stranske/Workflows/actions/workflows/health-50-security-scan.yml) |
+| **Health 51 Actions SAST (zizmor)** (`health-51-zizmor.yml`, maintenance bucket) | `push`, `pull_request`, `schedule` (weekly) | Static-analyze the GitHub Actions YAML shipped to consumer repos (zizmor) for injection/permission issues; upload findings to code scanning. | ⚪ Automatic/scheduled | [zizmor runs](https://github.com/stranske/Workflows/actions/workflows/health-51-zizmor.yml) |
| **Health 67 Integration Sync Check** (`health-67-integration-sync-check.yml`, maintenance bucket) | `push` (templates), `repository_dispatch`, `schedule` (daily) | Validate that Workflows-Integration-Tests repo stays in sync with templates. Creates issues when drift detected. | ⚪ Automatic/scheduled | [Integration sync runs](https://github.com/stranske/Workflows/actions/workflows/health-67-integration-sync-check.yml) |
| **Health 68 Consumer Sync Drift Check** (`health-68-consumer-sync-drift.yml`, maintenance bucket) | `push` (templates + manifest + scripts), `schedule` (daily), `workflow_dispatch` | Detect template/manifest drift across registered consumer repos. Creates or updates issues when drift is detected. | ⚪ Automatic/scheduled | [Consumer drift runs](https://github.com/stranske/Workflows/actions/workflows/health-68-consumer-sync-drift.yml) |
| **Health 70 Validate Sync Manifest** (`health-70-validate-sync-manifest.yml`, maintenance bucket) | `pull_request`, `push` | Validate that sync-manifest.yml includes all sync-able files. Fails PRs that add workflows/prompts/scripts without updating manifest. | ⚪ Required on PRs | [Manifest validation runs](https://github.com/stranske/Workflows/actions/workflows/health-70-validate-sync-manifest.yml) |
diff --git a/tests/workflows/test_workflow_naming.py b/tests/workflows/test_workflow_naming.py
--- a/tests/workflows/test_workflow_naming.py
+++ b/tests/workflows/test_workflow_naming.py
@@ -235,6 +235,7 @@
"health-44-gate-branch-protection.yml": "Health 44 Gate Branch Protection",
"health-codex-auth-check.yml": "Health 46 Codex Auth Check",
"health-50-security-scan.yml": "Health 50 Security Scan",
+ "health-51-zizmor.yml": "Health 51 Actions SAST (zizmor)",
"maint-45-cosmetic-repair.yml": "Maint 45 Cosmetic Repair",
"maint-46-post-ci.yml": "Maint 46 Post CI",
"maint-47-disable-legacy-workflows.yml": "Maint 47 Disable Legacy Workflows",You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 6ff6ad9. Configure here.
…tory docs The new workflow tripped test_workflow_naming + test_disable_legacy_workflows: every workflow must appear in EXPECTED_NAMES and in docs/ci/WORKFLOWS.md + docs/ci/WORKFLOW_SYSTEM.md. Mirrored health-50's entries. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new GitHub Actions “health” workflow to run zizmor against this repo’s workflow YAML (including the consumer/integration template workflow trees) and upload findings to Security → Code scanning as SARIF, with a small zizmor config file to reduce noise from non-SHA-pinned uses: refs.
Changes:
- Add
health-51-zizmor.ymlto runpipx run zizmoron workflow directories and upload SARIF results (currently advisory viacontinue-on-erroron the scan step). - Add
.github/zizmor.ymlto relax theunpinned-usesrule toref-pinto keep tag-pinneduses:refs out of the finding stream.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
.github/zizmor.yml |
Adds zizmor rule configuration (relax unpinned-uses to ref-pin). |
.github/workflows/health-51-zizmor.yml |
New Health 51 workflow to run zizmor over workflow YAML and upload SARIF to code scanning. |
| - name: Upload SARIF to code-scanning | ||
| if: ${{ hashFiles('zizmor.sarif') != '' }} | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| with: | ||
| sarif_file: zizmor.sarif | ||
| category: zizmor |
| name: Health 51 Actions SAST (zizmor) | ||
|
|
||
| # Static analysis of the GitHub Actions YAML this repo ships to 9 consumer repos via template | ||
| # sync — a template-injection / dangerous-trigger / over-permissioned bug here replicates | ||
| # fleet-wide. Advisory for now: findings go to Security -> Code scanning (category `zizmor`), |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ff6ad9fdb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -0,0 +1,82 @@ | |||
| name: Health 51 Actions SAST (zizmor) | |||
There was a problem hiding this comment.
Register Health 51 in workflow inventory
Adding this workflow without updating the repo's workflow inventory breaks the existing workflow tests: tests/workflows/test_workflow_naming.py requires every .github/workflows/*.yml to appear in EXPECTED_NAMES and in both docs/ci/WORKFLOW_SYSTEM.md and docs/ci/WORKFLOWS.md, but health-51-zizmor.yml is absent from all three. Any Gate run that executes those tests will fail before the new advisory scan can be merged cleanly.
Useful? React with 👍 / 👎.
| - name: Upload SARIF to code-scanning | ||
| if: ${{ hashFiles('zizmor.sarif') != '' }} | ||
| uses: github/codeql-action/upload-sarif@v4 |
There was a problem hiding this comment.
Avoid SARIF upload on read-only PR tokens
When this pull_request workflow is triggered from a fork or Dependabot branch, GitHub provides a read-only GITHUB_TOKEN, so the requested security-events: write permission is unavailable and this upload step will fail with an authorization error. Because the upload step is not guarded or marked continue-on-error, those advisory PR runs become red even though the scan step itself is explicitly non-blocking.
Useful? React with 👍 / 👎.

What
Adds zizmor GitHub Actions static analysis as
health-51-zizmor.yml+ a focused.github/zizmor.yml. Scans.github/workflowsand bothtemplates/*/.github/workflows— the YAML this repo syncs to 9 consumer repos, where one injection/permission bug replicates fleet-wide (audit §6/§7).Why advisory, not a hard gate (yet)
zizmor's baseline on the current tree is large — at high confidence: ~94
template-injection+ 14dangerous-triggers+ 2bot-conditionsacross 30+ workflow files.fail-on-hightoday would block every PR on a 100+ finding backlog. So instead:zizmor) — tracked, and GitHub annotates new findings on PR diffs (gate-on-new without wedging the backlog). NB: the first run onmainestablishes the baseline, so the initial PR view may show the whole backlog as "new" — expected.continue-on-error(advisory). Flip to a hard gate (dropcontinue-on-error, add the check to the branch ruleset) once the baseline is triaged — tracking issue to follow.Config
.github/zizmor.ymlrelaxesunpinned-usestoref-pin: the ~669 tag-pin findings (@v6,@v4) are a separate fleet-wide SHA-pinning decision (Dependabot/Renovate territory), not injection — kept out so the injection signal isn't buried.Validation
persist-credentials: falseforartipacked).unpinned-uses669 → 0, injection/permission audits retained.🤖 Generated with Claude Code
Note
Low Risk
Adds read-only CI and documentation; scan steps are advisory and do not change runtime application or auth behavior.
Overview
Adds Health 51 (
health-51-zizmor.yml), which runs zizmor over this repo’s workflows and both template workflow trees—the YAML synced to consumer repos—on PR/push (path-filtered), weekly schedule, and manual dispatch.Findings are emitted as SARIF and uploaded to Code scanning (
zizmorcategory). The scan and SARIF upload usecontinue-on-errorso a large existing injection baseline does not block merges; the intent is to surface new findings on PRs until triage enables hard gating..github/zizmor.ymlsetsunpinned-usesto ref-pin so tag-pinneduses:refs are not drowned out by hash-pin noise; injection and permission rules stay in focus.CI docs and
test_workflow_naming.pyregister the new workflow name.Reviewed by Cursor Bugbot for commit afea73c. Bugbot is set up for automated code reviews on this repo. Configure here.