diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 5b891444..f9602b81 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,9 +2,7 @@ name: build
on:
push:
- branches: [ master ]
- pull_request:
- branches: [ master ]
+ branches: [ master ]
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/.github/workflows/master_pr_checks.yaml b/.github/workflows/master_pr_checks.yaml
new file mode 100644
index 00000000..ff96fd7a
--- /dev/null
+++ b/.github/workflows/master_pr_checks.yaml
@@ -0,0 +1,38 @@
+name: master-pr
+
+on:
+ pull_request:
+ branches: [ master ]
+ types: [ edited, opened, synchronize, reopened ]
+jobs:
+ build:
+ name: Create cross-platform build
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ steps:
+ - uses: actions/checkout@v1
+
+ - name: Set up Go
+ 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 }}
+ ArmoBEServer: api.armo.cloud
+ ArmoERServer: report.armo.cloud
+ ArmoWebsite: portal.armo.cloud
+ CGO_ENABLED: 0
+ run: python build.py
+
+ - name: Upload build artifacts
+ uses: actions/upload-artifact@v2
+ with:
+ name: kubescape-${{ matrix.os }}
+ path: build/${{ matrix.os }}/kubescape
diff --git a/README.md b/README.md
index 609d0514..ba65ee9c 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!
[
](https://armosec.github.io/kubescape/)
@@ -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
@@ -70,45 +73,47 @@ brew install kubescape
| `-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` |
| `--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 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 sent | `true`/`false`|
+| `--keep-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
+* 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
+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](https://portal.armo.cloud/)
```
-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
+* Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI)
```
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
```
@@ -120,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 -
```
@@ -135,19 +140,19 @@ 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
```
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
@@ -177,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
@@ -204,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
@@ -228,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 f18f0de5..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 occured: (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"]
diff --git a/cautils/customerloader.go b/cautils/customerloader.go
index c9fd39b1..0eef7348 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,62 +39,117 @@ 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 - Delete local config & Do not send report (local)
+ 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)
+ clusterConfig.LoadConfig()
+
+ 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.Submit {
+ return clusterConfig // submit/default - Submit report
+ }
+ DeleteConfig(k8s)
+ return NewEmptyConfig() // local - Delete local config & Do not send 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("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 registering 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,
+ configObj: &ConfigObj{},
+ 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"
u.Host = getter.ArmoFEURL
if c.configObj == nil {
return
}
+ message := fmt.Sprintf("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 registering here: %s", u.String())
if c.configObj.CustomerAdminEMail != "" {
- msgStr := fmt.Sprintf("To view all controls and get remediation's ask access permissions to %s from %s", u.String(), c.configObj.CustomerAdminEMail)
- InfoTextDisplay(os.Stdout, msgStr+"\n")
+ InfoTextDisplay(os.Stdout, message+"\n")
return
}
u.Path = "account/sign-up"
@@ -97,8 +158,7 @@ func (c *ClusterConfig) GenerateURL() {
q.Add("customerGUID", c.configObj.CustomerGUID)
u.RawQuery = q.Encode()
- fmt.Println("To view all controls and get remediation's visit:")
- InfoTextDisplay(os.Stdout, u.String()+"\n")
+ InfoTextDisplay(os.Stdout, message+"\n")
}
@@ -109,6 +169,82 @@ func (c *ClusterConfig) GetCustomerGUID() string {
return ""
}
+func (c *ClusterConfig) SetCustomerGUID(customerGUID string) error {
+
+ if customerGUID != "" && c.GetCustomerGUID() != customerGUID {
+ c.configObj.CustomerGUID = customerGUID // override config customerGUID
+ }
+
+ 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
+ }
+ } else {
+ if err != nil && !strings.Contains(err.Error(), "already exists") {
+ return err
+ }
+ }
+
+ // update/create config
+ if c.existsConfigMap() {
+ c.updateConfigMap()
+ } else {
+ c.createConfigMap()
+ }
+ if existsConfigFile() {
+ c.updateConfigFile()
+ } else {
+ c.createConfigFile()
+ }
+
+ return nil
+}
+
+func (c *ClusterConfig) LoadConfig() {
+ // get from configMap
+ if c.existsConfigMap() {
+ c.configObj, _ = c.loadConfigFromConfigMap()
+ } else if existsConfigFile() { // get from file
+ c.configObj, _ = loadConfigFromFile()
+ } else {
+ c.configObj = &ConfigObj{}
+ }
+}
+
+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 +257,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 +277,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 +293,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 +323,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 +359,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 +385,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 +402,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/opapolicy/datastructures.go b/cautils/opapolicy/datastructures.go
index 30c031ab..ee36c234 100644
--- a/cautils/opapolicy/datastructures.go
+++ b/cautils/opapolicy/datastructures.go
@@ -41,7 +41,8 @@ type FrameworkReport struct {
}
type ControlReport struct {
armotypes.PortalBase `json:",inline"`
- ControlID string `json:"id"`
+ Control_ID string `json:"id,omitempty"` // to be Deprecated
+ ControlID string `json:"controlID"`
Name string `json:"name"`
RuleReports []RuleReport `json:"ruleReports"`
Remediation string `json:"remediation"`
@@ -101,7 +102,8 @@ type PolicyRule struct {
// Control represents a collection of rules which are combined together to single purpose
type Control struct {
armotypes.PortalBase `json:",inline"`
- ControlID string `json:"id"`
+ Control_ID string `json:"id,omitempty"` // to be Deprecated
+ ControlID string `json:"controlID"`
CreationTime string `json:"creationTime"`
Description string `json:"description"`
Remediation string `json:"remediation"`
diff --git a/cautils/opapolicy/datastructuresmethods.go b/cautils/opapolicy/datastructuresmethods.go
index 57a499e8..56835967 100644
--- a/cautils/opapolicy/datastructuresmethods.go
+++ b/cautils/opapolicy/datastructuresmethods.go
@@ -124,9 +124,9 @@ func (ruleReport *RuleReport) GetNumberOfFailedResources() int {
sum := 0
for i := len(ruleReport.RuleResponses) - 1; i >= 0; i-- {
if ruleReport.RuleResponses[i].GetSingleResultStatus() == "failed" {
- if !ruleReport.DeleteIfRedundantResponse(&ruleReport.RuleResponses[i], i) {
- sum++
- }
+ //if !ruleReport.DeleteIfRedundantResponse(&ruleReport.RuleResponses[i], i) {
+ sum += len(ruleReport.RuleResponses[i].AlertObject.K8SApiObjects)
+ //}
}
}
return sum
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/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])
}
}
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/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 34d24549..6ac188a6 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,
@@ -85,24 +87,24 @@ 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")
+ 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, "Deprecated. Please use `--keep-local` instead")
+ frameworkCmd.Flags().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")
+ frameworkCmd.Flags().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")
+ 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() {
@@ -117,17 +119,12 @@ 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()
- }
-
- if err := clusterConfig.SetCustomerGUID(); err != nil {
+ // 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()
@@ -187,3 +184,18 @@ func (clihandler *CLIHandler) Scan() error {
}
return nil
}
+
+func flagValidation() {
+ if scanInfo.DoNotSendResults {
+ fmt.Println("Deprecated. Please use `--keep-local` instead")
+ }
+
+ if scanInfo.Submit && scanInfo.Local {
+ fmt.Println("You can use `keep-local` or `submit`, but not both")
+ os.Exit(1)
+ }
+ if 100 < scanInfo.FailThreshold {
+ fmt.Println("bad argument: out of range threshold")
+ os.Exit(1)
+ }
+}
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/docs/summary.png b/docs/summary.png
index da405856..72007f72 100644
Binary files a/docs/summary.png and b/docs/summary.png differ
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
diff --git a/opaprocessor/processorhandler.go b/opaprocessor/processorhandler.go
index 78c2f6a4..44f059d5 100644
--- a/opaprocessor/processorhandler.go
+++ b/opaprocessor/processorhandler.go
@@ -126,6 +126,7 @@ func (opap *OPAProcessor) processControl(control *opapolicy.Control) (*opapolicy
controlReport := opapolicy.ControlReport{}
controlReport.PortalBase = control.PortalBase
controlReport.ControlID = control.ControlID
+ controlReport.Control_ID = control.Control_ID // TODO: delete when 'id' is deprecated
controlReport.Name = control.Name
controlReport.Description = control.Description
diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go
index d77eb565..e652e60e 100644
--- a/resultshandling/printer/printresults.go
+++ b/resultshandling/printer/printresults.go
@@ -107,7 +107,7 @@ func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {
printer.summary[cr.Name] = ControlSummary{
TotalResources: cr.GetNumberOfResources(),
- TotalFailed: cr.GetNumberOfFailedResources(),
+ TotalFailed: len(workloadsSummary) - cr.GetNumberOfWarningResources(),
TotalWarnign: cr.GetNumberOfWarningResources(),
WorkloadSummary: mapResources,
Description: cr.Description,