Repository: zizmorcore/zizmor (Rust)

Issue:
False positive for the `excessive-permission` audit when there is a single job
### Pre-submission checks

- [X] I am **not** reporting a bug (crash, false positive/negative, etc). These must be filed via the bug report template.
- [X] I have looked through the open issues for a duplicate request.

### What's the problem this feature will solve?

The `excessive-permission` audit reports an error when write permissions are declared at the workflow level, suggesting to scope them at the job level. This is indeed sound advice if there are multiple jobs in the workflow. While scanning some repositories though, I noticed that the audit returns an high confidence error even when there is a single job:

```yaml
permissions:
  contents: write

jobs:
  single:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
```

In this case, as far as I know putting the permission at the workflow or at the job level should make no difference (as long as there is a single job). This is causing a fair amount of noise when analyzing a large number of workflows.

### Describe the solution you'd like

When there is a single job, the link should not report a high confidence error. It might still be useful to have this trigger at the pedantic level, as it's still cleaner to define the permissions for the job.

### Additional context

_No response_


Discussion:
Thanks for the report @pietroalbini!

I agree this is too noisy: we can (and should) specifically carve out the "top-level permissions but only one job" case. I can look into that in a bit.
#337 will fix this -- `excessive-permissions` now only emits a "pedantic" finding if the workflow has only a single job.
Done. @pietroalbini LMK if you'd like a bugfix release for this, otherwise I'll likely roll it up tomorrow with anything else that gets reported.
Thanks! No need for a bugfix release :D



Patch:
diff --git a/src/audit/excessive_permissions.rs b/src/audit/excessive_permissions.rs
index 88e2e99d0..1941dd21b 100644
--- a/src/audit/excessive_permissions.rs
+++ b/src/audit/excessive_permissions.rs
@@ -7,7 +7,7 @@ use github_actions_models::{
 
 use super::{audit_meta, WorkflowAudit};
 use crate::{
-    finding::{Confidence, Severity},
+    finding::{Confidence, Persona, Severity},
     AuditState,
 };
 
@@ -56,12 +56,24 @@ impl WorkflowAudit for ExcessivePermissions {
         workflow: &'w crate::models::Workflow,
     ) -> anyhow::Result<Vec<crate::finding::Finding<'w>>> {
         let mut findings = vec![];
+
+        // Top-level permissions are a minor issue if there's only one
+        // job in the workflow, since they're equivalent to job-level
+        // permissions in that case. Emit only pedantic findings in
+        // that case.
+        let persona = if workflow.jobs.len() == 1 {
+            Persona::Pedantic
+        } else {
+            Persona::Regular
+        };
+
         // Top-level permissions.
         for (severity, confidence, note) in self.check_permissions(&workflow.permissions, None) {
             findings.push(
                 Self::finding()
                     .severity(severity)
                     .confidence(confidence)
+                    .persona(persona)
                     .add_location(
                         workflow
                             .location()


Test patch:
diff --git a/tests/snapshot.rs b/tests/snapshot.rs
index 5dd30d0c2..bcf649ea6 100644
--- a/tests/snapshot.rs
+++ b/tests/snapshot.rs
@@ -316,3 +316,21 @@ fn cache_poisoning() -> Result<()> {
 
     Ok(())
 }
+
+#[test]
+fn excessive_permissions() -> Result<()> {
+    insta::assert_snapshot!(zizmor()
+        .workflow(workflow_under_test(
+            "excessive-permissions/issue-336-repro.yml"
+        ))
+        .run()?);
+
+    insta::assert_snapshot!(zizmor()
+        .workflow(workflow_under_test(
+            "excessive-permissions/issue-336-repro.yml"
+        ))
+        .args(["--pedantic"])
+        .run()?);
+
+    Ok(())
+}
diff --git a/tests/snapshots/snapshot__excessive_permissions-2.snap b/tests/snapshots/snapshot__excessive_permissions-2.snap
new file mode 100644
index 000000000..ed8d7fff6
--- /dev/null
+++ b/tests/snapshots/snapshot__excessive_permissions-2.snap
@@ -0,0 +1,15 @@
+---
+source: tests/snapshot.rs
+expression: "zizmor().workflow(workflow_under_test(\"excessive-permissions/issue-336-repro.yml\")).args([\"--pedantic\"]).run()?"
+snapshot_kind: text
+---
+error[excessive-permissions]: overly broad workflow or job-level permissions
+ --> @@INPUT@@:3:1
+  |
+3 | / permissions:
+4 | |   contents: write
+  | |_________________^ contents: write is overly broad at the workflow level
+  |
+  = note: audit confidence → High
+
+1 finding: 0 unknown, 0 informational, 0 low, 0 medium, 1 high
diff --git a/tests/snapshots/snapshot__excessive_permissions.snap b/tests/snapshots/snapshot__excessive_permissions.snap
new file mode 100644
index 000000000..28aea9b99
--- /dev/null
+++ b/tests/snapshots/snapshot__excessive_permissions.snap
@@ -0,0 +1,6 @@
+---
+source: tests/snapshot.rs
+expression: "zizmor().workflow(workflow_under_test(\"excessive-permissions/issue-336-repro.yml\")).run()?"
+snapshot_kind: text
+---
+No findings to report. Good job! (1 suppressed)
diff --git a/tests/test-data/excessive-permissions/issue-336-repro.yml b/tests/test-data/excessive-permissions/issue-336-repro.yml
new file mode 100644
index 000000000..ca08c1b6f
--- /dev/null
+++ b/tests/test-data/excessive-permissions/issue-336-repro.yml
@@ -0,0 +1,12 @@
+on: push
+
+permissions:
+  contents: write
+
+jobs:
+  single:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+        with:
+          persist-credentials: false