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:
parent
a0b1431f41
commit
d812982b2e
25 changed files with 5705 additions and 189 deletions
2
Makefile
2
Makefile
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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 |
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -89,6 +89,8 @@ rules:
|
|||
- validatingpolicies
|
||||
- validatingpolicies/status
|
||||
- celpolicyexceptions
|
||||
- imageverificationpolicies
|
||||
- imageverificationpolicies/status
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
|
|
|
@ -57,6 +57,8 @@ rules:
|
|||
resources:
|
||||
- validatingpolicies
|
||||
- validatingpolicies/status
|
||||
- imageverificationpolicies
|
||||
- imageverificationpolicies/status
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
|
|
|
@ -95,6 +95,7 @@ crds:
|
|||
policies:
|
||||
validatingpolicies: true
|
||||
celpolicyexceptions: true
|
||||
imageverificationpolicies: true
|
||||
|
||||
# -- Install CRDs in group `reports.kyverno.io`
|
||||
reports:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
301
pkg/cel/engine/imageverifyengine.go
Normal file
301
pkg/cel/engine/imageverifyengine.go
Normal 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
|
||||
}
|
181
pkg/cel/engine/imageverifyengine_test.go
Normal file
181
pkg/cel/engine/imageverifyengine_test.go
Normal 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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
|
|
68
pkg/cel/libs/context/mock.go
Normal file
68
pkg/cel/libs/context/mock.go
Normal 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() {}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
86
pkg/imageverification/evaluator/validate.go
Normal file
86
pkg/imageverification/evaluator/validate.go
Normal 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
|
||||
}
|
|
@ -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) : []",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
58
pkg/webhooks/resource/ivpol/handler.go
Normal file
58
pkg/webhooks/resource/ivpol/handler.go
Normal 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...)
|
||||
}
|
Loading…
Add table
Reference in a new issue