From 3457a2a4e1bd05d2568058cd691321758c4ef7e2 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 13:27:07 +1000 Subject: [PATCH 1/6] UID2-7011: make zizmor severity floors centrally controllable min_severity and fail_severity now resolve with precedence: explicit caller input > calling repo's ZIZMOR_MIN_SEVERITY / ZIZMOR_FAIL_SEVERITY Actions variable > central default (high / never). Callers should be bare (no with:) so the org-wide rollout can be retuned without touching 50+ caller files: a central default change here ships to every bare caller via the moving v3 tag, and a repo variable flips one repo with no PR at all. Effective values are unchanged for all existing callers: the input defaults move from concrete values to an empty sentinel, and the fallback chain reproduces the old defaults when neither input nor variable is set. Also makes this repo's own caller bare (same behaviour as before via the central defaults) and documents the three-lever precedence in the README. Co-Authored-By: Claude Fable 5 --- .github/workflows/shared-zizmor-scan.yaml | 28 ++++++++++++++++------- .github/workflows/zizmor.yaml | 5 ++-- README.md | 15 ++++++++---- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/.github/workflows/shared-zizmor-scan.yaml b/.github/workflows/shared-zizmor-scan.yaml index 80327428..62fa21b7 100644 --- a/.github/workflows/shared-zizmor-scan.yaml +++ b/.github/workflows/shared-zizmor-scan.yaml @@ -9,11 +9,15 @@ on: min_severity: description: >- Lowest zizmor severity to report (informational|low|medium|high). - Defaults to `high` — the tier that maps to genuinely exploitable findings - in our estate (e.g. attacker-controllable template-injection). Lower to - `low`/`medium` to also surface defense-in-depth/hardening findings. + Precedence: this input if set, else the calling repo's `ZIZMOR_MIN_SEVERITY` + Actions variable, else `high` — the tier that maps to genuinely exploitable + findings in our estate (e.g. attacker-controllable template-injection). + Lower to `low`/`medium` to also surface defense-in-depth/hardening findings. + Prefer leaving this unset in callers: unset callers can be retuned centrally + (change the fallback here — ships to all `@v3` consumers) or per-repo with + no PR at all (set the repo variable). type: string - default: 'high' + default: '' min_confidence: description: >- Lowest zizmor confidence to report (low|medium|high). Keep at `low`: @@ -25,9 +29,13 @@ on: description: >- Block (fail the job) on findings at or above this severity: never|informational|low|medium|high. Independent of the report floor. - Defaults to `never` (non-blocking). Set to `high` to gate on High only. + Precedence: this input if set, else the calling repo's `ZIZMOR_FAIL_SEVERITY` + Actions variable, else `never` (non-blocking). Prefer leaving this unset in + callers: unset callers can be flipped to gating centrally (change the + fallback here — ships to all `@v3` consumers) or per-repo with no PR at all + (set the repo variable). type: string - default: 'never' + default: '' config: description: >- Path to a zizmor config file in the calling repo. NOTE: this FULLY @@ -75,9 +83,13 @@ jobs: env: ZIZMOR_VERSION: ${{ inputs.zizmor_version }} SCAN_PATHS: ${{ inputs.scan_paths }} - MIN_SEVERITY: ${{ inputs.min_severity }} + # Report/gate floors resolve: explicit caller input > calling repo's + # ZIZMOR_* Actions variable (vars resolves against the repo the run + # belongs to, i.e. the caller) > central default. Central default and + # repo variable are both changeable without touching caller files. + MIN_SEVERITY: ${{ inputs.min_severity || vars.ZIZMOR_MIN_SEVERITY || 'high' }} MIN_CONFIDENCE: ${{ inputs.min_confidence }} - FAIL_SEVERITY: ${{ inputs.fail_severity }} + FAIL_SEVERITY: ${{ inputs.fail_severity || vars.ZIZMOR_FAIL_SEVERITY || 'never' }} CONFIG: ${{ inputs.config }} run: | set -euo pipefail diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index 3a598bf3..3f5fb77c 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -14,6 +14,7 @@ permissions: jobs: zizmor: + # Bare call: severity floors inherit from the shared workflow's central + # defaults (report-only, High) and can be overridden per-repo via the + # ZIZMOR_MIN_SEVERITY / ZIZMOR_FAIL_SEVERITY Actions variables — no PR needed. uses: ./.github/workflows/shared-zizmor-scan.yaml - with: - fail_severity: never # report-only for now; set to `high` later to gate on High-severity diff --git a/README.md b/README.md index 134ab0c8..883c1e24 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ The marker goes in the commit message, not the branch name or PR title. This is `shared-zizmor-scan.yaml` runs [zizmor](https://docs.zizmor.sh) over a repo's GitHub Actions workflows to catch workflow-security issues. By default it runs all offline zizmor rules except `unpinned-uses` (disabled in -config — SHA-pinning was declined for UID2), reports **High-severity** findings -(`min_severity`), and is non-blocking (`fail_severity: never`). +config — SHA-pinning was declined for UID2), reports **High-severity** findings, +and is non-blocking. Adopt it by adding a small caller workflow to the target repo: @@ -63,10 +63,17 @@ permissions: jobs: zizmor: uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-zizmor-scan.yaml@v3 - with: - fail_severity: never # report-only; set to `high` to block PRs on High-severity findings ``` +Keep the caller **bare** (no `with:`) so the severity floors stay centrally +controllable. Three levers, in precedence order: + +1. **Explicit `with:` input** in the caller — per-repo pin, needs a PR to change. +2. **Repo Actions variables** `ZIZMOR_MIN_SEVERITY` / `ZIZMOR_FAIL_SEVERITY` — per-repo + override with no PR (`gh variable set ZIZMOR_FAIL_SEVERITY -b high -R `). +3. **Central defaults** in `shared-zizmor-scan.yaml` (report floor `high`, gate + `never`) — one PR here retunes every bare caller via `@v3`. + For one-off false positives in a consuming repo, add an inline `# zizmor: ignore[]` comment on the offending line. See the workflow's input descriptions for `min_severity`, `min_confidence`, `fail_severity`, and `config`. From a34a2009b52854818efef1a107e0f346993a8333 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 14:09:26 +1000 Subject: [PATCH 2/6] UID2-7011: skip green when the default scan finds nothing to scan zizmor exit 3 (no inputs collected) now splits by intent: with the default scan_paths '.', it means the repo genuinely has no GitHub Actions content - legitimate for ruleset-injected runs that target every repo in an org - so the job skips green with a notice in the summary. With caller-specified scan_paths, an empty collection is more likely a typo'd path and keeps failing closed (the fail-open concern from the original review stands). This lets the UnifiedID2 required-workflow ruleset target ~ALL repos instead of maintaining an include-list of repos with workflows. Co-Authored-By: Claude Fable 5 --- .github/workflows/shared-zizmor-scan.yaml | 31 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/shared-zizmor-scan.yaml b/.github/workflows/shared-zizmor-scan.yaml index 62fa21b7..6e92b531 100644 --- a/.github/workflows/shared-zizmor-scan.yaml +++ b/.github/workflows/shared-zizmor-scan.yaml @@ -53,6 +53,9 @@ on: they live) are covered too — not just `.github/workflows`. zizmor honors `.gitignore` when collecting inputs. Ensure the caller's trigger `paths` cover everywhere scannable files live, or changes can slip through. + With the default `.`, a repo with no scannable files skips green + (legitimate for ruleset-injected runs); with explicit paths, an empty + collection fails closed (probable typo). type: string default: '.' zizmor_version: @@ -133,12 +136,32 @@ jobs: set -e # Fail hard on anything that isn't a clean run (0) or a findings run - # (11-14). An errored or empty scan (bad scan_paths, install failure, arg - # error) must NOT pass silently under fail_severity: never — for a security - # control, fail-open is the worst outcome. Dump both streams so the real - # cause is visible. + # (11-14). An errored scan (install failure, arg error) must NOT pass + # silently under fail_severity: never — for a security control, + # fail-open is the worst outcome. Dump both streams so the real cause + # is visible. + # + # Exit 3 ("no inputs collected") splits by intent: when scanning the + # default '.', it just means the repo has no GitHub Actions content — + # legitimate for ruleset-injected runs that target every repo, so skip + # green with a notice. When the caller set scan_paths explicitly, an + # empty collection is more likely a typo'd path, so keep failing closed. case "${code}" in 0|11|12|13|14) : ;; + 3) + if [ "${SCAN_PATHS}" = "." ]; then + echo "No GitHub Actions workflows or composite actions in this repo; nothing to scan." + { + echo "## 🌈 zizmor — GitHub Actions security scan" + echo "" + echo "Nothing to scan: this repo has no GitHub Actions workflows or composite actions. Skipped." + } >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + echo "zizmor collected no inputs from scan_paths='${SCAN_PATHS}' (exit 3); failing." + cat zizmor.log report.txt + exit 1 + ;; *) echo "zizmor did not complete a scan (exit ${code}); failing." cat zizmor.log report.txt From 668f097945444d34557469a2acfdca9f27b54603 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 14:24:37 +1000 Subject: [PATCH 3/6] UID2-7011: drop per-repo Actions-variable overrides Revert the ZIZMOR_MIN_SEVERITY / ZIZMOR_FAIL_SEVERITY vars fallback and the empty-sentinel input defaults: minimal solution preferred - central control via bare callers inheriting the shared defaults is sufficient, and per-repo override support can be added later if staged per-repo gating turns out to need it. Inputs go back to concrete defaults (high / never). Co-Authored-By: Claude Fable 5 --- .github/workflows/shared-zizmor-scan.yaml | 34 ++++++++++------------- .github/workflows/zizmor.yaml | 3 +- README.md | 11 +++----- 3 files changed, 19 insertions(+), 29 deletions(-) diff --git a/.github/workflows/shared-zizmor-scan.yaml b/.github/workflows/shared-zizmor-scan.yaml index 6e92b531..98b069d9 100644 --- a/.github/workflows/shared-zizmor-scan.yaml +++ b/.github/workflows/shared-zizmor-scan.yaml @@ -9,15 +9,14 @@ on: min_severity: description: >- Lowest zizmor severity to report (informational|low|medium|high). - Precedence: this input if set, else the calling repo's `ZIZMOR_MIN_SEVERITY` - Actions variable, else `high` — the tier that maps to genuinely exploitable - findings in our estate (e.g. attacker-controllable template-injection). - Lower to `low`/`medium` to also surface defense-in-depth/hardening findings. - Prefer leaving this unset in callers: unset callers can be retuned centrally - (change the fallback here — ships to all `@v3` consumers) or per-repo with - no PR at all (set the repo variable). + Defaults to `high` — the tier that maps to genuinely exploitable findings + in our estate (e.g. attacker-controllable template-injection). Lower to + `low`/`medium` to also surface defense-in-depth/hardening findings. + Prefer leaving this unset in callers: bare callers inherit this default, + so the whole estate can be retuned by changing it here (ships to all + `@v3` consumers) without touching caller files. type: string - default: '' + default: 'high' min_confidence: description: >- Lowest zizmor confidence to report (low|medium|high). Keep at `low`: @@ -29,13 +28,12 @@ on: description: >- Block (fail the job) on findings at or above this severity: never|informational|low|medium|high. Independent of the report floor. - Precedence: this input if set, else the calling repo's `ZIZMOR_FAIL_SEVERITY` - Actions variable, else `never` (non-blocking). Prefer leaving this unset in - callers: unset callers can be flipped to gating centrally (change the - fallback here — ships to all `@v3` consumers) or per-repo with no PR at all - (set the repo variable). + Defaults to `never` (non-blocking). Prefer leaving this unset in callers: + bare callers inherit this default, so gating can be introduced centrally + by changing it here (ships to all `@v3` consumers) without touching + caller files. type: string - default: '' + default: 'never' config: description: >- Path to a zizmor config file in the calling repo. NOTE: this FULLY @@ -86,13 +84,9 @@ jobs: env: ZIZMOR_VERSION: ${{ inputs.zizmor_version }} SCAN_PATHS: ${{ inputs.scan_paths }} - # Report/gate floors resolve: explicit caller input > calling repo's - # ZIZMOR_* Actions variable (vars resolves against the repo the run - # belongs to, i.e. the caller) > central default. Central default and - # repo variable are both changeable without touching caller files. - MIN_SEVERITY: ${{ inputs.min_severity || vars.ZIZMOR_MIN_SEVERITY || 'high' }} + MIN_SEVERITY: ${{ inputs.min_severity }} MIN_CONFIDENCE: ${{ inputs.min_confidence }} - FAIL_SEVERITY: ${{ inputs.fail_severity || vars.ZIZMOR_FAIL_SEVERITY || 'never' }} + FAIL_SEVERITY: ${{ inputs.fail_severity }} CONFIG: ${{ inputs.config }} run: | set -euo pipefail diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index 3f5fb77c..b7306d2b 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -15,6 +15,5 @@ permissions: jobs: zizmor: # Bare call: severity floors inherit from the shared workflow's central - # defaults (report-only, High) and can be overridden per-repo via the - # ZIZMOR_MIN_SEVERITY / ZIZMOR_FAIL_SEVERITY Actions variables — no PR needed. + # defaults (report-only, High), so org-wide retunes are a single change there. uses: ./.github/workflows/shared-zizmor-scan.yaml diff --git a/README.md b/README.md index 883c1e24..669faf3e 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,10 @@ jobs: ``` Keep the caller **bare** (no `with:`) so the severity floors stay centrally -controllable. Three levers, in precedence order: - -1. **Explicit `with:` input** in the caller — per-repo pin, needs a PR to change. -2. **Repo Actions variables** `ZIZMOR_MIN_SEVERITY` / `ZIZMOR_FAIL_SEVERITY` — per-repo - override with no PR (`gh variable set ZIZMOR_FAIL_SEVERITY -b high -R `). -3. **Central defaults** in `shared-zizmor-scan.yaml` (report floor `high`, gate - `never`) — one PR here retunes every bare caller via `@v3`. +controllable: an explicit `with:` input pins a repo (and needs a per-repo PR to +change), while bare callers inherit the central defaults in +`shared-zizmor-scan.yaml` (report floor `high`, gate `never`) — one PR here +retunes every bare caller via `@v3`. For one-off false positives in a consuming repo, add an inline `# zizmor: ignore[]` comment on the offending line. See the workflow's input From 3b245b51e376563a113f7717c76815c3575ebe42 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 14:30:45 +1000 Subject: [PATCH 4/6] UID2-7011: trim comments Co-Authored-By: Claude Fable 5 --- .github/workflows/shared-zizmor-scan.yaml | 29 +++++++---------------- .github/workflows/zizmor.yaml | 3 +-- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/.github/workflows/shared-zizmor-scan.yaml b/.github/workflows/shared-zizmor-scan.yaml index 98b069d9..8a6e5aed 100644 --- a/.github/workflows/shared-zizmor-scan.yaml +++ b/.github/workflows/shared-zizmor-scan.yaml @@ -12,9 +12,7 @@ on: Defaults to `high` — the tier that maps to genuinely exploitable findings in our estate (e.g. attacker-controllable template-injection). Lower to `low`/`medium` to also surface defense-in-depth/hardening findings. - Prefer leaving this unset in callers: bare callers inherit this default, - so the whole estate can be retuned by changing it here (ships to all - `@v3` consumers) without touching caller files. + Leave unset in callers so this default stays the single tuning point. type: string default: 'high' min_confidence: @@ -28,10 +26,8 @@ on: description: >- Block (fail the job) on findings at or above this severity: never|informational|low|medium|high. Independent of the report floor. - Defaults to `never` (non-blocking). Prefer leaving this unset in callers: - bare callers inherit this default, so gating can be introduced centrally - by changing it here (ships to all `@v3` consumers) without touching - caller files. + Defaults to `never` (non-blocking). Leave unset in callers so gating + can be introduced by changing this default. type: string default: 'never' config: @@ -51,9 +47,8 @@ on: they live) are covered too — not just `.github/workflows`. zizmor honors `.gitignore` when collecting inputs. Ensure the caller's trigger `paths` cover everywhere scannable files live, or changes can slip through. - With the default `.`, a repo with no scannable files skips green - (legitimate for ruleset-injected runs); with explicit paths, an empty - collection fails closed (probable typo). + With the default `.` an empty repo skips green; with explicit paths an + empty scan fails closed. type: string default: '.' zizmor_version: @@ -130,16 +125,10 @@ jobs: set -e # Fail hard on anything that isn't a clean run (0) or a findings run - # (11-14). An errored scan (install failure, arg error) must NOT pass - # silently under fail_severity: never — for a security control, - # fail-open is the worst outcome. Dump both streams so the real cause - # is visible. - # - # Exit 3 ("no inputs collected") splits by intent: when scanning the - # default '.', it just means the repo has no GitHub Actions content — - # legitimate for ruleset-injected runs that target every repo, so skip - # green with a notice. When the caller set scan_paths explicitly, an - # empty collection is more likely a typo'd path, so keep failing closed. + # (11-14): an errored scan must not pass silently under + # fail_severity: never. Exception: exit 3 ("no inputs collected") on + # the default '.' scan means the repo has no Actions content — skip + # green; with explicit scan_paths it's more likely a typo — fail. case "${code}" in 0|11|12|13|14) : ;; 3) diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml index b7306d2b..934b79fb 100644 --- a/.github/workflows/zizmor.yaml +++ b/.github/workflows/zizmor.yaml @@ -14,6 +14,5 @@ permissions: jobs: zizmor: - # Bare call: severity floors inherit from the shared workflow's central - # defaults (report-only, High), so org-wide retunes are a single change there. + # Bare call: severity floors come from the shared workflow's defaults. uses: ./.github/workflows/shared-zizmor-scan.yaml From af34d880f8bc681a550e6c974e3033f11d7402f6 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 14:53:02 +1000 Subject: [PATCH 5/6] UID2-7011: drop redundant leave-unset guidance from input descriptions Co-Authored-By: Claude Fable 5 --- .github/workflows/shared-zizmor-scan.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/shared-zizmor-scan.yaml b/.github/workflows/shared-zizmor-scan.yaml index 8a6e5aed..9054ca9c 100644 --- a/.github/workflows/shared-zizmor-scan.yaml +++ b/.github/workflows/shared-zizmor-scan.yaml @@ -12,7 +12,6 @@ on: Defaults to `high` — the tier that maps to genuinely exploitable findings in our estate (e.g. attacker-controllable template-injection). Lower to `low`/`medium` to also surface defense-in-depth/hardening findings. - Leave unset in callers so this default stays the single tuning point. type: string default: 'high' min_confidence: @@ -26,8 +25,7 @@ on: description: >- Block (fail the job) on findings at or above this severity: never|informational|low|medium|high. Independent of the report floor. - Defaults to `never` (non-blocking). Leave unset in callers so gating - can be introduced by changing this default. + Defaults to `never` (non-blocking). type: string default: 'never' config: From b1f551cc259108eb2ff70410270d7ed5fdc14355 Mon Sep 17 00:00:00 2001 From: sean wibisono Date: Fri, 3 Jul 2026 14:56:39 +1000 Subject: [PATCH 6/6] UID2-7011: drop bare-caller guidance paragraph from README Co-Authored-By: Claude Fable 5 --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 669faf3e..f96141bd 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,6 @@ jobs: uses: IABTechLab/uid2-shared-actions/.github/workflows/shared-zizmor-scan.yaml@v3 ``` -Keep the caller **bare** (no `with:`) so the severity floors stay centrally -controllable: an explicit `with:` input pins a repo (and needs a per-repo PR to -change), while bare callers inherit the central defaults in -`shared-zizmor-scan.yaml` (report floor `high`, gate `never`) — one PR here -retunes every bare caller via `@v3`. - For one-off false positives in a consuming repo, add an inline `# zizmor: ignore[]` comment on the offending line. See the workflow's input descriptions for `min_severity`, `min_confidence`, `fail_severity`, and `config`.