From 76f400d0aa1dd1a347c1a634b3b3682aee94d5eb Mon Sep 17 00:00:00 2001 From: Daniel-GrunbergerCA Date: Sun, 10 Oct 2021 13:06:51 +0300 Subject: [PATCH 1/7] fix TestConvertLabelsToString test --- cautils/strutils_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cautils/strutils_test.go b/cautils/strutils_test.go index fd2495f6..6c93227b 100644 --- a/cautils/strutils_test.go +++ b/cautils/strutils_test.go @@ -13,7 +13,13 @@ func TestConvertLabelsToString(t *testing.T) { spilltedA := strings.Split(rsrt, ";") spilltedB := strings.Split(str, ";") for i := range spilltedA { - if spilltedA[i] != spilltedB[i] { + exists := false + for j := range spilltedB { + if spilltedB[j] == spilltedA[i] { + exists = true + } + } + if !exists { t.Errorf("%s != %s", spilltedA[i], spilltedB[i]) } } From e0e0a811eb13fa657728e1617c65fcc3bf64d087 Mon Sep 17 00:00:00 2001 From: Daniel-GrunbergerCA Date: Sun, 10 Oct 2021 14:01:35 +0300 Subject: [PATCH 2/7] Add supported frameworks to help msg --- cmd/framework.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/framework.go b/cmd/framework.go index 34d24549..e4ef0146 100644 --- a/cmd/framework.go +++ b/cmd/framework.go @@ -23,6 +23,7 @@ import ( var scanInfo cautils.ScanInfo var supportedFrameworks = []string{"nsa", "mitre"} +var validFrameworks = strings.Join(supportedFrameworks, ", ") type CLIHandler struct { policyHandler *policyhandler.PolicyHandler @@ -30,7 +31,8 @@ type CLIHandler struct { } var frameworkCmd = &cobra.Command{ - Use: "framework [``/`-`] [flags]", + + Use: fmt.Sprintf("framework [``/`-`] [flags]\nSupported frameworks: %s", validFrameworks), Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(supportedFrameworks, ", ")), Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin", ValidArgs: supportedFrameworks, From 0eba51c80be8940ae9f027a561e095653d129992 Mon Sep 17 00:00:00 2001 From: Daniel-GrunbergerCA Date: Sun, 10 Oct 2021 14:38:14 +0300 Subject: [PATCH 3/7] beautify help msgs --- cmd/download.go | 2 +- cmd/framework.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/download.go b/cmd/download.go index dfe69a58..a27123ac 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -11,7 +11,7 @@ import ( var downloadInfo cautils.DownloadInfo var downloadCmd = &cobra.Command{ - Use: "download framework ", + Use: fmt.Sprintf("Download framework [flags]\nSupported frameworks: %s", validFrameworks), Short: "Download framework controls", Long: ``, Args: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/framework.go b/cmd/framework.go index e4ef0146..ae6fb83f 100644 --- a/cmd/framework.go +++ b/cmd/framework.go @@ -87,12 +87,12 @@ func isValidFramework(framework string) bool { func init() { scanCmd.AddCommand(frameworkCmd) scanInfo = cautils.ScanInfo{} - frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Path to load framework from") - frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load framework from default path") - frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to file containing list of exceptions") - frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check") - frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`) - frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout") + frameworkCmd.Flags().StringVar(&scanInfo.UseFrom, "use-from", "", "Load local framework object from specified path. If not used will download latest") + frameworkCmd.Flags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load local framework object from default path. If not used will download latest") + frameworkCmd.Flags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from Armo management portal") + frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system, kube-public") + frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"`) + frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1") frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armosec backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features") From 659647353eea0926496c6dbd448f684b32d14c3a Mon Sep 17 00:00:00 2001 From: Daniel-GrunbergerCA Date: Sun, 10 Oct 2021 15:43:08 +0300 Subject: [PATCH 4/7] fix return code in help msg --- cmd/framework.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/framework.go b/cmd/framework.go index ae6fb83f..9738e365 100644 --- a/cmd/framework.go +++ b/cmd/framework.go @@ -94,7 +94,7 @@ func init() { frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"`) frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") - frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1") + frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code 1") frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armosec backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features") } From 59741b84d13dfc4288d4373943e18f68f24e9ead Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 11 Oct 2021 15:00:23 +0300 Subject: [PATCH 5/7] Update default upload of results to be opt-in --- .github/workflows/build.yaml | 1 + .github/workflows/build_dev.yaml | 4 + README.md | 26 +-- build.py | 2 +- cautils/customerloader.go | 324 +++++++++++++++++++++---------- cautils/getter/getpolicies.go | 3 + cautils/scaninfo.go | 23 ++- cmd/framework.go | 17 +- cmd/local_get.go | 3 +- install.sh | 14 +- 10 files changed, 268 insertions(+), 149 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5b891444..a5db2601 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,6 +5,7 @@ on: branches: [ master ] pull_request: branches: [ master ] + types: [ closed ] jobs: once: name: Create release diff --git a/.github/workflows/build_dev.yaml b/.github/workflows/build_dev.yaml index a3dc0086..bc5f11ff 100644 --- a/.github/workflows/build_dev.yaml +++ b/.github/workflows/build_dev.yaml @@ -20,6 +20,10 @@ jobs: uses: actions/setup-go@v2 with: go-version: 1.17 + + - name: Test + run: go test -v ./... + - name: Build env: RELEASE: v1.0.${{ github.run_number }} diff --git a/README.md b/README.md index 609d0514..5208d8a9 100644 --- a/README.md +++ b/README.md @@ -58,10 +58,13 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser ``` ## Install on macOS -``` -brew tap armosec/kubescape -brew install kubescape -``` + +1. ``` + brew tap armosec/kubescape + ``` +2. ``` + brew install kubescape + ``` ## Flags @@ -75,21 +78,22 @@ brew install kubescape | `--use-from` | | Load local framework object from specified path. If not used will download latest | | `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | | `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal | -| `--results-locally` | `false` | Kubescape sends scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features | `true`/`false`| +| `--submit` | `false` | If set, Kubescape will send scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. By default the results are not sent | `true`/`false`| +| `--local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false`| ## Usage & Examples ### Examples -* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework +* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to Armo portal ``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public +kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --submit ``` -* Scan a running Kubernetes cluster with [`mitre`](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/) framework +* Scan a running Kubernetes cluster with [`mitre`](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/) framework and submit results to Armo portal ``` -kubescape scan framework mitre --exclude-namespaces kube-system,kube-public +kubescape scan framework mitre --exclude-namespaces kube-system,kube-public --submit ``` * Scan local `yaml`/`json` files before deploying @@ -135,12 +139,12 @@ It is possible to run Kubescape offline! First download the framework and then scan with `--use-from` flag -* Download and save in file, if file name not specified, will store save to `~/.kubescape/.json` +1. Download and save in file, if file name not specified, will store save to `~/.kubescape/.json` ``` kubescape download framework nsa --output nsa.json ``` -* Scan using the downloaded framework +2. Scan using the downloaded framework ``` kubescape scan framework nsa --use-from nsa.json ``` diff --git a/build.py b/build.py index f18f0de5..7a623811 100644 --- a/build.py +++ b/build.py @@ -59,7 +59,7 @@ def main(): checkStatus(status, "Failed to get dependencies") except OSError: - print("An error occured: (Hint: check if go is installed)") + print("An error occurred: (Hint: check if go is installed)") raise # Build kubescape diff --git a/cautils/customerloader.go b/cautils/customerloader.go index c9fd39b1..3b08ea28 100644 --- a/cautils/customerloader.go +++ b/cautils/customerloader.go @@ -17,9 +17,15 @@ import ( const ( configMapName = "kubescape" - ConfigFileName = "config" + configFileName = "config" ) +func ConfigFileFullPath() string { return getter.GetDefaultPath(configFileName + ".json") } + +// ====================================================================================== +// =============================== Config structure ===================================== +// ====================================================================================== + type ConfigObj struct { CustomerGUID string `json:"customerGUID"` Token string `json:"invitationParam"` @@ -33,52 +39,103 @@ func (co *ConfigObj) Json() []byte { return []byte{} } +// ====================================================================================== +// =============================== interface ============================================ +// ====================================================================================== type IClusterConfig interface { - SetCustomerGUID() error + // setters + SetCustomerGUID(customerGUID string) error + + // getters GetCustomerGUID() string + GetConfigObj() *ConfigObj + GetK8sAPI() *k8sinterface.KubernetesApi + GetBackendAPI() getter.IBackend + GetDefaultNS() string GenerateURL() } -type ClusterConfig struct { - k8s *k8sinterface.KubernetesApi - defaultNS string - armoAPI *getter.ArmoAPI - configObj *ConfigObj +// ClusterConfigSetup - Setup the desired cluster behavior regarding submittion to the Armo BE +func ClusterConfigSetup(scanInfo *ScanInfo, k8s *k8sinterface.KubernetesApi, beAPI getter.IBackend) IClusterConfig { + /* + + If "First run (local config not found)" - + Default - Do not send report (local) + Local - Do not send report + Submit - Create tenant & Submit report + + If "Submitted but not signed up" - + Default - Submit report (submit) + Local - Delete local config & Do not send report + Submit - Submit report + + If "Signed up user" - + Default - Submit report (submit) + Local - Do not send report + Submit - Submit report + + */ + clusterConfig := NewClusterConfig(k8s, beAPI) + if !IsSubmitted(clusterConfig) { + if scanInfo.Submit { + return clusterConfig // submit - Create tenant & Submit report + } + return NewEmptyConfig() // local/default - Do not send report + } + if !IsRegistered(clusterConfig) { + if scanInfo.Local { + DeleteConfig(k8s) + return NewEmptyConfig() // local - Delete local config & Do not send report + } + return clusterConfig // submit/default - Submit report + } + if scanInfo.Local { + return NewEmptyConfig() // local - Do not send report + } + return clusterConfig // submit/default - Submit report } +// ====================================================================================== +// ============================= Mock Config ============================================ +// ====================================================================================== type EmptyConfig struct { } +func NewEmptyConfig() *EmptyConfig { return &EmptyConfig{} } +func (c *EmptyConfig) GetConfigObj() *ConfigObj { return &ConfigObj{} } +func (c *EmptyConfig) SetCustomerGUID(customerGUID string) error { return nil } +func (c *EmptyConfig) GetCustomerGUID() string { return "" } +func (c *EmptyConfig) GetK8sAPI() *k8sinterface.KubernetesApi { return nil } // TODO: return mock obj +func (c *EmptyConfig) GetDefaultNS() string { return k8sinterface.GetDefaultNamespace() } +func (c *EmptyConfig) GetBackendAPI() getter.IBackend { return nil } // TODO: return mock obj func (c *EmptyConfig) GenerateURL() { + message := fmt.Sprintf("If you wish to submit your cluster so you can control exceptions and maintain chronological scan results, please run Kubescape with the `--submit` flag or sign-up here: https://%s", getter.ArmoFEURL) + InfoTextDisplay(os.Stdout, message+"\n") } -func (c *EmptyConfig) SetCustomerGUID() error { - return nil +// ====================================================================================== +// ========================== Cluster Config ============================================ +// ====================================================================================== + +type ClusterConfig struct { + k8s *k8sinterface.KubernetesApi + defaultNS string + backendAPI getter.IBackend + configObj *ConfigObj } -func (c *EmptyConfig) GetCustomerGUID() string { - return "" -} - -func NewEmptyConfig() *EmptyConfig { - return &EmptyConfig{} -} - -func NewClusterConfig(k8s *k8sinterface.KubernetesApi, armoAPI *getter.ArmoAPI) *ClusterConfig { +func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBackend) *ClusterConfig { return &ClusterConfig{ - k8s: k8s, - armoAPI: armoAPI, - defaultNS: k8sinterface.GetDefaultNamespace(), + k8s: k8s, + backendAPI: backendAPI, + defaultNS: k8sinterface.GetDefaultNamespace(), } } -func createConfigJson() { - os.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), nil, 0664) +func (c *ClusterConfig) GetConfigObj() *ConfigObj { return c.configObj } +func (c *ClusterConfig) GetK8sAPI() *k8sinterface.KubernetesApi { return c.k8s } +func (c *ClusterConfig) GetDefaultNS() string { return c.defaultNS } +func (c *ClusterConfig) GetBackendAPI() getter.IBackend { return c.backendAPI } -} - -func update(configObj *ConfigObj) { - os.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), configObj.Json(), 0664) -} func (c *ClusterConfig) GenerateURL() { u := url.URL{} u.Scheme = "https" @@ -109,6 +166,82 @@ func (c *ClusterConfig) GetCustomerGUID() string { return "" } +func (c *ClusterConfig) SetCustomerGUID(customerGUID string) error { + + updateConfig := false + createConfig := false + + // get from configMap + if c.existsConfigMap() { + c.configObj, _ = c.loadConfigFromConfigMap() + } else if existsConfigFile() { // get from file + c.configObj, _ = loadConfigFromFile() + } else { + c.configObj = &ConfigObj{} + createConfig = true + } + if customerGUID != "" && c.GetCustomerGUID() != customerGUID { + c.configObj.CustomerGUID = customerGUID // override config customerGUID + updateConfig = true + } + + customerGUID = c.GetCustomerGUID() + + // get from armoBE + tenantResponse, err := c.backendAPI.GetCustomerGUID(customerGUID) + if err == nil && tenantResponse != nil { + if tenantResponse.AdminMail != "" { // this customer already belongs to some user + c.configObj.CustomerAdminEMail = tenantResponse.AdminMail + } else { + c.configObj.Token = tenantResponse.Token + c.configObj.CustomerGUID = tenantResponse.TenantID + } + updateConfig = true + } else { + if err != nil && !strings.Contains(err.Error(), "already exists") { + return err + } + } + if createConfig { + c.createConfigMap() + c.createConfigFile() + } else if updateConfig { + if c.existsConfigMap() { + c.updateConfigMap() + } + if existsConfigFile() { + c.updateConfigFile() + } + } + return nil +} +func (c *ClusterConfig) ToMapString() map[string]interface{} { + m := map[string]interface{}{} + bc, _ := json.Marshal(c.configObj) + json.Unmarshal(bc, &m) + return m +} +func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) { + if c.k8s == nil { + return nil, nil + } + configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + + if bData, err := json.Marshal(configMap.Data); err == nil { + return readConfig(bData) + } + return nil, nil +} + +func (c *ClusterConfig) existsConfigMap() bool { + _, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{}) + // TODO - check if has customerGUID + return err == nil +} + func (c *ClusterConfig) GetValueByKeyFromConfigMap(key string) (string, error) { configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{}) @@ -121,16 +254,17 @@ func (c *ClusterConfig) GetValueByKeyFromConfigMap(key string) (string, error) { } else { return "", fmt.Errorf("value does not exist") } - } func GetValueFromConfigJson(key string) (string, error) { - data, err := os.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json")) + data, err := os.ReadFile(ConfigFileFullPath()) if err != nil { return "", err } var obj map[string]interface{} - err = json.Unmarshal(data, &obj) + if err := json.Unmarshal(data, &obj); err != nil { + return "", err + } if val, ok := obj[key]; ok { return fmt.Sprint(val), nil } else { @@ -140,7 +274,7 @@ func GetValueFromConfigJson(key string) (string, error) { } func SetKeyValueInConfigJson(key string, value string) error { - data, err := os.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json")) + data, err := os.ReadFile(ConfigFileFullPath()) if err != nil { return err } @@ -156,7 +290,7 @@ func SetKeyValueInConfigJson(key string, value string) error { return err } - return os.WriteFile(getter.GetDefaultPath(ConfigFileName+".json"), newData, 0664) + return os.WriteFile(ConfigFileFullPath(), newData, 0664) } @@ -186,76 +320,11 @@ func (c *ClusterConfig) SetKeyValueInConfigmap(key string, value string) error { return err } -func (c *ClusterConfig) SetCustomerGUID() error { - - // get from configMap - if c.existsConfigMap() { - c.configObj, _ = c.loadConfigFromConfigMap() - } else if existsConfigJson() { // get from file - c.configObj, _ = loadConfigFromFile() - } else { - c.createConfigMap() - createConfigJson() - } - - customerGUID := c.GetCustomerGUID() - - // get from armoBE - tenantResponse, err := c.armoAPI.GetCustomerGUID(customerGUID) - if err == nil && tenantResponse != nil { - if tenantResponse.AdminMail != "" { // this customer already belongs to some user - if existsConfigJson() { - update(&ConfigObj{CustomerGUID: customerGUID, CustomerAdminEMail: tenantResponse.AdminMail}) - } - if c.existsConfigMap() { - c.configObj.CustomerAdminEMail = tenantResponse.AdminMail - c.updateConfigMap() - } - } else { - if existsConfigJson() { - update(&ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token}) - } - if c.existsConfigMap() { - c.configObj = &ConfigObj{CustomerGUID: tenantResponse.TenantID, Token: tenantResponse.Token} - c.updateConfigMap() - } - } - } else { - if err != nil && strings.Contains(err.Error(), "already exists") { - return nil - } - return err - } - return nil -} - -func (c *ClusterConfig) loadConfigFromConfigMap() (*ConfigObj, error) { - if c.k8s == nil { - return nil, nil - } - configMap, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - if bData, err := json.Marshal(configMap.Data); err == nil { - return readConfig(bData) - } - return nil, nil -} - -func (c *ClusterConfig) existsConfigMap() bool { - _, err := c.k8s.KubernetesClient.CoreV1().ConfigMaps(c.defaultNS).Get(context.Background(), configMapName, metav1.GetOptions{}) +func existsConfigFile() bool { + _, err := os.ReadFile(ConfigFileFullPath()) return err == nil } -func existsConfigJson() bool { - _, err := os.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json")) - - return err == nil - -} - func (c *ClusterConfig) createConfigMap() error { if c.k8s == nil { return nil @@ -287,6 +356,20 @@ func (c *ClusterConfig) updateConfigMap() error { return err } +func (c *ClusterConfig) updateConfigFile() error { + if err := os.WriteFile(ConfigFileFullPath(), c.configObj.Json(), 0664); err != nil { + return err + } + return nil +} + +func (c *ClusterConfig) createConfigFile() error { + if err := os.WriteFile(ConfigFileFullPath(), c.configObj.Json(), 0664); err != nil { + return err + } + return nil +} + func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) { if len(configMap.Data) == 0 { configMap.Data = make(map[string]string) @@ -299,7 +382,7 @@ func (c *ClusterConfig) updateConfigData(configMap *corev1.ConfigMap) { } } func loadConfigFromFile() (*ConfigObj, error) { - dat, err := os.ReadFile(getter.GetDefaultPath(ConfigFileName + ".json")) + dat, err := os.ReadFile(ConfigFileFullPath()) if err != nil { return nil, err } @@ -316,9 +399,38 @@ func readConfig(dat []byte) (*ConfigObj, error) { return configObj, err } -func (c *ClusterConfig) ToMapString() map[string]interface{} { - m := map[string]interface{}{} - bc, _ := json.Marshal(c.configObj) - json.Unmarshal(bc, &m) - return m + +// Check if the customer is submitted +func IsSubmitted(clusterConfig *ClusterConfig) bool { + return clusterConfig.existsConfigMap() || existsConfigFile() +} + +// Check if the customer is registered +func IsRegistered(clusterConfig *ClusterConfig) bool { + + // get from armoBE + tenantResponse, err := clusterConfig.backendAPI.GetCustomerGUID(clusterConfig.GetCustomerGUID()) + if err == nil && tenantResponse != nil { + if tenantResponse.AdminMail != "" { // this customer already belongs to some user + return true + } + } + return false +} + +func DeleteConfig(k8s *k8sinterface.KubernetesApi) error { + if err := DeleteConfigMap(k8s); err != nil { + return err + } + if err := DeleteConfigFile(); err != nil { + return err + } + return nil +} +func DeleteConfigMap(k8s *k8sinterface.KubernetesApi) error { + return k8s.KubernetesClient.CoreV1().ConfigMaps(k8sinterface.GetDefaultNamespace()).Delete(context.Background(), configMapName, metav1.DeleteOptions{}) +} + +func DeleteConfigFile() error { + return os.Remove(ConfigFileFullPath()) } diff --git a/cautils/getter/getpolicies.go b/cautils/getter/getpolicies.go index c66fb11c..59974d38 100644 --- a/cautils/getter/getpolicies.go +++ b/cautils/getter/getpolicies.go @@ -12,3 +12,6 @@ type IPolicyGetter interface { type IExceptionsGetter interface { GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) } +type IBackend interface { + GetCustomerGUID(customerGUID string) (*TenantResponse, error) +} diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index b0e681c6..5d8607dd 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -10,16 +10,19 @@ import ( type ScanInfo struct { Getters PolicyIdentifier opapolicy.PolicyIdentifier - UseExceptions string - UseFrom string - UseDefault bool - Format string - Output string - ExcludedNamespaces string - InputPatterns []string - Silent bool - FailThreshold uint16 - DoNotSendResults bool + UseExceptions string // Load exceptions configuration + UseFrom string // Load framework from local file (instead of download). Use when running offline + UseDefault bool // Load framework from cached file (instead of download). Use when running offline + Format string // Format results (table, json, junit ...) + Output string // Store results in an output file, Output file name + ExcludedNamespaces string // DEPRECATED? + InputPatterns []string // Yaml files input patterns + Silent bool // Silent mode - Do not print progress logs + FailThreshold uint16 // Failure score threshold + DoNotSendResults bool // DEPRECATED + Submit bool // Submit results to Armo BE + Local bool // Do not submit results + Account string // account ID } type Getters struct { diff --git a/cmd/framework.go b/cmd/framework.go index 9738e365..222d3056 100644 --- a/cmd/framework.go +++ b/cmd/framework.go @@ -95,7 +95,11 @@ func init() { frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code 1") - frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armosec backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features") + frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armo backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features") + frameworkCmd.Flags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Use this flag if you wish to send your Kubescape results to Armo backend to control exceptions and maintain chronological scan results. By default the results are not submitted") + frameworkCmd.Flags().BoolVarP(&scanInfo.Local, "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") + frameworkCmd.Flags().StringVarP(&scanInfo.Account, "account", "", "", "Account ID. Default will load accout ID from configMap/file") + } func CliSetup() error { @@ -119,15 +123,10 @@ func CliSetup() error { // policy handler setup policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s) - // load cluster config - var clusterConfig cautils.IClusterConfig - if !scanInfo.DoNotSendResults && k8sinterface.ConnectedToCluster { - clusterConfig = cautils.NewClusterConfig(k8s, getter.NewArmoAPI()) - } else { - clusterConfig = cautils.NewEmptyConfig() - } + // setup cluster config + clusterConfig := cautils.ClusterConfigSetup(&scanInfo, k8s, getter.NewArmoAPI()) - if err := clusterConfig.SetCustomerGUID(); err != nil { + if err := clusterConfig.SetCustomerGUID(scanInfo.Account); err != nil { fmt.Println(err) } cautils.CustomerGUID = clusterConfig.GetCustomerGUID() diff --git a/cmd/local_get.go b/cmd/local_get.go index e8499681..9807e794 100644 --- a/cmd/local_get.go +++ b/cmd/local_get.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/getter" "github.com/spf13/cobra" ) @@ -31,7 +30,7 @@ var localGetCmd = &cobra.Command{ val, err := cautils.GetValueFromConfigJson(key) if err != nil { if err.Error() == "value does not exist." { - fmt.Printf("Could net get value from: %s, reason: %s\n", getter.GetDefaultPath(cautils.ConfigFileName+".json"), err) + fmt.Printf("Could net get value from: %s, reason: %s\n", cautils.ConfigFileFullPath(), err) return nil } return err diff --git a/install.sh b/install.sh index 56dab602..c9cc5ceb 100755 --- a/install.sh +++ b/install.sh @@ -9,23 +9,17 @@ KUBESCAPE_EXEC=kubescape osName=$(uname -s) if [[ $osName == *"MINGW"* ]]; then - osName=windows-latest + osName=windows elif [[ $osName == *"Darwin"* ]]; then - osName=macos-latest + osName=macos else - osName=ubuntu-latest + osName=ubuntu fi -GITHUB_OWNER=armosec - -DOWNLOAD_URL=$(curl --silent "https://api.github.com/repos/$GITHUB_OWNER/kubescape/releases/latest" | grep -o "browser_download_url.*${osName}.*") -DOWNLOAD_URL=${DOWNLOAD_URL//\"} -DOWNLOAD_URL=${DOWNLOAD_URL/browser_download_url: /} - mkdir -p $BASE_DIR OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC - +DOWNLOAD_URL="https://github.com/armosec/kubescape/releases/latest/download/kubescape-${osName}-latest" curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT # Checking if SUDO needed/exists From 0c79df7299c693e4df7be40130c07aaf86611bff Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 11 Oct 2021 16:29:26 +0300 Subject: [PATCH 6/7] restore id after local run, update readme --- README.md | 5 +++-- cautils/customerloader.go | 3 +++ cmd/config.go | 1 - cmd/framework.go | 28 ++++++++++++++++++---------- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5208d8a9..bf984bd0 100644 --- a/README.md +++ b/README.md @@ -80,18 +80,19 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal | | `--submit` | `false` | If set, Kubescape will send scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. By default the results are not sent | `true`/`false`| | `--local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false`| +| `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | | ## Usage & Examples ### Examples -* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to Armo portal +* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to [Armo portal](https://portal.armo.cloud/) ``` kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --submit ``` -* Scan a running Kubernetes cluster with [`mitre`](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/) framework and submit results to Armo portal +* Scan a running Kubernetes cluster with [`mitre`](https://www.microsoft.com/security/blog/2020/04/02/attack-matrix-kubernetes/) framework and submit results to [Armo portal](https://portal.armo.cloud/) ``` kubescape scan framework mitre --exclude-namespaces kube-system,kube-public --submit ``` diff --git a/cautils/customerloader.go b/cautils/customerloader.go index 3b08ea28..339c4ddf 100644 --- a/cautils/customerloader.go +++ b/cautils/customerloader.go @@ -76,6 +76,9 @@ func ClusterConfigSetup(scanInfo *ScanInfo, k8s *k8sinterface.KubernetesApi, beA */ clusterConfig := NewClusterConfig(k8s, beAPI) + if err := clusterConfig.SetCustomerGUID(scanInfo.Account); err != nil { + fmt.Println(err) + } if !IsSubmitted(clusterConfig) { if scanInfo.Submit { return clusterConfig // submit - Create tenant & Submit report diff --git a/cmd/config.go b/cmd/config.go index 9b4e4d4a..10b0f3ec 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -10,7 +10,6 @@ var configCmd = &cobra.Command{ Short: "Set configuration", Long: ``, Run: func(cmd *cobra.Command, args []string) { - }, } diff --git a/cmd/framework.go b/cmd/framework.go index 222d3056..f4589e3b 100644 --- a/cmd/framework.go +++ b/cmd/framework.go @@ -95,20 +95,16 @@ func init() { frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code 1") - frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Kubescape sends scan results to Armo backend to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features") + frameworkCmd.Flags().BoolVarP(&scanInfo.DoNotSendResults, "results-locally", "", false, "Deprecated. Please use `--local` instead") frameworkCmd.Flags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Use this flag if you wish to send your Kubescape results to Armo backend to control exceptions and maintain chronological scan results. By default the results are not submitted") frameworkCmd.Flags().BoolVarP(&scanInfo.Local, "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") - frameworkCmd.Flags().StringVarP(&scanInfo.Account, "account", "", "", "Account ID. Default will load accout ID from configMap/file") + frameworkCmd.Flags().StringVarP(&scanInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file") } func CliSetup() error { flag.Parse() - - if 100 < scanInfo.FailThreshold { - fmt.Println("bad argument: out of range threshold") - os.Exit(1) - } + flagValidation() var k8s *k8sinterface.KubernetesApi if !scanInfo.ScanRunningCluster() { @@ -126,9 +122,6 @@ func CliSetup() error { // setup cluster config clusterConfig := cautils.ClusterConfigSetup(&scanInfo, k8s, getter.NewArmoAPI()) - if err := clusterConfig.SetCustomerGUID(scanInfo.Account); err != nil { - fmt.Println(err) - } cautils.CustomerGUID = clusterConfig.GetCustomerGUID() cautils.ClusterName = k8sinterface.GetClusterName() @@ -188,3 +181,18 @@ func (clihandler *CLIHandler) Scan() error { } return nil } + +func flagValidation() { + if scanInfo.DoNotSendResults { + fmt.Println("Deprecated. Please use `--local` instead") + } + + if scanInfo.Submit && scanInfo.Local { + fmt.Println("You can use `local` or `submit`, but not both") + os.Exit(1) + } + if 100 < scanInfo.FailThreshold { + fmt.Println("bad argument: out of range threshold") + os.Exit(1) + } +} From ce2f3dafae1d4371db6bd7f3c565f27f1d425eb9 Mon Sep 17 00:00:00 2001 From: Ferhat Yildiz Date: Fri, 8 Oct 2021 13:51:16 +0200 Subject: [PATCH 7/7] removed references to go mod tidy since there is a go.sum file now --- README.md | 40 +++++++++++++++++++--------------------- build.py | 11 +---------- build/Dockerfile | 3 +-- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index bf984bd0..023b0c88 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Kubescape is the first tool for testing if Kubernetes is deployed securely as defined in [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) -Use Kubescape to test clusters or scan single YAML files and integrate it to your processes. +Use Kubescape to test clusters or scan single YAML files and integrate it to your processes. @@ -28,16 +28,16 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp -### Click [πŸ‘](https://github.com/armosec/kubescape/stargazers) if you want us to continue to develop and improve Kubescape πŸ˜€ +### Click [πŸ‘](https://github.com/armosec/kubescape/stargazers) if you want us to continue to develop and improve Kubescape πŸ˜€ # Being part of the team -We invite you to our team! We are excited about this project and want to return the love we get. +We invite you to our team! We are excited about this project and want to return the love we get. Want to contribute? Want to discuss something? Have an issue? * Open a issue, we are trying to respond within 48 hours -* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! +* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! [logo](https://armosec.github.io/kubescape/) @@ -73,7 +73,7 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` 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` | +| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` | | `-o`/`--output` | print to stdout | Save scan result in file | | `--use-from` | | Load local framework object from specified path. If not used will download latest | | `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | @@ -83,10 +83,10 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | | ## Usage & Examples - + ### Examples -* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to [Armo portal](https://portal.armo.cloud/) +* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to [Armo portal](https://portal.armo.cloud/) ``` kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --submit ``` @@ -103,17 +103,17 @@ kubescape scan framework nsa *.yaml ``` -* Scan `yaml`/`json` files from url +* Scan `yaml`/`json` files from url ``` kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml ``` -* Output in `json` format +* Output in `json` format ``` kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json ``` -* Output in `junit xml` format +* Output in `junit xml` format ``` kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml ``` @@ -125,7 +125,7 @@ kubescape scan framework nsa --exceptions examples/exceptions.json ### Helm Support -* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout +* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout ``` helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa - ``` @@ -145,14 +145,14 @@ First download the framework and then scan with `--use-from` flag kubescape download framework nsa --output nsa.json ``` -2. Scan using the downloaded framework +2. Scan using the downloaded framework ``` kubescape scan framework nsa --use-from nsa.json ``` Kubescape is an open source project, we welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops. -# How to build +# How to build ## Build using python script @@ -182,7 +182,7 @@ git clone https://github.com/armosec/kubescape.git kubescape && cd "$_" 2. Build ``` -go mod tidy && go build -o kubescape . +go build -o kubescape . ``` 3. Run @@ -209,14 +209,14 @@ docker build -t kubescape -f build/Dockerfile . ## Tests Kubescape is running the following tests according to what is defined by [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/News-Features/Feature-Stories/Article-View/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) * Non-root containers -* Immutable container filesystem -* Privileged containers +* Immutable container filesystem +* Privileged containers * hostPID, hostIPC privileges * hostNetwork access * allowedHostPaths field * Protecting pod service account tokens * Resource policies -* Control plane hardening +* Control plane hardening * Exposed dashboard * Allow privilege escalation * Applications credentials in configuration files @@ -233,12 +233,10 @@ Kubescape is running the following tests according to what is defined by [Kubern ## Technology -Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls. +Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls. -The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/). +The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/). The results by default printed in a pretty "console friendly" manner, but they can be retrieved in JSON format for further processing. Kubescape is an open source project, we welcome your feedback and ideas for improvement. We’re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops. - - diff --git a/build.py b/build.py index 7a623811..2687e0ef 100644 --- a/build.py +++ b/build.py @@ -53,15 +53,6 @@ def main(): if not os.path.isdir(buildDir): os.makedirs(buildDir) - # Get dependencies - try: - status = subprocess.call(["go", "mod", "tidy"]) - checkStatus(status, "Failed to get dependencies") - - except OSError: - print("An error occurred: (Hint: check if go is installed)") - raise - # Build kubescape ldflags = "-w -s -X %s=%s -X %s=%s -X %s=%s -X %s=%s" \ % (buildUrl, releaseVersion, BE_SERVER_CONST, ArmoBEServer, @@ -79,4 +70,4 @@ def main(): print("Build Done") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/build/Dockerfile b/build/Dockerfile index 9e3fee01..4cf8dcb3 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -4,13 +4,12 @@ ENV GO111MODULE=on WORKDIR /work ADD . . -RUN go mod tidy RUN GOOS=linux CGO_ENABLED=0 go build -ldflags="-s -w " -installsuffix cgo -o kubescape . FROM alpine COPY --from=builder /work/kubescape /usr/bin/kubescape -# # Download the frameworks. Use the "--use-default" flag when running kubescape +# # Download the frameworks. Use the "--use-default" flag when running kubescape # RUN kubescape download framework nsa && kubescape download framework mitre CMD ["kubescape"]