chore: additional analyzers

Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
This commit is contained in:
Alex Jones
2023-04-13 21:25:41 +01:00
parent 5dcc19038f
commit 23071fd2e6
11 changed files with 527 additions and 0 deletions

1
go.mod
View File

@@ -119,6 +119,7 @@ require (
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect github.com/rivo/uniseg v0.4.4 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/rubenv/sql-migrate v1.3.1 // indirect github.com/rubenv/sql-migrate v1.3.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.37.0 // indirect github.com/samber/lo v1.37.0 // indirect

2
go.sum
View File

@@ -607,6 +607,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

View File

@@ -11,6 +11,7 @@ import (
var coreAnalyzerMap = map[string]common.IAnalyzer{ var coreAnalyzerMap = map[string]common.IAnalyzer{
"Pod": PodAnalyzer{}, "Pod": PodAnalyzer{},
"Deployment": DeploymentAnalyzer{},
"ReplicaSet": ReplicaSetAnalyzer{}, "ReplicaSet": ReplicaSetAnalyzer{},
"PersistentVolumeClaim": PvcAnalyzer{}, "PersistentVolumeClaim": PvcAnalyzer{},
"Service": ServiceAnalyzer{}, "Service": ServiceAnalyzer{},
@@ -21,6 +22,7 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{
var additionalAnalyzerMap = map[string]common.IAnalyzer{ var additionalAnalyzerMap = map[string]common.IAnalyzer{
"HorizontalPodAutoScaler": HpaAnalyzer{}, "HorizontalPodAutoScaler": HpaAnalyzer{},
"PodDisruptionBudget": PdbAnalyzer{}, "PodDisruptionBudget": PdbAnalyzer{},
"NetworkPolicy": NetworkPolicyAnalyzer{},
} }
func ListFilters() ([]string, []string, []string) { func ListFilters() ([]string, []string, []string) {

76
pkg/analyzer/cronjob.go Normal file
View File

@@ -0,0 +1,76 @@
package analyzer
import (
"fmt"
"time"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
cron "github.com/robfig/cron/v3"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type CronJobAnalyzer struct{}
func (analyzer CronJobAnalyzer) Analyze(config common.Analyzer) ([]common.Result, error) {
var results []common.Result
cronJobList, err := config.Client.GetClient().BatchV1().CronJobs("").List(config.Context, v1.ListOptions{})
if err != nil {
return results, err
}
for _, cronJob := range cronJobList.Items {
result := common.Result{
Kind: "CronJob",
Name: cronJob.Name,
}
if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
result.Error = append(result.Error, common.Failure{
Text: fmt.Sprintf("CronJob %s is suspended", cronJob.Name),
Sensitive: []common.Sensitive{},
})
} else {
// check the schedule format
if _, err := CheckCronScheduleIsValid(cronJob.Spec.Schedule); err != nil {
result.Error = append(result.Error, common.Failure{
Text: fmt.Sprintf("CronJob %s has an invalid schedule: %s", cronJob.Name, cronJob.Spec.Schedule),
Sensitive: []common.Sensitive{},
})
}
// check the starting deadline
if cronJob.Spec.StartingDeadlineSeconds != nil {
deadline := time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second
if deadline < 0 {
result = common.Result{
Kind: "CronJob",
Name: cronJob.Name,
Error: []common.Failure{
{
Text: fmt.Sprintf("CronJob %s has a negative starting deadline: %d seconds", cronJob.Name, *cronJob.Spec.StartingDeadlineSeconds),
Sensitive: []common.Sensitive{},
},
},
}
}
}
}
results = append(results, result)
}
return results, nil
}
// Check CRON schedule format
func CheckCronScheduleIsValid(schedule string) (bool, error) {
_, err := cron.ParseStandard(schedule)
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,130 @@
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestCronJobSuccess(t *testing.T) {
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "example-cronjob",
Namespace: "default",
Annotations: map[string]string{
"analysisDate": "2022-04-01",
},
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*/1 * * * *",
ConcurrencyPolicy: "Allow",
JobTemplate: batchv1.JobTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analyzer := CronJobAnalyzer{}
analysisResults, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 0)
assert.Equal(t, analysisResults[0].Name, "example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
assert.Equal(t, analysisResults[0].Error, "CronJob 'example-cronjob' has an annotation 'analysisDate', indicating it may need to be reviewed.")
}
func TestCronJobBroken(t *testing.T) {
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "example-cronjob",
Namespace: "default",
Annotations: map[string]string{
"analysisDate": "2022-04-01",
},
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*** * * * *",
ConcurrencyPolicy: "Allow",
JobTemplate: batchv1.JobTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analyzer := CronJobAnalyzer{}
analysisResults, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
assert.Equal(t, analysisResults[0].Name, "example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
}

View File

@@ -0,0 +1,46 @@
package analyzer
import (
"context"
"fmt"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
)
// DeploymentAnalyzer is an analyzer that checks for misconfigured Deployments
type DeploymentAnalyzer struct {
}
// Analyze scans all namespaces for Deployments with misconfigurations
func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
var results []common.Result
deployments, err := a.Client.GetClient().AppsV1().Deployments("").List(context.Background(), v1.ListOptions{})
if err != nil {
return nil, err
}
for _, deployment := range deployments.Items {
if *deployment.Spec.Replicas != deployment.Status.Replicas {
failureDetails := []common.Failure{
{
Text: fmt.Sprintf("Deployment %s has a mismatch between the desired and actual replicas", deployment.Name),
Sensitive: []common.Sensitive{},
},
}
result := common.Result{
Kind: "Deployment",
Name: fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name),
Error: failureDetails,
ParentObject: "",
}
results = append(results, result)
}
}
return results, nil
}

View File

@@ -0,0 +1,64 @@
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestDeploymentAnalyzer(t *testing.T) {
clientset := fake.NewSimpleClientset(&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: func() *int32 { i := int32(3); return &i }(),
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
Ports: []v1.ContainerPort{
{
ContainerPort: 80,
},
},
},
},
},
},
},
Status: appsv1.DeploymentStatus{
Replicas: 2,
AvailableReplicas: 1,
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
deploymentAnalyzer := DeploymentAnalyzer{}
analysisResults, err := deploymentAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
assert.Equal(t, analysisResults[0].Kind, "Deployment")
assert.Equal(t, analysisResults[0].Name, "default/example")
assert.Equal(t, len(analysisResults[0].Error), 1)
assert.Equal(t, analysisResults[0].Error[0].Text, "Deployment example has a mismatch between the desired and actual replicas")
}

66
pkg/analyzer/netpol.go Normal file
View File

@@ -0,0 +1,66 @@
package analyzer
import (
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type NetworkPolicyAnalyzer struct{}
func (NetworkPolicyAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
// get all network policies in the namespace
policies, err := a.Client.GetClient().NetworkingV1().
NetworkPolicies(a.Namespace).List(a.Context, metav1.ListOptions{})
if err != nil {
return nil, err
}
var preAnalysis = map[string]common.PreAnalysis{}
for _, policy := range policies.Items {
// Check if policy allows traffic to all pods in the namespace
if len(policy.Spec.PodSelector.MatchLabels) == 0 {
preAnalysis[fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)] = common.PreAnalysis{
NetworkPolicy: policy,
FailureDetails: []common.Failure{
{
Text: fmt.Sprintf("Network policy allows traffic to all pods in the namespace: %s", policy.Name),
},
},
}
continue
}
// Check if policy is not applied to any pods
podList, err := util.GetPodListByLabels(a.Client.GetClient(), a.Namespace, policy.Spec.PodSelector.MatchLabels)
if err != nil {
return nil, err
}
if len(podList.Items) == 0 {
preAnalysis[fmt.Sprintf("%s/%s", policy.Namespace, policy.Name)] = common.PreAnalysis{
NetworkPolicy: policy,
FailureDetails: []common.Failure{
{
Text: fmt.Sprintf("Network policy is not applied to any pods: %s", policy.Name),
},
},
}
}
}
var analysisResults []common.Result
for key, value := range preAnalysis {
currentAnalysis := common.Result{
Kind: "NetworkPolicy",
Name: key,
Error: value.FailureDetails,
ParentObject: "",
}
analysisResults = append(analysisResults, currentAnalysis)
}
return analysisResults, nil
}

122
pkg/analyzer/netpol_test.go Normal file
View File

@@ -0,0 +1,122 @@
package analyzer
import (
"context"
"testing"
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
v1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestNetpolNoPods(t *testing.T) {
clientset := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
Spec: networkingv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "example",
},
},
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
PodSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "database",
},
},
},
},
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analyzer := NetworkPolicyAnalyzer{}
results, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(results), 1)
assert.Equal(t, results[0].Kind, "NetworkPolicy")
}
func TestNetpolWithPod(t *testing.T) {
clientset := fake.NewSimpleClientset(&networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
Spec: networkingv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "example",
},
},
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
PodSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "database",
},
},
},
},
},
},
},
}, &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Labels: map[string]string{
"app": "example",
},
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example",
Image: "example",
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analyzer := NetworkPolicyAnalyzer{}
results, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(results), 0)
}

