zizmor (GitHub Actions)¶
zizmor is a static analyzer for GitHub Actions. ASH runs it over a repository's workflows and composite actions and reports template injection, dangerous triggers, credential persistence, unpinned actions, excessive permissions and the rest of zizmor's audits.
actionlint, also a builtin scanner, flags template injection from untrusted event data too. ASH does not deduplicate across scanners, so with both enabled such a step is reported by each, under its own rule id.
Enabling it¶
zizmor is a builtin scanner, enabled by default: a default scan runs it. scanners.zizmor.enabled: false in the ASH config turns it off, and --scanners and --exclude-scanners select it as they select any scanner. If no zizmor satisfying tool_version is found, ASH installs one with uv tool install (online only). If that fails, the scanner is reported MISSING and the scan exits 1 (the incomplete-scan gate), as for every builtin scanner; ash dependencies install and the container image provide it.
What gets scanned¶
ASH passes zizmor two kinds of file, taken from its scan set so .gitignore,
.ignore and global_settings.ignore_paths apply (the ignore files only while
honor_ignore_files is on, the default):
- workflows:
*.ymland*.yamldirectly inside a.github/workflowsdirectory (the only place GitHub runs a workflow from; subdirectories are not scanned), including when the source directory is itself.githubor.github/workflows; - composite actions: every
action.ymlandaction.yaml.
Files under directories named node_modules, .venv or venv are skipped, and so is a
file that is a symlink resolving outside the scan root, with a warning: zizmor
would read it where it points. In the source directory, such a link is left out
by the scan set before zizmor's check sees it, with the warning
Not scanning '<path>': link leaves the scanned tree, and is listed among the
inputs not scanned (see
What decides the files).
A broken symlink (its target is missing, or links
loop) is skipped with a warning too, since zizmor would fail the whole run on it. A repository
with neither kind of file does not start zizmor; the scanner reports SKIPPED
with zero findings, the same as cfn-nag on a repository with no templates.
zizmor is a Python package ASH runs through uv, so --python-only runs it.
A file named action.yml that is not a GitHub Actions definition is not counted.
A workflow or action that zizmor cannot load (a YAML syntax error, or a file in
.github/workflows that is not a valid workflow) is counted as a target that
failed, which --fail-on-incomplete-scanners reports.
Configuration¶
scanners:
zizmor:
enabled: true
options:
persona: regular # regular, pedantic or auditor
config_file: null # zizmor config; relative to where it is set, or absolute
online_audits: false # see "Network access and tokens"
honor_inline_ignores: false # see "Suppressions"
tool_version: ">=1.29.0,<2.0.0"
install_timeout: 300
scan_timeout: 1800
severity_threshold: null
persona: zizmor's persona.regularhas the fewest false positives,pedanticadds code-smell findings, andauditorreports everything.config_file: passed aszizmor --config, when the operator set it: from--config-overrides, or an ASH config file outside every git checkout and the scanned directory (one that is not repository-written). A relative path is taken from the directory of the ASH config file that sets it, or from the working directory for--config-overrides, not from the source directory (see Relative paths in the operator's settings); an absolute path is used as given. Without it, ASH runszizmor --no-config, so azizmor.ymlor.github/zizmor.ymlin the scanned repository is not read: a config there can disable audits or ignore findings, which zizmor then never reports, so they would be neither in ASH's results nor counted as suppressed.config_fileset by an ASH config inside a git checkout or the scanned directory is ignored with a warning, and an MCP client cannot set it at all (seeonline_auditsbelow). A configured file that does not exist fails the scan rather than being ignored. The file may be inside the scanned tree, but the scanned repository decides what is at such a path, so one there that leads out of the tree through a symbolic link fails the scan withERRORand zizmor is not run. zizmor quotes the offending text of a config it cannot parse in its error, and that error reaches ASH's log and the scanner's stderr log, so do not point this at a file holding anything else.honor_inline_ignores: when true, zizmor applies its own# zizmor: ignore[...]comments and theignorerules of an operator'sconfig_file. Off by default; see "Suppressions". Honored only when the operator sets it.tool_version: the pip-style constraint for installing zizmor. A value that is not a valid constraint is ignored with a warning and the default is used. It decides which zizmor is installed and which one onPATHis accepted, so a value other than the default is honored only when the operator sets it (--config-overrides, or an ASH config file outside every git checkout and the scanned directory); set by an ASH config inside one, it is ignored with a warning.install_timeout: seconds allowed foruv tool installof zizmor.scan_timeout: seconds zizmor may run; past it zizmor is killed and the scanner reportsERROR.severity_threshold: this scanner's override of the global severity threshold.
Network access and tokens¶
ASH always runs zizmor --offline unless online_audits: true. Offline is the
default because zizmor reads a GitHub token from GH_TOKEN, GITHUB_TOKEN or
ZIZMOR_GITHUB_TOKEN by itself, and a token that happens to be in a CI job's
environment is not permission to use it. While offline, ASH removes those three
variables from zizmor's environment.
online_audits: true drops zizmor --offline and lets those variables reach zizmor
unchanged, which enables the audits that query the GitHub API (for example
known-vulnerable actions and impostor commits). It is honored only when the
operator sets it, with --config-overrides or an ASH config file outside every
git checkout and the scanned directory; set by a config inside one, it is ignored
with a warning and zizmor runs offline without the tokens. An MCP client cannot
configure zizmor at all: its whole section, /scanners/zizmor/**, is denied to a
client whatever a profile's denied_paths says, as for every scanner that can send
data off the host. ASH never
reads, copies or logs the token and never puts it on zizmor's command line.
ASH's own offline mode (ASH_OFFLINE=true or ash scan --offline) overrides
online_audits. Under --sandbox, zizmor never gets the token: the sandbox
passes no GH_TOKEN, GITHUB_TOKEN or ZIZMOR_GITHUB_TOKEN, so online audits
cannot authenticate. It gets a network only when the operator lists zizmor in
sandbox.network_scanners, and never when the scan is offline.
ZIZMOR_CONFIG, ZIZMOR_OFFLINE and ZIZMOR_NO_ONLINE_AUDITS are always
removed from zizmor's environment, so a scan's result depends on the repository
and the ASH config rather than on the shell it ran in. Use config_file instead
of ZIZMOR_CONFIG.
Severity mapping¶
zizmor rates each finding with a severity and a confidence. ASH combines them:
| zizmor severity | confidence Medium or High | confidence Low |
|---|---|---|
| High | HIGH | MEDIUM |
| Medium | MEDIUM | LOW |
| Low | LOW | INFO |
| Informational | INFO | INFO |
zizmor's scale stops at High, so no zizmor finding is CRITICAL. A low-confidence
finding is one zizmor expects to be wrong some of the time, so it is reported one
band lower. The SARIF level is set to match (HIGH error, MEDIUM warning, LOW
note, INFO none), and zizmor's own values stay on the result as
properties["zizmor/severity"] and properties["zizmor/confidence"]. A finding
missing either value keeps zizmor's own level.
With the default MEDIUM threshold, the low-confidence artipacked finding
(Medium, Low) is reported LOW and does not fail the scan on its own.
Suppressions¶
Rule IDs are zizmor's audit names with a zizmor/ prefix, for example
zizmor/template-injection. ASH suppressions by rule, path and line all apply:
global_settings:
suppressions:
- rule_id: zizmor/dangerous-triggers
path: .github/workflows/release.yml
line_start: 3
reason: "Runs only on tags pushed by maintainers"
zizmor's own ignore comments
(# zizmor: ignore[template-injection]) are not honored by default. Such a comment
makes zizmor drop the finding before ASH sees it, so it would be neither reported
nor counted as suppressed; ASH runs zizmor --no-ignores, which also turns off
the ignore rules of an operator's config_file (its other settings still apply).
To honor both, the operator sets honor_inline_ignores: true with
--config-overrides or in an ASH config file outside every git checkout and the
scanned directory; set by an ASH config inside one, it is ignored with a warning. A zizmor.yml in
the scanned repository is never read. Package-scoped and symbol-scoped suppressions do not apply: zizmor reports
no packages, and a workflow has no functions or classes.
Installation¶
ash dependencies install installs zizmor with uv tool install, the newest
release within tool_version, and the ASH container image ships zizmor 1.30.1. ASH also uses any zizmor already on PATH whose
zizmor --version satisfies tool_version, which covers the nix flake (nixpkgs
supplies zizmor), Homebrew, and cargo install. Offline, ASH does not try to
install zizmor; install it first.
zizmor is MIT licensed.