diff --git a/providers/k8s/resources/applications.go b/providers/k8s/resources/applications.go new file mode 100644 index 0000000000..5cb957f035 --- /dev/null +++ b/providers/k8s/resources/applications.go @@ -0,0 +1,126 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "slices" + "sort" + + "go.mondoo.com/cnquery/v11/llx" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/util/convert" + "go.mondoo.com/cnquery/v11/types" +) + +const ( + // https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ + K8sApplicationName = "app.kubernetes.io/name" + K8sApplicationInstance = "app.kubernetes.io/instance" + K8sApplicationVersion = "app.kubernetes.io/version" + K8sApplicationComponent = "app.kubernetes.io/component" + K8sApplicationPartOf = "app.kubernetes.io/part-of" + K8sApplicationManagedBy = "app.kubernetes.io/managed-by" +) + +func (k *mqlK8s) apps() ([]interface{}, error) { + apps := map[string]k8sapp{} + + // fetch deployment resources + deployments := k.GetDeployments() + if deployments.Error != nil { + return nil, deployments.Error + } + + for i := range deployments.Data { + d := deployments.Data[i].(*mqlK8sDeployment) + labels := d.GetLabels().Data + extractApp(apps, labels) + } + + // fetch daemonset resources + daemonsets := k.GetDaemonsets() + if daemonsets.Error != nil { + return nil, daemonsets.Error + } + + for i := range daemonsets.Data { + d := daemonsets.Data[i].(*mqlK8sDaemonset) + labels := d.GetLabels().Data + extractApp(apps, labels) + } + + // return k8s app list + appList := []interface{}{} + for _, app := range apps { + r, err := CreateResource(k.MqlRuntime, "k8s.app", map[string]*llx.RawData{ + "__id": llx.StringData(app.name + "/" + app.instance), + "name": llx.StringData(app.name), + "version": llx.StringData(app.version), + "instance": llx.StringData(app.instance), + "managedBy": llx.StringData(app.managedBy), + "partOf": llx.StringData(app.partOf), + "components": llx.ArrayData(convert.SliceAnyToInterface(app.components), types.String), + }) + if err != nil { + return nil, err + } + + appList = append(appList, r) + } + + return appList, nil +} + +type k8sapp struct { + name string + version string + instance string + components []string + partOf string + managedBy string +} + +func extractApp(apps map[string]k8sapp, labels map[string]interface{}) { + name, nameOk := labels[K8sApplicationName] + instance, instanceOK := labels[K8sApplicationInstance] + version, versionOK := labels[K8sApplicationVersion] + component, componentOK := labels[K8sApplicationComponent] + partOf, partOfOK := labels[K8sApplicationPartOf] + managedBy, managedByOK := labels[K8sApplicationManagedBy] + + if !nameOk { + // if the name is not set, we cannot create an app + return + } + + app := k8sapp{ + name: name.(string), + } + if instanceOK { + app.instance = instance.(string) + } + if versionOK { + app.version = version.(string) + } + if componentOK { + app.components = []string{component.(string)} + } + if partOfOK { + app.partOf = partOf.(string) + } + if managedByOK { + app.managedBy = managedBy.(string) + } + + key := app.name + app.instance + if existing, ok := apps[key]; ok { + // if the app already exists, we need to merge the components + components := append(existing.components, app.components...) + sort.Strings(components) + components = slices.Compact(components) + existing.components = components + apps[app.name+app.instance] = existing + } else { + apps[app.name+app.instance] = app + } +} diff --git a/providers/k8s/resources/k8s.lr b/providers/k8s/resources/k8s.lr index ee9d08c636..1ec4228eb9 100644 --- a/providers/k8s/resources/k8s.lr +++ b/providers/k8s/resources/k8s.lr @@ -58,6 +58,8 @@ k8s { customresources() []k8s.customresource // Kubernetes admission webhook configurations validatingWebhookConfigurations() []k8s.admission.validatingwebhookconfiguration + // Kubernetes applications + apps() []k8s.app } // Kubernetes API resources @@ -865,6 +867,7 @@ private k8s.userinfo @defaults("username") { uid string } +// Kubernetes Validating Webhook Configuration private k8s.admission.validatingwebhookconfiguration @defaults("name") { // Mondoo ID for the Kubernetes object id string @@ -886,4 +889,20 @@ private k8s.admission.validatingwebhookconfiguration @defaults("name") { manifest() dict // Webhooks configuration webhooks() []dict +} + +// Kubernetes Application +private k8s.app { + // Application name + name string + // Application version + version string + // Application instance + instance string + // Managed by + managedBy string + // Name of the higher-level application + partOf string + // Components + components []string } \ No newline at end of file diff --git a/providers/k8s/resources/k8s.lr.go b/providers/k8s/resources/k8s.lr.go index 9b5927fd43..e5e2e8d800 100644 --- a/providers/k8s/resources/k8s.lr.go +++ b/providers/k8s/resources/k8s.lr.go @@ -162,6 +162,10 @@ func init() { // to override args, implement: initK8sAdmissionValidatingwebhookconfiguration(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) Create: createK8sAdmissionValidatingwebhookconfiguration, }, + "k8s.app": { + // to override args, implement: initK8sApp(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) + Create: createK8sApp, + }, } } @@ -302,6 +306,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "k8s.validatingWebhookConfigurations": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlK8s).GetValidatingWebhookConfigurations()).ToDataRes(types.Array(types.Resource("k8s.admission.validatingwebhookconfiguration"))) }, + "k8s.apps": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8s).GetApps()).ToDataRes(types.Array(types.Resource("k8s.app"))) + }, "k8s.apiresource.name": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlK8sApiresource).GetName()).ToDataRes(types.String) }, @@ -1334,6 +1341,24 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "k8s.admission.validatingwebhookconfiguration.webhooks": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlK8sAdmissionValidatingwebhookconfiguration).GetWebhooks()).ToDataRes(types.Array(types.Dict)) }, + "k8s.app.name": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetName()).ToDataRes(types.String) + }, + "k8s.app.version": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetVersion()).ToDataRes(types.String) + }, + "k8s.app.instance": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetInstance()).ToDataRes(types.String) + }, + "k8s.app.managedBy": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetManagedBy()).ToDataRes(types.String) + }, + "k8s.app.partOf": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetPartOf()).ToDataRes(types.String) + }, + "k8s.app.components": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlK8sApp).GetComponents()).ToDataRes(types.Array(types.String)) + }, } func GetData(resource plugin.Resource, field string, args map[string]*llx.RawData) *plugin.DataRes { @@ -1446,6 +1471,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlK8s).ValidatingWebhookConfigurations, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return }, + "k8s.apps": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8s).Apps, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, "k8s.apiresource.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlK8sApiresource).__id, ok = v.Value.(string) return @@ -2962,6 +2991,34 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlK8sAdmissionValidatingwebhookconfiguration).Webhooks, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) return }, + "k8s.app.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).__id, ok = v.Value.(string) + return + }, + "k8s.app.name": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).Name, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "k8s.app.version": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).Version, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "k8s.app.instance": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).Instance, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "k8s.app.managedBy": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).ManagedBy, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "k8s.app.partOf": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).PartOf, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, + "k8s.app.components": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlK8sApp).Components, ok = plugin.RawToTValue[[]interface{}](v.Value, v.Error) + return + }, } func SetData(resource plugin.Resource, field string, val *llx.RawData) error { @@ -3015,6 +3072,7 @@ type mqlK8s struct { NetworkPolicies plugin.TValue[[]interface{}] Customresources plugin.TValue[[]interface{}] ValidatingWebhookConfigurations plugin.TValue[[]interface{}] + Apps plugin.TValue[[]interface{}] } // createK8s creates a new instance of this resource @@ -3423,6 +3481,22 @@ func (c *mqlK8s) GetValidatingWebhookConfigurations() *plugin.TValue[[]interface }) } +func (c *mqlK8s) GetApps() *plugin.TValue[[]interface{}] { + return plugin.GetOrCompute[[]interface{}](&c.Apps, func() ([]interface{}, error) { + if c.MqlRuntime.HasRecording { + d, err := c.MqlRuntime.FieldResourceFromRecording("k8s", c.__id, "apps") + if err != nil { + return nil, err + } + if d != nil { + return d.Value.([]interface{}), nil + } + } + + return c.apps() + }) +} + // mqlK8sApiresource for the k8s.apiresource resource type mqlK8sApiresource struct { MqlRuntime *plugin.Runtime @@ -7100,3 +7174,72 @@ func (c *mqlK8sAdmissionValidatingwebhookconfiguration) GetWebhooks() *plugin.TV return c.webhooks() }) } + +// mqlK8sApp for the k8s.app resource +type mqlK8sApp struct { + MqlRuntime *plugin.Runtime + __id string + // optional: if you define mqlK8sAppInternal it will be used here + Name plugin.TValue[string] + Version plugin.TValue[string] + Instance plugin.TValue[string] + ManagedBy plugin.TValue[string] + PartOf plugin.TValue[string] + Components plugin.TValue[[]interface{}] +} + +// createK8sApp creates a new instance of this resource +func createK8sApp(runtime *plugin.Runtime, args map[string]*llx.RawData) (plugin.Resource, error) { + res := &mqlK8sApp{ + MqlRuntime: runtime, + } + + err := SetAllData(res, args) + if err != nil { + return res, err + } + + // to override __id implement: id() (string, error) + + if runtime.HasRecording { + args, err = runtime.ResourceFromRecording("k8s.app", res.__id) + if err != nil || args == nil { + return res, err + } + return res, SetAllData(res, args) + } + + return res, nil +} + +func (c *mqlK8sApp) MqlName() string { + return "k8s.app" +} + +func (c *mqlK8sApp) MqlID() string { + return c.__id +} + +func (c *mqlK8sApp) GetName() *plugin.TValue[string] { + return &c.Name +} + +func (c *mqlK8sApp) GetVersion() *plugin.TValue[string] { + return &c.Version +} + +func (c *mqlK8sApp) GetInstance() *plugin.TValue[string] { + return &c.Instance +} + +func (c *mqlK8sApp) GetManagedBy() *plugin.TValue[string] { + return &c.ManagedBy +} + +func (c *mqlK8sApp) GetPartOf() *plugin.TValue[string] { + return &c.PartOf +} + +func (c *mqlK8sApp) GetComponents() *plugin.TValue[[]interface{}] { + return &c.Components +} diff --git a/providers/k8s/resources/k8s.lr.manifest.yaml b/providers/k8s/resources/k8s.lr.manifest.yaml index ac1b6daf9c..558610ad97 100755 --- a/providers/k8s/resources/k8s.lr.manifest.yaml +++ b/providers/k8s/resources/k8s.lr.manifest.yaml @@ -5,6 +5,8 @@ resources: k8s: fields: apiResources: {} + apps: + min_mondoo_version: 9.0.0 clusterrolebindings: min_mondoo_version: 5.31.0 clusterroles: @@ -120,6 +122,19 @@ resources: platform: name: - kubernetes + k8s.app: + fields: + components: {} + instance: {} + managedBy: {} + name: {} + partOf: {} + version: {} + is_private: true + min_mondoo_version: 9.0.0 + platform: + name: + - kubernetes k8s.configmap: fields: annotations: {}