From 739e6a21c40db8299c94adff65a166e460892f9b Mon Sep 17 00:00:00 2001
From: Ammar Yasser <aerosound161@gmail.com>
Date: Thu, 19 Dec 2024 09:42:54 +0200
Subject: [PATCH] Mutate existing CLI support (#11453)

* feat: Add flags for target resources and add fake client initialization

Signed-off-by: aerosouund <aerosound161@gmail.com>

* feat: Add fake discovery client and cluster bool in the policy processor

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Use the full mutation engine policy response in the engine response

Signed-off-by: aerosouund <aerosound161@gmail.com>

* feat: Extract mutated targets from the policy responses and print them out

Signed-off-by: aerosouund <aerosound161@gmail.com>

* feat: Add TargetResources field in the cli test schema

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Generate CLI crds

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: modify checkResult to take an arbitrary actual resource and resource name

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: change getAndCompareResource to take a resource name and cascade it to GetResourceFromPath

Signed-off-by: aerosouund <aerosound161@gmail.com>

* test: Create a simple test to test mutate existing in the CLI

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Allow GetResourceFromPath to select a resource with a name from a multi resource yaml

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Modify the runTest command to return the TestResponse type

- Create a fake client, load the target resources in it and use it in the PolicyProcessor.
- Create the TestResponse which contains Trigger and Target fields, each is a map of gvk/name to the responses corresponding to that resource.

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Rewrite output.go to use the TestResponse type

- Check for both target and trigger
- Create logic for appending the resource array in case no resources are passed
- Move row creation logic into a separate method to avoid code duplication
- Extract the proper target resource based on rule type
- Create a function to extract mutated target from the engine response

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Move tests to the correct folder

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Use apiVersion/Kind/Name as the key in the test responses

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Use the apiVersion/Kind/name key schema in checking results and fix invalid resource name checking for generate policies

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Use better variable names for rows

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Account for Generate resources being an array

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Use generated resource name in checking the results and printing output

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Uncomment checks printing

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Remove bug in engine response creation

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Move the generate logic into an else block

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Fix namespace fetching bug in cel validator

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Use pointer to int in the test counter

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Remove redundant method

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Skip resources not being found in the manifests

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Create another field in the engine to denote if this is a cluster engine or an offline engine

Simply checking for the client being nil is no longer enough because for cli operations the client will be a fake client
A pointer to bool is chosen because callers who don't necessarily know what to pass should be able to pass nil

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: Add extra argument in fake client initiation

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: add extra argument in fuzz test

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: Add extra arg

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: Handle resources specified as ns/name as this schema will be deprecated in favor of apiVersion/Kind/Name

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: Fix linter complaints

Signed-off-by: ammar <ammar.yasser@vodafone.com>

* fix: Use comma separation as array separators as kubernetes names don't support commas

To avoid undefined array length on splitting on /
using commas will result in a fixed length since all resources will have an apiVersion, kind, namespace and name

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Change resource array type to an array of any instead of array of string

To support the use of a string or a TestResourceSpec

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Expect the resource array to be an array of string or array of TestResourceSpec

Assert that an array element is either of these types and match the resources in both cases according to the element type
Expect that the key in responses is now separated by commas instead of slashes

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Expect that the resource array is now of type array of any and modify tests that use it

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Skip response check if the policy name isnt whats in the result

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Match the name if its specified as ns/name

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Fix linter complaint

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Run codegen

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Create CLI CRDs

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Run codegen

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Fix linter complaints

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Cleanup invalid code used in FixTest to adapt it to the schema changes

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Check if resource is nil before extracting

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: use the loadResources method to open targets in a directory

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Account for target resources with the same name but different namespaces

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Add CLI test for mutate existing with the same name

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Infer resource name and namespace from the actual resource and account for resources with the same name and namespace but different kinds

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: remove extra line

Signed-off-by: aerosouund <aerosound161@gmail.com>

* feat: Add printing mutate existing resources to the output or to a file

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Minor fixes

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: fix linter complaint

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: codegen

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Revert result back to error

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Use io discard to not print resources in the test command

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Update vague comments and remove outdated ones

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Integrate mutate existing changes with diff generation

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Move resource key generation into a function

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Add a mutate existing test that fails

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: fix linter complaint

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Remove redundant comment

Signed-off-by: aerosouund <aerosound161@gmail.com>

* refactor: Fix array of any assignment in cli test

Signed-off-by: aerosouund <aerosound161@gmail.com>

* fix: Dont check duplicate strings for field that is an array of any

Signed-off-by: aerosouund <aerosound161@gmail.com>

* bug: Fix appending to the wrong array

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: run fix tests

Signed-off-by: aerosouund <aerosound161@gmail.com>

* chore: Run fix tests

Signed-off-by: aerosouund <aerosound161@gmail.com>

---------

Signed-off-by: aerosouund <aerosound161@gmail.com>
Signed-off-by: ammar <ammar.yasser@vodafone.com>
Signed-off-by: Ammar Yasser <aerosound161@gmail.com>
Co-authored-by: ammar <ammar.yasser@vodafone.com>
Co-authored-by: shuting <shuting@nirmata.com>
---
 cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go |  12 +
 .../apis/v1alpha1/test_result.go              |   2 +-
 .../kubectl-kyverno/commands/apply/command.go |  44 ++-
 .../kubectl-kyverno/commands/test/command.go  |  38 +--
 .../kubectl-kyverno/commands/test/compare.go  |  45 +--
 .../kubectl-kyverno/commands/test/output.go   | 264 +++++++++++++-----
 cmd/cli/kubectl-kyverno/commands/test/test.go |  57 +++-
 .../config/crds/cli.kyverno.io_tests.yaml     |   7 +-
 .../crds/cli.kyverno.io_userinfoes.yaml       |   2 +-
 .../config/crds/cli.kyverno.io_values.yaml    |   2 +-
 .../data/crds/cli.kyverno.io_tests.yaml       |   7 +-
 .../data/crds/cli.kyverno.io_userinfoes.yaml  |   2 +-
 .../data/crds/cli.kyverno.io_values.yaml      |   2 +-
 cmd/cli/kubectl-kyverno/fix/test.go           |  33 ++-
 cmd/cli/kubectl-kyverno/processor/generate.go |   1 +
 .../processor/policy_processor.go             |  35 ++-
 cmd/cli/kubectl-kyverno/resource/resource.go  |  17 +-
 cmd/cli/kubectl-kyverno/test/load.go          |   3 -
 cmd/cli/kubectl-kyverno/test/load_test.go     |  16 +-
 cmd/internal/engine.go                        |   1 +
 docs/user/cli/commands/kyverno_apply.md       |   2 +
 docs/user/cli/crd/index.html                  |  88 +++++-
 .../cli/crd/kyverno_kubectl.v1alpha1.html     |  31 +-
 pkg/engine/engine.go                          |  11 +-
 pkg/engine/fuzz_test.go                       |   3 +
 .../handlers/validation/validate_cel.go       |  10 +-
 pkg/engine/image_verify_test.go               |   2 +
 pkg/engine/mutation_test.go                   |   1 +
 pkg/engine/validation.go                      |   2 +-
 pkg/engine/validation_test.go                 |   1 +
 pkg/webhooks/resource/fake.go                 |   1 +
 pkg/webhooks/resource/validation_test.go      |   2 +
 .../add-default-resources/kyverno-test.yaml   |  14 +-
 .../global-anchor/kyverno-test.yaml           |  14 +-
 .../kyverno-test.yaml                         |  12 +-
 test/cli/test-mutate/kyverno-test.yaml        |  14 +-
 .../mutate-existing-fail/kyverno-test.yaml    |  18 ++
 .../mutate-existing-fail/mutated-secret.yaml  |   7 +
 .../mutate-existing-fail/policy.yaml          |  27 ++
 .../mutate-existing-fail/raw-secret.yaml      |   5 +
 .../mutate-existing-fail/trigger-cm.yaml      |   7 +
 .../mutate-existing/kyverno-test.yaml         |  17 ++
 .../mutate-existing/mutated-secret.yaml       |  15 +
 .../test-mutate/mutate-existing/policy.yaml   |  27 ++
 .../mutate-existing/raw-secret.yaml           |   5 +
 .../mutate-existing/trigger-cm.yaml           |   7 +
 .../same-name-mutate-existing/cm.yaml         |   7 +
 .../kyverno-test.yaml                         |  20 ++
 .../mutated-resources.yaml                    |  16 ++
 .../same-name-mutate-existing/policy.yaml     |  26 ++
 .../same-name-mutate-existing/secret-1.yaml   |   5 +
 .../same-name-mutate-existing/secret-2.yaml   |   5 +
 .../kyverno-test.yaml                         |  14 +-
 53 files changed, 812 insertions(+), 214 deletions(-)
 create mode 100644 test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing-fail/policy.yaml
 create mode 100644 test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml
 create mode 100644 test/cli/test-mutate/mutate-existing/kyverno-test.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing/mutated-secret.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing/policy.yaml
 create mode 100644 test/cli/test-mutate/mutate-existing/raw-secret.yaml
 create mode 100755 test/cli/test-mutate/mutate-existing/trigger-cm.yaml
 create mode 100755 test/cli/test-mutate/same-name-mutate-existing/cm.yaml
 create mode 100644 test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml
 create mode 100755 test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml
 create mode 100755 test/cli/test-mutate/same-name-mutate-existing/policy.yaml
 create mode 100644 test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml
 create mode 100644 test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml

diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
index e5c6f35b2e..4934bdd073 100644
--- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
+++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
@@ -24,6 +24,9 @@ type Test struct {
 	// Resources are the resource to be used in the test
 	Resources []string `json:"resources,omitempty"`
 
+	// Target Resources are for policies that have mutate existing
+	TargetResources []string `json:"targetResources,omitempty"`
+
 	// Variables is the values to be used in the test
 	Variables string `json:"variables,omitempty"`
 
@@ -54,6 +57,15 @@ type CheckResult struct {
 	Error v1alpha1.Any `json:"error"`
 }
 
+type TestResourceSpec struct {
+	Group       string `json:"group,omitempty"`
+	Version     string `json:"version,omitempty"`
+	Kind        string `json:"kind,omitempty"`
+	Namespace   string `json:"namespace,omitempty"`
+	Subresource string `json:"subresource,omitempty"`
+	Name        string `json:"name,omitempty"`
+}
+
 type CheckMatch struct {
 	// Resource filters engine responses
 	Resource *v1alpha1.Any `json:"resource,omitempty"`
diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go
index 6a9f2939cf..2f544723c6 100644
--- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go
+++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test_result.go
@@ -67,5 +67,5 @@ type TestResult struct {
 	TestResultDeprecated `json:",inline,omitempty"`
 
 	// Resources gives us the list of resources on which the policy is going to be applied.
-	Resources []string `json:"resources"`
+	Resources []any `json:"resources"`
 }
diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go
index 4a1e3881d3..c17a740a47 100644
--- a/cmd/cli/kubectl-kyverno/commands/apply/command.go
+++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go
@@ -34,6 +34,8 @@ import (
 	"github.com/spf13/cobra"
 	admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/client-go/dynamic"
 	"k8s.io/client-go/kubernetes"
 )
@@ -60,6 +62,7 @@ type ApplyCommandConfig struct {
 	AuditWarn             bool
 	ResourcePaths         []string
 	PolicyPaths           []string
+	TargetResourcePaths   []string
 	GitBranch             string
 	warnExitCode          int
 	warnNoPassed          bool
@@ -134,6 +137,8 @@ func Command() *cobra.Command {
 	}
 	cmd.Flags().StringSliceVarP(&applyCommandConfig.ResourcePaths, "resource", "r", []string{}, "Path to resource files")
 	cmd.Flags().StringSliceVarP(&applyCommandConfig.ResourcePaths, "resources", "", []string{}, "Path to resource files")
+	cmd.Flags().StringSliceVarP(&applyCommandConfig.TargetResourcePaths, "target-resource", "", []string{}, "Path to individual files containing target resources files for policies that have mutate existing")
+	cmd.Flags().StringSliceVarP(&applyCommandConfig.TargetResourcePaths, "target-resources", "", []string{}, "Path to a directory containing target resources files for policies that have mutate existing")
 	cmd.Flags().BoolVarP(&applyCommandConfig.Cluster, "cluster", "c", false, "Checks if policies should be applied to cluster in the current context")
 	cmd.Flags().StringVarP(&applyCommandConfig.MutateLogPath, "output", "o", "", "Prints the mutated/generated resources in provided file/directory")
 	// currently `set` flag supports variable for single policy applied on single resource
@@ -189,18 +194,28 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
 		return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("failed to decode yaml (%w)", err)
 	}
 	var store store.Store
-	rc, resources1, skipInvalidPolicies, responses1, dClient, err := c.initStoreAndClusterClient(&store, skipInvalidPolicies)
-	if err != nil {
-		return rc, resources1, skipInvalidPolicies, responses1, err
-	}
 	rc, resources1, skipInvalidPolicies, responses1, policies, vaps, vapBindings, err := c.loadPolicies(skipInvalidPolicies)
 	if err != nil {
 		return rc, resources1, skipInvalidPolicies, responses1, err
 	}
-	resources, err := c.loadResources(out, policies, vaps, dClient)
+	var targetResources []*unstructured.Unstructured
+	if len(c.TargetResourcePaths) > 0 {
+		targetResources, err = c.loadResources(out, c.TargetResourcePaths, policies, vaps, nil)
+		if err != nil {
+			return rc, resources1, skipInvalidPolicies, responses1, err
+		}
+	}
+
+	rc, resources1, skipInvalidPolicies, responses1, dClient, err := c.initStoreAndClusterClient(&store, skipInvalidPolicies, targetResources...)
 	if err != nil {
 		return rc, resources1, skipInvalidPolicies, responses1, err
 	}
+
+	resources, err := c.loadResources(out, c.ResourcePaths, policies, vaps, dClient)
+	if err != nil {
+		return rc, resources1, skipInvalidPolicies, responses1, err
+	}
+
 	var exceptions []*kyvernov2.PolicyException
 	if c.inlineExceptions {
 		exceptions = exception.SelectFrom(resources)
@@ -339,6 +354,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource(
 			Stdin:                c.Stdin,
 			Rc:                   &rc,
 			PrintPatchResource:   true,
+			Cluster:              c.Cluster,
 			Client:               dClient,
 			AuditWarn:            c.AuditWarn,
 			Subresources:         vars.Subresources(),
@@ -362,8 +378,8 @@ func (c *ApplyCommandConfig) applyPolicytoResource(
 	return &rc, resources, responses, nil
 }
 
-func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1beta1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) {
-	resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "")
+func (c *ApplyCommandConfig) loadResources(out io.Writer, paths []string, policies []kyvernov1.PolicyInterface, vap []admissionregistrationv1beta1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) {
+	resources, err := common.GetResourceAccordingToResourcePath(out, nil, paths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "")
 	if err != nil {
 		return resources, fmt.Errorf("failed to load resources (%w)", err)
 	}
@@ -429,7 +445,7 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli
 	return nil, nil, skipInvalidPolicies, nil, policies, vaps, vapBindings, nil
 }
 
-func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, dclient.Interface, error) {
+func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies SkippedInvalidPolicies, targetResources ...*unstructured.Unstructured) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, dclient.Interface, error) {
 	store.SetLocal(true)
 	store.SetRegistryAccess(c.RegistryAccess)
 	if c.Cluster {
@@ -455,6 +471,18 @@ func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipI
 			return nil, nil, skipInvalidPolicies, nil, nil, err
 		}
 	}
+	if len(targetResources) > 0 && !c.Cluster {
+		var targets []runtime.Object
+		for _, t := range targetResources {
+			targets = append(targets, t)
+		}
+
+		dClient, err = dclient.NewFakeClient(runtime.NewScheme(), map[schema.GroupVersionResource]string{}, targets...)
+		dClient.SetDiscovery(dclient.NewFakeDiscoveryClient(nil))
+		if err != nil {
+			return nil, nil, skipInvalidPolicies, nil, nil, err
+		}
+	}
 	return nil, nil, skipInvalidPolicies, nil, dClient, err
 }
 
diff --git a/cmd/cli/kubectl-kyverno/commands/test/command.go b/cmd/cli/kubectl-kyverno/commands/test/command.go
index 87670876f8..6676c0789d 100644
--- a/cmd/cli/kubectl-kyverno/commands/test/command.go
+++ b/cmd/cli/kubectl-kyverno/commands/test/command.go
@@ -17,7 +17,6 @@ import (
 	"github.com/sergi/go-diff/diffmatchpatch"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
-	"k8s.io/client-go/tools/cache"
 )
 
 func Command() *cobra.Command {
@@ -127,7 +126,7 @@ func testCommandExecute(
 			if err := printTestResult(filteredResults, responses, rc, &resultsTable, test.Fs, resourcePath); err != nil {
 				return fmt.Errorf("failed to print test result (%w)", err)
 			}
-			if err := printCheckResult(test.Test.Checks, responses, rc, &resultsTable); err != nil {
+			if err := printCheckResult(test.Test.Checks, *responses, rc, &resultsTable); err != nil {
 				return fmt.Errorf("failed to print test result (%w)", err)
 			}
 			fullTable.AddFailed(resultsTable.RawRows...)
@@ -152,19 +151,15 @@ func testCommandExecute(
 	return nil
 }
 
-func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath string, response engineapi.EngineResponse, rule engineapi.RuleResponse) (bool, string, string) {
+func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath string, response engineapi.EngineResponse, rule engineapi.RuleResponse, actualResource unstructured.Unstructured) (bool, string, string) {
 	expected := test.Result
 	// fallback to the deprecated field
 	if expected == "" {
 		expected = test.Status
 	}
 	// fallback on deprecated field
-	patchedResource := test.PatchedResource
-	if test.PatchedResources != "" {
-		patchedResource = test.PatchedResources
-	}
-	if patchedResource != "" {
-		equals, diff, err := getAndCompareResource([]*unstructured.Unstructured{&response.PatchedResource}, fs, filepath.Join(resoucePath, patchedResource))
+	if test.PatchedResource != "" {
+		equals, diff, err := getAndCompareResource(actualResource, fs, filepath.Join(resoucePath, test.PatchedResource))
 		if err != nil {
 			return false, err.Error(), "Resource error"
 		}
@@ -175,14 +170,14 @@ func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath stri
 		}
 	}
 	if test.GeneratedResource != "" {
-		equals, diff, err := getAndCompareResource(rule.GeneratedResources(), fs, filepath.Join(resoucePath, test.GeneratedResource))
+		equals, diff, err := getAndCompareResource(actualResource, fs, filepath.Join(resoucePath, test.GeneratedResource))
 		if err != nil {
 			return false, err.Error(), "Resource error"
 		}
 		if !equals {
 			dmp := diffmatchpatch.New()
 			legend := dmp.DiffPrettyText(dmp.DiffMain("only in expected", "only in actual", false))
-			return false, fmt.Sprintf("Generated resource didn't match the generated resource in the test result\n(%s)\n\n%s", legend, diff), "Resource diff"
+			return false, fmt.Sprintf("Patched resource didn't match the generated resource in the test result\n(%s)\n\n%s", legend, diff), "Resource diff"
 		}
 	}
 	result := report.ComputePolicyReportResult(false, response, rule)
@@ -192,27 +187,6 @@ func checkResult(test v1alpha1.TestResult, fs billy.Filesystem, resoucePath stri
 	return true, result.Message, "Ok"
 }
 
-func lookupEngineResponses(test v1alpha1.TestResult, resourceName string, responses ...engineapi.EngineResponse) []engineapi.EngineResponse {
-	matches := make([]engineapi.EngineResponse, 0, len(responses))
-	for _, response := range responses {
-		policy := response.Policy()
-		resource := response.Resource
-		pName := cache.MetaObjectToName(policy.MetaObject()).String()
-		rName := cache.MetaObjectToName(&resource).String()
-		if test.Kind != resource.GetKind() {
-			continue
-		}
-		if pName != test.Policy {
-			continue
-		}
-		if resourceName != "" && rName != resourceName && resource.GetName() != resourceName {
-			continue
-		}
-		matches = append(matches, response)
-	}
-	return matches
-}
-
 func lookupRuleResponses(test v1alpha1.TestResult, responses ...engineapi.RuleResponse) []engineapi.RuleResponse {
 	var matches []engineapi.RuleResponse
 	// Since there are no rules in case of validating admission policies, responses are returned without checking rule names.
diff --git a/cmd/cli/kubectl-kyverno/commands/test/compare.go b/cmd/cli/kubectl-kyverno/commands/test/compare.go
index 5feaa28aac..26aa307434 100644
--- a/cmd/cli/kubectl-kyverno/commands/test/compare.go
+++ b/cmd/cli/kubectl-kyverno/commands/test/compare.go
@@ -11,41 +11,26 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
-func getAndCompareResource(actualResources []*unstructured.Unstructured, fs billy.Filesystem, path string) (bool, string, error) {
-	expectedResources, err := resource.GetResourceFromPath(fs, path)
+func getAndCompareResource(actualResource unstructured.Unstructured, fs billy.Filesystem, path string) (bool, string, error) {
+	expectedResource, err := resource.GetResourceFromPath(fs, path, actualResource.GetAPIVersion(), actualResource.GetKind(), actualResource.GetNamespace(), actualResource.GetName())
 	if err != nil {
 		return false, "", fmt.Errorf("error: failed to load resource (%s)", err)
 	}
+	resource.FixupGenerateLabels(actualResource)
+	resource.FixupGenerateLabels(*expectedResource)
 
-	expectedResourcesMap := map[string]unstructured.Unstructured{}
-	for _, expectedResource := range expectedResources {
-		if expectedResource == nil {
-			continue
-		}
-		r := *expectedResource
-		resource.FixupGenerateLabels(r)
-		expectedResourcesMap[expectedResource.GetNamespace()+"/"+expectedResource.GetName()] = r
+	equals, err := resource.Compare(actualResource, *expectedResource, true)
+	if err != nil {
+		return false, "", fmt.Errorf("error: failed to compare resources (%s)", err)
 	}
-
-	for _, actualResource := range actualResources {
-		if actualResource == nil {
-			continue
-		}
-		r := *actualResource
-		resource.FixupGenerateLabels(r)
-		equals, err := resource.Compare(r, expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()], true)
-		if err != nil {
-			return false, "", fmt.Errorf("error: failed to compare resources (%s)", err)
-		}
-		if !equals {
-			log.Log.V(4).Info("Resource diff", "expected", expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()], "actual", r)
-			es, _ := yaml.Marshal(expectedResourcesMap[r.GetNamespace()+"/"+r.GetName()])
-			as, _ := yaml.Marshal(r)
-			dmp := diffmatchpatch.New()
-			diffs := dmp.DiffMain(string(es), string(as), false)
-			log.Log.V(4).Info("\n" + dmp.DiffPrettyText(diffs) + "\n")
-			return false, dmp.DiffPrettyText(diffs), nil
-		}
+	if !equals {
+		log.Log.V(4).Info("Resource diff", "expected", expectedResource, "actual", actualResource)
+		es, _ := yaml.Marshal(expectedResource)
+		as, _ := yaml.Marshal(actualResource)
+		dmp := diffmatchpatch.New()
+		diffs := dmp.DiffMain(string(es), string(as), false)
+		log.Log.V(4).Info("\n" + dmp.DiffPrettyText(diffs) + "\n")
+		return false, dmp.DiffPrettyText(diffs), nil
 	}
 	return true, "", nil
 }
diff --git a/cmd/cli/kubectl-kyverno/commands/test/output.go b/cmd/cli/kubectl-kyverno/commands/test/output.go
index eee1cb17e1..2a20b0d35e 100644
--- a/cmd/cli/kubectl-kyverno/commands/test/output.go
+++ b/cmd/cli/kubectl-kyverno/commands/test/output.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"io"
+	"strings"
 
 	"github.com/go-git/go-billy/v5"
 	"github.com/kyverno/kyverno-json/pkg/engine/assert"
@@ -12,12 +13,13 @@ import (
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/table"
 	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
 func printCheckResult(
 	checks []v1alpha1.CheckResult,
-	responses []engineapi.EngineResponse,
+	responses TestResponse,
 	rc *resultCounts,
 	resultsTable *table.Table,
 ) error {
@@ -25,7 +27,10 @@ func printCheckResult(
 	testCount := 1
 	for _, check := range checks {
 		// filter engine responses
-		matchingEngineResponses := responses
+		var matchingEngineResponses []engineapi.EngineResponse
+		for _, engineresponses := range responses.Trigger {
+			matchingEngineResponses = append(matchingEngineResponses, engineresponses...)
+		}
 		// 1. by resource
 		if check.Match.Resource != nil {
 			var filtered []engineapi.EngineResponse
@@ -160,9 +165,10 @@ func printCheckResult(
 	return nil
 }
 
+// a test that contains a policy that may contain several rules
 func printTestResult(
 	tests []v1alpha1.TestResult,
-	responses []engineapi.EngineResponse,
+	responses *TestResponse,
 	rc *resultCounts,
 	resultsTable *table.Table,
 	fs billy.Filesystem,
@@ -170,80 +176,144 @@ func printTestResult(
 ) error {
 	testCount := 1
 	for _, test := range tests {
-		// lookup matching engine responses (without the resource name)
-		// to reduce the search scope
-		responses := lookupEngineResponses(test, "", responses...)
-		// TODO fix deprecated fields
-		// identify the resources to be looked up
 		var resources []string
-		if test.Resources != nil {
-			resources = append(resources, test.Resources...)
-		} else if test.Resource != "" {
-			resources = append(resources, test.Resource)
+		// The test specifies certain resources to check, results will be checked for those resources only
+		if test.Resource != "" {
+			test.Resources = append(test.Resources, test.Resource)
 		}
-		for _, resource := range resources {
-			var rows []table.Row
-			// lookup matching engine responses (with the resource name this time)
-			for _, response := range lookupEngineResponses(test, resource, responses...) {
-				// lookup matching rule responses
-				for _, rule := range lookupRuleResponses(test, response.PolicyResponse.Rules...) {
-					// perform test checks
-					ok, message, reason := checkResult(test, fs, resoucePath, response, rule)
-					// if checks failed but we were expecting a fail it's considered a success
-					success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail)
-					row := table.Row{
-						RowCompact: table.RowCompact{
-							ID:        testCount,
-							Policy:    color.Policy("", test.Policy),
-							Rule:      color.Rule(test.Rule),
-							Resource:  color.Resource(test.Kind, test.Namespace, resource),
-							Reason:    reason,
-							IsFailure: !success,
-						},
-						Message: message,
-					}
-					if success {
-						row.Result = color.ResultPass()
-						if test.Result == policyreportv1alpha2.StatusSkip {
-							rc.Skip++
-						} else {
-							rc.Pass++
+		if test.Resources != nil {
+			for _, r := range test.Resources {
+				for _, m := range []map[string][]engineapi.EngineResponse{responses.Target, responses.Trigger} {
+					for resourceGVKAndName := range m {
+						nameParts := strings.Split(resourceGVKAndName, ",")
+						if resourceString, ok := r.(string); ok {
+							nsAndName := strings.Split(resourceString, "/")
+							if len(nsAndName) == 1 {
+								if resourceString == nameParts[len(nameParts)-1] {
+									resources = append(resources, resourceGVKAndName)
+								}
+							}
+							if len(nsAndName) == 2 {
+								if nsAndName[0] == nameParts[len(nameParts)-2] && nsAndName[1] == nameParts[len(nameParts)-1] {
+									resources = append(resources, resourceGVKAndName)
+								}
+							}
 						}
-					} else {
-						row.Result = color.ResultFail()
-						rc.Fail++
-					}
-					testCount++
-					rows = append(rows, row)
-				}
 
-				// if there are no RuleResponse, the resource has been excluded. This is a pass.
-				if len(rows) == 0 {
-					row := table.Row{
-						RowCompact: table.RowCompact{
-							ID:        testCount,
-							Policy:    color.Policy("", test.Policy),
-							Rule:      color.Rule(test.Rule),
-							Resource:  color.Resource(test.Kind, test.Namespace, resource),
-							Result:    color.ResultPass(),
-							Reason:    color.Excluded(),
-							IsFailure: false,
-						},
-						Message: color.Excluded(),
+						if resourceSpec, ok := r.(v1alpha1.TestResourceSpec); ok {
+							if resourceSpec.Group == "" {
+								if resourceSpec.Version != nameParts[0] {
+									continue
+								}
+							} else {
+								if resourceSpec.Group+"/"+resourceSpec.Version != nameParts[0] {
+									continue
+								}
+							}
+							if resourceSpec.Namespace != nameParts[len(nameParts)-2] {
+								continue
+							}
+							if resourceSpec.Name == nameParts[len(nameParts)-1] {
+								resources = append(resources, resourceGVKAndName)
+							}
+						}
 					}
-					rc.Skip++
-					testCount++
-					rows = append(rows, row)
 				}
 			}
-			// if not found
-			if len(rows) == 0 {
+		}
+
+		// The test specifies no resources, check all results
+		if len(resources) == 0 {
+			for r := range responses.Target {
+				resources = append(resources, r)
+			}
+			for r := range responses.Trigger {
+				resources = append(resources, r)
+			}
+		}
+
+		for _, resource := range resources {
+			var rows []table.Row
+			var resourceSkipped bool
+			if _, ok := responses.Trigger[resource]; ok {
+				for _, response := range responses.Trigger[resource] {
+					polNameNs := strings.Split(test.Policy, "/")
+					if response.Policy().GetName() != polNameNs[len(polNameNs)-1] {
+						continue
+					}
+					for _, rule := range lookupRuleResponses(test, response.PolicyResponse.Rules...) {
+						r := response.Resource
+
+						if rule.RuleType() != "Generation" {
+							if rule.RuleType() == "Mutation" {
+								r = response.PatchedResource
+							}
+
+							ok, message, reason := checkResult(test, fs, resoucePath, response, rule, r)
+							if strings.Contains(message, "not found in manifest") {
+								resourceSkipped = true
+								continue
+							}
+
+							success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail)
+							resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, strings.Replace(resource, ",", "/", -1))
+							rows = append(rows, resourceRows...)
+						} else {
+							generatedResources := rule.GeneratedResources()
+							for _, r := range generatedResources {
+								ok, message, reason := checkResult(test, fs, resoucePath, response, rule, *r)
+
+								success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail)
+								resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, r.GetName())
+								rows = append(rows, resourceRows...)
+							}
+						}
+					}
+
+					// if there are no RuleResponse, the resource has been excluded. This is a pass.
+					if len(rows) == 0 && !resourceSkipped {
+						row := table.Row{
+							RowCompact: table.RowCompact{
+								ID:        testCount,
+								Policy:    color.Policy("", test.Policy),
+								Rule:      color.Rule(test.Rule),
+								Resource:  color.Resource(test.Kind, test.Namespace, strings.Replace(resource, ",", "/", -1)),
+								Result:    color.ResultPass(),
+								Reason:    color.Excluded(),
+								IsFailure: false,
+							},
+							Message: color.Excluded(),
+						}
+						rc.Skip++
+						testCount++
+						rows = append(rows, row)
+					}
+				}
+			}
+
+			// Check if the resource specified exists in the targets
+			if _, ok := responses.Target[resource]; ok {
+				for _, response := range responses.Target[resource] {
+					// we are doing this twice which is kinda not nice
+					nameParts := strings.Split(resource, ",")
+					name, ns, kind, apiVersion := nameParts[len(nameParts)-1], nameParts[len(nameParts)-2], nameParts[len(nameParts)-3], nameParts[len(nameParts)-4]
+
+					r, rule := extractPatchedTargetFromEngineResponse(apiVersion, kind, name, ns, response)
+					ok, message, reason := checkResult(test, fs, resoucePath, response, *rule, *r)
+
+					success := ok || (!ok && test.Result == policyreportv1alpha2.StatusFail)
+					resourceRows := createRowsAccordingToResults(test, rc, &testCount, success, message, reason, strings.Replace(resource, ",", "/", -1))
+					rows = append(rows, resourceRows...)
+				}
+			}
+
+			if len(rows) == 0 && !resourceSkipped {
 				row := table.Row{
 					RowCompact: table.RowCompact{
 						ID:        testCount,
 						Policy:    color.Policy("", test.Policy),
 						Rule:      color.Rule(test.Rule),
-						Resource:  color.Resource(test.Kind, test.Namespace, resource),
+						Resource:  color.Resource(test.Kind, test.Namespace, strings.Replace(resource, ",", "/", -1)),
 						IsFailure: true,
 						Result:    color.ResultFail(),
 						Reason:    color.NotFound(),
@@ -261,6 +331,70 @@ func printTestResult(
 	return nil
 }
 
+func createRowsAccordingToResults(test v1alpha1.TestResult, rc *resultCounts, globalTestCounter *int, success bool, message string, reason string, resourceGVKAndName string) []table.Row {
+	resourceParts := strings.Split(resourceGVKAndName, "/")
+	rows := []table.Row{}
+	row := table.Row{
+		RowCompact: table.RowCompact{
+			ID:        *globalTestCounter,
+			Policy:    color.Policy("", test.Policy),
+			Rule:      color.Rule(test.Rule),
+			Resource:  color.Resource(strings.Join(resourceParts[:len(resourceParts)-1], "/"), test.Namespace, resourceParts[len(resourceParts)-1]),
+			Reason:    reason,
+			IsFailure: !success,
+		},
+		Message: message,
+	}
+	if success {
+		row.Result = color.ResultPass()
+		if test.Result == policyreportv1alpha2.StatusSkip {
+			rc.Skip++
+		} else {
+			rc.Pass++
+		}
+	} else {
+		row.Result = color.ResultFail()
+		rc.Fail++
+	}
+	*globalTestCounter++
+	rows = append(rows, row)
+
+	// if there are no RuleResponse, the resource has been excluded. This is a pass.
+	if len(rows) == 0 {
+		row := table.Row{
+			RowCompact: table.RowCompact{
+				ID:        *globalTestCounter,
+				Policy:    color.Policy("", test.Policy),
+				Rule:      color.Rule(test.Rule),
+				Resource:  color.Resource(strings.Join(resourceParts[:len(resourceParts)-1], "/"), test.Namespace, resourceParts[len(resourceParts)-1]), // todo: handle namespace
+				Result:    color.ResultPass(),
+				Reason:    color.Excluded(),
+				IsFailure: false,
+			},
+			Message: color.Excluded(),
+		}
+		rc.Skip++
+		*globalTestCounter++
+		rows = append(rows, row)
+	}
+	return rows
+}
+
+func extractPatchedTargetFromEngineResponse(apiVersion, kind, resourceName, resourceNamespace string, response engineapi.EngineResponse) (*unstructured.Unstructured, *engineapi.RuleResponse) {
+	for _, rule := range response.PolicyResponse.Rules {
+		r, _, _ := rule.PatchedTarget()
+		if r != nil {
+			if resourceNamespace == "" {
+				resourceNamespace = r.GetNamespace()
+			}
+			if r.GetAPIVersion() == apiVersion && r.GetKind() == kind && r.GetName() == resourceName && r.GetNamespace() == resourceNamespace {
+				return r, &rule
+			}
+		}
+	}
+	return nil, nil
+}
+
 func printFailedTestResult(out io.Writer, resultsTable table.Table, detailedResults bool) {
 	printer := table.NewTablePrinter(out)
 	for i := range resultsTable.RawRows {
diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go
index ea543b59ed..cb4c453369 100644
--- a/cmd/cli/kubectl-kyverno/commands/test/test.go
+++ b/cmd/cli/kubectl-kyverno/commands/test/test.go
@@ -26,9 +26,16 @@ import (
 	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
 	policyvalidation "github.com/kyverno/kyverno/pkg/validation/policy"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 )
 
-func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engineapi.EngineResponse, error) {
+type TestResponse struct {
+	Trigger map[string][]engineapi.EngineResponse
+	Target  map[string][]engineapi.EngineResponse
+}
+
+func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) (*TestResponse, error) {
 	// don't process test case with errors
 	if testCase.Err != nil {
 		return nil, testCase.Err
@@ -75,6 +82,25 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi
 			fmt.Fprintln(out, "  warning: found duplicated resource", dup.Kind, dup.Name, dup.Namespace)
 		}
 	}
+
+	targetResourcesPath := path.GetFullPaths(testCase.Test.TargetResources, testDir, isGit)
+	targetResources, err := common.GetResourceAccordingToResourcePath(out, testCase.Fs, targetResourcesPath, false, results.Policies, results.VAPs, dClient, "", false, testDir)
+	if err != nil {
+		return nil, fmt.Errorf("error: failed to load target resources (%s)", err)
+	}
+
+	targets := []runtime.Object{}
+	for _, t := range targetResources {
+		targets = append(targets, t)
+	}
+
+	// this will be a dclient containing all target resources. a policy may not do anything with any targets in these
+	dClient, err = dclient.NewFakeClient(runtime.NewScheme(), map[schema.GroupVersionResource]string{}, targets...)
+	if err != nil {
+		return nil, err
+	}
+	dClient.SetDiscovery(dclient.NewFakeDiscoveryClient(nil))
+
 	// exceptions
 	fmt.Fprintln(out, "  Loading exceptions", "...")
 	exceptionFullPath := path.GetFullPaths(testCase.Test.PolicyExceptions, testDir, isGit)
@@ -164,7 +190,12 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi
 	// execute engine
 	var engineResponses []engineapi.EngineResponse
 	var resultCounts processor.ResultCounts
+	testResponse := TestResponse{
+		Trigger: map[string][]engineapi.EngineResponse{},
+		Target:  map[string][]engineapi.EngineResponse{},
+	}
 	for _, resource := range uniques {
+		// the policy processor is for multiple policies at once
 		processor := processor.PolicyProcessor{
 			Store:                     &store,
 			Policies:                  validPolicies,
@@ -177,16 +208,28 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi
 			NamespaceSelectorMap:      vars.NamespaceSelectors(),
 			Rc:                        &resultCounts,
 			RuleToCloneSourceResource: ruleToCloneSourceResource,
+			Cluster:                   false,
 			Client:                    dClient,
 			Subresources:              vars.Subresources(),
-			Out:                       out,
+			Out:                       io.Discard,
 		}
 		ers, err := processor.ApplyPoliciesOnResource()
 		if err != nil {
 			return nil, fmt.Errorf("failed to apply policies on resource %v (%w)", resource.GetName(), err)
 		}
+		resourceKey := generateResourceKey(resource)
 		engineResponses = append(engineResponses, ers...)
+		testResponse.Trigger[resourceKey] = ers
 	}
+	for _, targetResource := range targetResources {
+		for _, engineResponse := range engineResponses {
+			if r, _ := extractPatchedTargetFromEngineResponse(targetResource.GetAPIVersion(), targetResource.GetKind(), targetResource.GetName(), targetResource.GetNamespace(), engineResponse); r != nil {
+				resourceKey := generateResourceKey(targetResource)
+				testResponse.Target[resourceKey] = append(testResponse.Target[resourceKey], engineResponse)
+			}
+		}
+	}
+
 	for _, resource := range uniques {
 		processor := processor.ValidatingAdmissionPolicyProcessor{
 			Policies:             results.VAPs,
@@ -200,7 +243,13 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool) ([]engi
 		if err != nil {
 			return nil, fmt.Errorf("failed to apply policies on resource %s (%w)", resource.GetName(), err)
 		}
-		engineResponses = append(engineResponses, ers...)
+		resourceKey := generateResourceKey(resource)
+		testResponse.Trigger[resourceKey] = append(testResponse.Trigger[resourceKey], ers...)
 	}
-	return engineResponses, nil
+	// this is an array of responses of all policies, generated by all of their rules
+	return &testResponse, nil
+}
+
+func generateResourceKey(resource *unstructured.Unstructured) string {
+	return resource.GetAPIVersion() + "," + resource.GetKind() + "," + resource.GetNamespace() + "," + resource.GetName()
 }
diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml
index 1357fd3002..14157d633b 100644
--- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml
+++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_tests.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: tests.cli.kyverno.io
 spec:
   group: cli.kyverno.io
@@ -182,6 +182,11 @@ spec:
               - result
               type: object
             type: array
+          targetResources:
+            description: Target Resources are for policies that have mutate existing
+            items:
+              type: string
+            type: array
           userinfo:
             description: UserInfo is the user info to be used in the test
             type: string
diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml
index 6bd9aca6db..7820dd44f8 100644
--- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml
+++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_userinfoes.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: userinfoes.cli.kyverno.io
 spec:
   group: cli.kyverno.io
diff --git a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml
index b227d23d93..f14203c22a 100644
--- a/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml
+++ b/cmd/cli/kubectl-kyverno/config/crds/cli.kyverno.io_values.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: values.cli.kyverno.io
 spec:
   group: cli.kyverno.io
diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml
index 1357fd3002..14157d633b 100644
--- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml
+++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_tests.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: tests.cli.kyverno.io
 spec:
   group: cli.kyverno.io
@@ -182,6 +182,11 @@ spec:
               - result
               type: object
             type: array
+          targetResources:
+            description: Target Resources are for policies that have mutate existing
+            items:
+              type: string
+            type: array
           userinfo:
             description: UserInfo is the user info to be used in the test
             type: string
diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml
index 6bd9aca6db..7820dd44f8 100644
--- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml
+++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_userinfoes.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: userinfoes.cli.kyverno.io
 spec:
   group: cli.kyverno.io
diff --git a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml
index b227d23d93..f14203c22a 100644
--- a/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml
+++ b/cmd/cli/kubectl-kyverno/data/crds/cli.kyverno.io_values.yaml
@@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.15.0
+    controller-gen.kubebuilder.io/version: (devel)
   name: values.cli.kyverno.io
 spec:
   group: cli.kyverno.io
diff --git a/cmd/cli/kubectl-kyverno/fix/test.go b/cmd/cli/kubectl-kyverno/fix/test.go
index 1409aa2ea6..c6ab462e97 100644
--- a/cmd/cli/kubectl-kyverno/fix/test.go
+++ b/cmd/cli/kubectl-kyverno/fix/test.go
@@ -37,7 +37,7 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error)
 			messages = append(messages, "test result should not use both `resource` and `resources` fields")
 		}
 		if result.Resource != "" {
-			var resources []string
+			var resources []any
 			messages = append(messages, "test result uses deprecated `resource` field, moving it into the `resources` field")
 			resources = append(resources, result.Resources...)
 			resources = append(resources, result.Resource)
@@ -67,7 +67,13 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error)
 	if compress {
 		compressed := map[v1alpha1.TestResultBase][]string{}
 		for _, result := range results {
-			compressed[result.TestResultBase] = append(compressed[result.TestResultBase], result.Resources...)
+			resourcesAsStringArray := []string{}
+			for _, resource := range result.Resources {
+				if r, ok := resource.(string); ok {
+					resourcesAsStringArray = append(resourcesAsStringArray, r)
+				}
+			}
+			compressed[result.TestResultBase] = append(compressed[result.TestResultBase], resourcesAsStringArray...)
 		}
 		results = nil
 		for k, v := range compressed {
@@ -76,9 +82,13 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error)
 				messages = append(messages, "test results contains duplicate resources")
 				v = unique.UnsortedList()
 			}
+			anyArray := make([]interface{}, len(v))
+			for i, r := range v {
+				anyArray[i] = r
+			}
 			results = append(results, v1alpha1.TestResult{
 				TestResultBase: k,
-				Resources:      v,
+				Resources:      anyArray,
 			})
 		}
 	}
@@ -104,14 +114,25 @@ func FixTest(test v1alpha1.Test, compress bool) (v1alpha1.Test, []string, error)
 		if x := cmp.Compare(a.CloneSourceResource, b.CloneSourceResource); x != 0 {
 			return x
 		}
-		slices.Sort(a.Resources)
-		slices.Sort(b.Resources)
+		asArray1 := []string{}
+		for _, r := range a.Resources {
+			resourceString, _ := r.(string)
+			asArray1 = append(asArray1, resourceString)
+		}
+		asArray2 := []string{}
+		for _, r := range b.Resources {
+			resourceString, _ := r.(string)
+			asArray2 = append(asArray1, resourceString)
+		}
+		slices.Sort(asArray1)
+		slices.Sort(asArray2)
+
 		if x := cmp.Compare(len(a.Resources), len(b.Resources)); x != 0 {
 			return x
 		}
 		if len(a.Resources) == len(b.Resources) {
 			for i := range a.Resources {
-				if x := cmp.Compare(a.Resources[i], b.Resources[i]); x != 0 {
+				if x := cmp.Compare(asArray1[i], asArray2[i]); x != 0 {
 					return x
 				}
 			}
diff --git a/cmd/cli/kubectl-kyverno/processor/generate.go b/cmd/cli/kubectl-kyverno/processor/generate.go
index 7d6d5411e0..d311273237 100644
--- a/cmd/cli/kubectl-kyverno/processor/generate.go
+++ b/cmd/cli/kubectl-kyverno/processor/generate.go
@@ -122,6 +122,7 @@ func initializeMockController(out io.Writer, s *store.Store, gvrToListKind map[s
 		imageverifycache.DisabledImageVerifyCache(),
 		store.ContextLoaderFactory(s, nil),
 		nil,
+		nil,
 	))
 	return c, nil
 }
diff --git a/cmd/cli/kubectl-kyverno/processor/policy_processor.go b/cmd/cli/kubectl-kyverno/processor/policy_processor.go
index 5e00cd8fef..95308d4c5d 100644
--- a/cmd/cli/kubectl-kyverno/processor/policy_processor.go
+++ b/cmd/cli/kubectl-kyverno/processor/policy_processor.go
@@ -43,6 +43,7 @@ type PolicyProcessor struct {
 	MutateLogPath             string
 	MutateLogPathIsDir        bool
 	Variables                 *variables.Variables
+	Cluster                   bool
 	UserInfo                  *kyvernov2.RequestInfo
 	PolicyReport              bool
 	NamespaceSelectorMap      map[string]map[string]string
@@ -72,6 +73,7 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse,
 	if rclient == nil {
 		rclient = registryclient.NewOrDie()
 	}
+	isCluster := false
 	eng := engine.NewEngine(
 		cfg,
 		config.NewDefaultMetricsConfiguration(),
@@ -81,13 +83,14 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse,
 		imageverifycache.DisabledImageVerifyCache(),
 		store.ContextLoaderFactory(p.Store, nil),
 		exceptions.New(policyExceptionLister),
+		&isCluster,
 	)
 	gvk, subresource := resource.GroupVersionKind(), ""
 	resourceKind := resource.GetKind()
 	resourceName := resource.GetName()
 	resourceNamespace := resource.GetNamespace()
 	// If --cluster flag is not set, then we need to find the top level resource GVK and subresource
-	if p.Client == nil {
+	if !p.Cluster {
 		for _, s := range p.Subresources {
 			subgvk := schema.GroupVersionKind{
 				Group:   s.Subresource.Group,
@@ -377,6 +380,21 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E
 		return fmt.Errorf("failed to marshal (%w)", err)
 	}
 
+	var yamlEncodedTargetResources [][]byte
+	for _, ruleResponese := range response.PolicyResponse.Rules {
+		patchedTarget, _, _ := ruleResponese.PatchedTarget()
+
+		if patchedTarget != nil {
+			yamlEncodedResource, err := yamlv2.Marshal(patchedTarget.Object)
+			if err != nil {
+				return fmt.Errorf("failed to marshal (%w)", err)
+			}
+
+			yamlEncodedResource = append(yamlEncodedResource, []byte("\n---\n")...)
+			yamlEncodedTargetResources = append(yamlEncodedTargetResources, yamlEncodedResource)
+		}
+	}
+
 	if p.MutateLogPath == "" {
 		resource := string(yamlEncodedResource) + string("\n---")
 		if len(strings.TrimSpace(resource)) > 0 {
@@ -384,6 +402,12 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E
 				fmt.Fprintf(p.Out, "\npolicy %s applied to %s:", response.Policy().GetName(), resourcePath)
 			}
 			fmt.Fprintf(p.Out, "\n"+resource+"\n") //nolint:govet
+			if len(yamlEncodedTargetResources) > 0 {
+				fmt.Fprintf(p.Out, "patched targets: \n")
+				for _, patchedTarget := range yamlEncodedTargetResources {
+					fmt.Fprintf(p.Out, "\n"+string(patchedTarget)+"\n")
+				}
+			}
 		}
 		return nil
 	}
@@ -409,11 +433,14 @@ func (p *PolicyProcessor) printOutput(resource interface{}, response engineapi.E
 		file = f
 	}
 	if _, err := file.Write([]byte(string(yamlEncodedResource) + "\n---\n\n")); err != nil {
-		if err := file.Close(); err != nil {
-			log.Log.Error(err, "failed to close file")
-		}
 		return err
 	}
+
+	for _, patchedTarget := range yamlEncodedTargetResources {
+		if _, err := file.Write(patchedTarget); err != nil {
+			return err
+		}
+	}
 	if err := file.Close(); err != nil {
 		return err
 	}
diff --git a/cmd/cli/kubectl-kyverno/resource/resource.go b/cmd/cli/kubectl-kyverno/resource/resource.go
index 177dea42a4..fbc574a94f 100644
--- a/cmd/cli/kubectl-kyverno/resource/resource.go
+++ b/cmd/cli/kubectl-kyverno/resource/resource.go
@@ -59,7 +59,8 @@ func YamlToUnstructured(resourceYaml []byte) (*unstructured.Unstructured, error)
 	return resource, nil
 }
 
-func GetResourceFromPath(fs billy.Filesystem, path string) ([]*unstructured.Unstructured, error) {
+// should be able to specify a single resource in a multi yaml file, dont error out when there are multiple resource
+func GetResourceFromPath(fs billy.Filesystem, path string, apiVersion, kind, resourceNamespace, resourceName string) (*unstructured.Unstructured, error) {
 	var resourceBytes []byte
 	if fs == nil {
 		data, err := GetFileBytes(path)
@@ -83,10 +84,18 @@ func GetResourceFromPath(fs billy.Filesystem, path string) ([]*unstructured.Unst
 	if err != nil {
 		return nil, err
 	}
-	if len(resources) == 0 {
-		return nil, fmt.Errorf("no resources found")
+
+	for _, r := range resources {
+		name := r.GetName()
+		ns := r.GetNamespace()
+		apiv := r.GetAPIVersion()
+		k := r.GetKind()
+
+		if apiv == apiVersion && k == kind && name == resourceName && ns == resourceNamespace {
+			return r, nil
+		}
 	}
-	return resources, nil
+	return nil, fmt.Errorf("resource with name %s not found in manifest", resourceName)
 }
 
 func GetFileBytes(path string) ([]byte, error) {
diff --git a/cmd/cli/kubectl-kyverno/test/load.go b/cmd/cli/kubectl-kyverno/test/load.go
index 155d964bc7..6865c2cf69 100644
--- a/cmd/cli/kubectl-kyverno/test/load.go
+++ b/cmd/cli/kubectl-kyverno/test/load.go
@@ -84,9 +84,6 @@ func LoadTest(fs billy.Filesystem, path string) TestCase {
 func cleanTest(test *v1alpha1.Test) {
 	test.Policies = removeDuplicateStrings(test.Policies)
 	test.Resources = removeDuplicateStrings(test.Resources)
-	for index, result := range test.Results {
-		test.Results[index].Resources = removeDuplicateStrings(result.Resources)
-	}
 }
 
 func removeDuplicateStrings(strings []string) []string {
diff --git a/cmd/cli/kubectl-kyverno/test/load_test.go b/cmd/cli/kubectl-kyverno/test/load_test.go
index 17d90e544f..5f9661dad7 100644
--- a/cmd/cli/kubectl-kyverno/test/load_test.go
+++ b/cmd/cli/kubectl-kyverno/test/load_test.go
@@ -65,7 +65,7 @@ func TestLoadTests(t *testing.T) {
 						Result: policyreportv1alpha2.StatusPass,
 						Rule:   "only-allow-trusted-images",
 					},
-					Resources: []string{
+					Resources: []any{
 						"test-pod-with-non-root-user-image",
 						"test-pod-with-trusted-registry",
 					},
@@ -97,7 +97,7 @@ func TestLoadTests(t *testing.T) {
 						Rule:              "generate-limitrange",
 						GeneratedResource: "generatedLimitRange.yaml",
 					},
-					Resources: []string{"hello-world-namespace"},
+					Resources: []any{"hello-world-namespace"},
 				}, {
 					TestResultBase: v1alpha1.TestResultBase{
 						Kind:              "Namespace",
@@ -106,7 +106,7 @@ func TestLoadTests(t *testing.T) {
 						Rule:              "generate-resourcequota",
 						GeneratedResource: "generatedResourceQuota.yaml",
 					},
-					Resources: []string{"hello-world-namespace"},
+					Resources: []any{"hello-world-namespace"},
 				}},
 			},
 		}},
@@ -134,7 +134,7 @@ func TestLoadTests(t *testing.T) {
 						Result: policyreportv1alpha2.StatusPass,
 						Rule:   "only-allow-trusted-images",
 					},
-					Resources: []string{
+					Resources: []any{
 						"test-pod-with-non-root-user-image",
 						"test-pod-with-trusted-registry",
 					},
@@ -160,7 +160,7 @@ func TestLoadTests(t *testing.T) {
 						Rule:              "generate-limitrange",
 						GeneratedResource: "generatedLimitRange.yaml",
 					},
-					Resources: []string{"hello-world-namespace"},
+					Resources: []any{"hello-world-namespace"},
 				}, {
 					TestResultBase: v1alpha1.TestResultBase{
 						Kind:              "Namespace",
@@ -169,7 +169,7 @@ func TestLoadTests(t *testing.T) {
 						Rule:              "generate-resourcequota",
 						GeneratedResource: "generatedResourceQuota.yaml",
 					},
-					Resources: []string{"hello-world-namespace"},
+					Resources: []any{"hello-world-namespace"},
 				}},
 			},
 		}},
@@ -234,7 +234,7 @@ func TestLoadTest(t *testing.T) {
 						Result: policyreportv1alpha2.StatusPass,
 						Rule:   "only-allow-trusted-images",
 					},
-					Resources: []string{
+					Resources: []any{
 						"test-pod-with-non-root-user-image",
 						"test-pod-with-trusted-registry",
 					},
@@ -263,7 +263,7 @@ func TestLoadTest(t *testing.T) {
 						Result: policyreportv1alpha2.StatusPass,
 						Rule:   "only-allow-trusted-images",
 					},
-					Resources: []string{
+					Resources: []any{
 						"test-pod-with-non-root-user-image",
 						"test-pod-with-trusted-registry",
 					},
diff --git a/cmd/internal/engine.go b/cmd/internal/engine.go
index 4745e4c24d..777dbe3917 100644
--- a/cmd/internal/engine.go
+++ b/cmd/internal/engine.go
@@ -53,6 +53,7 @@ func NewEngine(
 		ivCache,
 		factories.DefaultContextLoaderFactory(configMapResolver, factories.WithAPICallConfig(apiCallConfig), factories.WithGlobalContextStore(gctxStore)),
 		exceptionsSelector,
+		nil,
 	)
 }
 
diff --git a/docs/user/cli/commands/kyverno_apply.md b/docs/user/cli/commands/kyverno_apply.md
index 582e6b7ab9..c1976beba2 100644
--- a/docs/user/cli/commands/kyverno_apply.md
+++ b/docs/user/cli/commands/kyverno_apply.md
@@ -60,6 +60,8 @@ kyverno apply [flags]
   -s, --set strings                        Variables that are required
   -i, --stdin                              Optional mutate policy parameter to pipe directly through to kubectl
   -t, --table                              Show results in table format
+      --target-resource strings            Path to individual files containing target resources files for policies that have mutate existing
+      --target-resources strings           Path to a directory containing target resources files for policies that have mutate existing
   -u, --userinfo string                    Admission Info including Roles, Cluster Roles and Subjects
   -f, --values-file string                 File containing values for policy variables
       --warn-exit-code int                 Set the exit code for warnings; if failures or errors are found, will exit 1
diff --git a/docs/user/cli/crd/index.html b/docs/user/cli/crd/index.html
index 2271866e61..6be608f0a2 100644
--- a/docs/user/cli/crd/index.html
+++ b/docs/user/cli/crd/index.html
@@ -112,6 +112,17 @@ This field is deprecated, use <code>metadata.name</code> instead</p>
 </tr>
 <tr>
 <td>
+<code>targetResources</code><br/>
+<em>
+[]string
+</em>
+</td>
+<td>
+<p>Target Resources are for policies that have mutate existing</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>variables</code><br/>
 <em>
 string
@@ -655,6 +666,81 @@ Kubernetes meta/v1.APIResource
 </tbody>
 </table>
 <hr />
+<h3 id="cli.kyverno.io/v1alpha1.TestResourceSpec">TestResourceSpec
+</h3>
+<p>
+</p>
+<table class="table table-striped">
+<thead class="thead-dark">
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>group</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>version</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>kind</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>namespace</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>subresource</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+<tr>
+<td>
+<code>name</code><br/>
+<em>
+string
+</em>
+</td>
+<td>
+</td>
+</tr>
+</tbody>
+</table>
+<hr />
 <h3 id="cli.kyverno.io/v1alpha1.TestResult">TestResult
 </h3>
 <p>
@@ -706,7 +792,7 @@ TestResultDeprecated
 <td>
 <code>resources</code><br/>
 <em>
-[]string
+[]any
 </em>
 </td>
 <td>
diff --git a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
index 00fad5c552..61460d8278 100644
--- a/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
+++ b/docs/user/cli/crd/kyverno_kubectl.v1alpha1.html
@@ -193,6 +193,35 @@ This field is deprecated, use <code>metadata.name</code> instead</p>
   
     
     
+      <tr>
+        <td><code>targetResources</code>
+          
+          <span style="color:blue;"> *</span>
+          
+          </br>
+
+          
+          
+            
+              <span style="font-family: monospace">[]string</span>
+            
+          
+        </td>
+        <td>
+          
+
+          <p>Target Resources are for policies that have mutate existing</p>
+
+
+          
+
+          
+        </td>
+      </tr>
+    
+  
+    
+    
       <tr>
         <td><code>variables</code>
           
@@ -1475,7 +1504,7 @@ This field is deprecated, use <code>metadata.name</code> instead</p>
           
           
             
-              <span style="font-family: monospace">[]string</span>
+              <span style="font-family: monospace">[]any</span>
             
           
         </td>
diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go
index 8a1d0cb008..0dec3ebc49 100644
--- a/pkg/engine/engine.go
+++ b/pkg/engine/engine.go
@@ -31,6 +31,7 @@ type engine struct {
 	metricsConfiguration config.MetricsConfiguration
 	jp                   jmespath.Interface
 	client               engineapi.Client
+	isCluster            bool
 	rclientFactory       engineapi.RegistryClientFactory
 	ivCache              imageverifycache.Client
 	contextLoader        engineapi.ContextLoaderFactory
@@ -51,7 +52,12 @@ func NewEngine(
 	ivCache imageverifycache.Client,
 	contextLoader engineapi.ContextLoaderFactory,
 	exceptionSelector engineapi.PolicyExceptionSelector,
+	isCluster *bool,
 ) engineapi.Engine {
+	if isCluster == nil {
+		defaultCluster := true
+		isCluster = &defaultCluster
+	}
 	meter := otel.GetMeterProvider().Meter(metrics.MeterName)
 	resultCounter, err := meter.Int64Counter(
 		"kyverno_policy_results",
@@ -74,6 +80,7 @@ func NewEngine(
 		client:               client,
 		rclientFactory:       rclientFactory,
 		ivCache:              ivCache,
+		isCluster:            *isCluster,
 		contextLoader:        contextLoader,
 		exceptionSelector:    exceptionSelector,
 		resultCounter:        resultCounter,
@@ -107,8 +114,8 @@ func (e *engine) Mutate(
 	if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
 		policyResponse, patchedResource := e.mutate(ctx, logger, policyContext)
 		response = response.
-			WithPolicyResponse(policyResponse).
-			WithPatchedResource(patchedResource)
+			WithPatchedResource(patchedResource).
+			WithPolicyResponse(policyResponse)
 	}
 	response = response.WithStats(engineapi.NewExecutionStats(startTime, time.Now()))
 	e.reportMetrics(ctx, logger, policyContext.Operation(), policyContext.AdmissionOperation(), response)
diff --git a/pkg/engine/fuzz_test.go b/pkg/engine/fuzz_test.go
index 64656e5be2..5ff73847f0 100644
--- a/pkg/engine/fuzz_test.go
+++ b/pkg/engine/fuzz_test.go
@@ -43,6 +43,7 @@ var (
 		imageverifycache.DisabledImageVerifyCache(),
 		factories.DefaultContextLoaderFactory(nil),
 		nil,
+		nil,
 	)
 	initter sync.Once
 )
@@ -126,6 +127,7 @@ func FuzzVerifyImageAndPatchTest(f *testing.F) {
 			imageverifycache.DisabledImageVerifyCache(),
 			factories.DefaultContextLoaderFactory(nil),
 			nil,
+			nil,
 		)
 
 		_, _ = verifyImageAndPatchEngine.VerifyAndPatchImages(
@@ -271,6 +273,7 @@ func FuzzMutateTest(f *testing.F) {
 			imageverifycache.DisabledImageVerifyCache(),
 			factories.DefaultContextLoaderFactory(nil),
 			nil,
+			nil,
 		)
 		e.Mutate(
 			context.Background(),
diff --git a/pkg/engine/handlers/validation/validate_cel.go b/pkg/engine/handlers/validation/validate_cel.go
index 49d9303354..f6c3c4e443 100644
--- a/pkg/engine/handlers/validation/validate_cel.go
+++ b/pkg/engine/handlers/validation/validate_cel.go
@@ -30,12 +30,14 @@ import (
 )
 
 type validateCELHandler struct {
-	client engineapi.Client
+	client    engineapi.Client
+	isCluster bool
 }
 
-func NewValidateCELHandler(client engineapi.Client) (handlers.Handler, error) {
+func NewValidateCELHandler(client engineapi.Client, isCluster bool) (handlers.Handler, error) {
 	return validateCELHandler{
-		client: client,
+		client:    client,
+		isCluster: isCluster,
 	}, nil
 }
 
@@ -140,7 +142,7 @@ func (h validateCELHandler) Process(
 		ns = ""
 	}
 	if ns != "" {
-		if h.client != nil {
+		if h.client != nil && h.isCluster {
 			namespace, err = h.client.GetNamespace(ctx, ns, metav1.GetOptions{})
 			if err != nil {
 				return resource, handlers.WithResponses(
diff --git a/pkg/engine/image_verify_test.go b/pkg/engine/image_verify_test.go
index 1bf2e51bbc..6d63988315 100644
--- a/pkg/engine/image_verify_test.go
+++ b/pkg/engine/image_verify_test.go
@@ -324,6 +324,7 @@ func testVerifyAndPatchImages(
 		imageverifycache.DisabledImageVerifyCache(),
 		factories.DefaultContextLoaderFactory(cmResolver),
 		nil,
+		nil,
 	)
 	return e.VerifyAndPatchImages(
 		ctx,
@@ -1059,6 +1060,7 @@ func testImageVerifyCache(
 		ivCache,
 		factories.DefaultContextLoaderFactory(cmResolver),
 		nil,
+		nil,
 	)
 	return e.VerifyAndPatchImages(
 		ctx,
diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go
index 12d3c99064..fe3b5aca7e 100644
--- a/pkg/engine/mutation_test.go
+++ b/pkg/engine/mutation_test.go
@@ -42,6 +42,7 @@ func testMutate(
 		imageverifycache.DisabledImageVerifyCache(),
 		contextLoader,
 		nil,
+		nil,
 	)
 	return e.Mutate(
 		ctx,
diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go
index 165343a713..7bdc087790 100644
--- a/pkg/engine/validation.go
+++ b/pkg/engine/validation.go
@@ -51,7 +51,7 @@ func (e *engine) validate(
 				} else if hasValidatePss {
 					return validation.NewValidatePssHandler()
 				} else if hasValidateCEL {
-					return validation.NewValidateCELHandler(e.client)
+					return validation.NewValidateCELHandler(e.client, e.isCluster)
 				} else {
 					return validation.NewValidateResourceHandler()
 				}
diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go
index de2ec61c49..8a2a299ae7 100644
--- a/pkg/engine/validation_test.go
+++ b/pkg/engine/validation_test.go
@@ -41,6 +41,7 @@ func testValidate(
 		imageverifycache.DisabledImageVerifyCache(),
 		contextLoader,
 		nil,
+		nil,
 	)
 	return e.Validate(
 		ctx,
diff --git a/pkg/webhooks/resource/fake.go b/pkg/webhooks/resource/fake.go
index 51f7fbe677..09e2073c50 100644
--- a/pkg/webhooks/resource/fake.go
+++ b/pkg/webhooks/resource/fake.go
@@ -66,6 +66,7 @@ func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) *resour
 			imageverifycache.DisabledImageVerifyCache(),
 			factories.DefaultContextLoaderFactory(configMapResolver),
 			exceptions.New(peLister),
+			nil,
 		),
 	}
 }
diff --git a/pkg/webhooks/resource/validation_test.go b/pkg/webhooks/resource/validation_test.go
index f13c84abac..d0a28167d1 100644
--- a/pkg/webhooks/resource/validation_test.go
+++ b/pkg/webhooks/resource/validation_test.go
@@ -2086,6 +2086,7 @@ func TestValidate_failure_action_overrides(t *testing.T) {
 		imageverifycache.DisabledImageVerifyCache(),
 		factories.DefaultContextLoaderFactory(nil),
 		nil,
+		nil,
 	)
 	for i, tc := range testcases {
 		t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
@@ -2188,6 +2189,7 @@ func Test_RuleSelector(t *testing.T) {
 		imageverifycache.DisabledImageVerifyCache(),
 		factories.DefaultContextLoaderFactory(nil),
 		nil,
+		nil,
 	)
 	resp := eng.Validate(
 		context.TODO(),
diff --git a/test/cli/test-mutate/add-default-resources/kyverno-test.yaml b/test/cli/test-mutate/add-default-resources/kyverno-test.yaml
index a31438600c..343ba724b3 100644
--- a/test/cli/test-mutate/add-default-resources/kyverno-test.yaml
+++ b/test/cli/test-mutate/add-default-resources/kyverno-test.yaml
@@ -7,13 +7,6 @@ policies:
 resources:
 - resource.yaml
 results:
-- kind: Pod
-  patchedResources: patchedResource1.yaml
-  policy: add-default-resources
-  resources:
-  - nginx-demo1
-  result: pass
-  rule: add-default-requests
 - kind: Pod
   patchedResources: patchedResource3.yaml
   policy: add-default-resources
@@ -21,6 +14,13 @@ results:
   - nginx-demo3
   result: pass
   rule: add-default-requests
+- kind: Pod
+  patchedResources: patchedResource1.yaml
+  policy: add-default-resources
+  resources:
+  - nginx-demo1
+  result: pass
+  rule: add-default-requests
 - kind: Pod
   patchedResources: patchedResource2.yaml
   policy: add-default-resources
diff --git a/test/cli/test-mutate/global-anchor/kyverno-test.yaml b/test/cli/test-mutate/global-anchor/kyverno-test.yaml
index e413c72631..57d1fba0b7 100644
--- a/test/cli/test-mutate/global-anchor/kyverno-test.yaml
+++ b/test/cli/test-mutate/global-anchor/kyverno-test.yaml
@@ -7,13 +7,6 @@ policies:
 resources:
 - resources.yaml
 results:
-- kind: Pod
-  patchedResources: patchedResource.yaml
-  policy: add-safe-to-evict
-  resources:
-  - pod-with-emptydir-hostpath
-  result: pass
-  rule: annotate-empty-dir
 - kind: Pod
   patchedResources: patchedResourceWithVolume.yaml
   policy: add-safe-to-evict
@@ -21,6 +14,13 @@ results:
   - pod-with-emptydir-hostpath-1
   result: pass
   rule: annotate-empty-dir
+- kind: Pod
+  patchedResources: patchedResource.yaml
+  policy: add-safe-to-evict
+  resources:
+  - pod-with-emptydir-hostpath
+  result: pass
+  rule: annotate-empty-dir
 - kind: Pod
   policy: add-safe-to-evict
   resources:
diff --git a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml
index 6d88fcb8a4..241eadfaa2 100644
--- a/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml
+++ b/test/cli/test-mutate/karpenter-annotations-to-nodeselector/kyverno-test.yaml
@@ -7,6 +7,12 @@ policies:
 resources:
 - resource.yaml
 results:
+- kind: Pod
+  policy: karpenter-annotations-to-nodeselector
+  resources:
+  - soft-pod-antiaffinity-1-copy
+  result: pass
+  rule: hard-nodeselector-lifecycle-on-demand
 - kind: Pod
   patchedResources: patched.yaml
   policy: karpenter-annotations-to-nodeselector
@@ -14,9 +20,3 @@ results:
   - soft-pod-antiaffinity-1
   result: pass
   rule: hard-nodeselector-lifecycle-on-demand
-- kind: Pod
-  policy: karpenter-annotations-to-nodeselector
-  resources:
-  - soft-pod-antiaffinity-1-copy
-  result: pass
-  rule: hard-nodeselector-lifecycle-on-demand
diff --git a/test/cli/test-mutate/kyverno-test.yaml b/test/cli/test-mutate/kyverno-test.yaml
index df8be89492..fc70882001 100644
--- a/test/cli/test-mutate/kyverno-test.yaml
+++ b/test/cli/test-mutate/kyverno-test.yaml
@@ -14,13 +14,6 @@ results:
   - mydeploy
   result: pass
   rule: add-label
-- kind: Pod
-  patchedResources: patchedResource3.yaml
-  policy: add-label
-  resources:
-  - production/same-name-but-diff-namespace
-  result: pass
-  rule: add-label
 - kind: Pod
   patchedResources: patchedResource6.yaml
   policy: add-label
@@ -35,6 +28,13 @@ results:
   - testing/same-name-but-diff-namespace
   result: pass
   rule: add-label
+- kind: Pod
+  patchedResources: patchedResource3.yaml
+  policy: add-label
+  resources:
+  - production/same-name-but-diff-namespace
+  result: pass
+  rule: add-label
 - kind: Pod
   patchedResources: patchedResource1.yaml
   policy: add-label
diff --git a/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml b/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml
new file mode 100644
index 0000000000..37619aba43
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing-fail/kyverno-test.yaml
@@ -0,0 +1,18 @@
+apiVersion: cli.kyverno.io/v1alpha1
+kind: Test
+metadata:
+  name: kyverno-test.yaml
+policies:
+- policy.yaml
+resources:
+- trigger-cm.yaml
+results:
+- kind: ""
+  patchedResources: mutated-secret.yaml
+  policy: mutate-existing-secret
+  resources:
+  - secret-1
+  result: fail
+  rule: mutate-secret-on-configmap-create
+targetResources:
+- raw-secret.yaml
diff --git a/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml b/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml
new file mode 100755
index 0000000000..51e4b6ab0b
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing-fail/mutated-secret.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  labels:
+    otherlabel: bar
+  name: secret-1
+  namespace: staging
diff --git a/test/cli/test-mutate/mutate-existing-fail/policy.yaml b/test/cli/test-mutate/mutate-existing-fail/policy.yaml
new file mode 100755
index 0000000000..00731ebc95
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing-fail/policy.yaml
@@ -0,0 +1,27 @@
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+  name: mutate-existing-secret
+spec:
+  rules:
+  - match:
+      any:
+      - resources:
+          kinds:
+          - ConfigMap
+          names:
+          - dictionary-1
+          namespaces:
+          - staging
+    mutate:
+      mutateExistingOnPolicyUpdate: false
+      patchStrategicMerge:
+        metadata:
+          labels:
+            foo: bar
+      targets:
+      - apiVersion: v1
+        kind: Secret
+        name: secret-1
+        namespace: staging
+    name: mutate-secret-on-configmap-create
diff --git a/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml b/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml
new file mode 100644
index 0000000000..935cb1be92
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing-fail/raw-secret.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-1
+  namespace: staging
diff --git a/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml b/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml
new file mode 100755
index 0000000000..b458868bc4
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing-fail/trigger-cm.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+data:
+  foo: bar
+kind: ConfigMap
+metadata:
+  name: dictionary-1
+  namespace: staging
diff --git a/test/cli/test-mutate/mutate-existing/kyverno-test.yaml b/test/cli/test-mutate/mutate-existing/kyverno-test.yaml
new file mode 100644
index 0000000000..c446c3a4a3
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing/kyverno-test.yaml
@@ -0,0 +1,17 @@
+apiVersion: cli.kyverno.io/v1alpha1
+kind: Test
+metadata:
+  name: kyverno-test.yaml
+policies:
+- policy.yaml
+resources:
+- trigger-cm.yaml
+results:
+- kind: ""
+  patchedResources: mutated-secret.yaml
+  policy: mutate-existing-secret
+  resources: []
+  result: pass
+  rule: mutate-secret-on-configmap-create
+targetResources:
+- raw-secret.yaml
diff --git a/test/cli/test-mutate/mutate-existing/mutated-secret.yaml b/test/cli/test-mutate/mutate-existing/mutated-secret.yaml
new file mode 100755
index 0000000000..769daa4ab5
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing/mutated-secret.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+data:
+  foo: bar
+kind: ConfigMap
+metadata:
+  name: dictionary-1
+  namespace: staging
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  labels:
+    foo: bar
+  name: secret-1
+  namespace: staging
diff --git a/test/cli/test-mutate/mutate-existing/policy.yaml b/test/cli/test-mutate/mutate-existing/policy.yaml
new file mode 100755
index 0000000000..00731ebc95
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing/policy.yaml
@@ -0,0 +1,27 @@
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+  name: mutate-existing-secret
+spec:
+  rules:
+  - match:
+      any:
+      - resources:
+          kinds:
+          - ConfigMap
+          names:
+          - dictionary-1
+          namespaces:
+          - staging
+    mutate:
+      mutateExistingOnPolicyUpdate: false
+      patchStrategicMerge:
+        metadata:
+          labels:
+            foo: bar
+      targets:
+      - apiVersion: v1
+        kind: Secret
+        name: secret-1
+        namespace: staging
+    name: mutate-secret-on-configmap-create
diff --git a/test/cli/test-mutate/mutate-existing/raw-secret.yaml b/test/cli/test-mutate/mutate-existing/raw-secret.yaml
new file mode 100644
index 0000000000..935cb1be92
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing/raw-secret.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-1
+  namespace: staging
diff --git a/test/cli/test-mutate/mutate-existing/trigger-cm.yaml b/test/cli/test-mutate/mutate-existing/trigger-cm.yaml
new file mode 100755
index 0000000000..b458868bc4
--- /dev/null
+++ b/test/cli/test-mutate/mutate-existing/trigger-cm.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+data:
+  foo: bar
+kind: ConfigMap
+metadata:
+  name: dictionary-1
+  namespace: staging
diff --git a/test/cli/test-mutate/same-name-mutate-existing/cm.yaml b/test/cli/test-mutate/same-name-mutate-existing/cm.yaml
new file mode 100755
index 0000000000..b458868bc4
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/cm.yaml
@@ -0,0 +1,7 @@
+apiVersion: v1
+data:
+  foo: bar
+kind: ConfigMap
+metadata:
+  name: dictionary-1
+  namespace: staging
diff --git a/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml b/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml
new file mode 100644
index 0000000000..656eaa0316
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/kyverno-test.yaml
@@ -0,0 +1,20 @@
+apiVersion: cli.kyverno.io/v1alpha1
+kind: Test
+metadata:
+  name: kyverno-test.yaml
+policies:
+- policy.yaml
+resources:
+- cm.yaml
+results:
+- kind: ""
+  patchedResources: mutated-resources.yaml
+  policy: mutate-existing-secret
+  resources:
+  - staging/secret-1
+  - prod/secret-1
+  result: pass
+  rule: mutate-secret-on-configmap-create
+targetResources:
+- secret-1.yaml
+- secret-2.yaml
diff --git a/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml b/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml
new file mode 100755
index 0000000000..e8515724f4
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/mutated-resources.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  labels:
+    foo: bar
+  name: secret-1
+  namespace: staging
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  labels:
+    foo: bar
+  name: secret-1
+  namespace: prod
+
diff --git a/test/cli/test-mutate/same-name-mutate-existing/policy.yaml b/test/cli/test-mutate/same-name-mutate-existing/policy.yaml
new file mode 100755
index 0000000000..4fc8b6afa0
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/policy.yaml
@@ -0,0 +1,26 @@
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+  name: mutate-existing-secret
+spec:
+  rules:
+  - match:
+      any:
+      - resources:
+          kinds:
+          - ConfigMap
+          names:
+          - dictionary-1
+          namespaces:
+          - staging
+    mutate:
+      mutateExistingOnPolicyUpdate: false
+      patchStrategicMerge:
+        metadata:
+          labels:
+            foo: bar
+      targets:
+      - apiVersion: v1
+        kind: Secret
+        name: secret-1
+    name: mutate-secret-on-configmap-create
diff --git a/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml b/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml
new file mode 100644
index 0000000000..935cb1be92
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/secret-1.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-1
+  namespace: staging
diff --git a/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml b/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml
new file mode 100644
index 0000000000..00f314554d
--- /dev/null
+++ b/test/cli/test-mutate/same-name-mutate-existing/secret-2.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-1
+  namespace: prod
diff --git a/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml b/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml
index 4e44f2a671..bcf45a4156 100644
--- a/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml
+++ b/test/cli/test/mutate-keda-scaled-object/kyverno-test.yaml
@@ -7,13 +7,6 @@ policies:
 resources:
 - resources.yaml
 results:
-- kind: ScaledObject
-  patchedResources: patchedResource1.yaml
-  policy: keda-prometheus-serveraddress
-  resources:
-  - service-1
-  result: pass
-  rule: keda-prometheus-serveraddress
 - kind: ScaledObject
   patchedResources: patchedResource2.yaml
   policy: keda-prometheus-serveraddress
@@ -21,6 +14,13 @@ results:
   - service-2
   result: pass
   rule: keda-prometheus-serveraddress
+- kind: ScaledObject
+  patchedResources: patchedResource1.yaml
+  policy: keda-prometheus-serveraddress
+  resources:
+  - service-1
+  result: pass
+  rule: keda-prometheus-serveraddress
 - kind: ScaledObject
   policy: keda-prometheus-serveraddress
   resources: