diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index a275bb02ca..cd67d929e0 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -32,15 +32,17 @@ var ( ) var coreAnalyzerMap = map[string]common.IAnalyzer{ - "Pod": PodAnalyzer{}, - "Deployment": DeploymentAnalyzer{}, - "ReplicaSet": ReplicaSetAnalyzer{}, - "PersistentVolumeClaim": PvcAnalyzer{}, - "Service": ServiceAnalyzer{}, - "Ingress": IngressAnalyzer{}, - "StatefulSet": StatefulSetAnalyzer{}, - "CronJob": CronJobAnalyzer{}, - "Node": NodeAnalyzer{}, + "Pod": PodAnalyzer{}, + "Deployment": DeploymentAnalyzer{}, + "ReplicaSet": ReplicaSetAnalyzer{}, + "PersistentVolumeClaim": PvcAnalyzer{}, + "Service": ServiceAnalyzer{}, + "Ingress": IngressAnalyzer{}, + "StatefulSet": StatefulSetAnalyzer{}, + "CronJob": CronJobAnalyzer{}, + "Node": NodeAnalyzer{}, + "ValidatingWebhookConfiguration": ValidatingWebhookAnalyzer{}, + "MutatingWebhookConfiguration": MutatingWebhookAnalyzer{}, } var additionalAnalyzerMap = map[string]common.IAnalyzer{ diff --git a/pkg/analyzer/mutating_webhook.go b/pkg/analyzer/mutating_webhook.go new file mode 100644 index 0000000000..b5e7d224fb --- /dev/null +++ b/pkg/analyzer/mutating_webhook.go @@ -0,0 +1,148 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package analyzer + +import ( + "context" + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type MutatingWebhookAnalyzer struct{} + +func (MutatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + kind := "MutatingWebhookConfiguration" + apiDoc := kubernetes.K8sApiReference{ + Kind: kind, + ApiVersion: schema.GroupVersion{ + Group: "apps", + Version: "v1", + }, + OpenapiSchema: a.OpenapiSchema, + } + + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + mutatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.Background(), v1.ListOptions{}) + if err != nil { + return nil, err + } + + var preAnalysis = map[string]common.PreAnalysis{} + + for _, webhookConfig := range mutatingWebhooks.Items { + for _, webhook := range webhookConfig.Webhooks { + var failures []common.Failure + + svc := webhook.ClientConfig.Service + // Get the service + service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{}) + if err != nil { + // If the service is not found, we can't check the pods + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("Service %s not found as mapped to by Mutating Webhook %s", svc.Name, webhook.Name), + KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + { + Unmasked: svc.Name, + Masked: util.MaskString(svc.Name), + }, + }, + }) + continue + } + + // Get pods within service + pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{ + LabelSelector: util.MapToString(service.Spec.Selector), + }) + if err != nil { + return nil, err + } + + if len(pods.Items) == 0 { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("No active pods found within service %s as mapped to by Mutating Webhook %s", svc.Name, webhook.Name), + KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + }, + }) + + } + for _, pod := range pods.Items { + if pod.Status.Phase != "Running" { + doc := apiDoc.GetApiDocV2("spec.webhook") + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "Mutating Webhook (%s) is pointing to an inactive receiver pod (%s)", + webhook.Name, + pod.Name, + ), + KubernetesDoc: doc, + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + { + Unmasked: webhook.Name, + Masked: util.MaskString(webhook.Name), + }, + { + Unmasked: pod.Name, + Masked: util.MaskString(pod.Name), + }, + }, + }) + } + } + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ + MutatingWebhook: webhookConfig, + FailureDetails: failures, + } + AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) + } + } + } + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + + parent, _ := util.GetParent(a.Client, value.MutatingWebhook.ObjectMeta) + currentAnalysis.ParentObject = parent + a.Results = append(a.Results, currentAnalysis) + } + + return a.Results, nil +} diff --git a/pkg/analyzer/validating_webhook.go b/pkg/analyzer/validating_webhook.go new file mode 100644 index 0000000000..45855e5067 --- /dev/null +++ b/pkg/analyzer/validating_webhook.go @@ -0,0 +1,147 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package analyzer + +import ( + "context" + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ValidatingWebhookAnalyzer struct{} + +func (ValidatingWebhookAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + kind := "ValidatingWebhookConfgiguration" + apiDoc := kubernetes.K8sApiReference{ + Kind: kind, + ApiVersion: schema.GroupVersion{ + Group: "apps", + Version: "v1", + }, + OpenapiSchema: a.OpenapiSchema, + } + + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + validatingWebhooks, err := a.Client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().List(context.Background(), v1.ListOptions{}) + if err != nil { + return nil, err + } + var preAnalysis = map[string]common.PreAnalysis{} + + for _, webhookConfig := range validatingWebhooks.Items { + for _, webhook := range webhookConfig.Webhooks { + var failures []common.Failure + + svc := webhook.ClientConfig.Service + // Get the service + service, err := a.Client.GetClient().CoreV1().Services(svc.Namespace).Get(context.Background(), svc.Name, v1.GetOptions{}) + if err != nil { + // If the service is not found, we can't check the pods + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("Service %s not found as mapped to by Validating Webhook %s", svc.Name, webhook.Name), + KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + { + Unmasked: svc.Name, + Masked: util.MaskString(svc.Name), + }, + }, + }) + continue + } + + // Get pods within service + pods, err := a.Client.GetClient().CoreV1().Pods(svc.Namespace).List(context.Background(), v1.ListOptions{ + LabelSelector: util.MapToString(service.Spec.Selector), + }) + if err != nil { + return nil, err + } + + if len(pods.Items) == 0 { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("No active pods found within service %s as mapped to by Validating Webhook %s", svc.Name, webhook.Name), + KubernetesDoc: apiDoc.GetApiDocV2("spec.webhook.clientConfig.service"), + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + }, + }) + + } + for _, pod := range pods.Items { + if pod.Status.Phase != "Running" { + doc := apiDoc.GetApiDocV2("spec.webhook") + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "Validating Webhook (%s) is pointing to an inactive receiver pod (%s)", + webhook.Name, + pod.Name, + ), + KubernetesDoc: doc, + Sensitive: []common.Sensitive{ + { + Unmasked: webhookConfig.Namespace, + Masked: util.MaskString(webhookConfig.Namespace), + }, + { + Unmasked: webhook.Name, + Masked: util.MaskString(webhook.Name), + }, + { + Unmasked: pod.Name, + Masked: util.MaskString(pod.Name), + }, + }, + }) + } + } + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s/%s", webhookConfig.Namespace, webhook.Name)] = common.PreAnalysis{ + ValidatingWebhook: webhookConfig, + FailureDetails: failures, + } + AnalyzerErrorsMetric.WithLabelValues(kind, webhook.Name, webhookConfig.Namespace).Set(float64(len(failures))) + } + } + } + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + + parent, _ := util.GetParent(a.Client, value.ValidatingWebhook.ObjectMeta) + currentAnalysis.ParentObject = parent + a.Results = append(a.Results, currentAnalysis) + } + + return a.Results, nil +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 35bd539442..1ec0e04f03 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -20,6 +20,7 @@ import ( openapi_v2 "github.com/google/gnostic/openapiv2" "github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + regv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" autov1 "k8s.io/api/autoscaling/v1" v1 "k8s.io/api/core/v1" @@ -54,6 +55,8 @@ type PreAnalysis struct { StatefulSet appsv1.StatefulSet NetworkPolicy networkv1.NetworkPolicy Node v1.Node + ValidatingWebhook regv1.ValidatingWebhookConfiguration + MutatingWebhook regv1.MutatingWebhookConfiguration // Integrations TrivyVulnerabilityReport trivy.VulnerabilityReport } diff --git a/pkg/util/util.go b/pkg/util/util.go index d6b2f975ea..317a7de51e 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -94,6 +94,26 @@ func GetParent(client *kubernetes.Client, meta metav1.ObjectMeta) (string, bool) return GetParent(client, ds.ObjectMeta) } return "Ingress/" + ds.Name, false + + case "MutatingWebhookConfiguration": + mw, err := client.GetClient().AdmissionregistrationV1().MutatingWebhookConfigurations().Get(context.Background(), owner.Name, metav1.GetOptions{}) + if err != nil { + return "", false + } + if mw.OwnerReferences != nil { + return GetParent(client, mw.ObjectMeta) + } + return "MutatingWebhook/" + mw.Name, false + + case "ValidatingWebhookConfiguration": + vw, err := client.GetClient().AdmissionregistrationV1().ValidatingWebhookConfigurations().Get(context.Background(), owner.Name, metav1.GetOptions{}) + if err != nil { + return "", false + } + if vw.OwnerReferences != nil { + return GetParent(client, vw.ObjectMeta) + } + return "ValidatingWebhook/" + vw.Name, false } } } @@ -191,3 +211,11 @@ func EnsureDirExists(dir string) error { return err } + +func MapToString(m map[string]string) string { + var result string + for k, v := range m { + result += fmt.Sprintf("%s=%s,", k, v) + } + return result[:len(result)-1] +}