Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,112 @@ jobs:

[[ "${XFAIL}" == "true" ]] || { >&2 echo "expected step to fail"; exit 1; }

selftest-checks:
name: "TEST: Checks with ${{ matrix.findings }} findings"
if: github.actor != 'dependabot[bot]' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
strategy:
matrix:
include:
- findings: 0
check-name: zizmor selftest
fixture: checks-clean.yml
- findings: 51
check-name: zizmor selftest (xfail)
fixture: checks-findings.yml
runs-on: ubuntu-slim
permissions:
contents: read
checks: write
env:
CHECK_NAME: ${{ matrix.check-name }}
CHECK_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: $/
id: zizmor
continue-on-error: true
with:
advanced-security: false
checks: true
internal-checks-name: ${{ env.CHECK_NAME }}
inputs: test/assets/${{ matrix.fixture }}
online-audits: false
min-severity: high

- name: Assert check results and PR association
env:
GH_TOKEN: ${{ github.token }}
FINDINGS: ${{ matrix.findings }}
OUTCOME: ${{ steps.zizmor.outcome }}
PR_NUMBER: ${{ github.event.pull_request.number || 0 }}
run: |
expected=success
if [[ "${FINDINGS}" -gt 0 ]]; then expected=failure; fi
[[ "${OUTCOME}" == "${expected}" ]]
# GitHub replaces details_url on checks created with GITHUB_TOKEN.
# The API defaults to the latest check per name; each case has its own name.
gh api "repos/${GITHUB_REPOSITORY}/commits/${CHECK_SHA}/check-runs?per_page=100" \
| jq -e --arg name "${CHECK_NAME}" \
--arg conclusion "${expected}" --argjson count "${FINDINGS}" \
--argjson pr "${PR_NUMBER}" '
[.check_runs[] | select(.name == $name)] |
length == 1 and (.[0] | .status == "completed" and
.conclusion == $conclusion and .output.annotations_count == $count and
(if $pr == 0 then true else
.pull_requests | any(.number == $pr) end) and
(.output.summary | contains("View workflow run and logs")) and
(if $count == 0 then .output.title == "No findings"
else .output.text | contains("template-injection") end))'

selftest-checks-invalid-xfail:
name: "TEST: Checks rejects incompatible modes"
strategy:
matrix:
mode: [advanced-security, annotations]
runs-on: ubuntu-slim
steps:
- uses: $/
id: zizmor
continue-on-error: true
with:
checks: true
internal-checks-name: zizmor selftest (invalid xfail)
advanced-security: ${{ matrix.mode == 'advanced-security' }}
annotations: ${{ matrix.mode == 'annotations' }}

- name: Assert failure
env:
OUTCOME: ${{ steps.zizmor.outcome }}
run: test "${OUTCOME}" = failure

selftest-checks-permission-xfail:
name: "TEST: Checks requires write permission"
runs-on: ubuntu-slim
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: $/
id: zizmor
continue-on-error: true
with:
advanced-security: false
checks: true
internal-checks-name: zizmor selftest (permission xfail)
inputs: test/assets/checks-clean.yml
online-audits: false

- name: Assert failure
env:
OUTCOME: ${{ steps.zizmor.outcome }}
run: test "${OUTCOME}" = failure

