From 1c30528eeacd767206948e4be651b994c866c338 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 4 Nov 2021 12:06:34 +0200 Subject: [PATCH] support include ns --- README.md | 18 ++++++-- cautils/scaninfo.go | 1 + clihandler/cmd/scan.go | 3 +- clihandler/initcli.go | 13 +++++- resourcehandler/fieldselector.go | 76 ++++++++++++++++++++++++++++++++ resourcehandler/k8sresources.go | 40 +++++------------ 6 files changed, 117 insertions(+), 34 deletions(-) create mode 100644 resourcehandler/fieldselector.go diff --git a/README.md b/README.md index 8922d2f9..3ce34d2a 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | flag | default | description | options | |-----------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------| | `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | | +| `--include-namespaces` | Scan all namespaces | Scan specific namespaces | | | `-s`/`--silent` | Display progress messages | Silent progress messages | | | `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold | `0` -> `100` | | `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus` | @@ -110,12 +111,21 @@ kubescape scan framework mitre --submit kubescape scan control "Privileged container" ``` +* Scan specific namespaces +``` +kubescape scan framework nsa --include-namespaces development,staging,production +``` + +* Scan cluster and exclude some namespaces +``` +kubescape scan framework nsa --exclude-namespaces kube-system,kube-public +``` + * Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI) ``` kubescape scan framework nsa *.yaml ``` - * Scan kubernetes manifest files from a public github repository ``` kubescape scan framework nsa https://github.com/armosec/kubescape @@ -123,17 +133,17 @@ kubescape scan framework nsa https://github.com/armosec/kubescape * Output in `json` format ``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json +kubescape scan framework nsa --format json --output results.json ``` * Output in `junit xml` format ``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml +kubescape scan framework nsa --format junit --output results.xml ``` * Output in `prometheus` metrics format ``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format prometheus +kubescape scan framework nsa --format prometheus ``` * Scan with exceptions, objects with exceptions will be presented as `exclude` and not `fail` diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 2fb087c1..041aef05 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -19,6 +19,7 @@ type ScanInfo struct { Format string // Format results (table, json, junit ...) Output string // Store results in an output file, Output file name ExcludedNamespaces string // DEPRECATED? + IncludeNamespaces string // DEPRECATED? InputPatterns []string // Yaml files input patterns Silent bool // Silent mode - Do not print progress logs FailThreshold uint16 // Failure score threshold diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 5b79abbc..38f1b5d8 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -37,7 +37,8 @@ func init() { rootCmd.AddCommand(scanCmd) scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to Armo backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") - scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system, kube-public") + scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system,kube-public") + scanCmd.PersistentFlags().StringVar(&scanInfo.IncludeNamespaces, "include-namespaces", "", "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b") scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"/"prometheus"`) scanCmd.PersistentFlags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") diff --git a/clihandler/initcli.go b/clihandler/initcli.go index 6d165e44..1cd4297a 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -39,6 +39,17 @@ func getReporter(scanInfo *cautils.ScanInfo) reporter.IReport { return reporter.NewReportEventReceiver("", "") } + +func getFieldSelector(scanInfo *cautils.ScanInfo) resourcehandler.IFieldSelector { + if scanInfo.IncludeNamespaces != "" { + return resourcehandler.NewIncludeSelector(scanInfo.IncludeNamespaces) + } + if scanInfo.ExcludedNamespaces != "" { + return resourcehandler.NewExcludeSelector(scanInfo.ExcludedNamespaces) + } + + return &resourcehandler.EmptySelector{} +} func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { var resourceHandler resourcehandler.IResourceHandler var clusterConfig cautils.IClusterConfig @@ -55,7 +66,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler = reporter.NewReportMock() } else { k8s := k8sinterface.NewKubernetesApi() - resourceHandler = resourcehandler.NewK8sResourceHandler(k8s, scanInfo.ExcludedNamespaces) + resourceHandler = resourcehandler.NewK8sResourceHandler(k8s, getFieldSelector(scanInfo)) clusterConfig = cautils.ClusterConfigSetup(scanInfo, k8s, getter.GetArmoAPIConnector()) // setup reporter diff --git a/resourcehandler/fieldselector.go b/resourcehandler/fieldselector.go new file mode 100644 index 00000000..cfb2efa3 --- /dev/null +++ b/resourcehandler/fieldselector.go @@ -0,0 +1,76 @@ +package resourcehandler + +import ( + "fmt" + "strings" + + "github.com/armosec/k8s-interface/k8sinterface" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type IFieldSelector interface { + GetNamespacesSelector(*schema.GroupVersionResource) string +} + +type EmptySelector struct { +} + +func (es *EmptySelector) GetNamespacesSelector(resource *schema.GroupVersionResource) string { + return "" +} + +type ExcludeSelector struct { + namespace string +} + +func NewExcludeSelector(ns string) *ExcludeSelector { + return &ExcludeSelector{namespace: ns} +} + +type IncludeSelector struct { + namespace string +} + +func NewIncludeSelector(ns string) *IncludeSelector { + return &IncludeSelector{namespace: ns} +} +func (es *ExcludeSelector) GetNamespacesSelector(resource *schema.GroupVersionResource) string { + return getNamespacesSelector(resource, es.namespace, "!=") +} + +func (is *IncludeSelector) GetNamespacesSelector(resource *schema.GroupVersionResource) string { + return getNamespacesSelector(resource, is.namespace, "==") +} + +func getNamespacesSelector(resource *schema.GroupVersionResource, ns, operator string) string { + fieldSelectors := "" + fieldSelector := "metadata." + if resource.Resource == "namespaces" { + fieldSelector += "name" + } else if k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) { + fieldSelector += "namespace" + } else { + return "" + } + namespacesSlice := strings.Split(ns, ",") + for _, n := range namespacesSlice { + fieldSelectors += fmt.Sprintf("%s!=%s,", fieldSelector, n) + } + return fieldSelectors + +} +func setFieldSelector(listOptions *metav1.ListOptions, resource *schema.GroupVersionResource, excludedNamespaces string) { + fieldSelector := "metadata." + if resource.Resource == "namespaces" { + fieldSelector += "name" + } else if k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) { + fieldSelector += "namespace" + } else { + return + } + excludedNamespacesSlice := strings.Split(excludedNamespaces, ",") + for _, excludedNamespace := range excludedNamespacesSlice { + listOptions.FieldSelector += fmt.Sprintf("%s!=%s,", fieldSelector, excludedNamespace) + } +} diff --git a/resourcehandler/k8sresources.go b/resourcehandler/k8sresources.go index 74bc4922..5b6c0d08 100644 --- a/resourcehandler/k8sresources.go +++ b/resourcehandler/k8sresources.go @@ -3,7 +3,6 @@ package resourcehandler import ( "context" "fmt" - "strings" "github.com/armosec/kubescape/cautils" "github.com/armosec/opa-utils/reporthandling" @@ -21,14 +20,14 @@ import ( ) type K8sResourceHandler struct { - k8s *k8sinterface.KubernetesApi - excludedNamespaces string // excluded namespaces (separated by comma) + k8s *k8sinterface.KubernetesApi + fieldSelector IFieldSelector } -func NewK8sResourceHandler(k8s *k8sinterface.KubernetesApi, excludedNamespaces string) *K8sResourceHandler { +func NewK8sResourceHandler(k8s *k8sinterface.KubernetesApi, fieldSelector IFieldSelector) *K8sResourceHandler { return &K8sResourceHandler{ - k8s: k8s, - excludedNamespaces: excludedNamespaces, + k8s: k8s, + fieldSelector: fieldSelector, } } @@ -43,7 +42,7 @@ func (k8sHandler *K8sResourceHandler) GetResources(frameworks []reporthandling.F _, namespace, labels := armotypes.DigestPortalDesignator(designator) // pull k8s recourses - if err := k8sHandler.pullResources(k8sResourcesMap, namespace, labels, k8sHandler.excludedNamespaces); err != nil { + if err := k8sHandler.pullResources(k8sResourcesMap, namespace, labels); err != nil { return k8sResourcesMap, err } @@ -59,13 +58,13 @@ func (k8sHandler *K8sResourceHandler) GetClusterAPIServerInfo() *version.Info { } return clusterAPIServerInfo } -func (k8sHandler *K8sResourceHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string, excludedNamespaces string) error { +func (k8sHandler *K8sResourceHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string) error { var errs error for groupResource := range *k8sResources { apiGroup, apiVersion, resource := k8sinterface.StringToResourceGroup(groupResource) gvr := schema.GroupVersionResource{Group: apiGroup, Version: apiVersion, Resource: resource} - result, err := k8sHandler.pullSingleResource(&gvr, namespace, labels, excludedNamespaces) + result, err := k8sHandler.pullSingleResource(&gvr, namespace, labels) if err != nil { // handle error if errs == nil { @@ -81,13 +80,13 @@ func (k8sHandler *K8sResourceHandler) pullResources(k8sResources *cautils.K8SRes return errs } -func (k8sHandler *K8sResourceHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string, excludedNamespaces string) ([]unstructured.Unstructured, error) { +func (k8sHandler *K8sResourceHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string) ([]unstructured.Unstructured, error) { // set labels listOptions := metav1.ListOptions{} - if excludedNamespaces != "" { - setFieldSelector(&listOptions, resource, excludedNamespaces) - } + + listOptions.FieldSelector += k8sHandler.fieldSelector.GetNamespacesSelector(resource) + if len(labels) > 0 { set := k8slabels.Set(labels) listOptions.LabelSelector = set.AsSelector().String() @@ -110,18 +109,3 @@ func (k8sHandler *K8sResourceHandler) pullSingleResource(resource *schema.GroupV return result.Items, nil } - -func setFieldSelector(listOptions *metav1.ListOptions, resource *schema.GroupVersionResource, excludedNamespaces string) { - fieldSelector := "metadata." - if resource.Resource == "namespaces" { - fieldSelector += "name" - } else if k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) { - fieldSelector += "namespace" - } else { - return - } - excludedNamespacesSlice := strings.Split(excludedNamespaces, ",") - for _, excludedNamespace := range excludedNamespacesSlice { - listOptions.FieldSelector += fmt.Sprintf("%s!=%s,", fieldSelector, excludedNamespace) - } -}