From 0ee73430de77afc72eecaf5a23d646db7af6974d Mon Sep 17 00:00:00 2001
From: D N Siva Sathyaseelan
<95441117+sivasathyaseeelan@users.noreply.github.com>
Date: Thu, 5 Sep 2024 16:03:37 +0530
Subject: [PATCH] feat:Add support for condition validation across multiple
image verification attestations or context entry (#9960)
* added Validate in ImageVerification
Signed-off-by: sivasathyaseeelan
name
Name is the variable name.
+predicateType
(Appears on: ForEachValidation, +ValidateImageVerification, Validation)
@@ -2771,6 +2783,20 @@ bool
validate
Validation checks conditions across multiple image +verification attestations or context entries
+required
+(Appears on: +ImageVerification, +ImageVerification) +
++
ValidateImageVerification checks conditions across multiple image +verification attestations or context entries
+ +Field | +Description | +
---|---|
+message + +string + + |
+
+(Optional)
+ Message specifies a custom message to be displayed on failure. + |
+
+deny + + +Deny + + + |
+
+(Optional)
+ Deny defines conditions used to pass or fail a validation rule. + |
+
@@ -8837,6 +8911,20 @@ bool
validate
Validation checks conditions across multiple image +verification attestations or context entries
+required
name
+
+ *
+
+
+
+
+
+
+ string
+
+
+ Name is the variable name.
+ + + + + +predicateType
@@ -3319,6 +3348,7 @@ details.
(Appears in: ForEachValidation, + ValidateImageVerification, Validation)
@@ -5567,6 +5597,38 @@ Defaults to true. +validate
+
+ *
+
+
+
+
+
+
+
+ ValidateImageVerification
+
+
+
+ Validation checks conditions across multiple image +verification attestations or context entries
+ + + + + +required
@@ -9321,6 +9383,99 @@ See: https://kyverno.io/docs/writing-policies/preconditions/
+
+
+
+
+ + (Appears in: + ImageVerification) +
+ + +ValidateImageVerification checks conditions across multiple image +verification attestations or context entries
+ + + +Field | +Description | +
---|---|
message
+
+
+
+
+
+
+ string
+
+
+ |
+
+
+
+ Message specifies a custom message to be displayed on failure. + + + + + + |
+
deny
+
+
+
+
+
+
+
+ Deny
+
+
+
+ |
+
+
+
+ Deny defines conditions used to pass or fail a validation rule. + + + + + + |
+
validate
+
+ *
+
+
+
+
+
+
+
+ ValidateImageVerification
+
+
+
+ Validation checks conditions across multiple image +verification attestations or context entries
+ + + + + +required
diff --git a/pkg/client/applyconfigurations/kyverno/v1/attestation.go b/pkg/client/applyconfigurations/kyverno/v1/attestation.go
index 7958026d41..694710f74b 100644
--- a/pkg/client/applyconfigurations/kyverno/v1/attestation.go
+++ b/pkg/client/applyconfigurations/kyverno/v1/attestation.go
@@ -21,6 +21,7 @@ package v1
// AttestationApplyConfiguration represents an declarative configuration of the Attestation type for use
// with apply.
type AttestationApplyConfiguration struct {
+ Name *string `json:"name,omitempty"`
PredicateType *string `json:"predicateType,omitempty"`
Type *string `json:"type,omitempty"`
Attestors []AttestorSetApplyConfiguration `json:"attestors,omitempty"`
@@ -33,6 +34,14 @@ func Attestation() *AttestationApplyConfiguration {
return &AttestationApplyConfiguration{}
}
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *AttestationApplyConfiguration) WithName(value string) *AttestationApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
// WithPredicateType sets the PredicateType field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the PredicateType field is set to the value of the last call.
diff --git a/pkg/client/applyconfigurations/kyverno/v1/imageverification.go b/pkg/client/applyconfigurations/kyverno/v1/imageverification.go
index c6795df1b6..3334cdd513 100644
--- a/pkg/client/applyconfigurations/kyverno/v1/imageverification.go
+++ b/pkg/client/applyconfigurations/kyverno/v1/imageverification.go
@@ -25,26 +25,27 @@ import (
// ImageVerificationApplyConfiguration represents an declarative configuration of the ImageVerification type for use
// with apply.
type ImageVerificationApplyConfiguration struct {
- FailureAction *v1.ValidationFailureAction `json:"failureAction,omitempty"`
- Type *v1.ImageVerificationType `json:"type,omitempty"`
- Image *string `json:"image,omitempty"`
- ImageReferences []string `json:"imageReferences,omitempty"`
- SkipImageReferences []string `json:"skipImageReferences,omitempty"`
- Key *string `json:"key,omitempty"`
- Roots *string `json:"roots,omitempty"`
- Subject *string `json:"subject,omitempty"`
- Issuer *string `json:"issuer,omitempty"`
- AdditionalExtensions map[string]string `json:"additionalExtensions,omitempty"`
- Attestors []AttestorSetApplyConfiguration `json:"attestors,omitempty"`
- Attestations []AttestationApplyConfiguration `json:"attestations,omitempty"`
- Annotations map[string]string `json:"annotations,omitempty"`
- Repository *string `json:"repository,omitempty"`
- CosignOCI11 *bool `json:"cosignOCI11,omitempty"`
- MutateDigest *bool `json:"mutateDigest,omitempty"`
- VerifyDigest *bool `json:"verifyDigest,omitempty"`
- Required *bool `json:"required,omitempty"`
- ImageRegistryCredentials *ImageRegistryCredentialsApplyConfiguration `json:"imageRegistryCredentials,omitempty"`
- UseCache *bool `json:"useCache,omitempty"`
+ FailureAction *v1.ValidationFailureAction `json:"failureAction,omitempty"`
+ Type *v1.ImageVerificationType `json:"type,omitempty"`
+ Image *string `json:"image,omitempty"`
+ ImageReferences []string `json:"imageReferences,omitempty"`
+ SkipImageReferences []string `json:"skipImageReferences,omitempty"`
+ Key *string `json:"key,omitempty"`
+ Roots *string `json:"roots,omitempty"`
+ Subject *string `json:"subject,omitempty"`
+ Issuer *string `json:"issuer,omitempty"`
+ AdditionalExtensions map[string]string `json:"additionalExtensions,omitempty"`
+ Attestors []AttestorSetApplyConfiguration `json:"attestors,omitempty"`
+ Attestations []AttestationApplyConfiguration `json:"attestations,omitempty"`
+ Annotations map[string]string `json:"annotations,omitempty"`
+ Repository *string `json:"repository,omitempty"`
+ CosignOCI11 *bool `json:"cosignOCI11,omitempty"`
+ MutateDigest *bool `json:"mutateDigest,omitempty"`
+ VerifyDigest *bool `json:"verifyDigest,omitempty"`
+ Validation *ValidateImageVerificationApplyConfiguration `json:"validate,omitempty"`
+ Required *bool `json:"required,omitempty"`
+ ImageRegistryCredentials *ImageRegistryCredentialsApplyConfiguration `json:"imageRegistryCredentials,omitempty"`
+ UseCache *bool `json:"useCache,omitempty"`
}
// ImageVerificationApplyConfiguration constructs an declarative configuration of the ImageVerification type for use with
@@ -215,6 +216,14 @@ func (b *ImageVerificationApplyConfiguration) WithVerifyDigest(value bool) *Imag
return b
}
+// WithValidation sets the Validation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Validation field is set to the value of the last call.
+func (b *ImageVerificationApplyConfiguration) WithValidation(value *ValidateImageVerificationApplyConfiguration) *ImageVerificationApplyConfiguration {
+ b.Validation = value
+ return b
+}
+
// WithRequired sets the Required field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Required field is set to the value of the last call.
diff --git a/pkg/client/applyconfigurations/kyverno/v1/validateimageverification.go b/pkg/client/applyconfigurations/kyverno/v1/validateimageverification.go
new file mode 100644
index 0000000000..c3a4f53cd8
--- /dev/null
+++ b/pkg/client/applyconfigurations/kyverno/v1/validateimageverification.go
@@ -0,0 +1,48 @@
+/*
+Copyright The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1
+
+// ValidateImageVerificationApplyConfiguration represents an declarative configuration of the ValidateImageVerification type for use
+// with apply.
+type ValidateImageVerificationApplyConfiguration struct {
+ Message *string `json:"message,omitempty"`
+ Deny *DenyApplyConfiguration `json:"deny,omitempty"`
+}
+
+// ValidateImageVerificationApplyConfiguration constructs an declarative configuration of the ValidateImageVerification type for use with
+// apply.
+func ValidateImageVerification() *ValidateImageVerificationApplyConfiguration {
+ return &ValidateImageVerificationApplyConfiguration{}
+}
+
+// WithMessage sets the Message field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Message field is set to the value of the last call.
+func (b *ValidateImageVerificationApplyConfiguration) WithMessage(value string) *ValidateImageVerificationApplyConfiguration {
+ b.Message = &value
+ return b
+}
+
+// WithDeny sets the Deny field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Deny field is set to the value of the last call.
+func (b *ValidateImageVerificationApplyConfiguration) WithDeny(value *DenyApplyConfiguration) *ValidateImageVerificationApplyConfiguration {
+ b.Deny = value
+ return b
+}
diff --git a/pkg/client/applyconfigurations/kyverno/v2beta1/imageverification.go b/pkg/client/applyconfigurations/kyverno/v2beta1/imageverification.go
index dc2a7e8186..7665d6126e 100644
--- a/pkg/client/applyconfigurations/kyverno/v2beta1/imageverification.go
+++ b/pkg/client/applyconfigurations/kyverno/v2beta1/imageverification.go
@@ -26,18 +26,19 @@ import (
// ImageVerificationApplyConfiguration represents an declarative configuration of the ImageVerification type for use
// with apply.
type ImageVerificationApplyConfiguration struct {
- FailureAction *v1.ValidationFailureAction `json:"failureAction,omitempty"`
- Type *v1.ImageVerificationType `json:"type,omitempty"`
- ImageReferences []string `json:"imageReferences,omitempty"`
- SkipImageReferences []string `json:"skipImageReferences,omitempty"`
- Attestors []kyvernov1.AttestorSetApplyConfiguration `json:"attestors,omitempty"`
- Attestations []kyvernov1.AttestationApplyConfiguration `json:"attestations,omitempty"`
- Repository *string `json:"repository,omitempty"`
- MutateDigest *bool `json:"mutateDigest,omitempty"`
- VerifyDigest *bool `json:"verifyDigest,omitempty"`
- Required *bool `json:"required,omitempty"`
- ImageRegistryCredentials *kyvernov1.ImageRegistryCredentialsApplyConfiguration `json:"imageRegistryCredentials,omitempty"`
- UseCache *bool `json:"useCache,omitempty"`
+ FailureAction *v1.ValidationFailureAction `json:"failureAction,omitempty"`
+ Type *v1.ImageVerificationType `json:"type,omitempty"`
+ ImageReferences []string `json:"imageReferences,omitempty"`
+ SkipImageReferences []string `json:"skipImageReferences,omitempty"`
+ Attestors []kyvernov1.AttestorSetApplyConfiguration `json:"attestors,omitempty"`
+ Attestations []kyvernov1.AttestationApplyConfiguration `json:"attestations,omitempty"`
+ Repository *string `json:"repository,omitempty"`
+ MutateDigest *bool `json:"mutateDigest,omitempty"`
+ VerifyDigest *bool `json:"verifyDigest,omitempty"`
+ Validation *kyvernov1.ValidateImageVerificationApplyConfiguration `json:"validate,omitempty"`
+ Required *bool `json:"required,omitempty"`
+ ImageRegistryCredentials *kyvernov1.ImageRegistryCredentialsApplyConfiguration `json:"imageRegistryCredentials,omitempty"`
+ UseCache *bool `json:"useCache,omitempty"`
}
// ImageVerificationApplyConfiguration constructs an declarative configuration of the ImageVerification type for use with
@@ -132,6 +133,14 @@ func (b *ImageVerificationApplyConfiguration) WithVerifyDigest(value bool) *Imag
return b
}
+// WithValidation sets the Validation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Validation field is set to the value of the last call.
+func (b *ImageVerificationApplyConfiguration) WithValidation(value *kyvernov1.ValidateImageVerificationApplyConfiguration) *ImageVerificationApplyConfiguration {
+ b.Validation = value
+ return b
+}
+
// WithRequired sets the Required field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Required field is set to the value of the last call.
diff --git a/pkg/client/applyconfigurations/utils.go b/pkg/client/applyconfigurations/utils.go
index 3c799a0d0b..5addd1898a 100644
--- a/pkg/client/applyconfigurations/utils.go
+++ b/pkg/client/applyconfigurations/utils.go
@@ -141,6 +141,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &kyvernov1.TargetResourceSpecApplyConfiguration{}
case v1.SchemeGroupVersion.WithKind("UserInfo"):
return &kyvernov1.UserInfoApplyConfiguration{}
+ case v1.SchemeGroupVersion.WithKind("ValidateImageVerification"):
+ return &kyvernov1.ValidateImageVerificationApplyConfiguration{}
case v1.SchemeGroupVersion.WithKind("ValidatingAdmissionPolicyStatus"):
return &kyvernov1.ValidatingAdmissionPolicyStatusApplyConfiguration{}
case v1.SchemeGroupVersion.WithKind("Validation"):
diff --git a/pkg/engine/handlers/mutation/mutate_image.go b/pkg/engine/handlers/mutation/mutate_image.go
index 1a47d1543f..88f2e8f0f7 100644
--- a/pkg/engine/handlers/mutation/mutate_image.go
+++ b/pkg/engine/handlers/mutation/mutate_image.go
@@ -95,6 +95,7 @@ func (h mutateImageHandler) Process(
engineapi.RuleError(rule.Name, engineapi.ImageVerify, "failed to substitute variables", err, rule.ReportProperties),
)
}
+
var engineResponses []*engineapi.RuleResponse
var patches []jsonpatch.JsonPatchOperation
for _, imageVerify := range ruleCopy.VerifyImages {
@@ -141,22 +142,31 @@ func (h mutateImageHandler) Process(
func substituteVariables(rule kyvernov1.Rule, ctx enginecontext.EvalInterface, logger logr.Logger) (*kyvernov1.Rule, error) {
// remove attestations as variables are not substituted in them
+ hasValidateImageVerification := rule.HasValidateImageVerification()
ruleCopy := *rule.DeepCopy()
for i := range ruleCopy.VerifyImages {
for j := range ruleCopy.VerifyImages[i].Attestations {
ruleCopy.VerifyImages[i].Attestations[j].Conditions = nil
}
+ if hasValidateImageVerification {
+ ruleCopy.VerifyImages[i].Validation.Deny.RawAnyAllConditions = nil
+ }
}
+
var err error
ruleCopy, err = variables.SubstituteAllInRule(logger, ctx, ruleCopy)
if err != nil {
return nil, err
}
+
// replace attestations
for i := range ruleCopy.VerifyImages {
for j := range ruleCopy.VerifyImages[i].Attestations {
ruleCopy.VerifyImages[i].Attestations[j].Conditions = rule.VerifyImages[i].Attestations[j].Conditions
}
+ if hasValidateImageVerification {
+ ruleCopy.VerifyImages[i].Validation.Deny.RawAnyAllConditions = rule.VerifyImages[i].Validation.Deny.RawAnyAllConditions
+ }
}
return &ruleCopy, nil
}
diff --git a/pkg/engine/image_verify_test.go b/pkg/engine/image_verify_test.go
index 604a737970..4e255511a6 100644
--- a/pkg/engine/image_verify_test.go
+++ b/pkg/engine/image_verify_test.go
@@ -1508,3 +1508,229 @@ func Test_SkipImageReferences(t *testing.T) {
fmt.Sprintf("expected: %v, got: %v, failure: %v",
engineapi.RuleStatusPass, erSkip.PolicyResponse.Rules[0].Status(), erSkip.PolicyResponse.Rules[0].Message()))
}
+
+var multipleImageVerificationAttestationPolicyPass = `{
+ "apiVersion": "kyverno.io/v1",
+ "kind": "ClusterPolicy",
+ "metadata": {
+ "name": "check-image-attestation"
+ },
+ "spec": {
+ "validationFailureAction": "Enforce",
+ "webhookTimeoutSeconds": 30,
+ "failurePolicy": "Fail",
+ "rules": [
+ {
+ "name": "verify-attestation-notary",
+ "match": {
+ "any": [
+ {
+ "resources": {
+ "kinds": [
+ "Pod"
+ ]
+ }
+ }
+ ]
+ },
+ "context": [
+ {
+ "name": "keys",
+ "configMap": {
+ "name": "keys",
+ "namespace": "notary-verify-attestation"
+ }
+ }
+ ],
+ "verifyImages": [
+ {
+ "type": "Notary",
+ "imageReferences": [
+ "ghcr.io/kyverno/test-verify-image*"
+ ],
+ "attestations": [
+ {
+ "type": "sbom/cyclone-dx",
+ "name": "sbom",
+ "attestors": [
+ {
+ "entries": [
+ {
+ "certificates": {
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG\nTm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx\nMTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0\ndGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+\nb+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL\nhVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m\nIia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0\nVp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f\nETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG\nA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G\nCSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9\nkYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8\nZq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF\nByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ\n5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0\nuOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz\n-----END CERTIFICATE-----"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "vulnerability-scan",
+ "name": "scan",
+ "attestors": [
+ {
+ "entries": [
+ {
+ "certificates": {
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG\nTm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx\nMTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0\ndGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+\nb+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL\nhVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m\nIia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0\nVp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f\nETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG\nA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G\nCSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9\nkYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8\nZq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF\nByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ\n5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0\nuOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz\n-----END CERTIFICATE-----"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "validate": {
+ "deny": {
+ "conditions": {
+ "any": [
+ {
+ "key": "{{ time_after('{{ sbom.metadata.timestamp }}', '{{ scan.descriptor.timestamp }}' ) }}",
+ "operator": "Equals",
+ "value": "False"
+ }
+ ]
+ }
+ },
+ "message": "Sample Validation"
+ }
+ }
+ ]
+ }
+ ]
+ }
+}`
+
+var multipleImageVerificationAttestationPolicyFail = `{
+ "apiVersion": "kyverno.io/v1",
+ "kind": "ClusterPolicy",
+ "metadata": {
+ "name": "check-image-attestation"
+ },
+ "spec": {
+ "validationFailureAction": "Enforce",
+ "webhookTimeoutSeconds": 30,
+ "failurePolicy": "Fail",
+ "rules": [
+ {
+ "name": "verify-attestation-notary",
+ "match": {
+ "any": [
+ {
+ "resources": {
+ "kinds": [
+ "Pod"
+ ]
+ }
+ }
+ ]
+ },
+ "context": [
+ {
+ "name": "keys",
+ "configMap": {
+ "name": "keys",
+ "namespace": "notary-verify-attestation"
+ }
+ }
+ ],
+ "verifyImages": [
+ {
+ "type": "Notary",
+ "imageReferences": [
+ "ghcr.io/kyverno/test-verify-image*"
+ ],
+ "attestations": [
+ {
+ "type": "sbom/cyclone-dx",
+ "name": "sbom",
+ "attestors": [
+ {
+ "entries": [
+ {
+ "certificates": {
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG\nTm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx\nMTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0\ndGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+\nb+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL\nhVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m\nIia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0\nVp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f\nETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG\nA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G\nCSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9\nkYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8\nZq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF\nByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ\n5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0\nuOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz\n-----END CERTIFICATE-----"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "type": "vulnerability-scan",
+ "name": "scan",
+ "attestors": [
+ {
+ "entries": [
+ {
+ "certificates": {
+ "cert": "-----BEGIN CERTIFICATE-----\nMIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG\nTm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx\nMTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0\ndGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+\nb+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL\nhVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m\nIia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0\nVp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f\nETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG\nA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G\nCSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9\nkYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8\nZq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF\nByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ\n5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0\nuOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz\n-----END CERTIFICATE-----"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "validate": {
+ "deny": {
+ "conditions": {
+ "any": [
+ {
+ "key": "{{ time_after('{{ sbom.metadata.timestamp }}', '{{ scan.descriptor.timestamp }}' ) }}",
+ "operator": "Equals",
+ "value": "True"
+ }
+ ]
+ }
+ },
+ "message": "Sample Validation"
+ }
+ }
+ ]
+ }
+ ]
+ }
+}`
+
+func Test_MultipleImageVerificationAttestationPass(t *testing.T) {
+ policyContextPass := buildContext(t, multipleImageVerificationAttestationPolicyPass, excludeVerifyImageNotaryResourcePass, "")
+
+ // Passes as image is included and not excluded
+ erPass, ivm := testVerifyAndPatchImages(context.TODO(), registryclient.NewOrDie(), nil, policyContextPass, cfg)
+ assert.Equal(t, len(erPass.PolicyResponse.Rules), 1)
+ assert.Equal(t, erPass.PolicyResponse.Rules[0].Status(), engineapi.RuleStatusPass,
+ fmt.Sprintf("expected: %v, got: %v, failure: %v",
+ engineapi.RuleStatusPass, erPass.PolicyResponse.Rules[0].Status(), erPass.PolicyResponse.Rules[0].Message()))
+ assert.Equal(t, ivm.IsEmpty(), false)
+
+ policyContextSkip := buildContext(t, excludeVerifyImageNotaryPolicy, excludeVerifyImageNotaryResourceSkip, "")
+
+ // Skipped as image is excluded
+ erSkip, _ := testVerifyAndPatchImages(context.TODO(), registryclient.NewOrDie(), nil, policyContextSkip, cfg)
+ assert.Equal(t, len(erSkip.PolicyResponse.Rules), 1)
+ assert.Equal(t, erSkip.PolicyResponse.Rules[0].Status(), engineapi.RuleStatusSkip,
+ fmt.Sprintf("expected: %v, got: %v, failure: %v",
+ engineapi.RuleStatusPass, erSkip.PolicyResponse.Rules[0].Status(), erSkip.PolicyResponse.Rules[0].Message()))
+}
+
+func Test_MultipleImageVerificationAttestationFail(t *testing.T) {
+ policyContextPass := buildContext(t, multipleImageVerificationAttestationPolicyFail, excludeVerifyImageNotaryResourcePass, "")
+
+ // Passes as image is included and not excluded
+ erPass, ivm := testVerifyAndPatchImages(context.TODO(), registryclient.NewOrDie(), nil, policyContextPass, cfg)
+ assert.Equal(t, len(erPass.PolicyResponse.Rules), 1)
+ assert.Equal(t, erPass.PolicyResponse.Rules[0].Status(), engineapi.RuleStatusPass,
+ fmt.Sprintf("expected: %v, got: %v, failure: %v",
+ engineapi.RuleStatusPass, erPass.PolicyResponse.Rules[0].Status(), erPass.PolicyResponse.Rules[0].Message()))
+ assert.Equal(t, ivm.IsEmpty(), false)
+
+ policyContextSkip := buildContext(t, excludeVerifyImageNotaryPolicy, excludeVerifyImageNotaryResourceSkip, "")
+
+ // Skipped as image is excluded
+ erSkip, _ := testVerifyAndPatchImages(context.TODO(), registryclient.NewOrDie(), nil, policyContextSkip, cfg)
+ assert.Equal(t, len(erSkip.PolicyResponse.Rules), 1)
+ assert.Equal(t, erSkip.PolicyResponse.Rules[0].Status(), engineapi.RuleStatusSkip,
+ fmt.Sprintf("expected: %v, got: %v, failure: %v",
+ engineapi.RuleStatusPass, erSkip.PolicyResponse.Rules[0].Status(), erSkip.PolicyResponse.Rules[0].Message()))
+}
diff --git a/pkg/engine/internal/imageverifier.go b/pkg/engine/internal/imageverifier.go
index 440989cd75..c898a9c4ef 100644
--- a/pkg/engine/internal/imageverifier.go
+++ b/pkg/engine/internal/imageverifier.go
@@ -23,6 +23,8 @@ import (
"github.com/kyverno/kyverno/pkg/notary"
apiutils "github.com/kyverno/kyverno/pkg/utils/api"
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
+ stringutils "github.com/kyverno/kyverno/pkg/utils/strings"
+ "github.com/kyverno/kyverno/pkg/validation/policy"
"go.uber.org/multierr"
"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -220,6 +222,20 @@ func EvaluateConditions(
return variables.EvaluateAnyAllConditions(log, ctx, c)
}
+func getRawResp(statements []map[string]interface{}) ([]byte, error) {
+ for _, statement := range statements {
+ predicate, ok := statement["predicate"].(map[string]interface{})
+ if ok {
+ rawResp, err := json.Marshal(predicate)
+ if err != nil {
+ return nil, err
+ }
+ return rawResp, nil
+ }
+ }
+ return nil, fmt.Errorf("predicate not found in any statement")
+}
+
// verify applies policy rules to each matching image. The policy rule results and annotation patches are
// added to tme imageVerifier `resp` and `ivm` fields.
func (iv *ImageVerifier) Verify(
@@ -405,6 +421,7 @@ func (iv *ImageVerifier) verifyAttestations(
image := imageInfo.String()
for i, attestation := range imageVerify.Attestations {
var errorList []error
+
path := fmt.Sprintf(".attestations[%d]", i)
iv.logger.V(2).Info(fmt.Sprintf("attestation %+v", attestation))
@@ -437,6 +454,22 @@ func (iv *ImageVerifier) verifyAttestations(
continue
}
+ name := imageVerify.Attestations[i].Name
+
+ rawResp, err := getRawResp(cosignResp.Statements)
+ if err != nil {
+ iv.logger.Error(err, "Error while finding report in statement")
+ errorList = append(errorList, err)
+ continue
+ }
+
+ err = iv.policyContext.JSONContext().AddContextEntry(name, rawResp)
+ if err != nil {
+ iv.logger.Error(err, "failed to add resource data to context entry")
+ errorList = append(errorList, err)
+ continue
+ }
+
if imageInfo.Digest == "" {
imageInfo.Digest = cosignResp.Digest
image = imageInfo.String()
@@ -471,6 +504,18 @@ func (iv *ImageVerifier) verifyAttestations(
iv.logger.V(4).Info("attestation checks passed", "path", path, "image", imageInfo.String(), "type", attestation.Type)
}
+ if iv.rule.HasValidateImageVerification() {
+ for _, imageVerify := range iv.rule.VerifyImages {
+ if err := iv.validate(imageVerify, ctx); err != nil {
+ msg := fmt.Sprintf("validation in verifyImages failed: %v", err)
+ iv.logger.Error(err, "validation in verifyImages failed")
+ return engineapi.RuleFail(iv.rule.Name, engineapi.ImageVerify, msg, iv.rule.ReportProperties), imageInfo.Digest
+ }
+ }
+ msg := fmt.Sprintf("verifyImages validation is passed in %v rule", iv.rule.Name)
+ return engineapi.RulePass(iv.rule.Name, engineapi.ImageVerify, msg, iv.rule.ReportProperties), imageInfo.Digest
+ }
+
msg := fmt.Sprintf("verified image attestations for %s", image)
iv.logger.V(2).Info(msg)
return engineapi.RulePass(iv.rule.Name, engineapi.ImageVerify, msg, iv.rule.ReportProperties), imageInfo.Digest
@@ -745,3 +790,53 @@ func (iv *ImageVerifier) handleMutateDigest(ctx context.Context, digest string,
iv.logger.V(4).Info("adding digest patch", "image", imageInfo.String(), "patch", patch.Json())
return &patch, digest, nil
}
+
+func (iv *ImageVerifier) validate(imageVerify kyvernov1.ImageVerification, ctx context.Context) error {
+ spec := iv.policyContext.Policy().GetSpec()
+ background := spec.BackgroundProcessingEnabled()
+ err := policy.ValidateVariables(iv.policyContext.Policy(), background)
+ if err != nil {
+ return err
+ }
+
+ if imageVerify.Validation.Deny != nil {
+ if err := iv.validateDeny(imageVerify); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (iv *ImageVerifier) validateDeny(imageVerify kyvernov1.ImageVerification) error {
+ if deny, msg, err := CheckDenyPreconditions(iv.logger, iv.policyContext.JSONContext(), imageVerify.Validation.Deny.GetAnyAllConditions()); err != nil {
+ return fmt.Errorf("failed to check deny conditions: %v", err)
+ } else {
+ if deny {
+ return fmt.Errorf("%s", iv.getDenyMessage(imageVerify, deny, msg))
+ }
+ return nil
+ }
+}
+
+func (iv *ImageVerifier) getDenyMessage(imageVerify kyvernov1.ImageVerification, deny bool, msg string) string {
+ if !deny {
+ return fmt.Sprintf("validation imageVerify '%s' passed.", imageVerify.Validation.Message)
+ }
+
+ if imageVerify.Validation.Message == "" && msg == "" {
+ return fmt.Sprintf("validation error: imageVerify %s failed", imageVerify.Validation.Message)
+ }
+
+ s := stringutils.JoinNonEmpty([]string{imageVerify.Validation.Message, msg}, "; ")
+ raw, err := variables.SubstituteAll(iv.logger, iv.policyContext.JSONContext(), s)
+ if err != nil {
+ return msg
+ }
+
+ switch typed := raw.(type) {
+ case string:
+ return typed
+ default:
+ return "the produced message didn't resolve to a string, check your policy definition."
+ }
+}
diff --git a/pkg/validation/policy/validate.go b/pkg/validation/policy/validate.go
index 45da50169c..0ce03a51d9 100644
--- a/pkg/validation/policy/validate.go
+++ b/pkg/validation/policy/validate.go
@@ -424,6 +424,21 @@ func Validate(policy, oldPolicy kyvernov1.PolicyInterface, client dclient.Interf
if rule.HasVerifyImages() {
checkForDeprecatedFieldsInVerifyImages(rule, &warnings)
+
+ if rule.HasValidateImageVerification() {
+ for _, verifyImage := range rule.VerifyImages {
+ validationElem := verifyImage.Validation.DeepCopy()
+ if validationElem.Deny != nil {
+ validationElem.Deny.RawAnyAllConditions = nil
+ }
+ validationJson, err := json.Marshal(validationElem)
+ if err != nil {
+ return nil, err
+ }
+ checkForScaleSubresource(validationJson, allKinds, &warnings)
+ checkForStatusSubresource(validationJson, allKinds, &warnings)
+ }
+ }
}
checkForDeprecatedOperatorsInRule(rule, &warnings)
@@ -560,7 +575,7 @@ func hasInvalidVariables(policy kyvernov1.PolicyInterface, background bool) erro
ctx := buildContext(ruleCopy, background, mutateTarget)
if _, err := variables.SubstituteAllInRule(logging.GlobalLogger(), ctx, *ruleCopy); !variables.CheckNotFoundErr(err) {
- return fmt.Errorf("variable substitution failed for %s/%s: %s", policy.GetName(), ruleCopy.Name, err.Error())
+ return fmt.Errorf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error())
}
}
@@ -713,8 +728,12 @@ func ruleWithoutPattern(ruleCopy *kyvernov1.Rule) *kyvernov1.Rule {
func buildContext(rule *kyvernov1.Rule, background bool, target bool) *enginecontext.MockContext {
re := getAllowedVariables(background, target)
+
ctx := enginecontext.NewMockContext(re)
+
addContextVariables(rule.Context, ctx)
+ addImageVerifyVariables(rule, ctx)
+
for _, fe := range rule.Validation.ForEachValidation {
addContextVariables(fe.Context, ctx)
}
@@ -759,6 +778,16 @@ func addContextVariables(entries []kyvernov1.ContextEntry, ctx *enginecontext.Mo
}
}
+func addImageVerifyVariables(rule *kyvernov1.Rule, ctx *enginecontext.MockContext) {
+ if rule.HasValidateImageVerification() {
+ for _, verifyImage := range rule.VerifyImages {
+ for _, attestation := range verifyImage.Attestations {
+ ctx.AddVariable(attestation.Name + "*")
+ }
+ }
+ }
+}
+
func validateElementInForEach(document apiextensions.JSON) error {
jsonByte, err := json.Marshal(document)
if err != nil {
@@ -1011,6 +1040,16 @@ func validateResources(path *field.Path, rule kyvernov1.Rule) (string, error) {
}
}
}
+ if rule.HasValidateImageVerification() {
+ if target := vi.Validation.Deny.GetAnyAllConditions(); target != nil {
+ if path, err := validateConditions(target, "conditions"); err != nil {
+ return fmt.Sprintf("imageVerify.validate.deny.%s", path), err
+ }
+ if path, err := validateRawJSONConditionOperator(target, "conditions"); err != nil {
+ return fmt.Sprintf("imageVerify.validate.deny.%s", path), err
+ }
+ }
+ }
}
}
diff --git a/test/conformance/chainsaw/e2e-matrix.json b/test/conformance/chainsaw/e2e-matrix.json
index 6a600010c7..8fbdb33fa0 100644
--- a/test/conformance/chainsaw/e2e-matrix.json
+++ b/test/conformance/chainsaw/e2e-matrix.json
@@ -147,8 +147,8 @@
"verifyImages": [
"^verifyImages$/^clusterpolicy$/^cornercases$/^(multiple-attestors)\\[.*\\]$",
"^verifyImages$/^clusterpolicy$/^standard$/^(configmap-context-lookup|empty-image|failure-policy-test-noconfigmap-diffimage-success|failure-policy-test-noconfigmap-diffimage-success-deprecated|imageExtractors-complex|imageExtractors-complex-keyless|imageExtractors-none|imageExtractors-simple|keyed-basic|keyed-basic-namespace-selector|keyed-oci11|keyed-secret|keyed-tsa|keyless-attestation-invalid-attestor|keyless-attestation-regexp|keyless-attestations-multiple-subjects-1|keyless-attestations-multiple-subjects-2|keyless-attestations-multiple-subjects-3|keyless-attestations-multiple-subjects-4|keyless-attestations-multiple-subjects-counts-1)\\[.*\\]$",
- "^verifyImages$/^clusterpolicy$/^standard$/^(keyless-attestations-multiple-subjects-counts-2|keyless-attestations-multiple-subjects-counts-3|keyless-image-invalid-attestor|keyless-mutatedigest-verifydigest-required|keyless-nomutatedigest-noverifydigest-norequired|keyless-nomutatedigest-noverifydigest-required|mutateDigest-noverifyDigest-norequired|noconfigmap-diffimage-success|nomutateDigest-verifyDigest-norequired|notary-attestation-verification|notary-image-verification|notary-image-verification-secret-from-policy|rollback-image-verification|sigstore-attestation-verification-regexp|sigstore-attestation-verification-test|sigstore-image-verification-test|skip-image-reference|update-multi-containers|verify-image-background-audit|verify-image-background-basic)\\[.*\\]$",
- "^verifyImages$/^clusterpolicy$/^standard$/^(verify-image-background-existing|with-mutation)\\[.*\\]$"
+ "^verifyImages$/^clusterpolicy$/^standard$/^(keyless-attestations-multiple-subjects-counts-2|keyless-attestations-multiple-subjects-counts-3|keyless-image-invalid-attestor|keyless-mutatedigest-verifydigest-required|keyless-nomutatedigest-noverifydigest-norequired|keyless-nomutatedigest-noverifydigest-required|multiple-image-verification-attestations-fail|multiple-image-verification-attestations-pass|multiple-image-verification-attestations-trivy-vex-fail|multiple-image-verification-attestations-trivy-vex-pass|mutateDigest-noverifyDigest-norequired|noconfigmap-diffimage-success|nomutateDigest-verifyDigest-norequired|notary-attestation-verification|notary-image-verification|notary-image-verification-secret-from-policy|rollback-image-verification|sigstore-attestation-verification-regexp|sigstore-attestation-verification-test|sigstore-image-verification-test)\\[.*\\]$",
+ "^verifyImages$/^clusterpolicy$/^standard$/^(skip-image-reference|update-multi-containers|verify-image-background-audit|verify-image-background-basic|verify-image-background-existing|with-mutation)\\[.*\\]$"
],
"webhook-configurations": [
"^webhook-configurations$"
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/README.md b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/README.md
new file mode 100644
index 0000000000..bb1b675851
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/README.md
@@ -0,0 +1,12 @@
+## Description
+
+This test verifies multiple image attestations using notary signatures
+
+## Expected Behavior
+
+This test creates a cluster policy.
+When a pod is created with the image reference and the signature on multiple attestations matches, the pod creation is failure
+
+## Reference Issue(s)
+
+https://github.com/kyverno/kyverno/issues/9456
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/chainsaw-test.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/chainsaw-test.yaml
new file mode 100755
index 0000000000..d6e31ed308
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/chainsaw-test.yaml
@@ -0,0 +1,22 @@
+apiVersion: chainsaw.kyverno.io/v1alpha1
+kind: Test
+metadata:
+ creationTimestamp: null
+ name: multiple-image-verification-attestations-fail
+spec:
+ timeouts:
+ delete: 2m
+ steps:
+ - name: step-01
+ try:
+ - apply:
+ file: policy.yaml
+ - assert:
+ file: policy-ready.yaml
+ - name: step-02
+ try:
+ - apply:
+ expect:
+ - check:
+ ($error != null): true
+ file: pod.yaml
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/pod.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/pod.yaml
new file mode 100644
index 0000000000..e16637872d
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/pod.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ creationTimestamp: null
+ labels:
+ run: test
+ name: test
+ namespace: notary-verify-attestation
+spec:
+ containers:
+ - image: ghcr.io/kyverno/test-verify-image:signed
+ name: test
+ resources: {}
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+status: {}
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy-ready.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy-ready.yaml
new file mode 100644
index 0000000000..83c51e7057
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy-ready.yaml
@@ -0,0 +1,9 @@
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+status:
+ conditions:
+ - reason: Succeeded
+ status: "True"
+ type: Ready
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy.yaml
new file mode 100644
index 0000000000..1d34fa08a6
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-fail/policy.yaml
@@ -0,0 +1,83 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: notary-verify-attestation
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: keys
+ namespace: notary-verify-attestation
+data:
+ certificate: |-
+ -----BEGIN CERTIFICATE-----
+ MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+ BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
+ Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
+ MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
+ dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
+ DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
+ b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
+ hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
+ Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
+ Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
+ ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
+ A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
+ CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
+ kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
+ Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
+ ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
+ 5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
+ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
+ -----END CERTIFICATE-----
+---
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+spec:
+ validationFailureAction: Enforce
+ webhookTimeoutSeconds: 30
+ failurePolicy: Fail
+ rules:
+ - name: verify-attestation-notary
+ match:
+ any:
+ - resources:
+ kinds:
+ - Pod
+ context:
+ - name: keys
+ configMap:
+ name: keys
+ namespace: notary-verify-attestation
+ verifyImages:
+ - type: Notary
+ imageReferences:
+ - "ghcr.io/kyverno/test-verify-image*"
+ attestations:
+ - type: sbom/cyclone-dx
+ name: sbom
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ - type: vulnerability-scan
+ name: scan
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ validate:
+ deny:
+ conditions:
+ any:
+ - key: '{{ sbom.bomFormat }}'
+ operator: NotEquals
+ value: CycloneDX
+ message: test1
+ - key: "{{ time_after('{{ sbom.metadata.timestamp }}', '{{ scan.descriptor.timestamp }}' ) }}"
+ operator: Equals
+ value: True
+ message: test2
+ message: scan report should be created after sbom
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/README.md b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/README.md
new file mode 100644
index 0000000000..504750e3a4
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/README.md
@@ -0,0 +1,12 @@
+## Description
+
+This test verifies multiple image attestations using notary signatures
+
+## Expected Behavior
+
+This test creates a cluster policy.
+When a pod is created with the image reference and the signature on multiple attestations matches, the pod creation is successful
+
+## Reference Issue(s)
+
+https://github.com/kyverno/kyverno/issues/9456
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/chainsaw-test.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/chainsaw-test.yaml
new file mode 100755
index 0000000000..0cf5531cf4
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/chainsaw-test.yaml
@@ -0,0 +1,21 @@
+apiVersion: chainsaw.kyverno.io/v1alpha1
+kind: Test
+metadata:
+ creationTimestamp: null
+ name: multiple-image-verification-attestations-pass
+spec:
+ timeouts:
+ delete: 2m
+ steps:
+ - name: step-01
+ try:
+ - apply:
+ file: policy.yaml
+ - assert:
+ file: policy-ready.yaml
+ - name: step-02
+ try:
+ - apply:
+ file: pod.yaml
+ - assert:
+ file: pod-assert.yaml
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod-assert.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod-assert.yaml
new file mode 100644
index 0000000000..d18a0a10e9
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod-assert.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: test
+ namespace: notary-verify-attestation
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod.yaml
new file mode 100644
index 0000000000..e16637872d
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/pod.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ creationTimestamp: null
+ labels:
+ run: test
+ name: test
+ namespace: notary-verify-attestation
+spec:
+ containers:
+ - image: ghcr.io/kyverno/test-verify-image:signed
+ name: test
+ resources: {}
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+status: {}
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy-ready.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy-ready.yaml
new file mode 100644
index 0000000000..83c51e7057
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy-ready.yaml
@@ -0,0 +1,9 @@
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+status:
+ conditions:
+ - reason: Succeeded
+ status: "True"
+ type: Ready
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy.yaml
new file mode 100644
index 0000000000..9f10850711
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-pass/policy.yaml
@@ -0,0 +1,83 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: notary-verify-attestation
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: keys
+ namespace: notary-verify-attestation
+data:
+ certificate: |-
+ -----BEGIN CERTIFICATE-----
+ MIIDTTCCAjWgAwIBAgIJAPI+zAzn4s0xMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV
+ BAYTAlVTMQswCQYDVQQIDAJXQTEQMA4GA1UEBwwHU2VhdHRsZTEPMA0GA1UECgwG
+ Tm90YXJ5MQ0wCwYDVQQDDAR0ZXN0MB4XDTIzMDUyMjIxMTUxOFoXDTMzMDUxOTIx
+ MTUxOFowTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0
+ dGxlMQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3
+ DQEBAQUAA4IBDwAwggEKAoIBAQDNhTwv+QMk7jEHufFfIFlBjn2NiJaYPgL4eBS+
+ b+o37ve5Zn9nzRppV6kGsa161r9s2KkLXmJrojNy6vo9a6g6RtZ3F6xKiWLUmbAL
+ hVTCfYw/2n7xNlVMjyyUpE+7e193PF8HfQrfDFxe2JnX5LHtGe+X9vdvo2l41R6m
+ Iia04DvpMdG4+da2tKPzXIuLUz/FDb6IODO3+qsqQLwEKmmUee+KX+3yw8I6G1y0
+ Vp0mnHfsfutlHeG8gazCDlzEsuD4QJ9BKeRf2Vrb0ywqNLkGCbcCWF2H5Q80Iq/f
+ ETVO9z88R7WheVdEjUB8UrY7ZMLdADM14IPhY2Y+tLaSzEVZAgMBAAGjMjAwMAkG
+ A1UdEwQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0G
+ CSqGSIb3DQEBCwUAA4IBAQBX7x4Ucre8AIUmXZ5PUK/zUBVOrZZzR1YE8w86J4X9
+ kYeTtlijf9i2LTZMfGuG0dEVFN4ae3CCpBst+ilhIndnoxTyzP+sNy4RCRQ2Y/k8
+ Zq235KIh7uucq96PL0qsF9s2RpTKXxyOGdtp9+HO0Ty5txJE2txtLDUIVPK5WNDF
+ ByCEQNhtHgN6V20b8KU2oLBZ9vyB8V010dQz0NRTDLhkcvJig00535/LUylECYAJ
+ 5/jn6XKt6UYCQJbVNzBg/YPGc1RF4xdsGVDBben/JXpeGEmkdmXPILTKd9tZ5TC0
+ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
+ -----END CERTIFICATE-----
+---
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+spec:
+ validationFailureAction: Enforce
+ webhookTimeoutSeconds: 30
+ failurePolicy: Fail
+ rules:
+ - name: verify-attestation-notary
+ match:
+ any:
+ - resources:
+ kinds:
+ - Pod
+ context:
+ - name: keys
+ configMap:
+ name: keys
+ namespace: notary-verify-attestation
+ verifyImages:
+ - type: Notary
+ imageReferences:
+ - "ghcr.io/kyverno/test-verify-image*"
+ attestations:
+ - type: sbom/cyclone-dx
+ name: sbom
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ - type: vulnerability-scan
+ name: scan
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ validate:
+ deny:
+ conditions:
+ any:
+ - key: '{{ sbom.bomFormat }}'
+ operator: NotEquals
+ value: CycloneDX
+ message: test1
+ - key: "{{ time_after('{{ sbom.metadata.timestamp }}', '{{ scan.descriptor.timestamp }}' ) }}"
+ operator: Equals
+ value: False
+ message: test2
+ message: scan report should be created after sbom
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/README.md b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/README.md
new file mode 100644
index 0000000000..bb1b675851
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/README.md
@@ -0,0 +1,12 @@
+## Description
+
+This test verifies multiple image attestations using notary signatures
+
+## Expected Behavior
+
+This test creates a cluster policy.
+When a pod is created with the image reference and the signature on multiple attestations matches, the pod creation is failure
+
+## Reference Issue(s)
+
+https://github.com/kyverno/kyverno/issues/9456
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/chainsaw-test.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/chainsaw-test.yaml
new file mode 100755
index 0000000000..d030144bac
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/chainsaw-test.yaml
@@ -0,0 +1,22 @@
+apiVersion: chainsaw.kyverno.io/v1alpha1
+kind: Test
+metadata:
+ creationTimestamp: null
+ name: multiple-image-verification-attestations-trivy-vex-fail
+spec:
+ timeouts:
+ delete: 2m
+ steps:
+ - name: step-01
+ try:
+ - apply:
+ file: policy.yaml
+ - assert:
+ file: policy-ready.yaml
+ - name: step-02
+ try:
+ - apply:
+ expect:
+ - check:
+ ($error != null): true
+ file: pod.yaml
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/pod.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/pod.yaml
new file mode 100644
index 0000000000..e16637872d
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/pod.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ creationTimestamp: null
+ labels:
+ run: test
+ name: test
+ namespace: notary-verify-attestation
+spec:
+ containers:
+ - image: ghcr.io/kyverno/test-verify-image:signed
+ name: test
+ resources: {}
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+status: {}
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy-ready.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy-ready.yaml
new file mode 100644
index 0000000000..83c51e7057
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy-ready.yaml
@@ -0,0 +1,9 @@
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+status:
+ conditions:
+ - reason: Succeeded
+ status: "True"
+ type: Ready
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy.yaml
new file mode 100644
index 0000000000..95a160a1a3
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-fail/policy.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: notary-verify-attestation
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: keys
+ namespace: notary-verify-attestation
+data:
+ certificate: |-
+ -----BEGIN CERTIFICATE-----
+ MIIDmDCCAoCgAwIBAgIUCntgF4FftePAhEa6nZTsu/NMT3cwDQYJKoZIhvcNAQEL
+ BQAwTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxl
+ MQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwHhcNMjQwNjEwMTYzMTQ2
+ WhcNMzQwNjA4MTYzMTQ2WjBMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAO
+ BgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk5vdGFyeTENMAsGA1UEAwwEdGVzdDCC
+ ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkEGqbILiWye6C1Jz+jwwDY
+ k/rovpXzxS+EQDvfj/YKvx37Kr4cjboJORu3wtzICWhPUtVWZ21ShfjerKgNq0iB
+ mrlF4cqz2KcOfuUT3XBglH/NwhEAqOrGPQrMsoQEFWgnilr0RTc+j4vDnkdkcTj2
+ K/qPhQHRAeb97TdvFCqcZfAGqiOVUqzDGxd2INz/fJd4/nYRX3LJBn9pUGxqRwZV
+ ElP5B/aCBjJDdh6tAElT5aDnLGAB+3+W2YwG342ELyAl2ILpbSRUpKLNAfKEd7Nj
+ 1moIl4or5AIlTkgewZ/AK68HPFJEV3SwNbzkgAC+/mLVCD8tqu0o0ziyIUJtoQMC
+ AwEAAaNyMHAwHQYDVR0OBBYEFFTIzCppwv0vZnAVmETPm1CfMdcYMB8GA1UdIwQY
+ MBaAFFTIzCppwv0vZnAVmETPm1CfMdcYMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD
+ AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQB8/vfP
+ /TQ3X80JEZDsttdvd9NLm08bTJ/T+nh0DIiV10aHymQT9/u+iahfm1+7mj+uv8LS
+ Y63LepQCX5p9SoFzt513pbNYXMBbRrOKpth3DD49IPL2Gce86AFGydfrakd86CL1
+ 9MhFeWhtRf0KndyUX8J2s7jbpoN8HrN4/wZygiEqbQWZG8YtIZ9EewmoVMYirQqH
+ EvW93NcgmjiELuhjndcT/kHjhf8fUAgSuxiPIy6ern02fJjw40KzgiKNvxMoI9su
+ G2zu6gXmxkw+x0SMe9kX+Rg4hCIjTUM7dc66XL5LcTp4S5YEZNVC40/FgTIZoK0e
+ r1dC2/Y1SmmrIoA1
+ -----END CERTIFICATE-----
+---
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+spec:
+ validationFailureAction: Enforce
+ webhookTimeoutSeconds: 30
+ failurePolicy: Fail
+ rules:
+ - name: verify-attestation-notary
+ match:
+ any:
+ - resources:
+ kinds:
+ - Pod
+ context:
+ - name: keys
+ configMap:
+ name: keys
+ namespace: notary-verify-attestation
+ verifyImages:
+ - type: Notary
+ imageReferences:
+ - "ghcr.io/kyverno/test-verify-image*"
+ attestations:
+ - type: trivy/vulnerability-fail-test
+ name: trivy
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ - type: vex/cyclone-dx
+ name: vex
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ validate:
+ deny:
+ conditions:
+ any:
+ - key: '{{ trivy.Vulnerabilities[*].VulnerabilityID }}'
+ operator: AnyNotIn
+ value: '{{ vex.vulnerabilities[*].id }}'
+ message: All vulnerabilities in trivy and vex should be same
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/README.md b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/README.md
new file mode 100644
index 0000000000..43f533b82d
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/README.md
@@ -0,0 +1,12 @@
+## Description
+
+This test verifies multiple image attestations using notary signatures
+
+## Expected Behavior
+
+This test creates a cluster policy.
+When a pod is created with the image reference and the signature on multiple attestations matches, the pod creation is successful
+
+## Reference Issue(s)
+
+https://github.com/kyverno/kyverno/issues/9456
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/chainsaw-test.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/chainsaw-test.yaml
new file mode 100755
index 0000000000..a49bdddcd6
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/chainsaw-test.yaml
@@ -0,0 +1,21 @@
+apiVersion: chainsaw.kyverno.io/v1alpha1
+kind: Test
+metadata:
+ creationTimestamp: null
+ name: multiple-image-verification-attestations-trivy-vex-pass
+spec:
+ timeouts:
+ delete: 2m
+ steps:
+ - name: step-01
+ try:
+ - apply:
+ file: policy.yaml
+ - assert:
+ file: policy-ready.yaml
+ - name: step-02
+ try:
+ - apply:
+ file: pod.yaml
+ - assert:
+ file: pod-assert.yaml
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod-assert.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod-assert.yaml
new file mode 100644
index 0000000000..d18a0a10e9
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod-assert.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ name: test
+ namespace: notary-verify-attestation
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod.yaml
new file mode 100644
index 0000000000..e16637872d
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/pod.yaml
@@ -0,0 +1,16 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ creationTimestamp: null
+ labels:
+ run: test
+ name: test
+ namespace: notary-verify-attestation
+spec:
+ containers:
+ - image: ghcr.io/kyverno/test-verify-image:signed
+ name: test
+ resources: {}
+ dnsPolicy: ClusterFirst
+ restartPolicy: Always
+status: {}
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy-ready.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy-ready.yaml
new file mode 100644
index 0000000000..83c51e7057
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy-ready.yaml
@@ -0,0 +1,9 @@
+apiVersion: kyverno.io/v2beta1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+status:
+ conditions:
+ - reason: Succeeded
+ status: "True"
+ type: Ready
\ No newline at end of file
diff --git a/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy.yaml b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy.yaml
new file mode 100644
index 0000000000..b3024ec2b6
--- /dev/null
+++ b/test/conformance/chainsaw/verifyImages/clusterpolicy/standard/multiple-image-verification-attestations-trivy-vex-pass/policy.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: notary-verify-attestation
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: keys
+ namespace: notary-verify-attestation
+data:
+ certificate: |-
+ -----BEGIN CERTIFICATE-----
+ MIIDmDCCAoCgAwIBAgIUCntgF4FftePAhEa6nZTsu/NMT3cwDQYJKoZIhvcNAQEL
+ BQAwTDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMRAwDgYDVQQHDAdTZWF0dGxl
+ MQ8wDQYDVQQKDAZOb3RhcnkxDTALBgNVBAMMBHRlc3QwHhcNMjQwNjEwMTYzMTQ2
+ WhcNMzQwNjA4MTYzMTQ2WjBMMQswCQYDVQQGEwJVUzELMAkGA1UECAwCV0ExEDAO
+ BgNVBAcMB1NlYXR0bGUxDzANBgNVBAoMBk5vdGFyeTENMAsGA1UEAwwEdGVzdDCC
+ ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJkEGqbILiWye6C1Jz+jwwDY
+ k/rovpXzxS+EQDvfj/YKvx37Kr4cjboJORu3wtzICWhPUtVWZ21ShfjerKgNq0iB
+ mrlF4cqz2KcOfuUT3XBglH/NwhEAqOrGPQrMsoQEFWgnilr0RTc+j4vDnkdkcTj2
+ K/qPhQHRAeb97TdvFCqcZfAGqiOVUqzDGxd2INz/fJd4/nYRX3LJBn9pUGxqRwZV
+ ElP5B/aCBjJDdh6tAElT5aDnLGAB+3+W2YwG342ELyAl2ILpbSRUpKLNAfKEd7Nj
+ 1moIl4or5AIlTkgewZ/AK68HPFJEV3SwNbzkgAC+/mLVCD8tqu0o0ziyIUJtoQMC
+ AwEAAaNyMHAwHQYDVR0OBBYEFFTIzCppwv0vZnAVmETPm1CfMdcYMB8GA1UdIwQY
+ MBaAFFTIzCppwv0vZnAVmETPm1CfMdcYMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQD
+ AgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQB8/vfP
+ /TQ3X80JEZDsttdvd9NLm08bTJ/T+nh0DIiV10aHymQT9/u+iahfm1+7mj+uv8LS
+ Y63LepQCX5p9SoFzt513pbNYXMBbRrOKpth3DD49IPL2Gce86AFGydfrakd86CL1
+ 9MhFeWhtRf0KndyUX8J2s7jbpoN8HrN4/wZygiEqbQWZG8YtIZ9EewmoVMYirQqH
+ EvW93NcgmjiELuhjndcT/kHjhf8fUAgSuxiPIy6ern02fJjw40KzgiKNvxMoI9su
+ G2zu6gXmxkw+x0SMe9kX+Rg4hCIjTUM7dc66XL5LcTp4S5YEZNVC40/FgTIZoK0e
+ r1dC2/Y1SmmrIoA1
+ -----END CERTIFICATE-----
+---
+apiVersion: kyverno.io/v1
+kind: ClusterPolicy
+metadata:
+ name: check-image-attestation
+spec:
+ validationFailureAction: Enforce
+ webhookTimeoutSeconds: 30
+ failurePolicy: Fail
+ rules:
+ - name: verify-attestation-notary
+ match:
+ any:
+ - resources:
+ kinds:
+ - Pod
+ context:
+ - name: keys
+ configMap:
+ name: keys
+ namespace: notary-verify-attestation
+ verifyImages:
+ - type: Notary
+ imageReferences:
+ - "ghcr.io/kyverno/test-verify-image*"
+ attestations:
+ - type: trivy/vulnerability
+ name: trivy
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ - type: vex/cyclone-dx
+ name: vex
+ attestors:
+ - entries:
+ - certificates:
+ cert: "{{ keys.data.certificate }}"
+ validate:
+ deny:
+ conditions:
+ any:
+ - key: '{{ trivy.Vulnerabilities[*].VulnerabilityID }}'
+ operator: AnyNotIn
+ value: '{{ vex.vulnerabilities[*].id }}'
+ message: All vulnerabilities in trivy and vex should be same
\ No newline at end of file