feat(zizmor): severity-tiered SARIF gate (opt-in, advisory default) - #183
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Security Review — PR #183Tasks
|
|
Claude finished @kyle-sexton's task in 4m 32s —— View job Code Review — PR #183
SummaryThis is a clean, well-executed port of the SecurityProvenance pinning is strong. The SARIF schema validation checks Unknown SARIF levels fail closed. The provenance validator requires every result to have Token lifecycle is correct. The GH token is saved to a local variable, Annotation injection protection matches Path traversal in Correctness
Annotation cap vs. blocking count.
The renderer roundtrip is stable. Minor observations (no change required)
CLAUDE.md checklist
VerdictShip. The implementation is correct, the security properties are well-reasoned and tested, and the advisory default preserves backward compatibility. The observations above are informational — none warrant a blocking change. |
|
Claude finished @kyle-sexton's task in 3m 33s —— View job Security Review — PR #183Tasks
FindingsThe overall design is well-hardened. The annotation injection defenses (base64 transport, Two findings, both suggestions: SUGGESTION — Confidence: Medium
ids="$(jq -r --argjson levels "$blocking_levels" '
[.runs[0].results[] | select(.level as $l | $levels | index($l)) | .ruleId] | unique | join(", ")
' "$ZIZMOR_SARIF")"
echo "::error::zizmor reported $blocking_count finding(s) at or above severity $FAIL_ON_SEVERITY: $ids"The In practice, zizmor's rule IDs are hardcoded constants in the binary (verified by SHA-256), so no attacker-controlled content reaches SUGGESTION — Confidence: High
args=(
--format=sarif
"--persona=$PERSONA"
...
)All other sensitive string inputs ( No findings related to: trigger misuse, permission widening, secret exposure, path traversal (normalize_sarif_uri is solid), supply-chain (SHA-pinned at both layers), or annotation injection via message/file paths (base64 + escape chain is correct). |
|
Claude finished @kyle-sexton's task in 3m 19s —— View job Security Review — PR #183
SummaryNo CRITICAL or IMPORTANT findings. The implementation is well-hardened: trigger is SUGGESTION — Confidence: Medium
ids="$(jq -r ... '[.ruleId] | unique | join(", ")' "$ZIZMOR_SARIF")"
echo "::error::zizmor reported $blocking_count finding(s) at or above severity $FAIL_ON_SEVERITY: $ids"The SARIF schema check (guard L36–L53) validates In practice this is theoretical: zizmor's rule IDs are hardcoded constants in the SHA-256-verified binary, so no external data reaches them. The risk is still worth closing — SUGGESTION — Confidence: High
args=(
--format=sarif
"--persona=$PERSONA"
...
)
Checks with no findings
|
|
Claude finished @kyle-sexton's task in 2m 37s —— View job Code Review — PR #183
SummaryThis is a clean, well-tested port of the SecurityTrigger and permissions are correct. Token lifecycle is correct. Binary verification chain. Download → Provenance schema validation is fail-closed. Annotation injection chain is correct. jq base64-encodes all string fields before crossing the pipe boundary;
case "$PERSONA" in
regular|pedantic|auditor) ;;
*)
echo '::error::zizmor persona must be regular, pedantic, or auditor.'
exit 2
;;
esacCorrectnessSeverity mapping is correct. The SARIF level → blocking_levels mapping:
This matches zizmor's documented level mapping (error=high, warning=medium, note=low/info).
jq Annotation cap vs. blocking count. Renderer is stable. Test coverage17 guard cases span: all severity × level combinations, infrastructure failures (malformed JSON, wrong name, wrong version, unknown level, nonzero exit code, bad severity), advisory annotation smoke test, and located-annotation with workspace resolution. The CLAUDE.md checklist
VerdictShip. The two suggestions above ( |
Bring zizmor onto the constellation's SARIF-guard pattern (as used by
osv-scanner + govulncheck): run `--format=sarif`, parse it in a bash guard
that emits annotations for every finding and exits nonzero when any finding's
severity is at or above a caller-chosen threshold.
- New input `fail-on-severity` (never|low|medium|high, default `never`).
Default preserves today's advisory behavior. `fail-on-findings: true` kept
as a back-compat alias (== `fail-on-severity: low`).
- SARIF level mapping (source- and empirically-verified at zizmor 1.27.0):
high->error, medium->warning, low|informational->note. Guard blocks on the
level set for the threshold; `never`->{} advisory.
- New `.github/scripts/zizmor-sarif-guard.sh` reuses osv-scan-guard.sh's
hardened annotation machinery (base64/escape/normalize_sarif_uri path
defense), parameterized by level. `render-zizmor-sarif-guard.cjs` +
`# BEGIN/END GENERATED ZIZMOR SARIF GUARD` markers materialize it into
zizmor.yml (mirrors render-osv-scan-guard.cjs; `--check` sync-verified).
- `zizmor-sarif-guard.test.sh` (17 cases) + revised static
`zizmor-native.test.cjs`; wired into ci.yml + selector-conformance.yml.
- README zizmor section corrected (blocking now available; only SARIF *upload*
to code scanning stays deferred).
No code-scanning, no security-events:write, no org-wide ruleset change — gating
rides the caller's existing ci-status lane when opted in.
Verified: zizmor-sarif-guard.test.sh 17/17; node --test 5/5 (zizmor-native);
render --check in sync; shellcheck + shfmt clean; actionlint clean (YAML/expr).
Independent fresh-context security review was in flight at commit time — to be
reconciled before the PR is opened.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
- biome: render-zizmor-sarif-guard.cjs + zizmor-native.test.cjs to the repo's tab indentation (formatting only; rendered guard output unchanged, --check in sync). - exec-bit: zizmor-sarif-guard.test.sh carries a shebang, so mark it executable (100755), matching govulncheck-sarif-guard.test.sh. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Prior commit applied biome's DEFAULT config (tabs), which the repo's fixtures/typescript/good/biome.json overrides to 2-space and .editorconfig mandates as spaces. Reformat both .cjs with the repo biome config: the longer zizmor marker string wraps to a continuation line and a multi-line assert.match collapses inline. Rendered guard output unchanged (render --check in sync); no tabs (editorconfig clean); node --test 5/5. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
5fac354 to
017493d
Compare
|
Claude finished @kyle-sexton's task in 2m 23s —— View job Code Review — PR #183
SummaryThe latest commit ( SecurityTrigger and permissions are correct. Token lifecycle is correct. Binary verification chain. curl → Provenance schema validation is fail-closed. The Annotation injection chain is correct. jq base64-encodes all string fields before crossing the pipe boundary; All Two open suggestions (carried from prior reviews — not blocking)SUGGESTION — Confidence: Medium
ids="$(jq -r ... '[.ruleId] | unique | join(", ")' "$ZIZMOR_SARIF")"
echo "::error::zizmor reported $blocking_count finding(s) at or above severity $FAIL_ON_SEVERITY: $ids"
Fix: move SUGGESTION — Confidence: High
args=(
--format=sarif
"--persona=$PERSONA" # ← no case guard
...
)Every other string input is validated before use ( case "$PERSONA" in
regular|pedantic|auditor) ;;
*)
echo '::error::zizmor persona must be regular, pedantic, or auditor.'
exit 2
;;
esacCorrectnessSeverity mapping is correct.
This matches zizmor's documented level mapping (error=high, warning=medium, note=low/info). ✅
Annotation cap vs. blocking count. Renderer roundtrip is stable. Test coverage17 guard cases cover all severity × level combinations, infrastructure failures (malformed JSON, wrong tool name, wrong version, unknown level, nonzero exit code, bad severity), advisory annotation smoke test, and located-annotation with workspace resolution. Both CLAUDE.md checklist
VerdictShip. The two suggestions above ( |
|
Claude finished @kyle-sexton's task in 2m 17s —— View job Security Review — PR #183
SummaryNo CRITICAL or IMPORTANT findings. The implementation is well-hardened throughout. The trigger is Two SUGGESTION-level findings below. SUGGESTION — Confidence: Medium
ids="$(jq -r --argjson levels "$blocking_levels" '
[.runs[0].results[] | select(.level as $l | $levels | index($l)) | .ruleId] | unique | join(", ")
' "$ZIZMOR_SARIF")"
echo "::error::zizmor reported $blocking_count finding(s) at or above severity $FAIL_ON_SEVERITY: $ids"The provenance schema (L43–49) validates In practice this is theoretical: the binary is SHA-256 verified before execution, so its ruleId constants cannot be attacker-controlled. However, SUGGESTION — Confidence: High
args=(
--format=sarif
"--persona=$PERSONA" # no prior case-guard
...
)
case "$PERSONA" in
regular|pedantic|auditor) ;;
*)
echo '::error::zizmor persona must be regular, pedantic, or auditor.'
exit 2
;;
esacChecks with no findings
|
…#225) Adopt the new zizmor SARIF-guard gating shipped in ci-workflows #183. The zizmor lane now runs `--format=sarif` through the in-lane guard and **blocks on any high-severity (SARIF `error`) workflow-security finding** — template-injection, dangerous-triggers, unpinned-uses, etc. — while lower severities still only annotate. Gating rides the existing required `ci-status` lane: no code-scanning, no `security-events` grant, no org-wide ruleset change. ## Changes - `ci.yml`: bump the `zizmor.yml` pin to `31a5b76` and set `fail-on-severity: high`; comment updated. - `components/runner-policy/policy.json`: register the new runner-input contract for `zizmor.yml@31a5b76` with `fail-on-severity` in `allowedInputs` — `runner-policy` would otherwise reject the new pin + input (its lane is in `ci-status`). ## Safety Pre-flight `zizmor --persona=regular` (online) on this repo → **0 high findings** (3 notes: 2 `adhoc-packages`, 1 `use-trusted-publishing`). The `high` gate does not block existing workflows. `lint:runner-policy` + `test:runner-policy` (227/227) pass locally. ## Related No related issue: rolls out the consumer-side opt-in for the gating capability added in melodic-software/ci-workflows#183. The 3 remaining note-level findings are a separate follow-up (documented-ignore / OIDC trusted-publishing), below this gate's threshold. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Bring zizmor onto the constellation's SARIF-guard pattern (as used by
osv-scanner+govulncheck): run--format=sarif, parse it in a bash guard that emits annotations for every finding and exits nonzero when any finding's severity is at or above a caller-chosen threshold. Lets a consumer make high-severity workflow-security findings BLOCK via their existingci-statuslane — no code-scanning, nosecurity-events: write, no org-wide ruleset change.What changed
fail-on-severity(never|low|medium|high, defaultnever— preserves today's advisory behavior).fail-on-findings: truekept as a back-compat alias (== fail-on-severity: low).--format=github→--format=sarif; new.github/scripts/zizmor-sarif-guard.shreusesosv-scan-guard.sh's hardened annotation machinery (base64/escape/normalize_sarif_uripath defense), parameterized by level. Level mapping (source- + empirically-verified at zizmor 1.27.0): high→error, medium→warning, low|info→note.render-zizmor-sarif-guard.cjs+# BEGIN/END GENERATED ZIZMOR SARIF GUARDmarkers materialize the guard intozizmor.yml(mirrorsrender-osv-scan-guard.cjs;--checksync-verified).zizmor-sarif-guard.test.sh(17 cases) + revisedzizmor-native.test.cjs; wired intoci.yml+selector-conformance.yml. README zizmor section corrected.Verification
zizmor-sarif-guard.test.sh17/17;node --test5/5 (zizmor-native);render --checkin sync; shellcheck + shfmt clean; actionlint clean.Related
No related issue: proactive constellation hygiene/security improvement. Consumer opt-in (e.g.
standardsatfail-on-severity: high) follows in each consumer's own PR.