From 78838a606dc1fe790d1308cc7d2cd80f2bdfd92e Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Fri, 8 Apr 2022 07:54:03 -0600 Subject: [PATCH] Add a --namespace flag to the in-cluster audit (#742) --- cmd/polaris/audit.go | 35 +++++++++++------ docs/cli.md | 1 + examples/config-full.yaml | 1 + examples/config.yaml | 2 + pkg/config/config.go | 1 + pkg/kube/resources.go | 33 ++++++++++++---- pkg/kube/resources_test.go | 77 ++++++++++++++++++++++++++++++-------- test/fixtures.go | 22 ++++++++++- 8 files changed, 138 insertions(+), 34 deletions(-) diff --git a/cmd/polaris/audit.go b/cmd/polaris/audit.go index 0618038f..f0c6d5c6 100644 --- a/cmd/polaris/audit.go +++ b/cmd/polaris/audit.go @@ -32,17 +32,20 @@ import ( "sigs.k8s.io/yaml" ) -var setExitCode bool -var onlyShowFailedTests bool -var minScore int -var auditOutputURL string -var auditOutputFile string -var auditOutputFormat string -var resourceToAudit string -var useColor bool -var helmChart string -var helmValues string -var checks []string +var ( + setExitCode bool + onlyShowFailedTests bool + minScore int + auditOutputURL string + auditOutputFile string + auditOutputFormat string + resourceToAudit string + useColor bool + helmChart string + helmValues string + checks []string + auditNamespace string +) func init() { rootCmd.AddCommand(auditCmd) @@ -59,6 +62,7 @@ func init() { auditCmd.PersistentFlags().StringVar(&helmChart, "helm-chart", "", "Will fill out Helm template") auditCmd.PersistentFlags().StringVar(&helmValues, "helm-values", "", "Optional flag to add helm values") auditCmd.PersistentFlags().StringSliceVar(&checks, "checks", []string{}, "Optional flag to specify specific checks to check") + auditCmd.PersistentFlags().StringVar(&auditNamespace, "namespace", "", "Namespace to audit. Only applies to in-cluster audits") } var auditCmd = &cobra.Command{ @@ -80,6 +84,15 @@ var auditCmd = &cobra.Command{ } } } + if auditNamespace != "" { + if helmChart != "" { + logrus.Warn("--namespace and --helm-chart are mutually exclusive. --namespace will be ignored.") + } + if auditPath != "" { + logrus.Warn("--namespace and --audit-path are mutually exclusive. --namespace will be ignored.") + } + config.Namespace = auditNamespace + } if helmChart != "" { var err error auditPath, err = ProcessHelmTemplates(helmChart, helmValues) diff --git a/docs/cli.md b/docs/cli.md index 013ffeb5..1212747d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -45,6 +45,7 @@ webhook --helm-chart string Will fill out Helm template --helm-values string Optional flag to add helm values -h, --help help for audit + --namespace string Namespace to audit. Only applies to in-cluster audits --only-show-failed-tests If specified, audit output will only show failed tests. --output-file string Destination file for audit results. --output-url string Destination URL to send audit results. diff --git a/examples/config-full.yaml b/examples/config-full.yaml index 0578b71c..31346f6a 100644 --- a/examples/config-full.yaml +++ b/examples/config-full.yaml @@ -88,3 +88,4 @@ customChecks: not: pattern: ^quay.io +namespce: test-ns diff --git a/examples/config.yaml b/examples/config.yaml index a06a2dac..c9dfdf86 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -202,3 +202,5 @@ exemptions: - kube-hunter rules: - runAsRootAllowed + +namespace: test-ns diff --git a/pkg/config/config.go b/pkg/config/config.go index aee3dd0f..0ce9e39c 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -38,6 +38,7 @@ type Configuration struct { DisallowAnnotationExemptions bool `json:"disallowAnnotationExemptions"` Mutations []string `json:"mutations"` KubeContext string `json:"kubeContext"` + Namespace string `json:"namespace"` } // Exemption represents an exemption to normal rules diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go index 5fa94919..6d15749d 100644 --- a/pkg/kube/resources.go +++ b/pkg/kube/resources.go @@ -253,19 +253,38 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac logrus.Errorf("Error fetching Cluster API version: %v", err) return nil, err } - provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, "Cluster", clusterName) + + sourceType := "Cluster" + if c.Namespace != "" { + logrus.Debug("namespace is specififed in config, setting source type to ClusterNamespace") + sourceType = "ClusterNamespace" + } + provider := newResourceProvider(serverVersion.Major+"."+serverVersion.Minor, sourceType, clusterName) nodes, err := kube.CoreV1().Nodes().List(ctx, listOpts) if err != nil { logrus.Errorf("Error fetching Nodes: %v", err) return nil, err } - namespaces, err := kube.CoreV1().Namespaces().List(ctx, listOpts) - if err != nil { - logrus.Errorf("Error fetching Namespaces: %v", err) - return nil, err + + var namespaces *corev1.NamespaceList + if c.Namespace != "" { + ns, err := kube.CoreV1().Namespaces().Get(ctx, c.Namespace, metav1.GetOptions{}) + if err != nil { + return nil, err + } + namespaces = &corev1.NamespaceList{ + Items: []corev1.Namespace{*ns}, + } + } else { + nsList, err := kube.CoreV1().Namespaces().List(ctx, listOpts) + if err != nil { + logrus.Errorf("Error fetching Namespaces: %v", err) + return nil, err + } + namespaces = nsList } - pods, err := kube.CoreV1().Pods("").List(ctx, listOpts) + pods, err := kube.CoreV1().Pods(c.Namespace).List(ctx, listOpts) if err != nil { logrus.Errorf("Error fetching Pods: %v", err) return nil, err @@ -310,7 +329,7 @@ func CreateResourceProviderFromAPI(ctx context.Context, kube kubernetes.Interfac return nil, err } - objects, err := (*dynamic).Resource(mapping.Resource).Namespace("").List(ctx, metav1.ListOptions{}) + objects, err := (*dynamic).Resource(mapping.Resource).Namespace(c.Namespace).List(ctx, metav1.ListOptions{}) if err != nil { logrus.Warnf("Error retrieving parent object API %s and Kind %s because of error: %v", mapping.Resource.Version, mapping.Resource.Resource, err) return nil, err diff --git a/pkg/kube/resources_test.go b/pkg/kube/resources_test.go index 50d1826e..d5699bb0 100644 --- a/pkg/kube/resources_test.go +++ b/pkg/kube/resources_test.go @@ -97,15 +97,6 @@ func TestAddResourcesFromReader(t *testing.T) { func TestGetResourceFromAPI(t *testing.T) { k8s, dynamicInterface := test.SetupTestAPI(test.GetMockControllers("test")...) - resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, "test", &dynamicInterface, conf.Configuration{}) - assert.Equal(t, nil, err, "Error should be nil") - - assert.Equal(t, "Cluster", resources.SourceType, "Should have type Path") - assert.Equal(t, "test", resources.SourceName, "Should have source name") - assert.IsType(t, time.Now(), resources.CreationTime, "Creation time should be set") - - assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes") - assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers") expectedNames := map[string]bool{ "deploy": false, @@ -114,12 +105,68 @@ func TestGetResourceFromAPI(t *testing.T) { "statefulset": false, "daemonset": false, } - for _, controllers := range resources.Resources { - for _, ctrl := range controllers { - expectedNames[ctrl.ObjectMeta.GetName()] = true - } + + tests := []struct { + name string + config conf.Configuration + want *ResourceProvider + wantErr bool + clusterName string + }{ + { + name: "standard", + config: conf.Configuration{}, + clusterName: "test1", + want: &ResourceProvider{ + SourceType: "Cluster", + SourceName: "test1", + CreationTime: time.Now(), + }, + }, + { + name: "namespaced", + config: conf.Configuration{ + Namespace: "test", + }, + clusterName: "test2", + want: &ResourceProvider{ + SourceType: "ClusterNamespace", + SourceName: "test2", + CreationTime: time.Now(), + }, + }, + { + name: "namespace does not exist", + config: conf.Configuration{ + Namespace: "test3", + }, + clusterName: "test3", + wantErr: true, + }, } - for name, val := range expectedNames { - assert.Equal(t, true, val, name) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resources, err := CreateResourceProviderFromAPI(context.Background(), k8s, tt.clusterName, &dynamicInterface, tt.config) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.SourceType, resources.SourceType) + assert.Equal(t, tt.want.SourceName, resources.SourceName) + assert.IsType(t, tt.want.CreationTime, resources.CreationTime) + assert.Equal(t, 0, len(resources.Nodes), "Should not have any nodes") + assert.Equal(t, 5, len(resources.Resources), "Should have 5 controllers") + + for _, controllers := range resources.Resources { + for _, ctrl := range controllers { + expectedNames[ctrl.ObjectMeta.GetName()] = true + } + } + for name, val := range expectedNames { + assert.Equal(t, true, val, name) + } + } + }) } } diff --git a/test/fixtures.go b/test/fixtures.go index 3e4b4317..b10be572 100644 --- a/test/fixtures.go +++ b/test/fixtures.go @@ -102,7 +102,10 @@ func MockController(apiVersion, kind, namespace, name string, spec map[string]in if err != nil { panic(err) } - json.Unmarshal(b, &dest) + err = json.Unmarshal(b, &dest) + if err != nil { + panic(err) + } return pod } @@ -172,6 +175,15 @@ func MockReplicationController(namespace, name string) (corev1.ReplicationContro return rc, pod } +// MockNamespace returns a namespace object. +func MockNamespace(name string) corev1.Namespace { + return corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } +} + // SetupTestAPI creates a test kube API struct. func SetupTestAPI(objects ...runtime.Object) (kubernetes.Interface, dynamic.Interface) { scheme := runtime.NewScheme() @@ -235,12 +247,19 @@ func SetupTestAPI(objects ...runtime.Object) (kubernetes.Interface, dynamic.Inte {Name: "poddisruptionbudgets", Namespaced: true, Kind: "PodDisruptionBudget", Version: "v1"}, }, }, + { + GroupVersion: "core/v1", + APIResources: []metav1.APIResource{ + {Name: "namespaces", Namespaced: false, Kind: "Namespace"}, + }, + }, } return k, dynamicClient } // GetMockControllers returns mocked controllers for 5 major controller types func GetMockControllers(namespace string) []runtime.Object { + ns := MockNamespace(namespace) deploy, deployPod := MockDeploy(namespace, "deploy") statefulset, statefulsetPod := MockStatefulSet(namespace, "statefulset") daemonset, daemonsetPod := MockDaemonSet(namespace, "daemonset") @@ -252,5 +271,6 @@ func GetMockControllers(namespace string) []runtime.Object { &statefulset, &statefulsetPod, &cronjob, &cronjobPod, &job, &jobPod, + &ns, } }