From 6f7bd7451b6ab2c2cd799f6fe3a15c90d77ff42a Mon Sep 17 00:00:00 2001
From: Sambhav Kothari <sambhavs.email@gmail.com>
Date: Mon, 11 Apr 2022 10:30:38 +0100
Subject: [PATCH] Refactor image extraction to allow extracting custom
 resources (#3572)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* refactor: image extraction

Signed-off-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>

* Refactor image extraction to allow extracting custom resources

Signed-off-by: Sambhav Kothari <skothari44@bloomberg.net>

Co-authored-by: Charles-Edouard Brétéché <charled.breteche@gmail.com>
---
 pkg/engine/context/context.go         |  22 ++--
 pkg/engine/context/imageutils.go      | 183 +-------------------------
 pkg/engine/context/imageutils_test.go | 125 ------------------
 pkg/engine/imageVerify.go             |  89 ++++++-------
 pkg/utils/image/infos.go              |  77 +++++++++++
 pkg/utils/image/infos_test.go         |  73 ++++++++++
 pkg/utils/kube/image.go               | 119 +++++++++++++++++
 pkg/utils/kube/image_test.go          |  69 ++++++++++
 8 files changed, 401 insertions(+), 356 deletions(-)
 delete mode 100644 pkg/engine/context/imageutils_test.go
 create mode 100644 pkg/utils/image/infos.go
 create mode 100644 pkg/utils/image/infos_test.go
 create mode 100644 pkg/utils/kube/image.go
 create mode 100644 pkg/utils/kube/image_test.go

diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go
index 59e24fe16c..4050f54f80 100644
--- a/pkg/engine/context/context.go
+++ b/pkg/engine/context/context.go
@@ -8,6 +8,8 @@ import (
 	jsonpatch "github.com/evanphx/json-patch/v5"
 	kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
 	pkgcommon "github.com/kyverno/kyverno/pkg/common"
+	imageutils "github.com/kyverno/kyverno/pkg/utils/image"
+	kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
 	"github.com/pkg/errors"
 	admissionv1 "k8s.io/api/admission/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -58,13 +60,13 @@ type Interface interface {
 	AddElement(data map[string]interface{}, index int) error
 
 	// AddImageInfo adds image info to the context
-	AddImageInfo(info *ImageInfo) error
+	AddImageInfo(info imageutils.ImageInfo) error
 
 	// AddImageInfos adds image infos to the context
 	AddImageInfos(resource *unstructured.Unstructured) error
 
 	// ImageInfo returns image infos present in the context
-	ImageInfo() *Images
+	ImageInfo() map[string]map[string]imageutils.ImageInfo
 
 	// Checkpoint creates a copy of the current internal state and pushes it into a stack of stored states.
 	Checkpoint()
@@ -86,7 +88,7 @@ type context struct {
 	mutex              sync.RWMutex
 	jsonRaw            []byte
 	jsonRawCheckpoints [][]byte
-	images             *Images
+	images             map[string]map[string]imageutils.ImageInfo
 }
 
 // NewContext returns a new context
@@ -209,7 +211,7 @@ func (ctx *context) AddElement(data map[string]interface{}, index int) error {
 	return addToContext(ctx, data)
 }
 
-func (ctx *context) AddImageInfo(info *ImageInfo) error {
+func (ctx *context) AddImageInfo(info imageutils.ImageInfo) error {
 	data := map[string]interface{}{
 		"image":    info.String(),
 		"registry": info.Registry,
@@ -222,16 +224,18 @@ func (ctx *context) AddImageInfo(info *ImageInfo) error {
 }
 
 func (ctx *context) AddImageInfos(resource *unstructured.Unstructured) error {
-	initContainersImgs, containersImgs, ephemeralContainersImgs := extractImageInfo(resource, logger)
-	if len(initContainersImgs) == 0 && len(containersImgs) == 0 && len(ephemeralContainersImgs) == 0 {
+	images, err := kubeutils.ExtractImagesFromResource(*resource)
+	if err != nil {
+		return err
+	}
+	if len(images) == 0 {
 		return nil
 	}
-	images := newImages(initContainersImgs, containersImgs, ephemeralContainersImgs)
-	ctx.images = &images
+	ctx.images = images
 	return addToContext(ctx, images, "images")
 }
 
-func (ctx *context) ImageInfo() *Images {
+func (ctx *context) ImageInfo() map[string]map[string]imageutils.ImageInfo {
 	return ctx.images
 }
 
diff --git a/pkg/engine/context/imageutils.go b/pkg/engine/context/imageutils.go
index 8d1ee2d505..c6859982cd 100644
--- a/pkg/engine/context/imageutils.go
+++ b/pkg/engine/context/imageutils.go
@@ -2,179 +2,10 @@ package context
 
 import (
 	"fmt"
-	"strconv"
-	"strings"
 
-	"github.com/distribution/distribution/reference"
-	"github.com/go-logr/logr"
 	engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
-	"github.com/pkg/errors"
-	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-type ImageInfo struct {
-	// Registry is the URL address of the image registry e.g. `docker.io`
-	Registry string `json:"registry,omitempty"`
-
-	// Name is the image name portion e.g. `busybox`
-	Name string `json:"name"`
-
-	// Path is the repository path and image name e.g. `some-repository/busybox`
-	Path string `json:"path"`
-
-	// Tag is the image tag e.g. `v2`
-	Tag string `json:"tag,omitempty"`
-
-	// Digest is the image digest portion e.g. `sha256:128c6e3534b842a2eec139999b8ce8aa9a2af9907e2b9269550809d18cd832a3`
-	Digest string `json:"digest,omitempty"`
-
-	// JSONPointer is full JSON path to this image e.g. `/spec/containers/0/image`
-	JSONPointer string `json:"jsonPath,omitempty"`
-}
-
-func (i *ImageInfo) String() string {
-	image := i.Registry + "/" + i.Path + ":" + i.Tag
-	// image that needs only digest and not the tag
-	if i.Digest != "" {
-		image = i.Registry + "/" + i.Path + "@" + i.Digest
-	}
-	return image
-}
-
-type ContainerImage struct {
-	ImageInfo
-	Name string
-}
-
-type Images struct {
-	InitContainers      map[string]ImageInfo `json:"initContainers,omitempty"`
-	Containers          map[string]ImageInfo `json:"containers"`
-	EphemeralContainers map[string]ImageInfo `json:"ephemeralContainers"`
-}
-
-func newImages(initContainersImgs, containersImgs, ephemeralContainersImgs []ContainerImage) Images {
-	initContainers := make(map[string]ImageInfo)
-	for _, resource := range initContainersImgs {
-		initContainers[resource.Name] = resource.ImageInfo
-	}
-	containers := make(map[string]ImageInfo)
-	for _, resource := range containersImgs {
-		containers[resource.Name] = resource.ImageInfo
-	}
-	ephemeralContainers := make(map[string]ImageInfo)
-	for _, resource := range ephemeralContainersImgs {
-		ephemeralContainers[resource.Name] = resource.ImageInfo
-	}
-	return Images{
-		InitContainers:      initContainers,
-		Containers:          containers,
-		EphemeralContainers: ephemeralContainers,
-	}
-}
-
-type imageExtractor struct {
-	fields []string
-}
-
-func (i imageExtractor) extract(tag string, resource *unstructured.Unstructured) []ContainerImage {
-	f := append(i.fields[:len(i.fields):len(i.fields)], tag)
-	if containers, ok, _ := unstructured.NestedSlice(resource.UnstructuredContent(), f...); ok {
-		return extractImageInfos(containers, "/"+strings.Join(f, "/"))
-	}
-	return nil
-}
-
-var extractors = map[string]imageExtractor{
-	"Pod":         {[]string{"spec"}},
-	"CronJob":     {[]string{"spec", "jobTemplate", "spec", "template", "spec"}},
-	"Deployment":  {[]string{"spec", "template", "spec"}},
-	"DaemonSet":   {[]string{"spec", "template", "spec"}},
-	"Job":         {[]string{"spec", "template", "spec"}},
-	"StatefulSet": {[]string{"spec", "template", "spec"}},
-}
-
-func extractImageInfo(resource *unstructured.Unstructured, log logr.Logger) (initContainersImgs, containersImgs, ephemeralContainersImgs []ContainerImage) {
-	extractor := extractors[resource.GetKind()]
-	initContainersImgs = extractor.extract("initContainers", resource)
-	containersImgs = extractor.extract("containers", resource)
-	ephemeralContainersImgs = extractor.extract("ephemeralContainers", resource)
-	return
-}
-
-func extractImageInfos(containers []interface{}, jsonPath string) []ContainerImage {
-	img, err := convertToImageInfo(containers, jsonPath)
-	if err != nil {
-		logger.Error(err, "failed to extract image info", "element", containers)
-	}
-	return img
-}
-
-func convertToImageInfo(containers []interface{}, jsonPath string) (images []ContainerImage, err error) {
-	var errs []string
-	var index = 0
-	for _, ctr := range containers {
-		if container, ok := ctr.(map[string]interface{}); ok {
-			var name, image string
-			name = container["name"].(string)
-			if _, ok := container["image"]; ok {
-				image = container["image"].(string)
-			}
-			jp := strings.Join([]string{jsonPath, strconv.Itoa(index), "image"}, "/")
-			imageInfo, err := newImageInfo(image, jp)
-			if err != nil {
-				errs = append(errs, err.Error())
-				continue
-			}
-			images = append(images, ContainerImage{*imageInfo, name})
-		}
-		index++
-	}
-	if len(errs) == 0 {
-		return images, nil
-	}
-	return images, errors.Errorf("%s", strings.Join(errs, ";"))
-}
-
-func newImageInfo(image, jsonPointer string) (*ImageInfo, error) {
-	image = addDefaultDomain(image)
-	ref, err := reference.Parse(image)
-	if err != nil {
-		return nil, errors.Wrapf(err, "bad image: %s", image)
-	}
-	var registry, path, name, tag, digest string
-	if named, ok := ref.(reference.Named); ok {
-		registry = reference.Domain(named)
-		path = reference.Path(named)
-		name = path[strings.LastIndex(path, "/")+1:]
-	}
-	if tagged, ok := ref.(reference.Tagged); ok {
-		tag = tagged.Tag()
-	}
-	if digested, ok := ref.(reference.Digested); ok {
-		digest = digested.Digest().String()
-	}
-	// set default tag - the domain is set via addDefaultDomain before parsing
-	if digest == "" && tag == "" {
-		tag = "latest"
-	}
-	return &ImageInfo{
-		Registry:    registry,
-		Name:        name,
-		Path:        path,
-		Tag:         tag,
-		Digest:      digest,
-		JSONPointer: jsonPointer,
-	}, nil
-}
-
-func addDefaultDomain(name string) string {
-	i := strings.IndexRune(name, '/')
-	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
-		return "docker.io/" + name
-	}
-	return name
-}
-
 // MutateResourceWithImageInfo will set images to their canonical form so that they can be compared
 // in a predictable manner. This sets the default registry as `docker.io` and the tag as `latest` if
 // these are missing.
@@ -183,19 +14,15 @@ func MutateResourceWithImageInfo(raw []byte, ctx Interface) error {
 	if images == nil {
 		return nil
 	}
+	var patches [][]byte
 	buildJSONPatch := func(op, path, value string) []byte {
 		p := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value":"%s" }`, op, path, value)
 		return []byte(p)
 	}
-	var patches [][]byte
-	for _, info := range images.Containers {
-		patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
-	}
-	for _, info := range images.InitContainers {
-		patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
-	}
-	for _, info := range images.EphemeralContainers {
-		patches = append(patches, buildJSONPatch("replace", info.JSONPointer, info.String()))
+	for _, infoMaps := range images {
+		for _, info := range infoMaps {
+			patches = append(patches, buildJSONPatch("replace", info.Pointer, info.String()))
+		}
 	}
 	patchedResource, err := engineutils.ApplyPatches(raw, patches)
 	if err != nil {
diff --git a/pkg/engine/context/imageutils_test.go b/pkg/engine/context/imageutils_test.go
deleted file mode 100644
index 1b43eb7f7a..0000000000
--- a/pkg/engine/context/imageutils_test.go
+++ /dev/null
@@ -1,125 +0,0 @@
-package context
-
-import (
-	"testing"
-
-	"github.com/kyverno/kyverno/pkg/engine/utils"
-	"github.com/stretchr/testify/assert"
-	"sigs.k8s.io/controller-runtime/pkg/log"
-)
-
-func Test_extractImageInfo(t *testing.T) {
-	tests := []struct {
-		raw                 []byte
-		containers          []ContainerImage
-		initContainers      []ContainerImage
-		ephemeralContainers []ContainerImage
-	}{
-		{
-			raw:                 []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"initContainers": [{"name": "init","image": "index.docker.io/busybox:v1.2.3"}],"containers": [{"name": "nginx","image": "nginx:latest"}], "ephemeralContainers": [{"name": "ephemeral", "image":"test/nginx:latest"}]}}`),
-			initContainers:      []ContainerImage{{Name: "init", ImageInfo: ImageInfo{Registry: "index.docker.io", Name: "busybox", Path: "busybox", Tag: "v1.2.3", JSONPointer: "/spec/initContainers/0/image"}}},
-			containers:          []ContainerImage{{Name: "nginx", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "nginx", Tag: "latest", JSONPointer: "/spec/containers/0/image"}}},
-			ephemeralContainers: []ContainerImage{{Name: "ephemeral", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", JSONPointer: "/spec/ephemeralContainers/0/image"}}},
-		},
-		{
-			raw:                 []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"containers": [{"name": "nginx","image": "test/nginx:latest"}]}}`),
-			initContainers:      []ContainerImage{},
-			containers:          []ContainerImage{{Name: "nginx", ImageInfo: ImageInfo{Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", JSONPointer: "/spec/containers/0/image"}}},
-			ephemeralContainers: []ContainerImage{},
-		},
-		{
-			raw:                 []byte(`{"apiVersion": "apps/v1","kind": "Deployment","metadata": {"name": "myapp"},"spec": {"selector": {"matchLabels": {"app": "myapp"}},"template": {"metadata": {"labels": {"app": "myapp"}},"spec": {"initContainers": [{"name": "init","image": "fictional.registry.example:10443/imagename:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}],"containers": [{"name": "myapp","image": "fictional.registry.example:10443/imagename"}],"ephemeralContainers": [{"name": "ephemeral","image": "fictional.registry.example:10443/imagename:tag@sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}] }}}}`),
-			initContainers:      []ContainerImage{{Name: "init", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", JSONPointer: "/spec/template/spec/initContainers/0/image", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}}},
-			containers:          []ContainerImage{{Name: "myapp", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "latest", JSONPointer: "/spec/template/spec/containers/0/image"}}},
-			ephemeralContainers: []ContainerImage{{Name: "ephemeral", ImageInfo: ImageInfo{Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", JSONPointer: "/spec/template/spec/ephemeralContainers/0/image", Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}}},
-		},
-		{
-			raw:        []byte(`{"apiVersion": "batch/v1beta1","kind": "CronJob","metadata": {"name": "hello"},"spec": {"schedule": "*/1 * * * *","jobTemplate": {"spec": {"template": {"spec": {"containers": [{"name": "hello","image": "test.example.com/test/my-app:v2"}]}}}}}}`),
-			containers: []ContainerImage{{Name: "hello", ImageInfo: ImageInfo{Registry: "test.example.com", Name: "my-app", Path: "test/my-app", Tag: "v2", JSONPointer: "/spec/jobTemplate/spec/template/spec/containers/0/image"}}},
-		},
-	}
-
-	for _, test := range tests {
-		resource, err := utils.ConvertToUnstructured(test.raw)
-		assert.Nil(t, err)
-
-		init, container, ephemeral := extractImageInfo(resource, log.Log.WithName("TestExtractImageInfo"))
-		if len(test.initContainers) > 0 {
-			assert.Equal(t, test.initContainers, init, "unexpected initContainers %s", resource.GetName())
-		}
-
-		if len(test.containers) > 0 {
-			assert.Equal(t, test.containers, container, "unexpected containers %s", resource.GetName())
-		}
-
-		if len(test.ephemeralContainers) > 0 {
-			assert.Equal(t, test.ephemeralContainers, ephemeral, "unexpected ephemeralContainers %s", resource.GetName())
-		}
-	}
-}
-
-func Test_ImageInfo_String(t *testing.T) {
-	validateImageInfo(t,
-		"nginx",
-		"nginx",
-		"nginx",
-		"docker.io",
-		"latest",
-		"",
-		"docker.io/nginx:latest")
-
-	validateImageInfo(t,
-		"nginx:v10.3",
-		"nginx",
-		"nginx",
-		"docker.io",
-		"v10.3",
-		"",
-		"docker.io/nginx:v10.3")
-
-	validateImageInfo(t,
-		"docker.io/test/nginx:v10.3",
-		"nginx",
-		"test/nginx",
-		"docker.io",
-		"v10.3",
-		"",
-		"docker.io/test/nginx:v10.3")
-
-	validateImageInfo(t,
-		"test/nginx",
-		"nginx",
-		"test/nginx",
-		"docker.io",
-		"latest",
-		"",
-		"docker.io/test/nginx:latest")
-
-	validateImageInfo(t,
-		"localhost:4443/test/nginx",
-		"nginx",
-		"test/nginx",
-		"localhost:4443",
-		"latest",
-		"",
-		"localhost:4443/test/nginx:latest")
-	validateImageInfo(t,
-		"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
-		"centos",
-		"test/centos",
-		"docker.io",
-		"",
-		"sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
-		"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f")
-}
-
-func validateImageInfo(t *testing.T, raw, name, path, registry, tag, digest, str string) {
-	i1, err := newImageInfo(raw, "/spec/containers/0/image")
-	assert.Nil(t, err)
-	assert.Equal(t, name, i1.Name)
-	assert.Equal(t, path, i1.Path)
-	assert.Equal(t, registry, i1.Registry)
-	assert.Equal(t, tag, i1.Tag)
-	assert.Equal(t, digest, i1.Digest)
-	assert.Equal(t, str, i1.String())
-}
diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go
index 2ee6b198e8..0a96474879 100644
--- a/pkg/engine/imageVerify.go
+++ b/pkg/engine/imageVerify.go
@@ -5,18 +5,18 @@ import (
 	"fmt"
 	"time"
 
-	v1 "github.com/kyverno/kyverno/api/kyverno/v1"
-	"github.com/kyverno/kyverno/pkg/autogen"
-	"github.com/kyverno/kyverno/pkg/engine/variables"
-	"github.com/kyverno/kyverno/pkg/registryclient"
-	"github.com/pkg/errors"
-
 	"github.com/go-logr/logr"
 	wildcard "github.com/kyverno/go-wildcard"
+	v1 "github.com/kyverno/kyverno/api/kyverno/v1"
+	"github.com/kyverno/kyverno/pkg/autogen"
 	"github.com/kyverno/kyverno/pkg/cosign"
 	"github.com/kyverno/kyverno/pkg/engine/context"
 	"github.com/kyverno/kyverno/pkg/engine/response"
 	"github.com/kyverno/kyverno/pkg/engine/utils"
+	"github.com/kyverno/kyverno/pkg/engine/variables"
+	"github.com/kyverno/kyverno/pkg/registryclient"
+	imageutils "github.com/kyverno/kyverno/pkg/utils/image"
+	"github.com/pkg/errors"
 	"sigs.k8s.io/controller-runtime/pkg/log"
 )
 
@@ -81,9 +81,7 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe
 		}
 
 		for _, imageVerify := range ruleCopy.VerifyImages {
-			iv.verify(imageVerify, images.Containers)
-			iv.verify(imageVerify, images.InitContainers)
-			iv.verify(imageVerify, images.EphemeralContainers)
+			iv.verify(imageVerify, images)
 		}
 	}
 
@@ -125,36 +123,39 @@ type imageVerifier struct {
 	resp          *response.EngineResponse
 }
 
-func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]context.ImageInfo) {
+func (iv *imageVerifier) verify(imageVerify *v1.ImageVerification, images map[string]map[string]imageutils.ImageInfo) {
 	imagePattern := imageVerify.Image
 
-	for _, imageInfo := range images {
-		image := imageInfo.String()
-		jmespath := utils.JsonPointerToJMESPath(imageInfo.JSONPointer)
-		changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
-		if err == nil && !changed {
-			iv.logger.V(4).Info("no change in image, skipping check", "image", image)
-			continue
-		}
-
-		if !wildcard.Match(imagePattern, image) {
-			iv.logger.V(4).Info("image does not match pattern", "image", image, "pattern", imagePattern)
-			continue
-		}
-
-		var ruleResp *response.RuleResponse
-		if len(imageVerify.Attestations) == 0 {
-			var digest string
-			ruleResp, digest = iv.verifySignature(imageVerify, imageInfo)
-			if ruleResp.Status == response.RuleStatusPass {
-				iv.patchDigest(imageInfo, digest, ruleResp)
+	for _, infoMap := range images {
+		for _, imageInfo := range infoMap {
+			path := imageInfo.Path
+			image := imageInfo.String()
+			jmespath := utils.JsonPointerToJMESPath(path)
+			changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
+			if err == nil && !changed {
+				iv.logger.V(4).Info("no change in image, skipping check", "image", image)
+				continue
 			}
-		} else {
-			ruleResp = iv.attestImage(imageVerify, imageInfo)
-		}
 
-		iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
-		incrementAppliedCount(iv.resp)
+			if !wildcard.Match(imagePattern, image) {
+				iv.logger.V(4).Info("image does not match pattern", "image", image, "pattern", imagePattern)
+				continue
+			}
+
+			var ruleResp *response.RuleResponse
+			if len(imageVerify.Attestations) == 0 {
+				var digest string
+				ruleResp, digest = iv.verifySignature(imageVerify, imageInfo)
+				if ruleResp.Status == response.RuleStatusPass {
+					iv.patchDigest(path, imageInfo, digest, ruleResp)
+				}
+			} else {
+				ruleResp = iv.attestImage(imageVerify, imageInfo)
+			}
+
+			iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp)
+			incrementAppliedCount(iv.resp)
+		}
 	}
 }
 
@@ -167,7 +168,7 @@ func getSignatureRepository(imageVerify *v1.ImageVerification) string {
 	return repository
 }
 
-func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imageInfo context.ImageInfo) (*response.RuleResponse, string) {
+func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imageInfo imageutils.ImageInfo) (*response.RuleResponse, string) {
 	image := imageInfo.String()
 	iv.logger.Info("verifying image", "image", image)
 
@@ -219,11 +220,11 @@ func (iv *imageVerifier) verifySignature(imageVerify *v1.ImageVerification, imag
 	return ruleResp, digest
 }
 
-func (iv *imageVerifier) patchDigest(imageInfo context.ImageInfo, digest string, ruleResp *response.RuleResponse) {
+func (iv *imageVerifier) patchDigest(path string, imageInfo imageutils.ImageInfo, digest string, ruleResp *response.RuleResponse) {
 	if imageInfo.Digest == "" {
-		patch, err := makeAddDigestPatch(imageInfo, digest)
+		patch, err := makeAddDigestPatch(path, imageInfo, digest)
 		if err != nil {
-			iv.logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", imageInfo.JSONPointer)
+			iv.logger.Error(err, "failed to patch image with digest", "image", imageInfo.String(), "jsonPath", path)
 		} else {
 			iv.logger.V(4).Info("patching verified image with digest", "patch", string(patch))
 			ruleResp.Patches = [][]byte{patch}
@@ -231,15 +232,15 @@ func (iv *imageVerifier) patchDigest(imageInfo context.ImageInfo, digest string,
 	}
 }
 
-func makeAddDigestPatch(imageInfo context.ImageInfo, digest string) ([]byte, error) {
+func makeAddDigestPatch(path string, imageInfo imageutils.ImageInfo, digest string) ([]byte, error) {
 	var patch = make(map[string]interface{})
 	patch["op"] = "replace"
-	patch["path"] = imageInfo.JSONPointer
+	patch["path"] = path
 	patch["value"] = imageInfo.String() + "@" + digest
 	return json.Marshal(patch)
 }
 
-func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo context.ImageInfo) *response.RuleResponse {
+func (iv *imageVerifier) attestImage(imageVerify *v1.ImageVerification, imageInfo imageutils.ImageInfo) *response.RuleResponse {
 	image := imageInfo.String()
 	start := time.Now()
 
@@ -291,7 +292,7 @@ func buildStatementMap(statements []map[string]interface{}) map[string][]map[str
 	return results
 }
 
-func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]interface{}, img context.ImageInfo) (bool, error) {
+func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]interface{}, img imageutils.ImageInfo) (bool, error) {
 	if len(a.Conditions) == 0 {
 		return true, nil
 	}
@@ -308,7 +309,7 @@ func (iv *imageVerifier) checkAttestations(a *v1.Attestation, s map[string]inter
 		return false, errors.Wrapf(err, fmt.Sprintf("failed to add Statement to the context %v", s))
 	}
 
-	if err := iv.policyContext.JSONContext.AddImageInfo(&img); err != nil {
+	if err := iv.policyContext.JSONContext.AddImageInfo(img); err != nil {
 		return false, errors.Wrapf(err, fmt.Sprintf("failed to add image to the context %v", s))
 	}
 
diff --git a/pkg/utils/image/infos.go b/pkg/utils/image/infos.go
new file mode 100644
index 0000000000..ee75393e94
--- /dev/null
+++ b/pkg/utils/image/infos.go
@@ -0,0 +1,77 @@
+package image
+
+import (
+	"strings"
+
+	"github.com/distribution/distribution/reference"
+	"github.com/pkg/errors"
+)
+
+type ImageInfo struct {
+	// Registry is the URL address of the image registry e.g. `docker.io`
+	Registry string `json:"registry,omitempty"`
+
+	// Name is the image name portion e.g. `busybox`
+	Name string `json:"name"`
+
+	// Path is the repository path and image name e.g. `some-repository/busybox`
+	Path string `json:"path"`
+
+	// Tag is the image tag e.g. `v2`
+	Tag string `json:"tag,omitempty"`
+
+	// Digest is the image digest portion e.g. `sha256:128c6e3534b842a2eec139999b8ce8aa9a2af9907e2b9269550809d18cd832a3`
+	Digest string `json:"digest,omitempty"`
+
+	// Pointer is the path to the image object in the resource
+	Pointer string `json:"-"`
+}
+
+func (i *ImageInfo) String() string {
+	image := i.Registry + "/" + i.Path
+	if i.Digest != "" {
+		return image + "@" + i.Digest
+	} else {
+		return image + ":" + i.Tag
+	}
+}
+
+func GetImageInfo(image string, pointer string) (*ImageInfo, error) {
+	image = addDefaultDomain(image)
+	ref, err := reference.Parse(image)
+	if err != nil {
+		return nil, errors.Wrapf(err, "bad image: %s", image)
+	}
+	var registry, path, name, tag, digest string
+	if named, ok := ref.(reference.Named); ok {
+		registry = reference.Domain(named)
+		path = reference.Path(named)
+		name = path[strings.LastIndex(path, "/")+1:]
+	}
+	if tagged, ok := ref.(reference.Tagged); ok {
+		tag = tagged.Tag()
+	}
+	if digested, ok := ref.(reference.Digested); ok {
+		digest = digested.Digest().String()
+	}
+	// set default tag - the domain is set via addDefaultDomain before parsing
+	if digest == "" && tag == "" {
+		tag = "latest"
+	}
+	return &ImageInfo{
+		Registry: registry,
+		Name:     name,
+		Path:     path,
+		Tag:      tag,
+		Digest:   digest,
+		Pointer:  pointer,
+	}, nil
+}
+
+func addDefaultDomain(name string) string {
+	i := strings.IndexRune(name, '/')
+	if i == -1 || (!strings.ContainsAny(name[:i], ".:") && name[:i] != "localhost" && strings.ToLower(name[:i]) == name[:i]) {
+		return "docker.io/" + name
+	}
+	return name
+}
diff --git a/pkg/utils/image/infos_test.go b/pkg/utils/image/infos_test.go
new file mode 100644
index 0000000000..f608e855c1
--- /dev/null
+++ b/pkg/utils/image/infos_test.go
@@ -0,0 +1,73 @@
+package image
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_GetImageInfo(t *testing.T) {
+	validateImageInfo(t,
+		"nginx",
+		"nginx",
+		"nginx",
+		"docker.io",
+		"latest",
+		"",
+		"docker.io/nginx:latest")
+
+	validateImageInfo(t,
+		"nginx:v10.3",
+		"nginx",
+		"nginx",
+		"docker.io",
+		"v10.3",
+		"",
+		"docker.io/nginx:v10.3")
+
+	validateImageInfo(t,
+		"docker.io/test/nginx:v10.3",
+		"nginx",
+		"test/nginx",
+		"docker.io",
+		"v10.3",
+		"",
+		"docker.io/test/nginx:v10.3")
+
+	validateImageInfo(t,
+		"test/nginx",
+		"nginx",
+		"test/nginx",
+		"docker.io",
+		"latest",
+		"",
+		"docker.io/test/nginx:latest")
+
+	validateImageInfo(t,
+		"localhost:4443/test/nginx",
+		"nginx",
+		"test/nginx",
+		"localhost:4443",
+		"latest",
+		"",
+		"localhost:4443/test/nginx:latest")
+	validateImageInfo(t,
+		"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
+		"centos",
+		"test/centos",
+		"docker.io",
+		"",
+		"sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f",
+		"docker.io/test/centos@sha256:dead07b4d8ed7e29e98de0f4504d87e8880d4347859d839686a31da35a3b532f")
+}
+
+func validateImageInfo(t *testing.T, raw, name, path, registry, tag, digest, str string) {
+	i1, err := GetImageInfo(raw, "")
+	assert.Nil(t, err)
+	assert.Equal(t, name, i1.Name)
+	assert.Equal(t, path, i1.Path)
+	assert.Equal(t, registry, i1.Registry)
+	assert.Equal(t, tag, i1.Tag)
+	assert.Equal(t, digest, i1.Digest)
+	assert.Equal(t, str, i1.String())
+}
diff --git a/pkg/utils/kube/image.go b/pkg/utils/kube/image.go
new file mode 100644
index 0000000000..93bf021ea1
--- /dev/null
+++ b/pkg/utils/kube/image.go
@@ -0,0 +1,119 @@
+package kube
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	imageutils "github.com/kyverno/kyverno/pkg/utils/image"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+var (
+	podExtractors               = BuildStandardExtractors("spec")
+	podControllerExtractors     = BuildStandardExtractors("spec", "template", "spec")
+	cronjobControllerExtractors = BuildStandardExtractors("spec", "jobTemplate", "spec", "template", "spec")
+	registeredExtractors        = map[string][]ImageExtractor{
+		"Pod":         podExtractors,
+		"DaemonSet":   podControllerExtractors,
+		"Deployment":  podControllerExtractors,
+		"ReplicaSet":  podControllerExtractors,
+		"StatefulSet": podControllerExtractors,
+		"CronJob":     cronjobControllerExtractors,
+		"Job":         podControllerExtractors,
+	}
+)
+
+type ImageExtractor struct {
+	fields []string
+	key    string
+	value  string
+	name   string
+}
+
+func (i *ImageExtractor) ExtractFromResource(resource interface{}) (map[string]imageutils.ImageInfo, error) {
+	imageInfo := map[string]imageutils.ImageInfo{}
+	if err := extract(resource, []string{}, i.key, i.value, i.fields, &imageInfo); err != nil {
+		return nil, err
+	}
+	return imageInfo, nil
+}
+
+func extract(obj interface{}, path []string, keyPath, valuePath string, fields []string, imageInfos *map[string]imageutils.ImageInfo) error {
+	if obj == nil {
+		return nil
+	}
+	if len(fields) > 0 && fields[0] == "*" {
+		switch typedObj := obj.(type) {
+		case []interface{}:
+			for i, v := range typedObj {
+				if err := extract(v, append(path, strconv.Itoa(i)), keyPath, valuePath, fields[1:], imageInfos); err != nil {
+					return err
+				}
+			}
+		case map[string]interface{}:
+			for i, v := range typedObj {
+				if err := extract(v, append(path, i), keyPath, valuePath, fields[1:], imageInfos); err != nil {
+					return err
+				}
+			}
+		case interface{}:
+			return fmt.Errorf("invalid type")
+		}
+		return nil
+	}
+	output, ok := obj.(map[string]interface{})
+	if !ok {
+		return fmt.Errorf("invalid image config")
+	}
+	if len(fields) == 0 {
+		pointer := fmt.Sprintf("/%s/%s", strings.Join(path, "/"), valuePath)
+		key := pointer
+		if keyPath != "" {
+			key, ok = output[keyPath].(string)
+			if !ok {
+				return fmt.Errorf("invalid key")
+			}
+		}
+		value, ok := output[valuePath].(string)
+		if !ok {
+			return fmt.Errorf("invalid value")
+		}
+		if imageInfo, err := imageutils.GetImageInfo(value, pointer); err != nil {
+			return fmt.Errorf("invalid image %s", value)
+		} else {
+			(*imageInfos)[key] = *imageInfo
+		}
+		return nil
+	}
+	currentPath := fields[0]
+	return extract(output[currentPath], append(path, currentPath), keyPath, valuePath, fields[1:], imageInfos)
+}
+
+func BuildStandardExtractors(tags ...string) []ImageExtractor {
+	var extractors []ImageExtractor
+	for _, tag := range []string{"initContainers", "containers", "ephemeralContainers"} {
+		var t []string
+		t = append(t, tags...)
+		t = append(t, tag)
+		t = append(t, "*")
+		extractors = append(extractors, ImageExtractor{fields: t, key: "name", value: "image", name: tag})
+	}
+	return extractors
+}
+
+func LookupImageExtractor(kind string) []ImageExtractor {
+	return registeredExtractors[kind]
+}
+
+func ExtractImagesFromResource(resource unstructured.Unstructured) (map[string]map[string]imageutils.ImageInfo, error) {
+	infos := map[string]map[string]imageutils.ImageInfo{}
+	for _, extractor := range LookupImageExtractor(resource.GetKind()) {
+		if infoMap, err := extractor.ExtractFromResource(resource.Object); err != nil {
+			return nil, err
+		} else if infoMap != nil && len(infoMap) > 0 {
+			infos[extractor.name] = infoMap
+		}
+	}
+	return infos, nil
+}
diff --git a/pkg/utils/kube/image_test.go b/pkg/utils/kube/image_test.go
new file mode 100644
index 0000000000..ae65a49fec
--- /dev/null
+++ b/pkg/utils/kube/image_test.go
@@ -0,0 +1,69 @@
+package kube
+
+import (
+	"testing"
+
+	"github.com/kyverno/kyverno/pkg/engine/utils"
+	"github.com/kyverno/kyverno/pkg/utils/image"
+	imageutils "github.com/kyverno/kyverno/pkg/utils/image"
+	"gotest.tools/assert"
+)
+
+func Test_extractImageInfo(t *testing.T) {
+	tests := []struct {
+		raw    []byte
+		images map[string]map[string]imageutils.ImageInfo
+	}{
+		{
+			raw: []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"initContainers": [{"name": "init","image": "index.docker.io/busybox:v1.2.3"}],"containers": [{"name": "nginx","image": "nginx:latest"}], "ephemeralContainers": [{"name": "ephemeral", "image":"test/nginx:latest"}]}}`),
+			images: map[string]map[string]image.ImageInfo{
+				"initContainers": {
+					"init": {Registry: "index.docker.io", Name: "busybox", Path: "busybox", Tag: "v1.2.3", Pointer: "/spec/initContainers/0/image"},
+				},
+				"containers": {
+					"nginx": {Registry: "docker.io", Name: "nginx", Path: "nginx", Tag: "latest", Pointer: "/spec/containers/0/image"},
+				},
+				"ephemeralContainers": {
+					"ephemeral": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/spec/ephemeralContainers/0/image"},
+				},
+			},
+		},
+		{
+			raw: []byte(`{"apiVersion": "v1","kind": "Pod","metadata": {"name": "myapp"},"spec": {"containers": [{"name": "nginx","image": "test/nginx:latest"}]}}`),
+			images: map[string]map[string]imageutils.ImageInfo{
+				"containers": {
+					"nginx": {Registry: "docker.io", Name: "nginx", Path: "test/nginx", Tag: "latest", Pointer: "/spec/containers/0/image"},
+				},
+			},
+		},
+		{
+			raw: []byte(`{"apiVersion": "apps/v1","kind": "Deployment","metadata": {"name": "myapp"},"spec": {"selector": {"matchLabels": {"app": "myapp"}},"template": {"metadata": {"labels": {"app": "myapp"}},"spec": {"initContainers": [{"name": "init","image": "fictional.registry.example:10443/imagename:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"}],"containers": [{"name": "myapp","image": "fictional.registry.example:10443/imagename"}],"ephemeralContainers": [{"name": "ephemeral","image": "fictional.registry.example:10443/imagename:tag@sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"}] }}}}`),
+			images: map[string]map[string]imageutils.ImageInfo{
+				"initContainers": {
+					"init": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", Pointer: "/spec/template/spec/initContainers/0/image"},
+				},
+				"containers": {
+					"myapp": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "latest", Pointer: "/spec/template/spec/containers/0/image"},
+				},
+				"ephemeralContainers": {
+					"ephemeral": {Registry: "fictional.registry.example:10443", Name: "imagename", Path: "imagename", Tag: "tag", Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Pointer: "/spec/template/spec/ephemeralContainers/0/image"},
+				},
+			},
+		},
+		{
+			raw: []byte(`{"apiVersion": "batch/v1beta1","kind": "CronJob","metadata": {"name": "hello"},"spec": {"schedule": "*/1 * * * *","jobTemplate": {"spec": {"template": {"spec": {"containers": [{"name": "hello","image": "test.example.com/test/my-app:v2"}]}}}}}}`),
+			images: map[string]map[string]imageutils.ImageInfo{
+				"containers": {
+					"hello": {Registry: "test.example.com", Name: "my-app", Path: "test/my-app", Tag: "v2", Pointer: "/spec/jobTemplate/spec/template/spec/containers/0/image"},
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		resource, err := utils.ConvertToUnstructured(test.raw)
+		assert.NilError(t, err)
+		images, err := ExtractImagesFromResource(*resource)
+		assert.DeepEqual(t, test.images, images)
+	}
+}