selftest-fail-on-no-inputs-xfail:
name: "TEST: 'fail-on-no-inputs: true' causes failure when no inputs are collected"
runs-on: ubuntu-latest
Expand Down Expand Up @@ -227,6 +333,9 @@ jobs:
- selftest-plain-gha-hazmat-offline-audits-xfail
- selftest-annotations
- selftest-annotations-advanced-security-exclusive-xfail
- selftest-checks
- selftest-checks-invalid-xfail
- selftest-checks-permission-xfail
- selftest-fail-on-no-inputs-xfail
- selftest-fail-on-no-inputs-disabled
- selftest-output-file-output-is-present-when-advanced-security
Expand All @@ -240,3 +349,4 @@ jobs:
uses: re-actors/alls-green@b5b5b37504aa4183270bd3d855c52a67f212be35 # v1.3.0
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: selftest-checks
64 changes: 60 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Run [`zizmor`] from GitHub Actions!
- [`token`](#token)
- [`advanced-security`](#advanced-security)
- [`annotations`](#annotations)
- [`checks`](#checks)
- [`internal-checks-name`](#internal-checks-name)
- [`color`](#color)
- [`config`](#config)
- [`fail-on-no-inputs`](#fail-on-no-inputs)
Expand Down Expand Up @@ -235,7 +237,8 @@ which uses the newest `zizmor` release this action knows about.
*Default*: `${{ github.token }}`

`token` is the GitHub token to use for accessing the GitHub REST API
during online audits.
during online audits and Checks reporting. When `checks: true`, this token
must have `checks: write` permission, even if online audits are disabled.

### `advanced-security`

Expand Down Expand Up @@ -270,6 +273,50 @@ for GitHub annotations to create annotations for findings.
> also set `advanced-security: false`. The action will refuse to run
> if you do not do this.

This option is also incompatible with `checks: true`.

### `checks`

*Default*: `false`

`checks` displays findings in a GitHub check, supporting more findings than
[workflow annotations](#annotations). The action fails when zizmor reports
findings or encounters an error, or when results cannot be published.

Set `advanced-security: false` and leave `annotations: false` when enabling
Checks reporting. Add `checks: write` to the job's permissions:

```yaml
permissions:
contents: read
checks: write
```

Then add these inputs to the zizmor action step:

```yaml
with:
advanced-security: false
checks: true
```

Requires zizmor v1.6.0 or later.

> [!IMPORTANT]
> Fork and Dependabot pull requests normally receive a read-only token,
> even when the workflow requests `checks: write`. Use plain output or
> [workflow annotations](#annotations) for those runs.

### `internal-checks-name`

*Default*: `zizmor`

> [!WARNING]
> Most users should not override this value.

When used with `checks: true`, this sets the name of the Check as it appears in
GitHub's Checks API. Typical usage does not require users to set this.

### `color`

*Default*: `true`
Expand Down Expand Up @@ -316,14 +363,17 @@ The following table summarizes the permissions required and when:
| Permission | Description | Required when? |
| ---------- | ----------- | --------------- |
| `security-events: write` | Required to upload results to [Advanced Security]. | When `advanced-security: true` (the default). |
| `checks: write` | Required to publish a check run and its annotations. | When `checks: true`. |
| `contents: read` | Required to read the contents of the repository. | When `advanced-security: true` *and* the parent repository is private. |
| `actions: read` | Required to read the actions of the repository. | When `advanced-security: true` *and* the parent repository is private. |

Or, as a decision tree:

```mermaid
graph TD
A["Are you using Advanced Security (the default)?"] -->|No| B@{ shape: diamond, label: "permissions: {}"}
A["Are you using Advanced Security (the default)?"] -->|No| F[Are you using Checks?]
F -->|No| B@{ shape: diamond, label: "permissions: {}"}
F -->|Yes| G@{ shape: diamond, label: "checks: write"}
A -->|Yes| C[Is your repository public?]
C -->|Yes| D@{ shape: diamond, label: "security-events: write"}
C -->|No| E@{shape: diamond, label: "actions: read
Expand All @@ -343,6 +393,12 @@ If you see this error, it _probably_ means that you are running the action
from a self-hosted runner without Python installed. Install Python onto the
runner, or make sure it is on `PATH` by the time this action runs.

### "Checks reporting requires jq" or "Checks reporting requires curl >= 7.76.0"

Checks reporting requires `jq` and `curl` v7.76.0 or later. If you see either
error on a self-hosted runner, install the missing tool and ensure it is on
`PATH` before running the action.

### Changes introduce security alerts but no PR checks are shown

> [!NOTE]
Expand All @@ -368,9 +424,9 @@ If you hit this behavior, you have a few options:
but you **must** configure it manually — `zizmor-action` cannot do
it for you.
2. Set `advanced-security: false` and use another output format, like
[annotations](#annotations) or the default ("plain") console format
[checks](#checks), [annotations](#annotations), or the default ("plain") console format
(which you get by default when you set `advanced-security: false`).
With either of these approaches you lose the stateful tracking and triage
With these approaches you lose the stateful tracking and triage
of Advanced Security, but you'll also avoid this issue.

If you choose to switch to annotations, please keep in mind
Expand Down
37 changes: 29 additions & 8 deletions action.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,22 @@ if [[ "${GHA_ZIZMOR_ADVANCED_SECURITY}" == "true" && "${GHA_ZIZMOR_ANNOTATIONS}"
die "If you meant to enable 'annotations: true', you must explicitly set 'advanced-security: false'"
fi

if [[ "${GHA_ZIZMOR_CHECKS:-false}" == "true" ]]; then
[[ "${GHA_ZIZMOR_ADVANCED_SECURITY}" != "true" && "${GHA_ZIZMOR_ANNOTATIONS}" != "true" ]] \
|| die "'checks: true' requires 'advanced-security: false' and 'annotations: false'"
installed jq || die "Checks reporting requires jq"
installed curl || die "Checks reporting requires curl >= 7.76.0"
[[ -n "${GHA_ZIZMOR_TOKEN}" ]] || die "Checks reporting requires a token with 'checks: write' permission"
[[ -n "${GHA_ZIZMOR_INTERNAL_CHECKS_NAME}" ]] || die "'internal-checks-name' must not be empty"
fi

if [[ "${GHA_ZIZMOR_ADVANCED_SECURITY}" == "true" ]]; then
arguments+=("--format=sarif")
output "sarif-file" "${output}"
elif [[ "${GHA_ZIZMOR_ANNOTATIONS}" == "true" ]]; then
arguments+=("--format=github")
elif [[ "${GHA_ZIZMOR_CHECKS:-false}" == "true" ]]; then
arguments+=("--format=json-v1")
fi

[[ -n "${GHA_ZIZMOR_COLLECT}" ]] && arguments+=("--collect=${GHA_ZIZMOR_COLLECT}")
Expand All @@ -85,6 +96,12 @@ read -r requirement _ < "${lockfile}"
[[ "${requirement}" == zizmor==* ]] || die "Missing zizmor pin in ${lockfile}"
zizmor_version="${requirement#zizmor==}"

if [[ "${GHA_ZIZMOR_CHECKS:-false}" == "true" ]]; then
IFS=. read -r major minor _ <<< "${zizmor_version}"
(( major > 1 || (major == 1 && minor >= 6) )) \
|| die "Checks reporting requires zizmor >= 1.6.0"
fi

# The lock pins every wheel for this version by hash, so `--require-hashes`
# gives us the same guarantee the pinned container digests used to: pip picks
# the wheel matching the runner and verifies it against that set.
Expand Down Expand Up @@ -125,14 +142,18 @@ chmod +x "${zizmor}"
# as one or more flags.
cd "${GITHUB_WORKSPACE}"

# shellcheck disable=SC2086
GH_TOKEN="${GHA_ZIZMOR_TOKEN}" "${zizmor}" \
"${arguments[@]}" \
-- \
${GHA_ZIZMOR_INPUTS} \
| tee "${output}"

exitcode="${PIPESTATUS[0]}"
if [[ "${GHA_ZIZMOR_CHECKS:-false}" == "true" ]]; then
exitcode=0
# shellcheck disable=SC2086
GH_TOKEN="${GHA_ZIZMOR_TOKEN}" "${zizmor}" \
"${arguments[@]}" -- ${GHA_ZIZMOR_INPUTS} > "${output}" || exitcode=$?
bash "${GITHUB_ACTION_PATH}/checks.sh" "${output}" "${exitcode}"
else
# shellcheck disable=SC2086
GH_TOKEN="${GHA_ZIZMOR_TOKEN}" "${zizmor}" \
"${arguments[@]}" -- ${GHA_ZIZMOR_INPUTS} | tee "${output}"
exitcode="${PIPESTATUS[0]}"
fi
dbg "zizmor exited with code ${exitcode}"

if [[ "${exitcode}" -eq 3 ]]; then
Expand Down
23 changes: 22 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ inputs:
default: latest
token:
description: |
The GitHub API token to use for zizmor online-audits (if enabled).
The GitHub API token to use for zizmor online-audits and Checks reporting
(if enabled). Checks reporting requires checks: write permission.
required: false
default: ${{ github.token }}
advanced-security:
Expand All @@ -86,8 +87,24 @@ inputs:

This option is **mutually exclusive** with `advanced-security: true`.
You must explicitly set `advanced-security: false` to enable this option.
It is also mutually exclusive with `checks: true`.
required: false
default: "false"
checks:
description: |
Whether to display zizmor's findings in a GitHub check.

Requires checks: write permission and zizmor >= 1.6.0.
Mutually exclusive with advanced-security and annotations: set both to false.
Findings and publishing errors fail the action.
required: false
default: "false"
internal-checks-name:
description: |
Override the check name for this action's internal self-tests.
Leave this at its default for normal use.
required: false
default: zizmor
config:
description: Path to a custom zizmor configuration file Path (e.g., zizmor.yml).
required: false
Expand Down Expand Up @@ -123,6 +140,10 @@ runs:
GHA_ZIZMOR_ADVANCED_SECURITY: ${{ inputs.advanced-security }}
GHA_ZIZMOR_COLOR: ${{ inputs.color }}
GHA_ZIZMOR_ANNOTATIONS: ${{ inputs.annotations }}
GHA_ZIZMOR_CHECKS: ${{ inputs.checks }}
# Custom checks need the PR head commit to appear on its Checks tab.
GHA_ZIZMOR_CHECKS_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
GHA_ZIZMOR_INTERNAL_CHECKS_NAME: ${{ inputs.internal-checks-name }}
GHA_ZIZMOR_CONFIG: ${{ inputs.config }}
GHA_ZIZMOR_FAIL_ON_NO_INPUTS: ${{ inputs.fail-on-no-inputs }}
shell: bash
Expand Down
Loading
Loading