Repository: bridgecrewio/checkov

Issue:
BC_K8S_14/CKV_K8S_15 "Image Pull Policy should be Always" should not be reported when using a digest
**Describe the issue**
[BC_K8S_14/CKV_K8S_15 "Image Pull Policy should be Always"](https://docs.bridgecrew.io/docs/bc_k8s_14) should not be reported when using a digest.

Per the documentation:
>  When the imagePullPolicy is set to Always, you ensure the latest version of the image is deployed every time the pod is started.

By specifying the digest, the same image is always used, so there is no concern about the latest version (since the the version cannot change), negating the need to always pull the image.

**Examples**
Save the following as `deployment.yaml`:
```yaml
apiVersion: v1
kind: Pod
metadata:
  name: <name>
spec:
  containers:
    - name: nginx
      image: 'nginx@sha256:b0ad43f7ee5edbc0effbc14645ae7055e21bc1973aee5150745632a24a752661'
      imagePullPolicy: IfNotPresent
```
Run: `checkov --framework kubernetes -f deployment.yaml -c CKV_K8S_15`

Actual result:
FAILED

Expected result:
Passed

**Version (please complete the following information):**
 - Checkov Version 2.3.126


Discussion:

Resolving patch:
diff --git a/checkov/kubernetes/checks/resource/k8s/ImagePullPolicyAlways.py b/checkov/kubernetes/checks/resource/k8s/ImagePullPolicyAlways.py
--- a/checkov/kubernetes/checks/resource/k8s/ImagePullPolicyAlways.py
+++ b/checkov/kubernetes/checks/resource/k8s/ImagePullPolicyAlways.py
@@ -29,6 +29,9 @@ def scan_container_conf(self, metadata: Dict[str, Any], conf: Dict[str, Any]) ->
                 return CheckResult.UNKNOWN
             if "@" in image_val:
                 image_val = image_val[0 : image_val.index("@")]
+                has_digest = True
+            else:
+                has_digest = False
             if "imagePullPolicy" not in conf:
                 image_tag_match = re.findall(DOCKER_IMAGE_REGEX, image_val)
                 if len(image_tag_match) != 1:
@@ -38,11 +41,13 @@ def scan_container_conf(self, metadata: Dict[str, Any], conf: Dict[str, Any]) ->
                 if tag == "latest" or tag == "":
                     # Default imagePullPolicy = Always
                     return CheckResult.PASSED
+                elif has_digest:
+                    return CheckResult.PASSED
                 else:
                     # Default imagePullPolicy = IfNotPresent
                     return CheckResult.FAILED
             else:
-                if conf["imagePullPolicy"] != "Always":
+                if not has_digest and conf["imagePullPolicy"] != "Always":
                     return CheckResult.FAILED
 
         else:
diff --git a/checkov/terraform/checks/resource/kubernetes/ImagePullPolicyAlways.py b/checkov/terraform/checks/resource/kubernetes/ImagePullPolicyAlways.py
--- a/checkov/terraform/checks/resource/kubernetes/ImagePullPolicyAlways.py
+++ b/checkov/terraform/checks/resource/kubernetes/ImagePullPolicyAlways.py
@@ -7,7 +7,7 @@ class ImagePullPolicyAlways(BaseResourceCheck):
     def __init__(self):
         """
         Image pull policy should be set to always to ensure you get the correct image and imagePullSecrets are correct
-        Default is 'IfNotPresent' unless image tag is omitted or :latest
+        Default is 'IfNotPresent' unless image tag/digest is omitted or :latest
         https://kubernetes.io/docs/concepts/configuration/overview/#container-images
 
         An admission controller could be used to enforce imagePullPolicy
@@ -47,6 +47,8 @@ def scan_resource_conf(self, conf) -> CheckResult:
                         name = container.get("image")[0]
                         if "latest" in name:
                             break
+                        if "@" in name:
+                            break
                 self.evaluated_keys = [f'{evaluated_keys_path}/[0]/container/[{idx}]']
                 return CheckResult.FAILED
             return CheckResult.PASSED


Test patch:
diff --git a/tests/kubernetes/checks/example_ImagePullPolicy/imageWithDigest-DefaultPullPolicy-PASSED.yaml b/tests/kubernetes/checks/example_ImagePullPolicy/imageWithDigest-DefaultPullPolicy-PASSED.yaml
new file mode 100644
--- /dev/null
+++ b/tests/kubernetes/checks/example_ImagePullPolicy/imageWithDigest-DefaultPullPolicy-PASSED.yaml
@@ -0,0 +1,117 @@
+# Source: https://kubernetes.io/docs/tutorials/stateful-application/cassandra/
+# https://github.com/kubernetes/examples/tree/master/cassandra
+
+apiVersion: v1
+kind: Service
+metadata:
+  labels:
+    app: cassandra
+  name: cassandra
+spec:
+  clusterIP: None
+  ports:
+  - port: 9042
+  selector:
+    app: cassandra
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: cassandra
+  labels:
+    app: cassandra
+spec:
+  serviceName: cassandra
+  replicas: 3
+  selector:
+    matchLabels:
+      app: cassandra
+  template:
+    metadata:
+      labels:
+        app: cassandra
+    spec:
+      terminationGracePeriodSeconds: 1800
+      containers:
+      - name: cassandra
+        image: gcr.io/google-samples/cassandra@sha256:12345678
+        ports:
+        - containerPort: 7000
+          name: intra-node
+        - containerPort: 7001
+          name: tls-intra-node
+        - containerPort: 7199
+          name: jmx
+        - containerPort: 9042
+          name: cql
+        resources:
+          limits:
+            cpu: "500m"
+            memory: 1Gi
+          requests:
+            cpu: "500m"
+            memory: 1Gi
+        securityContext:
+          capabilities:
+            add:
+              - IPC_LOCK
+        lifecycle:
+          preStop:
+            exec:
+              command: 
+              - /bin/sh
+              - -c
+              - nodetool drain
+        env:
+          - name: MAX_HEAP_SIZE
+            value: 512M
+          - name: HEAP_NEWSIZE
+            value: 100M
+          - name: CASSANDRA_SEEDS
+            value: "cassandra-0.cassandra.default.svc.cluster.local"
+          - name: CASSANDRA_CLUSTER_NAME
+            value: "K8Demo"
+          - name: CASSANDRA_DC
+            value: "DC1-K8Demo"
+          - name: CASSANDRA_RACK
+            value: "Rack1-K8Demo"
+          - name: POD_IP
+            valueFrom:
+              fieldRef:
+                fieldPath: status.podIP
+        readinessProbe:
+          exec:
+            command:
+            - /bin/bash
+            - -c
+            - /ready-probe.sh
+          initialDelaySeconds: 15
+          timeoutSeconds: 5
+        # These volume mounts are persistent. They are like inline claims,
+        # but not exactly because the names need to match exactly one of
+        # the stateful pod volumes.
+        volumeMounts:
+        - name: cassandra-data
+          mountPath: /cassandra_data
+  # These are converted to volume claims by the controller
+  # and mounted at the paths mentioned above.
+  # do not use these in production until ssd GCEPersistentDisk or other ssd pd
+  volumeClaimTemplates:
+  - metadata:
+      name: cassandra-data
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      storageClassName: fast
+      resources:
+        requests:
+          storage: 1Gi
+---
+kind: StorageClass
+apiVersion: storage.k8s.io/v1
+metadata:
+  name: fast
+provisioner: k8s.io/minikube-hostpath
+parameters:
+  type: pd-ssd
+
+
diff --git a/tests/kubernetes/checks/test_ImagePullPolicyAlways.py b/tests/kubernetes/checks/test_ImagePullPolicyAlways.py
--- a/tests/kubernetes/checks/test_ImagePullPolicyAlways.py
+++ b/tests/kubernetes/checks/test_ImagePullPolicyAlways.py
@@ -16,7 +16,7 @@ def test_summary(self):
         report = runner.run(root_folder=test_files_dir,runner_filter=RunnerFilter(checks=[check.id]))
         summary = report.get_summary()
 
-        self.assertEqual(summary['passed'], 5)
+        self.assertEqual(summary['passed'], 6)
         self.assertEqual(summary['failed'], 3)
         self.assertEqual(summary['skipped'], 0)
         self.assertEqual(summary['parsing_errors'], 0)
diff --git a/tests/terraform/checks/resource/kubernetes/example_ImagePullPolicyAlways/main.tf b/tests/terraform/checks/resource/kubernetes/example_ImagePullPolicyAlways/main.tf
--- a/tests/terraform/checks/resource/kubernetes/example_ImagePullPolicyAlways/main.tf
+++ b/tests/terraform/checks/resource/kubernetes/example_ImagePullPolicyAlways/main.tf
@@ -1512,3 +1512,82 @@ resource "kubernetes_deployment_v1" "pass2" {
     }
   }
 }
+
+#happy path
+resource "kubernetes_deployment_v1" "pass3" {
+  metadata {
+    name = "terraform-example"
+    labels = {
+      k8s-app = "nginx"
+    }
+  }
+
+  spec {
+    replicas = 3
+
+    selector {
+      match_labels = {
+        k8s-app = "nginx"
+      }
+    }
+
+    template {
+      metadata {
+        labels = {
+          k8s-app = "nginx"
+        }
+      }
+
+      spec {
+        container {
+          image             = "nginx@sha256:4a1c4b21597c1b4415bdbecb28a3296c6b5e23ca4f9feeb599860a1dac6a0108"
+          name              = "example22"
+
+          security_context {
+            privileged = false
+          }
+
+          env {
+            name  = "environment"
+            value = "test"
+          }
+
+          port {
+            container_port = 8080
+          }
+
+          liveness_probe {
+            http_get {
+              path = "/nginx_status"
+              port = 80
+
+              http_header {
+                name  = "X-Custom-Header"
+                value = "Awesome"
+              }
+            }
+
+            initial_delay_seconds = 3
+            period_seconds        = 3
+          }
+        }
+
+        dns_config {
+          nameservers = ["1.1.1.1", "8.8.8.8", "9.9.9.9"]
+          searches    = ["example.com"]
+
+          option {
+            name  = "ndots"
+            value = 1
+          }
+
+          option {
+            name = "use-vc"
+          }
+        }
+
+        dns_policy = "None"
+      }
+    }
+  }
+}
diff --git a/tests/terraform/checks/resource/kubernetes/test_ImagePullPolicyAlways.py b/tests/terraform/checks/resource/kubernetes/test_ImagePullPolicyAlways.py
--- a/tests/terraform/checks/resource/kubernetes/test_ImagePullPolicyAlways.py
+++ b/tests/terraform/checks/resource/kubernetes/test_ImagePullPolicyAlways.py
@@ -26,6 +26,7 @@ def test(self):
             "kubernetes_deployment.pass2",
             "kubernetes_deployment_v1.pass",
             "kubernetes_deployment_v1.pass2",
+            "kubernetes_deployment_v1.pass3",
         }
 
         failing_resources = {
@@ -42,8 +43,8 @@ def test(self):
         passed_check_resources = {c.resource for c in report.passed_checks}
         failed_check_resources = {c.resource for c in report.failed_checks}
 
-        self.assertEqual(summary["passed"], 4 * 2)
-        self.assertEqual(summary["failed"], 4 * 2)
+        self.assertEqual(summary["passed"], len(passing_resources))
+        self.assertEqual(summary["failed"], len(failing_resources))
         self.assertEqual(summary["skipped"], 0)
         self.assertEqual(summary["parsing_errors"], 0)