1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-05 15:37:19 +00:00

Validating policy audit annotations (#12115)

* feat: return single result from validating policy evaluation

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* feat: support audit annotations for validating policies

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* fix error message

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* feat: return single result from validating policy evaluation

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* feat: support audit annotations for validating policies

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* fix error message

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* fix testcase

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

* rebase with main

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>

---------

Signed-off-by: Frank Jogeleit <frank.jogeleit@web.de>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
This commit is contained in:
Frank Jogeleit 2025-02-19 14:10:01 +01:00 committed by GitHub
parent e01e57355a
commit fef88ab433
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 69 additions and 49 deletions

View file

@ -2,14 +2,12 @@ package engine
import (
"context"
"fmt"
"strings"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/cel/matching"
"github.com/kyverno/kyverno/pkg/cel/utils"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/handlers"
admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
@ -203,33 +201,30 @@ func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, attr a
}
autogenIndex = index
}
results, matchedExceptions, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context, autogenIndex)
result, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context, autogenIndex)
// TODO: error is about match conditions here ?
if err != nil {
response.Rules = handlers.WithResponses(engineapi.RuleError("evaluation", engineapi.Validation, "failed to load context", err, nil))
} else if len(matchedExceptions) > 0 {
} else if len(result.Exceptions) > 0 {
var keys []string
for i := range matchedExceptions {
key, err := cache.MetaNamespaceKeyFunc(&matchedExceptions[i])
for i := range result.Exceptions {
key, err := cache.MetaNamespaceKeyFunc(&result.Exceptions[i])
if err != nil {
response.Rules = handlers.WithResponses(engineapi.RuleError("exception", engineapi.Validation, "failed to compute exception key", err, nil))
return response
}
keys = append(keys, key)
}
response.Rules = handlers.WithResponses(engineapi.RuleSkip("exception", engineapi.Validation, "rule is skipped due to policy exception: "+strings.Join(keys, ", "), nil).WithCELExceptions(matchedExceptions))
response.Rules = handlers.WithResponses(engineapi.RuleSkip("exception", engineapi.Validation, "rule is skipped due to policy exception: "+strings.Join(keys, ", "), nil).WithCELExceptions(result.Exceptions))
} else {
for index, validationResult := range results {
ruleName := fmt.Sprintf("rule-%d", index)
if validationResult.Error != nil {
response.Rules = append(response.Rules, *engineapi.RuleError(ruleName, engineapi.Validation, "error", err, nil))
} else if result, err := utils.ConvertToNative[bool](validationResult.Result); err != nil {
response.Rules = append(response.Rules, *engineapi.RuleError(ruleName, engineapi.Validation, "conversion error", err, nil))
} else if result {
response.Rules = append(response.Rules, *engineapi.RulePass(ruleName, engineapi.Validation, "success", nil))
} else {
response.Rules = append(response.Rules, *engineapi.RuleFail(ruleName, engineapi.Validation, validationResult.Message, nil))
}
// TODO: do we want to set a rule name?
ruleName := ""
if result.Error != nil {
response.Rules = append(response.Rules, *engineapi.RuleError(ruleName, engineapi.Validation, "error", err, nil))
} else if result.Result {
response.Rules = append(response.Rules, *engineapi.RulePass(ruleName, engineapi.Validation, "success", nil))
} else {
response.Rules = append(response.Rules, *engineapi.RuleFail(ruleName, engineapi.Validation, result.Message, result.AuditAnnotations))
}
}
return response

View file

@ -21,13 +21,16 @@ import (
)
type EvaluationResult struct {
Error error
Message string
Result ref.Val
Error error
Message string
Index int
Result bool
AuditAnnotations map[string]string
Exceptions []policiesv1alpha1.CELPolicyException
}
type CompiledPolicy interface {
Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface, int) ([]EvaluationResult, []policiesv1alpha1.CELPolicyException, error)
Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface, int) (*EvaluationResult, error)
}
type compiledValidation struct {
@ -65,21 +68,21 @@ func (p *compiledPolicy) Evaluate(
namespace runtime.Object,
context contextlib.ContextInterface,
autogenIndex int,
) ([]EvaluationResult, []policiesv1alpha1.CELPolicyException, error) {
) (*EvaluationResult, error) {
// check if the resource matches an exception
if len(p.exceptions) > 0 {
matchedExceptions := make([]policiesv1alpha1.CELPolicyException, 0)
for _, polex := range p.exceptions {
match, err := p.match(ctx, attr, request, namespace, polex.matchConditions)
if err != nil {
return nil, nil, err
return nil, err
}
if match {
matchedExceptions = append(matchedExceptions, polex.exception)
}
}
if len(matchedExceptions) > 0 {
return nil, matchedExceptions, nil
return &EvaluationResult{Exceptions: matchedExceptions}, nil
}
}
@ -98,26 +101,26 @@ func (p *compiledPolicy) Evaluate(
}
match, err := p.match(ctx, attr, request, namespace, matchConditions)
if err != nil {
return nil, nil, err
return nil, err
}
if !match {
return nil, nil, nil
return nil, nil
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
}
objectVal, err := objectToResolveVal(attr.GetObject())
if err != nil {
return nil, nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
}
oldObjectVal, err := objectToResolveVal(attr.GetOldObject())
if err != nil {
return nil, nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
}
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
}
vars := lazy.NewMapValue(VariablesType)
data := map[string]any{
@ -140,13 +143,16 @@ func (p *compiledPolicy) Evaluate(
return nil
})
}
results := make([]EvaluationResult, 0, len(validations))
for _, validation := range validations {
for index, validation := range validations {
out, _, err := validation.program.ContextEval(ctx, data)
if err != nil {
return nil, err
}
// evaluate only when rule fails
var message string
if outcome, err := utils.ConvertToNative[bool](out); err == nil && !outcome {
message = validation.message
message := validation.message
if validation.messageExpression != nil {
if out, _, err := validation.messageExpression.ContextEval(ctx, data); err != nil {
message = fmt.Sprintf("failed to evaluate message expression: %s", err)
@ -156,14 +162,34 @@ func (p *compiledPolicy) Evaluate(
message = msg
}
}
auditAnnotations := make(map[string]string, 0)
for key, annotation := range p.auditAnnotations {
out, _, err := annotation.ContextEval(ctx, data)
if err != nil {
return nil, fmt.Errorf("failed to evaluate auditAnnotation '%s': %w", key, err)
}
// evaluate only when rule fails
if outcome, err := utils.ConvertToNative[string](out); err == nil && outcome != "" {
auditAnnotations[key] = outcome
} else if err != nil {
return nil, fmt.Errorf("failed to convert auditAnnotation '%s' expression: %w", key, err)
}
}
return &EvaluationResult{
Result: outcome,
Message: message,
Index: index,
Error: err,
AuditAnnotations: auditAnnotations,
}, nil
} else if err != nil {
return &EvaluationResult{Error: err}, nil
}
results = append(results, EvaluationResult{
Result: out,
Message: message,
Error: err,
})
}
return results, nil, nil
return &EvaluationResult{Result: true}, nil
}
func (p *compiledPolicy) match(

View file

@ -65,9 +65,9 @@ func (h *handler) admissionResponse(request celengine.EngineRequest, response ce
for _, rule := range policy.Rules {
switch rule.Status() {
case engineapi.RuleStatusFail:
errs = append(errs, fmt.Errorf("Policy %s rule %s failed: %s", policy.Policy.GetName(), rule.Name(), rule.Message()))
errs = append(errs, fmt.Errorf("Policy %s failed: %s", policy.Policy.GetName(), rule.Message()))
case engineapi.RuleStatusError:
errs = append(errs, fmt.Errorf("Policy %s rule %s error: %s", policy.Policy.GetName(), rule.Name(), rule.Message()))
errs = append(errs, fmt.Errorf("Policy %s error: %s", policy.Policy.GetName(), rule.Message()))
}
}
}
@ -75,9 +75,9 @@ func (h *handler) admissionResponse(request celengine.EngineRequest, response ce
for _, rule := range policy.Rules {
switch rule.Status() {
case engineapi.RuleStatusFail:
warnings = append(warnings, fmt.Sprintf("Policy %s rule %s failed: %s", policy.Policy.GetName(), rule.Name(), rule.Message()))
warnings = append(warnings, fmt.Sprintf("Policy %s failed: %s", policy.Policy.GetName(), rule.Message()))
case engineapi.RuleStatusError:
warnings = append(warnings, fmt.Sprintf("Policy %s rule %s error: %s", policy.Policy.GetName(), rule.Name(), rule.Message()))
warnings = append(warnings, fmt.Sprintf("Policy %s error: %s", policy.Policy.GetName(), rule.Message()))
}
}
}

View file

@ -26,4 +26,4 @@ spec:
expect:
- check:
($error): >-
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels rule rule-0 failed: Deployment labels must be env=prod
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels failed: Deployment labels must be env=prod

View file

@ -15,7 +15,6 @@ results:
- message: success
policy: check-deployment-labels
result: pass
rule: rule-0
scored: true
source: KyvernoValidatingPolicy
summary:

View file

@ -18,4 +18,4 @@ spec:
expect:
- check:
($error): >-
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels rule rule-0 failed: Deployment labels must be env=prod
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels failed: Deployment labels must be env=prod

View file

@ -18,4 +18,4 @@ spec:
expect:
- check:
($error): >-
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels rule rule-0 failed: Deployment labels must be env=prod
admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy check-deployment-labels failed: Deployment labels must be env=prod