From b89edafdba30668201abebe088096ceb479db4ab Mon Sep 17 00:00:00 2001 From: Pratik Shah Date: Tue, 8 Nov 2022 19:28:06 +0530 Subject: [PATCH] Fixed issue-3709: Image verify rule gives error for non-existing configmap (#19) Signed-off-by: Pratik Shah --- pkg/engine/imageVerify.go | 54 +++++++++++++++++- pkg/engine/imageVerifyValidate.go | 7 +++ pkg/engine/imageVerify_test.go | 95 +++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 1 deletion(-) diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index aa747b77f966..d1a2c3d040a9 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "reflect" + "regexp" "strings" "time" @@ -18,6 +19,7 @@ import ( "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/registryclient" + "github.com/kyverno/kyverno/pkg/utils/api" apiutils "github.com/kyverno/kyverno/pkg/utils/api" "github.com/kyverno/kyverno/pkg/utils/wildcard" "github.com/pkg/errors" @@ -25,6 +27,46 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) +func requiredValidation(policyContext *PolicyContext, rule *kyvernov1.Rule) (bool, error) { + imageInfo := &api.ImageInfo{} + var runVerification = true + images := policyContext.JSONContext.ImageInfo() + imageData, err := json.Marshal(images["containers"]["test"]) + if err != nil { + return false, err + } + err = json.Unmarshal(imageData, &imageInfo) + if err != nil { + return false, err + } + // if registry is empty, it means policy rule has only "*" in image reference + // hence allow running verification + if imageInfo.Registry == "" { + return runVerification, nil + } + imageName := fmt.Sprintf("%s/%s:%s", imageInfo.Registry, imageInfo.Name, imageInfo.Tag) + for _, verifyImage := range rule.VerifyImages { + runVerification = true + if len(verifyImage.ImageReferences) > 0 { + for _, imageRef := range verifyImage.ImageReferences { + regex := regexp.MustCompile(imageRef) + groups := regex.FindAllStringSubmatch(imageName, -1) + if len(groups) == 0 { + runVerification = false + } + } + } + if verifyImage.Image != "" { + regex := regexp.MustCompile(verifyImage.Image) + groups := regex.FindAllStringSubmatch(imageName, -1) + if len(groups) == 0 { + runVerification = false + } + } + } + return runVerification, nil +} + func VerifyAndPatchImages(policyContext *PolicyContext) (*response.EngineResponse, *ImageVerificationMetadata) { resp := &response.EngineResponse{} images := policyContext.JSONContext.ImageInfo() @@ -66,6 +108,17 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (*response.EngineRespons logger.V(3).Info("processing image verification rule", "ruleSelector", applyRules) + var err error + runVerification, err := requiredValidation(policyContext, rule) + if err != nil { + appendError(resp, rule, fmt.Sprintf("failed to load image info: %s", err.Error()), response.RuleStatusError) + continue + } + if !runVerification { + logger.V(3).Info("skipping image verification rule", "ruleSelector", applyRules) + continue + } + policyContext.JSONContext.Restore() if err := LoadContext(logger, rule.Context, policyContext, rule.Name); err != nil { appendError(resp, rule, fmt.Sprintf("failed to load context: %s", err.Error()), response.RuleStatusError) @@ -73,7 +126,6 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (*response.EngineRespons } ruleImages := images - var err error if rule.ImageExtractors != nil { if ruleImages, err = policyContext.JSONContext.GenerateCustomImageInfo(&policyContext.NewResource, rule.ImageExtractors); err != nil { appendError(resp, rule, fmt.Sprintf("failed to extract images: %s", err.Error()), response.RuleStatusError) diff --git a/pkg/engine/imageVerifyValidate.go b/pkg/engine/imageVerifyValidate.go index e75bc0162761..e5ee64ad7c33 100644 --- a/pkg/engine/imageVerifyValidate.go +++ b/pkg/engine/imageVerifyValidate.go @@ -19,6 +19,13 @@ func processImageValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyver } log = log.WithValues("rule", rule.Name) + runValidation, err := requiredValidation(ctx, rule) + if err != nil { + log.Error(err, fmt.Sprintf("failed to load image info: %s", err.Error())) + } + if !runValidation { + return ruleResponse(*rule, response.Validation, "image verified", response.RuleStatusPass, nil) + } if err := LoadContext(log, rule.Context, ctx, rule.Name); err != nil { if _, ok := err.(gojmespath.NotFoundError); ok { log.V(3).Info("failed to load context", "reason", err.Error()) diff --git a/pkg/engine/imageVerify_test.go b/pkg/engine/imageVerify_test.go index a7d0e6449aaf..94aa7eeec87f 100644 --- a/pkg/engine/imageVerify_test.go +++ b/pkg/engine/imageVerify_test.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" kyverno "github.com/kyverno/kyverno/api/kyverno/v1" + client "github.com/kyverno/kyverno/pkg/clients/dclient" "github.com/kyverno/kyverno/pkg/cosign" "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/response" @@ -305,6 +306,63 @@ var testSampleMultipleKeyPolicy = ` } ` +var testConfigMapMissing = `{ + "apiVersion": "kyverno.io/v1", + "kind": "ClusterPolicy", + "metadata": { + "annotations": { + "pod-policies.kyverno.io/autogen-controllers": "none" + }, + "name": "image-verify-polset" + }, + "spec": { + "background": false, + "failurePolicy": "Fail", + "rules": [ + { + "context": [ + { + "configMap": { + "name": "myconfigmap", + "namespace": "somens" + }, + "name": "myconfigmap" + } + ], + "match": { + "resources": { + "kinds": [ + "Pod" + ] + } + }, + "name": "image-verify-pol1", + "verifyImages": [ + { + "imageReferences": [ + "ghcr.io/*" + ], + "attestors": [ + { + "count": 1, + "entries": [ + { + "keys": { + "publicKeys": "{{myconfigmap.data.configmapkey}}" + } + } + ] + } + ] + } + ] + } + ], + "validationFailureAction": "audit", + "webhookTimeoutSeconds": 30 + } +}` + var testSampleResource = `{ "apiVersion": "v1", "kind": "Pod", @@ -319,9 +377,46 @@ var testSampleResource = `{ } }` +var testConfigMapMissingResource = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "labels": { + "run": "test" + }, + "name": "test" + }, + "spec": { + "containers": [ + { + "image": "nginx:latest", + "name": "test", + "resources": {} + } + ] + } +}` + var testVerifyImageKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8nXRh950IZbRj8Ra/N9sbqOPZrfM5/KAQN0/KjHcorm/J5yctVd7iEcnessRQjU917hmKO6JWVGHpDguIyakZA==\n-----END PUBLIC KEY-----\n` var testOtherKey = `-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpNlOGZ323zMlhs4bcKSpAKQvbcWi5ZLRmijm6SqXDy0Fp0z0Eal+BekFnLzs8rUXUaXlhZ3hNudlgFJH+nFNMw==\n-----END PUBLIC KEY-----\n` +func Test_ConfigMapMissingSuccess(t *testing.T) { + policyContext := buildContext(t, testConfigMapMissing, testConfigMapMissingResource, "") + cosign.ClearMock() + err, _ := VerifyAndPatchImages(policyContext) + assert.Equal(t, len(err.PolicyResponse.Rules), 0) +} + +func Test_ConfigMapMissingFailure(t *testing.T) { + ghcrImage := strings.Replace(testConfigMapMissingResource, "nginx:latest", "ghcr.io/kyverno/test-verify-image:signed", -1) + policyContext := buildContext(t, testConfigMapMissing, ghcrImage, "") + policyContext.Client = client.NewEmptyFakeClient() + cosign.ClearMock() + err, _ := VerifyAndPatchImages(policyContext) + assert.Equal(t, len(err.PolicyResponse.Rules), 1) + assert.Equal(t, err.PolicyResponse.Rules[0].Status, response.RuleStatusError, err.PolicyResponse.Rules[0].Message) +} + func Test_SignatureGoodSigned(t *testing.T) { policyContext := buildContext(t, testSampleSingleKeyPolicy, testSampleResource, "") cosign.ClearMock()