1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-22 07:41:10 +00:00

fix: image parse func and add chainsaw tests (#12396)

* fix: image parse func and add chainsaw tests

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

* fix: linter

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

---------

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
This commit is contained in:
Vishal Choudhary 2025-03-13 12:31:40 +05:30 committed by GitHub
parent 6fdbdbce28
commit d56e6037a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 598 additions and 116 deletions

View file

@ -204,7 +204,7 @@ func (e *ivengine) handleMutation(ctx context.Context, policies []CompiledImageV
} }
if p, errList := c.Compile(e.logger, ivpol.Policy); errList != nil { 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 { } else {
result, err := p.Evaluate(ctx, ictx, attr, request, namespace, true) result, err := p.Evaluate(ctx, ictx, attr, request, namespace, true)
if err != nil { if err != nil {

View file

@ -91,6 +91,10 @@ uOKpF5rWAruB5PCIrquamOejpXV9aQA/K2JQDuc0mcKz
}, },
}, },
Verifications: []admissionregistrationv1.Validation{ 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)", Expression: "images.containers.map(image, verifyImageSignatures(image, [attestors.notary])).all(e, e > 0)",
Message: "failed to verify image with notary cert", Message: "failed to verify image with notary cert",

View file

@ -3,6 +3,7 @@ package cel
import ( import (
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/ext" "github.com/google/cel-go/ext"
"github.com/kyverno/kyverno/pkg/cel/libs/image"
"k8s.io/apiserver/pkg/cel/library" "k8s.io/apiserver/pkg/cel/library"
) )
@ -30,5 +31,6 @@ func NewEnv() (*cel.Env, error) {
library.Lists(), library.Lists(),
library.Regex(), library.Regex(),
library.URLs(), library.URLs(),
image.ImageLib(),
) )
} }

View file

