1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-26 01:24:26 +00:00
kyverno/pkg/cel/engine/imageverifyengine.go
Charles-Edouard Brétéché e785ee4882
feat: add globalcontext CEL lib (#12425)
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
2025-03-17 16:53:32 +01:00

299 lines
9.5 KiB
Go

package engine
import (
"context"
"encoding/json"
"fmt"
"reflect"
"github.com/kyverno/kyverno/api/kyverno"
resourcelib "github.com/kyverno/kyverno/pkg/cel/libs/resource"
"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 {
provider ImageVerifyPolProviderFunc
nsResolver NamespaceResolver
matcher matching.Matcher
lister k8scorev1.SecretInterface
registryOpts []imagedataloader.Option
}
func NewImageVerifyEngine(provider ImageVerifyPolProviderFunc, nsResolver NamespaceResolver, matcher matching.Matcher, lister k8scorev1.SecretInterface, registryOpts []imagedataloader.Option) ImageVerifyEngine {
return &ivengine{
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 resourcelib.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(ivpol.Policy); errList != nil {
response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to compile policy", errList.ToAggregate(), 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, 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)
}
responses[pol.Policy.GetName()] = resp
}
}
return maps.Values(responses), nil
}