Repository: zizmorcore/zizmor (Rust)

Issue:
[BUG]: unpinned-uses false positive for local extensions
### 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 the [open issues](https://github.com/woodruffw/zizmor/issues?q=is%3Aissue+is%3Aopen+) for a duplicate report.

### Expected behavior

I expect `unpinned-use` to allow _local_ (i.e., in the same repo) actions to be unpinined.

### Actual behavior

```
warning[unpinned-uses]: unpinned action reference
   --> /Users/alex_gaynor/projects/cryptography/.github/workflows/ci.yml:384:9
    |
384 |         uses: ./.github/actions/cache
    |         ----------------------------- action is not pinned to a tag, branch, or hash ref
    |
    = note: audit confidence → High
```

### Reproduction steps

```
$ git clone https://github.com/pyca/cryptography
$ zizmor cryptography
```

### Logs

_No response_

### Additional context

_No response_


Discussion:
Yep, that's a bug. Will fix today.
#439 will fix this!



Patch:
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 0aba69012..d383168f5 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -9,7 +9,10 @@ of `zizmor`.
 
 ## Next (UNRELEASED)
 
-Nothing to see here (yet!)
+### Improved
+
+* The [unpinned-uses] audit no longer flags local reusable workflows or actions
+  as unpinned/unhashed (#439)
 
 ## v1.1.1
 
@@ -429,3 +432,4 @@ This is one of `zizmor`'s bigger recent releases! Key enhancements include:
 [github-env]: ./audits.md#github-env
 [template-injection]: ./audits.md#template-injection
 [secrets-inherit]: ./audits.md#secrets-inherit
+[unpinned-uses]: ./audits.md#unpinned-uses
diff --git a/src/audit/unpinned_uses.rs b/src/audit/unpinned_uses.rs
index bcbfdf252..f38b723e2 100644
--- a/src/audit/unpinned_uses.rs
+++ b/src/audit/unpinned_uses.rs
@@ -10,6 +10,13 @@ audit_meta!(UnpinnedUses, "unpinned-uses", "unpinned action reference");
 
 impl UnpinnedUses {
     pub fn evaluate_pinning<'u>(&self, uses: &Uses) -> Option<(&'u str, Severity, Persona)> {
+        // Don't evaluate pinning for local `uses:`, since unpinned references
+        // are fully controlled by the repository anyways.
+        // TODO: auditor-level findings instead, perhaps?
+        if matches!(uses, Uses::Local(_)) {
+            return None;
+        }
+
         if uses.unpinned() {
             Some((
                 "action is not pinned to a tag, branch, or hash ref",
@@ -43,24 +50,22 @@ impl Audit for UnpinnedUses {
             return Ok(vec![]);
         };
 
-        let Some((annotation, severity, persona)) = self.evaluate_pinning(uses) else {
-            return Ok(vec![]);
+        if let Some((annotation, severity, persona)) = self.evaluate_pinning(uses) {
+            findings.push(
+                Self::finding()
+                    .confidence(Confidence::High)
+                    .severity(severity)
+                    .persona(persona)
+                    .add_location(
+                        step.location()
+                            .primary()
+                            .with_keys(&["uses".into()])
+                            .annotated(annotation),
+                    )
+                    .build(step.workflow())?,
+            );
         };
 
-        findings.push(
-            Self::finding()
-                .confidence(Confidence::High)
-                .severity(severity)
-                .persona(persona)
-                .add_location(
-                    step.location()
-                        .primary()
-                        .with_keys(&["uses".into()])
-                        .annotated(annotation),
-                )
-                .build(step.workflow())?,
-        );
-
         Ok(findings)
     }
 
@@ -74,24 +79,22 @@ impl Audit for UnpinnedUses {
             return Ok(vec![]);
         };
 
-        let Some((annotation, severity, persona)) = self.evaluate_pinning(uses) else {
-            return Ok(vec![]);
+        if let Some((annotation, severity, persona)) = self.evaluate_pinning(uses) {
+            findings.push(
+                Self::finding()
+                    .confidence(Confidence::High)
+                    .severity(severity)
+                    .persona(persona)
+                    .add_location(
+                        step.location()
+                            .primary()
+                            .with_keys(&["uses".into()])
+                            .annotated(annotation),
+                    )
+                    .build(step.action())?,
+            );
         };
 
-        findings.push(
-            Self::finding()
-                .confidence(Confidence::High)
-                .severity(severity)
-                .persona(persona)
-                .add_location(
-                    step.location()
-                        .primary()
-                        .with_keys(&["uses".into()])
-                        .annotated(annotation),
-                )
-                .build(step.action())?,
-        );
-
         Ok(findings)
     }
 }


Test patch:
diff --git a/tests/snapshot.rs b/tests/snapshot.rs
index ed12e79d2..2243d1993 100644
--- a/tests/snapshot.rs
+++ b/tests/snapshot.rs
@@ -217,6 +217,11 @@ fn unpinned_uses() -> Result<()> {
         .args(["--pedantic"])
         .run()?);
 
+    insta::assert_snapshot!(zizmor()
+        .workflow(workflow_under_test("unpinned-uses/issue-433-repro.yml"))
+        .args(["--pedantic"])
+        .run()?);
+
     Ok(())
 }
 
diff --git a/tests/snapshots/snapshot__unpinned_uses-4.snap b/tests/snapshots/snapshot__unpinned_uses-4.snap
new file mode 100644
index 000000000..bc36fba46
--- /dev/null
+++ b/tests/snapshots/snapshot__unpinned_uses-4.snap
@@ -0,0 +1,6 @@
+---
+source: tests/snapshot.rs
+expression: "zizmor().workflow(workflow_under_test(\"unpinned-uses/issue-433-repro.yml\")).args([\"--pedantic\"]).run()?"
+snapshot_kind: text
+---
+No findings to report. Good job!
diff --git a/tests/test-data/unpinned-uses/issue-433-repro.yml b/tests/test-data/unpinned-uses/issue-433-repro.yml
new file mode 100644
index 000000000..6fc0f6225
--- /dev/null
+++ b/tests/test-data/unpinned-uses/issue-433-repro.yml
@@ -0,0 +1,17 @@
+# repro case for https://github.com/woodruffw/zizmor/issues/433
+
+name: issue-433-repro
+
+on: push
+
+jobs:
+  issue-433-repro:
+    runs-on: ubuntu-latest
+    steps:
+      - name: no-finding-1
+        # workflow is local, so tagging is superfluous
+        uses: ./.github/workflows/reusable.yml
+
+      - name: no-finding-2
+        # no pedantic finding for tag-pinned local workflows
+        uses: ./.github/workflows/reusable.yml@tag