From a7b62e46996c48b9b03bcad0ef5142c9e9308630 Mon Sep 17 00:00:00 2001 From: Maximilian Rink Date: Fri, 19 Jun 2026 10:35:57 +0200 Subject: [PATCH 1/2] feat: filter scheduled scans by labels --- api/v1alpha2/mondooauditconfig_types.go | 8 + api/v1alpha2/zz_generated.deepcopy.go | 31 ++- .../k8s.mondoo.com_mondooauditconfigs.yaml | 195 +++++++++++++- .../k8s.mondoo.com_mondooauditconfigs.yaml | 237 +++++++++++++++++- .../k8s.mondoo.com_mondooauditconfigs.yaml | 192 ++++++++++++++ .../k8s_v1alpha2_mondooauditconfig.yaml | 28 +++ controllers/container_image/conditions.go | 20 ++ .../container_image/deployment_handler.go | 1 + .../deployment_handler_test.go | 21 ++ controllers/container_image/resources.go | 62 ++++- controllers/container_image/resources_test.go | 90 +++++++ controllers/k8s_scan/conditions.go | 20 ++ controllers/k8s_scan/deployment_handler.go | 2 + .../k8s_scan/deployment_handler_test.go | 53 ++++ controllers/k8s_scan/resources.go | 79 +++++- controllers/k8s_scan/resources_test.go | 118 +++++++++ docs/development.md | 26 ++ docs/user-manual.md | 40 ++- 18 files changed, 1191 insertions(+), 32 deletions(-) diff --git a/api/v1alpha2/mondooauditconfig_types.go b/api/v1alpha2/mondooauditconfig_types.go index f5d02a575..63801968d 100644 --- a/api/v1alpha2/mondooauditconfig_types.go +++ b/api/v1alpha2/mondooauditconfig_types.go @@ -50,6 +50,14 @@ type MondooAuditConfigSpec struct { type Filtering struct { Namespaces FilteringSpec `json:"namespaces,omitempty"` + // NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + // It is evaluated in addition to namespace include/exclude filtering. + // +optional + NamespaceLabelSelector *metav1.LabelSelector `json:"namespaceLabelSelector,omitempty"` + // ObjectLabelSelector selects Kubernetes objects by their own labels. + // It is passed to cnspec Kubernetes discovery for scheduled scans. + // +optional + ObjectLabelSelector *metav1.LabelSelector `json:"objectLabelSelector,omitempty"` } type FilteringSpec struct { diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index e9ab44c45..98c2d2614 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -8,7 +8,8 @@ package v1alpha2 import ( - "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -48,7 +49,7 @@ func (in *Containers) DeepCopyInto(out *Containers) { in.Resources.DeepCopyInto(&out.Resources) if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -135,7 +136,7 @@ func (in *ExternalCluster) DeepCopyInto(out *ExternalCluster) { *out = *in if in.KubeconfigSecretRef != nil { in, out := &in.KubeconfigSecretRef, &out.KubeconfigSecretRef - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } if in.ServiceAccountAuth != nil { @@ -165,7 +166,7 @@ func (in *ExternalCluster) DeepCopyInto(out *ExternalCluster) { } if in.PrivateRegistriesPullSecretRef != nil { in, out := &in.PrivateRegistriesPullSecretRef, &out.PrivateRegistriesPullSecretRef - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } } @@ -184,6 +185,16 @@ func (in *ExternalCluster) DeepCopy() *ExternalCluster { func (in *Filtering) DeepCopyInto(out *Filtering) { *out = *in in.Namespaces.DeepCopyInto(&out.Namespaces) + if in.NamespaceLabelSelector != nil { + in, out := &in.NamespaceLabelSelector, &out.NamespaceLabelSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.ObjectLabelSelector != nil { + in, out := &in.ObjectLabelSelector, &out.ObjectLabelSelector + *out = new(v1.LabelSelector) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Filtering. @@ -549,7 +560,7 @@ func (in *MondooOperatorConfigSpec) DeepCopyInto(out *MondooOperatorConfigSpec) } if in.ImagePullSecrets != nil { in, out := &in.ImagePullSecrets, &out.ImagePullSecrets - *out = make([]v1.LocalObjectReference, len(*in)) + *out = make([]corev1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.ImageRegistry != nil { @@ -604,7 +615,7 @@ func (in *Nodes) DeepCopyInto(out *Nodes) { in.Resources.DeepCopyInto(&out.Resources) if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -672,12 +683,12 @@ func (in *Scanner) DeepCopyInto(out *Scanner) { out.PrivateRegistriesPullSecretRef = in.PrivateRegistriesPullSecretRef if in.PrivateRegistriesPullSecretRefs != nil { in, out := &in.PrivateRegistriesPullSecretRefs, &out.PrivateRegistriesPullSecretRefs - *out = make([]v1.LocalObjectReference, len(*in)) + *out = make([]corev1.LocalObjectReference, len(*in)) copy(*out, *in) } if in.Env != nil { in, out := &in.Env, &out.Env - *out = make([]v1.EnvVar, len(*in)) + *out = make([]corev1.EnvVar, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -715,12 +726,12 @@ func (in *VaultAuthConfig) DeepCopyInto(out *VaultAuthConfig) { *out = *in if in.CACertSecretRef != nil { in, out := &in.CACertSecretRef, &out.CACertSecretRef - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } if in.TargetCACertSecretRef != nil { in, out := &in.TargetCACertSecretRef, &out.TargetCACertSecretRef - *out = new(v1.LocalObjectReference) + *out = new(corev1.LocalObjectReference) **out = **in } } diff --git a/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml index a23412324..86288dac7 100644 --- a/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml +++ b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml @@ -443,6 +443,54 @@ spec: type: object filtering: properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -461,6 +509,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubernetesResources: properties: @@ -486,8 +582,57 @@ spec: filtering: description: |- Filtering allows namespace filtering specific to this external cluster. - If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. + If omitted, the external cluster inherits the global filtering from MondooAuditConfigSpec.Filtering. + Set an empty filtering object to scan all namespaces for this external cluster even when global filtering is configured. properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -506,6 +651,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubeconfigSecretRef: description: |- diff --git a/charts/mondoo-operator/files/crds/k8s.mondoo.com_mondooauditconfigs.yaml b/charts/mondoo-operator/files/crds/k8s.mondoo.com_mondooauditconfigs.yaml index 0ad3a9716..86288dac7 100644 --- a/charts/mondoo-operator/files/crds/k8s.mondoo.com_mondooauditconfigs.yaml +++ b/charts/mondoo-operator/files/crds/k8s.mondoo.com_mondooauditconfigs.yaml @@ -341,6 +341,13 @@ spec: clusterName: description: ClusterName is the AKS cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string loginServer: description: |- LoginServer is the ACR login server URL (e.g., "myregistry.azurecr.io"). @@ -370,6 +377,13 @@ spec: clusterName: description: ClusterName is the EKS cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string region: description: Region is the AWS region. type: string @@ -395,6 +409,13 @@ spec: clusterName: description: ClusterName is the GKE cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string googleServiceAccount: description: |- GoogleServiceAccount is the Google service account to impersonate. @@ -422,6 +443,54 @@ spec: type: object filtering: properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -440,6 +509,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubernetesResources: properties: @@ -465,8 +582,57 @@ spec: filtering: description: |- Filtering allows namespace filtering specific to this external cluster. - If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. + If omitted, the external cluster inherits the global filtering from MondooAuditConfigSpec.Filtering. + Set an empty filtering object to scan all namespaces for this external cluster even when global filtering is configured. properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -485,6 +651,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubeconfigSecretRef: description: |- @@ -708,6 +922,13 @@ spec: clusterName: description: ClusterName is the AKS cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string loginServer: description: |- LoginServer is the ACR login server URL (e.g., "myregistry.azurecr.io"). @@ -737,6 +958,13 @@ spec: clusterName: description: ClusterName is the EKS cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string region: description: Region is the AWS region. type: string @@ -762,6 +990,13 @@ spec: clusterName: description: ClusterName is the GKE cluster name. type: string + endpoint: + description: |- + Endpoint optionally overrides the Kubernetes API server endpoint URL. + When set, the init container uses this URL instead of the auto-discovered endpoint. + Must start with "https://". + pattern: ^https:// + type: string googleServiceAccount: description: |- GoogleServiceAccount is the Google service account to impersonate. diff --git a/config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml b/config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml index a64202493..86288dac7 100644 --- a/config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml +++ b/config/crd/bases/k8s.mondoo.com_mondooauditconfigs.yaml @@ -443,6 +443,54 @@ spec: type: object filtering: properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -461,6 +509,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubernetesResources: properties: @@ -489,6 +585,54 @@ spec: If omitted, the external cluster inherits the global filtering from MondooAuditConfigSpec.Filtering. Set an empty filtering object to scan all namespaces for this external cluster even when global filtering is configured. properties: + namespaceLabelSelector: + description: |- + NamespaceLabelSelector selects Kubernetes namespaces by their own labels. + It is evaluated in addition to namespace include/exclude filtering. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic namespaces: properties: exclude: @@ -507,6 +651,54 @@ spec: type: string type: array type: object + objectLabelSelector: + description: |- + ObjectLabelSelector selects Kubernetes objects by their own labels. + It is passed to cnspec Kubernetes discovery for scheduled scans. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic type: object kubeconfigSecretRef: description: |- diff --git a/config/samples/k8s_v1alpha2_mondooauditconfig.yaml b/config/samples/k8s_v1alpha2_mondooauditconfig.yaml index 311beac75..5e67667f2 100644 --- a/config/samples/k8s_v1alpha2_mondooauditconfig.yaml +++ b/config/samples/k8s_v1alpha2_mondooauditconfig.yaml @@ -85,6 +85,19 @@ spec: # # namespaces: # # exclude: # # - kube-system + # # namespaceLabelSelector: + # # matchLabels: + # # tenant: team-a + # # matchExpressions: + # # - key: environment + # # operator: In + # # values: ["prod", "stage"] + # # objectLabelSelector: + # # matchLabels: + # # app: frontend + # # matchExpressions: + # # - key: scan.mondoo.com/disabled + # # operator: DoesNotExist # # 2. Service Account Token (simpler alternative): # externalClusters: @@ -181,3 +194,18 @@ spec: # include: # - default # - production + # Select namespaces by label for scheduled scans. + # namespaceLabelSelector: + # matchLabels: + # tenant: team-a + # matchExpressions: + # - key: environment + # operator: In + # values: ["prod", "stage"] + # Select Kubernetes objects by label for scheduled scans. + # objectLabelSelector: + # matchLabels: + # app: frontend + # matchExpressions: + # - key: scan.mondoo.com/disabled + # operator: DoesNotExist diff --git a/controllers/container_image/conditions.go b/controllers/container_image/conditions.go index 37c1dab53..dfe1bfa66 100644 --- a/controllers/container_image/conditions.go +++ b/controllers/container_image/conditions.go @@ -4,6 +4,8 @@ package container_image import ( + "errors" + "go.mondoo.com/mondoo-operator/api/v1alpha2" "go.mondoo.com/mondoo-operator/pkg/utils/k8s" "go.mondoo.com/mondoo-operator/pkg/utils/mondoo" @@ -53,3 +55,21 @@ func updateImageScanningConditions(config *v1alpha2.MondooAuditConfig, degradedS config.Status.Conditions = mondoo.SetMondooAuditCondition( config.Status.Conditions, v1alpha2.K8sContainerImageScanningDegraded, status, reason, msg, updateCheck, affectedPods, memoryLimit) } + +func updateImageScanningConfigErrorCondition(config *v1alpha2.MondooAuditConfig, err error) { + reason := "KubernetesContainerImageScanConfigInvalid" + var labelSelectorErr invalidLabelSelectorError + if errors.As(err, &labelSelectorErr) { + reason = "InvalidLabelSelector" + } + config.Status.Conditions = mondoo.SetMondooAuditCondition( + config.Status.Conditions, + v1alpha2.K8sContainerImageScanningDegraded, + corev1.ConditionTrue, + reason, + err.Error(), + mondoo.UpdateConditionIfReasonOrMessageChange, + []string{}, + "", + ) +} diff --git a/controllers/container_image/deployment_handler.go b/controllers/container_image/deployment_handler.go index 786072256..a6b2fd1a6 100644 --- a/controllers/container_image/deployment_handler.go +++ b/controllers/container_image/deployment_handler.go @@ -152,6 +152,7 @@ func (n *DeploymentHandler) syncConfigMap(ctx context.Context, clusterUid string desired, err := ConfigMap(integrationMrn, clusterUid, *n.Mondoo, *n.MondooOperatorConfig) if err != nil { logger.Error(err, "failed to generate desired ConfigMap with inventory") + updateImageScanningConfigErrorCondition(n.Mondoo, err) return err } diff --git a/controllers/container_image/deployment_handler_test.go b/controllers/container_image/deployment_handler_test.go index 9d7fc52de..34eb6848a 100644 --- a/controllers/container_image/deployment_handler_test.go +++ b/controllers/container_image/deployment_handler_test.go @@ -380,6 +380,27 @@ func (s *DeploymentHandlerSuite) TestReconcile_K8sContainerImageScanningStatus() s.Equal(corev1.ConditionFalse, condition.Status) } +func (s *DeploymentHandlerSuite) TestReconcile_InvalidLabelSelectorCondition() { + s.auditConfig.Spec.Filtering.ObjectLabelSelector = &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "track", Operator: metav1.LabelSelectorOpIn}, + }, + } + d := s.createDeploymentHandler() + s.NoError(d.KubeClient.Create(s.ctx, &s.auditConfig)) + + result, err := d.Reconcile(s.ctx) + s.Error(err) + s.True(result.IsZero()) + + s.Require().Len(d.Mondoo.Status.Conditions, 1) + condition := d.Mondoo.Status.Conditions[0] + s.Equal("InvalidLabelSelector", condition.Reason) + s.Contains(condition.Message, "filtering.objectLabelSelector") + s.Equal(corev1.ConditionTrue, condition.Status) + s.Equal(mondoov1alpha2.K8sContainerImageScanningDegraded, condition.Type) +} + func (s *DeploymentHandlerSuite) TestReconcile_DisableContainerImageScanning() { d := s.createDeploymentHandler() mondooAuditConfig := &s.auditConfig diff --git a/controllers/container_image/resources.go b/controllers/container_image/resources.go index a27031a75..ea0d50b08 100644 --- a/controllers/container_image/resources.go +++ b/controllers/container_image/resources.go @@ -29,8 +29,24 @@ const ( CronJobNameSuffix = "-containers-scan" InventoryConfigMapBase = "-containers-inventory" + + k8sOptionNamespaceLabelSelector = "namespace-label-selector" + k8sOptionObjectLabelSelector = "object-label-selector" ) +type invalidLabelSelectorError struct { + field string + err error +} + +func (e invalidLabelSelectorError) Error() string { + return e.err.Error() +} + +func (e invalidLabelSelectorError) Unwrap() error { + return e.err +} + func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) *batchv1.CronJob { ls := CronJobLabels(*m) @@ -253,6 +269,12 @@ func ConfigMapName(prefix string) string { } func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) (string, error) { + options, err := inventoryOptions(m.Spec.Filtering) + if err != nil { + return "", err + } + options["disable-cache"] = "false" + inv := &inventory.Inventory{ Metadata: &inventory.ObjectMeta{ Name: "mondoo-k8s-containers-inventory", @@ -262,12 +284,8 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, { Connections: []*inventory.Config{ { - Type: "k8s", - Options: map[string]string{ - "namespaces": strings.Join(m.Spec.Filtering.Namespaces.Include, ","), - "namespaces-exclude": strings.Join(m.Spec.Filtering.Namespaces.Exclude, ","), - "disable-cache": "false", - }, + Type: "k8s", + Options: options, Discover: &inventory.Discovery{ Targets: []string{"container-images"}, }, @@ -313,6 +331,38 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, return string(invBytes), nil } +func inventoryOptions(filtering v1alpha2.Filtering) (map[string]string, error) { + options := map[string]string{ + "namespaces": strings.Join(filtering.Namespaces.Include, ","), + "namespaces-exclude": strings.Join(filtering.Namespaces.Exclude, ","), + } + if err := addLabelSelectorOption(options, k8sOptionNamespaceLabelSelector, "namespaceLabelSelector", filtering.NamespaceLabelSelector); err != nil { + return nil, err + } + if err := addLabelSelectorOption(options, k8sOptionObjectLabelSelector, "objectLabelSelector", filtering.ObjectLabelSelector); err != nil { + return nil, err + } + return options, nil +} + +func addLabelSelectorOption(options map[string]string, optionName, fieldName string, selector *metav1.LabelSelector) error { + if selector == nil { + return nil + } + parsed, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + return invalidLabelSelectorError{ + field: fieldName, + err: fmt.Errorf("invalid filtering.%s: %w", fieldName, err), + } + } + if parsed.Empty() { + return nil + } + options[optionName] = parsed.String() + return nil +} + // WIFServiceAccountName returns the name for the container registry WIF ServiceAccount func WIFServiceAccountName(prefix string) string { return fmt.Sprintf("%s-cr-wif", prefix) diff --git a/controllers/container_image/resources_test.go b/controllers/container_image/resources_test.go index f88876d5f..ead762ffe 100644 --- a/controllers/container_image/resources_test.go +++ b/controllers/container_image/resources_test.go @@ -16,6 +16,7 @@ import ( "go.mondoo.com/mondoo-operator/api/v1alpha2" "go.mondoo.com/mql/v13/providers-sdk/v1/inventory" + "k8s.io/apimachinery/pkg/labels" ) const testClusterUID = "abcdefg" @@ -205,6 +206,95 @@ func TestInventory_WithoutContainerProxy(t *testing.T) { assert.False(t, hasContainerProxy) } +func TestInventory_WithLabelSelectors(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tenant": "team-a"}, + }, + ObjectLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "api"}, + }, + }, + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + options := inv.Spec.Assets[0].Connections[0].Options + assert.Equal(t, "tenant=team-a", options[k8sOptionNamespaceLabelSelector]) + assert.Equal(t, "app=api", options[k8sOptionObjectLabelSelector]) +} + +func TestInventory_WithLabelSelectorMatchExpressions(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tenant", Operator: metav1.LabelSelectorOpIn, Values: []string{"team-a", "team-b"}}, + {Key: "scan.mondoo.com/disabled", Operator: metav1.LabelSelectorOpDoesNotExist}, + }, + }, + ObjectLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "track", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"legacy"}}, + }, + }, + }, + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + options := inv.Spec.Assets[0].Connections[0].Options + namespaceSelector, err := labels.Parse(options[k8sOptionNamespaceLabelSelector]) + require.NoError(t, err) + assert.True(t, namespaceSelector.Matches(labels.Set{"tenant": "team-a"})) + assert.False(t, namespaceSelector.Matches(labels.Set{"tenant": "team-c"})) + assert.False(t, namespaceSelector.Matches(labels.Set{"tenant": "team-a", "scan.mondoo.com/disabled": "true"})) + + objectSelector, err := labels.Parse(options[k8sOptionObjectLabelSelector]) + require.NoError(t, err) + assert.True(t, objectSelector.Matches(labels.Set{"app": "frontend", "track": "stable"})) + assert.False(t, objectSelector.Matches(labels.Set{"app": "frontend", "track": "legacy"})) + assert.False(t, objectSelector.Matches(labels.Set{"app": "backend", "track": "stable"})) +} + +func TestInventory_InvalidLabelSelector(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + ObjectLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "track", Operator: metav1.LabelSelectorOpIn}, + }, + }, + }, + }, + } + + _, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + + require.Error(t, err) + assert.Contains(t, err.Error(), "filtering.objectLabelSelector") +} + func TestCronJob_WIF_GKE(t *testing.T) { m := testAuditConfig() m.Spec.Containers.WorkloadIdentity = &v1alpha2.WorkloadIdentityConfig{ diff --git a/controllers/k8s_scan/conditions.go b/controllers/k8s_scan/conditions.go index 91a30fc79..30d4c16d6 100644 --- a/controllers/k8s_scan/conditions.go +++ b/controllers/k8s_scan/conditions.go @@ -4,6 +4,8 @@ package k8s_scan import ( + "errors" + "go.mondoo.com/mondoo-operator/api/v1alpha2" "go.mondoo.com/mondoo-operator/pkg/utils/k8s" "go.mondoo.com/mondoo-operator/pkg/utils/mondoo" @@ -53,3 +55,21 @@ func updateWorkloadsConditions(config *v1alpha2.MondooAuditConfig, degradedStatu config.Status.Conditions = mondoo.SetMondooAuditCondition( config.Status.Conditions, v1alpha2.K8sResourcesScanningDegraded, status, reason, msg, updateCheck, affectedPods, memoryLimit) } + +func updateWorkloadsConfigErrorCondition(config *v1alpha2.MondooAuditConfig, err error) { + reason := "KubernetesResourcesScanConfigInvalid" + var labelSelectorErr invalidLabelSelectorError + if errors.As(err, &labelSelectorErr) { + reason = "InvalidLabelSelector" + } + config.Status.Conditions = mondoo.SetMondooAuditCondition( + config.Status.Conditions, + v1alpha2.K8sResourcesScanningDegraded, + corev1.ConditionTrue, + reason, + err.Error(), + mondoo.UpdateConditionIfReasonOrMessageChange, + []string{}, + "", + ) +} diff --git a/controllers/k8s_scan/deployment_handler.go b/controllers/k8s_scan/deployment_handler.go index b59d1f2a9..650d57e15 100644 --- a/controllers/k8s_scan/deployment_handler.go +++ b/controllers/k8s_scan/deployment_handler.go @@ -269,6 +269,7 @@ func (n *DeploymentHandler) syncConfigMap(ctx context.Context, integrationMrn, c desired, err := ConfigMap(integrationMrn, clusterUid, *n.Mondoo, *n.MondooOperatorConfig) if err != nil { logger.Error(err, "failed to generate desired ConfigMap with inventory") + updateWorkloadsConfigErrorCondition(n.Mondoo, err) return err } @@ -361,6 +362,7 @@ func (n *DeploymentHandler) syncExternalClusterConfigMap(ctx context.Context, in desired, err := ExternalClusterConfigMap(integrationMrn, clusterUid, cluster, *n.Mondoo, *n.MondooOperatorConfig) if err != nil { logger.Error(err, "failed to generate desired ConfigMap for external cluster", "cluster", cluster.Name) + updateWorkloadsConfigErrorCondition(n.Mondoo, err) return err } diff --git a/controllers/k8s_scan/deployment_handler_test.go b/controllers/k8s_scan/deployment_handler_test.go index 8d4f450e3..79f922c3c 100644 --- a/controllers/k8s_scan/deployment_handler_test.go +++ b/controllers/k8s_scan/deployment_handler_test.go @@ -210,6 +210,59 @@ func (s *DeploymentHandlerSuite) TestReconcile_K8sResourceScanningStatus() { s.Equal(corev1.ConditionFalse, condition.Status) } +func (s *DeploymentHandlerSuite) TestReconcile_InvalidLabelSelectorCondition() { + s.auditConfig.Spec.Filtering.NamespaceLabelSelector = &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tenant", Operator: metav1.LabelSelectorOpIn}, + }, + } + d := s.createDeploymentHandler() + s.NoError(d.KubeClient.Create(s.ctx, &s.auditConfig)) + + result, err := d.Reconcile(s.ctx) + s.Error(err) + s.True(result.IsZero()) + + s.Require().Len(d.Mondoo.Status.Conditions, 1) + condition := d.Mondoo.Status.Conditions[0] + s.Equal("InvalidLabelSelector", condition.Reason) + s.Contains(condition.Message, "filtering.namespaceLabelSelector") + s.Equal(corev1.ConditionTrue, condition.Status) + s.Equal(mondoov1alpha2.K8sResourcesScanningDegraded, condition.Type) +} + +func (s *DeploymentHandlerSuite) TestReconcile_ExternalClusterInvalidLabelSelectorCondition() { + s.auditConfig.Spec.KubernetesResources.Enable = false + s.auditConfig.Spec.KubernetesResources.ExternalClusters = []mondoov1alpha2.ExternalCluster{ + { + Name: "external", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "external-kubeconfig", + }, + Filtering: &mondoov1alpha2.Filtering{ + ObjectLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "app", Operator: metav1.LabelSelectorOpIn}, + }, + }, + }, + }, + } + d := s.createDeploymentHandler() + s.NoError(d.KubeClient.Create(s.ctx, &s.auditConfig)) + + result, err := d.Reconcile(s.ctx) + s.Error(err) + s.True(result.IsZero()) + + s.Require().Len(d.Mondoo.Status.Conditions, 1) + condition := d.Mondoo.Status.Conditions[0] + s.Equal("InvalidLabelSelector", condition.Reason) + s.Contains(condition.Message, "filtering.objectLabelSelector") + s.Equal(corev1.ConditionTrue, condition.Status) + s.Equal(mondoov1alpha2.K8sResourcesScanningDegraded, condition.Type) +} + func (s *DeploymentHandlerSuite) TestReconcile_Disable() { d := s.createDeploymentHandler() mondooAuditConfig := &s.auditConfig diff --git a/controllers/k8s_scan/resources.go b/controllers/k8s_scan/resources.go index f22bf3bd4..481886847 100644 --- a/controllers/k8s_scan/resources.go +++ b/controllers/k8s_scan/resources.go @@ -26,10 +26,25 @@ import ( ) const ( - CronJobNameSuffix = "-k8s-scan" - InventoryConfigMapBase = "-k8s-inventory" + CronJobNameSuffix = "-k8s-scan" + InventoryConfigMapBase = "-k8s-inventory" + k8sOptionNamespaceLabelSelector = "namespace-label-selector" + k8sOptionObjectLabelSelector = "object-label-selector" ) +type invalidLabelSelectorError struct { + field string + err error +} + +func (e invalidLabelSelectorError) Error() string { + return e.err.Error() +} + +func (e invalidLabelSelectorError) Unwrap() error { + return e.err +} + // K8sDiscoveryTargets defines explicit targets for K8s resource scanning // (excludes container-images which is handled by the separate containers controller) var K8sDiscoveryTargets = []string{ @@ -1014,6 +1029,11 @@ func ExternalClusterConfigMap(integrationMRN, operatorClusterUID string, cluster } func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) (string, error) { + options, err := inventoryOptions(m.Spec.Filtering) + if err != nil { + return "", err + } + inv := &inventory.Inventory{ Metadata: &inventory.ObjectMeta{ Name: "mondoo-k8s-resources-inventory", @@ -1023,11 +1043,8 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, { Connections: []*inventory.Config{ { - Type: "k8s", - Options: map[string]string{ - "namespaces": strings.Join(m.Spec.Filtering.Namespaces.Include, ","), - "namespaces-exclude": strings.Join(m.Spec.Filtering.Namespaces.Exclude, ","), - }, + Type: "k8s", + Options: options, Discover: &inventory.Discovery{ Targets: K8sDiscoveryTargets, }, @@ -1075,6 +1092,11 @@ func Inventory(integrationMRN, clusterUID string, m v1alpha2.MondooAuditConfig, func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster v1alpha2.ExternalCluster, m v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOperatorConfig) (string, error) { filtering := externalClusterFiltering(cluster, m) + options, err := inventoryOptions(filtering) + if err != nil { + return "", err + } + options["disable-cache"] = "false" // Determine discovery targets based on whether container image scanning is enabled // Make a copy to avoid mutating the shared slice @@ -1093,12 +1115,8 @@ func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster { Connections: []*inventory.Config{ { - Type: "k8s", - Options: map[string]string{ - "namespaces": strings.Join(filtering.Namespaces.Include, ","), - "namespaces-exclude": strings.Join(filtering.Namespaces.Exclude, ","), - "disable-cache": "false", - }, + Type: "k8s", + Options: options, Discover: &inventory.Discovery{ Targets: targets, }, @@ -1155,6 +1173,41 @@ func externalClusterFiltering(cluster v1alpha2.ExternalCluster, m v1alpha2.Mondo return m.Spec.Filtering } +func inventoryOptions(filtering v1alpha2.Filtering) (map[string]string, error) { + options := map[string]string{ + "namespaces": strings.Join(filtering.Namespaces.Include, ","), + "namespaces-exclude": strings.Join(filtering.Namespaces.Exclude, ","), + } + + if err := addLabelSelectorOption(options, k8sOptionNamespaceLabelSelector, "namespaceLabelSelector", filtering.NamespaceLabelSelector); err != nil { + return nil, err + } + if err := addLabelSelectorOption(options, k8sOptionObjectLabelSelector, "objectLabelSelector", filtering.ObjectLabelSelector); err != nil { + return nil, err + } + + return options, nil +} + +func addLabelSelectorOption(options map[string]string, optionName, fieldName string, selector *metav1.LabelSelector) error { + if selector == nil { + return nil + } + + parsedSelector, err := metav1.LabelSelectorAsSelector(selector) + if err != nil { + return invalidLabelSelectorError{ + field: fieldName, + err: fmt.Errorf("invalid filtering.%s: %w", fieldName, err), + } + } + if !parsedSelector.Empty() { + options[optionName] = parsedSelector.String() + } + + return nil +} + func buildEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { envVars := feature_flags.AllFeatureFlagsAsEnv() diff --git a/controllers/k8s_scan/resources_test.go b/controllers/k8s_scan/resources_test.go index a2342a289..4cc500586 100644 --- a/controllers/k8s_scan/resources_test.go +++ b/controllers/k8s_scan/resources_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" @@ -359,6 +360,94 @@ func TestInventory_WithoutContainerProxy(t *testing.T) { assert.False(t, hasContainerProxy) } +func TestInventory_WithLabelSelectors(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tenant": "team-a"}, + }, + ObjectLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "api"}, + }, + }, + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + options := inv.Spec.Assets[0].Connections[0].Options + assert.Equal(t, "tenant=team-a", options[k8sOptionNamespaceLabelSelector]) + assert.Equal(t, "app=api", options[k8sOptionObjectLabelSelector]) +} + +func TestInventory_WithLabelSelectorMatchExpressions(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tenant", Operator: metav1.LabelSelectorOpIn, Values: []string{"team-a", "team-b"}}, + {Key: "scan.mondoo.com/disabled", Operator: metav1.LabelSelectorOpDoesNotExist}, + }, + }, + ObjectLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "frontend"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "track", Operator: metav1.LabelSelectorOpNotIn, Values: []string{"legacy"}}, + }, + }, + }, + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + options := inv.Spec.Assets[0].Connections[0].Options + namespaceSelector, err := labels.Parse(options[k8sOptionNamespaceLabelSelector]) + require.NoError(t, err) + assert.True(t, namespaceSelector.Matches(labels.Set{"tenant": "team-a"})) + assert.False(t, namespaceSelector.Matches(labels.Set{"tenant": "team-c"})) + assert.False(t, namespaceSelector.Matches(labels.Set{"tenant": "team-a", "scan.mondoo.com/disabled": "true"})) + + objectSelector, err := labels.Parse(options[k8sOptionObjectLabelSelector]) + require.NoError(t, err) + assert.True(t, objectSelector.Matches(labels.Set{"app": "frontend", "track": "stable"})) + assert.False(t, objectSelector.Matches(labels.Set{"app": "frontend", "track": "legacy"})) + assert.False(t, objectSelector.Matches(labels.Set{"app": "backend", "track": "stable"})) +} + +func TestInventory_InvalidLabelSelector(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + Spec: v1alpha2.MondooAuditConfigSpec{ + Filtering: v1alpha2.Filtering{ + ObjectLabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "app", Operator: metav1.LabelSelectorOperator("Invalid")}, + }, + }, + }, + }, + } + + _, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "filtering.objectLabelSelector") +} + func TestExternalClusterInventory_WithContainerProxy(t *testing.T) { auditConfig := v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, @@ -394,6 +483,35 @@ func externalClusterInventoryOptions(t *testing.T, auditConfig v1alpha2.MondooAu return inv.Spec.Assets[0].Connections[0].Options } +func TestExternalClusterInventory_WithLabelSelectors(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cluster := v1alpha2.ExternalCluster{ + Name: "remote-cluster", + Filtering: &v1alpha2.Filtering{ + NamespaceLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"tenant": "team-b"}, + }, + ObjectLabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "worker"}, + }, + }, + } + + invStr, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + options := inv.Spec.Assets[0].Connections[0].Options + assert.Equal(t, "tenant=team-b", options[k8sOptionNamespaceLabelSelector]) + assert.Equal(t, "app=worker", options[k8sOptionObjectLabelSelector]) + assert.Equal(t, "false", options["disable-cache"]) +} + // envToMap converts a slice of EnvVar to a map for easy lookup. func envToMap(envVars []corev1.EnvVar) map[string]string { m := make(map[string]string, len(envVars)) diff --git a/docs/development.md b/docs/development.md index 09303963d..7791e4e5b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -403,6 +403,19 @@ externalClusters: namespaces: exclude: - kube-system + namespaceLabelSelector: + matchLabels: + tenant: team-a + matchExpressions: + - key: environment + operator: In + values: ["prod", "stage"] + objectLabelSelector: + matchLabels: + app: frontend + matchExpressions: + - key: scan.mondoo.com/disabled + operator: DoesNotExist # Enable container image scanning for this external cluster containerImageScanning: true # Reference to private registry credentials for this cluster @@ -453,6 +466,19 @@ spec: # include: # - production # - staging + namespaceLabelSelector: + matchLabels: + tenant: team-a + matchExpressions: + - key: environment + operator: In + values: ["prod", "stage"] + objectLabelSelector: + matchLabels: + app: frontend + matchExpressions: + - key: scan.mondoo.com/disabled + operator: DoesNotExist ``` Note: If both `include` and `exclude` are specified, only `include` is used. diff --git a/docs/user-manual.md b/docs/user-manual.md index 3fec86221..3f0f75a92 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -223,6 +223,31 @@ spec: - ... ``` +You can also select namespaces and Kubernetes objects for scheduled scans with Kubernetes label selectors: + +```yaml +spec: + filtering: + namespaceLabelSelector: + matchLabels: + tenant: team-a + matchExpressions: + - key: environment + operator: In + values: ["prod", "stage"] + objectLabelSelector: + matchLabels: + app: frontend + matchExpressions: + - key: scan.mondoo.com/disabled + operator: DoesNotExist +``` + +The operator passes these selectors to cnspec Kubernetes discovery as +`namespace-label-selector` and `object-label-selector` scan options. Use an +operator build with a cnspec Kubernetes provider that supports those options; +older scanner images ignore them and will scan the broader configured scope. + ## Scanning External Clusters The Mondoo Operator can scan remote Kubernetes clusters from a central installation. This is useful for: @@ -295,6 +320,19 @@ externalClusters: exclude: - kube-system - monitoring + namespaceLabelSelector: + matchLabels: + tenant: team-a + matchExpressions: + - key: environment + operator: In + values: ["prod", "stage"] + objectLabelSelector: + matchLabels: + app: frontend + matchExpressions: + - key: scan.mondoo.com/disabled + operator: DoesNotExist # Enable container image scanning for this cluster containerImageScanning: true # Private registry credentials for this cluster @@ -309,7 +347,7 @@ externalClusters: | `name` | Unique identifier for the cluster (used in CronJob names) | | `kubeconfigSecretRef` | Reference to Secret containing kubeconfig | | `schedule` | Override the default scan schedule (cron format) | -| `filtering` | Namespace include/exclude specific to this cluster | +| `filtering` | Namespace include/exclude and label-selector filtering specific to this cluster | | `containerImageScanning` | Enable container image scanning for this cluster | | `privateRegistriesPullSecretRef` | Registry credentials for private images | From 0b16a53c9cdc7735813091cd0130fe6fd1637b63 Mon Sep 17 00:00:00 2001 From: Maximilian Rink Date: Fri, 19 Jun 2026 10:35:57 +0200 Subject: [PATCH 2/2] ci: isolate link-check configuration --- .github/actions/link-check/config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/link-check/config.json b/.github/actions/link-check/config.json index c9b016b75..b9891da4a 100644 --- a/.github/actions/link-check/config.json +++ b/.github/actions/link-check/config.json @@ -1,3 +1,3 @@ { - "aliveStatusCodes": [429, 200, 406] -} \ No newline at end of file + "aliveStatusCodes": [200, 406, 429] +}