1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-28 02:18:15 +00:00

feat: apply .matchConditions when generating reports (#9599)

* enable matchconditions for reports

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* update

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* add chainsaw tests

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* fix: linter issues

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* chore: move files

Signed-off-by: ShutingZhao <shuting@nirmata.com>

---------

Signed-off-by: ShutingZhao <shuting@nirmata.com>
This commit is contained in:
shuting 2024-02-02 16:32:28 +08:00 committed by GitHub
parent c6d4f33ae9
commit 5f0d53fe34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 372 additions and 10 deletions

View file

@ -91,7 +91,7 @@ func (e *engine) Validate(
startTime := time.Now()
response := engineapi.NewEngineResponseFromPolicyContext(policyContext)
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.validate"), policyContext)
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
policyResponse := e.validate(ctx, logger, policyContext)
response = response.WithPolicyResponse(policyResponse)
}
@ -107,7 +107,7 @@ func (e *engine) Mutate(
startTime := time.Now()
response := engineapi.NewEngineResponseFromPolicyContext(policyContext)
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.mutate"), policyContext)
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
policyResponse, patchedResource := e.mutate(ctx, logger, policyContext)
response = response.
WithPolicyResponse(policyResponse).
@ -125,7 +125,7 @@ func (e *engine) Generate(
startTime := time.Now()
response := engineapi.NewEngineResponseFromPolicyContext(policyContext)
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.generate"), policyContext)
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
policyResponse := e.generateResponse(ctx, logger, policyContext)
response = response.WithPolicyResponse(policyResponse)
}
@ -142,7 +142,7 @@ func (e *engine) VerifyAndPatchImages(
response := engineapi.NewEngineResponseFromPolicyContext(policyContext)
ivm := engineapi.ImageVerificationMetadata{}
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.verify"), policyContext)
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
policyResponse, patchedResource, innerIvm := e.verifyAndPatchImages(ctx, logger, policyContext)
response, ivm = response.
WithPolicyResponse(policyResponse).
@ -160,7 +160,7 @@ func (e *engine) ApplyBackgroundChecks(
startTime := time.Now()
response := engineapi.NewEngineResponseFromPolicyContext(policyContext)
logger := internal.LoggerWithPolicyContext(logging.WithName("engine.background"), policyContext)
if internal.MatchPolicyContext(logger, policyContext, e.configuration) {
if internal.MatchPolicyContext(logger, e.client, policyContext, e.configuration) {
policyResponse := e.applyBackgroundChecks(ctx, logger, policyContext)
response = response.WithPolicyResponse(policyResponse)
}

View file

@ -12,6 +12,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/internal"
engineutils "github.com/kyverno/kyverno/pkg/engine/utils"
celutils "github.com/kyverno/kyverno/pkg/utils/cel"
vaputils "github.com/kyverno/kyverno/pkg/validatingadmissionpolicy"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -108,7 +109,7 @@ func (h validateCELHandler) Process(
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
// compile CEL expressions
compiler, err := celutils.NewCompiler(validations, auditAnnotations, matchConditions, variables)
compiler, err := celutils.NewCompiler(validations, auditAnnotations, vaputils.ConvertMatchConditionsV1(matchConditions), variables)
if err != nil {
return resource, handlers.WithError(rule, engineapi.Validation, "Error while creating composited compiler", err)
}

View file

@ -1,15 +1,22 @@
package internal
import (
"context"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
celutils "github.com/kyverno/kyverno/pkg/utils/cel"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
)
func MatchPolicyContext(logger logr.Logger, policyContext engineapi.PolicyContext, configuration config.Configuration) bool {
func MatchPolicyContext(logger logr.Logger, client engineapi.Client, policyContext engineapi.PolicyContext, configuration config.Configuration) bool {
policy := policyContext.Policy()
old := policyContext.OldResource()
new := policyContext.NewResource()
@ -22,6 +29,13 @@ func MatchPolicyContext(logger logr.Logger, policyContext engineapi.PolicyContex
logger.V(2).Info("configuration resource filters doesn't match resource")
return false
}
if policy.GetSpec().GetMatchConditions() != nil {
if !checkMatchConditions(logger, client, policyContext, gvk, subresource) {
logger.V(2).Info("webhookConfiguration.matchConditions doesn't match request")
return false
}
}
return true
}
@ -51,3 +65,33 @@ func checkNamespacedPolicy(policy kyvernov1.PolicyInterface, resources ...unstru
}
return true
}
func checkMatchConditions(logger logr.Logger, client engineapi.Client, policyContext engineapi.PolicyContext, gvk schema.GroupVersionKind, subresource string) bool {
policy := policyContext.Policy()
old := policyContext.OldResource()
new := policyContext.NewResource()
new.SetGroupVersionKind(gvk)
old.SetGroupVersionKind(gvk)
gvr := schema.GroupVersionResource(policyContext.RequestResource())
requestInfo := policyContext.AdmissionInfo().AdmissionUserInfo
userInfo := NewUser(requestInfo.Username, requestInfo.UID, requestInfo.Groups)
admissionAttributes := admission.NewAttributesRecord(new.DeepCopyObject(), old.DeepCopyObject(), gvk, new.GetNamespace(), new.GetName(), gvr, subresource, admission.Operation(policyContext.Operation()), nil, false, &userInfo)
scheme := runtime.NewScheme()
scheme.AddKnownTypes(gvk.GroupVersion())
versionedAttr, err := admission.NewVersionedAttributes(admissionAttributes, admissionAttributes.GetKind(), admission.NewObjectInterfacesFromScheme(scheme))
if err != nil {
logger.Error(err, "error creating versioned attributes")
return false
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}
compiler, err := celutils.NewCompiler(nil, nil, policy.GetSpec().GetMatchConditions(), nil)
if err != nil {
logger.Error(err, "error creating composited compiler")
return false
}
matchConditionFilter := compiler.CompileMatchExpressions(optionalVars)
matcher := matchconditions.NewMatcher(matchConditionFilter, nil, policy.GetKind(), "", policy.GetName())
result := matcher.Match(context.TODO(), versionedAttr, nil, nil)
return result.Matches
}

View file

@ -1,6 +1,7 @@
package cel
import (
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
@ -13,14 +14,14 @@ type Compiler struct {
// CEL expressions
validateExpressions []admissionregistrationv1alpha1.Validation
auditAnnotationExpressions []admissionregistrationv1alpha1.AuditAnnotation
matchExpressions []admissionregistrationv1alpha1.MatchCondition
matchExpressions []admissionregistrationv1.MatchCondition
variables []admissionregistrationv1alpha1.Variable
}
func NewCompiler(
validations []admissionregistrationv1alpha1.Validation,
auditAnnotations []admissionregistrationv1alpha1.AuditAnnotation,
matchConditions []admissionregistrationv1alpha1.MatchCondition,
matchConditions []admissionregistrationv1.MatchCondition,
variables []admissionregistrationv1alpha1.Variable,
) (*Compiler, error) {
compositedCompiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))

View file

@ -1,6 +1,7 @@
package validatingadmissionpolicy
import (
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/api/admissionregistration/v1beta1"
)
@ -37,6 +38,14 @@ func convertMatchConditions(v1alpha1conditions []v1alpha1.MatchCondition) []v1be
return v1beta1conditions
}
func ConvertMatchConditionsV1(v1alpha1conditions []v1alpha1.MatchCondition) []admissionregistrationv1.MatchCondition {
var v1conditions []admissionregistrationv1.MatchCondition
for _, m := range v1alpha1conditions {
v1conditions = append(v1conditions, admissionregistrationv1.MatchCondition(m))
}
return v1conditions
}
func convertVariables(v1alpha1variables []v1alpha1.Variable) []v1beta1.Variable {
var v1beta1variables []v1beta1.Variable
for _, v := range v1alpha1variables {

View file

@ -202,7 +202,8 @@ func validateResource(policy v1alpha1.ValidatingAdmissionPolicy, binding *v1alph
var ruleResp *engineapi.RuleResponse
// compile CEL expressions
compiler, err := celutils.NewCompiler(policy.Spec.Validations, policy.Spec.AuditAnnotations, policy.Spec.MatchConditions, policy.Spec.Variables)
matchConditions := ConvertMatchConditionsV1(policy.Spec.MatchConditions)
compiler, err := celutils.NewCompiler(policy.Spec.Validations, policy.Spec.AuditAnnotations, matchConditions, policy.Spec.Variables)
if err != nil {
return engineResponse, err
}

View file

@ -0,0 +1,12 @@
## Description
This test creates a policy with `matchConditions` and two pods, it then expects a background scan report to be created for the pod in the selected namespace `match-conditions-standard-ns` other than `default`.
## Steps
1. - Create the testing namespace `match-conditions-standard-ns`
1. - Create pods in `match-conditions-standard-ns` and `default` namespaces
1. - Create a cluster policy
- Assert the policy becomes ready
1. - Assert a policy report is created for the pod in `match-conditions-standard-ns`
1. - Assert a policy report is not created for the pod in `default`

View file

@ -0,0 +1,33 @@
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
creationTimestamp: null
name: report-deletion
spec:
steps:
- name: step-00
try:
- apply:
file: ns.yaml
- assert:
file: ns.yaml
- name: step-01
try:
- apply:
file: pod.yaml
- assert:
file: pod.yaml
- name: step-02
try:
- apply:
file: policy.yaml
- assert:
file: policy-assert.yaml
- name: step-03
try:
- assert:
file: report-assert.yaml
- name: step-04
try:
- error:
file: report-error.yaml

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: match-conditions-standard-ns

View file

@ -0,0 +1,18 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-latest-tag
spec:
containers:
- image: nginx:latest
name: pod
---
apiVersion: v1
kind: Pod
metadata:
name: pod-latest-tag
namespace: match-conditions-standard-ns
spec:
containers:
- image: nginx:latest
name: pod

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: cpol-match-conditions-standard
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,40 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
annotations:
pod-policies.kyverno.io/autogen-controllers: none
name: cpol-match-conditions-standard
spec:
admission: true
background: true
webhookConfiguration:
matchConditions:
- name: "select-namespace"
expression: '(object.metadata.namespace == "match-conditions-standard-ns")'
rules:
- match:
any:
- resources:
kinds:
- Pod
name: require-image-tag
validate:
message: An image tag is required
pattern:
spec:
containers:
- image: '*:*'
- match:
any:
- resources:
kinds:
- Pod
name: validate-image-tag
validate:
message: Using a mutable image tag e.g. 'latest' is not allowed
pattern:
spec:
containers:
- image: '!*:latest'
validationFailureAction: Enforce
failurePolicy: Ignore

View file

@ -0,0 +1,18 @@
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
namespace: match-conditions-standard-ns
ownerReferences:
- apiVersion: v1
kind: Pod
name: pod-latest-tag
scope:
apiVersion: v1
kind: Pod
name: pod-latest-tag
summary:
error: 0
fail: 1
pass: 1
skip: 0
warn: 0

View file

@ -0,0 +1,18 @@
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
namespace: default
ownerReferences:
- apiVersion: v1
kind: Pod
name: pod-latest-tag
scope:
apiVersion: v1
kind: Pod
name: pod-latest-tag
summary:
error: 0
fail: 1
pass: 1
skip: 0
warn: 0

View file

@ -0,0 +1,12 @@
## Description
This test creates a policy with `matchConditions` and two pods, it then expects a background scan report to be created for the pod in the selected namespace `match-conditions-standard-ns` other than `default`.
## Steps
1. - Create the testing namespace `match-conditions-standard-ns`
1. - Create pods in `match-conditions-standard-ns` and `default` namespaces
1. - Create a cluster policy
- Assert the policy becomes ready
1. - Assert a policy report is created for the pod in `match-conditions-standard-ns`
1. - Assert a policy report is not created for the pod in `default`

View file

@ -0,0 +1,33 @@
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
creationTimestamp: null
name: report-deletion
spec:
steps:
- name: step-00
try:
- apply:
file: ns.yaml
- assert:
file: ns.yaml
- name: step-01
try:
- apply:
file: pod.yaml
- assert:
file: pod.yaml
- name: step-02
try:
- apply:
file: policy.yaml
- assert:
file: policy-assert.yaml
- name: step-03
try:
- error:
file: report-error-1.yaml
- name: step-04
try:
- error:
file: report-error-2.yaml

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: match-conditions-userinfo-ns

View file

@ -0,0 +1,18 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-latest-tag
spec:
containers:
- image: nginx:latest
name: pod
---
apiVersion: v1
kind: Pod
metadata:
name: pod-latest-tag
namespace: match-conditions-userinfo-ns
spec:
containers:
- image: nginx:latest
name: pod

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: cpol-match-conditions-userinfo
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,42 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
annotations:
pod-policies.kyverno.io/autogen-controllers: none
name: cpol-match-conditions-userinfo
spec:
admission: true
background: true
webhookConfiguration:
matchConditions:
- name: "select-namespace"
expression: '(object.metadata.namespace == "match-conditions-userinfo-ns")'
- name: 'select-by-groups'
expression: '("system:masters" in request.userInfo.groups)'
rules:
- match:
any:
- resources:
kinds:
- Pod
name: require-image-tag
validate:
message: An image tag is required
pattern:
spec:
containers:
- image: '*:*'
- match:
any:
- resources:
kinds:
- Pod
name: validate-image-tag
validate:
message: Using a mutable image tag e.g. 'latest' is not allowed
pattern:
spec:
containers:
- image: '!*:latest'
validationFailureAction: Enforce
failurePolicy: Ignore

View file

@ -0,0 +1,18 @@
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
namespace: match-conditions-userinfo-ns
ownerReferences:
- apiVersion: v1
kind: Pod
name: pod-latest-tag
scope:
apiVersion: v1
kind: Pod
name: pod-latest-tag
summary:
error: 0
fail: 1
pass: 1
skip: 0
warn: 0

View file

@ -0,0 +1,18 @@
apiVersion: wgpolicyk8s.io/v1alpha2
kind: PolicyReport
metadata:
namespace: default
ownerReferences:
- apiVersion: v1
kind: Pod
name: pod-latest-tag
scope:
apiVersion: v1
kind: Pod
name: pod-latest-tag
summary:
error: 0
fail: 1
pass: 1
skip: 0
warn: 0