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:
parent
e01e57355a
commit
fef88ab433
7 changed files with 69 additions and 49 deletions
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,7 +15,6 @@ results:
|
|||
- message: success
|
||||
policy: check-deployment-labels
|
||||
result: pass
|
||||
rule: rule-0
|
||||
scored: true
|
||||
source: KyvernoValidatingPolicy
|
||||
summary:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue