From 231e7a681e6628647faba37349eb03e34e01ae80 Mon Sep 17 00:00:00 2001
From: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com>
Date: Thu, 1 Feb 2024 03:58:14 +0530
Subject: [PATCH] Support PolicyExceptions with CLI (#9525)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* loding policyExecptions from  func

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* adding PolicyExceptions in crds

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* adding PolicyExceptions in GetPolicy function

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* adding policyexceptions in Load function

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* resolve error becuase of now Getpolicy return policyexceptions

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* added -exception flag loaded policyexception

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* added policyexceptions in processor and NewEngine

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* Revert "added -exception flag loaded policyexception"

This reverts commit f53b205c089e780033b14c9a6343a141c20875b6.

* Revert "Added support for PolicyExceptions for apply command "

This reverts commit 82689ea0c1b914c566ac8ef30ab863f33d9b5460.

* Update cmd/cli/kubectl-kyverno/commands/test/test.go

loading exceptions with policies

Co-authored-by: Mariam Fahmy <mariamfahmy66@gmail.com>
Signed-off-by: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com>

* updated GetFullPaths function and remove unnecessary code

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* added tests for loading exceptions in GetPolicy function

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* added tests for loading policy exceptions

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* Used selector in List function

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* generated cli crd

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* updated loadpolicy_test tests and corrected kind

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* resolved unit test error in path_test.go file

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* limiting the usage of exceptions to ValidatingAdmissionPolicies

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>

* remove changes in common code

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fixes

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* codegen

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Sanskarzz <sanskar.gur@gmail.com>
Signed-off-by: Sanskar Gurdasani <92817635+Sanskarzz@users.noreply.github.com>
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: Mariam Fahmy <mariamfahmy66@gmail.com>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
---
 cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go |  3 ++
 .../kubectl-kyverno/commands/apply/command.go | 49 ++++++++++++-------
 .../commands/apply/command_test.go            | 34 ++++++-------
 cmd/cli/kubectl-kyverno/commands/test/test.go | 19 ++++++-
 .../config/crds/cli.kyverno.io_tests.yaml     |  6 +++
 .../data/crds/cli.kyverno.io_tests.yaml       |  6 +++
 cmd/cli/kubectl-kyverno/exception/load.go     | 20 +++++++-
 .../kubectl-kyverno/exception/load_test.go    |  4 +-
 .../kubectl-kyverno/processor/exceptions.go   | 21 ++++++++
 .../processor/policy_processor.go             |  8 ++-
 docs/user/cli/commands/kyverno_apply.md       |  1 +
 11 files changed, 130 insertions(+), 41 deletions(-)
 create mode 100644 cmd/cli/kubectl-kyverno/processor/exceptions.go

diff --git a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
index 4595a01431..cece9cba50 100644
--- a/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
+++ b/cmd/cli/kubectl-kyverno/apis/v1alpha1/test.go
@@ -38,6 +38,9 @@ type Test struct {
 
 	// Values are the values to be used in the test
 	Values *ValuesSpec `json:"values,omitempty"`
+
+	// Policy Exceptions are the policy exceptions to be used in the test
+	PolicyExceptions []string `json:"exceptions,omitempty"`
 }
 
 type CheckResult struct {
diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command.go b/cmd/cli/kubectl-kyverno/commands/apply/command.go
index 7281d4caad..e51ac1d439 100644
--- a/cmd/cli/kubectl-kyverno/commands/apply/command.go
+++ b/cmd/cli/kubectl-kyverno/commands/apply/command.go
@@ -13,8 +13,10 @@ import (
 	"github.com/go-git/go-billy/v5/memfs"
 	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
 	"github.com/kyverno/kyverno/api/kyverno/v1beta1"
+	kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/command"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/deprecations"
+	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/exception"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/output/color"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy"
@@ -41,12 +43,12 @@ import (
 
 const divider = "----------------------------------------------------------------------"
 
-type SkippedInvalidPolicies struct {
+type skippedInvalidPolicies struct {
 	skipped []string
 	invalid []string
 }
 
-type ApplyCommandConfig struct {
+type applyCommandConfig struct {
 	KubeConfig     string
 	Context        string
 	Namespace      string
@@ -64,11 +66,12 @@ type ApplyCommandConfig struct {
 	GitBranch      string
 	warnExitCode   int
 	warnNoPassed   bool
+	exception      []string
 }
 
 func Command() *cobra.Command {
 	var removeColor, detailedResults, table bool
-	applyCommandConfig := &ApplyCommandConfig{}
+	applyCommandConfig := &applyCommandConfig{}
 	cmd := &cobra.Command{
 		Use:          "apply",
 		Short:        command.FormatDescription(true, websiteUrl, false, description...),
@@ -114,10 +117,11 @@ func Command() *cobra.Command {
 	cmd.Flags().BoolVar(&removeColor, "remove-color", false, "Remove any color from output")
 	cmd.Flags().BoolVar(&detailedResults, "detailed-results", false, "If set to true, display detailed results")
 	cmd.Flags().BoolVarP(&table, "table", "t", false, "Show results in table format")
+	cmd.Flags().StringSliceVar(&applyCommandConfig.exception, "exception", nil, "Policy exception to be considered when evaluating policies against resources")
 	return cmd
 }
 
-func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) {
+func (c *applyCommandConfig) applyCommandHelper(out io.Writer) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) {
 	rc, resources1, skipInvalidPolicies, responses1, err := c.checkArguments()
 	if err != nil {
 		return rc, resources1, skipInvalidPolicies, responses1, err
@@ -156,13 +160,21 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
 	if err != nil {
 		return rc, resources1, skipInvalidPolicies, responses1, err
 	}
+	exceptions, err := exception.Load(c.exception...)
+	if err != nil {
+		return rc, resources1, skipInvalidPolicies, responses1, fmt.Errorf("Error: failed to load exceptions (%s)", err)
+	}
 	if !c.Stdin {
 		var policyRulesCount int
 		for _, policy := range policies {
 			policyRulesCount += len(autogen.ComputeRules(policy))
 		}
 		policyRulesCount += len(vaps)
-		fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources))
+		if len(exceptions) > 0 {
+			fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s) with %d exception(s)...\n", policyRulesCount, len(resources), len(exceptions))
+		} else {
+			fmt.Fprintf(out, "\nApplying %d policy rule(s) to %d resource(s)...\n", policyRulesCount, len(resources))
+		}
 	}
 
 	rc, resources1, responses1, err = c.applyPolicytoResource(
@@ -171,6 +183,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
 		variables,
 		policies,
 		resources,
+		exceptions,
 		&skipInvalidPolicies,
 		dClient,
 		userInfo,
@@ -189,7 +202,7 @@ func (c *ApplyCommandConfig) applyCommandHelper(out io.Writer) (*processor.Resul
 	return rc, resources1, skipInvalidPolicies, responses, nil
 }
 
-func (c *ApplyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error, bool) {
+func (c *applyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error, bool) {
 	mutateLogPathIsDir, err := checkMutateLogPath(c.MutateLogPath)
 	if err != nil {
 		return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("failed to create file/folder (%w)", err), false
@@ -197,7 +210,7 @@ func (c *ApplyCommandConfig) getMutateLogPathIsDir(skipInvalidPolicies SkippedIn
 	return nil, nil, skipInvalidPolicies, nil, err, mutateLogPathIsDir
 }
 
-func (c *ApplyCommandConfig) applyValidatingAdmissionPolicytoResource(
+func (c *applyCommandConfig) applyValidatingAdmissionPolicytoResource(
 	vaps []v1alpha1.ValidatingAdmissionPolicy,
 	vapBindings []v1alpha1.ValidatingAdmissionPolicyBinding,
 	resources []*unstructured.Unstructured,
@@ -223,13 +236,14 @@ func (c *ApplyCommandConfig) applyValidatingAdmissionPolicytoResource(
 	return responses, nil
 }
 
-func (c *ApplyCommandConfig) applyPolicytoResource(
+func (c *applyCommandConfig) applyPolicytoResource(
 	out io.Writer,
 	store *store.Store,
 	vars *variables.Variables,
 	policies []kyvernov1.PolicyInterface,
 	resources []*unstructured.Unstructured,
-	skipInvalidPolicies *SkippedInvalidPolicies,
+	exceptions []*kyvernov2beta1.PolicyException,
+	skipInvalidPolicies *skippedInvalidPolicies,
 	dClient dclient.Interface,
 	userInfo *v1beta1.RequestInfo,
 	mutateLogPathIsDir bool,
@@ -262,6 +276,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource(
 			Store:                store,
 			Policies:             validPolicies,
 			Resource:             *resource,
+			PolicyExceptions:     exceptions,
 			MutateLogPath:        c.MutateLogPath,
 			MutateLogPathIsDir:   mutateLogPathIsDir,
 			Variables:            vars,
@@ -285,7 +300,7 @@ func (c *ApplyCommandConfig) applyPolicytoResource(
 	return &rc, resources, responses, nil
 }
 
-func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) {
+func (c *applyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.PolicyInterface, vap []v1alpha1.ValidatingAdmissionPolicy, dClient dclient.Interface) ([]*unstructured.Unstructured, error) {
 	resources, err := common.GetResourceAccordingToResourcePath(out, nil, c.ResourcePaths, c.Cluster, policies, vap, dClient, c.Namespace, c.PolicyReport, "")
 	if err != nil {
 		return resources, fmt.Errorf("failed to load resources (%w)", err)
@@ -293,7 +308,7 @@ func (c *ApplyCommandConfig) loadResources(out io.Writer, policies []kyvernov1.P
 	return resources, nil
 }
 
-func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) {
+func (c *applyCommandConfig) loadPolicies(skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, []kyvernov1.PolicyInterface, []v1alpha1.ValidatingAdmissionPolicy, []v1alpha1.ValidatingAdmissionPolicyBinding, error) {
 	// load policies
 	var policies []kyvernov1.PolicyInterface
 	var vaps []v1alpha1.ValidatingAdmissionPolicy
@@ -301,13 +316,11 @@ func (c *ApplyCommandConfig) loadPolicies(skipInvalidPolicies SkippedInvalidPoli
 
 	for _, path := range c.PolicyPaths {
 		isGit := source.IsGit(path)
-
 		if isGit {
 			gitSourceURL, err := url.Parse(path)
 			if err != nil {
 				return nil, nil, skipInvalidPolicies, nil, nil, nil, nil, fmt.Errorf("failed to load policies (%w)", err)
 			}
-
 			pathElems := strings.Split(gitSourceURL.Path[1:], "/")
 			if len(pathElems) <= 1 {
 				err := fmt.Errorf("invalid URL path %s - expected https://<any_git_source_domain>/:owner/:repository/:branch (without --git-branch flag) OR https://<any_git_source_domain>/:owner/:repository/:directory (with --git-branch flag)", gitSourceURL.Path)
@@ -349,7 +362,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, error, dclient.Interface) {
+func (c *applyCommandConfig) initStoreAndClusterClient(store *store.Store, skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error, dclient.Interface) {
 	store.SetLocal(true)
 	store.SetRegistryAccess(c.RegistryAccess)
 	if c.Cluster {
@@ -378,7 +391,7 @@ func (c *ApplyCommandConfig) initStoreAndClusterClient(store *store.Store, skipI
 	return nil, nil, skipInvalidPolicies, nil, err, dClient
 }
 
-func (c *ApplyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipInvalidPolicies SkippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) {
+func (c *applyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipInvalidPolicies skippedInvalidPolicies) (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) {
 	// empty the previous contents of the file just in case if the file already existed before with some content(so as to perform overwrites)
 	// the truncation of files for the case when mutateLogPath is dir, is handled under pkg/kyverno/apply/common.go
 	if !mutateLogPathIsDir && c.MutateLogPath != "" {
@@ -392,8 +405,8 @@ func (c *ApplyCommandConfig) cleanPreviousContent(mutateLogPathIsDir bool, skipI
 	return nil, nil, skipInvalidPolicies, nil, nil
 }
 
-func (c *ApplyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstructured.Unstructured, SkippedInvalidPolicies, []engineapi.EngineResponse, error) {
-	var skipInvalidPolicies SkippedInvalidPolicies
+func (c *applyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstructured.Unstructured, skippedInvalidPolicies, []engineapi.EngineResponse, error) {
+	var skipInvalidPolicies skippedInvalidPolicies
 	if c.ValuesFile != "" && c.Variables != nil {
 		return nil, nil, skipInvalidPolicies, nil, fmt.Errorf("pass the values either using set flag or values_file flag")
 	}
@@ -409,7 +422,7 @@ func (c *ApplyCommandConfig) checkArguments() (*processor.ResultCounts, []*unstr
 	return nil, nil, skipInvalidPolicies, nil, nil
 }
 
-func printSkippedAndInvalidPolicies(out io.Writer, skipInvalidPolicies SkippedInvalidPolicies) {
+func printSkippedAndInvalidPolicies(out io.Writer, skipInvalidPolicies skippedInvalidPolicies) {
 	if len(skipInvalidPolicies.skipped) > 0 {
 		fmt.Fprintln(out, divider)
 		fmt.Fprintln(out, "Policies Skipped (as required variables are not provided by the user):")
diff --git a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
index dcb95742d5..e32ef1a30b 100644
--- a/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
+++ b/cmd/cli/kubectl-kyverno/commands/apply/command_test.go
@@ -18,7 +18,7 @@ func Test_Apply(t *testing.T) {
 	type TestCase struct {
 		gitBranch             string
 		expectedPolicyReports []policyreportv1alpha2.PolicyReport
-		config                ApplyCommandConfig
+		config                applyCommandConfig
 		stdinFile             string
 	}
 	// copy disallow_latest_tag.yaml to local path
@@ -28,7 +28,7 @@ func Test_Apply(t *testing.T) {
 
 	testcases := []*TestCase{
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"},
 				PolicyReport:  true,
@@ -44,7 +44,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{localFileName},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"},
 				PolicyReport:  true,
@@ -60,7 +60,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"},
 				PolicyReport:  true,
@@ -76,7 +76,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/apply/policies"},
 				ResourcePaths: []string{"../../../../../test/cli/apply/resource"},
 				PolicyReport:  true,
@@ -92,7 +92,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"},
 				PolicyReport:  true,
@@ -109,7 +109,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"-"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_latest_tag.yaml"},
 				PolicyReport:  true,
@@ -127,7 +127,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/best_practices/disallow_latest_tag.yaml"},
 				ResourcePaths: []string{"-"},
 				PolicyReport:  true,
@@ -163,7 +163,7 @@ func Test_Apply(t *testing.T) {
 		// 	}},
 		// },
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/apply/policies-set"},
 				ResourcePaths: []string{"../../../../../test/cli/apply/resources-set"},
 				Variables:     []string{"request.operation=UPDATE"},
@@ -180,7 +180,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/deployment1.yaml"},
 				PolicyReport:  true,
@@ -196,7 +196,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployments-replica/deployment2.yaml"},
 				PolicyReport:  true,
@@ -212,7 +212,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/pod1.yaml"},
 				PolicyReport:  true,
@@ -228,7 +228,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/disallow-host-path/pod2.yaml"},
 				PolicyReport:  true,
@@ -244,7 +244,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/deployment1.yaml"},
 				PolicyReport:  true,
@@ -260,7 +260,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/policy.yaml"},
 				ResourcePaths: []string{"../../../../../test/cli/test-validating-admission-policy/check-deployment-labels/deployment2.yaml"},
 				PolicyReport:  true,
@@ -276,7 +276,7 @@ func Test_Apply(t *testing.T) {
 			}},
 		},
 		{
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"https://github.com/kyverno/policies/best-practices/require-labels/", "../../../../../test/best_practices/disallow_latest_tag.yaml"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"},
 				GitBranch:     "main",
@@ -294,7 +294,7 @@ func Test_Apply(t *testing.T) {
 		},
 		{
 			// Same as the above test case but the policy paths are reordered
-			config: ApplyCommandConfig{
+			config: applyCommandConfig{
 				PolicyPaths:   []string{"../../../../../test/best_practices/disallow_latest_tag.yaml", "https://github.com/kyverno/policies/best-practices/require-labels/"},
 				ResourcePaths: []string{"../../../../../test/resources/pod_with_version_tag.yaml"},
 				GitBranch:     "main",
diff --git a/cmd/cli/kubectl-kyverno/commands/test/test.go b/cmd/cli/kubectl-kyverno/commands/test/test.go
index 711efad54e..0dc44e789a 100644
--- a/cmd/cli/kubectl-kyverno/commands/test/test.go
+++ b/cmd/cli/kubectl-kyverno/commands/test/test.go
@@ -7,6 +7,7 @@ import (
 	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
 	"github.com/kyverno/kyverno/api/kyverno/v1beta1"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/deprecations"
+	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/exception"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/path"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/policy"
@@ -74,6 +75,17 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa
 			fmt.Fprintln(out, "  Warning: found duplicated resource", dup.Kind, dup.Name, dup.Namespace)
 		}
 	}
+	// exceptions
+	fmt.Fprintln(out, "  Loading exceptions", "...")
+	exceptionFullPath := path.GetFullPaths(testCase.Test.PolicyExceptions, testDir, isGit)
+	exceptions, err := exception.Load(exceptionFullPath...)
+	if err != nil {
+		return nil, fmt.Errorf("Error: failed to load exceptions (%s)", err)
+	}
+	// Validates that exceptions cannot be used with ValidatingAdmissionPolicies.
+	if len(validatingAdmissionPolicies) > 0 && len(exceptions) > 0 {
+		return nil, fmt.Errorf("Error: Currently, the use of exceptions in conjunction with ValidatingAdmissionPolicies is not supported.")
+	}
 	// init store
 	var store store.Store
 	store.SetLocal(true)
@@ -81,7 +93,11 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa
 	if vars != nil {
 		vars.SetInStore(&store)
 	}
-	fmt.Fprintln(out, "  Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "...")
+	if len(exceptions) > 0 {
+		fmt.Fprintln(out, "  Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "with", len(exceptions), pluralize.Pluralize(len(exceptions), "exception", "exceptions"), "...")
+	} else {
+		fmt.Fprintln(out, "  Applying", len(policies)+len(validatingAdmissionPolicies), pluralize.Pluralize(len(policies)+len(validatingAdmissionPolicies), "policy", "policies"), "to", len(uniques), pluralize.Pluralize(len(uniques), "resource", "resources"), "...")
+	}
 	// TODO document the code below
 	ruleToCloneSourceResource := map[string]string{}
 	for _, policy := range policies {
@@ -144,6 +160,7 @@ func runTest(out io.Writer, testCase test.TestCase, registryAccess bool, auditWa
 			Store:                     &store,
 			Policies:                  validPolicies,
 			Resource:                  *resource,
+			PolicyExceptions:          exceptions,
 			MutateLogPath:             "",
 			Variables:                 vars,
 			UserInfo:                  userInfo,
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 43e3639148..d5ae056ea1 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
@@ -24,6 +24,12 @@ spec:
               of an object. Servers should convert recognized schemas to the latest
               internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
             type: string
+          exceptions:
+            description: Policy Exceptions are the policy exceptions to be used in
+              the test
+            items:
+              type: string
+            type: array
           kind:
             description: 'Kind is a string value representing the REST resource this
               object represents. Servers may infer this from the endpoint the client
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 43e3639148..d5ae056ea1 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
@@ -24,6 +24,12 @@ spec:
               of an object. Servers should convert recognized schemas to the latest
               internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
             type: string
+          exceptions:
+            description: Policy Exceptions are the policy exceptions to be used in
+              the test
+            items:
+              type: string
+            type: array
           kind:
             description: 'Kind is a string value representing the REST resource this
               object represents. Servers may infer this from the endpoint the client
diff --git a/cmd/cli/kubectl-kyverno/exception/load.go b/cmd/cli/kubectl-kyverno/exception/load.go
index 641f1b5083..ddc5c126ca 100644
--- a/cmd/cli/kubectl-kyverno/exception/load.go
+++ b/cmd/cli/kubectl-kyverno/exception/load.go
@@ -2,6 +2,8 @@ package exception
 
 import (
 	"fmt"
+	"os"
+	"path/filepath"
 
 	kyvernov2 "github.com/kyverno/kyverno/api/kyverno/v2"
 	kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
@@ -19,7 +21,23 @@ var (
 	exceptionV2      = schema.GroupVersion(kyvernov2.GroupVersion).WithKind("PolicyException")
 )
 
-func Load(content []byte) ([]*kyvernov2beta1.PolicyException, error) {
+func Load(paths ...string) ([]*kyvernov2beta1.PolicyException, error) {
+	var out []*kyvernov2beta1.PolicyException
+	for _, path := range paths {
+		bytes, err := os.ReadFile(filepath.Clean(path))
+		if err != nil {
+			return nil, fmt.Errorf("unable to read yaml (%w)", err)
+		}
+		exceptions, err := load(bytes)
+		if err != nil {
+			return nil, fmt.Errorf("unable to load exceptions (%w)", err)
+		}
+		out = append(out, exceptions...)
+	}
+	return out, nil
+}
+
+func load(content []byte) ([]*kyvernov2beta1.PolicyException, error) {
 	documents, err := yamlutils.SplitDocuments(content)
 	if err != nil {
 		return nil, err
diff --git a/cmd/cli/kubectl-kyverno/exception/load_test.go b/cmd/cli/kubectl-kyverno/exception/load_test.go
index 733300f29a..31fb276d25 100644
--- a/cmd/cli/kubectl-kyverno/exception/load_test.go
+++ b/cmd/cli/kubectl-kyverno/exception/load_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
-func Test_Load(t *testing.T) {
+func Test_load(t *testing.T) {
 	tests := []struct {
 		name       string
 		policies   string
@@ -31,7 +31,7 @@ func Test_Load(t *testing.T) {
 			bytes, err := os.ReadFile(tt.policies)
 			require.NoError(t, err)
 			require.NoError(t, err)
-			if res, err := Load(bytes); (err != nil) != tt.wantErr {
+			if res, err := load(bytes); (err != nil) != tt.wantErr {
 				t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
 			} else if len(res) != tt.wantLoaded {
 				t.Errorf("Load() loaded amount = %v, wantLoaded %v", len(res), tt.wantLoaded)
diff --git a/cmd/cli/kubectl-kyverno/processor/exceptions.go b/cmd/cli/kubectl-kyverno/processor/exceptions.go
new file mode 100644
index 0000000000..bf6cb2b4c7
--- /dev/null
+++ b/cmd/cli/kubectl-kyverno/processor/exceptions.go
@@ -0,0 +1,21 @@
+package processor
+
+import (
+	kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+type policyExceptionLister struct {
+	exceptions []*kyvernov2beta1.PolicyException
+}
+
+func (l *policyExceptionLister) List(selector labels.Selector) ([]*kyvernov2beta1.PolicyException, error) {
+	var out []*kyvernov2beta1.PolicyException
+	for _, exception := range l.exceptions {
+		exceptionLabels := labels.Set(exception.GetLabels())
+		if selector.Matches(exceptionLabels) {
+			out = append(out, exception)
+		}
+	}
+	return out, nil
+}
diff --git a/cmd/cli/kubectl-kyverno/processor/policy_processor.go b/cmd/cli/kubectl-kyverno/processor/policy_processor.go
index e95356fad5..7b592a8a57 100644
--- a/cmd/cli/kubectl-kyverno/processor/policy_processor.go
+++ b/cmd/cli/kubectl-kyverno/processor/policy_processor.go
@@ -11,6 +11,7 @@ import (
 	json_patch "github.com/evanphx/json-patch/v5"
 	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
 	kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
+	kyvernov2beta1 "github.com/kyverno/kyverno/api/kyverno/v2beta1"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/apis/v1alpha1"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/log"
 	"github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/store"
@@ -38,6 +39,7 @@ type PolicyProcessor struct {
 	Store                     *store.Store
 	Policies                  []kyvernov1.PolicyInterface
 	Resource                  unstructured.Unstructured
+	PolicyExceptions          []*kyvernov2beta1.PolicyException
 	MutateLogPath             string
 	MutateLogPathIsDir        bool
 	Variables                 *variables.Variables
@@ -59,11 +61,13 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse,
 	jp := jmespath.New(cfg)
 	resource := p.Resource
 	namespaceLabels := p.NamespaceSelectorMap[p.Resource.GetNamespace()]
+	policyExceptionLister := &policyExceptionLister{
+		exceptions: p.PolicyExceptions,
+	}
 	var client engineapi.Client
 	if p.Client != nil {
 		client = adapters.Client(p.Client)
 	}
-
 	rclient := p.Store.GetRegistryClient()
 	if rclient == nil {
 		rclient = registryclient.NewOrDie()
@@ -76,7 +80,7 @@ func (p *PolicyProcessor) ApplyPoliciesOnResource() ([]engineapi.EngineResponse,
 		factories.DefaultRegistryClientFactory(adapters.RegistryClient(rclient), nil),
 		imageverifycache.DisabledImageVerifyCache(),
 		store.ContextLoaderFactory(p.Store, nil),
-		nil,
+		policyExceptionLister,
 		"",
 	)
 	gvk, subresource := resource.GroupVersionKind(), ""
diff --git a/docs/user/cli/commands/kyverno_apply.md b/docs/user/cli/commands/kyverno_apply.md
index b609ba2d78..1771b3acd9 100644
--- a/docs/user/cli/commands/kyverno_apply.md
+++ b/docs/user/cli/commands/kyverno_apply.md
@@ -41,6 +41,7 @@ kyverno apply [flags]
   -c, --cluster              Checks if policies should be applied to cluster in the current context
       --context string       The name of the kubeconfig context to use
       --detailed-results     If set to true, display detailed results
+      --exception strings    Policy exception to be considered when evaluating policies against resources
   -b, --git-branch string    test git repository branch
   -h, --help                 help for apply
       --kubeconfig string    path to kubeconfig file with authorization and master location information