Skip to content

[ci] zizmor: security-lint GitHub Actions workflows (high-severity gate)#64

Merged
trilamsr merged 2 commits into
mainfrom
worktree-zizmor-gate
May 19, 2026
Merged

[ci] zizmor: security-lint GitHub Actions workflows (high-severity gate)#64
trilamsr merged 2 commits into
mainfrom
worktree-zizmor-gate

Conversation

@trilamsr

@trilamsr trilamsr commented May 19, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Wires zizmor into make ci
as a security-lint gate over .github/workflows/. Gates at
--min-severity=high — the dangerous classes (template injection,
untrusted input in run-blocks, over-broad permissions) — with
lower-confidence findings tracked in FOLLOWUPS for incremental
hardening.

Audit + findings (39 total in v1.25.2 dry-run)

Severity Audit Count Action
High template-injection 1 Fixed inline (release.yml:45)
High cache-poisoning 2 Ignored inline via # zizmor: ignore[cache-poisoning] comments with rationale
Medium ref-version-mismatch 1 Deferred to FOLLOWUPS
Low artipacked 18 Deferred to FOLLOWUPS
Informational template-injection 1 Deferred to FOLLOWUPS

The one dangerous finding (fixed)

release.yml:45 had tag="${{ inputs.tag }}" — textbook
template-injection: GitHub interpolates ${{ }} into the shell
command BEFORE the shell sees it, bypassing shell quoting entirely.
Reworked to pass via env var so the shell quotes the value itself.
zizmor audit-confidence: High; the only finding this PR fixes inline.

Accept-the-risk ignores

Two actions/setup-go@... with: cache: true cache-poisoning warnings
in release.yml (verify + build jobs). Both ignored with rationale
via inline # zizmor: ignore[cache-poisoning] comments on the
cache: true line — inline ignores move with the surrounding code,
so future edits to the workflow don't reactivate the warning at a
shifted line number. .github/zizmor.yml carries project-wide config
only.

Rationale: both are zizmor-confidence-Low; the cache is keyed on
go.sum which is the same trust root the M3 reproducible-rebuild +
cosign + SLSA provenance chain already validates. Reverting
cache: true adds ~30s per release with no integrity improvement.

Deferred (separate FOLLOWUPS row)

The 18 low + 1 medium + 1 info findings are defensive hardening, not
blockers. Cleanest path: lift the gate to --min-severity=medium
then low one tier at a time in focused PRs. Trigger: when the M21
supply-chain audit asks for it OR when external contributor onboarding
raises an Actions-security concern.

Install + invocation

  • Local (macOS): brew install zizmor (1.25.2)
  • Local (Linux): pip install zizmor==1.25.2
  • CI: pinned to v1.25.2 via pip install --user; $HOME/.local/bin appended to $GITHUB_PATH after install so the next step finds the binary
  • Invocation: scripts/zizmor.sh pins --config .github/zizmor.yml --min-severity=high --no-progress

Closes the FOLLOWUPS "CI Actions linters" row. Both halves now landed
— actionlint via #62, zizmor via this PR.

Linked issue(s)

No linked issue.

Release notes

[SECURITY] All GitHub Actions workflows are now security-linted by zizmor in CI; the dry-run surfaced one real template-injection in release.yml's tag-resolution step, fixed inline.

Checklist

  • make zizmor exit 0 (22 ignored, 16 suppressed, 0 high findings)
  • make ci exit 0 end-to-end (rebased onto post-[ci] actionlint: wire as a make ci gate via the go.mod tool stanza #62 main; both actionlint + zizmor in the chain)
  • CI verify job exercised the install step end-to-end (green)
  • Inline ignores survive line-shift (verified by swap from line-pinned .github/zizmor.yml entries)
  • CHANGELOG ## [Unreleased] > ### Security entry added
  • Commits are signed off
  • One real injection vector fixed inline; lower-confidence findings documented and deferred with a triage path

Test plan

  • Dry-run: 39 total findings → after fix + ignores, 0 high
  • CI green on this PR (verify, build, Analyze, CodeQL, pr-lint all pass)
  • After merge: a future PR that introduces ${{ ... }} directly in a run: block (or other high-severity zizmor audit class) fails CI

🤖 Generated with Claude Code

@trilamsr trilamsr force-pushed the worktree-zizmor-gate branch from 6a208c1 to 69297f1 Compare May 19, 2026 03:01
trilamsr and others added 2 commits May 18, 2026 21:03
Wires zizmor (https://github.com/zizmorcore/zizmor) into `make ci`
as a security-lint gate over `.github/workflows/`. Gates at
`--min-severity=high` — the dangerous classes (template injection,
untrusted input in run-blocks, over-broad permissions) — with
lower-confidence findings tracked in FOLLOWUPS for incremental
hardening.

Audit + findings — 39 total in the v1.25.2 dry-run:

  Severity   Audit                    Count
  --------   ------------------------ -----
  High       template-injection       1     ← FIXED INLINE
  High       cache-poisoning          2     ← ignored with rationale
  Medium     ref-version-mismatch     1     ← deferred (cosmetic)
  Low        artipacked               18    ← deferred (defensive)
  Info       template-injection       1     ← deferred (low confidence)

Inline fix (the one dangerous finding):

- `release.yml:45` — `tag="${{ inputs.tag }}"` was a textbook
  template-injection vector: the GitHub-side `${{ }}` interpolates
  the input value into the shell command BEFORE the shell sees it,
  bypassing shell quoting entirely. Reworked to pass via `env:
  INPUT_TAG: ${{ inputs.tag }}` so the shell quotes the value itself
  via `$INPUT_TAG`. zizmor audit-confidence: High; the only finding
  this PR fixes inline.

Accept-the-risk ignores (`.github/zizmor.yml`):

- Two `actions/setup-go@... with: cache: true` cache-poisoning
  warnings in `release.yml` (verify + build jobs). Both are
  zizmor-confidence-Low; the cache is keyed on go.sum which is the
  same trust root the M3 reproducible-rebuild + cosign + SLSA
  provenance chain already validates. Reverting `cache: true` adds
  ~30s per release with no integrity improvement.

Deferred (separate FOLLOWUPS row) — the 18 low + 1 medium + 1 info
findings are defensive hardening, not blockers. Cleanest path: lift
the gate to `--min-severity=medium` then `low` one tier at a time in
focused PRs.

Install + invocation:

- Local (macOS): `brew install zizmor` (1.25.2).
- Local (Linux): `pip install zizmor==1.25.2`.
- CI: pinned to v1.25.2 via `pip install --user` in the verify job.
- `scripts/zizmor.sh` pins the invocation:
  `--config .github/zizmor.yml --min-severity=high --no-progress`.

Closes the zizmor half of the FOLLOWUPS "CI Actions linters" row.
actionlint half remains open (separate PR #62 in flight).

Verification:
- Dry-run: 39 total findings → after fix + ignores, 0 high.
- `make zizmor` exit 0 (22 ignored, 16 suppressed, 0 high).
- `make ci` exit 0 end-to-end (kernelevents test had one transient
  flake on first run; passed on retry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tri Lam <trilamsr@gmail.com>
Adds a `## [Unreleased] > ### Security` entry covering the
user-visible security improvement that lands with this PR: the new
`make zizmor` gate, the inline template-injection fix at
release.yml:45, the two accept-the-risk inline ignores on the
cache-poisoning warnings, and the FOLLOWUPS deferral for the 20
lower-severity findings.

Mirrors the format of the SHA-pin + cosign-tightening entries that
landed alongside the omnibus follow-ups PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Tri Lam <trilamsr@gmail.com>
@trilamsr trilamsr force-pushed the worktree-zizmor-gate branch from e529eed to 47d5f26 Compare May 19, 2026 04:05
@trilamsr trilamsr enabled auto-merge (squash) May 19, 2026 04:09
@trilamsr trilamsr merged commit 07e5cd9 into main May 19, 2026
5 checks passed
@trilamsr trilamsr deleted the worktree-zizmor-gate branch May 19, 2026 04:12
trilamsr added a commit that referenced this pull request May 19, 2026
## What this PR does

Follow-up to #64 (zizmor security-lint gate). Trims comments that
didn't pass the six-months-cold-reader test. The commit was prepared
on the #64 branch but pushed seconds after that PR auto-merged, so it
never made it in — re-applying as a standalone follow-up.

Comments trimmed:

- `scripts/zizmor.sh`: dropped per-flag descriptions in the header
  block — `--no-progress` and `--config` don't need explainers; the
  WHY for `--min-severity=high` stays. Also fixed a stale error-
  message prefix copied from register-lint.
- `.github/zizmor.yml`: 6 lines → 3. Kept the one load-bearing claim
  ("ignores live inline").
- `release.yml` cache-poisoning ignores: 6 lines → 2. Kept the WHY
  (cache keyed on go.sum, trust root M3 already validates); dropped
  the meta-pointer to zizmor.yml (which no longer carries the
  rationale) and the audit-confidence parens.
- `release.yml` INPUT_TAG env comment: 4 lines → 2.

No behavior change. `make ci` exit 0 (one transient kernelevents
flake on first run, passed on retry — same intermittent test seen
in this session's PRs #55, #60, #62, #63, #64).

## Linked issue(s)

_No linked issue._ Follows up #64.

## Release notes

```release-notes
NONE
```

## Checklist

- [x] `make ci` exit 0
- [x] No behavior change; pure documentation hygiene
- [x] Commits are signed off

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tri Lam <trilamsr@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
trilamsr added a commit that referenced this pull request May 19, 2026
#68)

## What this PR does

Two small fixes that turn this session's lessons into code instead of
notes-to-self:

### 1. Deflake `TestJournaldSource_StreamsMockOutput`

The 2s deadline for the mock-journalctl test was tight on loaded
macOS / CI runners — subprocess spawn + fixture read + goroutine
schedule could miss it. The test flaked on 5 of 5 PRs in this
session that exercised the full suite (#55, #60, #62, #63, #64).

Bumped the deadline 2s → 5s. The non-flake budget for "no record
ever emitted" regressions stays well under the deadline; 5s just
absorbs runner jitter without hiding real bugs.

### 2. Warn locally when `shellcheck` isn't on PATH

`actionlint` silently skips run-block shellcheck when shellcheck
isn't on PATH (macOS doesn't ship shellcheck by default). PR #62
passed `make actionlint` locally and then surfaced 4 shellcheck
findings in CI — exactly the failure mode the gate is supposed to
prevent.

`make actionlint` now prints a `WARNING: shellcheck not on PATH;
actionlint will skip run-block shellcheck.` line with `brew
install` / `apt-get install` hints when the binary is missing.
The underlying lint still runs; the warning just makes the gap
visible to the macOS dev.

## Linked issue(s)

_No linked issue._

## Release notes

```release-notes
NONE
```

## Checklist

- [x] `make ci` exit 0 (test passes deterministically with new deadline)
- [x] `make actionlint` shows the warning when shellcheck is uninstalled
(verified locally)
- [x] No behavior change in CI (shellcheck already on Linux runners)
- [x] Commits are signed off

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Tri Lam <trilamsr@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant