diff --git a/pkg/cel/engine/imageverifyengine.go b/pkg/cel/engine/imageverifyengine.go index a8810463b8..4e5b8bf40f 100644 --- a/pkg/cel/engine/imageverifyengine.go +++ b/pkg/cel/engine/imageverifyengine.go @@ -204,7 +204,7 @@ func (e *ivengine) handleMutation(ctx context.Context, policies []CompiledImageV } if p, errList := c.Compile(e.logger, ivpol.Policy); errList != nil { - response.Result = *engineapi.RuleError("evaluation", engineapi.ImageVerify, "failed to compile policy", err, 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 { diff --git a/pkg/cel/engine/imageverifyengine_test.go b/pkg/cel/engine/imageverifyengine_test.go index 823a4f64b8..3481fcf0bc 100644 --- a/pkg/cel/engine/imageverifyengine_test.go +++ b/pkg/cel/engine/imageverifyengine_test.go @@ -91,6 +91,10 @@ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz }, }, Verifications: []admissionregistrationv1.Validation{ + { + Expression: "images.containers.map(i, image(i).registry() == \"ghcr.io\" ).all(e, e)", + Message: "images are not from ghcr registry", + }, { Expression: "images.containers.map(image, verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)", Message: "failed to verify image with notary cert", diff --git a/pkg/cel/env.go b/pkg/cel/env.go index 7f0ce9c8f8..f37d59df41 100644 --- a/pkg/cel/env.go +++ b/pkg/cel/env.go @@ -3,6 +3,7 @@ package cel import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/ext" + "github.com/kyverno/kyverno/pkg/cel/libs/image" "k8s.io/apiserver/pkg/cel/library" ) @@ -30,5 +31,6 @@ func NewEnv() (*cel.Env, error) { library.Lists(), library.Regex(), library.URLs(), + image.ImageLib(), ) } diff --git a/pkg/cel/libs/context/impl.go b/pkg/cel/libs/context/impl.go index 94bde3e03a..c3fbece134 100644 --- a/pkg/cel/libs/context/impl.go +++ b/pkg/cel/libs/context/impl.go @@ -62,21 +62,6 @@ func (c *impl) get_imagedata_string(ctx ref.Val, image ref.Val) ref.Val { } } -func (c *impl) parse_imagereference_string(ctx ref.Val, image ref.Val) ref.Val { - if self, err := utils.ConvertToNative[Context](ctx); err != nil { - return types.WrapErr(err) - } else if image, err := utils.ConvertToNative[string](image); err != nil { - return types.WrapErr(err) - } else { - parsedRef, err := self.ParseImageReference(image) - if err != nil { - // Errors are not expected here since Parse is a more lenient parser than ParseRequestURI. - return types.NewErr("failed to parse image data: %v", err) - } - return c.NativeToValue(parsedRef) - } -} - func (c *impl) list_resources_string_string_string(args ...ref.Val) ref.Val { if self, err := utils.ConvertToNative[Context](args[0]); err != nil { return types.WrapErr(err) diff --git a/pkg/cel/libs/context/impl_test.go b/pkg/cel/libs/context/impl_test.go index 885f4274ed..da0ecc5889 100644 --- a/pkg/cel/libs/context/impl_test.go +++ b/pkg/cel/libs/context/impl_test.go @@ -2,6 +2,7 @@ package context import ( "context" + "encoding/json" "errors" "strings" "testing" @@ -146,7 +147,7 @@ func Test_impl_get_imagedata_string(t *testing.T) { env, err := base.Extend(options...) assert.NoError(t, err) assert.NotNil(t, env) - ast, issues := env.Compile(`context.GetImageData("ghcr.io/kyverno/kyverno:latest")`) + ast, issues := env.Compile(`context.GetImageData("ghcr.io/kyverno/kyverno:latest").resolvedImage`) assert.Nil(t, issues) assert.NotNil(t, ast) prog, err := env.Program(ast) @@ -154,54 +155,30 @@ func Test_impl_get_imagedata_string(t *testing.T) { assert.NotNil(t, prog) data := map[string]any{ "context": Context{&MockCtx{ - GetImageDataFunc: func(image string) (*imagedataloader.ImageData, error) { + GetImageDataFunc: func(image string) (map[string]interface{}, error) { idl, err := imagedataloader.New(nil) assert.NoError(t, err) - return idl.FetchImageData(context.TODO(), image) + data, err := idl.FetchImageData(context.TODO(), image) + if err != nil { + return nil, err + } + raw, err := json.Marshal(data.Data()) + if err != nil { + return nil, err + } + apiData := map[string]interface{}{} + err = json.Unmarshal(raw, &apiData) + if err != nil { + return nil, err + } + return apiData, nil }, }, }} out, _, err := prog.Eval(data) assert.NoError(t, err) - img := out.Value().(*imagedataloader.ImageData) - assert.Equal(t, img.Tag, "latest") - assert.True(t, strings.HasPrefix(img.ResolvedImage, "ghcr.io/kyverno/kyverno:latest@sha256:")) - assert.True(t, img.ConfigData != nil) - assert.True(t, img.Manifest != nil) - assert.True(t, img.ImageIndex != nil) -} - -func Test_impl_parse_image_ref_string(t *testing.T) { - opts := Lib() - base, err := cel.NewEnv(opts) - assert.NoError(t, err) - assert.NotNil(t, base) - options := []cel.EnvOption{ - cel.Variable("context", ContextType), - } - env, err := base.Extend(options...) - assert.NoError(t, err) - assert.NotNil(t, env) - ast, issues := env.Compile(`context.ParseImageReference("ghcr.io/kyverno/kyverno:latest")`) - assert.Nil(t, issues) - assert.NotNil(t, ast) - prog, err := env.Program(ast) - assert.NoError(t, err) - assert.NotNil(t, prog) - data := map[string]any{ - "context": Context{&MockCtx{ - ParseImageReferenceFunc: func(image string) (imagedataloader.ImageReference, error) { - return imagedataloader.ParseImageReference(image) - }, - }, - }, - } - out, _, err := prog.Eval(data) - assert.NoError(t, err) - img := out.Value().(imagedataloader.ImageReference) - assert.Equal(t, img.Tag, "latest") - assert.Equal(t, img.Identifier, "latest") - assert.Equal(t, img.Image, "ghcr.io/kyverno/kyverno:latest") + resolvedImg := out.Value().(string) + assert.True(t, strings.HasPrefix(resolvedImg, "ghcr.io/kyverno/kyverno:latest@sha256:")) } func Test_impl_get_resource_string_string_string_string(t *testing.T) { diff --git a/pkg/cel/libs/context/lib.go b/pkg/cel/libs/context/lib.go index 935ecc57d5..7a0041c7d2 100644 --- a/pkg/cel/libs/context/lib.go +++ b/pkg/cel/libs/context/lib.go @@ -67,18 +67,10 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) { cel.MemberOverload( "get_imagedata_string", []*cel.Type{ContextType, types.StringType}, - imageDataType.CelType(), + types.DynType, cel.BinaryBinding(impl.get_imagedata_string), ), }, - "ParseImageReference": { - cel.MemberOverload( - "parse_image_reference_string", - []*cel.Type{ContextType, types.StringType}, - imageReferenceType.CelType(), - cel.BinaryBinding(impl.parse_imagereference_string), - ), - }, "ListResources": { // TODO: should not use DynType in return cel.MemberOverload( diff --git a/pkg/cel/libs/context/mock.go b/pkg/cel/libs/context/mock.go index a0fac0ce6f..c2f6fa6d11 100644 --- a/pkg/cel/libs/context/mock.go +++ b/pkg/cel/libs/context/mock.go @@ -2,18 +2,16 @@ 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) - GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error) + GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error) + GetGlobalReferenceFunc func(string, string) (any, error) + GetImageDataFunc func(string) (map[string]interface{}, error) + ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error) + GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error) } func (mock *MockCtx) GetConfigMap(ns string, n string) (*unstructured.Unstructured, error) { @@ -24,14 +22,10 @@ func (mock *MockCtx) GetGlobalReference(n, p string) (any, error) { return mock.GetGlobalReferenceFunc(n, p) } -func (mock *MockCtx) GetImageData(n string) (*imagedataloader.ImageData, error) { +func (mock *MockCtx) GetImageData(n string) (map[string]interface{}, 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) } diff --git a/pkg/cel/libs/context/types.go b/pkg/cel/libs/context/types.go index dae29c5267..b948206fda 100644 --- a/pkg/cel/libs/context/types.go +++ b/pkg/cel/libs/context/types.go @@ -2,23 +2,20 @@ package context import ( "github.com/google/cel-go/common/types" - "github.com/kyverno/kyverno/pkg/imageverification/imagedataloader" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiservercel "k8s.io/apiserver/pkg/cel" ) var ( - ContextType = types.NewOpaqueType("context.Context") - configMapType = BuildConfigMapType() - imageDataType = BuildImageDataType() - imageReferenceType = BuildImageReferenceType() + ContextType = types.NewOpaqueType("context.Context") + configMapType = BuildConfigMapType() + imageDataType = BuildImageDataType() ) type ContextInterface interface { GetConfigMap(string, string) (*unstructured.Unstructured, error) GetGlobalReference(string, string) (any, error) - GetImageData(string) (*imagedataloader.ImageData, error) - ParseImageReference(string) (imagedataloader.ImageReference, error) + GetImageData(string) (map[string]interface{}, error) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) } @@ -81,20 +78,6 @@ func BuildImageDataType() *apiservercel.DeclType { return apiservercel.NewObjectType("imageData", fields(f...)) } -func BuildImageReferenceType() *apiservercel.DeclType { - f := make([]*apiservercel.DeclField, 0) - f = append(f, - field("image", apiservercel.StringType, true), - field("resolvedImage", apiservercel.StringType, true), - field("registry", apiservercel.StringType, true), - field("repository", apiservercel.StringType, true), - field("tag", apiservercel.StringType, false), - field("digest", apiservercel.StringType, false), - field("identifier", apiservercel.StringType, true), - ) - return apiservercel.NewObjectType("imageReference", fields(f...)) -} - func field(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField { return apiservercel.NewDeclField(name, declType, required, nil, nil) } diff --git a/pkg/cel/libs/image/lib.go b/pkg/cel/libs/image/lib.go new file mode 100644 index 0000000000..4d3521d7c8 --- /dev/null +++ b/pkg/cel/libs/image/lib.go @@ -0,0 +1,158 @@ +package image + +import ( + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/go-containerregistry/pkg/name" +) + +func ImageLib() cel.EnvOption { + return cel.Lib(imageLib) +} + +var imageLib = &imageLibType{} + +type imageLibType struct{} + +func (*imageLibType) LibraryName() string { + return "kyverno.Image" +} + +func (*imageLibType) Types() []*cel.Type { + return []*cel.Type{ImageType} +} + +func (*imageLibType) declarations() map[string][]cel.FunctionOpt { + return map[string][]cel.FunctionOpt{ + "image": { + cel.Overload("string_to_image", []*cel.Type{cel.StringType}, ImageType, cel.UnaryBinding((stringToImage))), + }, + "isImage": { + cel.Overload("is_image_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(isImage)), + }, + "containsDigest": { + cel.MemberOverload("image_contains_digest", []*cel.Type{ImageType}, cel.BoolType, cel.UnaryBinding(imageContainsDigest)), + }, + "registry": { + cel.MemberOverload("image_registry", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageRegistry)), + }, + "repository": { + cel.MemberOverload("image_repository", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageRepository)), + }, + "identifier": { + cel.MemberOverload("image_identifier", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageIdentifier)), + }, + "tag": { + cel.MemberOverload("image_tag", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageTag)), + }, + "digest": { + cel.MemberOverload("image_digest", []*cel.Type{ImageType}, cel.StringType, cel.UnaryBinding(imageDigest)), + }, + } +} + +func (i *imageLibType) CompileOptions() []cel.EnvOption { + imageLibraryDecls := i.declarations() + options := make([]cel.EnvOption, 0, len(imageLibraryDecls)) + for name, overloads := range imageLibraryDecls { + options = append(options, cel.Function(name, overloads...)) + } + return options +} + +func (*imageLibType) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +func isImage(arg ref.Val) ref.Val { + str, ok := arg.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + + _, err := name.ParseReference(str) + if err != nil { + return types.Bool(false) + } + + return types.Bool(true) +} + +func stringToImage(arg ref.Val) ref.Val { + str, ok := arg.Value().(string) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + + v, err := name.ParseReference(str) + if err != nil { + return types.WrapErr(err) + } + + return Image{ImageReference: ConvertToImageRef(v)} +} + +func imageContainsDigest(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.Bool(len(v.Digest) != 0) +} + +func imageRegistry(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.String(v.Registry) +} + +func imageRepository(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.String(v.Repository) +} + +func imageIdentifier(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.String(v.Identifier) +} + +func imageTag(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.String(v.Tag) +} + +func imageDigest(arg ref.Val) ref.Val { + v, ok := arg.Value().(ImageReference) + if !ok { + return types.MaybeNoSuchOverloadErr(arg) + } + return types.String(v.Digest) +} + +func ConvertToImageRef(ref name.Reference) ImageReference { + var img ImageReference + img.Image = ref.String() + img.Registry = ref.Context().RegistryStr() + img.Repository = ref.Context().RepositoryStr() + img.Identifier = ref.Identifier() + + if _, ok := ref.(name.Tag); ok { + img.Tag = ref.Identifier() + } else { + img.Digest = ref.Identifier() + } + + return img +} diff --git a/pkg/cel/libs/image/lib_test.go b/pkg/cel/libs/image/lib_test.go new file mode 100644 index 0000000000..24068c26fd --- /dev/null +++ b/pkg/cel/libs/image/lib_test.go @@ -0,0 +1,226 @@ +package image_test + +import ( + "regexp" + "testing" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/go-containerregistry/pkg/name" + "github.com/kyverno/kyverno/pkg/cel/libs/image" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/util/sets" +) + +func testImageLib(t *testing.T, expr string, expectResult ref.Val, expectRuntimeErrPattern string, expectCompileErrs []string) { + env, err := cel.NewEnv( + image.ImageLib(), + ) + if err != nil { + t.Fatalf("%v", err) + } + compiled, issues := env.Compile(expr) + + if len(expectCompileErrs) > 0 { + missingCompileErrs := []string{} + matchedCompileErrs := sets.New[int]() + for _, expectedCompileErr := range expectCompileErrs { + compiledPattern, err := regexp.Compile(expectedCompileErr) + if err != nil { + t.Fatalf("failed to compile expected err regex: %v", err) + } + + didMatch := false + + for i, compileError := range issues.Errors() { + if compiledPattern.Match([]byte(compileError.Message)) { + didMatch = true + matchedCompileErrs.Insert(i) + } + } + + if !didMatch { + missingCompileErrs = append(missingCompileErrs, expectedCompileErr) + } else if len(matchedCompileErrs) != len(issues.Errors()) { + unmatchedErrs := []cel.Error{} + for i, issue := range issues.Errors() { + if !matchedCompileErrs.Has(i) { + unmatchedErrs = append(unmatchedErrs, *issue) + } + } + require.Empty(t, unmatchedErrs, "unexpected compilation errors") + } + } + + require.Empty(t, missingCompileErrs, "expected compilation errors") + return + } else if len(issues.Errors()) > 0 { + for _, err := range issues.Errors() { + t.Errorf("unexpected compile error: %v", err) + } + t.FailNow() + } + + prog, err := env.Program(compiled) + if err != nil { + t.Fatalf("%v", err) + } + res, _, err := prog.Eval(map[string]interface{}{}) + if len(expectRuntimeErrPattern) > 0 { + if err == nil { + t.Fatalf("no runtime error thrown. Expected: %v", expectRuntimeErrPattern) + } else if matched, regexErr := regexp.MatchString(expectRuntimeErrPattern, err.Error()); regexErr != nil { + t.Fatalf("failed to compile expected err regex: %v", regexErr) + } else if !matched { + t.Fatalf("unexpected err: %v", err) + } + } else if err != nil { + t.Fatalf("%v", err) + } else if expectResult != nil { + converted := res.Equal(expectResult).Value().(bool) + require.True(t, converted, "expectation not equal to output") + } else { + t.Fatal("expected result must not be nil") + } +} + +func TestImage(t *testing.T) { + trueVal := types.Bool(true) + falseVal := types.Bool(false) + + cases := []struct { + name string + expr string + expectValue ref.Val + expectedCompileErr []string + expectedRuntimeErr string + }{ + { + name: "parse", + expr: `image("registry.k8s.io/kube-apiserver-arm64:latest")`, + expectValue: image.Image{ImageReference: image.ConvertToImageRef(name.MustParseReference("registry.k8s.io/kube-apiserver-arm64:latest"))}, + }, + { + name: "parse_invalid_image", + expr: `image("registry.k8s.io/kube-apiserver-arm64:@")`, + expectedRuntimeErr: "could not parse reference: registry.k8s.io/kube-apiserver-arm64:@", + }, + { + name: "isImage", + expr: `isImage("registry.k8s.io/kube-apiserver-arm64:latest")`, + expectValue: trueVal, + }, + { + name: "isImage_false", + expr: `isImage("registry.k8s.io/kube-apiserver-arm64:@")`, + expectValue: falseVal, + }, + { + name: "isImage_noOverload", + expr: `isImage(0)`, + expectedCompileErr: []string{"found no matching overload for 'isImage' applied to.*"}, + }, + { + name: "contains_digest_no_identifier", + expr: `image("registry.k8s.io/kube-apiserver-arm64").containsDigest()`, + expectValue: falseVal, + }, + { + name: "contains_digest_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:latest").containsDigest()`, + expectValue: falseVal, + }, + { + name: "contains_digest_true", + expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").containsDigest()`, + expectValue: trueVal, + }, + { + name: "contains_digest_with_tag_true", + expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").containsDigest()`, + expectValue: trueVal, + }, + { + name: "registry", + expr: `image("registry.k8s.io/kube-apiserver-arm64").registry() == "registry.k8s.io"`, + expectValue: trueVal, + }, + { + name: "registry_matches", + expr: `image("registry.k8s.io/kube-apiserver-arm64").registry().matches("(registry.k8s.io|ghcr.io)")`, + expectValue: trueVal, + }, + { + name: "repository", + expr: `image("registry.k8s.io/kube-apiserver-arm64").repository() == "kube-apiserver-arm64"`, + expectValue: trueVal, + }, + { + name: "identifier_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").identifier()`, + expectValue: types.String("testtag"), + }, + { + name: "default_identifier", + expr: `image("registry.k8s.io/kube-apiserver-arm64").identifier()`, + expectValue: types.String("latest"), + }, + { + name: "identifer_digest", + expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").identifier()`, + expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"), + }, + { + name: "identifer_digest_and_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").identifier()`, + expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"), + }, + { + name: "tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").tag()`, + expectValue: types.String("testtag"), + }, + { + name: "default_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64").tag()`, + expectValue: types.String("latest"), + }, + { + name: "no_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").tag()`, + expectValue: types.String(""), + }, + { + name: "identifier_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").identifier()`, + expectValue: types.String("testtag"), + }, + { + name: "no_digest", + expr: `image("registry.k8s.io/kube-apiserver-arm64").digest()`, + expectValue: types.String(""), + }, + { + name: "digest_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:testtag").digest()`, + expectValue: types.String(""), + }, + { + name: "digest", + expr: `image("registry.k8s.io/kube-apiserver-arm64@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").digest()`, + expectValue: types.String("sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"), + }, + { + name: "digest_digest_and_tag", + expr: `image("registry.k8s.io/kube-apiserver-arm64:latest@sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2").digest() == "sha256:6aefddb645ee6963afd681b1845c661d0ea4c3b20ab9db86d9e753b203d385f2"`, + expectValue: trueVal, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + testImageLib(t, c.expr, c.expectValue, c.expectedRuntimeErr, c.expectedCompileErr) + }) + } +} diff --git a/pkg/cel/libs/image/types.go b/pkg/cel/libs/image/types.go new file mode 100644 index 0000000000..0853256dd5 --- /dev/null +++ b/pkg/cel/libs/image/types.go @@ -0,0 +1,60 @@ +package image + +import ( + "fmt" + "reflect" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +var ImageType = cel.ObjectType("kyverno.image") + +type ImageReference struct { + Image string `json:"image,omitempty"` + Registry string `json:"registry,omitempty"` + Repository string `json:"repository,omitempty"` + Identifier string `json:"identifier,omitempty"` + Tag string `json:"tag,omitempty"` + Digest string `json:"digest,omitempty"` +} + +type Image struct { + ImageReference +} + +func (v Image) ConvertToNative(typeDesc reflect.Type) (interface{}, error) { + if reflect.TypeOf(v.ImageReference).AssignableTo(typeDesc) { + return v.ImageReference, nil + } + if reflect.TypeOf("").AssignableTo(typeDesc) { + return v.ImageReference.Image, nil + } + return nil, fmt.Errorf("type conversion error from 'Image' to '%v'", typeDesc) +} + +func (v Image) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case ImageType: + return v + default: + return types.NewErr("type conversion error from '%s' to '%s'", ImageType, typeVal) + } +} + +func (v Image) Equal(other ref.Val) ref.Val { + img, ok := other.(Image) + if !ok { + return types.MaybeNoSuchOverloadErr(other) + } + return types.Bool(reflect.DeepEqual(v.ImageReference, img.ImageReference)) +} + +func (v Image) Type() ref.Type { + return ImageType +} + +func (v Image) Value() interface{} { + return v.ImageReference +} diff --git a/pkg/cel/policy/context.go b/pkg/cel/policy/context.go index 22fb3e716a..1906495997 100644 --- a/pkg/cel/policy/context.go +++ b/pkg/cel/policy/context.go @@ -91,9 +91,13 @@ func (cp *contextProvider) GetGlobalReference(name, projection string) (any, err } } -func (cp *contextProvider) GetImageData(image string) (*imagedataloader.ImageData, error) { +func (cp *contextProvider) GetImageData(image string) (map[string]interface{}, error) { // TODO: get image credentials from image verification policies? - return cp.imagedata.FetchImageData(context.TODO(), image) + data, err := cp.imagedata.FetchImageData(context.TODO(), image) + if err != nil { + return nil, err + } + return getValue(data.Data()) } func isLikelyKubernetesObject(data any) bool { @@ -132,10 +136,6 @@ func (cp *contextProvider) GetResource(apiVersion, resource, namespace, name str return resourceInteface.Get(context.TODO(), name, metav1.GetOptions{}) } -func (cp *contextProvider) ParseImageReference(image string) (imagedataloader.ImageReference, error) { - return imagedataloader.ParseImageReference(image) -} - func (cp *contextProvider) getResourceClient(groupVersion schema.GroupVersion, resource string, namespace string) dynamic.ResourceInterface { client := cp.dclient.Resource(groupVersion.WithResource(resource)) if namespace != "" { diff --git a/pkg/cel/policy/fake_context.go b/pkg/cel/policy/fake_context.go index ad6b7a7ebe..5cad341c3d 100644 --- a/pkg/cel/policy/fake_context.go +++ b/pkg/cel/policy/fake_context.go @@ -3,7 +3,6 @@ package policy import ( "fmt" - "github.com/kyverno/kyverno/pkg/imageverification/imagedataloader" kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -48,14 +47,10 @@ func (cp *FakeContextProvider) GetGlobalReference(string, string) (any, error) { panic("not implemented") } -func (cp *FakeContextProvider) GetImageData(string) (*imagedataloader.ImageData, error) { +func (cp *FakeContextProvider) GetImageData(string) (map[string]interface{}, error) { panic("not implemented") } -func (cp *FakeContextProvider) ParseImageReference(image string) (imagedataloader.ImageReference, error) { - return imagedataloader.ParseImageReference(image) -} - func (cp *FakeContextProvider) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) { gv, err := schema.ParseGroupVersion(apiVersion) if err != nil { diff --git a/pkg/cel/policy/utils.go b/pkg/cel/policy/utils.go new file mode 100644 index 0000000000..ea8243cc6e --- /dev/null +++ b/pkg/cel/policy/utils.go @@ -0,0 +1,16 @@ +package policy + +import "encoding/json" + +func getValue(data any) (map[string]interface{}, error) { + raw, err := json.Marshal(data) + if err != nil { + return nil, err + } + apiData := map[string]interface{}{} + err = json.Unmarshal(raw, &apiData) + if err != nil { + return nil, err + } + return apiData, nil +} diff --git a/pkg/imageverification/imagedataloader/loader.go b/pkg/imageverification/imagedataloader/loader.go index cdff814dae..67981c4c64 100644 --- a/pkg/imageverification/imagedataloader/loader.go +++ b/pkg/imageverification/imagedataloader/loader.go @@ -160,10 +160,7 @@ type ImageData struct { RemoteOpts []remote.Option NameOpts []name.Option - ImageReference `json:",inline"` - ImageIndex interface{} `json:"imageIndex,omitempty"` - Manifest *gcrv1.Manifest `json:"manifest,omitempty"` - ConfigData *gcrv1.ConfigFile `json:"config,omitempty"` + ImageDescriptor `json:",inline"` NameRef name.Reference desc *remote.Descriptor @@ -173,6 +170,13 @@ type ImageData struct { verifiedIntotoPayloads map[string][]byte } +type ImageDescriptor struct { + ImageReference `json:",inline"` + ImageIndex interface{} `json:"imageIndex,omitempty"` + Manifest *gcrv1.Manifest `json:"manifest,omitempty"` + ConfigData *gcrv1.ConfigFile `json:"config,omitempty"` +} + type referrerData struct { layerDescriptor *gcrv1.Descriptor data []byte @@ -201,6 +205,10 @@ func (i *ImageData) WithDigest(digest string) string { return i.NameRef.Context().Digest(digest).String() } +func (i *ImageData) Data() ImageDescriptor { + return i.ImageDescriptor +} + func (i *ImageData) loadReferrers() error { if i.referrersManifest != nil { return nil diff --git a/test/conformance/chainsaw/validating-policies/context/imagereference/bad-deployment.yaml b/test/conformance/chainsaw/validating-policies/context/imagereference/bad-deployment.yaml new file mode 100644 index 0000000000..ea1eb6eedf --- /dev/null +++ b/test/conformance/chainsaw/validating-policies/context/imagereference/bad-deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:latest diff --git a/test/conformance/chainsaw/validating-policies/context/imagereference/chainsaw-test.yaml b/test/conformance/chainsaw/validating-policies/context/imagereference/chainsaw-test.yaml new file mode 100755 index 0000000000..8386a1dec7 --- /dev/null +++ b/test/conformance/chainsaw/validating-policies/context/imagereference/chainsaw-test.yaml @@ -0,0 +1,24 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: image-data +spec: + steps: + - name: create policy + try: + - create: + file: policy.yaml + - sleep: + duration: 10s + - name: create deployment + try: + - create: + file: deployment.yaml + - name: create bad deployment + try: + - apply: + expect: + - check: + ($error != null): true + file: bad-deployment.yaml diff --git a/test/conformance/chainsaw/validating-policies/context/imagereference/deployment.yaml b/test/conformance/chainsaw/validating-policies/context/imagereference/deployment.yaml new file mode 100644 index 0000000000..ec70157faf --- /dev/null +++ b/test/conformance/chainsaw/validating-policies/context/imagereference/deployment.yaml @@ -0,0 +1,19 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: ghcr.io/kyverno/kyverno diff --git a/test/conformance/chainsaw/validating-policies/context/imagereference/policy.yaml b/test/conformance/chainsaw/validating-policies/context/imagereference/policy.yaml new file mode 100644 index 0000000000..25b71c57e6 --- /dev/null +++ b/test/conformance/chainsaw/validating-policies/context/imagereference/policy.yaml @@ -0,0 +1,20 @@ +apiVersion: policies.kyverno.io/v1alpha1 +kind: ValidatingPolicy +metadata: + name: check-images +spec: + matchConstraints: + resourceRules: + - apiGroups: [apps] + apiVersions: [v1] + operations: [CREATE, UPDATE] + resources: [deployments] + variables: + - name: images + expression: >- + object.spec.template.spec.containers.map(e, image(e.image)) + validations: + - expression: >- + variables.images.map(i, i.registry() == "ghcr.io" && !i.containsDigest()).all(e, e) + message: >- + Deployment must be have images from ghcr and images should be tagged