diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index a275bb02ca..dde3741a1c 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -41,6 +41,8 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{ "StatefulSet": StatefulSetAnalyzer{}, "CronJob": CronJobAnalyzer{}, "Node": NodeAnalyzer{}, + "ValidatingWebhook": ValidatingWebhookAnalyzer{}, + "MutatingWebhook": 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..78dc5dece7 --- /dev/null +++ b/pkg/analyzer/mutating-webhook.go @@ -0,0 +1,111 @@ +/* +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 + pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(context.Background(), v1.ListOptions{}) + if err != nil { + return nil, err + } + for _, pod := range pods.Items { + if pod.Name != svc.Name || pod.Namespace != svc.Namespace || 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..9b9cf5cc9d --- /dev/null +++ b/pkg/analyzer/validating-webhook.go @@ -0,0 +1,110 @@ +/* +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 + pods, err := a.Client.GetClient().CoreV1().Pods(a.Namespace).List(context.Background(), v1.ListOptions{}) + if err != nil { + return nil, err + } + for _, pod := range pods.Items { + if pod.Name != svc.Name || pod.Namespace != svc.Namespace || 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..8dbdfbaeeb 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 } } }