-
Notifications
You must be signed in to change notification settings - Fork 3
Add harden-github-actions skill #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
da3b17d
c2be5ed
739d154
6fe6117
cf34832
d16fa07
f77229f
91c6092
8dbc4eb
8923ad6
f874bac
2eb39a2
ffe7e22
82ea6f0
da43535
a0cbd3d
ef2e125
98b3c93
57c51ad
e4093a3
18a4c02
8272d1f
3863a34
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,267 @@ | ||||||
| --- | ||||||
| name: zizmor-resolution | ||||||
| description: > | ||||||
| Use when resolving zizmor warnings in GitHub Actions workflows, hardening CI | ||||||
| pipelines, or pinning actions to SHA hashes. Covers artipacked, template-injection, | ||||||
| excessive-permissions, secrets-outside-env, dependabot-execution, and when to | ||||||
| suppress vs fix. | ||||||
| --- | ||||||
|
|
||||||
| # Resolving Zizmor Warnings in GitHub Actions | ||||||
|
Comment on lines
+1
to
+10
|
||||||
|
|
||||||
| ## Overview | ||||||
|
|
||||||
| zizmor identifies security vulnerabilities in GitHub Actions workflows. This skill documents | ||||||
| the decision guidelines for resolving each warning type: when to fix, how to fix, and when | ||||||
| to suppress with an inline comment explaining why. | ||||||
|
|
||||||
| **Core principle:** Fix the vulnerability whenever possible. Suppress only when the fix would | ||||||
| break required functionality, and always include a reason in the suppression comment. | ||||||
|
|
||||||
|
Comment on lines
+10
to
+20
|
||||||
| ## Prerequisites | ||||||
|
|
||||||
| This work should be done on a branch in a git worktree. Before starting any work, verify | ||||||
| you are in the worktree directory and on the correct branch: | ||||||
|
|
||||||
| ```bash | ||||||
| pwd # should be the worktree path | ||||||
| git branch # should show the feature branch, not main | ||||||
| ``` | ||||||
|
|
||||||
| ## Workflow Order | ||||||
|
|
||||||
| Always work in this order. Each step is a separate commit. | ||||||
|
|
||||||
| 1. **Pin actions** with `pinact run` | ||||||
| 2. **Address zizmor warnings** by severity (high → medium → low → informational). | ||||||
| 3. **Ensure all permissions are job-level.** Check every workflow file for top-level | ||||||
| `permissions:` blocks. Replace with `permissions: {}` and add per-job permissions. | ||||||
| This goes beyond what zizmor flags — zizmor misses single-job workflows. Commit. | ||||||
| 4. **Run actionlint** and fix any findings. Commit. | ||||||
| 5. **Add zizmor CI job** using the standard template | ||||||
| 6. **Add local workflow linting** to `bin/setup` and `bin/ci` (see below). Skip if these | ||||||
| scripts don't exist in the project. | ||||||
| 7. **Configure dependabot** to batch github-actions updates weekly | ||||||
|
|
||||||
| ## Running pinact | ||||||
|
|
||||||
| Run `pinact run --min-age 10` from the repository root. This pins all actions in `.github/workflows/` to SHA hashes, skipping any versions published less than 10 days ago. | ||||||
|
|
||||||
| ## Running zizmor | ||||||
|
|
||||||
| **Always run zizmor with a GitHub token** so that online audits (like `ref-version-mismatch` | ||||||
| and `impostor-commit`) can resolve SHAs against the GitHub API. Without a token, these audits | ||||||
| are silently skipped and findings will only surface in CI. | ||||||
|
|
||||||
| ```bash | ||||||
| GITHUB_TOKEN=$(gh auth token) zizmor . | ||||||
| ``` | ||||||
|
|
||||||
| Filter severity by passing the flag `--min-severity=<level>` where level can be `high`, `medium`, or `low`. Informational warnings may be emitted by omitting this flag entirely. | ||||||
|
|
||||||
| ### Auto-fix workflow | ||||||
|
|
||||||
| For each severity level (high, then medium, then low, then informational): | ||||||
|
|
||||||
| 1. Run `zizmor --fix=all --min-severity=<level> .` to auto-correct fixable findings (`--fix` alone uses safe mode which silently holds back some fixes; use `--fix=all` and rely on diff review as the safety net) | ||||||
| 2. **STOP and review the diff.** Check each auto-fix against the Decision Guide below. | ||||||
| - `cache-poisoning` fixes will disable caching — almost always revert these and suppress instead | ||||||
| - `artipacked` fixes add `persist-credentials: false` — revert if the workflow needs `git push` | ||||||
| - `superfluous-actions` fixes replace actions with inline code — always revert these and suppress instead | ||||||
| - `bot-conditions` auto-fix replaces `github.actor` with `user.login` — revert and apply the dual check instead (see rule file) | ||||||
| - `template-injection` fixes are generally correct | ||||||
| 3. Revert any incorrect fixes | ||||||
| 4. For reverted fixes, apply the correct resolution manually (e.g., suppress with a reason) | ||||||
| 5. Manually fix anything `--fix` didn't handle. **For `excessive-permissions`: you MUST research each action's permissions. Do not guess. See the permission research process below.** | ||||||
| 6. Run `zizmor --min-severity=<level> .` to verify a clean check at this severity level | ||||||
| 7. Commit | ||||||
|
|
||||||
| After completing all default severity levels, run a pedantic pass: | ||||||
|
|
||||||
| 1. Run `zizmor --persona=pedantic --min-severity=high .` | ||||||
| 2. Address findings the same way as above — the most common pedantic finding is | ||||||
| `excessive-permissions` on single-job workflows where zizmor's default persona doesn't | ||||||
| flag it. Apply the same fix: `permissions: {}` at workflow level, scoped per job. | ||||||
| 3. Run `zizmor --persona=pedantic --min-severity=high .` to verify clean | ||||||
| 4. Commit | ||||||
|
|
||||||
| ## Decision Guide by Rule | ||||||
|
|
||||||
| When you encounter a zizmor finding, read the corresponding rule file in `references/` for | ||||||
| full decision guidance, suppression checklists, and examples. Only read the rules you need. | ||||||
|
|
||||||
| | Rule | File | Action | | ||||||
| |------|------|--------| | ||||||
| | `artipacked` | `references/rule-artipacked.md` | Fix (add `persist-credentials: false`); suppress only if job does `git push` | | ||||||
| | `template-injection` | `references/rule-template-injection.md` | Always fix (move expressions to `env:` vars) | | ||||||
| | `excessive-permissions` | `references/rule-excessive-permissions.md` | Always fix (set `permissions: {}` at workflow level, scope per job) | | ||||||
| | `dangerous-triggers` | `references/rule-dangerous-triggers.md` | Fix or suppress with 5-point checklist | | ||||||
| | `secrets-outside-env` | `references/rule-secrets-outside-env.md` | Fix (add `environment:`) or suppress with 3-point checklist | | ||||||
| | `bot-conditions` | `references/rule-bot-conditions.md` | Always fix (dual check: `actor` + `user.login`); revert auto-fix | | ||||||
| | `superfluous-actions` | `references/rule-superfluous-actions.md` | Always suppress (never replace with inline code) | | ||||||
| | `cache-poisoning` | `references/rule-cache-poisoning.md` | Suppress (default); revert auto-fixes; only escalate if custom cache keys | | ||||||
| | `unpinned-images` | `references/rule-unpinned-images.md` | Suppress (default); digest pinning is nontrivial | | ||||||
| | `dependabot-execution` | `references/rule-dependabot-execution.md` | Fix or suppress with 3-point checklist | | ||||||
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) | | ||||||
|
||||||
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 10` to all ecosystems) | | |
| | `dependabot-cooldown` | `references/rule-dependabot-cooldown.md` | Always fix (add `cooldown: default-days: 7` to all ecosystems) | |
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The decision-guide Markdown table starts each row with ||, which renders as an extra empty column in GitHub Markdown. Change the table rows to start with a single | so the table displays correctly.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The standard CI job template doesn't declare job-level permissions:. Earlier the skill instructs setting workflow-level permissions: {}; if users follow that, this job will fail (at minimum actions/checkout typically needs contents: read). Add an explicit minimal permissions: block to the lint-actions job in the template.
Copilot
AI
Mar 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Common Mistakes" Markdown table also starts rows with ||, which will render an unintended blank first column. Use a single leading | for standard Markdown table formatting.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,71 @@ | ||||||
| # GitHub Actions Permission Mappings | ||||||
|
|
||||||
| GITHUB_TOKEN permission requirements for common GitHub Actions. Used by the | ||||||
| `harden-github-actions` skill when resolving `excessive-permissions` findings. | ||||||
|
|
||||||
| **If an action is not in this table, you MUST fetch its README on GitHub and read the | ||||||
| documented permission requirements before proceeding. Then add it to this table.** | ||||||
|
|
||||||
| ## Actions | ||||||
|
|
||||||
| | Action | Permissions | Notes | | ||||||
| |--------|------------|-------| | ||||||
| | `actions/ai-inference` | `models: read` | + `contents: read` recommended; PAT required for MCP server feature | | ||||||
| | `actions/attest-build-provenance` | `id-token: write`, `attestations: write`, `contents: read` | + `packages: write` when `push-to-registry: true` for container images | | ||||||
| | `actions/cache` | none | Uses Actions cache service via implicit runner credentials, not GITHUB_TOKEN | | ||||||
| | `actions/checkout` | `contents: read` | | | ||||||
|
Comment on lines
+11
to
+16
|
||||||
| | `actions/configure-pages` | none | Reads Pages config only; `enablement: true` needs a PAT with `pages:write` | | ||||||
| | `actions/create-github-app-token` | none | Authenticates via GitHub App credentials, not GITHUB_TOKEN; *generates* a token with its own permissions | | ||||||
| | `actions/create-release` | `contents: write` | Archived/unmaintained; consider `softprops/action-gh-release` instead | | ||||||
| | `actions/dependency-review-action` | `contents: read` | + `pull-requests: write` if `comment-summary-in-pr` is `always` or `on-failure` | | ||||||
| | `actions/deploy-pages` | `pages: write`, `id-token: write` | | | ||||||
| | `actions/download-artifact` | none | `actions: read` only when using `github-token` input to download from other repos/workflow runs | | ||||||
| | `actions/github-script` | depends on script | No fixed permissions; required permissions depend on which GitHub API calls the script makes. Key distinction: PR comments (`gh pr comment`, `updateIssueComment` on PR objects) need `pull-requests: write`; applying labels via the Issues API needs `issues: write`. Read the script to determine. | | ||||||
| | `actions/labeler` | `contents: read`, `pull-requests: write` | + `issues: write` only if the action needs to create labels that don't already exist | | ||||||
| | `actions/setup-go` | `contents: read` | | | ||||||
| | `actions/setup-java` | `contents: read` | | | ||||||
| | `actions/setup-node` | `contents: read` | | | ||||||
| | `actions/setup-ruby` | `contents: read` | | | ||||||
| | `actions/stale` | `issues: write`, `pull-requests: write` | + `contents: write` when `delete-branch: true` | | ||||||
| | `actions/upload-artifact` | none | Uses Actions artifact storage via implicit runner credentials | | ||||||
| | `actions/upload-pages-artifact` | none | Only creates a tar archive | | ||||||
| | `actions/upload-release-asset` | `contents: write` | Archived/unmaintained; consider `softprops/action-gh-release` instead | | ||||||
| | `anchore/sbom-action` | `contents: write` | + `actions: read` when attaching release assets (implicit for public repos) | | ||||||
| | `aquasecurity/trivy-action` | `contents: read` | + `security-events: write` when uploading SARIF; + `contents: write` when submitting SBOMs to Dependency Graph | | ||||||
| | `basecamp/sdk` (sub-actions) | varies | Contains 5 composite actions under `actions/`. Only `release-orchestrate` needs a token: `actions: read`, `contents: write`. The others (`conformance-run`, `rubric-check`, `service-drift`, `smithy-verify`) need none. | | ||||||
| | `cachix/install-nix-action` | none | Uses `github.token` only to avoid API rate limits when downloading Nix | | ||||||
| | `dependabot/fetch-metadata` | `pull-requests: read` | + `pull-requests: write` for auto-approving; + `contents: write` for auto-merging; PAT required for `alert-lookup` or `compat-lookup` | | ||||||
| | `dev-build-deploy/commit-me` | `pull-requests: read` | + `pull-requests: write` when `update-labels: true`; + `contents: write` for automatic rebase-merge detection | | ||||||
| | `devcontainers/ci` | none | When pushing images to ghcr.io, the workflow must log in separately (e.g., `docker/login-action`), which requires `packages: write` | | ||||||
| | `docker/build-push-action` | `contents: read` | Registry auth (e.g., `packages: write` for GHCR) is handled by `docker/login-action`, not this action | | ||||||
| | `docker/login-action` | none | The action itself needs no permissions; pass `secrets.GITHUB_TOKEN` as the `password` input. The *token* needs `packages: write` for GHCR push, `packages: read` for pull-only. | | ||||||
| | `docker/metadata-action` | `contents: read` | Reads repo context (tags, branches, commits) only | | ||||||
| | `docker/setup-buildx-action` | none | Only configures the Docker Buildx builder environment | | ||||||
| | `docker/setup-qemu-action` | none | Installs QEMU static binaries for multi-platform builds | | ||||||
| | `dorny/paths-filter` | `pull-requests: read` | Only on `pull_request` events; on `push` events uses git commands directly and needs no permissions | | ||||||
| | `elastic/docs` | not a standard action | Elastic's internal documentation build tooling. Contains a composite action at `.github/actions/docs-preview` needing `issues: write` for PR comments, but not intended for external use. Check the repo source if encountered. | | ||||||
| | `github/codeql-action` (/init, /analyze, /upload-sarif) | `security-events: write`, `contents: read` | `security-events: write` for all advanced setup workflows; `contents: read` additionally required for private repos | | ||||||
| | `golangci/golangci-lint-action` | `contents: read` | + `pull-requests: read` if using `only-new-issues`; + `checks: write` for inline PR annotations | | ||||||
| | `google-github-actions/release-please-action` | `contents: write`, `pull-requests: write` | May also need "Allow GitHub Actions to create and approve pull requests" enabled in repo settings | | ||||||
| | `goreleaser/goreleaser-action` | `contents: write` | Token passed via `env: GITHUB_TOKEN`; PAT with `repo` scope needed for cross-repo operations (e.g., Homebrew taps) | | ||||||
| | `gradle/actions` (sub-actions) | varies | `setup-gradle`: none for basic use; `contents: write` for dependency graph submission; `pull-requests: write` for PR comment summaries. `dependency-submission`: `contents: write`. `wrapper-validation`: none. | | ||||||
| | `ko-build/setup-ko` | none | + `packages: write` when pushing container images to ghcr.io; no permissions needed if only building | | ||||||
| | `necko-actions/setup-smithy` | none | Installs Smithy CLI and adds to PATH | | ||||||
| | `ossf/scorecard-action` | `security-events: write`, `id-token: write`, `contents: read` | For public repos with `publish_results: true`; private repos also need `issues: read`, `pull-requests: read`, `checks: read` | | ||||||
| | `reviewdog/action-rubocop` | `contents: read`, `pull-requests: write` | For `github-pr-review` and `github-pr-check` reporters; `checks: write` may also be needed for `github-check` reporter | | ||||||
| | `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. | | ||||||
|
||||||
| | `rhysd/actionlint` | none | Not a standard action (no `action.yml`); used by downloading the binary via shell script or Docker. Does not use GITHUB_TOKEN. | | |
| | `rhysd/actionlint` | none | Lints workflow files only; does not call GitHub APIs or use `GITHUB_TOKEN`. | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This skill frontmatter is missing a
triggers:list. Other skills in this repo consistently includetriggers:(see e.g.skills/install-md/SKILL.md), and the skill-crafting guide documents it as part of the required frontmatter. Add appropriate trigger phrases so the skill can be activated reliably.