Repository: woodruffw/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_

Pull request description:
fix: excessive-permissions: be less noisy on one-job workflows
This should reduce the amount of noise in single-job workflows, per #336.

CC @pietroalbini

Signed-off-by: William Woodruff <william@yossarian.net>

Fixes #336.

Patch:
diff --git a/src/audit/excessive_permissions.rs b/src/audit/excessive_permissions.rs
index 88e2e99..1941dd2 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 5dd30d0..bcf649e 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 0000000..ed8d7ff
--- /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 0000000..28aea9b
--- /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 0000000..ca08c1b
--- /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