1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-20 23:02:36 +00:00

feat: webhook handlers for image verification (#12318)

* feat: webhook support for image verification

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* feat: add validation

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: add tests

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: tests

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: ci

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: codegen

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: trim prefix

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: only use matched policies

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: conflicts

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: remove commented code

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

---------

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Vishal Choudhary 2025-03-11 13:08:11 +05:30 committed by GitHub
parent a0b1431f41
commit d812982b2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 5705 additions and 189 deletions

View file

@ -617,6 +617,7 @@ codegen-cli-crds: codegen-crds-cli
@cp config/crds/kyverno/kyverno.io_policyexceptions.yaml cmd/cli/kubectl-kyverno/data/crds
@cp config/crds/policies.kyverno.io/policies.kyverno.io_celpolicyexceptions.yaml cmd/cli/kubectl-kyverno/data/crds
@cp config/crds/policies.kyverno.io/policies.kyverno.io_validatingpolicies.yaml cmd/cli/kubectl-kyverno/data/crds
@cp config/crds/policies.kyverno.io/policies.kyverno.io_imageverificationpolicies.yaml cmd/cli/kubectl-kyverno/data/crds
@cp cmd/cli/kubectl-kyverno/config/crds/* cmd/cli/kubectl-kyverno/data/crds
.PHONY: codegen-docs-all
@ -671,6 +672,7 @@ codegen-helm-crds: codegen-crds-all
$(call generate_crd,kyverno.io_updaterequests.yaml,kyverno,kyverno.io,kyverno,updaterequests)
$(call generate_crd,policies.kyverno.io_celpolicyexceptions.yaml,policies.kyverno.io,policies.kyverno.io,policies,celpolicyexceptions)
$(call generate_crd,policies.kyverno.io_validatingpolicies.yaml,policies.kyverno.io,policies.kyverno.io,policies,validatingpolicies)
$(call generate_crd,policies.kyverno.io_imageverificationpolicies.yaml,policies.kyverno.io,policies.kyverno.io,policies,imageverificationpolicies)
$(call generate_crd,reports.kyverno.io_clusterephemeralreports.yaml,reports,reports.kyverno.io,reports,clusterephemeralreports)
$(call generate_crd,reports.kyverno.io_ephemeralreports.yaml,reports,reports.kyverno.io,reports,ephemeralreports)
$(call generate_crd,wgpolicyk8s.io_clusterpolicyreports.yaml,policyreport,wgpolicyk8s.io,wgpolicyk8s,clusterpolicyreports)

View file

@ -11,6 +11,7 @@ const (
// Well known annotations
AnnotationAutogenControllers = "pod-policies.kyverno.io/autogen-controllers"
AnnotationImageVerify = "kyverno.io/verify-images"
AnnotationImageVerifyOutcomes = "kyverno.io/image-verification-outcomes"
AnnotationPolicyCategory = "policies.kyverno.io/category"
AnnotationPolicyScored = "policies.kyverno.io/scored"
AnnotationPolicySeverity = "policies.kyverno.io/severity"

View file

@ -1,6 +1,9 @@
package v1alpha1
import (
"strings"
"github.com/kyverno/kyverno/api/kyverno"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -21,6 +24,20 @@ type ImageVerificationPolicy struct {
Status PolicyStatus `json:"status,omitempty"`
}
func (s *ImageVerificationPolicy) GetName() string {
name := s.Name
if s.Annotations == nil {
if _, found := s.Annotations[kyverno.AnnotationAutogenControllers]; found {
if strings.HasPrefix(name, "autogen-cronjobs-") {
return strings.TrimPrefix(name, "autogen-cronjobs-")
} else if strings.HasPrefix(name, "autogen-") {
return strings.TrimPrefix(name, "autogen-")
}
}
}
return name
}
func (s *ImageVerificationPolicy) GetMatchConstraints() admissionregistrationv1.MatchResources {
if s.Spec.MatchConstraints == nil {
return admissionregistrationv1.MatchResources{}

View file

@ -260,6 +260,7 @@ The chart values are organised per component.
| crds.groups.kyverno | object | `{"celpolicyexceptions":true,"cleanuppolicies":true,"clustercleanuppolicies":true,"clusterpolicies":true,"globalcontextentries":true,"policies":true,"policyexceptions":true,"updaterequests":true,"validatingpolicies":true}` | Install CRDs in group `kyverno.io` |
| crds.groups.policies.validatingpolicies | bool | `true` | |
| crds.groups.policies.celpolicyexceptions | bool | `true` | |
| crds.groups.policies.imageverificationpolicies | bool | `true` | |
| crds.groups.reports | object | `{"clusterephemeralreports":true,"ephemeralreports":true}` | Install CRDs in group `reports.kyverno.io` |
| crds.groups.wgpolicyk8s | object | `{"clusterpolicyreports":true,"policyreports":true}` | Install CRDs in group `wgpolicyk8s.io` |
| crds.annotations | object | `{}` | Additional CRDs annotations |

View file

@ -89,6 +89,8 @@ rules:
- validatingpolicies
- validatingpolicies/status
- celpolicyexceptions
- imageverificationpolicies
- imageverificationpolicies/status
verbs:
- create
- delete

View file

@ -57,6 +57,8 @@ rules:
resources:
- validatingpolicies
- validatingpolicies/status
- imageverificationpolicies
- imageverificationpolicies/status
verbs:
- create
- delete

View file

@ -95,6 +95,7 @@ crds:
policies:
validatingpolicies: true
celpolicyexceptions: true
imageverificationpolicies: true
# -- Install CRDs in group `reports.kyverno.io`
reports:

View file

@ -644,7 +644,7 @@ func main() {
os.Exit(1)
}
celEngine = celengine.NewEngine(
provider,
provider.CompiledValidationPolicies,
func(name string) *corev1.Namespace {
ns, err := setup.KubeClient.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{})
if err != nil {

File diff suppressed because it is too large Load diff

View file

@ -47,6 +47,11 @@ func autogenIvPols(ivpol *policiesv1alpha1.ImageVerificationPolicy, controllerSe
}
var err error
policy := ivpol.DeepCopy()
if controllers == "cronjobs" {
policy.Name = "autogen-cronjobs-" + policy.Name
} else {
policy.Name = "autogen-" + policy.Name
}
operations := ivpol.Spec.MatchConstraints.ResourceRules[0].Operations
// create a resource rule for pod controllers
policy.Spec.MatchConstraints = createMatchConstraints(controllers, operations)

View file

@ -44,7 +44,7 @@ var (
Images: []policiesv1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.containers.map(e, e.image)",
Expression: "object.spec.containers.map(e, e.image)",
},
},
Attestors: []policiesv1alpha1.Attestor{
@ -109,20 +109,21 @@ func Test_AutogenImageVerify(t *testing.T) {
cronimg := []policiesv1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.jobTemplate.spec.template.spec.containers.map(e, e.image)",
Expression: "object.spec.jobTemplate.spec.template.spec.containers.map(e, e.image)",
},
}
podctrlimg := []policiesv1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.template.spec.containers.map(e, e.image)",
Expression: "object.spec.template.spec.containers.map(e, e.image)",
},
}
autogenerated, err := GetAutogenRulesImageVerify(ivpol)
assert.NoError(t, err)
assert.Equal(t, len(autogenerated), 1)
assert.Equal(t, autogenerated[0].Name, "autogen-cronjobs-test")
assert.Equal(t, autogenerated[0].Spec.MatchConstraints.ResourceRules, cronRule)
assert.Equal(t, len(autogenerated[0].Spec.Images), 1)
assert.Equal(t, autogenerated[0].Spec.Images, cronimg)
@ -133,6 +134,7 @@ func Test_AutogenImageVerify(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, len(autogenerated), 2)
assert.Equal(t, autogenerated[0].Spec.MatchConstraints.ResourceRules, cronRule)
assert.Equal(t, autogenerated[1].Name, "autogen-test")
assert.Equal(t, autogenerated[1].Spec.MatchConstraints.ResourceRules, podctrl)
assert.Equal(t, len(autogenerated[1].Spec.Images), 1)
assert.Equal(t, autogenerated[1].Spec.Images, podctrlimg)

View file

@ -0,0 +1,301 @@
package engine
import (
"context"
"encoding/json"
"fmt"
"reflect"
"github.com/go-logr/logr"
"github.com/kyverno/kyverno/api/kyverno"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/cel/matching"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
eval "github.com/kyverno/kyverno/pkg/imageverification/evaluator"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
"golang.org/x/exp/maps"
"gomodules.xyz/jsonpatch/v2"
admissionv1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
type ImageVerifyEngine interface {
HandleMutating(context.Context, EngineRequest) (eval.ImageVerifyEngineResponse, []jsonpatch.JsonPatchOperation, error)
HandleValidating(ctx context.Context, request EngineRequest) (eval.ImageVerifyEngineResponse, error)
}
type ivengine struct {
logger logr.Logger
provider ImageVerifyPolProviderFunc
nsResolver NamespaceResolver
matcher matching.Matcher
lister k8scorev1.SecretInterface
registryOpts []imagedataloader.Option
}
func NewImageVerifyEngine(logger logr.Logger, provider ImageVerifyPolProviderFunc, nsResolver NamespaceResolver, matcher matching.Matcher, lister k8scorev1.SecretInterface, registryOpts []imagedataloader.Option) ImageVerifyEngine {
return &ivengine{
logger: logger,
provider: provider,
nsResolver: nsResolver,
matcher: matcher,
lister: lister,
registryOpts: registryOpts,
}
}
func (e *ivengine) HandleValidating(ctx context.Context, request EngineRequest) (eval.ImageVerifyEngineResponse, error) {
var response eval.ImageVerifyEngineResponse
// fetch compiled policies
policies, err := e.provider.ImageVerificationPolicies(ctx)
if err != nil {
return response, err
}
// load objects
object, oldObject, err := admissionutils.ExtractResources(nil, request.request)
if err != nil {
return response, err
}
response.Resource = &object
if response.Resource.Object == nil {
response.Resource = &oldObject
}
// default dry run
dryRun := false
if request.request.DryRun != nil {
dryRun = *request.request.DryRun
}
// create admission attributes
attr := admission.NewAttributesRecord(
&object,
&oldObject,
schema.GroupVersionKind(request.request.Kind),
request.request.Namespace,
request.request.Name,
schema.GroupVersionResource(request.request.Resource),
request.request.SubResource,
admission.Operation(request.request.Operation),
nil,
dryRun,
// TODO
nil,
)
// resolve namespace
var namespace runtime.Object
if ns := request.request.Namespace; ns != "" {
namespace = e.nsResolver(ns)
}
// evaluate policies
responses, err := e.handleValidation(policies, attr, namespace)
if err != nil {
return response, err
}
response.Policies = append(response.Policies, responses...)
return response, nil
}
func (e *ivengine) HandleMutating(ctx context.Context, request EngineRequest) (eval.ImageVerifyEngineResponse, []jsonpatch.JsonPatchOperation, error) {
var response eval.ImageVerifyEngineResponse
// fetch compiled policies
policies, err := e.provider.ImageVerificationPolicies(ctx)
if err != nil {
return response, nil, err
}
// load objects
object, oldObject, err := admissionutils.ExtractResources(nil, request.request)
if err != nil {
return response, nil, err
}
response.Resource = &object
if response.Resource.Object == nil {
response.Resource = &oldObject
}
// default dry run
dryRun := false
if request.request.DryRun != nil {
dryRun = *request.request.DryRun
}
// create admission attributes
attr := admission.NewAttributesRecord(
&object,
&oldObject,
schema.GroupVersionKind(request.request.Kind),
request.request.Namespace,
request.request.Name,
schema.GroupVersionResource(request.request.Resource),
request.request.SubResource,
admission.Operation(request.request.Operation),
nil,
dryRun,
// TODO
nil,
)
// resolve namespace
var namespace runtime.Object
if ns := request.request.Namespace; ns != "" {
namespace = e.nsResolver(ns)
}
// evaluate policies
responses, patches, err := e.handleMutation(ctx, policies, attr, &request.request, namespace, request.context)
if err != nil {
return response, nil, err
}
response.Policies = append(response.Policies, responses...)
return response, patches, nil
}
func (e *ivengine) matchPolicy(policy CompiledImageVerificationPolicy, attr admission.Attributes, namespace runtime.Object) (bool, error) {
match := func(constraints *admissionregistrationv1.MatchResources) (bool, error) {
criteria := matchCriteria{constraints: constraints}
matches, err := e.matcher.Match(&criteria, attr, namespace)
if err != nil {
return false, err
}
return matches, nil
}
// match against main policy constraints
matches, err := match(policy.Policy.Spec.MatchConstraints)
if err != nil {
return false, err
}
if matches {
return true, nil
}
return false, nil
}
func (e *ivengine) handleMutation(ctx context.Context, policies []CompiledImageVerificationPolicy, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) ([]eval.ImageVerifyPolicyResponse, []jsonpatch.JsonPatchOperation, error) {
results := make(map[string]eval.ImageVerifyPolicyResponse, len(policies))
filteredPolicies := make([]CompiledImageVerificationPolicy, 0)
if e.matcher != nil {
for _, pol := range policies {
matches, err := e.matchPolicy(pol, attr, namespace)
response := eval.ImageVerifyPolicyResponse{
Policy: pol.Policy,
Actions: pol.Actions,
}
if err != nil {
response.Result = *engineapi.RuleError("match", engineapi.ImageVerify, "failed to execute matching", err, nil)
results[pol.Policy.GetName()] = response
} else if matches {
filteredPolicies = append(filteredPolicies, pol)
}
}
}
ictx, err := imagedataloader.NewImageContext(e.lister, e.registryOpts...)
if err != nil {
return nil, nil, err
}
c := eval.NewCompiler(ictx, e.lister, request.RequestResource)
for _, ivpol := range filteredPolicies {
response := eval.ImageVerifyPolicyResponse{
Policy: ivpol.Policy,
Actions: ivpol.Actions,
}
if p, errList := c.Compile(e.logger, ivpol.Policy); errList != nil {
response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to compile policy", err, nil)
} else {
result, err := p.Evaluate(ctx, ictx, attr, request, namespace, true)
if err != nil {
response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to evaluate policy", err, nil)
} else {
ruleName := ivpol.Policy.GetName()
if result.Error != nil {
response.Result = *engineapi.RuleError(ruleName, engineapi.ImageVerify, "error", err, nil)
} else if result.Result {
response.Result = *engineapi.RulePass(ruleName, engineapi.ImageVerify, "success", nil)
} else {
response.Result = *engineapi.RuleFail(ruleName, engineapi.ImageVerify, result.Message, nil)
}
}
}
results[ivpol.Policy.GetName()] = response
}
ann, err := objectAnnotations(attr)
if err != nil {
return nil, nil, err
}
patches, err := eval.MakeImageVerifyOutcomePatch(len(ann) != 0, e.logger, results)
if err != nil {
return nil, nil, err
}
return maps.Values(results), patches, nil
}
func objectAnnotations(attr admission.Attributes) (map[string]string, error) {
obj := attr.GetObject()
if obj == nil || reflect.ValueOf(obj).IsNil() {
return nil, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
u := &unstructured.Unstructured{Object: ret}
return u.GetAnnotations(), nil
}
func (e *ivengine) handleValidation(policies []CompiledImageVerificationPolicy, attr admission.Attributes, namespace runtime.Object) ([]eval.ImageVerifyPolicyResponse, error) {
responses := make(map[string]eval.ImageVerifyPolicyResponse)
annotations, err := objectAnnotations(attr)
if err != nil {
return nil, err
}
if len(annotations) == 0 {
return nil, fmt.Errorf("annotations not present on object, image verification failed")
}
filteredPolicies := make([]CompiledImageVerificationPolicy, 0)
if e.matcher != nil {
for _, pol := range policies {
matches, err := e.matchPolicy(pol, attr, namespace)
response := eval.ImageVerifyPolicyResponse{
Policy: pol.Policy,
Actions: pol.Actions,
}
if err != nil {
response.Result = *engineapi.RuleError("match", engineapi.ImageVerify, "failed to execute matching", err, nil)
responses[pol.Policy.GetName()] = response
} else if matches {
filteredPolicies = append(filteredPolicies, pol)
}
}
}
if data, found := annotations[kyverno.AnnotationImageVerifyOutcomes]; !found {
return nil, fmt.Errorf("%s annotation not present", kyverno.AnnotationImageVerifyOutcomes)
} else {
var outcomes map[string]eval.ImageVerificationOutcome
if err := json.Unmarshal([]byte(data), &outcomes); err != nil {
return nil, err
}
for _, pol := range filteredPolicies {
resp := eval.ImageVerifyPolicyResponse{
Policy: pol.Policy,
Actions: pol.Actions,
}
if o, found := outcomes[pol.Policy.GetName()]; !found {
resp.Result = *engineapi.RuleFail(pol.Policy.GetName(), engineapi.ImageVerify, "policy not evaluated", nil)
} else {
resp.Result = *engineapi.NewRuleResponse(o.Name, engineapi.ImageVerify, o.Message, o.Status, o.Properties)
}
}
}
return maps.Values(responses), nil
}

View file

@ -0,0 +1,181 @@
package engine
import (
"context"
"encoding/json"
"testing"
"github.com/go-logr/logr"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/cel/matching"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
eval "github.com/kyverno/kyverno/pkg/imageverification/evaluator"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/admission/v1"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
)
var (
signedImage = "ghcr.io/kyverno/test-verify-image:signed"
unsignedImage = "ghcr.io/kyverno/test-verify-image:unsigned"
ivpol = &policiesv1alpha1.ImageVerificationPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "ivpol-notary",
},
Spec: policiesv1alpha1.ImageVerificationPolicySpec{
MatchConstraints: &admissionregistrationv1.MatchResources{
ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Update,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
},
},
},
},
},
EvaluationConfiguration: &policiesv1alpha1.EvaluationConfiguration{
Mode: policiesv1alpha1.EvaluationModeKubernetes,
},
ImageRules: []policiesv1alpha1.ImageRule{
{
Glob: "ghcr.io/*",
},
},
Images: []policiesv1alpha1.Image{},
Attestors: []policiesv1alpha1.Attestor{
{
Name: "notary",
Notary: &policiesv1alpha1.Notary{
Certs: `-----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-----`,
},
},
},
Attestations: []policiesv1alpha1.Attestation{
{
Name: "sbom",
Referrer: &policiesv1alpha1.Referrer{
Type: "sbom/cyclone-dx",
},
},
},
Verifications: []admissionregistrationv1.Validation{
{
Expression: "images.containers.map(image, verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)",
Message: "failed to verify image with notary cert",
},
{
Expression: "images.containers.map(image, verifyAttestationSignatures(image, attestations.sbom ,[attestors.notary])).all(e, e > 0)",
Message: "failed to verify attestation with notary cert",
},
{
Expression: "images.containers.map(image, payload(image, attestations.sbom).bomFormat == 'CycloneDX').all(e, e)",
Message: "sbom is not a cyclone dx sbom",
},
},
},
}
ivfunc = func(ctx context.Context) ([]CompiledImageVerificationPolicy, error) {
return []CompiledImageVerificationPolicy{
{
Policy: ivpol,
Actions: sets.Set[admissionregistrationv1.ValidationAction]{admissionregistrationv1.Deny: sets.Empty{}},
},
}, nil
}
nsResolver = func(_ string) *corev1.Namespace {
return &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
}
}
pod = `{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "test-pod",
"namespace": ""
},
"spec": {
"containers": [
{
"name": "nginx",
"image": "ghcr.io/kyverno/test-verify-image:signed"
}
]
}
}
`
)
func Test_ImageVerifyEngine(t *testing.T) {
engine := NewImageVerifyEngine(logr.Discard(), ivfunc, nsResolver, matching.NewMatcher(), nil, nil)
engineRequest := EngineRequest{
request: v1.AdmissionRequest{
Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: apiruntime.RawExtension{
Raw: []byte(pod),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
},
context: &contextlib.MockCtx{},
}
resp, patches, err := engine.HandleMutating(context.Background(), engineRequest)
assert.NoError(t, err)
assert.Equal(t, len(resp.Policies), 1)
response := resp.Policies[0]
assert.Equal(t, response.Result.Name(), "ivpol-notary")
assert.Equal(t, response.Result.Status(), engineapi.RuleStatusPass)
assert.Equal(t, len(patches), 2)
outcomePatch := patches[1]
data, ok := outcomePatch.Value.(string)
assert.True(t, ok)
var outcomes map[string]eval.ImageVerificationOutcome
err = json.Unmarshal([]byte(data), &outcomes)
assert.NoError(t, err)
v, ok := outcomes["ivpol-notary"]
assert.True(t, ok)
assert.Equal(t, v.Status, engineapi.RuleStatusPass)
}

View file

@ -6,12 +6,14 @@ import (
"sync"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
"github.com/kyverno/kyverno/pkg/cel/autogen"
"github.com/kyverno/kyverno/pkg/cel/policy"
policiesv1alpha1listers "github.com/kyverno/kyverno/pkg/client/listers/policies.kyverno.io/v1alpha1"
"golang.org/x/exp/maps"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/workqueue"
ctrl "sigs.k8s.io/controller-runtime"
@ -21,29 +23,46 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
type CompiledPolicy struct {
type CompiledValidatingPolicy struct {
Actions sets.Set[admissionregistrationv1.ValidationAction]
Policy policiesv1alpha1.ValidatingPolicy
CompiledPolicy policy.CompiledPolicy
}
type Provider interface {
CompiledPolicies(context.Context) ([]CompiledPolicy, error)
type CompiledImageVerificationPolicy struct {
Policy *policiesv1alpha1.ImageVerificationPolicy
Actions sets.Set[admissionregistrationv1.ValidationAction]
}
type ProviderFunc func(context.Context) ([]CompiledPolicy, error)
type Provider interface {
CompiledValidationPolicies(context.Context) ([]CompiledValidatingPolicy, error)
ImageVerificationPolicies(context.Context) ([]CompiledImageVerificationPolicy, error)
}
func (f ProviderFunc) CompiledPolicies(ctx context.Context) ([]CompiledPolicy, error) {
type reconcilers struct {
*policyReconciler
*ivpolpolicyReconciler
}
type VPolProviderFunc func(context.Context) ([]CompiledValidatingPolicy, error)
func (f VPolProviderFunc) CompiledValidationPolicies(ctx context.Context) ([]CompiledValidatingPolicy, error) {
return f(ctx)
}
func NewProvider(compiler policy.Compiler, policies []policiesv1alpha1.ValidatingPolicy, exceptions []*policiesv1alpha1.CELPolicyException) (ProviderFunc, error) {
compiled := make([]CompiledPolicy, 0, len(policies))
for _, vp := range policies {
type ImageVerifyPolProviderFunc func(context.Context) ([]CompiledImageVerificationPolicy, error)
func (f ImageVerifyPolProviderFunc) ImageVerificationPolicies(ctx context.Context) ([]CompiledImageVerificationPolicy, error) {
return f(ctx)
}
func NewProvider(compiler policy.Compiler, vpolicies []policiesv1alpha1.ValidatingPolicy, exceptions []*policiesv1alpha1.CELPolicyException) (VPolProviderFunc, error) {
compiled := make([]CompiledValidatingPolicy, 0, len(vpolicies))
for _, vp := range vpolicies {
var matchedExceptions []policiesv1alpha1.CELPolicyException
for _, polex := range exceptions {
for _, ref := range polex.Spec.PolicyRefs {
if ref.Name == vp.GetName() {
if ref.Name == vp.GetName() && ref.Kind == vp.GetKind() {
matchedExceptions = append(matchedExceptions, *polex)
}
}
@ -56,13 +75,13 @@ func NewProvider(compiler policy.Compiler, policies []policiesv1alpha1.Validatin
if len(actions) == 0 {
actions.Insert(admissionregistrationv1.Deny)
}
compiled = append(compiled, CompiledPolicy{
compiled = append(compiled, CompiledValidatingPolicy{
Actions: actions,
Policy: vp,
CompiledPolicy: policy,
})
}
provider := func(context.Context) ([]CompiledPolicy, error) {
provider := func(context.Context) ([]CompiledValidatingPolicy, error) {
return compiled, nil
}
return provider, nil
@ -73,65 +92,76 @@ func NewKubeProvider(
mgr ctrl.Manager,
polexLister policiesv1alpha1listers.CELPolicyExceptionLister,
) (Provider, error) {
exceptionHandlerFuncs := &handler.Funcs{
CreateFunc: func(
ctx context.Context,
tce event.TypedCreateEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tce.Object.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
UpdateFunc: func(
ctx context.Context,
tue event.TypedUpdateEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tue.ObjectNew.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
DeleteFunc: func(
ctx context.Context,
tde event.TypedDeleteEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tde.Object.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
}
r := newPolicyReconciler(compiler, mgr.GetClient(), polexLister)
err := ctrl.NewControllerManagedBy(mgr).
For(&policiesv1alpha1.ValidatingPolicy{}).
Watches(&policiesv1alpha1.CELPolicyException{}, &handler.Funcs{
CreateFunc: func(
ctx context.Context,
tce event.TypedCreateEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tce.Object.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
UpdateFunc: func(
ctx context.Context,
tue event.TypedUpdateEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tue.ObjectNew.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
DeleteFunc: func(
ctx context.Context,
tde event.TypedDeleteEvent[client.Object],
trli workqueue.TypedRateLimitingInterface[reconcile.Request],
) {
polex := tde.Object.(*policiesv1alpha1.CELPolicyException)
for _, ref := range polex.Spec.PolicyRefs {
trli.Add(reconcile.Request{
NamespacedName: client.ObjectKey{
Name: ref.Name,
},
})
}
},
}).
Watches(&policiesv1alpha1.CELPolicyException{}, exceptionHandlerFuncs).
Complete(r)
if err != nil {
return nil, fmt.Errorf("failed to construct manager: %w", err)
}
return r, nil
ivpolr := newivPolicyReconciler(mgr.GetClient(), polexLister)
err = ctrl.NewControllerManagedBy(mgr).
For(&policiesv1alpha1.ImageVerificationPolicy{}).
Watches(&policiesv1alpha1.CELPolicyException{}, exceptionHandlerFuncs).
Complete(ivpolr)
if err != nil {
return nil, fmt.Errorf("failed to construct manager: %w", err)
}
return reconcilers{r, ivpolr}, nil
}
type policyReconciler struct {
client client.Client
compiler policy.Compiler
lock *sync.RWMutex
policies map[string]CompiledPolicy
policies map[string]CompiledValidatingPolicy
polexLister policiesv1alpha1listers.CELPolicyExceptionLister
}
@ -144,7 +174,7 @@ func newPolicyReconciler(
client: client,
compiler: compiler,
lock: &sync.RWMutex{},
policies: map[string]CompiledPolicy{},
policies: map[string]CompiledValidatingPolicy{},
polexLister: polexLister,
}
}
@ -169,7 +199,7 @@ func (r *policyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, nil
}
// get exceptions that match the policy
exceptions, err := r.ListExceptions(policy.GetName())
exceptions, err := listExceptions(r.polexLister, policy.GetName())
if err != nil {
return ctrl.Result{}, err
}
@ -185,7 +215,7 @@ func (r *policyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
if len(actions) == 0 {
actions.Insert(admissionregistrationv1.Deny)
}
r.policies[req.NamespacedName.String()] = CompiledPolicy{
r.policies[req.NamespacedName.String()] = CompiledValidatingPolicy{
Actions: actions,
Policy: policy,
CompiledPolicy: compiled,
@ -193,14 +223,14 @@ func (r *policyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
return ctrl.Result{}, nil
}
func (r *policyReconciler) CompiledPolicies(ctx context.Context) ([]CompiledPolicy, error) {
func (r *policyReconciler) CompiledValidationPolicies(ctx context.Context) ([]CompiledValidatingPolicy, error) {
r.lock.RLock()
defer r.lock.RUnlock()
return maps.Values(r.policies), nil
}
func (r *policyReconciler) ListExceptions(policyName string) ([]policiesv1alpha1.CELPolicyException, error) {
polexList, err := r.polexLister.List(labels.Everything())
func listExceptions(polexLister policiesv1alpha1listers.CELPolicyExceptionLister, policyName string) ([]policiesv1alpha1.CELPolicyException, error) {
polexList, err := polexLister.List(labels.Everything())
if err != nil {
return nil, err
}
@ -214,3 +244,75 @@ func (r *policyReconciler) ListExceptions(policyName string) ([]policiesv1alpha1
}
return exceptions, nil
}
type ivpolpolicyReconciler struct {
client client.Client
lock *sync.RWMutex
policies map[string]CompiledImageVerificationPolicy
polexLister policiesv1alpha1listers.CELPolicyExceptionLister
}
func newivPolicyReconciler(
client client.Client,
polexLister policiesv1alpha1listers.CELPolicyExceptionLister,
) *ivpolpolicyReconciler {
return &ivpolpolicyReconciler{
client: client,
lock: &sync.RWMutex{},
policies: map[string]CompiledImageVerificationPolicy{},
polexLister: polexLister,
}
}
func (r *ivpolpolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var policy policiesv1alpha1.ImageVerificationPolicy
err := r.client.Get(ctx, req.NamespacedName, &policy)
if errors.IsNotFound(err) {
r.lock.Lock()
defer r.lock.Unlock()
delete(r.policies, req.NamespacedName.String())
return ctrl.Result{}, nil
}
if err != nil {
return ctrl.Result{}, err
}
// todo: exception support
// get exceptions that match the policy
// exceptions, err := listExceptions(r.polexLister, policy.GetName())
// if err != nil {
// return ctrl.Result{}, err
// }
autogeneratedIvPols, err := autogen.GetAutogenRulesImageVerify(&policy)
if err != nil {
return ctrl.Result{}, err
}
r.lock.Lock()
defer r.lock.Unlock()
actions := sets.New(policy.Spec.ValidationAction...)
if len(actions) == 0 {
actions.Insert(admissionregistrationv1.Deny)
}
r.policies[req.NamespacedName.String()] = CompiledImageVerificationPolicy{
Policy: &policy,
Actions: actions,
}
for _, p := range autogeneratedIvPols {
namespacedName := types.NamespacedName{
Namespace: p.Namespace,
Name: p.Name,
}
r.policies[namespacedName.String()] = CompiledImageVerificationPolicy{
Policy: p,
Actions: actions,
}
}
return ctrl.Result{}, nil
}
func (r *ivpolpolicyReconciler) ImageVerificationPolicies(ctx context.Context) ([]CompiledImageVerificationPolicy, error) {
r.lock.RLock()
defer r.lock.RUnlock()
return maps.Values(r.policies), nil
}

View file

@ -84,10 +84,10 @@ func (r *EngineRequest) AdmissionRequest() admissionv1.AdmissionRequest {
type EngineResponse struct {
Resource *unstructured.Unstructured
Policies []PolicyResponse
Policies []ValidatingPolicyResponse
}
type PolicyResponse struct {
type ValidatingPolicyResponse struct {
Actions sets.Set[admissionregistrationv1.ValidationAction]
Policy policiesv1alpha1.ValidatingPolicy
Rules []engineapi.RuleResponse
@ -100,12 +100,12 @@ type Engine interface {
type NamespaceResolver = func(string) *corev1.Namespace
type engine struct {
provider Provider
provider VPolProviderFunc
nsResolver NamespaceResolver
matcher matching.Matcher
}
func NewEngine(provider Provider, nsResolver NamespaceResolver, matcher matching.Matcher) Engine {
func NewEngine(provider VPolProviderFunc, nsResolver NamespaceResolver, matcher matching.Matcher) Engine {
return &engine{
provider: provider,
nsResolver: nsResolver,
@ -116,7 +116,7 @@ func NewEngine(provider Provider, nsResolver NamespaceResolver, matcher matching
func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineResponse, error) {
var response EngineResponse
// fetch compiled policies
policies, err := e.provider.CompiledPolicies(ctx)
policies, err := e.provider.CompiledValidationPolicies(ctx)
if err != nil {
return response, err
}
@ -170,7 +170,7 @@ func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineRespo
return response, nil
}
func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, namespace runtime.Object) (bool, int, error) {
func (e *engine) matchPolicy(policy CompiledValidatingPolicy, attr admission.Attributes, namespace runtime.Object) (bool, int, error) {
match := func(constraints *admissionregistrationv1.MatchResources) (bool, error) {
criteria := matchCriteria{constraints: constraints}
matches, err := e.matcher.Match(&criteria, attr, namespace)
@ -205,8 +205,8 @@ func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, n
return false, -1, nil
}
func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, jsonPayload interface{}, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) PolicyResponse {
response := PolicyResponse{
func (e *engine) handlePolicy(ctx context.Context, policy CompiledValidatingPolicy, jsonPayload interface{}, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) ValidatingPolicyResponse {
response := ValidatingPolicyResponse{
Actions: policy.Actions,
Policy: policy.Policy,
}

View file

@ -13,39 +13,6 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type ctx struct {
GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error)
GetGlobalReferenceFunc func(string, string) (any, error)
GetImageDataFunc func(string) (*imagedataloader.ImageData, error)
ParseImageReferenceFunc func(string) (imagedataloader.ImageReference, error)
ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error)
GetResourcesFunc func(string, string, string, string) (*unstructured.Unstructured, error)
}
func (mock *ctx) GetConfigMap(ns string, n string) (*unstructured.Unstructured, error) {
return mock.GetConfigMapFunc(ns, n)
}
func (mock *ctx) GetGlobalReference(n, p string) (any, error) {
return mock.GetGlobalReferenceFunc(n, p)
}
func (mock *ctx) GetImageData(n string) (*imagedataloader.ImageData, error) {
return mock.GetImageDataFunc(n)
}
func (mock *ctx) ParseImageReference(n string) (imagedataloader.ImageReference, error) {
return mock.ParseImageReferenceFunc(n)
}
func (mock *ctx) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
return mock.ListResourcesFunc(apiVersion, resource, namespace)
}
func (mock *ctx) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) {
return mock.GetResourcesFunc(apiVersion, resource, namespace, name)
}
func Test_impl_get_configmap_string_string(t *testing.T) {
opts := Lib()
base, err := cel.NewEnv(opts)
@ -65,46 +32,19 @@ func Test_impl_get_configmap_string_string(t *testing.T) {
assert.NotNil(t, prog)
called := false
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
GetConfigMapFunc: func(string, string) (*unstructured.Unstructured, error) {
called = true
return &unstructured.Unstructured{}, nil
},
}},
}
},
}}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
assert.NotNil(t, out)
assert.True(t, called)
}
type mockGctxStore struct {
data map[string]store.Entry
}
func (m *mockGctxStore) Get(name string) (store.Entry, bool) {
entry, ok := m.data[name]
return entry, ok
}
func (m *mockGctxStore) Set(name string, data store.Entry) {
if m.data == nil {
m.data = make(map[string]store.Entry)
}
m.data[name] = data
}
type mockEntry struct {
data any
err error
}
func (m *mockEntry) Get(_ string) (any, error) {
return m.data, m.err
}
func (m *mockEntry) Stop() {}
func Test_impl_get_globalreference_string_string(t *testing.T) {
opts := Lib()
base, err := cel.NewEnv(opts)
@ -161,7 +101,7 @@ func Test_impl_get_globalreference_string_string(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
mockStore := &mockGctxStore{data: tt.gctxStoreData}
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
GetGlobalReferenceFunc: func(name string, path string) (any, error) {
ent, ok := mockStore.Get(name)
if !ok {
@ -213,14 +153,14 @@ func Test_impl_get_imagedata_string(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, prog)
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
GetImageDataFunc: func(image string) (*imagedataloader.ImageData, error) {
idl, err := imagedataloader.New(nil)
assert.NoError(t, err)
return idl.FetchImageData(context.TODO(), image)
},
}},
}
},
}}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
img := out.Value().(*imagedataloader.ImageData)
@ -249,13 +189,14 @@ func Test_impl_parse_image_ref_string(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, prog)
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
ParseImageReferenceFunc: func(image string) (imagedataloader.ImageReference, error) {
idl, err := imagedataloader.New(nil)
assert.NoError(t, err)
return idl.ParseImageReference(image)
},
}},
},
},
}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
@ -283,7 +224,7 @@ func Test_impl_get_resource_string_string_string_string(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, prog)
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
GetResourcesFunc: func(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) {
return &unstructured.Unstructured{
Object: map[string]any{
@ -296,7 +237,8 @@ func Test_impl_get_resource_string_string_string_string(t *testing.T) {
},
}, nil
},
}},
},
},
}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
@ -323,7 +265,7 @@ func Test_impl_list_resources_string_string_string(t *testing.T) {
assert.NoError(t, err)
assert.NotNil(t, prog)
data := map[string]any{
"context": Context{&ctx{
"context": Context{&MockCtx{
ListResourcesFunc: func(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
return &unstructured.UnstructuredList{
Items: []unstructured.Unstructured{
@ -340,8 +282,8 @@ func Test_impl_list_resources_string_string_string(t *testing.T) {
},
}, nil
},
}},
}
},
}}
out, _, err := prog.Eval(data)
assert.NoError(t, err)
object := out.Value().(map[string]any)

View file

@ -0,0 +1,68 @@
package context
import (
"github.com/kyverno/kyverno/pkg/globalcontext/store"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
// MOCK FOR TESTING
type MockCtx struct {
GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error)
GetGlobalReferenceFunc func(string, string) (any, error)
GetImageDataFunc func(string) (*imagedataloader.ImageData, error)
ParseImageReferenceFunc func(string) (imagedataloader.ImageReference, error)
ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error)
GetResourcesFunc func(string, string, string, string) (*unstructured.Unstructured, error)
}
func (mock *MockCtx) GetConfigMap(ns string, n string) (*unstructured.Unstructured, error) {
return mock.GetConfigMapFunc(ns, n)
}
func (mock *MockCtx) GetGlobalReference(n, p string) (any, error) {
return mock.GetGlobalReferenceFunc(n, p)
}
func (mock *MockCtx) GetImageData(n string) (*imagedataloader.ImageData, error) {
return mock.GetImageDataFunc(n)
}
func (mock *MockCtx) ParseImageReference(n string) (imagedataloader.ImageReference, error) {
return mock.ParseImageReferenceFunc(n)
}
func (mock *MockCtx) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
return mock.ListResourcesFunc(apiVersion, resource, namespace)
}
func (mock *MockCtx) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) {
return mock.GetResourcesFunc(apiVersion, resource, namespace, name)
}
type mockGctxStore struct {
data map[string]store.Entry
}
func (m *mockGctxStore) Get(name string) (store.Entry, bool) {
entry, ok := m.data[name]
return entry, ok
}
func (m *mockGctxStore) Set(name string, data store.Entry) {
if m.data == nil {
m.data = make(map[string]store.Entry)
}
m.data[name] = data
}
type mockEntry struct {
data any
err error
}
func (m *mockEntry) Get(_ string) (any, error) {
return m.data, m.err
}
func (m *mockEntry) Stop() {}

View file

@ -14,13 +14,12 @@ import (
k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
func Evaluate(ctx context.Context, logger logr.Logger, ivpols []*v1alpha1.ImageVerificationPolicy, request interface{}, admissionAttr admission.Attributes, namespace runtime.Object, lister k8scorev1.SecretInterface, registryOpts ...imagedataloader.Option) ([]*EvaluationResult, error) {
func Evaluate(ctx context.Context, logger logr.Logger, ivpols []*v1alpha1.ImageVerificationPolicy, request interface{}, admissionAttr admission.Attributes, namespace runtime.Object, lister k8scorev1.SecretInterface, registryOpts ...imagedataloader.Option) (map[string]*EvaluationResult, error) {
ictx, err := imagedataloader.NewImageContext(lister, registryOpts...)
if err != nil {
return nil, err
}
// TODO: use environmentconfig, add support for other controllers (autogen)
isAdmissionRequest := false
var gvr *metav1.GroupVersionResource
if r, ok := request.(*admissionv1.AdmissionRequest); ok {
@ -31,7 +30,7 @@ func Evaluate(ctx context.Context, logger logr.Logger, ivpols []*v1alpha1.ImageV
policies := filterPolicies(ivpols, isAdmissionRequest)
c := NewCompiler(ictx, lister, gvr)
results := make([]*EvaluationResult, 0)
results := make(map[string]*EvaluationResult, len(policies))
for _, ivpol := range policies {
p, errList := c.Compile(logger, ivpol)
if errList != nil {
@ -42,7 +41,7 @@ func Evaluate(ctx context.Context, logger logr.Logger, ivpols []*v1alpha1.ImageV
if err != nil {
return nil, err
}
results = append(results, result)
results[ivpol.Name] = result
}
return results, nil
}

View file

@ -95,11 +95,11 @@ func Test_Eval(t *testing.T) {
result, err := Evaluate(context.Background(), logr.Discard(), []*policiesv1alpha1.ImageVerificationPolicy{ivpol}, obj(signedImage), nil, nil, nil)
assert.NoError(t, err)
assert.True(t, len(result) == 1)
assert.True(t, result[0].Result)
assert.True(t, result[ivpol.Name].Result)
result, err = Evaluate(context.Background(), logr.Discard(), []*policiesv1alpha1.ImageVerificationPolicy{ivpol}, obj(unsignedImage), nil, nil, nil)
assert.NoError(t, err)
assert.True(t, len(result) == 1)
assert.False(t, result[0].Result)
assert.Equal(t, result[0].Message, "failed to verify image with notary cert")
assert.False(t, result[ivpol.Name].Result)
assert.Equal(t, result[ivpol.Name].Message, "failed to verify image with notary cert")
}

View file

@ -0,0 +1,86 @@
package eval
import (
"encoding/json"
"strings"
"github.com/go-logr/logr"
"github.com/kyverno/kyverno/api/kyverno"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"gomodules.xyz/jsonpatch/v2"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/sets"
)
type ImageVerificationOutcome struct {
// Name is the rule name specified in policy
Name string `json:"name,omitempty"`
// RuleType is the rule type (Mutation,Generation,Validation) for Kyverno Policy
RuleType engineapi.RuleType `json:"ruleType,omitempty"`
// Message is the message response from the rule application
Message string `json:"message,omitempty"`
// Status rule status
Status engineapi.RuleStatus `json:"status,omitempty"`
// EmitWarning enable passing rule message as warning to api server warning header
EmitWarning bool `json:"emitWarning,omitempty"`
// Properties are the additional properties from the rule that will be added to the policy report result
Properties map[string]string `json:"properties,omitempty"`
}
type ImageVerifyEngineResponse struct {
Resource *unstructured.Unstructured
Policies []ImageVerifyPolicyResponse
}
type ImageVerifyPolicyResponse struct {
Policy *policiesv1alpha1.ImageVerificationPolicy
Actions sets.Set[admissionregistrationv1.ValidationAction]
Result engineapi.RuleResponse
}
func outcomeFromPolicyResponse(responses map[string]ImageVerifyPolicyResponse) map[string]ImageVerificationOutcome {
outcomes := make(map[string]ImageVerificationOutcome)
for pol, resp := range responses {
outcomes[pol] = ImageVerificationOutcome{
Name: resp.Result.Name(),
RuleType: resp.Result.RuleType(),
Message: resp.Result.Message(),
Status: resp.Result.Status(),
EmitWarning: resp.Result.EmitWarning(),
Properties: resp.Result.Properties(),
}
}
return outcomes
}
func MakeImageVerifyOutcomePatch(hasAnnotations bool, log logr.Logger, responses map[string]ImageVerifyPolicyResponse) ([]jsonpatch.JsonPatchOperation, error) {
patches := make([]jsonpatch.JsonPatchOperation, 0)
annotationKey := "/metadata/annotations/" + strings.ReplaceAll(kyverno.AnnotationImageVerifyOutcomes, "/", "~1")
if !hasAnnotations {
patch := jsonpatch.JsonPatchOperation{
Operation: "add",
Path: "/metadata/annotations",
Value: map[string]string{},
}
log.V(4).Info("adding annotation patch", "patch", patch)
patches = append(patches, patch)
}
outcomes := outcomeFromPolicyResponse(responses)
data, err := json.Marshal(outcomes)
if err != nil {
return nil, err
}
patch := jsonpatch.JsonPatchOperation{
Operation: "add",
Path: annotationKey,
Value: string(data),
}
log.V(4).Info("adding image verification patch", "patch", patch)
patches = append(patches, patch)
return patches, nil
}

View file

@ -12,43 +12,43 @@ var (
podImageExtractors = []v1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.containers.map(e, e.image)",
Expression: "has(object.spec.containers) ? object.spec.containers.map(e, e.image) : []",
},
{
Name: "initContainers",
Expression: "request.object.spec.initContainers.map(e, e.image)",
Expression: "has(object.spec.initContainers) ? object.spec.initContainers.map(e, e.image) : []",
},
{
Name: "ephemeralContainers",
Expression: "request.object.spec.ephemeralContainers.map(e, e.image)",
Expression: "has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers.map(e, e.image) : []",
},
}
podControllerImageExtractors = []v1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.template.spec.containers.map(e, e.image)",
Expression: "has(object.spec.template.spec.containers) ? object.spec.template.spec.containers.map(e, e.image) : []",
},
{
Name: "initContainers",
Expression: "request.object.spec.template.spec.initContainers.map(e, e.image)",
Expression: "has(object.spec.template.spec.initContainers) ? object.spec.template.spec.initContainers.map(e, e.image) : []",
},
{
Name: "ephemeralContainers",
Expression: "request.object.spec.template.spec.ephemeralContainers.map(e, e.image)",
Expression: "has(object.spec.template.spec.ephemeralContainers) ? object.spec.template.spec.ephemeralContainers.map(e, e.image) : []",
},
}
cronJobImageExtractors = []v1alpha1.Image{
{
Name: "containers",
Expression: "request.object.spec.jobTemplate.spec.template.spec.containers.map(e, e.image)",
Expression: "has(object.spec.jobTemplate.spec.template.spec.containers) ? object.spec.jobTemplate.spec.template.spec.containers.map(e, e.image) : []",
},
{
Name: "initContainers",
Expression: "request.object.spec.jobTemplate.spec.template.spec.initContainers.map(e, e.image)",
Expression: "has(object.spec.jobTemplate.spec.template.spec.initContainers) ? object.spec.jobTemplate.spec.template.spec.initContainers.map(e, e.image) : []",
},
{
Name: "ephemeralContainers",
Expression: "request.object.spec.jobTemplate.spec.template.spec.ephemeralContainers.map(e, e.image)",
Expression: "has(object.spec.jobTemplate.spec.template.spec.ephemeralContainers) ? object.spec.jobTemplate.spec.template.spec.ephemeralContainers.map(e, e.image) : []",
},
}
)

View file

@ -62,31 +62,31 @@ func Test_Match(t *testing.T) {
"nginx:latest",
"alpine:latest",
},
"object": map[string]any{
"spec": map[string]any{
"containers": []map[string]string{
{
"image": "kyverno/image-one",
},
{
"image": "kyverno/image-two",
},
},
"object": map[string]any{
"spec": map[string]any{
"containers": []map[string]string{
{
"image": "kyverno/image-one",
},
"initContainers": []map[string]string{
{
"image": "kyverno/init-image-one",
},
{
"image": "kyverno/init-image-two",
},
{
"image": "kyverno/image-two",
},
"ephemeralContainers": []map[string]string{
{
"image": "kyverno/ephr-image-one",
},
{
"image": "kyverno/ephr-image-two",
},
},
"initContainers": []map[string]string{
{
"image": "kyverno/init-image-one",
},
{
"image": "kyverno/init-image-two",
},
},
"ephemeralContainers": []map[string]string{
{
"image": "kyverno/ephr-image-one",
},
{
"image": "kyverno/ephr-image-two",
},
},
},
@ -132,7 +132,7 @@ func Test_Match(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c, errList := CompileImageExtractors(field.NewPath("spec", "images"), tt.imageExtractor, tt.gvr, []cel.EnvOption{cel.Variable(policy.RequestKey, types.DynType)})
c, errList := CompileImageExtractors(field.NewPath("spec", "images"), tt.imageExtractor, tt.gvr, []cel.EnvOption{cel.Variable(policy.RequestKey, types.DynType), cel.Variable(policy.ObjectKey, types.DynType)})
assert.Nil(t, errList)
images, err := ExtractImages(c, tt.request)
if tt.wantErr {

View file

@ -0,0 +1,58 @@
package ivpol
import (
"context"
"fmt"
"time"
"github.com/go-logr/logr"
celengine "github.com/kyverno/kyverno/pkg/cel/engine"
celpolicy "github.com/kyverno/kyverno/pkg/cel/policy"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/mutate/patch"
eval "github.com/kyverno/kyverno/pkg/imageverification/evaluator"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
jsonutils "github.com/kyverno/kyverno/pkg/utils/json"
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
)
type handler struct {
context celpolicy.Context
engine celengine.ImageVerifyEngine
}
func New(
engine celengine.ImageVerifyEngine,
context celpolicy.Context,
) *handler {
return &handler{
context: context,
engine: engine,
}
}
func (h *handler) Mutate(ctx context.Context, logger logr.Logger, admissionRequest handlers.AdmissionRequest, failurePolicy string, startTime time.Time) handlers.AdmissionResponse {
request := celengine.RequestFromAdmission(h.context, admissionRequest.AdmissionRequest)
response, patches, err := h.engine.HandleMutating(ctx, request)
if err != nil {
return admissionutils.Response(admissionRequest.UID, err)
}
rawPatches := jsonutils.JoinPatches(patch.ConvertPatches(patches...)...)
return h.mutationResponse(request, response, rawPatches)
}
func (h *handler) mutationResponse(request celengine.EngineRequest, response eval.ImageVerifyEngineResponse, rawPatches []byte) handlers.AdmissionResponse {
var warnings []string
for _, policy := range response.Policies {
if policy.Actions.Has(admissionregistrationv1.Warn) {
switch policy.Result.Status() {
case engineapi.RuleStatusFail:
warnings = append(warnings, fmt.Sprintf("Policy %s failed: %s", policy.Policy.GetName(), policy.Result.Message()))
case engineapi.RuleStatusError:
warnings = append(warnings, fmt.Sprintf("Policy %s error: %s", policy.Policy.GetName(), policy.Result.Message()))
}
}
}
return admissionutils.MutationResponse(request.AdmissionRequest().UID, rawPatches, warnings...)
}