View File

@@ -36,6 +36,7 @@ type PreAnalysis struct {
HorizontalPodAutoscalers autov1.HorizontalPodAutoscaler HorizontalPodAutoscalers autov1.HorizontalPodAutoscaler
PodDisruptionBudget policyv1.PodDisruptionBudget PodDisruptionBudget policyv1.PodDisruptionBudget
StatefulSet appsv1.StatefulSet StatefulSet appsv1.StatefulSet
NetworkPolicy networkv1.NetworkPolicy
// Integrations // Integrations
TrivyVulnerabilityReport trivy.VulnerabilityReport TrivyVulnerabilityReport trivy.VulnerabilityReport
} }

View File

@@ -8,7 +8,9 @@ import (
"regexp" "regexp"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k "k8s.io/client-go/kubernetes"
) )
var anonymizePattern = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;':\",./<>?") var anonymizePattern = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_=+[]{}|;':\",./<>?")
@@ -133,3 +135,18 @@ func ReplaceIfMatch(text string, pattern string, replacement string) string {
func GetCacheKey(provider string, sEnc string) string { func GetCacheKey(provider string, sEnc string) string {
return fmt.Sprintf("%s-%s", provider, sEnc) return fmt.Sprintf("%s-%s", provider, sEnc)
} }
func GetPodListByLabels(client k.Interface,
namespace string,
labels map[string]string) (*v1.PodList, error) {
pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{
LabelSelector: metav1.FormatLabelSelector(&metav1.LabelSelector{
MatchLabels: labels,
}),
})
if err != nil {
return nil, err
}
return pods, nil
}