Repository: woodruffw/zizmor (rust)

Issue:
Feature: `unpinned-uses` should emit pedantic findings for tagged-but-not-hash-pinned
Follow-on to #733: right now we emit a finding if an image is untagged or has the `:latest` tag, but we could/should probably do something similar to `unpinned-uses`:

1. If the image is hash-pinned, no finding at all
2. If the image is tag-pinned without a hash, emit a pedantic finding
3. If the image is tag-pinned to `:latest`, emit a normal finding (we do this already)
4. If the image is unpinned, emit a normal finding (we do this already)

Pull request:
feat: emit pedantic finding for tagged OCI images
Pedantic findings are emitted by the unpinned-images audit when it encounters OCI image references that use a tag other than `latest`.

The pedantic finding is resolved, will not be produced when the image reference uses a SHA256 hash rather than a tag.

This closes #736

Patch:
diff --git a/docs/audits.md b/docs/audits.md
index 1c09c0b..c8c098a 100644
--- a/docs/audits.md
+++ b/docs/audits.md
@@ -1445,21 +1445,35 @@ This can be a security risk:
 1. Registries may not consistently enforce immutable image tags
 2. Completely unpinned images can be changed at any time by the OCI registry.
 
+By default, this audit applies the following policy:
+
+* Regular findings are created for all image references missing a tag
+
+    ```yaml
+    container:
+      image: foo/bar
+    ```
+
+    or using the `latest` tag:
+
+    ```yaml
+    container:
+      image: foo/bar:latest
+    ```
+
+* Pedantic findings are created for all image references using a tag (`!= latest`) rather than SHA256 hash.
+
+    ```yaml
+    container:
+      image: foo/bar:not-a-sha256
+    ```
+
 Other resources:
 
 - [Aqua: The Challenges of Uniquely Identifying Your Images]
 - [GitHub: Safeguard your containers with new container signing capability in GitHub Actions]
 
-```yaml
-container:
-  image: foo/bar
-```
-and where that version is `latest`:
 
-```yaml
-container:
-  image: foo/bar:latest
-```
 
 ### Remediation
 
diff --git a/src/audit/unpinned_images.rs b/src/audit/unpinned_images.rs
index b4cf92a..af5dab1 100644
--- a/src/audit/unpinned_images.rs
+++ b/src/audit/unpinned_images.rs
@@ -1,7 +1,7 @@
 use anyhow::Result;
 
 use crate::{
-    finding::{Confidence, Finding, Severity, SymbolicLocation},
+    finding::{Confidence, Finding, Persona, Severity, SymbolicLocation},
     models::JobExt as _,
     state::AuditState,
 };
@@ -18,6 +18,7 @@ impl UnpinnedImages {
         &self,
         location: SymbolicLocation<'doc>,
         annotation: &str,
+        persona: Persona,
         job: &super::NormalJob<'doc>,
     ) -> Result<Finding<'doc>> {
         let mut annotated_location = location;
@@ -26,6 +27,7 @@ impl UnpinnedImages {
             .severity(Severity::High)
             .confidence(Confidence::High)
             .add_location(annotated_location)
+            .persona(persona)
             .build(job.parent())
     }
 }
@@ -78,6 +80,7 @@ impl Audit for UnpinnedImages {
                         findings.push(self.build_finding(
                             location,
                             "container image is pinned to latest",
+                            Persona::Regular,
                             job,
                         )?);
                     }
@@ -85,10 +88,18 @@ impl Audit for UnpinnedImages {
                         findings.push(self.build_finding(
                             location,
                             "container image is unpinned",
+                            Persona::Regular,
+                            job,
+                        )?);
+                    }
+                    Some(_) => {
+                        findings.push(self.build_finding(
+                            location,
+                            "container image is not pinned to a SHA256 hash",
+                            Persona::Pedantic,
                             job,
                         )?);
                     }
-                    Some(_) => continue,
                 },
             }
         }


Test patch:
diff --git a/tests/integration/snapshot.rs b/tests/integration/snapshot.rs
index 682d31f..5ce3c69 100644
--- a/tests/integration/snapshot.rs
+++ b/tests/integration/snapshot.rs
@@ -691,6 +691,7 @@ fn unpinned_images() -> Result<()> {
     insta::assert_snapshot!(
         zizmor()
             .input(input_under_test("unpinned-images.yml"))
+            .args(["--persona=pedantic"])
             .run()?
     );
 
diff --git a/tests/integration/snapshots/integration__e2e__issue_569.snap b/tests/integration/snapshots/integration__e2e__issue_569.snap
index be45b3d..ca543a5 100644
--- a/tests/integration/snapshots/integration__e2e__issue_569.snap
+++ b/tests/integration/snapshots/integration__e2e__issue_569.snap
@@ -183,4 +183,4 @@ error[unpinned-uses]: unpinned action reference
    |
    = note: audit confidence → High
 
-47 findings (28 suppressed): 0 unknown, 0 informational, 1 low, 0 medium, 18 high
+48 findings (29 suppressed): 0 unknown, 0 informational, 1 low, 0 medium, 18 high
diff --git a/tests/integration/snapshots/integration__snapshot__unpinned_images.snap b/tests/integration/snapshots/integration__snapshot__unpinned_images.snap
index 4b3ddaf..e2d87f0 100644
--- a/tests/integration/snapshots/integration__snapshot__unpinned_images.snap
+++ b/tests/integration/snapshots/integration__snapshot__unpinned_images.snap
@@ -1,6 +1,6 @@
 ---
 source: tests/integration/snapshot.rs
-expression: "zizmor().input(input_under_test(\"unpinned-images.yml\")).run()?"
+expression: "zizmor().input(input_under_test(\"unpinned-images.yml\")).args([\"--persona=pedantic\"]).run()?"
 ---
 error[unpinned-images]: unpinned image references
   --> @@INPUT@@:16:7
@@ -34,4 +34,20 @@ error[unpinned-images]: unpinned image references
    |
    = note: audit confidence → High
 
-4 findings: 0 unknown, 0 informational, 0 low, 0 medium, 4 high
+error[unpinned-images]: unpinned image references
+  --> @@INPUT@@:46:7
+   |
+46 |       image: fake.example.com/example:0.0.348
+   |       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ container image is not pinned to a SHA256 hash
+   |
+   = note: audit confidence → High
+
+error[unpinned-images]: unpinned image references
+  --> @@INPUT@@:54:9
+   |
+54 |         image: fake.example.com/redis:7.4.3
+   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ container image is not pinned to a SHA256 hash
+   |
+   = note: audit confidence → High
+
+6 findings: 0 unknown, 0 informational, 0 low, 0 medium, 6 high