diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc5f74f..d794d57e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -338,7 +338,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" cache: npm diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index e8f06bf8..8e205483 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -156,7 +156,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" - name: Audit the locked npm dependencies (install-free) @@ -186,7 +186,7 @@ jobs: with: python-version: "3.14" - name: Set up Node - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "24" - name: Generate the Python engine SBOM (license-complete, hash-locked core runtime) diff --git a/.github/zizmor.yml b/.github/zizmor.yml index 11b76936..6d06249d 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -10,7 +10,8 @@ rules: # A CLA assistant REQUIRES pull_request_target to record signatures + comment on fork PRs. This # workflow never checks out untrusted PR code, pins the third-party action by SHA, and gates on the # immutable comment body / event name — so the usual pull_request_target risk (running attacker - # code with a write token) does not apply. See the SECURITY note in cla.yml. + # code with a write token) does not apply. The action is pinned at ca4a40a7d100 (v2.6.1); its + # archived-upstream status is adjudicated under archived-uses below. - cla.yml # A notice that a SCHEDULED run failed can only be written by something observing that run AFTER # it finishes — no other trigger sees another workflow's conclusion, so `workflow_run` is not a @@ -22,7 +23,9 @@ rules: # tests/test_nightly_notice.py rather than merely asserted here: # * NO checkout, and no third-party actions at all — `test_it_pulls_in_no_third_party_actions` # fails if a `uses:` ever appears. Nothing from the triggering ref is fetched, let alone run. - # That is a STRONGER claim than cla.yml's above, which does check out. + # That is a STRONGER claim than cla.yml's above: cla.yml runs a third-party action with a + # write token — SHA-pinned, so it cannot change under us, but still third-party code — + # whereas this job runs no third-party code at all. # * It only reacts to `workflow_run.event == 'schedule'` # (`test_it_only_reacts_to_scheduled_runs`), so a PR — including a fork PR — has no path to # the job at all. @@ -36,16 +39,76 @@ rules: artipacked: ignore: # artipacked flags a checkout that persists the token's git credential — a risk only if that - # credential can leak into an uploaded artifact. The jobs below do not push and never upload the - # repo/.git tree, so the persisted credential is inert noise here. + # credential can leak into an uploaded artifact. The jobs below never upload the repo/.git tree, + # so the persisted credential is inert noise here. # - # NOT suppressed (fixed at the source instead): release.yml sets persist-credentials: false on both - # checkouts because it DOES upload build artifacts. dependabot-lock-resync.yml persists a + # NOT suppressed (fixed at the source instead): release.yml's checkouts set persist-credentials: + # false, because it DOES upload build artifacts. dependabot-lock-resync.yml persists a # narrowly-scoped App token ON PURPOSE to push the resynced locks (documented inline) and is listed # here for that reason. - benchmark.yml - ci.yml - dependabot-lock-resync.yml - - release-sync-check.yml - security.yml - vuln-metrics.yml + + bot-conditions: + ignore: + # zizmor flags `github.triggering_actor == 'dependabot[bot]'` in the resync job's `if:`, because an + # actor context reflects the LAST actor on the run, not the PR's author. True — and it is why that + # expression is the SECOND half of a conjunction and never the control. The control is the FIRST + # half, `github.event.pull_request.user.login == 'dependabot[bot]'`: the immutable PR author, and + # the exact replacement this audit recommends. A conjunction only NARROWS, so a spoofed actor can + # make this job SKIP, never run. zizmor agrees — it treats `And` as non-dominating and reports this + # at MEDIUM confidence for that reason. + # + # Skipping is safe by design: DEP-1 lock/export sync is independently enforced by security.yml's + # `pip-audit` job, step "Check the lockfile is in sync with pyproject (DEP-1)", which + # `git diff --exit-code`s the re-exports on an unfiltered pull_request trigger and is a REQUIRED + # context. The actor half is an operational self-trigger skip: this job's own App-token push fires + # a `synchronize` whose triggering actor is mefor-lock-resync[bot], a run that sees no Dependabot + # secrets. See the SECURITY MODEL block and the numbered note above that `if:`. + # + # DO NOT TAKE ZIZMOR'S AUTO-FIX. It is classified SAFE, so a bare `zizmor --fix` applies it + # silently: for a `pull_request` trigger it substitutes `github.event.pull_request.user.login`, + # collapsing the conjunction into `A && A` and deleting the self-trigger skip. Nothing in CI would + # catch that — every test and required check stays green while the skip is gone. + # + # The premise this suppression rests on — clause 1 present, conjoined, no `||` — is PINNED by + # tests/test_dep1_lock_resync_lockstep.py rather than merely asserted here. Line-anchored on + # purpose: a file-level entry would also swallow a future actor check added to this file as a SOLE + # gate, which is the shape that IS exploitable. Accepted residual: an in-place rewrite that keeps + # the line count would not re-fire the finding — that is what the test is for. If the line moves, + # RE-ANCHOR it; do not broaden it to the file. + - dependabot-lock-resync.yml:76 + + archived-uses: + ignore: + # contributor-assistant/github-action is ARCHIVED upstream (GitHub API `archived: true`; the date + # is observed from its last push, 2026-03-23, since `archived_at` is null). v2.6.1 is both the last + # tag and the last release, and the pin ca4a40a7d1004f18d9960b404b97e5f30a505a08 equals that tag + # exactly — so there is no future patch to bump to. Recorded as an ACCEPTED RESIDUAL, not a false + # positive: the finding is true. + # + # What the SHA pin buys, and what it does not. It closes the TAMPER axis completely — a bundled JS + # action's pinned SHA fully determines the bytes that run, and a deleted or re-registered namespace + # makes this step FAIL rather than run a stranger's code. It does NOT close the axis this audit is + # about: a flaw in the code already at that SHA will never be patched. + # + # NOT REPLACED, deliberately. `cla` is a REQUIRED status context (.github/required-contexts.txt) + # and it is this JOB's own conclusion, so a broken step blocks every contribution. + # pull_request_target / issue_comment workflows run only from the DEFAULT branch, so a swap cannot + # be exercised on the PR that makes it — it lands on main untested, with approvals at 0 and + # auto-merge armed. On a step holding a write token that any GitHub user can trigger by comment, + # trading a frozen SHA-pinned bundle for a less-reviewed fork is not self-evidently a security win. + # + # WHAT WILL FORCE THE DECISION, and it is not this lint: action.yml at this SHA declares + # `runs.using: node20`, as does the archived repo's HEAD, so no upstream release can ever + # re-declare node24. GitHub removes Node20 in fall 2026. Re-check before then. Full record and the + # replacement candidate: docs/adr/0034 (amendment, 2026-08-01). + # + # Line-anchored on purpose: a file-level entry would silently swallow a SECOND archived action + # added here later. Accepted cost is drift — an inserted line re-fires the finding with no change + # in risk. Re-anchor; do not broaden. Delete this entry when the action is replaced, or if upstream + # un-archives (the audit re-checks that every run). + - cla.yml:44 diff --git a/ci/locks/ci-scanners.lock b/ci/locks/ci-scanners.lock index 471ed1fb..06c3474f 100644 --- a/ci/locks/ci-scanners.lock +++ b/ci/locks/ci-scanners.lock @@ -220,15 +220,15 @@ urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via requests -zizmor==1.5.2 \ - --hash=sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680 \ - --hash=sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173 \ - --hash=sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4 \ - --hash=sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a \ - --hash=sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4 \ - --hash=sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e \ - --hash=sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996 \ - --hash=sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6 \ - --hash=sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30 \ - --hash=sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a \ - --hash=sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b +zizmor==1.28.0 \ + --hash=sha256:0949f57a6d20deeb9c509705afce8de233c166475e25be305985a1fe553c6e0d \ + --hash=sha256:149dba59a8bd2897960ee54c40ae8b5801a71293bad71c8d2913c74ab66a294c \ + --hash=sha256:354e6cb98a15a88593a6f7ac6236b092b83a2aad5c4768ee750f9b3262f668d9 \ + --hash=sha256:6897f02b0d02fd709f5ebc13cd37d97ad464219e0ba86a03f6d86f7777b7b102 \ + --hash=sha256:6d5a300b80bf4c12e9cbe78ffd3874ec3f94b65bea78c994ec18157e2846ece0 \ + --hash=sha256:7b00018cf2cc948c3b3e010c1a1f30fdc001f0d062640469f17e0ef006e74eb7 \ + --hash=sha256:833c360ba5a9c74ca45007b8c79939826fca0c5ed65144fa08f63a7009cef096 \ + --hash=sha256:9819f91f0ef486e4af98a6aacfed23c6b4067fabcecb88d49231228a3d43f821 \ + --hash=sha256:ae2cab67ce713e760e0d1b61ad749d374693ea2b310337aab11cd446748267f3 \ + --hash=sha256:c93b30d211b0b0c38905a8803cc2653fff37de03eb77105302a02f709773bd4a \ + --hash=sha256:ef69d198dcf6835c9b6eea8673cbc5c36f10b3f1b98b51d28266b7e01c3704d4 diff --git a/constraints.lock b/constraints.lock index 457dbc6d..d68ee180 100644 --- a/constraints.lock +++ b/constraints.lock @@ -63,7 +63,7 @@ defusedxml==0.7.1 # pyx12 elementpath==5.1.3 # via xmlschema -fastapi==0.139.2 +fastapi==0.140.4 # via messagefoundry fhir-core==1.1.9 # via fhir-resources @@ -152,7 +152,7 @@ pathspec==1.1.1 # via mypy pluggy==1.6.0 # via pytest -prometheus-client==0.25.0 +prometheus-client==0.26.0 # via messagefoundry protobuf==6.33.6 # via diff --git a/docker/locks/requirements-core.lock b/docker/locks/requirements-core.lock index c59fc7a0..d1c53f74 100644 --- a/docker/locks/requirements-core.lock +++ b/docker/locks/requirements-core.lock @@ -139,9 +139,9 @@ defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 # via messagefoundry -fastapi==0.139.2 \ - --hash=sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e \ - --hash=sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c +fastapi==0.140.4 \ + --hash=sha256:4a040c5d047486a62c497eb6e926a6d6b281efb7f99b8b50288489ba1fa0a468 \ + --hash=sha256:6a1f8ec2f01ae47c96dc991d49660ce4f31f50ed0d7c9df80c5e57037bfe1240 # via messagefoundry h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ @@ -192,9 +192,9 @@ ldap3==2.9.1 \ --hash=sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70 \ --hash=sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f # via messagefoundry -prometheus-client==0.25.0 \ - --hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \ - --hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1 +prometheus-client==0.26.0 \ + --hash=sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b \ + --hash=sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6 # via messagefoundry psutil==7.2.2 \ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ diff --git a/docker/locks/requirements-sqlserver.lock b/docker/locks/requirements-sqlserver.lock index ad3e94e2..2e0855d7 100644 --- a/docker/locks/requirements-sqlserver.lock +++ b/docker/locks/requirements-sqlserver.lock @@ -143,9 +143,9 @@ defusedxml==0.7.1 \ --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 # via messagefoundry -fastapi==0.139.2 \ - --hash=sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e \ - --hash=sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c +fastapi==0.140.4 \ + --hash=sha256:4a040c5d047486a62c497eb6e926a6d6b281efb7f99b8b50288489ba1fa0a468 \ + --hash=sha256:6a1f8ec2f01ae47c96dc991d49660ce4f31f50ed0d7c9df80c5e57037bfe1240 # via messagefoundry h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ @@ -196,9 +196,9 @@ ldap3==2.9.1 \ --hash=sha256:5869596fc4948797020d3f03b7939da938778a0f9e2009f7a072ccf92b8e8d70 \ --hash=sha256:f3e7fc4718e3f09dda568b57100095e0ce58633bcabbed8667ce3f8fbaa4229f # via messagefoundry -prometheus-client==0.25.0 \ - --hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \ - --hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1 +prometheus-client==0.26.0 \ + --hash=sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b \ + --hash=sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6 # via messagefoundry psutil==7.2.2 \ --hash=sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372 \ diff --git a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md index c96d4533..4dfc96d5 100644 --- a/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md +++ b/docs/adr/0034-static-analysis-triage-policy-accepted-risk-register.md @@ -433,3 +433,36 @@ why export sync must be verified with `git diff` and never a raw `diff`. Fixed a **Still open, not done:** `quality-advisory.yml`'s `pipx install ruff` fallback installs *unpinned* ruff instead of failing closed, and `constraints.lock` is `sed`-scraped for a `ruff==` pin that — unlike `packaging==` — has **no PR-time canary test**. Both are recorded here; neither closes a Scorecard alert. + +## Amendment — 2026-08-01: zizmor `archived-uses` on the CLA action (accepted residual) + +Adopting zizmor 1.28.0 (from 1.5.2) turned on audits the old pin could not run. Four of the five +findings against the otherwise-unchanged tree were resolved in-tree or as justified non-findings; this +one is an accepted residual. + +* **Finding.** `warning[archived-uses]`, `.github/workflows/cla.yml:44` — + `contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1`. Upstream is + archived (API `archived: true`, `archived_at: null`; last push 2026-03-23) and v2.6.1 is the final + release. The pin equals that tag's commit exactly, so there is no later patch to move to. +* **Compensating control, and its limit.** The full-SHA pin closes tampering: a bundled JS action's SHA + fully determines the bytes that run, and a vanished namespace fails the step rather than running + someone else's code. It does not close the unpatched-code axis the audit names. +* **Why not replaced now.** `cla` is a required status context and is the job's own conclusion, so a + broken step blocks every PR. `pull_request_target`/`issue_comment` workflows run only from the default + branch, so a replacement cannot be exercised on the PR that makes it — it lands on `main` untested, + with `required_approving_review_count: 0` and auto-merge armed. +* **Hard revisit: before Node20 removal.** `action.yml` at the pin — and at the archived HEAD — declares + `runs.using: node20`. An archived repo can never re-declare node24, so GitHub's fall-2026 Node20 + removal, not this lint, forces fork-or-replace. No firm date is published; treat mid-September 2026 as + the planning date and re-check before then. +* **Contingency, verified 2026-08-01.** There is no canonical successor — the archived README directs + users to fork. The best candidate found is `iainmcgin/cla-github-action`, Apache-2.0, not archived, + last pushed 2026-06-17, 4 stars, single personal maintainer. Note v3.2.0 is an **annotated** tag whose + ref resolves to tag object `07f1588b0cee15f89a489a77704c9d45d39ec0a1`; the commit `uses:` must pin is + `0d27e5a16278d4adb6b0c4b92f08ad27b0a21dc8` (dereferenced and confirmed, not assumed). Adoption is + gated on at least: accounting for the shipped `dist/index.js` (a built bundle that is not + human-reviewable, and a source review does not prove `dist/` was built from it); confirming + `signatures/version1/cla.json` stays format-compatible; verifying the inputs `cla.yml` passes still + exist with the same semantics; a decision on `require-opener-as-author`, which defaults to true and + fails the check; and a rehearsal in a scratch repo. Land in a low-traffic window with a revert + prepared. diff --git a/pyproject.toml b/pyproject.toml index 847114b1..ea18189a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -263,7 +263,7 @@ messagefoundry-tray = "messagefoundry.tray.__main__:main" ci-scanners = [ "bandit==1.9.4", "pip-audit==2.10.1", - "zizmor==1.5.2", + "zizmor==1.28.0", ] # The ADVISORY measurement tools (quality-advisory.yml). Exact where something PARSES the tool's # output, a floor where nothing does: diff --git a/requirements.lock b/requirements.lock index bdd8b27f..5dc2b8f1 100644 --- a/requirements.lock +++ b/requirements.lock @@ -317,9 +317,9 @@ elementpath==5.1.3 \ --hash=sha256:35e98cc2e26332305a49b32eaaebfb7289d1963c88d3989ff5ccb02c46809820 \ --hash=sha256:c46f5e0e36c149b892308843e1e394bdee17ceafbc18cfa39250403dd8475a4e # via xmlschema -fastapi==0.139.2 \ - --hash=sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e \ - --hash=sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c +fastapi==0.140.4 \ + --hash=sha256:4a040c5d047486a62c497eb6e926a6d6b281efb7f99b8b50288489ba1fa0a468 \ + --hash=sha256:6a1f8ec2f01ae47c96dc991d49660ce4f31f50ed0d7c9df80c5e57037bfe1240 # via messagefoundry fhir-core==1.1.9 \ --hash=sha256:61f23a711199d8d74390fd473cc5ddf75a1f4def97fd8e9f59a28bc9d5222261 \ @@ -570,9 +570,9 @@ pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest -prometheus-client==0.25.0 \ - --hash=sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28 \ - --hash=sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1 +prometheus-client==0.26.0 \ + --hash=sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b \ + --hash=sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6 # via messagefoundry protobuf==6.33.6 \ --hash=sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326 \ diff --git a/tests/test_dep1_lock_resync_lockstep.py b/tests/test_dep1_lock_resync_lockstep.py index 6b0240c8..7bd961db 100644 --- a/tests/test_dep1_lock_resync_lockstep.py +++ b/tests/test_dep1_lock_resync_lockstep.py @@ -20,6 +20,8 @@ import re from pathlib import Path +from tests._workflow_contexts import load_workflow + _WORKFLOWS = Path(__file__).resolve().parent.parent / ".github" / "workflows" _GATE = _WORKFLOWS / "security.yml" _RESYNC = _WORKFLOWS / "dependabot-lock-resync.yml" @@ -98,3 +100,22 @@ def test_constraints_lock_is_in_the_set() -> None: "dependabot-lock-resync.yml stopped re-exporting constraints.lock — every Dependabot uv PR " "will be red with no bot-reachable path to green (see this module's docstring)" ) + + +def test_the_resync_gate_keeps_its_immutable_author_conjunct() -> None: + """The zizmor ``bot-conditions`` suppression is sound only while clause 1 is present. + + zizmor flags ``github.triggering_actor`` in this job's ``if:``. That is safe ONLY because it is + conjoined with the immutable PR-author check: a conjunction can only narrow, so a spoofed actor + makes the job skip, never run. Drop clause 1 -- or turn the ``&&`` into ``||`` -- and the residual + actor check becomes a SOLE gate, the exploitable shape. ``.github/zizmor.yml``'s bot-conditions + entry asserts that shape is absent; this is what makes the assertion true rather than asserted. + """ + condition = load_workflow("dependabot-lock-resync.yml")["jobs"]["resync"]["if"] + assert "github.event.pull_request.user.login == 'dependabot[bot]'" in condition, ( + "the immutable author gate is gone; the zizmor bot-conditions suppression is now unsound" + ) + assert "&&" in condition, "the author gate must CONJOIN the actor check, not replace it" + assert "||" not in condition, ( + "a disjunction lets the spoofable actor check alone open the job — the dominating shape" + ) diff --git a/tests/test_zizmor_config_hygiene.py b/tests/test_zizmor_config_hygiene.py new file mode 100644 index 00000000..b7286288 --- /dev/null +++ b/tests/test_zizmor_config_hygiene.py @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 MessageFoundry Organization and contributors +"""Every zizmor suppression must name a workflow that still exists. + +``.github/zizmor.yml`` was asserted by nothing, so its ``release-sync-check.yml`` entry survived that +workflow's deletion (59fbc938, 2026-07-26) and sat there reading as coverage. zizmor's ignore entries +are BASE filenames (``filename.yml[:line[:column]]``) resolved against the scan target, and +zizmor.yml's job runs ``zizmor .github/workflows`` — so the ignore namespace is exactly that +directory's contents, and an entry naming anything else can never match. + +A dead entry is worse than noise: this file's header calls every entry "a REVIEWED, justified +non-finding", so a name that cannot match reads as a reviewed risk that is actually unexamined. +""" + +from __future__ import annotations + +from tests._workflow_contexts import ROOT, WORKFLOWS + +_CONFIG = ROOT / ".github" / "zizmor.yml" + + +def test_every_zizmor_ignore_names_a_live_workflow() -> None: + import yaml + + assert _CONFIG.is_file(), f"{_CONFIG} is missing — this guard cannot pass vacuously" + config = yaml.safe_load(_CONFIG.read_text(encoding="utf-8")) or {} + scanned: list[str] = [] + for audit, body in (config.get("rules") or {}).items(): + for entry in (body or {}).get("ignore") or []: + basename = str(entry).split(":", 1)[0] + scanned.append(f"{audit} -> {entry}") + assert (WORKFLOWS / basename).is_file(), ( + f"{audit}.ignore names {basename}, which is not in .github/workflows/ — the " + "workflow was deleted or renamed and its suppression must go with it" + ) + # Print what was covered: a gate that silently scanned nothing looks identical to a passing one. + print(f"scanned {len(scanned)} zizmor ignore entries: {scanned}") + assert scanned, ( + "parsed zero ignore entries — the config shape changed and this guard went blind" + ) diff --git a/uv.lock b/uv.lock index 8949b977..89351632 100644 --- a/uv.lock +++ b/uv.lock @@ -553,7 +553,7 @@ wheels = [ [[package]] name = "fastapi" -version = "0.139.2" +version = "0.140.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-doc" }, @@ -562,9 +562,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cd/95/d3f0ae10836324a2eab98a52b61210ac609f08200bf4bb0dc8132d32f78a/fastapi-0.139.2.tar.gz", hash = "sha256:333145a6891e9b5b3cfceb69baf817e8240cde4d4588ae5a10bf56ffacb6255e", size = 423428, upload-time = "2026-07-16T15:06:17.912Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/24/da8da082a850c9732dd5a5b3ccdfe51844750e42f83c430b0d8e241eab2e/fastapi-0.140.4.tar.gz", hash = "sha256:4a040c5d047486a62c497eb6e926a6d6b281efb7f99b8b50288489ba1fa0a468", size = 421514, upload-time = "2026-07-27T15:46:48.011Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" }, + { url = "https://files.pythonhosted.org/packages/65/77/2915c0cc8b602f3d611294920171c67ce396b43cda5fa63f3e4b465d635e/fastapi-0.140.4-py3-none-any.whl", hash = "sha256:6a1f8ec2f01ae47c96dc991d49660ce4f31f50ed0d7c9df80c5e57037bfe1240", size = 130958, upload-time = "2026-07-27T15:46:49.175Z" }, ] [[package]] @@ -1126,7 +1126,7 @@ ci-quality = [ ci-scanners = [ { name = "bandit", specifier = "==1.9.4" }, { name = "pip-audit", specifier = "==2.10.1" }, - { name = "zizmor", specifier = "==1.5.2" }, + { name = "zizmor", specifier = "==1.28.0" }, ] [[package]] @@ -1445,11 +1445,11 @@ wheels = [ [[package]] name = "prometheus-client" -version = "0.25.0" +version = "0.26.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, ] [[package]] @@ -2313,18 +2313,18 @@ wheels = [ [[package]] name = "zizmor" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/48/a692769e2bbb62635d55849175849bb9fec2240d0a16e16bd28d8cfe314f/zizmor-1.5.2.tar.gz", hash = "sha256:848f04c0d84b085dfb79c66951404372ebf9b3dd0c73076f1baac83db29e74e4", size = 296921, upload-time = "2025-03-23T15:01:14.821Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/31/f717638faf223776b239040574f67bfdeb2ef3279673c08a099a525c9f4f/zizmor-1.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:03b27c46d87e96a0acdf78190fbb674c174f67ded396c74dadd64d7c4ecba680", size = 4687915, upload-time = "2025-03-23T15:01:06.138Z" }, - { url = "https://files.pythonhosted.org/packages/1f/0a/0a401630e9f98b8660c20a75cfb4995245c738da90d9bc1c3cd709da8a8a/zizmor-1.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a40feabc2c043aca60f9edbd35a676d97d684af5b06ed61a7752869c79b3be30", size = 4443376, upload-time = "2025-03-23T15:01:04.384Z" }, - { url = "https://files.pythonhosted.org/packages/ce/cf/91527ae1e53e3be260545630e740bd34f5dfa566b23360dea5b07d15e4e5/zizmor-1.5.2-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9d40780b19da7901423de80ccce083a8c0d2114e0cf6432aa20d8e60d15e97f6", size = 4601870, upload-time = "2025-03-23T15:00:57.752Z" }, - { url = "https://files.pythonhosted.org/packages/b2/04/4cdec1dff48ef4bb733344568d9d6a8a82cc655581787866cf38d999001a/zizmor-1.5.2-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:df1326fe9f9ddcbe9d862a97571c848276882d0ceeee39e18fcfc9ec5c66cd7b", size = 4513121, upload-time = "2025-03-23T15:01:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/dc/27/7fbb2e2ad2d33de12b15014b762d39b1529b94bcbfebf55b735e67e00bd4/zizmor-1.5.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:0c5f656106c430748858ae459c41c9eed09a1e01e5f42015d80c8fe34740d173", size = 4840803, upload-time = "2025-03-23T15:01:02.222Z" }, - { url = "https://files.pythonhosted.org/packages/4d/80/6061f5d37cadd185c3e921bc20b8fcf34b262cad009911c50010b7e8d3d5/zizmor-1.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e18b06af4208624ef707e7859b07539870ca26b74a89537e1a6bfba2e44fc9a", size = 4590530, upload-time = "2025-03-23T15:01:08.435Z" }, - { url = "https://files.pythonhosted.org/packages/35/61/777a1ac136d8f256d94f2f269baeb11a6874a9241953fd6694fd4eb5751c/zizmor-1.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:506b355c693d40df01bdf26fd1dfcb41801af7b8c51767d4159efdc2655965c4", size = 4525617, upload-time = "2025-03-23T15:01:10.738Z" }, - { url = "https://files.pythonhosted.org/packages/09/46/8ba5dc116afd6105774ba527d66e7ecf635b2fdf56daddd18ef2933201e6/zizmor-1.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8c65b4dbc9690d3f0f5d9756ae0b5ca8d3dfb4f806d82a5195836a3b613ae996", size = 4914859, upload-time = "2025-03-23T15:01:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/df/0e/9fd2a2e3bbc904878c74a4270aef61be8564f88d68a3dc6ebdc3e0ea2b81/zizmor-1.5.2-py3-none-win32.whl", hash = "sha256:dcef697a88983e7ce6948df1603e2325e2cf5c523828af94615e6c432fa8d98a", size = 3939633, upload-time = "2025-03-23T15:01:18.196Z" }, - { url = "https://files.pythonhosted.org/packages/94/60/837501b8fc475086f5f26c72fa77513cedb5f761744bca809fdc2ee68e6a/zizmor-1.5.2-py3-none-win_amd64.whl", hash = "sha256:86395dd985ed6bf9acffea8a900b30d8d4dd4c9e20421f16f2db7ad92299e24e", size = 4435978, upload-time = "2025-03-23T15:01:16.284Z" }, +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/64/92/6b58872ac1184a441037bdc05c658a3900f49401b25f6e834a268ef6ff52/zizmor-1.28.0.tar.gz", hash = "sha256:6d5a300b80bf4c12e9cbe78ffd3874ec3f94b65bea78c994ec18157e2846ece0", size = 550169, upload-time = "2026-07-21T22:16:27.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/48/a725a1c34e5565eeeb4e4a93c59662f206e5156f2029627c66b2373edf55/zizmor-1.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:149dba59a8bd2897960ee54c40ae8b5801a71293bad71c8d2913c74ab66a294c", size = 8909502, upload-time = "2026-07-21T22:16:07.47Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/ba6d0eb2b336b4c4826bb0c9ed23c3239d46d71d8c1ddd2d395efeff7f52/zizmor-1.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0949f57a6d20deeb9c509705afce8de233c166475e25be305985a1fe553c6e0d", size = 8520554, upload-time = "2026-07-21T22:16:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/4f/07/69aba8157056fc0949cbdc787d8437813961e204a117c629d34afc827d6d/zizmor-1.28.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:9819f91f0ef486e4af98a6aacfed23c6b4067fabcecb88d49231228a3d43f821", size = 8759226, upload-time = "2026-07-21T22:16:11.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/b65717a3ca4573611f47612e67be5458ff700e857f49f759741fa67b0519/zizmor-1.28.0-py3-none-manylinux_2_28_armv7l.whl", hash = "sha256:354e6cb98a15a88593a6f7ac6236b092b83a2aad5c4768ee750f9b3262f668d9", size = 8382005, upload-time = "2026-07-21T22:16:13.896Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b4/f823bd2a1ba6dc432fdcbd249d4c442ce79bd137d34d986fce5c181f2800/zizmor-1.28.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ae2cab67ce713e760e0d1b61ad749d374693ea2b310337aab11cd446748267f3", size = 9175601, upload-time = "2026-07-21T22:16:16.177Z" }, + { url = "https://files.pythonhosted.org/packages/f3/75/adad17b0320eb3bc9ed797019fccf69a809affdf5ba9402bdc8b910957e1/zizmor-1.28.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7b00018cf2cc948c3b3e010c1a1f30fdc001f0d062640469f17e0ef006e74eb7", size = 8792861, upload-time = "2026-07-21T22:16:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3d/4d5583f24730f404c104eacf8aca4ca5a3d4c480e9d3cfe3eadd93351fd6/zizmor-1.28.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6897f02b0d02fd709f5ebc13cd37d97ad464219e0ba86a03f6d86f7777b7b102", size = 8343070, upload-time = "2026-07-21T22:16:20.092Z" }, + { url = "https://files.pythonhosted.org/packages/97/e2/4521e6dd56bc0d44eefb177794e261925c5cd00ab647b17e5513bddcb985/zizmor-1.28.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ef69d198dcf6835c9b6eea8673cbc5c36f10b3f1b98b51d28266b7e01c3704d4", size = 9262222, upload-time = "2026-07-21T22:16:21.89Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/5d841d197c821921cb8243352c610ca2505a86871c10f81efe4da190d24c/zizmor-1.28.0-py3-none-win32.whl", hash = "sha256:833c360ba5a9c74ca45007b8c79939826fca0c5ed65144fa08f63a7009cef096", size = 7541161, upload-time = "2026-07-21T22:16:23.965Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f5/3f4591746e5a9c7e8efc2e9e02aa43e1aa0957fe33cae64ce386c74b882e/zizmor-1.28.0-py3-none-win_amd64.whl", hash = "sha256:c93b30d211b0b0c38905a8803cc2653fff37de03eb77105302a02f709773bd4a", size = 8619340, upload-time = "2026-07-21T22:16:25.6Z" }, ]