@ -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 { func (c *impl) list_resources_string_string_string(args ...ref.Val) ref.Val {
if self, err := utils.ConvertToNative[Context](args[0]); err != nil { if self, err := utils.ConvertToNative[Context](args[0]); err != nil {
return types.WrapErr(err) return types.WrapErr(err)

View file

@ -2,6 +2,7 @@ package context
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"strings" "strings"
"testing" "testing"
@ -146,7 +147,7 @@ func Test_impl_get_imagedata_string(t *testing.T) {
env, err := base.Extend(options...) env, err := base.Extend(options...)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, env) 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.Nil(t, issues)
assert.NotNil(t, ast) assert.NotNil(t, ast)
prog, err := env.Program(ast) prog, err := env.Program(ast)
@ -154,54 +155,30 @@ func Test_impl_get_imagedata_string(t *testing.T) {
assert.NotNil(t, prog) assert.NotNil(t, prog)
data := map[string]any{ data := map[string]any{
"context": Context{&MockCtx{ "context": Context{&MockCtx{
GetImageDataFunc: func(image string) (*imagedataloader.ImageData, error) { GetImageDataFunc: func(image string) (map[string]interface{}, error) {
idl, err := imagedataloader.New(nil) idl, err := imagedataloader.New(nil)
assert.NoError(t, err) 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) out, _, err := prog.Eval(data)
assert.NoError(t, err) assert.NoError(t, err)
img := out.Value().(*imagedataloader.ImageData) resolvedImg := out.Value().(string)
assert.Equal(t, img.Tag, "latest") assert.True(t, strings.HasPrefix(resolvedImg, "ghcr.io/kyverno/kyverno:latest@sha256:"))
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")
} }
func Test_impl_get_resource_string_string_string_string(t *testing.T) { func Test_impl_get_resource_string_string_string_string(t *testing.T) {

View file

@ -67,18 +67,10 @@ func (c *lib) extendEnv(env *cel.Env) (*cel.Env, error) {
cel.MemberOverload( cel.MemberOverload(
"get_imagedata_string", "get_imagedata_string",
[]*cel.Type{ContextType, types.StringType}, []*cel.Type{ContextType, types.StringType},
imageDataType.CelType(), types.DynType,
cel.BinaryBinding(impl.get_imagedata_string), 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": { "ListResources": {
// TODO: should not use DynType in return // TODO: should not use DynType in return
cel.MemberOverload( cel.MemberOverload(

View file

@ -2,18 +2,16 @@ package context
import ( import (
"github.com/kyverno/kyverno/pkg/globalcontext/store" "github.com/kyverno/kyverno/pkg/globalcontext/store"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
) )
// MOCK FOR TESTING // MOCK FOR TESTING
type MockCtx struct { type MockCtx struct {
GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error) GetConfigMapFunc func(string, string) (*unstructured.Unstructured, error)
GetGlobalReferenceFunc func(string, string) (any, error) GetGlobalReferenceFunc func(string, string) (any, error)
GetImageDataFunc func(string) (*imagedataloader.ImageData, error) GetImageDataFunc func(string) (map[string]interface{}, error)
ParseImageReferenceFunc func(string) (imagedataloader.ImageReference, error) ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error)
ListResourcesFunc func(string, string, string) (*unstructured.UnstructuredList, error) GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error)
GetResourceFunc func(string, string, string, string) (*unstructured.Unstructured, error)
} }
func (mock *MockCtx) GetConfigMap(ns string, n 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) 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) 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) { func (mock *MockCtx) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
return mock.ListResourcesFunc(apiVersion, resource, namespace) return mock.ListResourcesFunc(apiVersion, resource, namespace)
} }

View file

@ -2,23 +2,20 @@ package context
import ( import (
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apiservercel "k8s.io/apiserver/pkg/cel" apiservercel "k8s.io/apiserver/pkg/cel"
) )
var ( var (
ContextType = types.NewOpaqueType("context.Context") ContextType = types.NewOpaqueType("context.Context")
configMapType = BuildConfigMapType() configMapType = BuildConfigMapType()
imageDataType = BuildImageDataType() imageDataType = BuildImageDataType()
imageReferenceType = BuildImageReferenceType()
) )
type ContextInterface interface { type ContextInterface interface {
GetConfigMap(string, string) (*unstructured.Unstructured, error) GetConfigMap(string, string) (*unstructured.Unstructured, error)
GetGlobalReference(string, string) (any, error) GetGlobalReference(string, string) (any, error)
GetImageData(string) (*imagedataloader.ImageData, error) GetImageData(string) (map[string]interface{}, error)
ParseImageReference(string) (imagedataloader.ImageReference, error)
ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error)
GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error) GetResource(apiVersion, resource, namespace, name string) (*unstructured.Unstructured, error)
} }
@ -81,20 +78,6 @@ func BuildImageDataType() *apiservercel.DeclType {
return apiservercel.NewObjectType("imageData", fields(f...)) 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 { func field(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil) return apiservercel.NewDeclField(name, declType, required, nil, nil)
} }

158
pkg/cel/libs/image/lib.go Normal file
View file

@ -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
}

View file

@ -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)
})
}
}

View file

@ -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
}

View file

@ -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? // 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 { 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{}) 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 { func (cp *contextProvider) getResourceClient(groupVersion schema.GroupVersion, resource string, namespace string) dynamic.ResourceInterface {
client := cp.dclient.Resource(groupVersion.WithResource(resource)) client := cp.dclient.Resource(groupVersion.WithResource(resource))
if namespace != "" { if namespace != "" {

View file

@ -3,7 +3,6 @@ package policy
import ( import (
"fmt" "fmt"
"github.com/kyverno/kyverno/pkg/imageverification/imagedataloader"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
@ -48,14 +47,10 @@ func (cp *FakeContextProvider) GetGlobalReference(string, string) (any, error) {
panic("not implemented") panic("not implemented")
} }
func (cp *FakeContextProvider) GetImageData(string) (*imagedataloader.ImageData, error) { func (cp *FakeContextProvider) GetImageData(string) (map[string]interface{}, error) {
panic("not implemented") 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) { func (cp *FakeContextProvider) ListResources(apiVersion, resource, namespace string) (*unstructured.UnstructuredList, error) {
gv, err := schema.ParseGroupVersion(apiVersion) gv, err := schema.ParseGroupVersion(apiVersion)
if err != nil { if err != nil {

16
pkg/cel/policy/utils.go Normal file
View file

@ -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
}

View file

@ -160,10 +160,7 @@ type ImageData struct {
RemoteOpts []remote.Option RemoteOpts []remote.Option
NameOpts []name.Option NameOpts []name.Option
ImageReference `json:",inline"` ImageDescriptor `json:",inline"`
ImageIndex interface{} `json:"imageIndex,omitempty"`
Manifest *gcrv1.Manifest `json:"manifest,omitempty"`
ConfigData *gcrv1.ConfigFile `json:"config,omitempty"`
NameRef name.Reference NameRef name.Reference
desc *remote.Descriptor desc *remote.Descriptor
@ -173,6 +170,13 @@ type ImageData struct {
verifiedIntotoPayloads map[string][]byte 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 { type referrerData struct {
layerDescriptor *gcrv1.Descriptor layerDescriptor *gcrv1.Descriptor
data []byte data []byte
@ -201,6 +205,10 @@ func (i *ImageData) WithDigest(digest string) string {
return i.NameRef.Context().Digest(digest).String() return i.NameRef.Context().Digest(digest).String()
} }
func (i *ImageData) Data() ImageDescriptor {
return i.ImageDescriptor
}
func (i *ImageData) loadReferrers() error { func (i *ImageData) loadReferrers() error {
if i.referrersManifest != nil { if i.referrersManifest != nil {
return nil return nil

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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