Repository: zizmorcore/zizmor (Rust)

Issue:
[BUG]: False positive: `uv publish` uses trusted publishing
### Pre-submission checks

- [x] I am **not** filing a feature request. These should be filed via the feature request form instead.
- [x] I have looked through both the [open](https://github.com/zizmorcore/zizmor/issues?q=is%3Aissue+is%3Aopen+) and [closed](https://github.com/zizmorcore/zizmor/issues?q=is%3Aissue%20state%3Aclosed) issues for a duplicate report.

### zizmor version

v1.14.1,, 6c8b25171a2be4e79ce8a7e7374b547e38087ef7

### Expected behavior

`uv publish` with `id-token: write` is accepted as trusted publishing



### Actual behavior

`uv publish` is always considered token-based publishing.

```yaml
name: "Publish to PyPI"

on:
  workflow_call:

jobs:
  pypi-publish:
    name: Upload to PyPI
    runs-on: ubuntu-latest
    environment:
      name: release
    permissions:
      # For PyPI's trusted publishing.
      id-token: write
    steps:
      - name: "Install uv"
        uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
      - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
        with:
          pattern: wheels-*
          path: wheels
      - name: Publish to PyPi
        run: uv publish -v wheels/*
```

```
$ uvx zizmor .github/workflows/publish-pypi.yml -v
DEBUG collect_inputs: zizmor::config: discovering config for local input `.github/workflows/publish-pypi.yml`
DEBUG collect_inputs: zizmor::config: attempting config discovery in `.github/workflows`
DEBUG collect_inputs: zizmor::config: found `/home/konsti/projects/ty/.git`, stopping search
DEBUG collect_inputs: zizmor::registry::input: registering workflow input as with key file://.github/workflows/publish-pypi.yml
 INFO zizmor::registry: skipping impostor-commit: can't run without a GitHub API token
 INFO zizmor::registry: skipping ref-confusion: can't run without a GitHub API token
 INFO zizmor::registry: skipping known-vulnerable-actions: can't run without a GitHub API token
 INFO zizmor::registry: skipping stale-action-refs: can't run without a GitHub API token
 INFO zizmor::registry: skipping ref-version-mismatch: can't run without a GitHub API token
DEBUG audit: zizmor: running artipacked on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running unsound-contains on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running excessive-permissions on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running dangerous-triggers on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running use-trusted-publishing on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running template-injection on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running hardcoded-container-credentials on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running self-hosted-runner on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running unpinned-uses on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running undocumented-permissions on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running insecure-commands on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running github-env on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running cache-poisoning on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running secrets-inherit on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running bot-conditions on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running overprovisioned-secrets on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running unredacted-secrets on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running forbidden-uses on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running obfuscation on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running unpinned-images on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running anonymous-definition on file://.github/workflows/publish-pypi.yml
DEBUG audit: zizmor: running unsound-condition on file://.github/workflows/publish-pypi.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/publish-pypi.yml
info[use-trusted-publishing]: prefer trusted publishing for authentication
  --> .github/workflows/publish-pypi.yml:23:14
   |
23 |         run: uv publish -v wheels/*
   |         ---  ^^^^^^^^^^^^^^^^^^^^^^ this command
   |         |
   |         this step
   |
   = note: audit confidence → High

3 findings (2 suppressed): 1 informational, 0 low, 0 medium, 0 high
```




### Reproduction steps

See above.

### Logs

```plain text
See above.
```

### Additional context

Implemented here:

https://github.com/zizmorcore/zizmor/blob/6c8b25171a2be4e79ce8a7e7374b547e38087ef7/crates/zizmor/src/audit/use_trusted_publishing.rs#L156

ref #282 


Discussion:
The rule also matches `uv publish --trusted-publishing always` which fails if trusted publishing is not available.
Thanks @konstin! Yeah, this looks like a regression, maybve with #1161. I'll look at it now.
Yeah, here's the problematic change: https://github.com/zizmorcore/zizmor/pull/1161/files#diff-a678c93ad59a932c634a2a235d0da1f77fa0729d1aa3f4c5b3f5d240f65c6f33L305-R346

I hadn't thought through the consequences of that enough -- our ability to detect these publishing scenarios via CLI options is too imprecise at the moment for us to distinguish between incomplete TP migrations and complete ones. I'll revert that part of the change.

CC @KristianGrafana for viz 🙂 



Patch:
diff --git a/crates/zizmor/src/audit/use_trusted_publishing.rs b/crates/zizmor/src/audit/use_trusted_publishing.rs
index f1d59094a..cc7e76197 100644
--- a/crates/zizmor/src/audit/use_trusted_publishing.rs
+++ b/crates/zizmor/src/audit/use_trusted_publishing.rs
@@ -340,10 +340,19 @@ impl Audit for UseTrustedPublishing {
         // In addition to the shared action matching above, we can
         // also check for some `run:` patterns that indicate publishing
         // without Trusted Publishing.
-        // We check this regardless of id-token permission state because:
-        // 1. If no id-token: indicates missing trusted publishing setup
-        // 2. If has id-token but uses manual tokens: indicates hybrid/incomplete migration
-        if let StepBodyCommon::Run { run, .. } = step.body() {
+
+        // We can only check these reliably on workflows and not actions,
+        // since we need to be able to see the `id-token` permission's
+        // state to filter out any false positives.
+        //
+        // NOTE(ww): With #1161 we loosened this check and turned the
+        // "has ID token" check into a confidence modifier rather than
+        // a strict filter. This ended up being overly imprecise, since a lot
+        // of publishing commands use trusted publishing implicitly if
+        // the environment supports it. We reverted this with #1191.
+        if let StepBodyCommon::Run { run, .. } = step.body()
+            && !step.parent.has_id_token()
+        {
             let shell = step.shell().unwrap_or_else(|| {
                 tracing::debug!(
                     "use-trusted-publishing: couldn't determine shell type for {workflow}:{job} step {stepno}",
@@ -356,21 +365,10 @@ impl Audit for UseTrustedPublishing {
             });
 
             for subfeature in self.trusted_publishing_command_candidates(run, shell)? {
-                // Adjust confidence based on whether id-token permission is present
-                let confidence = if step.parent.has_id_token() {
-                    // Higher confidence when id-token is present but manual tokens are still used
-                    // This indicates a hybrid/incomplete migration that should be flagged
-                    Confidence::High
-                } else {
-                    // Low confidence when no id-token - could be intentional for non-TP registries
-                    // or legitimate reasons not to use trusted publishing
-                    Confidence::Low
-                };
-
                 findings.push(
                     Self::finding()
                         .severity(Severity::Informational)
-                        .confidence(confidence)
+                        .confidence(Confidence::High)
                         .add_location(step.location().hidden())
                         .add_location(
                             step.location()
diff --git a/docs/release-notes.md b/docs/release-notes.md
index a4224acd0..c5e8510b8 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -9,6 +9,12 @@ of `zizmor`.
 
 ## Next (UNRELEASED)
 
+### Bug Fixes 🐛
+
+* Fixed a bug where the [use-trusted-publishing] audit would produce-false
+  positive findings for some `run:` blocks that implicitly performed
+  trusted publishing (#1191)
+
 ## 1.14.1
 
 ### Bug Fixes 🐛


Test patch:
diff --git a/crates/zizmor/tests/integration/snapshot.rs b/crates/zizmor/tests/integration/snapshot.rs
index 2e36088b8..352016bfd 100644
--- a/crates/zizmor/tests/integration/snapshot.rs
+++ b/crates/zizmor/tests/integration/snapshot.rs
@@ -306,6 +306,15 @@ fn use_trusted_publishing() -> Result<()> {
             .run()?
     );
 
+    // No use-trusted-publishing findings expected here.
+    insta::assert_snapshot!(
+        zizmor()
+            .input(input_under_test(
+                "use-trusted-publishing/issue-1191-repro.yml"
+            ))
+            .run()?
+    );
+
     Ok(())
 }
 
diff --git a/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap b/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap
index f6f2f9cef..6ff95a2c9 100644
--- a/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap
+++ b/crates/zizmor/tests/integration/snapshots/integration__e2e__gha_hazmat.snap
@@ -265,7 +265,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 error[cache-poisoning]: runtime artifacts potentially vulnerable to a cache poisoning attack
   --> .github/workflows/cache-poisoning.yml:36:9
diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-3.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-3.snap
index efd93b6a8..46d5645a5 100644
--- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-3.snap
+++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-3.snap
@@ -10,7 +10,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:17:14
@@ -20,7 +20,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:24:11
@@ -33,7 +33,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
 27 | |             --no-verify
    | |_______________________^ this command
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:37:14
@@ -43,7 +43,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:42:14
@@ -53,7 +53,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:49:11
@@ -66,7 +66,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
 52 | |             --no-verify
    | |_______________________^ this command
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:56:14
@@ -76,7 +76,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:63:11
@@ -89,6 +89,6 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
 66 | |             publish
    | |___________________^ this command
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 9 findings (1 suppressed): 8 informational, 0 low, 0 medium, 0 high
diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-4.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-4.snap
index 7d96a09c9..8c36b415a 100644
--- a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-4.snap
+++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-4.snap
@@ -21,7 +21,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:26:9
@@ -42,7 +42,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:43:14
@@ -52,7 +52,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:49:14
@@ -62,7 +62,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:57:11
@@ -73,7 +73,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
 57 |           npm publish --access public
    |           ^^^^^^^^^^^^^^^^^^^^^^^^^^^ this command
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:63:14
@@ -83,7 +83,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:69:14
@@ -93,7 +93,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:75:14
@@ -103,7 +103,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
   --> @@INPUT@@:81:14
@@ -113,47 +113,7 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
    |         |
    |         this step
    |
-   = note: audit confidence → Low
-
-info[use-trusted-publishing]: prefer trusted publishing for authentication
-   --> @@INPUT@@:100:14
-    |
-100 |         run: npm publish --provenance
-    |         ---  ^^^^^^^^^^^^^^^^^^^^^^^^ this command
-    |         |
-    |         this step
-    |
-    = note: audit confidence → High
-
-info[use-trusted-publishing]: prefer trusted publishing for authentication
-   --> @@INPUT@@:109:14
-    |
-109 |         run: npm publish --provenance
-    |         ---  ^^^^^^^^^^^^^^^^^^^^^^^^ this command
-    |         |
-    |         this step
-    |
-    = note: audit confidence → High
-
-info[use-trusted-publishing]: prefer trusted publishing for authentication
-   --> @@INPUT@@:113:14
-    |
-113 |         run: npm publish --provenance
-    |         ---  ^^^^^^^^^^^^^^^^^^^^^^^^ this command
-    |         |
-    |         this step
-    |
-    = note: audit confidence → High
-
-info[use-trusted-publishing]: prefer trusted publishing for authentication
-   --> @@INPUT@@:123:14
-    |
-123 |         run: npm publish
-    |         ---  ^^^^^^^^^^^ this command
-    |         |
-    |         this step
-    |
-    = note: audit confidence → High
+   = note: audit confidence → High
 
 info[use-trusted-publishing]: prefer trusted publishing for authentication
    --> @@INPUT@@:129:9
@@ -166,14 +126,4 @@ info[use-trusted-publishing]: prefer trusted publishing for authentication
     |
     = note: audit confidence → High
 
-info[use-trusted-publishing]: prefer trusted publishing for authentication
-   --> @@INPUT@@:134:14
-    |
-134 |         run: npm publish
-    |         ---  ^^^^^^^^^^^ this command
-    |         |
-    |         this step
-    |
-    = note: audit confidence → High
-
-18 findings (1 suppressed): 17 informational, 0 low, 0 medium, 0 high
+13 findings (1 suppressed): 12 informational, 0 low, 0 medium, 0 high
diff --git a/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-5.snap b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-5.snap
new file mode 100644
index 000000000..2b1572b13
--- /dev/null
+++ b/crates/zizmor/tests/integration/snapshots/integration__snapshot__use_trusted_publishing-5.snap
@@ -0,0 +1,5 @@
+---
+source: crates/zizmor/tests/integration/snapshot.rs
+expression: "zizmor().input(input_under_test(\"use-trusted-publishing/issue-1191-repro.yml\")).run()?"
+---
+No findings to report. Good job! (2 suppressed)
diff --git a/crates/zizmor/tests/integration/test-data/use-trusted-publishing/issue-1191-repro.yml b/crates/zizmor/tests/integration/test-data/use-trusted-publishing/issue-1191-repro.yml
new file mode 100644
index 000000000..e2f40617b
--- /dev/null
+++ b/crates/zizmor/tests/integration/test-data/use-trusted-publishing/issue-1191-repro.yml
@@ -0,0 +1,24 @@
+# https://github.com/zizmorcore/zizmor/issues/1191
+name: "Publish to PyPI"
+
+on:
+  workflow_call:
+
+jobs:
+  pypi-publish:
+    name: Upload to PyPI
+    runs-on: ubuntu-latest
+    environment:
+      name: release
+    permissions:
+      # For PyPI's trusted publishing.
+      id-token: write
+    steps:
+      - name: "Install uv"
+        uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
+      - uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
+        with:
+          pattern: wheels-*
+          path: wheels
+      - name: Publish to PyPi
+        run: uv publish -v wheels/*