From 76e306dbc27c8ee20ae722c190b6e980f1ea8bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Tue, 7 Feb 2023 17:51:25 +0100 Subject: [PATCH] refactor: use more engine internals (#6247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: use more engine internals Signed-off-by: Charles-Edouard Brétéché * imageverifier Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché * rule skip and exceptions fix Signed-off-by: Charles-Edouard Brétéché * fix Signed-off-by: Charles-Edouard Brétéché --------- Signed-off-by: Charles-Edouard Brétéché --- api/kyverno/v1/image_verification_types.go | 7 + pkg/engine/attestation_test.go | 3 +- pkg/engine/background.go | 22 +- pkg/engine/exceptions.go | 8 +- pkg/engine/imageVerify.go | 628 ++------------------- pkg/engine/imageVerifyValidate.go | 6 +- pkg/engine/imageVerify_test.go | 22 +- pkg/engine/internal/imageverifier.go | 540 ++++++++++++++++++ pkg/engine/internal/preconditions.go | 35 ++ pkg/engine/internal/response.go | 18 +- pkg/engine/k8smanifest.go | 4 +- pkg/engine/mutation.go | 19 +- pkg/engine/utils.go | 28 - pkg/engine/validation.go | 89 +-- pkg/engine/validation_test.go | 2 +- 15 files changed, 705 insertions(+), 726 deletions(-) create mode 100644 pkg/engine/internal/imageverifier.go create mode 100644 pkg/engine/internal/preconditions.go diff --git a/api/kyverno/v1/image_verification_types.go b/api/kyverno/v1/image_verification_types.go index 1379d762c4..c5f7683ab7 100644 --- a/api/kyverno/v1/image_verification_types.go +++ b/api/kyverno/v1/image_verification_types.go @@ -96,6 +96,13 @@ type AttestorSet struct { Entries []Attestor `json:"entries,omitempty" yaml:"entries,omitempty"` } +func (as AttestorSet) RequiredCount() int { + if as.Count == nil || *as.Count == 0 { + return len(as.Entries) + } + return *as.Count +} + type Attestor struct { // Keys specifies one or more public keys // +kubebuilder:validation:Optional diff --git a/pkg/engine/attestation_test.go b/pkg/engine/attestation_test.go index 2c716d48f2..443e0bb1c8 100644 --- a/pkg/engine/attestation_test.go +++ b/pkg/engine/attestation_test.go @@ -6,6 +6,7 @@ import ( v1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/engine/context" + "github.com/kyverno/kyverno/pkg/engine/internal" "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/utils/api" "github.com/kyverno/kyverno/pkg/utils/image" @@ -257,7 +258,7 @@ func Test_Conditions(t *testing.T) { err := json.Unmarshal([]byte(scanPredicate), &dataMap) assert.NilError(t, err) - pass, err := evaluateConditions(conditions, ctx, dataMap, logging.GlobalLogger()) + pass, err := internal.EvaluateConditions(conditions, ctx, dataMap, logging.GlobalLogger()) assert.NilError(t, err) assert.Equal(t, pass, true) } diff --git a/pkg/engine/background.go b/pkg/engine/background.go index 19c5dc5888..7b40ba879a 100644 --- a/pkg/engine/background.go +++ b/pkg/engine/background.go @@ -86,17 +86,17 @@ func (e *engine) filterRule( kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...) subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(e.client, kindsInPolicy, policyContext) - // check if there is a corresponding policy exception - ruleResp := hasPolicyExceptions(logger, e.exceptionSelector, policyContext, &rule, subresourceGVKToAPIResource, e.configuration) - if ruleResp != nil { - return ruleResp - } - ruleType := engineapi.Mutation if rule.HasGenerate() { ruleType = engineapi.Generation } + // check if there is a corresponding policy exception + ruleResp := hasPolicyExceptions(logger, ruleType, e.exceptionSelector, policyContext, &rule, subresourceGVKToAPIResource, e.configuration) + if ruleResp != nil { + return ruleResp + } + startTime := time.Now() policy := policyContext.Policy() @@ -155,15 +155,7 @@ func (e *engine) filterRule( // evaluate pre-conditions if !variables.EvaluateConditions(logger, ctx, copyConditions) { logger.V(4).Info("skip rule as preconditions are not met", "rule", ruleCopy.Name) - return &engineapi.RuleResponse{ - Name: ruleCopy.Name, - Type: ruleType, - Status: engineapi.RuleStatusSkip, - ExecutionStats: engineapi.ExecutionStats{ - ProcessingTime: time.Since(startTime), - Timestamp: startTime.Unix(), - }, - } + return internal.RuleSkip(ruleCopy, ruleType, "") } // build rule Response diff --git a/pkg/engine/exceptions.go b/pkg/engine/exceptions.go index 51e1aa11ea..719a5558b8 100644 --- a/pkg/engine/exceptions.go +++ b/pkg/engine/exceptions.go @@ -8,6 +8,7 @@ import ( kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" "github.com/kyverno/kyverno/pkg/config" engineapi "github.com/kyverno/kyverno/pkg/engine/api" + "github.com/kyverno/kyverno/pkg/engine/internal" matched "github.com/kyverno/kyverno/pkg/utils/match" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -73,6 +74,7 @@ func matchesException( // A rule response is returned when an exception is matched, or there is an error. func hasPolicyExceptions( log logr.Logger, + ruleType engineapi.RuleType, selector engineapi.PolicyExceptionSelector, ctx engineapi.PolicyContext, rule *kyvernov1.Rule, @@ -93,11 +95,7 @@ func hasPolicyExceptions( } } log.V(3).Info("policy rule skipped due to policy exception", "exception", key) - return &engineapi.RuleResponse{ - Name: rule.Name, - Message: "rule skipped due to policy exception " + key, - Status: engineapi.RuleStatusSkip, - } + return internal.RuleSkip(rule, ruleType, "rule skipped due to policy exception "+key) } return nil } diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index c940134116..4f776e4819 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -2,19 +2,13 @@ package engine import ( "context" - "encoding/json" - "errors" "fmt" - "net" - "reflect" "strings" "time" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" "github.com/kyverno/kyverno/pkg/autogen" - "github.com/kyverno/kyverno/pkg/config" - "github.com/kyverno/kyverno/pkg/cosign" engineapi "github.com/kyverno/kyverno/pkg/engine/api" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/internal" @@ -23,11 +17,8 @@ import ( "github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/tracing" apiutils "github.com/kyverno/kyverno/pkg/utils/api" - "github.com/kyverno/kyverno/pkg/utils/jsonpointer" "github.com/kyverno/kyverno/pkg/utils/wildcard" "go.opentelemetry.io/otel/trace" - "go.uber.org/multierr" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func (e *engine) verifyAndPatchImages( @@ -68,7 +59,7 @@ func (e *engine) verifyAndPatchImages( if len(rule.VerifyImages) == 0 { return } - + startTime := time.Now() kindsInPolicy := append(rule.MatchResources.GetKinds(), rule.ExcludeResources.GetKinds()...) subresourceGVKToAPIResource := GetSubresourceGVKToAPIResourceMap(e.client, kindsInPolicy, policyContext) @@ -77,7 +68,7 @@ func (e *engine) verifyAndPatchImages( } // check if there is a corresponding policy exception - ruleResp := hasPolicyExceptions(logger, e.exceptionSelector, policyContext, rule, subresourceGVKToAPIResource, e.configuration) + ruleResp := hasPolicyExceptions(logger, engineapi.ImageVerify, e.exceptionSelector, policyContext, rule, subresourceGVKToAPIResource, e.configuration) if ruleResp != nil { resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) return @@ -87,42 +78,54 @@ func (e *engine) verifyAndPatchImages( ruleImages, imageRefs, err := e.extractMatchingImages(policyContext, rule) if err != nil { - appendResponse(resp, rule, fmt.Sprintf("failed to extract images: %s", err.Error()), engineapi.RuleStatusError) - return - } - if len(ruleImages) == 0 { - appendResponse( - resp, - rule, - fmt.Sprintf("skip run verification as image in resource not found in imageRefs '%s'", imageRefs), - engineapi.RuleStatusSkip, + internal.AddRuleResponse( + &resp.PolicyResponse, + internal.RuleError(rule, engineapi.ImageVerify, "failed to extract images", err), + startTime, + ) + return + } + if len(ruleImages) == 0 { + internal.AddRuleResponse( + &resp.PolicyResponse, + internal.RuleSkip( + rule, + engineapi.ImageVerify, + fmt.Sprintf("skip run verification as image in resource not found in imageRefs '%s'", imageRefs), + ), + startTime, ) return } - policyContext.JSONContext().Restore() if err := internal.LoadContext(ctx, e.contextLoader, rule.Context, policyContext, rule.Name); err != nil { - appendResponse(resp, rule, fmt.Sprintf("failed to load context: %s", err.Error()), engineapi.RuleStatusError) + internal.AddRuleResponse( + &resp.PolicyResponse, + internal.RuleError(rule, engineapi.ImageVerify, "failed to load context", err), + startTime, + ) return } - ruleCopy, err := substituteVariables(rule, policyContext.JSONContext(), logger) if err != nil { - appendResponse(resp, rule, fmt.Sprintf("failed to substitute variables: %s", err.Error()), engineapi.RuleStatusError) + internal.AddRuleResponse( + &resp.PolicyResponse, + internal.RuleError(rule, engineapi.ImageVerify, "failed to substitute variables", err), + startTime, + ) return } - - iv := &imageVerifier{ - logger: logger, - rclient: rclient, - policyContext: policyContext, - rule: ruleCopy, - resp: resp, - ivm: ivm, - } - + iv := internal.NewImageVerifier( + logger, + rclient, + policyContext, + ruleCopy, + ivm, + ) for _, imageVerify := range ruleCopy.VerifyImages { - iv.verify(ctx, imageVerify, ruleImages, e.configuration) + for _, r := range iv.Verify(ctx, imageVerify, ruleImages, e.configuration) { + internal.AddRuleResponse(&resp.PolicyResponse, r, startTime) + } } }, ) @@ -153,6 +156,16 @@ func getMatchingImages(images map[string]map[string]apiutils.ImageInfo, rule *ky return imageInfos, strings.Join(imageRefs, ",") } +func imageMatches(image string, imagePatterns []string) bool { + for _, imagePattern := range imagePatterns { + if wildcard.Match(imagePattern, image) { + return true + } + } + + return false +} + func (e *engine) extractMatchingImages(policyContext engineapi.PolicyContext, rule *kyvernov1.Rule) ([]apiutils.ImageInfo, string, error) { var ( images map[string]map[string]apiutils.ImageInfo @@ -172,12 +185,6 @@ func (e *engine) extractMatchingImages(policyContext engineapi.PolicyContext, ru return matchingImages, imageRefs, nil } -func appendResponse(resp *engineapi.EngineResponse, rule *kyvernov1.Rule, msg string, status engineapi.RuleStatus) { - rr := internal.RuleResponse(*rule, engineapi.ImageVerify, msg, status) - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *rr) - incrementErrorCount(resp) -} - func substituteVariables(rule *kyvernov1.Rule, ctx enginecontext.EvalInterface, logger logr.Logger) (*kyvernov1.Rule, error) { // remove attestations as variables are not substituted in them ruleCopy := *rule.DeepCopy() @@ -198,542 +205,3 @@ func substituteVariables(rule *kyvernov1.Rule, ctx enginecontext.EvalInterface, return &ruleCopy, nil } - -type imageVerifier struct { - logger logr.Logger - rclient registryclient.Client - policyContext engineapi.PolicyContext - rule *kyvernov1.Rule - resp *engineapi.EngineResponse - ivm *engineapi.ImageVerificationMetadata -} - -// verify applies policy rules to each matching image. The policy rule results and annotation patches are -// added to tme imageVerifier `resp` and `ivm` fields. -func (iv *imageVerifier) verify(ctx context.Context, imageVerify kyvernov1.ImageVerification, matchedImageInfos []apiutils.ImageInfo, cfg config.Configuration) { - // for backward compatibility - imageVerify = *imageVerify.Convert() - - for _, imageInfo := range matchedImageInfos { - image := imageInfo.String() - - if hasImageVerifiedAnnotationChanged(iv.policyContext, iv.logger) { - msg := engineapi.ImageVerifyAnnotationKey + " annotation cannot be changed" - iv.logger.Info("image verification error", "reason", msg) - ruleResp := internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail) - iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp) - incrementAppliedCount(iv.resp) - continue - } - - pointer := jsonpointer.ParsePath(imageInfo.Pointer).JMESPath() - changed, err := iv.policyContext.JSONContext().HasChanged(pointer) - if err == nil && !changed { - iv.logger.V(4).Info("no change in image, skipping check", "image", image) - continue - } - - verified, err := isImageVerified(iv.policyContext.NewResource(), image, iv.logger) - if err == nil && verified { - iv.logger.Info("image was previously verified, skipping check", "image", image) - continue - } - - ruleResp, digest := iv.verifyImage(ctx, imageVerify, imageInfo, cfg) - - if imageVerify.MutateDigest { - patch, retrievedDigest, err := iv.handleMutateDigest(ctx, digest, imageInfo) - if err != nil { - ruleResp = internal.RuleError(iv.rule, engineapi.ImageVerify, "failed to update digest", err) - } else if patch != nil { - if ruleResp == nil { - ruleResp = internal.RuleResponse(*iv.rule, engineapi.ImageVerify, "mutated image digest", engineapi.RuleStatusPass) - } - - ruleResp.Patches = append(ruleResp.Patches, patch) - imageInfo.Digest = retrievedDigest - image = imageInfo.String() - } - } - - if ruleResp != nil { - if len(imageVerify.Attestors) > 0 || len(imageVerify.Attestations) > 0 { - verified := ruleResp.Status == engineapi.RuleStatusPass - iv.ivm.Add(image, verified) - } - - iv.resp.PolicyResponse.Rules = append(iv.resp.PolicyResponse.Rules, *ruleResp) - incrementAppliedCount(iv.resp) - } - } -} - -func (iv *imageVerifier) handleMutateDigest(ctx context.Context, digest string, imageInfo apiutils.ImageInfo) ([]byte, string, error) { - if imageInfo.Digest != "" { - return nil, "", nil - } - - if digest == "" { - desc, err := iv.rclient.FetchImageDescriptor(ctx, imageInfo.String()) - if err != nil { - return nil, "", err - } - digest = desc.Digest.String() - } - - patch, err := makeAddDigestPatch(imageInfo, digest) - if err != nil { - return nil, "", fmt.Errorf("failed to create image digest patch: %w", err) - } - - iv.logger.V(4).Info("adding digest patch", "image", imageInfo.String(), "patch", string(patch)) - - return patch, digest, nil -} - -func hasImageVerifiedAnnotationChanged(ctx engineapi.PolicyContext, log logr.Logger) bool { - newResource := ctx.NewResource() - oldResource := ctx.OldResource() - if reflect.DeepEqual(newResource, unstructured.Unstructured{}) || - reflect.DeepEqual(oldResource, unstructured.Unstructured{}) { - return false - } - - key := engineapi.ImageVerifyAnnotationKey - newValue := newResource.GetAnnotations()[key] - oldValue := oldResource.GetAnnotations()[key] - result := newValue != oldValue - if result { - log.V(2).Info("annotation mismatch", "oldValue", oldValue, "newValue", newValue, "key", key) - } - - return result -} - -func imageMatches(image string, imagePatterns []string) bool { - for _, imagePattern := range imagePatterns { - if wildcard.Match(imagePattern, image) { - return true - } - } - - return false -} - -func (iv *imageVerifier) verifyImage( - ctx context.Context, - imageVerify kyvernov1.ImageVerification, - imageInfo apiutils.ImageInfo, - cfg config.Configuration, -) (*engineapi.RuleResponse, string) { - if len(imageVerify.Attestors) <= 0 && len(imageVerify.Attestations) <= 0 { - return nil, "" - } - - image := imageInfo.String() - iv.logger.V(2).Info("verifying image signatures", "image", image, - "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) - - if err := iv.policyContext.JSONContext().AddImageInfo(imageInfo, cfg); err != nil { - iv.logger.Error(err, "failed to add image to context") - msg := fmt.Sprintf("failed to add image to context %s: %s", image, err.Error()) - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusError), "" - } - - if len(imageVerify.Attestors) > 0 { - if !matchImageReferences(imageVerify.ImageReferences, imageInfo.String()) { - return nil, "" - } - - ruleResp, cosignResp := iv.verifyAttestors(ctx, imageVerify.Attestors, imageVerify, imageInfo, "") - if ruleResp.Status != engineapi.RuleStatusPass { - return ruleResp, "" - } - - if len(imageVerify.Attestations) == 0 { - return ruleResp, cosignResp.Digest - } - - if imageInfo.Digest == "" { - imageInfo.Digest = cosignResp.Digest - } - - if len(imageVerify.Attestations) == 0 { - return ruleResp, cosignResp.Digest - } - - if imageInfo.Digest == "" { - imageInfo.Digest = cosignResp.Digest - } - } - - return iv.verifyAttestations(ctx, imageVerify, imageInfo) -} - -func (iv *imageVerifier) verifyAttestors( - ctx context.Context, - attestors []kyvernov1.AttestorSet, - imageVerify kyvernov1.ImageVerification, - imageInfo apiutils.ImageInfo, - predicateType string, -) (*engineapi.RuleResponse, *cosign.Response) { - var cosignResponse *cosign.Response - image := imageInfo.String() - - for i, attestorSet := range attestors { - var err error - path := fmt.Sprintf(".attestors[%d]", i) - iv.logger.V(4).Info("verifying attestors", "path", path) - cosignResponse, err = iv.verifyAttestorSet(ctx, attestorSet, imageVerify, imageInfo, path) - if err != nil { - iv.logger.Error(err, "failed to verify image") - return iv.handleRegistryErrors(image, err), nil - } - } - - if cosignResponse == nil { - return internal.RuleError(iv.rule, engineapi.ImageVerify, "invalid response", fmt.Errorf("nil")), nil - } - - msg := fmt.Sprintf("verified image signatures for %s", image) - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusPass), cosignResponse -} - -// handle registry network errors as a rule error (instead of a policy failure) -func (iv *imageVerifier) handleRegistryErrors(image string, err error) *engineapi.RuleResponse { - msg := fmt.Sprintf("failed to verify image %s: %s", image, err.Error()) - var netErr *net.OpError - if errors.As(err, &netErr) { - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusError) - } - - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail) -} - -func (iv *imageVerifier) verifyAttestations( - ctx context.Context, - imageVerify kyvernov1.ImageVerification, - imageInfo apiutils.ImageInfo, -) (*engineapi.RuleResponse, string) { - image := imageInfo.String() - for i, attestation := range imageVerify.Attestations { - var attestationError error - path := fmt.Sprintf(".attestations[%d]", i) - - if attestation.PredicateType == "" { - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, path+": missing predicateType", engineapi.RuleStatusFail), "" - } - - if len(attestation.Attestors) == 0 { - // add an empty attestor to allow fetching and checking attestations - attestation.Attestors = []kyvernov1.AttestorSet{{Entries: []kyvernov1.Attestor{{}}}} - } - - for j, attestor := range attestation.Attestors { - attestorPath := fmt.Sprintf("%s.attestors[%d]", path, j) - requiredCount := getRequiredCount(attestor) - verifiedCount := 0 - - for _, a := range attestor.Entries { - entryPath := fmt.Sprintf("%s.entries[%d]", attestorPath, i) - opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image, &imageVerify.Attestations[i]) - cosignResp, err := cosign.FetchAttestations(ctx, iv.rclient, *opts) - if err != nil { - iv.logger.Error(err, "failed to fetch attestations") - return iv.handleRegistryErrors(image, err), "" - } - - if imageInfo.Digest == "" { - imageInfo.Digest = cosignResp.Digest - image = imageInfo.String() - } - - attestationError = iv.verifyAttestation(cosignResp.Statements, attestation, imageInfo) - if attestationError != nil { - attestationError = fmt.Errorf("%s: %w", entryPath+subPath, attestationError) - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, attestationError.Error(), engineapi.RuleStatusFail), "" - } - - verifiedCount++ - if verifiedCount >= requiredCount { - iv.logger.V(2).Info("image attestations verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount) - break - } - } - - if verifiedCount < requiredCount { - msg := fmt.Sprintf("image attestations verification failed, verifiedCount: %v, requiredCount: %v", verifiedCount, requiredCount) - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail), "" - } - } - - iv.logger.V(4).Info("attestation checks passed", "path", path, "image", imageInfo.String(), "predicateType", attestation.PredicateType) - } - - msg := fmt.Sprintf("verified image attestations for %s", image) - iv.logger.V(2).Info(msg) - return internal.RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusPass), imageInfo.Digest -} - -func (iv *imageVerifier) verifyAttestorSet( - ctx context.Context, - attestorSet kyvernov1.AttestorSet, - imageVerify kyvernov1.ImageVerification, - imageInfo apiutils.ImageInfo, - path string, -) (*cosign.Response, error) { - var errorList []error - verifiedCount := 0 - attestorSet = expandStaticKeys(attestorSet) - requiredCount := getRequiredCount(attestorSet) - image := imageInfo.String() - - for i, a := range attestorSet.Entries { - var entryError error - var cosignResp *cosign.Response - attestorPath := fmt.Sprintf("%s.entries[%d]", path, i) - iv.logger.V(4).Info("verifying attestorSet", "path", attestorPath) - - if a.Attestor != nil { - nestedAttestorSet, err := kyvernov1.AttestorSetUnmarshal(a.Attestor) - if err != nil { - entryError = fmt.Errorf("failed to unmarshal nested attestor %s: %w", attestorPath, err) - } else { - attestorPath += ".attestor" - cosignResp, entryError = iv.verifyAttestorSet(ctx, *nestedAttestorSet, imageVerify, imageInfo, attestorPath) - } - } else { - opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image, nil) - cosignResp, entryError = cosign.VerifySignature(ctx, iv.rclient, *opts) - if entryError != nil { - entryError = fmt.Errorf("%s: %w", attestorPath+subPath, entryError) - } - } - - if entryError == nil { - verifiedCount++ - if verifiedCount >= requiredCount { - iv.logger.V(2).Info("image attestors verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount) - return cosignResp, nil - } - } else { - errorList = append(errorList, entryError) - } - } - - err := multierr.Combine(errorList...) - iv.logger.Info("image attestors verification failed", "verifiedCount", verifiedCount, "requiredCount", requiredCount, "errors", err.Error()) - return nil, err -} - -func expandStaticKeys(attestorSet kyvernov1.AttestorSet) kyvernov1.AttestorSet { - var entries []kyvernov1.Attestor - for _, e := range attestorSet.Entries { - if e.Keys != nil { - keys := splitPEM(e.Keys.PublicKeys) - if len(keys) > 1 { - moreEntries := createStaticKeyAttestors(keys) - entries = append(entries, moreEntries...) - continue - } - } - - entries = append(entries, e) - } - - return kyvernov1.AttestorSet{ - Count: attestorSet.Count, - Entries: entries, - } -} - -func splitPEM(pem string) []string { - keys := strings.SplitAfter(pem, "-----END PUBLIC KEY-----") - if len(keys) < 1 { - return keys - } - - return keys[0 : len(keys)-1] -} - -func createStaticKeyAttestors(keys []string) []kyvernov1.Attestor { - var attestors []kyvernov1.Attestor - for _, k := range keys { - a := kyvernov1.Attestor{ - Keys: &kyvernov1.StaticKeyAttestor{ - PublicKeys: k, - }, - } - attestors = append(attestors, a) - } - - return attestors -} - -func getRequiredCount(as kyvernov1.AttestorSet) int { - if as.Count == nil || *as.Count == 0 { - return len(as.Entries) - } - - return *as.Count -} - -func (iv *imageVerifier) buildOptionsAndPath(attestor kyvernov1.Attestor, imageVerify kyvernov1.ImageVerification, image string, attestation *kyvernov1.Attestation) (*cosign.Options, string) { - path := "" - opts := &cosign.Options{ - ImageRef: image, - Repository: imageVerify.Repository, - Annotations: imageVerify.Annotations, - } - - if imageVerify.Roots != "" { - opts.Roots = imageVerify.Roots - } - - if attestation != nil { - opts.PredicateType = attestation.PredicateType - opts.FetchAttestations = true - } - - if attestor.Keys != nil { - path = path + ".keys" - if attestor.Keys.PublicKeys != "" { - opts.Key = attestor.Keys.PublicKeys - } else if attestor.Keys.Secret != nil { - opts.Key = fmt.Sprintf("k8s://%s/%s", attestor.Keys.Secret.Namespace, - attestor.Keys.Secret.Name) - } else if attestor.Keys.KMS != "" { - opts.Key = attestor.Keys.KMS - } - if attestor.Keys.Rekor != nil { - opts.RekorURL = attestor.Keys.Rekor.URL - } - opts.SignatureAlgorithm = attestor.Keys.SignatureAlgorithm - } else if attestor.Certificates != nil { - path = path + ".certificates" - opts.Cert = attestor.Certificates.Certificate - opts.CertChain = attestor.Certificates.CertificateChain - if attestor.Certificates.Rekor != nil { - opts.RekorURL = attestor.Certificates.Rekor.URL - } - } else if attestor.Keyless != nil { - path = path + ".keyless" - if attestor.Keyless.Rekor != nil { - opts.RekorURL = attestor.Keyless.Rekor.URL - } - - opts.Roots = attestor.Keyless.Roots - opts.Issuer = attestor.Keyless.Issuer - opts.Subject = attestor.Keyless.Subject - opts.AdditionalExtensions = attestor.Keyless.AdditionalExtensions - } - - if attestor.Repository != "" { - opts.Repository = attestor.Repository - } - - if attestor.Annotations != nil { - opts.Annotations = attestor.Annotations - } - - return opts, path -} - -func makeAddDigestPatch(imageInfo apiutils.ImageInfo, digest string) ([]byte, error) { - patch := make(map[string]interface{}) - patch["op"] = "replace" - patch["path"] = imageInfo.Pointer - patch["value"] = imageInfo.String() + "@" + digest - return json.Marshal(patch) -} - -func (iv *imageVerifier) verifyAttestation(statements []map[string]interface{}, attestation kyvernov1.Attestation, imageInfo apiutils.ImageInfo) error { - if attestation.PredicateType == "" { - return fmt.Errorf("a predicateType is required") - } - - image := imageInfo.String() - statementsByPredicate, types := buildStatementMap(statements) - iv.logger.V(4).Info("checking attestations", "predicates", types, "image", image) - - statements = statementsByPredicate[attestation.PredicateType] - if statements == nil { - iv.logger.Info("no attestations found for predicate", "type", attestation.PredicateType, "predicates", types, "image", imageInfo.String()) - return fmt.Errorf("attestions not found for predicate type %s", attestation.PredicateType) - } - - for _, s := range statements { - iv.logger.Info("checking attestation", "predicates", types, "image", imageInfo.String()) - val, err := iv.checkAttestations(attestation, s) - if err != nil { - return fmt.Errorf("failed to check attestations: %w", err) - } - - if !val { - return fmt.Errorf("attestation checks failed for %s and predicate %s", imageInfo.String(), attestation.PredicateType) - } - } - - return nil -} - -func buildStatementMap(statements []map[string]interface{}) (map[string][]map[string]interface{}, []string) { - results := map[string][]map[string]interface{}{} - var predicateTypes []string - for _, s := range statements { - predicateType := s["predicateType"].(string) - if results[predicateType] != nil { - results[predicateType] = append(results[predicateType], s) - } else { - results[predicateType] = []map[string]interface{}{s} - } - - predicateTypes = append(predicateTypes, predicateType) - } - - return results, predicateTypes -} - -func (iv *imageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string]interface{}) (bool, error) { - if len(a.Conditions) == 0 { - return true, nil - } - - iv.policyContext.JSONContext().Checkpoint() - defer iv.policyContext.JSONContext().Restore() - - return evaluateConditions(a.Conditions, iv.policyContext.JSONContext(), s, iv.logger) -} - -func evaluateConditions( - conditions []kyvernov1.AnyAllConditions, - ctx enginecontext.Interface, - s map[string]interface{}, - log logr.Logger, -) (bool, error) { - predicate, ok := s["predicate"].(map[string]interface{}) - if !ok { - return false, fmt.Errorf("failed to extract predicate from statement: %v", s) - } - - if err := enginecontext.AddJSONObject(ctx, predicate); err != nil { - return false, fmt.Errorf("failed to add Statement to the context %v: %w", s, err) - } - - c, err := variables.SubstituteAllInConditions(log, ctx, conditions) - if err != nil { - return false, fmt.Errorf("failed to substitute variables in attestation conditions: %w", err) - } - - pass := variables.EvaluateAnyAllConditions(log, ctx, c) - return pass, nil -} - -func matchImageReferences(imageReferences []string, image string) bool { - for _, imageRef := range imageReferences { - if wildcard.Match(imageRef, image) { - return true - } - } - return false -} diff --git a/pkg/engine/imageVerifyValidate.go b/pkg/engine/imageVerifyValidate.go index a81446622c..f2037d7a7e 100644 --- a/pkg/engine/imageVerifyValidate.go +++ b/pkg/engine/imageVerifyValidate.go @@ -30,7 +30,7 @@ func (e *engine) processImageValidationRule( return internal.RuleResponse(*rule, engineapi.Validation, err.Error(), engineapi.RuleStatusError) } if len(matchingImages) == 0 { - return internal.RuleResponse(*rule, engineapi.Validation, "image verified", engineapi.RuleStatusSkip) + return internal.RuleSkip(rule, engineapi.Validation, "image verified") } if err := internal.LoadContext(ctx, e.contextLoader, rule.Context, enginectx, rule.Name); err != nil { if _, ok := err.(gojmespath.NotFoundError); ok { @@ -42,7 +42,7 @@ func (e *engine) processImageValidationRule( return internal.RuleError(rule, engineapi.Validation, "failed to load context", err) } - preconditionsPassed, err := checkPreconditions(log, enginectx, rule.RawAnyAllConditions) + preconditionsPassed, err := internal.CheckPreconditions(log, enginectx, rule.RawAnyAllConditions) if err != nil { return internal.RuleError(rule, engineapi.Validation, "failed to evaluate preconditions", err) } @@ -52,7 +52,7 @@ func (e *engine) processImageValidationRule( return nil } - return internal.RuleResponse(*rule, engineapi.Validation, "preconditions not met", engineapi.RuleStatusSkip) + return internal.RuleSkip(rule, engineapi.Validation, "preconditions not met") } for _, v := range rule.VerifyImages { diff --git a/pkg/engine/imageVerify_test.go b/pkg/engine/imageVerify_test.go index 3a9cc48550..3fbf56af05 100644 --- a/pkg/engine/imageVerify_test.go +++ b/pkg/engine/imageVerify_test.go @@ -13,6 +13,7 @@ import ( engineapi "github.com/kyverno/kyverno/pkg/engine/api" enginecontext "github.com/kyverno/kyverno/pkg/engine/context" "github.com/kyverno/kyverno/pkg/engine/context/resolvers" + "github.com/kyverno/kyverno/pkg/engine/internal" "github.com/kyverno/kyverno/pkg/engine/utils" "github.com/kyverno/kyverno/pkg/logging" "github.com/kyverno/kyverno/pkg/registryclient" @@ -681,29 +682,28 @@ func Test_NestedAttestors(t *testing.T) { } func Test_ExpandKeys(t *testing.T) { - as := expandStaticKeys(createStaticKeyAttestorSet("", true, false, false)) + as := internal.ExpandStaticKeys(createStaticKeyAttestorSet("", true, false, false)) assert.Equal(t, 1, len(as.Entries)) - as = expandStaticKeys(createStaticKeyAttestorSet(testOtherKey, true, false, false)) + as = internal.ExpandStaticKeys(createStaticKeyAttestorSet(testOtherKey, true, false, false)) assert.Equal(t, 1, len(as.Entries)) - as = expandStaticKeys(createStaticKeyAttestorSet(testOtherKey+testOtherKey+testOtherKey, true, false, false)) + as = internal.ExpandStaticKeys(createStaticKeyAttestorSet(testOtherKey+testOtherKey+testOtherKey, true, false, false)) assert.Equal(t, 3, len(as.Entries)) - as = expandStaticKeys(createStaticKeyAttestorSet("", false, true, false)) + as = internal.ExpandStaticKeys(createStaticKeyAttestorSet("", false, true, false)) assert.Equal(t, 1, len(as.Entries)) assert.DeepEqual(t, &kyverno.SecretReference{Name: "testsecret", Namespace: "default"}, as.Entries[0].Keys.Secret) - as = expandStaticKeys(createStaticKeyAttestorSet("", false, false, true)) + as = internal.ExpandStaticKeys(createStaticKeyAttestorSet("", false, false, true)) assert.Equal(t, 1, len(as.Entries)) assert.DeepEqual(t, "gcpkms://projects/test_project_id/locations/asia-south1/keyRings/test_key_ring_name/cryptoKeys/test_key_name/versions/1", as.Entries[0].Keys.KMS) - as = expandStaticKeys((createStaticKeyAttestorSet(testOtherKey, true, true, false))) + as = internal.ExpandStaticKeys((createStaticKeyAttestorSet(testOtherKey, true, true, false))) assert.Equal(t, 2, len(as.Entries)) assert.DeepEqual(t, testOtherKey, as.Entries[0].Keys.PublicKeys) - assert.DeepEqual(t, &kyverno.SecretReference{Name: "testsecret", Namespace: "default"}, - as.Entries[1].Keys.Secret) + assert.DeepEqual(t, &kyverno.SecretReference{Name: "testsecret", Namespace: "default"}, as.Entries[1].Keys.Secret) } func createStaticKeyAttestorSet(s string, withPublicKey, withSecret, withKMS bool) kyverno.AttestorSet { @@ -746,18 +746,18 @@ func Test_ChangedAnnotation(t *testing.T) { policyContext := buildContext(t, testPolicyGood, testResource, testResource) - hasChanged := hasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) + hasChanged := internal.HasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) assert.Equal(t, hasChanged, false) policyContext = buildContext(t, testPolicyGood, newResource, testResource) - hasChanged = hasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) + hasChanged = internal.HasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) assert.Equal(t, hasChanged, true) annotationOld := fmt.Sprintf("\"annotations\": {\"%s\": \"%s\"}", annotationKey, "false") oldResource := strings.ReplaceAll(testResource, "\"annotations\": {}", annotationOld) policyContext = buildContext(t, testPolicyGood, newResource, oldResource) - hasChanged = hasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) + hasChanged = internal.HasImageVerifiedAnnotationChanged(policyContext, logging.GlobalLogger()) assert.Equal(t, hasChanged, true) } diff --git a/pkg/engine/internal/imageverifier.go b/pkg/engine/internal/imageverifier.go new file mode 100644 index 0000000000..ff343be8f9 --- /dev/null +++ b/pkg/engine/internal/imageverifier.go @@ -0,0 +1,540 @@ +package internal + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "reflect" + "strings" + + "github.com/go-logr/logr" + kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" + "github.com/kyverno/kyverno/pkg/config" + "github.com/kyverno/kyverno/pkg/cosign" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" + enginecontext "github.com/kyverno/kyverno/pkg/engine/context" + "github.com/kyverno/kyverno/pkg/engine/variables" + "github.com/kyverno/kyverno/pkg/registryclient" + apiutils "github.com/kyverno/kyverno/pkg/utils/api" + "github.com/kyverno/kyverno/pkg/utils/jsonpointer" + "github.com/kyverno/kyverno/pkg/utils/wildcard" + "go.uber.org/multierr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type ImageVerifier struct { + logger logr.Logger + rclient registryclient.Client + policyContext engineapi.PolicyContext + rule *kyvernov1.Rule + ivm *engineapi.ImageVerificationMetadata +} + +func NewImageVerifier( + logger logr.Logger, + rclient registryclient.Client, + policyContext engineapi.PolicyContext, + rule *kyvernov1.Rule, + ivm *engineapi.ImageVerificationMetadata, +) *ImageVerifier { + return &ImageVerifier{ + logger: logger, + rclient: rclient, + policyContext: policyContext, + rule: rule, + ivm: ivm, + } +} + +func HasImageVerifiedAnnotationChanged(ctx engineapi.PolicyContext, log logr.Logger) bool { + newResource := ctx.NewResource() + oldResource := ctx.OldResource() + if reflect.DeepEqual(newResource, unstructured.Unstructured{}) || + reflect.DeepEqual(oldResource, unstructured.Unstructured{}) { + return false + } + newValue := newResource.GetAnnotations()[engineapi.ImageVerifyAnnotationKey] + oldValue := oldResource.GetAnnotations()[engineapi.ImageVerifyAnnotationKey] + result := newValue != oldValue + if result { + log.V(2).Info("annotation mismatch", "oldValue", oldValue, "newValue", newValue, "key", engineapi.ImageVerifyAnnotationKey) + } + return result +} + +func matchImageReferences(imageReferences []string, image string) bool { + for _, imageRef := range imageReferences { + if wildcard.Match(imageRef, image) { + return true + } + } + return false +} + +func isImageVerified(resource unstructured.Unstructured, image string, log logr.Logger) (bool, error) { + if reflect.DeepEqual(resource, unstructured.Unstructured{}) { + return false, fmt.Errorf("nil resource") + } + annotations := resource.GetAnnotations() + if len(annotations) == 0 { + return false, nil + } + data, ok := annotations[engineapi.ImageVerifyAnnotationKey] + if !ok { + log.V(2).Info("missing image metadata in annotation", "key", engineapi.ImageVerifyAnnotationKey) + return false, fmt.Errorf("image is not verified") + } + ivm, err := engineapi.ParseImageMetadata(data) + if err != nil { + log.Error(err, "failed to parse image verification metadata", "data", data) + return false, fmt.Errorf("failed to parse image metadata: %w", err) + } + return ivm.IsVerified(image), nil +} + +func ExpandStaticKeys(attestorSet kyvernov1.AttestorSet) kyvernov1.AttestorSet { + var entries []kyvernov1.Attestor + for _, e := range attestorSet.Entries { + if e.Keys != nil { + keys := splitPEM(e.Keys.PublicKeys) + if len(keys) > 1 { + moreEntries := createStaticKeyAttestors(keys) + entries = append(entries, moreEntries...) + continue + } + } + entries = append(entries, e) + } + return kyvernov1.AttestorSet{ + Count: attestorSet.Count, + Entries: entries, + } +} + +func splitPEM(pem string) []string { + keys := strings.SplitAfter(pem, "-----END PUBLIC KEY-----") + if len(keys) < 1 { + return keys + } + return keys[0 : len(keys)-1] +} + +func createStaticKeyAttestors(keys []string) []kyvernov1.Attestor { + var attestors []kyvernov1.Attestor + for _, k := range keys { + a := kyvernov1.Attestor{ + Keys: &kyvernov1.StaticKeyAttestor{ + PublicKeys: k, + }, + } + attestors = append(attestors, a) + } + return attestors +} + +func buildStatementMap(statements []map[string]interface{}) (map[string][]map[string]interface{}, []string) { + results := map[string][]map[string]interface{}{} + var predicateTypes []string + for _, s := range statements { + predicateType := s["predicateType"].(string) + if results[predicateType] != nil { + results[predicateType] = append(results[predicateType], s) + } else { + results[predicateType] = []map[string]interface{}{s} + } + predicateTypes = append(predicateTypes, predicateType) + } + return results, predicateTypes +} + +func makeAddDigestPatch(imageInfo apiutils.ImageInfo, digest string) ([]byte, error) { + patch := make(map[string]interface{}) + patch["op"] = "replace" + patch["path"] = imageInfo.Pointer + patch["value"] = imageInfo.String() + "@" + digest + return json.Marshal(patch) +} + +func EvaluateConditions( + conditions []kyvernov1.AnyAllConditions, + ctx enginecontext.Interface, + s map[string]interface{}, + log logr.Logger, +) (bool, error) { + predicate, ok := s["predicate"].(map[string]interface{}) + if !ok { + return false, fmt.Errorf("failed to extract predicate from statement: %v", s) + } + if err := enginecontext.AddJSONObject(ctx, predicate); err != nil { + return false, fmt.Errorf("failed to add Statement to the context %v: %w", s, err) + } + c, err := variables.SubstituteAllInConditions(log, ctx, conditions) + if err != nil { + return false, fmt.Errorf("failed to substitute variables in attestation conditions: %w", err) + } + pass := variables.EvaluateAnyAllConditions(log, ctx, c) + return pass, nil +} + +// verify applies policy rules to each matching image. The policy rule results and annotation patches are +// added to tme imageVerifier `resp` and `ivm` fields. +func (iv *ImageVerifier) Verify( + ctx context.Context, + imageVerify kyvernov1.ImageVerification, + matchedImageInfos []apiutils.ImageInfo, + cfg config.Configuration, +) []*engineapi.RuleResponse { + var responses []*engineapi.RuleResponse + + // for backward compatibility + imageVerify = *imageVerify.Convert() + + for _, imageInfo := range matchedImageInfos { + image := imageInfo.String() + + if HasImageVerifiedAnnotationChanged(iv.policyContext, iv.logger) { + msg := engineapi.ImageVerifyAnnotationKey + " annotation cannot be changed" + iv.logger.Info("image verification error", "reason", msg) + responses = append(responses, RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail)) + continue + } + + pointer := jsonpointer.ParsePath(imageInfo.Pointer).JMESPath() + changed, err := iv.policyContext.JSONContext().HasChanged(pointer) + if err == nil && !changed { + iv.logger.V(4).Info("no change in image, skipping check", "image", image) + continue + } + + verified, err := isImageVerified(iv.policyContext.NewResource(), image, iv.logger) + if err == nil && verified { + iv.logger.Info("image was previously verified, skipping check", "image", image) + continue + } + + ruleResp, digest := iv.verifyImage(ctx, imageVerify, imageInfo, cfg) + + if imageVerify.MutateDigest { + patch, retrievedDigest, err := iv.handleMutateDigest(ctx, digest, imageInfo) + if err != nil { + responses = append(responses, RuleError(iv.rule, engineapi.ImageVerify, "failed to update digest", err)) + } else if patch != nil { + if ruleResp == nil { + ruleResp = RuleResponse(*iv.rule, engineapi.ImageVerify, "mutated image digest", engineapi.RuleStatusPass) + } + ruleResp.Patches = append(ruleResp.Patches, patch) + imageInfo.Digest = retrievedDigest + image = imageInfo.String() + } + } + + if ruleResp != nil { + if len(imageVerify.Attestors) > 0 || len(imageVerify.Attestations) > 0 { + iv.ivm.Add(image, ruleResp.Status == engineapi.RuleStatusPass) + } + responses = append(responses, ruleResp) + } + } + return responses +} + +func (iv *ImageVerifier) verifyImage( + ctx context.Context, + imageVerify kyvernov1.ImageVerification, + imageInfo apiutils.ImageInfo, + cfg config.Configuration, +) (*engineapi.RuleResponse, string) { + if len(imageVerify.Attestors) <= 0 && len(imageVerify.Attestations) <= 0 { + return nil, "" + } + image := imageInfo.String() + iv.logger.V(2).Info("verifying image signatures", "image", image, "attestors", len(imageVerify.Attestors), "attestations", len(imageVerify.Attestations)) + if err := iv.policyContext.JSONContext().AddImageInfo(imageInfo, cfg); err != nil { + iv.logger.Error(err, "failed to add image to context") + msg := fmt.Sprintf("failed to add image to context %s: %s", image, err.Error()) + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusError), "" + } + if len(imageVerify.Attestors) > 0 { + if !matchImageReferences(imageVerify.ImageReferences, image) { + return nil, "" + } + ruleResp, cosignResp := iv.verifyAttestors(ctx, imageVerify.Attestors, imageVerify, imageInfo, "") + if ruleResp.Status != engineapi.RuleStatusPass { + return ruleResp, "" + } + if len(imageVerify.Attestations) == 0 { + return ruleResp, cosignResp.Digest + } + if imageInfo.Digest == "" { + imageInfo.Digest = cosignResp.Digest + } + if len(imageVerify.Attestations) == 0 { + return ruleResp, cosignResp.Digest + } + if imageInfo.Digest == "" { + imageInfo.Digest = cosignResp.Digest + } + } + return iv.verifyAttestations(ctx, imageVerify, imageInfo) +} + +func (iv *ImageVerifier) verifyAttestors( + ctx context.Context, + attestors []kyvernov1.AttestorSet, + imageVerify kyvernov1.ImageVerification, + imageInfo apiutils.ImageInfo, + predicateType string, +) (*engineapi.RuleResponse, *cosign.Response) { + var cosignResponse *cosign.Response + image := imageInfo.String() + for i, attestorSet := range attestors { + var err error + path := fmt.Sprintf(".attestors[%d]", i) + iv.logger.V(4).Info("verifying attestors", "path", path) + cosignResponse, err = iv.verifyAttestorSet(ctx, attestorSet, imageVerify, imageInfo, path) + if err != nil { + iv.logger.Error(err, "failed to verify image") + return iv.handleRegistryErrors(image, err), nil + } + } + if cosignResponse == nil { + return RuleError(iv.rule, engineapi.ImageVerify, "invalid response", fmt.Errorf("nil")), nil + } + msg := fmt.Sprintf("verified image signatures for %s", image) + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusPass), cosignResponse +} + +// handle registry network errors as a rule error (instead of a policy failure) +func (iv *ImageVerifier) handleRegistryErrors(image string, err error) *engineapi.RuleResponse { + msg := fmt.Sprintf("failed to verify image %s: %s", image, err.Error()) + var netErr *net.OpError + if errors.As(err, &netErr) { + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusError) + } + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail) +} + +func (iv *ImageVerifier) verifyAttestations( + ctx context.Context, + imageVerify kyvernov1.ImageVerification, + imageInfo apiutils.ImageInfo, +) (*engineapi.RuleResponse, string) { + image := imageInfo.String() + for i, attestation := range imageVerify.Attestations { + var attestationError error + path := fmt.Sprintf(".attestations[%d]", i) + + if attestation.PredicateType == "" { + return RuleResponse(*iv.rule, engineapi.ImageVerify, path+": missing predicateType", engineapi.RuleStatusFail), "" + } + + if len(attestation.Attestors) == 0 { + // add an empty attestor to allow fetching and checking attestations + attestation.Attestors = []kyvernov1.AttestorSet{{Entries: []kyvernov1.Attestor{{}}}} + } + + for j, attestor := range attestation.Attestors { + attestorPath := fmt.Sprintf("%s.attestors[%d]", path, j) + requiredCount := attestor.RequiredCount() + verifiedCount := 0 + + for _, a := range attestor.Entries { + entryPath := fmt.Sprintf("%s.entries[%d]", attestorPath, i) + opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image, &imageVerify.Attestations[i]) + cosignResp, err := cosign.FetchAttestations(ctx, iv.rclient, *opts) + if err != nil { + iv.logger.Error(err, "failed to fetch attestations") + return iv.handleRegistryErrors(image, err), "" + } + + if imageInfo.Digest == "" { + imageInfo.Digest = cosignResp.Digest + image = imageInfo.String() + } + + attestationError = iv.verifyAttestation(cosignResp.Statements, attestation, imageInfo) + if attestationError != nil { + attestationError = fmt.Errorf("%s: %w", entryPath+subPath, attestationError) + return RuleResponse(*iv.rule, engineapi.ImageVerify, attestationError.Error(), engineapi.RuleStatusFail), "" + } + + verifiedCount++ + if verifiedCount >= requiredCount { + iv.logger.V(2).Info("image attestations verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount) + break + } + } + + if verifiedCount < requiredCount { + msg := fmt.Sprintf("image attestations verification failed, verifiedCount: %v, requiredCount: %v", verifiedCount, requiredCount) + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusFail), "" + } + } + + iv.logger.V(4).Info("attestation checks passed", "path", path, "image", imageInfo.String(), "predicateType", attestation.PredicateType) + } + + msg := fmt.Sprintf("verified image attestations for %s", image) + iv.logger.V(2).Info(msg) + return RuleResponse(*iv.rule, engineapi.ImageVerify, msg, engineapi.RuleStatusPass), imageInfo.Digest +} + +func (iv *ImageVerifier) verifyAttestorSet( + ctx context.Context, + attestorSet kyvernov1.AttestorSet, + imageVerify kyvernov1.ImageVerification, + imageInfo apiutils.ImageInfo, + path string, +) (*cosign.Response, error) { + var errorList []error + verifiedCount := 0 + attestorSet = ExpandStaticKeys(attestorSet) + requiredCount := attestorSet.RequiredCount() + image := imageInfo.String() + + for i, a := range attestorSet.Entries { + var entryError error + var cosignResp *cosign.Response + attestorPath := fmt.Sprintf("%s.entries[%d]", path, i) + iv.logger.V(4).Info("verifying attestorSet", "path", attestorPath) + + if a.Attestor != nil { + nestedAttestorSet, err := kyvernov1.AttestorSetUnmarshal(a.Attestor) + if err != nil { + entryError = fmt.Errorf("failed to unmarshal nested attestor %s: %w", attestorPath, err) + } else { + attestorPath += ".attestor" + cosignResp, entryError = iv.verifyAttestorSet(ctx, *nestedAttestorSet, imageVerify, imageInfo, attestorPath) + } + } else { + opts, subPath := iv.buildOptionsAndPath(a, imageVerify, image, nil) + cosignResp, entryError = cosign.VerifySignature(ctx, iv.rclient, *opts) + if entryError != nil { + entryError = fmt.Errorf("%s: %w", attestorPath+subPath, entryError) + } + } + + if entryError == nil { + verifiedCount++ + if verifiedCount >= requiredCount { + iv.logger.V(2).Info("image attestors verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount) + return cosignResp, nil + } + } else { + errorList = append(errorList, entryError) + } + } + + err := multierr.Combine(errorList...) + iv.logger.Info("image attestors verification failed", "verifiedCount", verifiedCount, "requiredCount", requiredCount, "errors", err.Error()) + return nil, err +} + +func (iv *ImageVerifier) buildOptionsAndPath(attestor kyvernov1.Attestor, imageVerify kyvernov1.ImageVerification, image string, attestation *kyvernov1.Attestation) (*cosign.Options, string) { + path := "" + opts := &cosign.Options{ + ImageRef: image, + Repository: imageVerify.Repository, + Annotations: imageVerify.Annotations, + } + if imageVerify.Roots != "" { + opts.Roots = imageVerify.Roots + } + if attestation != nil { + opts.PredicateType = attestation.PredicateType + opts.FetchAttestations = true + } + if attestor.Keys != nil { + path = path + ".keys" + if attestor.Keys.PublicKeys != "" { + opts.Key = attestor.Keys.PublicKeys + } else if attestor.Keys.Secret != nil { + opts.Key = fmt.Sprintf("k8s://%s/%s", attestor.Keys.Secret.Namespace, + attestor.Keys.Secret.Name) + } else if attestor.Keys.KMS != "" { + opts.Key = attestor.Keys.KMS + } + if attestor.Keys.Rekor != nil { + opts.RekorURL = attestor.Keys.Rekor.URL + } + opts.SignatureAlgorithm = attestor.Keys.SignatureAlgorithm + } else if attestor.Certificates != nil { + path = path + ".certificates" + opts.Cert = attestor.Certificates.Certificate + opts.CertChain = attestor.Certificates.CertificateChain + if attestor.Certificates.Rekor != nil { + opts.RekorURL = attestor.Certificates.Rekor.URL + } + } else if attestor.Keyless != nil { + path = path + ".keyless" + if attestor.Keyless.Rekor != nil { + opts.RekorURL = attestor.Keyless.Rekor.URL + } + opts.Roots = attestor.Keyless.Roots + opts.Issuer = attestor.Keyless.Issuer + opts.Subject = attestor.Keyless.Subject + opts.AdditionalExtensions = attestor.Keyless.AdditionalExtensions + } + if attestor.Repository != "" { + opts.Repository = attestor.Repository + } + if attestor.Annotations != nil { + opts.Annotations = attestor.Annotations + } + return opts, path +} + +func (iv *ImageVerifier) verifyAttestation(statements []map[string]interface{}, attestation kyvernov1.Attestation, imageInfo apiutils.ImageInfo) error { + if attestation.PredicateType == "" { + return fmt.Errorf("a predicateType is required") + } + image := imageInfo.String() + statementsByPredicate, types := buildStatementMap(statements) + iv.logger.V(4).Info("checking attestations", "predicates", types, "image", image) + statements = statementsByPredicate[attestation.PredicateType] + if statements == nil { + iv.logger.Info("no attestations found for predicate", "type", attestation.PredicateType, "predicates", types, "image", imageInfo.String()) + return fmt.Errorf("attestions not found for predicate type %s", attestation.PredicateType) + } + for _, s := range statements { + iv.logger.Info("checking attestation", "predicates", types, "image", imageInfo.String()) + val, err := iv.checkAttestations(attestation, s) + if err != nil { + return fmt.Errorf("failed to check attestations: %w", err) + } + if !val { + return fmt.Errorf("attestation checks failed for %s and predicate %s", imageInfo.String(), attestation.PredicateType) + } + } + return nil +} + +func (iv *ImageVerifier) checkAttestations(a kyvernov1.Attestation, s map[string]interface{}) (bool, error) { + if len(a.Conditions) == 0 { + return true, nil + } + iv.policyContext.JSONContext().Checkpoint() + defer iv.policyContext.JSONContext().Restore() + return EvaluateConditions(a.Conditions, iv.policyContext.JSONContext(), s, iv.logger) +} + +func (iv *ImageVerifier) handleMutateDigest(ctx context.Context, digest string, imageInfo apiutils.ImageInfo) ([]byte, string, error) { + if imageInfo.Digest != "" { + return nil, "", nil + } + if digest == "" { + desc, err := iv.rclient.FetchImageDescriptor(ctx, imageInfo.String()) + if err != nil { + return nil, "", err + } + digest = desc.Digest.String() + } + patch, err := makeAddDigestPatch(imageInfo, digest) + if err != nil { + return nil, "", fmt.Errorf("failed to create image digest patch: %w", err) + } + iv.logger.V(4).Info("adding digest patch", "image", imageInfo.String(), "patch", string(patch)) + return patch, digest, nil +} diff --git a/pkg/engine/internal/preconditions.go b/pkg/engine/internal/preconditions.go new file mode 100644 index 0000000000..1fa26a5d3b --- /dev/null +++ b/pkg/engine/internal/preconditions.go @@ -0,0 +1,35 @@ +package internal + +import ( + "fmt" + + "github.com/go-logr/logr" + engineapi "github.com/kyverno/kyverno/pkg/engine/api" + "github.com/kyverno/kyverno/pkg/engine/utils" + "github.com/kyverno/kyverno/pkg/engine/variables" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" +) + +func CheckPreconditions(logger logr.Logger, ctx engineapi.PolicyContext, anyAllConditions apiextensions.JSON) (bool, error) { + preconditions, err := variables.SubstituteAllInPreconditions(logger, ctx.JSONContext(), anyAllConditions) + if err != nil { + return false, fmt.Errorf("failed to substitute variables in preconditions: %w", err) + } + typeConditions, err := utils.TransformConditions(preconditions) + if err != nil { + return false, fmt.Errorf("failed to parse preconditions: %w", err) + } + return variables.EvaluateConditions(logger, ctx.JSONContext(), typeConditions), nil +} + +func CheckDenyPreconditions(logger logr.Logger, ctx engineapi.PolicyContext, anyAllConditions apiextensions.JSON) (bool, error) { + preconditions, err := variables.SubstituteAll(logger, ctx.JSONContext(), anyAllConditions) + if err != nil { + return false, fmt.Errorf("failed to substitute variables in deny conditions: %w", err) + } + typeConditions, err := utils.TransformConditions(preconditions) + if err != nil { + return false, fmt.Errorf("failed to parse deny conditions: %w", err) + } + return variables.EvaluateConditions(logger, ctx.JSONContext(), typeConditions), nil +} diff --git a/pkg/engine/internal/response.go b/pkg/engine/internal/response.go index a353280732..35ab9cf78a 100644 --- a/pkg/engine/internal/response.go +++ b/pkg/engine/internal/response.go @@ -11,8 +11,11 @@ import ( ) func RuleError(rule *kyvernov1.Rule, ruleType engineapi.RuleType, msg string, err error) *engineapi.RuleResponse { - msg = fmt.Sprintf("%s: %s", msg, err.Error()) - return RuleResponse(*rule, ruleType, msg, engineapi.RuleStatusError) + return RuleResponse(*rule, ruleType, fmt.Sprintf("%s: %s", msg, err.Error()), engineapi.RuleStatusError) +} + +func RuleSkip(rule *kyvernov1.Rule, ruleType engineapi.RuleType, msg string) *engineapi.RuleResponse { + return RuleResponse(*rule, ruleType, msg, engineapi.RuleStatusSkip) } func RuleResponse(rule kyvernov1.Rule, ruleType engineapi.RuleType, msg string, status engineapi.RuleStatus) *engineapi.RuleResponse { @@ -25,6 +28,17 @@ func RuleResponse(rule kyvernov1.Rule, ruleType engineapi.RuleType, msg string, return resp } +func AddRuleResponse(resp *engineapi.PolicyResponse, ruleResp *engineapi.RuleResponse, startTime time.Time) { + ruleResp.ExecutionStats.ProcessingTime = time.Since(startTime) + ruleResp.ExecutionStats.Timestamp = startTime.Unix() + resp.Rules = append(resp.Rules, *ruleResp) + if ruleResp.Status == engineapi.RuleStatusPass || ruleResp.Status == engineapi.RuleStatusFail { + resp.RulesAppliedCount++ + } else if ruleResp.Status == engineapi.RuleStatusError { + resp.RulesErrorCount++ + } +} + func BuildResponse(ctx engineapi.PolicyContext, resp *engineapi.EngineResponse, startTime time.Time) *engineapi.EngineResponse { resp.NamespaceLabels = ctx.NamespaceLabels() if reflect.DeepEqual(resp, engineapi.EngineResponse{}) { diff --git a/pkg/engine/k8smanifest.go b/pkg/engine/k8smanifest.go index cc1216275d..11d604200f 100644 --- a/pkg/engine/k8smanifest.go +++ b/pkg/engine/k8smanifest.go @@ -169,8 +169,8 @@ func verifyManifest( func verifyManifestAttestorSet(resource unstructured.Unstructured, attestorSet kyvernov1.AttestorSet, vo *k8smanifest.VerifyResourceOption, path string, uid string, logger logr.Logger) (bool, string, error) { verifiedCount := 0 - attestorSet = expandStaticKeys(attestorSet) - requiredCount := getRequiredCount(attestorSet) + attestorSet = internal.ExpandStaticKeys(attestorSet) + requiredCount := attestorSet.RequiredCount() errorList := []error{} verifiedMessageList := []string{} failedMessageList := []string{} diff --git a/pkg/engine/mutation.go b/pkg/engine/mutation.go index 27492801fe..8ebb3c21f9 100644 --- a/pkg/engine/mutation.go +++ b/pkg/engine/mutation.go @@ -75,8 +75,7 @@ func (e *engine) mutate( } // check if there is a corresponding policy exception - ruleResp := hasPolicyExceptions(logger, e.exceptionSelector, policyContext, &computeRules[i], subresourceGVKToAPIResource, e.configuration) - if ruleResp != nil { + if ruleResp := hasPolicyExceptions(logger, engineapi.Mutation, e.exceptionSelector, policyContext, &computeRules[i], subresourceGVKToAPIResource, e.configuration); ruleResp != nil { resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) return } @@ -155,15 +154,9 @@ func (e *engine) mutate( } matchedResource = mutateResp.PatchedResource - ruleResponse := buildRuleResponse(ruleCopy, mutateResp, patchedResource) - if ruleResponse != nil { - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResponse) - if ruleResponse.Status == engineapi.RuleStatusError { - incrementErrorCount(resp) - } else { - incrementAppliedCount(resp) - } + if ruleResponse := buildRuleResponse(ruleCopy, mutateResp, patchedResource); ruleResponse != nil { + internal.AddRuleResponse(&resp.PolicyResponse, ruleResponse, startTime) } } }, @@ -187,7 +180,7 @@ func (e *engine) mutate( } func mutateResource(rule *kyvernov1.Rule, ctx engineapi.PolicyContext, resource unstructured.Unstructured, logger logr.Logger) *mutate.Response { - preconditionsPassed, err := checkPreconditions(logger, ctx, rule.GetAnyAllConditions()) + preconditionsPassed, err := internal.CheckPreconditions(logger, ctx, rule.GetAnyAllConditions()) if err != nil { return mutate.NewErrorResponse("failed to evaluate preconditions", err) } @@ -219,7 +212,7 @@ func (f *forEachMutator) mutateForEach(ctx context.Context) *mutate.Response { return mutate.NewErrorResponse("failed to load context", err) } - preconditionsPassed, err := checkPreconditions(f.log, f.policyContext, f.rule.GetAnyAllConditions()) + preconditionsPassed, err := internal.CheckPreconditions(f.log, f.policyContext, f.rule.GetAnyAllConditions()) if err != nil { return mutate.NewErrorResponse("failed to evaluate preconditions", err) } @@ -283,7 +276,7 @@ func (f *forEachMutator) mutateElements(ctx context.Context, foreach kyvernov1.F return mutate.NewErrorResponse(fmt.Sprintf("failed to load to mutate.foreach[%d].context", index), err) } - preconditionsPassed, err := checkPreconditions(f.log, policyContext, foreach.AnyAllConditions) + preconditionsPassed, err := internal.CheckPreconditions(f.log, policyContext, foreach.AnyAllConditions) if err != nil { return mutate.NewErrorResponse(fmt.Sprintf("failed to evaluate mutate.foreach[%d].preconditions", index), err) } diff --git a/pkg/engine/utils.go b/pkg/engine/utils.go index 6d753f31e1..d81fa748ed 100644 --- a/pkg/engine/utils.go +++ b/pkg/engine/utils.go @@ -6,21 +6,16 @@ import ( "strings" "time" - "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" "github.com/kyverno/kyverno/cmd/cli/kubectl-kyverno/utils/store" - engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/context" - "github.com/kyverno/kyverno/pkg/engine/utils" - "github.com/kyverno/kyverno/pkg/engine/variables" datautils "github.com/kyverno/kyverno/pkg/utils/data" matchutils "github.com/kyverno/kyverno/pkg/utils/match" "github.com/kyverno/kyverno/pkg/utils/wildcard" "golang.org/x/exp/slices" authenticationv1 "k8s.io/api/authentication/v1" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -324,21 +319,6 @@ func ManagedPodResource(policy kyvernov1.PolicyInterface, resource unstructured. return false } -func checkPreconditions(logger logr.Logger, ctx engineapi.PolicyContext, anyAllConditions apiextensions.JSON) (bool, error) { - preconditions, err := variables.SubstituteAllInPreconditions(logger, ctx.JSONContext(), anyAllConditions) - if err != nil { - return false, fmt.Errorf("failed to substitute variables in preconditions: %w", err) - } - - typeConditions, err := utils.TransformConditions(preconditions) - if err != nil { - return false, fmt.Errorf("failed to parse preconditions: %w", err) - } - - pass := variables.EvaluateConditions(logger, ctx.JSONContext(), typeConditions) - return pass, nil -} - func evaluateList(jmesPath string, ctx context.EvalInterface) ([]interface{}, error) { i, err := ctx.Query(jmesPath) if err != nil { @@ -353,14 +333,6 @@ func evaluateList(jmesPath string, ctx context.EvalInterface) ([]interface{}, er return l, nil } -func incrementAppliedCount(resp *engineapi.EngineResponse) { - resp.PolicyResponse.RulesAppliedCount++ -} - -func incrementErrorCount(resp *engineapi.EngineResponse) { - resp.PolicyResponse.RulesErrorCount++ -} - // invertedElement inverted the order of element for patchStrategicMerge policies as kustomize patch revering the order of patch resources. func invertedElement(elements []interface{}) { for i, j := 0, len(elements)-1; i < j; i, j = i+1, j-1 { diff --git a/pkg/engine/validation.go b/pkg/engine/validation.go index 89cf778b7a..2451376ba6 100644 --- a/pkg/engine/validation.go +++ b/pkg/engine/validation.go @@ -15,7 +15,6 @@ import ( "github.com/kyverno/kyverno/pkg/config" engineapi "github.com/kyverno/kyverno/pkg/engine/api" "github.com/kyverno/kyverno/pkg/engine/internal" - "github.com/kyverno/kyverno/pkg/engine/utils" "github.com/kyverno/kyverno/pkg/engine/validate" "github.com/kyverno/kyverno/pkg/engine/variables" "github.com/kyverno/kyverno/pkg/pss" @@ -38,17 +37,18 @@ func (e *engine) validate( startTime := time.Now() logger := internal.BuildLogger(policyContext) logger.V(4).Info("start validate policy processing", "startTime", startTime) - resp := e.validateResource(ctx, logger, policyContext) - defer logger.V(4).Info("finished policy processing", "processingTime", resp.PolicyResponse.ProcessingTime.String(), "validationRulesApplied", resp.PolicyResponse.RulesAppliedCount) - return internal.BuildResponse(policyContext, resp, startTime) + policyResponse := e.validateResource(ctx, logger, policyContext) + defer logger.V(4).Info("finished policy processing", "processingTime", policyResponse.ProcessingTime.String(), "validationRulesApplied", policyResponse.RulesAppliedCount) + engineResponse := &engineapi.EngineResponse{PolicyResponse: *policyResponse} + return internal.BuildResponse(policyContext, engineResponse, startTime) } func (e *engine) validateResource( ctx context.Context, log logr.Logger, enginectx engineapi.PolicyContext, -) *engineapi.EngineResponse { - resp := &engineapi.EngineResponse{} +) *engineapi.PolicyResponse { + resp := &engineapi.PolicyResponse{} enginectx.JSONContext().Checkpoint() defer enginectx.JSONContext().Restore() @@ -93,7 +93,7 @@ func (e *engine) validateResource( return nil } // check if there is a corresponding policy exception - ruleResp := hasPolicyExceptions(log, e.exceptionSelector, enginectx, rule, subresourceGVKToAPIResource, e.configuration) + ruleResp := hasPolicyExceptions(log, engineapi.Validation, e.exceptionSelector, enginectx, rule, subresourceGVKToAPIResource, e.configuration) if ruleResp != nil { return ruleResp } @@ -110,10 +110,11 @@ func (e *engine) validateResource( }, ) if ruleResp != nil { - addRuleResponse(log, resp, ruleResp, startTime) - if applyRules == kyvernov1.ApplyOne && resp.PolicyResponse.RulesAppliedCount > 0 { - break - } + internal.AddRuleResponse(resp, ruleResp, startTime) + log.V(4).Info("finished processing rule", "processingTime", ruleResp.ExecutionStats.ProcessingTime.String()) + } + if applyRules == kyvernov1.ApplyOne && resp.RulesAppliedCount > 0 { + break } } @@ -130,20 +131,6 @@ func (e *engine) processValidationRule( return v.validate(ctx) } -func addRuleResponse(log logr.Logger, resp *engineapi.EngineResponse, ruleResp *engineapi.RuleResponse, startTime time.Time) { - ruleResp.ExecutionStats.ProcessingTime = time.Since(startTime) - ruleResp.ExecutionStats.Timestamp = startTime.Unix() - log.V(4).Info("finished processing rule", "processingTime", ruleResp.ExecutionStats.ProcessingTime.String()) - - if ruleResp.Status == engineapi.RuleStatusPass || ruleResp.Status == engineapi.RuleStatusFail { - incrementAppliedCount(resp) - } else if ruleResp.Status == engineapi.RuleStatusError { - incrementErrorCount(resp) - } - - resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *ruleResp) -} - type validator struct { log logr.Logger policyContext engineapi.PolicyContext @@ -215,13 +202,13 @@ func (v *validator) validate(ctx context.Context) *engineapi.RuleResponse { return internal.RuleError(v.rule, engineapi.Validation, "failed to load context", err) } - preconditionsPassed, err := checkPreconditions(v.log, v.policyContext, v.anyAllConditions) + preconditionsPassed, err := internal.CheckPreconditions(v.log, v.policyContext, v.anyAllConditions) if err != nil { return internal.RuleError(v.rule, engineapi.Validation, "failed to evaluate preconditions", err) } if !preconditionsPassed { - return internal.RuleResponse(*v.rule, engineapi.Validation, "preconditions not met", engineapi.RuleStatusSkip) + return internal.RuleSkip(v.rule, engineapi.Validation, "preconditions not met") } if v.deny != nil { @@ -271,7 +258,7 @@ func (v *validator) validateForEach(ctx context.Context) *engineapi.RuleResponse if v.forEach == nil { return nil } - return internal.RuleResponse(*v.rule, engineapi.Validation, "rule skipped", engineapi.RuleStatusSkip) + return internal.RuleSkip(v.rule, engineapi.Validation, "rule skipped") } return internal.RuleResponse(*v.rule, engineapi.Validation, "rule passed", engineapi.RuleStatusPass) } @@ -371,27 +358,14 @@ func (v *validator) loadContext(ctx context.Context) error { } func (v *validator) validateDeny() *engineapi.RuleResponse { - anyAllCond := v.deny.GetAnyAllConditions() - anyAllCond, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), anyAllCond) - if err != nil { - return internal.RuleError(v.rule, engineapi.Validation, "failed to substitute variables in deny conditions", err) + if deny, err := internal.CheckDenyPreconditions(v.log, v.policyContext, v.deny.GetAnyAllConditions()); err != nil { + return internal.RuleError(v.rule, engineapi.Validation, "failed to check deny preconditions", err) + } else { + if deny { + return internal.RuleResponse(*v.rule, engineapi.Validation, v.getDenyMessage(deny), engineapi.RuleStatusFail) + } + return internal.RuleResponse(*v.rule, engineapi.Validation, v.getDenyMessage(deny), engineapi.RuleStatusPass) } - - if err = v.substituteDeny(); err != nil { - return internal.RuleError(v.rule, engineapi.Validation, "failed to substitute variables in rule", err) - } - - denyConditions, err := utils.TransformConditions(anyAllCond) - if err != nil { - return internal.RuleError(v.rule, engineapi.Validation, "invalid deny conditions", err) - } - - deny := variables.EvaluateConditions(v.log, v.policyContext.JSONContext(), denyConditions) - if deny { - return internal.RuleResponse(*v.rule, engineapi.Validation, v.getDenyMessage(deny), engineapi.RuleStatusFail) - } - - return internal.RuleResponse(*v.rule, engineapi.Validation, v.getDenyMessage(deny), engineapi.RuleStatusPass) } func (v *validator) getDenyMessage(deny bool) string { @@ -565,7 +539,7 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *engine v.log.V(3).Info("validation error", "path", pe.Path, "error", err.Error()) if pe.Skip { - return internal.RuleResponse(*v.rule, engineapi.Validation, pe.Error(), engineapi.RuleStatusSkip) + return internal.RuleSkip(v.rule, engineapi.Validation, pe.Error()) } if pe.Path == "" { @@ -625,9 +599,8 @@ func (v *validator) validatePatterns(resource unstructured.Unstructured) *engine for _, err := range skippedAnyPatternErrors { errorStr = append(errorStr, err.Error()) } - v.log.V(4).Info(fmt.Sprintf("Validation rule '%s' skipped. %s", v.rule.Name, errorStr)) - return internal.RuleResponse(*v.rule, engineapi.Validation, strings.Join(errorStr, " "), engineapi.RuleStatusSkip) + return internal.RuleSkip(v.rule, engineapi.Validation, strings.Join(errorStr, " ")) } else if len(failedAnyPatternsErrors) > 0 { var errorStr []string for _, err := range failedAnyPatternsErrors { @@ -705,7 +678,6 @@ func (v *validator) substitutePatterns() error { if err != nil { return err } - v.pattern = i.(apiextensions.JSON) return nil } @@ -715,22 +687,9 @@ func (v *validator) substitutePatterns() error { if err != nil { return err } - v.anyPattern = i.(apiextensions.JSON) return nil } return nil } - -func (v *validator) substituteDeny() error { - if v.deny == nil { - return nil - } - i, err := variables.SubstituteAll(v.log, v.policyContext.JSONContext(), v.deny) - if err != nil { - return err - } - v.deny = i.(*kyvernov1.Deny) - return nil -} diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 296c9fe9cf..76b4f8ed5f 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -1940,7 +1940,7 @@ func Test_Flux_Kustomization_PathNotPresent(t *testing.T) { // referred variable path not present resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team"},"prune":true,"validation":"client"}}`), expectedResults: []engineapi.RuleStatus{engineapi.RuleStatusPass, engineapi.RuleStatusError}, - expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "failed to substitute variables in deny conditions: failed to resolve request.object.spec.sourceRef.namespace at path /0/key: JMESPath query failed: Unknown key \"namespace\" in path"}, + expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "failed to check deny preconditions: failed to substitute variables in deny conditions: failed to resolve request.object.spec.sourceRef.namespace at path /0/key: JMESPath query failed: Unknown key \"namespace\" in path"}, }, { name: "resource-with-violation",