diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 081f3939..03c6b1ac 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -46,6 +46,12 @@ jobs:
CGO_ENABLED: 0
run: python3 --version && python3 build.py
+ - name: Smoke Testing
+ env:
+ RELEASE: v1.0.${{ github.run_number }}
+ KUBESCAPE_SKIP_UPDATE_CHECK: "true"
+ run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
+
- name: Upload Release binaries
id: upload-release-asset
uses: actions/upload-release-asset@v1
@@ -71,8 +77,8 @@ jobs:
run: echo quay.io/armosec/kubescape:v1.0.${{ github.run_number }} > build_tag.txt
- name: Build the Docker image
- run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt)
-
+ run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt) --build-arg run_number=${{ github.run_number }}
+
- name: Re-Tag Image to latest
run: docker tag $(cat build_tag.txt) quay.io/armosec/kubescape:latest
diff --git a/.github/workflows/build_dev.yaml b/.github/workflows/build_dev.yaml
index 9bbd45a5..2ee5e2b0 100644
--- a/.github/workflows/build_dev.yaml
+++ b/.github/workflows/build_dev.yaml
@@ -30,6 +30,12 @@ jobs:
CGO_ENABLED: 0
run: python3 --version && python3 build.py
+ - name: Smoke Testing
+ env:
+ RELEASE: v1.0.${{ github.run_number }}
+ KUBESCAPE_SKIP_UPDATE_CHECK: "true"
+ run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
+
- name: Upload build artifacts
uses: actions/upload-artifact@v2
with:
@@ -50,7 +56,7 @@ jobs:
run: echo quay.io/armosec/kubescape:dev-v1.0.${{ github.run_number }} > build_tag.txt
- name: Build the Docker image
- run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt)
+ run: docker build . --file build/Dockerfile --tag $(cat build_tag.txt) --build-arg run_number=${{ github.run_number }}
- name: Login to Quay.io
env: # Or as an environment variable
diff --git a/.github/workflows/master_pr_checks.yaml b/.github/workflows/master_pr_checks.yaml
index 53fca59f..b4a3bbca 100644
--- a/.github/workflows/master_pr_checks.yaml
+++ b/.github/workflows/master_pr_checks.yaml
@@ -31,8 +31,9 @@ jobs:
CGO_ENABLED: 0
run: python3 --version && python3 build.py
- - name: Upload build artifacts
- uses: actions/upload-artifact@v2
- with:
- name: kubescape-${{ matrix.os }}
- path: build/${{ matrix.os }}/kubescape
+ - name: Smoke Testing
+ env:
+ RELEASE: v1.0.${{ github.run_number }}
+ KUBESCAPE_SKIP_UPDATE_CHECK: "true"
+ run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape
+
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index bf33394d..3c3ed147 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
*kubescape*
*debug*
*vender*
+*.pyc*
.idea
\ No newline at end of file
diff --git a/README.md b/README.md
index 6bcc3cb6..d885269d 100644
--- a/README.md
+++ b/README.md
@@ -24,13 +24,22 @@ curl -s https://raw.githubusercontent.com/armosec/kubescape/master/install.sh |
## Run:
```
-kubescape scan framework nsa
+kubescape scan framework nsa --submit
```
+
+
+> 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.
+
+
+
### 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.
@@ -42,8 +51,16 @@ Want to contribute? Want to discuss something? Have an issue?
[
](https://armosec.github.io/kubescape/)
+
# Options and examples
+## Tutorials
+
+* [Overview](https://youtu.be/wdBkt_0Qhbg)
+* [Scanning Kubernetes YAML files](https://youtu.be/Ox6DaR7_4ZI)
+* [Scan Kubescape on an air-gapped environment (offline support)](https://youtu.be/IGXL9s37smM)
+* [Managing exceptions in the Kubescape SaaS version](https://youtu.be/OzpvxGmCR80)
+
## Install on Windows
**Requires powershell v5.0+**
@@ -69,80 +86,99 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser
## Flags
-| flag | default | description | options |
-| --- | --- | --- | --- |
-| `-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` |
-| `-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 |
-| `--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 | |
+| 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 is below threshold | `0` -> `100` |
+| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus` |
+| `-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 | |
+| `--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 and submit results to [ARMO portal](https://portal.armo.cloud/)
+#### Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to the [Kubescape SaaS version](https://portal.armo.cloud/)
```
kubescape scan framework nsa --submit
```
-* Scan a running Kubernetes cluster with [`MITRE ATT&CK®`](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) framework and submit results to [ARMO portal](https://portal.armo.cloud/)
+#### Scan a running Kubernetes cluster with [`MITRE ATT&CK®`](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) framework and submit results to the [Kubescape SaaS version](https://portal.armo.cloud/)
```
kubescape scan framework mitre --submit
```
-* Scan a running Kubernetes cluster with a specific control using the control name or control ID. [List of controls](https://hub.armo.cloud/docs/controls)
+#### Scan a running Kubernetes cluster with a specific control using the control name or control ID. [List of controls](https://hub.armo.cloud/docs/controls)
```
kubescape scan control "Privileged container"
```
-* Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI)
+#### 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
+#### Scan kubernetes manifest files from a public github repository
```
kubescape scan framework nsa https://github.com/armosec/kubescape
```
-* Output in `json` format
+#### 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
+#### 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
```
-* Scan with exceptions, objects with exceptions will be presented as `exclude` and not `fail`
+#### Output in `prometheus` metrics format - Contributed by [@Joibel](https://github.com/Joibel)
```
-kubescape scan framework nsa --exceptions examples/exceptions.json
+kubescape scan framework nsa --format prometheus
```
-### Helm Support
+#### Scan with exceptions, objects with exceptions will be presented as `exclude` and not `fail`
+[Full documentation](examples/exceptions/README.md)
+```
+kubescape scan framework nsa --exceptions examples/exceptions/exclude-kube-namespaces.json
+```
-* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout
+#### Scan Helm charts - 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 -
```
-for example:
+e.g.
```
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
```
+
+
### Offline Support
+[Video tutorial](https://youtu.be/IGXL9s37smM)
+
It is possible to run Kubescape offline!
First download the framework and then scan with `--use-from` flag
@@ -157,13 +193,40 @@ kubescape download framework nsa --output nsa.json
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.
+## Scan Periodically using Helm - Contributed by [@yonahd](https://github.com/yonahd)
+
+You can scan your cluster periodically by adding a `CronJob` that will repeatedly trigger kubescape
+
+```
+helm install kubescape examples/helm_chart/
+```
+
+## Scan using docker image
+
+Official Docker image `quay.io/armosec/kubescape`
+
+```
+docker run -v "$(pwd)/example.yaml:/app/example.yaml quay.io/armosec/kubescape scan framework nsa /app/example.yaml
+```
+
+# Submit data manually
+
+Use the `submit` command if you wish to submit data manually
+
+## Submit scan results manually
+
+First, scan your cluster using the `json` format flag: `kubescape scan framework --format json --output path/to/results.json`.
+
+Now you can submit the results to the Kubaescape SaaS version -
+```
+kubescape submit results path/to/results.json
+```
# How to build
## Build using python (3.7^) script
-Kubescpae can be built using:
+Kubescape can be built using:
``` sh
python build.py
@@ -199,12 +262,8 @@ go build -o kubescape .
4. Enjoy :zany_face:
-## Docker Support
+## Docker Build
-### Official Docker image
-```
-quay.io/armosec/kubescape
-```
### Build your own Docker image
1. Clone Project
@@ -217,10 +276,11 @@ git clone https://github.com/armosec/kubescape.git kubescape && cd "$_"
docker build -t kubescape -f build/Dockerfile .
```
+
# Under the hood
## 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/)
+Kubescape is running the following tests according to what is defined by [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/)
* Non-root containers
* Immutable container filesystem
* Privileged containers
diff --git a/build.py b/build.py
index 2104775d..ec8a5497 100644
--- a/build.py
+++ b/build.py
@@ -41,7 +41,7 @@ def main():
# Set some variables
packageName = getPackageName()
- buildUrl = "github.com/armosec/kubescape/clihandler/cmd.BuildNumber"
+ buildUrl = "github.com/armosec/kubescape/cautils.BuildNumber"
releaseVersion = os.getenv("RELEASE")
ArmoBEServer = os.getenv("ArmoBEServer")
ArmoERServer = os.getenv("ArmoERServer")
@@ -60,9 +60,6 @@ def main():
status = subprocess.call(["go", "build", "-o", "%s/%s" % (buildDir, packageName), "-ldflags" ,ldflags])
checkStatus(status, "Failed to build kubescape")
- test_cli_prints(buildDir,packageName)
-
-
sha1 = hashlib.sha1()
with open(buildDir + "/" + packageName, "rb") as kube:
sha1.update(kube.read())
@@ -70,13 +67,7 @@ def main():
kube_sha.write(sha1.hexdigest())
print("Build Done")
-
-def test_cli_prints(buildDir,packageName):
- bin_cli = os.path.abspath(os.path.join(buildDir,packageName))
-
- print(f"testing CLI prints on {bin_cli}")
- status = str(subprocess.check_output([bin_cli, "-h"]))
- assert "download" in status, "download is missing: " + status
-
+
+
if __name__ == "__main__":
main()
diff --git a/build/Dockerfile b/build/Dockerfile
index f80803cb..55e168c5 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -1,5 +1,10 @@
FROM golang:1.17-alpine as builder
#ENV GOPROXY=https://goproxy.io,direct
+
+ARG run_number
+
+ENV RELEASE=v1.0.${run_number}
+
ENV GO111MODULE=
ENV CGO_ENABLED=0
diff --git a/cautils/customerloader.go b/cautils/customerloader.go
index 499e7630..6bbacf0a 100644
--- a/cautils/customerloader.go
+++ b/cautils/customerloader.go
@@ -8,10 +8,10 @@ import (
"os"
"strings"
- "github.com/armosec/kubescape/cautils/getter"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/armosec/k8s-interface/k8sinterface"
+ "github.com/armosec/kubescape/cautils/getter"
corev1 "k8s.io/api/core/v1"
)
@@ -109,8 +109,10 @@ func ClusterConfigSetup(scanInfo *ScanInfo, k8s *k8sinterface.KubernetesApi, beA
return NewEmptyConfig() // local - Delete local config & Do not send report
}
if scanInfo.Local {
+ scanInfo.Submit = false
return NewEmptyConfig() // local - Do not send report
}
+ scanInfo.Submit = true
return clusterConfig // submit/default - Submit report
}
@@ -127,9 +129,9 @@ 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) GetClusterName() string { return k8sinterface.GetClusterName() }
+func (c *EmptyConfig) GetClusterName() string { return adoptClusterName(k8sinterface.GetClusterName()) }
func (c *EmptyConfig) GenerateURL() {
- message := fmt.Sprintf("\nCheckout for more cool features: https://%s\n", getter.GetArmoAPIConnector().GetFrontendURL())
+ message := fmt.Sprintf("\nYou 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\n", getter.GetArmoAPIConnector().GetFrontendURL())
InfoTextDisplay(os.Stdout, fmt.Sprintf("\n%s\n", message))
}
@@ -152,12 +154,14 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken
defaultNS: k8sinterface.GetDefaultNamespace(),
}
}
+
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 (c *ClusterConfig) GenerateURL() {
+ message := "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: "
u := url.URL{}
u.Scheme = "https"
@@ -165,9 +169,8 @@ func (c *ClusterConfig) GenerateURL() {
if c.configObj == nil {
return
}
- message := fmt.Sprintf("\nCheckout for more cool features: https://%s\n", getter.GetArmoAPIConnector().GetFrontendURL())
if c.configObj.CustomerAdminEMail != "" {
- InfoTextDisplay(os.Stdout, message+"\n")
+ InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n")
return
}
u.Path = "account/sign-up"
@@ -176,8 +179,7 @@ func (c *ClusterConfig) GenerateURL() {
q.Add("customerGUID", c.configObj.CustomerGUID)
u.RawQuery = q.Encode()
- InfoTextDisplay(os.Stdout, message+"\n")
-
+ InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n")
}
func (c *ClusterConfig) GetCustomerGUID() string {
@@ -242,7 +244,7 @@ func (c *ClusterConfig) setCustomerGUID(customerGUID string) {
}
func (c *ClusterConfig) setClusterName(clusterName string) {
- c.configObj.ClusterName = clusterName
+ c.configObj.ClusterName = adoptClusterName(clusterName)
}
func (c *ClusterConfig) GetClusterName() string {
return c.configObj.ClusterName
@@ -471,3 +473,7 @@ func DeleteConfigMap(k8s *k8sinterface.KubernetesApi) error {
func DeleteConfigFile() error {
return os.Remove(ConfigFileFullPath())
}
+
+func adoptClusterName(clusterName string) string {
+ return strings.ReplaceAll(clusterName, "/", "-")
+}
diff --git a/cautils/datastructures.go b/cautils/datastructures.go
index 2639f734..f9c92550 100644
--- a/cautils/datastructures.go
+++ b/cautils/datastructures.go
@@ -13,6 +13,7 @@ type OPASessionObj struct {
K8SResources *K8SResources
Exceptions []armotypes.PostureExceptionPolicy
PostureReport *reporthandling.PostureReport
+ RegoInputData RegoInputData // map[][]
}
func NewOPASessionObj(frameworks []reporthandling.Framework, k8sResources *K8SResources) *OPASessionObj {
@@ -49,3 +50,9 @@ type Exception struct {
Namespaces []string `json:"namespaces"`
Regex string `json:"regex"` // not supported
}
+
+type RegoInputData struct {
+ PostureControlInputs map[string][]string `json:"postureControlInputs"`
+ // ClusterName string `json:"clusterName"`
+ // K8sConfig RegoK8sConfig `json:"k8sconfig"`
+}
diff --git a/cautils/datastructuresmethods.go b/cautils/datastructuresmethods.go
new file mode 100644
index 00000000..6cd9941e
--- /dev/null
+++ b/cautils/datastructuresmethods.go
@@ -0,0 +1,26 @@
+package cautils
+
+import (
+ "encoding/json"
+
+ "github.com/open-policy-agent/opa/storage"
+ "github.com/open-policy-agent/opa/storage/inmem"
+ "github.com/open-policy-agent/opa/util"
+)
+
+func (data *RegoInputData) SetControlsInputs(controlsInputs map[string][]string) {
+ data.PostureControlInputs = controlsInputs
+}
+
+func (data *RegoInputData) TOStorage() (storage.Store, error) {
+ var jsonObj map[string]interface{}
+ bytesData, err := json.Marshal(*data)
+ if err != nil {
+ return nil, err
+ }
+ // glog.Infof("RegoDependenciesData: %s", bytesData)
+ if err := util.UnmarshalJSON(bytesData, &jsonObj); err != nil {
+ return nil, err
+ }
+ return inmem.NewFromObject(jsonObj), nil
+}
diff --git a/cautils/getter/armoapi.go b/cautils/getter/armoapi.go
index 5c4d299d..87e04395 100644
--- a/cautils/getter/armoapi.go
+++ b/cautils/getter/armoapi.go
@@ -1,11 +1,14 @@
package getter
import (
+ "encoding/json"
"fmt"
"net/http"
+ "strings"
"time"
"github.com/armosec/armoapi-go/armotypes"
+ "github.com/armosec/opa-utils/gitregostore"
"github.com/armosec/opa-utils/reporthandling"
"github.com/golang/glog"
)
@@ -28,10 +31,12 @@ var (
// Armo API for downloading policies
type ArmoAPI struct {
- httpClient *http.Client
- apiURL string
- erURL string
- feURL string
+ httpClient *http.Client
+ apiURL string
+ erURL string
+ feURL string
+ customerGUID string
+ gs *gitregostore.GitRegoStore
}
var globalArmoAPIConnecctor *ArmoAPI
@@ -80,9 +85,13 @@ func NewARMOAPICustomized(armoERURL, armoBEURL, armoFEURL string) *ArmoAPI {
func newArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{Timeout: time.Duration(61) * time.Second},
+ gs: gitregostore.InitDefaultGitRegoStore(-1),
}
}
+func (armoAPI *ArmoAPI) SetCustomerGUID(customerGUID string) {
+ armoAPI.customerGUID = customerGUID
+}
func (armoAPI *ArmoAPI) GetFrontendURL() string {
return armoAPI.feURL
}
@@ -92,7 +101,7 @@ func (armoAPI *ArmoAPI) GetReportReceiverURL() string {
}
func (armoAPI *ArmoAPI) GetFramework(name string) (*reporthandling.Framework, error) {
- respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name))
+ respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getFrameworkURL(name), nil)
if err != nil {
return nil, err
}
@@ -106,12 +115,26 @@ func (armoAPI *ArmoAPI) GetFramework(name string) (*reporthandling.Framework, er
return framework, err
}
+func (armoAPI *ArmoAPI) GetControl(policyName string) (*reporthandling.Control, error) {
+ var control *reporthandling.Control
+ var err error
+ if strings.HasPrefix(policyName, "C-") || strings.HasPrefix(policyName, "c-") {
+ control, err = armoAPI.gs.GetOPAControlByID(policyName)
+ } else {
+ control, err = armoAPI.gs.GetOPAControlByName(policyName)
+ }
+ if err != nil {
+ return nil, err
+ }
+ return control, nil
+}
+
func (armoAPI *ArmoAPI) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
exceptions := []armotypes.PostureExceptionPolicy{}
if customerGUID == "" {
return exceptions, nil
}
- respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName))
+ respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getExceptionsURL(customerGUID, clusterName), nil)
if err != nil {
return nil, err
}
@@ -128,7 +151,7 @@ func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, e
if customerGUID != "" {
url = fmt.Sprintf("%s?customerGUID=%s", url, customerGUID)
}
- respStr, err := HttpGetter(armoAPI.httpClient, url)
+ respStr, err := HttpGetter(armoAPI.httpClient, url, nil)
if err != nil {
return nil, err
}
@@ -140,6 +163,75 @@ func (armoAPI *ArmoAPI) GetCustomerGUID(customerGUID string) (*TenantResponse, e
return tenant, nil
}
+// ControlsInputs // map[][]
+func (armoAPI *ArmoAPI) GetAccountConfig(customerGUID, clusterName string) (*armotypes.CustomerConfig, error) {
+ accountConfig := &armotypes.CustomerConfig{}
+ if customerGUID == "" {
+ return accountConfig, nil
+ }
+ respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getAccountConfig(customerGUID, clusterName), nil)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = JSONDecoder(respStr).Decode(&accountConfig); err != nil {
+ return nil, err
+ }
+
+ return accountConfig, nil
+}
+
+// ControlsInputs // map[][]
+func (armoAPI *ArmoAPI) GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error) {
+ accountConfig, err := armoAPI.GetAccountConfig(customerGUID, clusterName)
+ if err == nil {
+ return accountConfig.Settings.PostureControlInputs, nil
+ }
+ return nil, err
+}
+
+func (armoAPI *ArmoAPI) ListCustomFrameworks(customerGUID string) ([]string, error) {
+ respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getListFrameworkURL(), nil)
+ if err != nil {
+ return nil, err
+ }
+ frs := []reporthandling.Framework{}
+ if err = json.Unmarshal([]byte(respStr), &frs); err != nil {
+ return nil, err
+ }
+
+ frameworkList := []string{}
+ for _, fr := range frs {
+ if !isNativeFramework(fr.Name) {
+ frameworkList = append(frameworkList, fr.Name)
+ }
+ }
+
+ return frameworkList, nil
+}
+
+func (armoAPI *ArmoAPI) ListFrameworks(customerGUID string) ([]string, error) {
+ respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.getListFrameworkURL(), nil)
+ if err != nil {
+ return nil, err
+ }
+ frs := []reporthandling.Framework{}
+ if err = json.Unmarshal([]byte(respStr), &frs); err != nil {
+ return nil, err
+ }
+
+ frameworkList := []string{}
+ for _, fr := range frs {
+ if isNativeFramework(fr.Name) {
+ frameworkList = append(frameworkList, strings.ToLower(fr.Name))
+ } else {
+ frameworkList = append(frameworkList, fr.Name)
+ }
+ }
+
+ return frameworkList, nil
+}
+
type TenantResponse struct {
TenantID string `json:"tenantId"`
Token string `json:"token"`
diff --git a/cautils/getter/armoapiutils.go b/cautils/getter/armoapiutils.go
index 001a6540..4991fcea 100644
--- a/cautils/getter/armoapiutils.go
+++ b/cautils/getter/armoapiutils.go
@@ -5,20 +5,37 @@ import (
"strings"
)
+var NativeFrameworks = []string{"nsa", "mitre", "armobest"}
+
func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string {
u := url.URL{}
u.Scheme = "https"
u.Host = armoAPI.apiURL
- u.Path = "v1/armoFrameworks"
+ u.Path = "api/v1/armoFrameworks"
q := u.Query()
- q.Add("customerGUID", "11111111-1111-1111-1111-111111111111")
- q.Add("frameworkName", strings.ToUpper(frameworkName))
- q.Add("getRules", "true")
+ q.Add("customerGUID", armoAPI.customerGUID)
+ if isNativeFramework(frameworkName) {
+ q.Add("frameworkName", strings.ToUpper(frameworkName))
+ } else {
+ // For customer framework has to be the way it was added
+ q.Add("frameworkName", frameworkName)
+ }
u.RawQuery = q.Encode()
return u.String()
}
+func (armoAPI *ArmoAPI) getListFrameworkURL() string {
+ u := url.URL{}
+ u.Scheme = "https"
+ u.Host = armoAPI.apiURL
+ u.Path = "api/v1/armoFrameworks"
+ q := u.Query()
+ q.Add("customerGUID", armoAPI.customerGUID)
+ u.RawQuery = q.Encode()
+
+ return u.String()
+}
func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) string {
u := url.URL{}
u.Scheme = "https"
@@ -35,6 +52,22 @@ func (armoAPI *ArmoAPI) getExceptionsURL(customerGUID, clusterName string) strin
return u.String()
}
+func (armoAPI *ArmoAPI) getAccountConfig(customerGUID, clusterName string) string {
+ u := url.URL{}
+ u.Scheme = "https"
+ u.Host = armoAPI.apiURL
+ u.Path = "api/v1/armoCustomerConfiguration"
+
+ q := u.Query()
+ q.Add("customerGUID", customerGUID)
+ if clusterName != "" { // TODO - fix customer name support in Armo BE
+ q.Add("clusterName", clusterName)
+ }
+ u.RawQuery = q.Encode()
+
+ return u.String()
+}
+
func (armoAPI *ArmoAPI) getCustomerURL() string {
u := url.URL{}
u.Scheme = "https"
diff --git a/cautils/getter/downloadreleasedpolicy.go b/cautils/getter/downloadreleasedpolicy.go
index 944c1bd1..caac33d2 100644
--- a/cautils/getter/downloadreleasedpolicy.go
+++ b/cautils/getter/downloadreleasedpolicy.go
@@ -11,26 +11,22 @@ import (
// ======================================== DownloadReleasedPolicy =======================================================
// =======================================================================================================================
-// Download released version
+// Use gitregostore to get policies from github release
type DownloadReleasedPolicy struct {
gs *gitregostore.GitRegoStore
}
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
return &DownloadReleasedPolicy{
- gs: gitregostore.InitDefaultGitRegoStore(),
+ gs: gitregostore.InitDefaultGitRegoStore(-1),
}
}
-// Return control per name/id using ARMO api
func (drp *DownloadReleasedPolicy) GetControl(policyName string) (*reporthandling.Control, error) {
var control *reporthandling.Control
var err error
- if strings.HasPrefix(policyName, "C-") || strings.HasPrefix(policyName, "c-") {
- control, err = drp.gs.GetOPAControlByID(policyName)
- } else {
- control, err = drp.gs.GetOPAControlByName(policyName)
- }
+
+ control, err = drp.gs.GetOPAControl(policyName)
if err != nil {
return nil, err
}
@@ -44,3 +40,16 @@ func (drp *DownloadReleasedPolicy) GetFramework(name string) (*reporthandling.Fr
}
return framework, err
}
+
+func isNativeFramework(framework string) bool {
+ return contains(NativeFrameworks, framework)
+}
+
+func contains(s []string, str string) bool {
+ for _, v := range s {
+ if strings.EqualFold(v, str) {
+ return true
+ }
+ }
+ return false
+}
diff --git a/cautils/getter/getpolicies.go b/cautils/getter/getpolicies.go
index 6d496d78..819efe02 100644
--- a/cautils/getter/getpolicies.go
+++ b/cautils/getter/getpolicies.go
@@ -7,7 +7,7 @@ import (
type IPolicyGetter interface {
GetFramework(name string) (*reporthandling.Framework, error)
- GetControl(policyName string) (*reporthandling.Control, error)
+ GetControl(name string) (*reporthandling.Control, error)
}
type IExceptionsGetter interface {
@@ -16,3 +16,7 @@ type IExceptionsGetter interface {
type IBackend interface {
GetCustomerGUID(customerGUID string) (*TenantResponse, error)
}
+
+type IControlsInputsGetter interface {
+ GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error)
+}
diff --git a/cautils/getter/getpoliciesutils.go b/cautils/getter/getpoliciesutils.go
index 7949eb70..a20f2b1d 100644
--- a/cautils/getter/getpoliciesutils.go
+++ b/cautils/getter/getpoliciesutils.go
@@ -1,6 +1,7 @@
package getter
import (
+ "bytes"
"encoding/json"
"fmt"
"io"
@@ -77,12 +78,14 @@ func JSONDecoder(origin string) *json.Decoder {
return dec
}
-func HttpGetter(httpClient *http.Client, fullURL string) (string, error) {
+func HttpGetter(httpClient *http.Client, fullURL string, headers map[string]string) (string, error) {
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return "", err
}
+ addHeaders(req, headers)
+
resp, err := httpClient.Do(req)
if err != nil {
return "", err
@@ -94,6 +97,32 @@ func HttpGetter(httpClient *http.Client, fullURL string) (string, error) {
return respStr, nil
}
+func HttpPost(httpClient *http.Client, fullURL string, headers map[string]string, body []byte) (string, error) {
+
+ req, err := http.NewRequest("POST", fullURL, bytes.NewReader(body))
+ if err != nil {
+ return "", err
+ }
+ addHeaders(req, headers)
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return "", err
+ }
+ respStr, err := httpRespToString(resp)
+ if err != nil {
+ return "", err
+ }
+ return respStr, nil
+}
+
+func addHeaders(req *http.Request, headers map[string]string) {
+ if len(headers) >= 0 { // might be nil
+ for k, v := range headers {
+ req.Header.Add(k, v)
+ }
+ }
+}
+
// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
func httpRespToString(resp *http.Response) (string, error) {
if resp == nil || resp.Body == nil {
diff --git a/cautils/getter/loadpolicy.go b/cautils/getter/loadpolicy.go
index c976a478..a3c1fb9e 100644
--- a/cautils/getter/loadpolicy.go
+++ b/cautils/getter/loadpolicy.go
@@ -17,12 +17,12 @@ const DefaultLocalStore = ".kubescape"
// Load policies from a local repository
type LoadPolicy struct {
- filePath string
+ filePaths []string
}
-func NewLoadPolicy(filePath string) *LoadPolicy {
+func NewLoadPolicy(filePaths []string) *LoadPolicy {
return &LoadPolicy{
- filePath: filePath,
+ filePaths: filePaths,
}
}
@@ -30,37 +30,58 @@ func NewLoadPolicy(filePath string) *LoadPolicy {
func (lp *LoadPolicy) GetControl(controlName string) (*reporthandling.Control, error) {
control := &reporthandling.Control{}
- f, err := os.ReadFile(lp.filePath)
+ filePath := lp.filePath()
+ f, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
- err = json.Unmarshal(f, control)
+ if err = json.Unmarshal(f, control); err != nil {
+ return control, err
+ }
if controlName != "" && !strings.EqualFold(controlName, control.Name) && !strings.EqualFold(controlName, control.ControlID) {
- return nil, fmt.Errorf("control from file not matching")
+ framework, err := lp.GetFramework(control.Name)
+ if err != nil {
+ return nil, fmt.Errorf("control from file not matching")
+ } else {
+ for _, ctrl := range framework.Controls {
+ if strings.EqualFold(ctrl.Name, controlName) || strings.EqualFold(ctrl.ControlID, controlName) {
+ control = &ctrl
+ break
+ }
+ }
+ }
}
return control, err
}
func (lp *LoadPolicy) GetFramework(frameworkName string) (*reporthandling.Framework, error) {
-
framework := &reporthandling.Framework{}
- f, err := os.ReadFile(lp.filePath)
- if err != nil {
- return nil, err
- }
+ var err error
+ for _, filePath := range lp.filePaths {
+ f, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
- err = json.Unmarshal(f, framework)
+ if err = json.Unmarshal(f, framework); err != nil {
+ return framework, err
+ }
+ if strings.EqualFold(frameworkName, framework.Name) {
+ break
+ }
+ }
if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) {
+
return nil, fmt.Errorf("framework from file not matching")
}
return framework, err
}
func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotypes.PostureExceptionPolicy, error) {
-
+ filePath := lp.filePath()
exception := []armotypes.PostureExceptionPolicy{}
- f, err := os.ReadFile(lp.filePath)
+ f, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
@@ -68,3 +89,25 @@ func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotyp
err = json.Unmarshal(f, &exception)
return exception, err
}
+
+func (lp *LoadPolicy) GetControlsInputs(customerGUID, clusterName string) (map[string][]string, error) {
+ filePath := lp.filePath()
+ accountConfig := &armotypes.CustomerConfig{}
+ f, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = json.Unmarshal(f, &accountConfig); err == nil {
+ return accountConfig.Settings.PostureControlInputs, nil
+ }
+ return nil, err
+}
+
+// temporary support for a list of files
+func (lp *LoadPolicy) filePath() string {
+ if len(lp.filePaths) > 0 {
+ return lp.filePaths[0]
+ }
+ return ""
+}
diff --git a/cautils/reporterutils.go b/cautils/reporterutils.go
deleted file mode 100644
index 7e4eabe0..00000000
--- a/cautils/reporterutils.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package cautils
-
-const (
- ComponentIdentifier = "Posture"
-)
diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go
index 8ca73ca6..02f667b2 100644
--- a/cautils/scaninfo.go
+++ b/cautils/scaninfo.go
@@ -3,20 +3,21 @@ package cautils
import (
"path/filepath"
- "github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/opa-utils/reporthandling"
)
type ScanInfo struct {
Getters
- PolicyIdentifier reporthandling.PolicyIdentifier
- UseExceptions string // Load exceptions configuration
- UseFrom string // Load framework from local file (instead of download). Use when running offline
+ PolicyIdentifier []reporthandling.PolicyIdentifier
+ UseExceptions string // Load file with exceptions configuration
+ ControlsInputs string // Load file with inputs for controls
+ 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?
+ IncludeNamespaces string // DEPRECATED?
InputPatterns []string // Yaml files input patterns
Silent bool // Silent mode - Do not print progress logs
FailThreshold uint16 // Failure score threshold
@@ -24,44 +25,45 @@ type ScanInfo struct {
Local bool // Do not submit results
Account string // account ID
FrameworkScan bool // false if scanning control
+ ScanAll bool // true if scan all frameworks
}
type Getters struct {
- ExceptionsGetter getter.IExceptionsGetter
- PolicyGetter getter.IPolicyGetter
+ ExceptionsGetter getter.IExceptionsGetter
+ ControlsInputsGetter getter.IControlsInputsGetter
+ PolicyGetter getter.IPolicyGetter
}
func (scanInfo *ScanInfo) Init() {
scanInfo.setUseFrom()
scanInfo.setUseExceptions()
+ scanInfo.setAccountConfig()
scanInfo.setOutputFile()
- scanInfo.setGetter()
}
func (scanInfo *ScanInfo) setUseExceptions() {
if scanInfo.UseExceptions != "" {
// load exceptions from file
- scanInfo.ExceptionsGetter = getter.NewLoadPolicy(scanInfo.UseExceptions)
+ scanInfo.ExceptionsGetter = getter.NewLoadPolicy([]string{scanInfo.UseExceptions})
} else {
scanInfo.ExceptionsGetter = getter.GetArmoAPIConnector()
}
+}
+func (scanInfo *ScanInfo) setAccountConfig() {
+ if scanInfo.ControlsInputs != "" {
+ // load account config from file
+ scanInfo.ControlsInputsGetter = getter.NewLoadPolicy([]string{scanInfo.ControlsInputs})
+ } else {
+ scanInfo.ControlsInputsGetter = getter.GetArmoAPIConnector()
+ }
}
func (scanInfo *ScanInfo) setUseFrom() {
- if scanInfo.UseFrom != "" {
- return
- }
if scanInfo.UseDefault {
- scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
- }
-}
-func (scanInfo *ScanInfo) setGetter() {
- if scanInfo.UseFrom != "" {
- // load from file
- scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
- } else {
- scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
+ for _, policy := range scanInfo.PolicyIdentifier {
+ scanInfo.UseFrom = append(scanInfo.UseFrom, getter.GetDefaultPath(policy.Name+".json"))
+ }
}
}
@@ -85,21 +87,22 @@ func (scanInfo *ScanInfo) ScanRunningCluster() bool {
return len(scanInfo.InputPatterns) == 0
}
-func (scanInfo *ScanInfo) SetClusterConfig() (IClusterConfig, *k8sinterface.KubernetesApi) {
- var clusterConfig IClusterConfig
- var k8s *k8sinterface.KubernetesApi
- if !scanInfo.ScanRunningCluster() {
- k8sinterface.ConnectedToCluster = false
- clusterConfig = NewEmptyConfig()
- } else {
- k8s = k8sinterface.NewKubernetesApi()
- // setup cluster config
- clusterConfig = ClusterConfigSetup(scanInfo, k8s, getter.GetArmoAPIConnector())
+func (scanInfo *ScanInfo) SetPolicyIdentifiers(policies []string, kind reporthandling.NotificationPolicyKind) {
+ for _, policy := range policies {
+ if !scanInfo.contains(policy) {
+ newPolicy := reporthandling.PolicyIdentifier{}
+ newPolicy.Kind = kind // reporthandling.KindFramework
+ newPolicy.Name = policy
+ scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
+ }
}
- return clusterConfig, k8s
}
-// func (scanInfo *ScanInfo) ConnectedToCluster(k8s k8sinterface.) bool {
-// _, err := k8s.KubernetesClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
-// return err == nil
-// }
+func (scanInfo *ScanInfo) contains(policyName string) bool {
+ for _, policy := range scanInfo.PolicyIdentifier {
+ if policy.Name == policyName {
+ return true
+ }
+ }
+ return false
+}
diff --git a/cautils/versioncheck.go b/cautils/versioncheck.go
new file mode 100644
index 00000000..7d9119ba
--- /dev/null
+++ b/cautils/versioncheck.go
@@ -0,0 +1,121 @@
+package cautils
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "os"
+
+ "github.com/armosec/kubescape/cautils/getter"
+ pkgutils "github.com/armosec/utils-go/utils"
+)
+
+const SKIP_VERSION_CHECK = "KUBESCAPE_SKIP_UPDATE_CHECK"
+
+var BuildNumber string
+
+type IVersionCheckHandler interface {
+ CheckLatestVersion(*VersionCheckRequest) error
+}
+
+func NewIVersionCheckHandler() IVersionCheckHandler {
+ if v, ok := os.LookupEnv(SKIP_VERSION_CHECK); ok && pkgutils.StringToBool(v) {
+ return NewVersionCheckHandlerMock()
+ }
+ return NewVersionCheckHandler()
+}
+
+type VersionCheckHandlerMock struct {
+}
+
+func NewVersionCheckHandlerMock() *VersionCheckHandlerMock {
+ return &VersionCheckHandlerMock{}
+}
+
+type VersionCheckHandler struct {
+ versionURL string
+}
+type VersionCheckRequest struct {
+ Client string `json:"client"` // kubescape
+ ClientVersion string `json:"clientVersion"` // kubescape version
+ Framework string `json:"framework"` // framework name
+ FrameworkVersion string `json:"frameworkVersion"` // framework version
+ ScanningTarget string `json:"target"` // scanning target- cluster/yaml
+}
+
+type VersionCheckResponse struct {
+ Client string `json:"client"` // kubescape
+ ClientUpdate string `json:"clientUpdate"` // kubescape latest version
+ Framework string `json:"framework"` // framework name
+ FrameworkUpdate string `json:"frameworkUpdate"` // framework latest version
+ Message string `json:"message"` // alert message
+}
+
+func NewVersionCheckHandler() *VersionCheckHandler {
+ return &VersionCheckHandler{
+ versionURL: "https://us-central1-elated-pottery-310110.cloudfunctions.net/ksgf1v1",
+ }
+}
+func NewVersionCheckRequest(buildNumber, frameworkName, frameworkVersion, scanningTarget string) *VersionCheckRequest {
+ if buildNumber == "" {
+ buildNumber = "unknown"
+ }
+ return &VersionCheckRequest{
+ Client: "kubescape",
+ ClientVersion: buildNumber,
+ Framework: frameworkName,
+ FrameworkVersion: frameworkVersion,
+ ScanningTarget: scanningTarget,
+ }
+}
+
+func (v *VersionCheckHandlerMock) CheckLatestVersion(versionData *VersionCheckRequest) error {
+ fmt.Println("Skipping version check")
+ return nil
+}
+
+func (v *VersionCheckHandler) CheckLatestVersion(versionData *VersionCheckRequest) error {
+
+ latestVersion, err := v.getLatestVersion(versionData)
+ if err != nil || latestVersion == nil {
+ return fmt.Errorf("failed to get latest version: %v", err)
+ }
+
+ if latestVersion.ClientUpdate != "" {
+ fmt.Println(warningMessage(latestVersion.Client, latestVersion.ClientUpdate))
+ }
+
+ // TODO - Enable after supporting framework version
+ // if latestVersion.FrameworkUpdate != "" {
+ // fmt.Println(warningMessage(latestVersion.Framework, latestVersion.FrameworkUpdate))
+ // }
+
+ if latestVersion.Message != "" {
+ fmt.Println(latestVersion.Message)
+ }
+
+ return nil
+}
+
+func (v *VersionCheckHandler) getLatestVersion(versionData *VersionCheckRequest) (*VersionCheckResponse, error) {
+
+ reqBody, err := json.Marshal(*versionData)
+ if err != nil {
+ return nil, fmt.Errorf("in 'CheckLatestVersion' failed to json.Marshal, reason: %s", err.Error())
+ }
+
+ resp, err := getter.HttpPost(http.DefaultClient, v.versionURL, map[string]string{"Content-Type": "application/json"}, reqBody)
+ if err != nil {
+ return nil, err
+ }
+
+ vResp := &VersionCheckResponse{}
+ if err = getter.JSONDecoder(resp).Decode(vResp); err != nil {
+ return nil, err
+ }
+ return vResp, nil
+}
+
+func warningMessage(kind, release string) string {
+ return fmt.Sprintf("Warning: '%s' is not updated to the latest release: '%s'", kind, release)
+}
diff --git a/clihandler/cliinterfaces/submit.go b/clihandler/cliinterfaces/submit.go
new file mode 100644
index 00000000..6428ad69
--- /dev/null
+++ b/clihandler/cliinterfaces/submit.go
@@ -0,0 +1,17 @@
+package cliinterfaces
+
+import (
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/resultshandling/reporter"
+ "github.com/armosec/opa-utils/reporthandling"
+)
+
+type ISubmitObjects interface {
+ SetResourcesReport() (*reporthandling.PostureReport, error)
+}
+
+type SubmitInterfaces struct {
+ SubmitObjects ISubmitObjects
+ Reporter reporter.IReport
+ ClusterConfig cautils.IClusterConfig
+}
diff --git a/clihandler/cmd/cluster_get.go b/clihandler/cmd/cluster_get.go
index 0c0740df..e74f8db7 100644
--- a/clihandler/cmd/cluster_get.go
+++ b/clihandler/cmd/cluster_get.go
@@ -7,7 +7,6 @@ import (
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
- "github.com/armosec/kubescape/clihandler"
"github.com/spf13/cobra"
)
@@ -15,7 +14,7 @@ var getCmd = &cobra.Command{
Use: "get ",
Short: "Get configuration in cluster",
Long: ``,
- ValidArgs: clihandler.SupportedFrameworks,
+ ValidArgs: getter.NativeFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 || len(args) > 1 {
return fmt.Errorf("requires one argument")
diff --git a/clihandler/cmd/control.go b/clihandler/cmd/control.go
index 0618e0c4..a0772767 100644
--- a/clihandler/cmd/control.go
+++ b/clihandler/cmd/control.go
@@ -2,10 +2,12 @@ package cmd
import (
"fmt"
+ "io"
"os"
"strings"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/clihandler"
"github.com/armosec/opa-utils/reporthandling"
"github.com/spf13/cobra"
@@ -13,25 +15,55 @@ import (
// controlCmd represents the control command
var controlCmd = &cobra.Command{
- Use: "control /",
- Short: fmt.Sprintf("The control you wish to use for scan. It must be present in at least one of the folloiwng frameworks: %s", clihandler.ValidFrameworks),
+ Use: "control /.\nExamples:\n$ kubescape scan control C-0058,C-0057 [flags]\n$ kubescape scan contol C-0058 [flags]\n$ kubescape scan control 'privileged container,allowed hostpath' [flags]",
+ Short: fmt.Sprintf("The control you wish to use for scan. It must be present in at least one of the folloiwng frameworks: %s", getter.NativeFrameworks),
Args: func(cmd *cobra.Command, args []string) error {
- if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
- return fmt.Errorf("requires at least one argument")
+ if len(args) > 0 {
+ controls := strings.Split(args[0], ",")
+ if len(controls) > 1 {
+ if controls[1] == "" {
+ return fmt.Errorf("usage: ,")
+ }
+ }
+ } else {
+ return fmt.Errorf("requires at least one control name")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
flagValidationControl()
- scanInfo.PolicyIdentifier = reporthandling.PolicyIdentifier{}
- if !(cmd.Flags().Lookup("use-from").Changed) {
- scanInfo.PolicyIdentifier.Name = strings.ToLower(args[0])
+ scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
+
+ if len(args) == 0 {
+ scanInfo.SetPolicyIdentifiers(getter.NativeFrameworks, reporthandling.KindFramework)
+ scanInfo.ScanAll = true
+ } else { // expected control or list of control sepparated by ","
+
+ // Read controls from input args
+ scanInfo.SetPolicyIdentifiers(strings.Split(args[0], ","), reporthandling.KindControl)
+
+ if len(args) > 1 {
+ if len(args[1:]) == 0 || args[1] != "-" {
+ scanInfo.InputPatterns = args[1:]
+ } else { // store stdin to file - do NOT move to separate function !!
+ tempFile, err := os.CreateTemp(".", "tmp-kubescape*.yaml")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tempFile.Name())
+
+ if _, err := io.Copy(tempFile, os.Stdin); err != nil {
+ return err
+ }
+ scanInfo.InputPatterns = []string{tempFile.Name()}
+ }
+ }
}
+
scanInfo.FrameworkScan = false
- scanInfo.PolicyIdentifier.Kind = reporthandling.KindControl
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
- err := clihandler.CliSetup(scanInfo)
+ err := clihandler.ScanCliSetup(&scanInfo)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
@@ -51,3 +83,22 @@ func flagValidationControl() {
os.Exit(1)
}
}
+
+func setScanForFirstControl(controls []string) []reporthandling.PolicyIdentifier {
+ newPolicy := reporthandling.PolicyIdentifier{}
+ newPolicy.Kind = reporthandling.KindControl
+ newPolicy.Name = controls[0]
+ scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
+ return scanInfo.PolicyIdentifier
+}
+
+func SetScanForGivenControls(controls []string) []reporthandling.PolicyIdentifier {
+ for _, control := range controls {
+ control := strings.TrimLeft(control, " ")
+ newPolicy := reporthandling.PolicyIdentifier{}
+ newPolicy.Kind = reporthandling.KindControl
+ newPolicy.Name = control
+ scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
+ }
+ return scanInfo.PolicyIdentifier
+}
diff --git a/clihandler/cmd/download.go b/clihandler/cmd/download.go
index 6ed2dadf..53734548 100644
--- a/clihandler/cmd/download.go
+++ b/clihandler/cmd/download.go
@@ -6,14 +6,13 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
- "github.com/armosec/kubescape/clihandler"
"github.com/spf13/cobra"
)
var downloadInfo cautils.DownloadInfo
var downloadCmd = &cobra.Command{
- Use: fmt.Sprintf("download framework/control / [flags]\nSupported frameworks: %s", clihandler.ValidFrameworks),
+ Use: fmt.Sprintf("download framework/control / [flags]\nSupported frameworks: %s", getter.NativeFrameworks),
Short: "Download framework/control",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
diff --git a/clihandler/cmd/framework.go b/clihandler/cmd/framework.go
index bd0bbfe6..e381ecff 100644
--- a/clihandler/cmd/framework.go
+++ b/clihandler/cmd/framework.go
@@ -7,78 +7,85 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/clihandler"
"github.com/armosec/opa-utils/reporthandling"
-
"github.com/spf13/cobra"
)
var frameworkCmd = &cobra.Command{
-
- Use: fmt.Sprintf("framework [``/`-`] [flags]\nSupported frameworks: %s", clihandler.ValidFrameworks),
- Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(clihandler.SupportedFrameworks, ", ")),
+ Use: fmt.Sprintf("framework [``/`-`] [flags]\nExamples:\n$ kubescape scan framework nsa [flags]\n$ kubescape scan framework mitre,nsa [flags]\n$ kubescape scan framework 'nsa, mitre' [flags]\nSupported frameworks: %s", getter.NativeFrameworks),
+ Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(getter.NativeFrameworks, ", ")),
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
- ValidArgs: clihandler.SupportedFrameworks,
+ ValidArgs: getter.NativeFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
- if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
- return fmt.Errorf("requires at least one argument")
- } else if len(args) > 0 {
- if !isValidFramework(strings.ToLower(args[0])) {
- return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(clihandler.SupportedFrameworks, ", ")))
+ if len(args) > 0 {
+ frameworks := strings.Split(args[0], ",")
+ if len(frameworks) > 1 {
+ if frameworks[1] == "" {
+ return fmt.Errorf("usage: ,")
+ }
}
+ } else {
+ return fmt.Errorf("requires at least one framework name")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
- scanInfo.PolicyIdentifier = reporthandling.PolicyIdentifier{}
- scanInfo.PolicyIdentifier.Kind = reporthandling.KindFramework
flagValidationFramework()
- if !(cmd.Flags().Lookup("use-from").Changed) {
- scanInfo.PolicyIdentifier.Name = strings.ToLower(args[0])
- }
- if len(args) > 0 {
- if len(args[1:]) == 0 || args[1] != "-" {
- scanInfo.InputPatterns = args[1:]
- } else { // store stout to file
- tempFile, err := os.CreateTemp(".", "tmp-kubescape*.yaml")
- if err != nil {
- return err
- }
- defer os.Remove(tempFile.Name())
+ var frameworks []string
- if _, err := io.Copy(tempFile, os.Stdin); err != nil {
- return err
+ if len(args) == 0 { // scan all frameworks
+ frameworks = getter.NativeFrameworks
+ scanInfo.ScanAll = true
+ } else {
+ // Read frameworks from input args
+ frameworks = strings.Split(args[0], ",")
+
+ if len(args) > 1 {
+ if len(args[1:]) == 0 || args[1] != "-" {
+ scanInfo.InputPatterns = args[1:]
+ } else { // store stdin to file - do NOT move to separate function !!
+ tempFile, err := os.CreateTemp(".", "tmp-kubescape*.yaml")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(tempFile.Name())
+
+ if _, err := io.Copy(tempFile, os.Stdin); err != nil {
+ return err
+ }
+ scanInfo.InputPatterns = []string{tempFile.Name()}
}
- scanInfo.InputPatterns = []string{tempFile.Name()}
}
}
+ scanInfo.SetPolicyIdentifiers(frameworks, reporthandling.KindFramework)
+
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
- err := clihandler.CliSetup(scanInfo)
+ err := clihandler.ScanCliSetup(&scanInfo)
if err != nil {
- fmt.Fprintf(os.Stderr, "error: %v\n", err)
- os.Exit(1)
+ return err
}
return nil
},
}
-func isValidFramework(framework string) bool {
- return cautils.StringInSlice(clihandler.SupportedFrameworks, framework) != cautils.ValueNotFound
-}
-
func init() {
scanCmd.AddCommand(frameworkCmd)
scanInfo = cautils.ScanInfo{}
scanInfo.FrameworkScan = true
- 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 flagValidationFramework() {
+// func SetScanForFirstFramework(frameworks []string) []reporthandling.PolicyIdentifier {
+// newPolicy := reporthandling.PolicyIdentifier{}
+// newPolicy.Kind = reporthandling.KindFramework
+// newPolicy.Name = frameworks[0]
+// scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
+// return scanInfo.PolicyIdentifier
+// }
+func flagValidationFramework() {
if scanInfo.Submit && scanInfo.Local {
fmt.Println("You can use `keep-local` or `submit`, but not both")
os.Exit(1)
diff --git a/clihandler/cmd/rbac.go b/clihandler/cmd/rbac.go
new file mode 100644
index 00000000..a8c97e37
--- /dev/null
+++ b/clihandler/cmd/rbac.go
@@ -0,0 +1,80 @@
+package cmd
+
+import (
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/armosec/k8s-interface/k8sinterface"
+ "github.com/armosec/kubescape/clihandler"
+ "github.com/armosec/kubescape/clihandler/cliinterfaces"
+ "github.com/armosec/kubescape/resultshandling/reporter"
+ "github.com/armosec/opa-utils/reporthandling"
+ "github.com/armosec/rbac-utils/rbacscanner"
+ uuid "github.com/satori/go.uuid"
+ "github.com/spf13/cobra"
+)
+
+type RBACObjects struct {
+ scanner *rbacscanner.RbacScannerFromK8sAPI
+}
+
+func NewRBACObjects(scanner *rbacscanner.RbacScannerFromK8sAPI) *RBACObjects {
+ return &RBACObjects{scanner: scanner}
+}
+
+func (rbacObjects *RBACObjects) SetResourcesReport() (*reporthandling.PostureReport, error) {
+ resources, err := rbacObjects.scanner.ListResources()
+ if err != nil {
+ return nil, err
+ }
+ return &reporthandling.PostureReport{
+ ReportID: uuid.NewV4().String(),
+ ReportGenerationTime: time.Now().UTC(),
+ CustomerGUID: rbacObjects.scanner.CustomerGUID,
+ ClusterName: rbacObjects.scanner.ClusterName,
+ RBACObjects: *resources,
+ }, nil
+}
+
+// rabcCmd represents the RBAC command
+var rabcCmd = &cobra.Command{
+ Use: "rbac \nExample:\n$ kubescape submit rbac",
+ Short: "Submit cluster's Role-Based Access Control(RBAC)",
+ Long: ``,
+ RunE: func(cmd *cobra.Command, args []string) error {
+
+ k8s := k8sinterface.NewKubernetesApi()
+
+ // get config
+ clusterConfig, err := getSubmittedClusterConfig(k8s)
+ if err != nil {
+ return err
+ }
+
+ clusterName := clusterConfig.GetClusterName()
+ customerGUID := clusterConfig.GetCustomerGUID()
+
+ // list RBAC
+ rbacObjects := NewRBACObjects(rbacscanner.NewRbacScannerFromK8sAPI(k8s, customerGUID, clusterName))
+
+ // submit resources
+ r := reporter.NewReportEventReceiver(customerGUID, clusterName)
+
+ submitInterfaces := cliinterfaces.SubmitInterfaces{
+ ClusterConfig: clusterConfig,
+ SubmitObjects: rbacObjects,
+ Reporter: r,
+ }
+
+ if err := clihandler.Submit(submitInterfaces); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ return nil
+ },
+}
+
+func init() {
+ submitCmd.AddCommand(rabcCmd)
+}
diff --git a/clihandler/cmd/results.go b/clihandler/cmd/results.go
new file mode 100644
index 00000000..72b8afe7
--- /dev/null
+++ b/clihandler/cmd/results.go
@@ -0,0 +1,105 @@
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "time"
+
+ "github.com/armosec/k8s-interface/k8sinterface"
+ "github.com/armosec/kubescape/clihandler"
+ "github.com/armosec/kubescape/clihandler/cliinterfaces"
+ "github.com/armosec/kubescape/resultshandling/reporter"
+ "github.com/armosec/opa-utils/reporthandling"
+ uuid "github.com/satori/go.uuid"
+ "github.com/spf13/cobra"
+)
+
+type ResultsObject struct {
+ filePath string
+ customerGUID string
+ clusterName string
+}
+
+func NewResultsObject(customerGUID, clusterName, filePath string) *ResultsObject {
+ return &ResultsObject{
+ filePath: filePath,
+ customerGUID: customerGUID,
+ clusterName: clusterName,
+ }
+}
+
+func (resultsObject *ResultsObject) SetResourcesReport() (*reporthandling.PostureReport, error) {
+ // load framework results from json file
+ frameworkReports, err := loadResultsFromFile(resultsObject.filePath)
+ if err != nil {
+ return nil, err
+ }
+
+ return &reporthandling.PostureReport{
+ FrameworkReports: frameworkReports,
+ ReportID: uuid.NewV4().String(),
+ ReportGenerationTime: time.Now().UTC(),
+ CustomerGUID: resultsObject.customerGUID,
+ ClusterName: resultsObject.clusterName,
+ }, nil
+}
+
+var resultsCmd = &cobra.Command{
+ Use: "results \nExample:\n$ kubescape submit results path/to/results.json",
+ Short: "Submit a pre scanned results file. The file must be in json format",
+ Long: ``,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf("missing results file")
+ }
+
+ k8s := k8sinterface.NewKubernetesApi()
+
+ // get config
+ clusterConfig, err := getSubmittedClusterConfig(k8s)
+ if err != nil {
+ return err
+ }
+
+ clusterName := clusterConfig.GetClusterName()
+ customerGUID := clusterConfig.GetCustomerGUID()
+
+ resultsObjects := NewResultsObject(customerGUID, clusterName, args[0])
+
+ // submit resources
+ r := reporter.NewReportEventReceiver(customerGUID, clusterName)
+
+ submitInterfaces := cliinterfaces.SubmitInterfaces{
+ ClusterConfig: clusterConfig,
+ SubmitObjects: resultsObjects,
+ Reporter: r,
+ }
+
+ if err := clihandler.Submit(submitInterfaces); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ return nil
+ },
+}
+
+func init() {
+ submitCmd.AddCommand(resultsCmd)
+}
+
+func loadResultsFromFile(filePath string) ([]reporthandling.FrameworkReport, error) {
+ frameworkReports := []reporthandling.FrameworkReport{}
+ f, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, err
+ }
+ if err = json.Unmarshal(f, &frameworkReports); err != nil {
+ frameworkReport := reporthandling.FrameworkReport{}
+ if err = json.Unmarshal(f, &frameworkReport); err != nil {
+ return frameworkReports, err
+ }
+ frameworkReports = append(frameworkReports, frameworkReport)
+ }
+ return frameworkReports, nil
+}
diff --git a/clihandler/cmd/root.go b/clihandler/cmd/root.go
index 7f51c38a..dafd262f 100644
--- a/clihandler/cmd/root.go
+++ b/clihandler/cmd/root.go
@@ -31,6 +31,7 @@ func Execute() {
}
func init() {
+ rootCmd.PersistentFlags().StringVarP(&scanInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file")
flag.CommandLine.StringVar(&armoBEURLs, "environment", "", envFlagUsage)
rootCmd.PersistentFlags().StringVar(&armoBEURLs, "environment", "", envFlagUsage)
rootCmd.PersistentFlags().MarkHidden("environment")
diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go
index e86e5a02..c69e5ab9 100644
--- a/clihandler/cmd/scan.go
+++ b/clihandler/cmd/scan.go
@@ -5,6 +5,7 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
"github.com/spf13/cobra"
)
@@ -16,26 +17,35 @@ var scanCmd = &cobra.Command{
Short: "Scan the current running cluster or yaml files",
Long: `The action you want to perform`,
Args: func(cmd *cobra.Command, args []string) error {
- if len(args) == 0 {
- return fmt.Errorf("requires one argument: framework/control")
- }
- if !strings.EqualFold(args[0], "framework") && !strings.EqualFold(args[0], "control") {
- return fmt.Errorf("invalid parameter '%s'. Supported parameters: framework, control", args[0])
+ if len(args) > 0 {
+ if !strings.EqualFold(args[0], "framework") && !strings.EqualFold(args[0], "control") {
+ return fmt.Errorf("invalid parameter '%s'. Supported parameters: framework, control", args[0])
+ }
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ scanInfo.ScanAll = true
+ frameworks := getter.NativeFrameworks
+ frameworkArgs := []string{strings.Join(frameworks, ",")}
+ frameworkCmd.RunE(cmd, frameworkArgs)
+ }
},
}
func init() {
rootCmd.AddCommand(scanCmd)
- scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system, kube-public")
- scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"`)
+ 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().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")
- scanCmd.PersistentFlags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code 1")
- scanCmd.PersistentFlags().StringVar(&scanInfo.UseFrom, "use-from", "", "Load local framework object from specified path. If not used will download latest")
- scanCmd.PersistentFlags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load local framework object from default path. If not used will download latest")
- scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from Armo management portal")
+ scanCmd.PersistentFlags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent below which the command fails and returns exit code 1")
+ scanCmd.PersistentFlags().StringSliceVar(&scanInfo.UseFrom, "use-from", nil, "Load local policy object from specified path. If not used will download latest")
+ scanCmd.PersistentFlags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load local policy object from default path. If not used will download latest")
+ scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from ARMO management portal")
+ scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal")
}
diff --git a/clihandler/cmd/submit.go b/clihandler/cmd/submit.go
new file mode 100644
index 00000000..f0b07ae8
--- /dev/null
+++ b/clihandler/cmd/submit.go
@@ -0,0 +1,27 @@
+package cmd
+
+import (
+ "github.com/armosec/k8s-interface/k8sinterface"
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
+ "github.com/spf13/cobra"
+)
+
+var submitCmd = &cobra.Command{
+ Use: "submit ",
+ Short: "Submit an object to the Kubescape SaaS version",
+ Long: ``,
+ Run: func(cmd *cobra.Command, args []string) {
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(submitCmd)
+}
+
+func getSubmittedClusterConfig(k8s *k8sinterface.KubernetesApi) (*cautils.ClusterConfig, error) {
+ clusterConfig := cautils.NewClusterConfig(k8s, getter.GetArmoAPIConnector())
+ clusterConfig.LoadConfig()
+ err := clusterConfig.SetConfig(scanInfo.Account)
+ return clusterConfig, err
+}
diff --git a/clihandler/cmd/version.go b/clihandler/cmd/version.go
index 45ba5bc2..aa47b0cb 100644
--- a/clihandler/cmd/version.go
+++ b/clihandler/cmd/version.go
@@ -1,49 +1,24 @@
package cmd
import (
- "encoding/json"
"fmt"
- "io"
- "net/http"
+ "github.com/armosec/kubescape/cautils"
"github.com/spf13/cobra"
)
-var BuildNumber string
-
var versionCmd = &cobra.Command{
Use: "version",
Short: "Get current version",
Long: ``,
RunE: func(cmd *cobra.Command, args []string) error {
- fmt.Println("Your current version is: " + BuildNumber)
+ v := cautils.NewIVersionCheckHandler()
+ v.CheckLatestVersion(cautils.NewVersionCheckRequest(cautils.BuildNumber, "", "", ""))
+ fmt.Println("Your current version is: " + cautils.BuildNumber)
return nil
},
}
-func GetLatestVersion() (string, error) {
- latestVersion := "https://api.github.com/repos/armosec/kubescape/releases/latest"
- resp, err := http.Get(latestVersion)
- if err != nil {
- return "unknown", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, err.Error())
- }
- defer resp.Body.Close()
- if resp.StatusCode < 200 || 301 < resp.StatusCode {
- return "unknown", fmt.Errorf("failed to download file, status code: %s", resp.Status)
- }
-
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return "unknown", fmt.Errorf("failed to read response body from '%s', reason: %s", latestVersion, err.Error())
- }
- var data map[string]interface{}
- err = json.Unmarshal(body, &data)
- if err != nil {
- return "unknown", fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestVersion, err.Error())
- }
- return fmt.Sprintf("%v", data["tag_name"]), nil
-}
-
func init() {
rootCmd.AddCommand(versionCmd)
}
diff --git a/clihandler/initcli.go b/clihandler/initcli.go
index c335702d..852af93b 100644
--- a/clihandler/initcli.go
+++ b/clihandler/initcli.go
@@ -3,47 +3,115 @@ package clihandler
import (
"fmt"
"os"
- "strings"
"github.com/armosec/armoapi-go/armotypes"
+ "github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
+ "github.com/armosec/kubescape/clihandler/cliinterfaces"
"github.com/armosec/kubescape/opaprocessor"
"github.com/armosec/kubescape/policyhandler"
+ "github.com/armosec/kubescape/resourcehandler"
"github.com/armosec/kubescape/resultshandling"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/reporter"
"github.com/armosec/opa-utils/reporthandling"
+ "github.com/golang/glog"
)
-type CLIHandler struct {
- policyHandler *policyhandler.PolicyHandler
- scanInfo *cautils.ScanInfo
+type componentInterfaces struct {
+ clusterConfig cautils.IClusterConfig
+ resourceHandler resourcehandler.IResourceHandler
+ report reporter.IReport
+ printerHandler printer.IPrinter
}
-var SupportedFrameworks = []string{"nsa", "mitre"}
-var ValidFrameworks = strings.Join(SupportedFrameworks, ", ")
+func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces {
+ var resourceHandler resourcehandler.IResourceHandler
+ var clusterConfig cautils.IClusterConfig
+ var reportHandler reporter.IReport
+ var scanningTarget string
-func CliSetup(scanInfo cautils.ScanInfo) error {
+ if !scanInfo.ScanRunningCluster() {
+ k8sinterface.ConnectedToCluster = false
+ clusterConfig = cautils.NewEmptyConfig()
- clusterConfig, k8s := scanInfo.SetClusterConfig()
+ // load fom file
+ resourceHandler = resourcehandler.NewFileResourceHandler(scanInfo.InputPatterns)
+
+ // set mock report (do not send report)
+ reportHandler = reporter.NewReportMock()
+ scanningTarget = "yaml"
+ } else {
+ k8s := k8sinterface.NewKubernetesApi()
+ resourceHandler = resourcehandler.NewK8sResourceHandler(k8s, getFieldSelector(scanInfo))
+ clusterConfig = cautils.ClusterConfigSetup(scanInfo, k8s, getter.GetArmoAPIConnector())
+
+ // setup reporter
+ reportHandler = getReporter(scanInfo)
+ scanningTarget = "cluster"
+ }
+
+ v := cautils.NewIVersionCheckHandler()
+ v.CheckLatestVersion(cautils.NewVersionCheckRequest(cautils.BuildNumber, policyIdentifierNames(scanInfo.PolicyIdentifier), "", scanningTarget))
+
+ // setup printer
+ printerHandler := printer.GetPrinter(scanInfo.Format)
+ printerHandler.SetWriter(scanInfo.Output)
+
+ return componentInterfaces{
+ clusterConfig: clusterConfig,
+ resourceHandler: resourceHandler,
+ report: reportHandler,
+ printerHandler: printerHandler,
+ }
+}
+func setPolicyGetter(scanInfo *cautils.ScanInfo, customerGUID string) {
+ if len(scanInfo.UseFrom) > 0 {
+ //load from file
+ scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
+ } else {
+ if customerGUID == "" || !scanInfo.FrameworkScan {
+ scanInfo.PolicyGetter = getter.NewDownloadReleasedPolicy()
+ } else {
+ g := getter.GetArmoAPIConnector()
+ g.SetCustomerGUID(customerGUID)
+ scanInfo.PolicyGetter = g
+ if scanInfo.ScanAll {
+ frameworks, err := g.ListCustomFrameworks(customerGUID)
+ if err != nil {
+ glog.Error("failed to get custom frameworks") // handle error
+ return
+ }
+ scanInfo.SetPolicyIdentifiers(frameworks, reporthandling.KindFramework)
+ }
+ }
+ }
+}
+
+func ScanCliSetup(scanInfo *cautils.ScanInfo) error {
+
+ interfaces := getInterfaces(scanInfo)
+
+ setPolicyGetter(scanInfo, interfaces.clusterConfig.GetCustomerGUID())
processNotification := make(chan *cautils.OPASessionObj)
reportResults := make(chan *cautils.OPASessionObj)
- // policy handler setup
- policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s)
-
- if err := clusterConfig.SetConfig(scanInfo.Account); err != nil {
+ if err := interfaces.clusterConfig.SetConfig(scanInfo.Account); err != nil {
fmt.Println(err)
}
- cautils.ClusterName = clusterConfig.GetClusterName()
- cautils.CustomerGUID = clusterConfig.GetCustomerGUID()
-
+ cautils.ClusterName = interfaces.clusterConfig.GetClusterName() // TODO - Deprecated
+ cautils.CustomerGUID = interfaces.clusterConfig.GetCustomerGUID() // TODO - Deprecated
+ interfaces.report.SetClusterName(interfaces.clusterConfig.GetClusterName())
+ interfaces.report.SetCustomerGUID(interfaces.clusterConfig.GetCustomerGUID())
// cli handler setup
go func() {
- cli := NewCLIHandler(policyHandler, scanInfo)
- if err := cli.Scan(); err != nil {
+ // policy handler setup
+ policyHandler := policyhandler.NewPolicyHandler(&processNotification, interfaces.resourceHandler)
+
+ if err := Scan(policyHandler, scanInfo); err != nil {
fmt.Println(err)
os.Exit(1)
}
@@ -55,41 +123,30 @@ func CliSetup(scanInfo cautils.ScanInfo) error {
opaprocessorObj.ProcessRulesListenner()
}()
- resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output))
+ resultsHandling := resultshandling.NewResultsHandler(&reportResults, interfaces.report, interfaces.printerHandler)
score := resultsHandling.HandleResults(scanInfo)
// print report url
- if scanInfo.FrameworkScan {
- clusterConfig.GenerateURL()
- }
+ interfaces.clusterConfig.GenerateURL()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {
- return fmt.Errorf("Scan score is bellow threshold")
+ return fmt.Errorf("Scan score is below threshold")
}
return nil
}
-func NewCLIHandler(policyHandler *policyhandler.PolicyHandler, scanInfo cautils.ScanInfo) *CLIHandler {
- return &CLIHandler{
- scanInfo: &scanInfo,
- policyHandler: policyHandler,
- }
-}
-
-func (clihandler *CLIHandler) Scan() error {
+func Scan(policyHandler *policyhandler.PolicyHandler, scanInfo *cautils.ScanInfo) error {
cautils.ScanStartDisplay()
policyNotification := &reporthandling.PolicyNotification{
NotificationType: reporthandling.TypeExecPostureScan,
- Rules: []reporthandling.PolicyIdentifier{
- clihandler.scanInfo.PolicyIdentifier,
- },
- Designators: armotypes.PortalDesignator{},
+ Rules: scanInfo.PolicyIdentifier,
+ Designators: armotypes.PortalDesignator{},
}
switch policyNotification.NotificationType {
case reporthandling.TypeExecPostureScan:
- if err := clihandler.policyHandler.HandleNotificationRequest(policyNotification, clihandler.scanInfo); err != nil {
+ if err := policyHandler.HandleNotificationRequest(policyNotification, scanInfo); err != nil {
return err
}
@@ -98,3 +155,21 @@ func (clihandler *CLIHandler) Scan() error {
}
return nil
}
+
+func Submit(submitInterfaces cliinterfaces.SubmitInterfaces) error {
+
+ // list resources
+ postureReport, err := submitInterfaces.SubmitObjects.SetResourcesReport()
+ if err != nil {
+ return err
+ }
+
+ // report
+ if err := submitInterfaces.Reporter.ActionSendReport(&cautils.OPASessionObj{PostureReport: postureReport}); err != nil {
+ return err
+ }
+ fmt.Printf("\nData has been submitted successfully")
+ submitInterfaces.ClusterConfig.GenerateURL()
+
+ return nil
+}
diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go
new file mode 100644
index 00000000..38d647b9
--- /dev/null
+++ b/clihandler/initcliutils.go
@@ -0,0 +1,41 @@
+package clihandler
+
+import (
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/resourcehandler"
+ "github.com/armosec/kubescape/resultshandling/reporter"
+ "github.com/armosec/opa-utils/reporthandling"
+)
+
+func getReporter(scanInfo *cautils.ScanInfo) reporter.IReport {
+ if !scanInfo.Submit {
+ return reporter.NewReportMock()
+ }
+ if !scanInfo.FrameworkScan {
+ return reporter.NewReportMock()
+ }
+
+ 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 policyIdentifierNames(pi []reporthandling.PolicyIdentifier) string {
+ policiesNames := ""
+ for i := range pi {
+ policiesNames += pi[i].Name
+ if i+1 < len(pi) {
+ policiesNames += ","
+ }
+ }
+ return policiesNames
+}
diff --git a/docs/run-options.md b/docs/run-options.md
index 8a94afc0..f2e0adf2 100644
--- a/docs/run-options.md
+++ b/docs/run-options.md
@@ -13,7 +13,7 @@ kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
| --- | --- | --- | --- |
| `-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` |
+| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result is below threshold| `0` -> `100` |
| `-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 |
@@ -25,7 +25,7 @@ kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
### 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/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
@@ -85,5 +85,3 @@ 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.
-
-
diff --git a/examples/cronJob-support/README.md b/examples/cronJob-support/README.md
new file mode 100644
index 00000000..064be710
--- /dev/null
+++ b/examples/cronJob-support/README.md
@@ -0,0 +1,85 @@
+# Periodically Kubescape Scanning
+
+You can scan your cluster periodically by adding a `CronJob` that will repeatedly trigger kubescape
+
+* Setup [scanning & submitting](#scanning-and-submitting)
+* Setup [scanning without submitting](#scanning-without-submitting)
+
+## Scanning And Submitting
+
+If you wish to periodically scan and submit the result to the [Kubescape SaaS version](https://portal.armo.cloud/) where you can benefit the features the SaaS version provides, please follow this instructions ->
+
+1. Apply kubescape namespace
+ ```
+ kubectl apply ks-namespace.yaml
+ ```
+
+2. Apply serviceAccount and roles
+ ```
+ kubectl apply ks-serviceAccount.yaml
+ ```
+
+3. Setup and apply configMap
+
+ Before you apply the configMap you need to set the account ID and cluster name in the `ks-configMap.yaml` file.
+
+ * Set cluster name:
+ Run `kubectl config current-context` and set the result in the `data.clusterName` field
+ * Set account ID:
+ 1. Navigate to the [Kubescape SaaS version](https://portal.armo.cloud/) and login/sign up for free
+ 2. Click the `Add Cluster` button on the top right of the page
+
+
+ 3. Copy the value of `--account` and set it in the `data.customerGUID` field
+
+
+
+ Make sure the configMap looks as following;
+ ```
+ kind: ConfigMap
+ apiVersion: v1
+ metadata:
+ name: kubescape
+ labels:
+ app: kubescape
+ namespace: kubescape
+ data:
+ config.json: |
+ {
+ "customerGUID": "XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXX",
+ "clusterName": "my-awesome-cluster-name"
+ }
+ ```
+
+ Finally, apply the configMap
+ ```
+ kubectl apply ks-configMap.yaml
+ ```
+
+4. Apply CronJob
+
+ Before you apply the cronJob, make sure the scanning frequency suites your needs
+ ```
+ kubectl apply ks-cronJob-submit.yaml
+ ```
+
+## Scanning Without Submitting
+
+If you wish to periodically scan but not submit the scan results, follow this instructions ->
+
+1. Apply kubescape namespace
+ ```
+ kubectl apply ks-namespace.yaml
+ ```
+
+2. Apply serviceAccount and roles
+ ```
+ kubectl apply ks-serviceAccount.yaml
+ ```
+
+3. Apply CronJob
+
+ Before you apply the cronJob, make sure the scanning frequency suites your needs
+ ```
+ kubectl apply ks-cronJob-non-submit.yaml
+ ```
\ No newline at end of file
diff --git a/examples/cronJob-support/ks-configMap.yaml b/examples/cronJob-support/ks-configMap.yaml
new file mode 100644
index 00000000..59aaf979
--- /dev/null
+++ b/examples/cronJob-support/ks-configMap.yaml
@@ -0,0 +1,14 @@
+# ------------------- Kubescape User/Customer ID ------------------- #
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: kubescape
+ labels:
+ app: kubescape
+ namespace: kubescape
+data:
+ config.json: |
+ {
+ "customerGUID": "",
+ "clusterName": ""
+ }
\ No newline at end of file
diff --git a/examples/cronJob-support/ks-cronJob-non-submit.yaml b/examples/cronJob-support/ks-cronJob-non-submit.yaml
new file mode 100644
index 00000000..ec7f1ab1
--- /dev/null
+++ b/examples/cronJob-support/ks-cronJob-non-submit.yaml
@@ -0,0 +1,32 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: kubescape
+ labels:
+ app: kubescape
+ namespace: kubescape
+spec:
+ # ┌────────────────── timezone (optional)
+ # | ┌───────────── minute (0 - 59)
+ # | │ ┌───────────── hour (0 - 23)
+ # | │ │ ┌───────────── day of the month (1 - 31)
+ # | │ │ │ ┌───────────── month (1 - 12)
+ # | │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
+ # | │ │ │ │ │ 7 is also Sunday on some systems)
+ # | │ │ │ │ │
+ # | │ │ │ │ │
+ # CRON_TZ=UTC * * * * *
+ schedule: "0 0 1 * *"
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ containers:
+ - name: kubescape
+ image: quay.io/armosec/kubescape:latest
+ imagePullPolicy: IfNotPresent
+ command: ["/bin/sh","-c"]
+ args:
+ - kubescape scan framework nsa
+ restartPolicy: OnFailure
+ serviceAccountName: kubescape-discovery
diff --git a/examples/cronJob-support/ks-cronJob-submit.yaml b/examples/cronJob-support/ks-cronJob-submit.yaml
new file mode 100644
index 00000000..3969d653
--- /dev/null
+++ b/examples/cronJob-support/ks-cronJob-submit.yaml
@@ -0,0 +1,40 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: kubescape
+ labels:
+ app: kubescape
+ namespace: kubescape
+spec:
+ # ┌────────────────── timezone (optional)
+ # | ┌───────────── minute (0 - 59)
+ # | │ ┌───────────── hour (0 - 23)
+ # | │ │ ┌───────────── day of the month (1 - 31)
+ # | │ │ │ ┌───────────── month (1 - 12)
+ # | │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
+ # | │ │ │ │ │ 7 is also Sunday on some systems)
+ # | │ │ │ │ │
+ # | │ │ │ │ │
+ # CRON_TZ=UTC * * * * *
+ schedule: "0 0 1 * *"
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ containers:
+ - name: kubescape
+ image: quay.io/armosec/kubescape:latest
+ imagePullPolicy: IfNotPresent
+ command: ["/bin/sh","-c"]
+ args:
+ - kubescape scan framework nsa --submit
+ volumeMounts:
+ - name: kubescape-config-volume
+ mountPath: /root/.kubescape/config.json
+ subPath: config.json
+ restartPolicy: OnFailure
+ serviceAccountName: kubescape-discovery
+ volumes:
+ - name: kubescape-config-volume
+ configMap:
+ name: kubescape
diff --git a/examples/cronJob-support/ks-namespace.yaml b/examples/cronJob-support/ks-namespace.yaml
new file mode 100644
index 00000000..54de4704
--- /dev/null
+++ b/examples/cronJob-support/ks-namespace.yaml
@@ -0,0 +1,7 @@
+# ------------------- Kubescape User/Customer ID ------------------- #
+kind: Namespace
+apiVersion: v1
+metadata:
+ name: kubescape
+ labels:
+ app: kubescape
diff --git a/examples/cronJob-support/ks-serviceAccount.yaml b/examples/cronJob-support/ks-serviceAccount.yaml
new file mode 100644
index 00000000..ace8a33a
--- /dev/null
+++ b/examples/cronJob-support/ks-serviceAccount.yaml
@@ -0,0 +1,61 @@
+---
+# ------------------- Kubescape Service Account ------------------- #
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ app: kubescape
+ name: kubescape-discovery
+ namespace: kubescape
+
+---
+# ------------------- Kubescape Role & Role Binding ------------------- #
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: kubescape-discovery-role
+ namespace: kubescape
+rules:
+- apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["get", "list", "describe"]
+
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: kubescape-discovery-binding
+ namespace: kubescape
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: kubescape-discovery-role
+subjects:
+- kind: ServiceAccount
+ name: kubescape-discovery
+
+---
+# ------------------- Kubescape Cluster Role & Cluster Role Binding ------------------- #
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: kubescape-discovery-clusterroles
+ # "namespace" omitted since ClusterRoles are not namespaced
+rules:
+- apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["get", "list", "describe"]
+
+---
+kind: ClusterRoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+ name: kubescape-discovery-role-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: kubescape-discovery-clusterroles
+subjects:
+- kind: ServiceAccount
+ name: kubescape-discovery
+ namespace: kubescape
\ No newline at end of file
diff --git a/examples/cronJob-support/screenshots/account.png b/examples/cronJob-support/screenshots/account.png
new file mode 100644
index 00000000..edeebf87
Binary files /dev/null and b/examples/cronJob-support/screenshots/account.png differ
diff --git a/examples/cronJob-support/screenshots/add-cluster.png b/examples/cronJob-support/screenshots/add-cluster.png
new file mode 100644
index 00000000..05eb64a6
Binary files /dev/null and b/examples/cronJob-support/screenshots/add-cluster.png differ
diff --git a/examples/cronjob/ks-cronjob.yaml b/examples/cronjob/ks-cronjob.yaml
index b00b2aff..4dfc503f 100644
--- a/examples/cronjob/ks-cronjob.yaml
+++ b/examples/cronjob/ks-cronjob.yaml
@@ -1,3 +1,10 @@
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+#
+# This file is DEPRECATE, please navigate to the official docs ->
+# https://github.com/armosec/kubescape/tree/master/examples/cronJob-support/README.md
+#
+# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
---
# ------------------- Kubescape Service Account ------------------- #
apiVersion: v1
diff --git a/examples/exceptions/README.md b/examples/exceptions/README.md
new file mode 100644
index 00000000..4fd13019
--- /dev/null
+++ b/examples/exceptions/README.md
@@ -0,0 +1,179 @@
+# Kubescape Exceptions
+
+Kubescape Exceptions is the proper way of excluding failed resources from effecting the risk score.
+
+e.g. When a `kube-system` resource fails and it is ok, simply add the resource to the exceptions configurations.
+
+## Definitions
+
+
+* `name`- Exception name - unique name representing the exception
+* `policyType`- Do not change
+* `actions`- List of available actions. Currently alertOnly is supported
+* `resources`- List of resources to apply this exception on
+ * `designatorType: Attributes`- An attribute-based declaration {key: value}
+ Supported keys:
+ * `name`: k8s resource name (case-sensitive, regex supported)
+ * `kind`: k8s resource kind (case-sensitive, regex supported)
+ * `namespace`: k8s resource namespace (case-sensitive, regex supported)
+ * `cluster`: k8s cluster name (usually it is the `current-context`) (case-sensitive, regex supported)
+ * resource labels as key value (case-sensitive, regex NOT supported)
+* `posturePolicies`- An attribute-based declaration {key: value}
+ * `frameworkName` - Framework names can be find [here](https://github.com/armosec/regolibrary/tree/master/frameworks)
+ * `controlName` - Control names can be find [here](https://github.com/armosec/regolibrary/tree/master/controls)
+ * `controlID` - Not yet supported
+ * `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules)
+
+
+## Usage
+
+The `resources` list and `posturePolicies` list are design to be a combination of the resources and policies to exclude
+> You must declare at least one resource and one policy
+
+e.g. If you wish to exclude all namespaces with the label `"environment": "dev"`, the resource list should look as following:
+```
+"resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "namespace": ".*",
+ "environment": "dev"
+ }
+ }
+]
+```
+
+But if you wish to exclude all namespaces **OR** any resource with the label `"environment": "dev"`, the resource list should look as following:
+```
+"resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "namespace": ".*"
+ }
+ },
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "environment": "dev"
+ }
+ }
+]
+```
+
+Same works with the `posturePolicies` list ->
+
+e.g. If you wish to exclude the resources declared in the `resources` list that failed when scanning the `NSA` framework **AND** failed the `Allowed hostPath` control, the `posturePolicies` list should look as following:
+```
+"posturePolicies": [
+ {
+ "frameworkName": "NSA",
+ "controlName": "Allowed hostPath"
+ }
+]
+```
+
+But if you wish to exclude the resources declared in the `resources` list that failed when scanning the `NSA` framework **OR** failed the `Allowed hostPath` control, the `posturePolicies` list should look as following:
+```
+"posturePolicies": [
+ {
+ "frameworkName": "NSA"
+ },
+ {
+ "controlName": "Allowed hostPath"
+ }
+]
+```
+
+## Examples
+
+Here are some examples demonstrating the different ways the exceptions file can be configured
+
+
+### Exclude control
+
+Exclude the ["Allowed hostPath" control](https://github.com/armosec/regolibrary/blob/master/controls/allowedhostpath.json#L2) by declaring the control in the `"posturePolicies"` section.
+
+The resources
+
+```
+[
+ {
+ "name": "exclude-allowed-hostPath-control",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "kind": ".*"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "controlName": "Allowed hostPath"
+ }
+ ]
+ }
+]
+```
+
+### Exclude deployments in the default namespace that failed the "Allowed hostPath" control
+```
+[
+ {
+ "name": "exclude-deployments-in-ns-default",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "namespace": "default",
+ "kind": "Deployment"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "controlName": "Allowed hostPath"
+ }
+ ]
+ }
+]
+```
+
+### Exclude resources with label "app=nginx" running in a minikube cluster that failed the "NSA" or "MITRE" framework
+```
+[
+ {
+ "name": "exclude-nginx-minikube",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "cluster": "minikube",
+ "app": "nginx"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "frameworkName": "NSA"
+ },
+ {
+ "frameworkName": "MITRE"
+ }
+ ]
+ }
+]
+```
\ No newline at end of file
diff --git a/examples/exceptions/exclude-allowed-hostPath-control.json b/examples/exceptions/exclude-allowed-hostPath-control.json
new file mode 100644
index 00000000..106dfaa6
--- /dev/null
+++ b/examples/exceptions/exclude-allowed-hostPath-control.json
@@ -0,0 +1,22 @@
+[
+ {
+ "name": "exclude-allowed-hostPath-control",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "kind": ".*"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "controlName": "Allowed hostPath"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/examples/exceptions/exclude-deployments-in-ns-default.json b/examples/exceptions/exclude-deployments-in-ns-default.json
new file mode 100644
index 00000000..dceb4010
--- /dev/null
+++ b/examples/exceptions/exclude-deployments-in-ns-default.json
@@ -0,0 +1,23 @@
+[
+ {
+ "name": "exclude-deployments-in-ns-default",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "namespace": "default",
+ "kind": "Deployment"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "controlName": "Allowed hostPath"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/examples/exceptions.json b/examples/exceptions/exclude-kube-namespaces.json
similarity index 85%
rename from examples/exceptions.json
rename to examples/exceptions/exclude-kube-namespaces.json
index 7f67a52b..a8aecd4a 100644
--- a/examples/exceptions.json
+++ b/examples/exceptions/exclude-kube-namespaces.json
@@ -28,6 +28,12 @@
"posturePolicies": [
{
"frameworkName": "NSA"
+ },
+ {
+ "frameworkName": "MITRE"
+ },
+ {
+ "frameworkName": "ArmoBest"
}
]
}
diff --git a/examples/exceptions/exclude-nginx-in-minikube.json b/examples/exceptions/exclude-nginx-in-minikube.json
new file mode 100644
index 00000000..c0e3cdfe
--- /dev/null
+++ b/examples/exceptions/exclude-nginx-in-minikube.json
@@ -0,0 +1,26 @@
+[
+ {
+ "name": "exclude-nginx-in-minikube",
+ "policyType": "postureExceptionPolicy",
+ "actions": [
+ "alertOnly"
+ ],
+ "resources": [
+ {
+ "designatorType": "Attributes",
+ "attributes": {
+ "cluster": "minikube",
+ "app": "nginx"
+ }
+ }
+ ],
+ "posturePolicies": [
+ {
+ "frameworkName": "NSA"
+ },
+ {
+ "frameworkName": "MITRE"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/examples/helm_chart/Chart.yaml b/examples/helm_chart/Chart.yaml
new file mode 100644
index 00000000..79069252
--- /dev/null
+++ b/examples/helm_chart/Chart.yaml
@@ -0,0 +1,29 @@
+apiVersion: v2
+name: kubescape
+description:
+ Kubescape is the first open-source tool for testing if Kubernetes is deployed securely according to multiple frameworks
+ regulatory, customized company policies and DevSecOps best practices, such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) and the [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) .
+ Kubescape scans K8s clusters, YAML files, and HELM charts, and detect misconfigurations and software vulnerabilities at early stages of the CI/CD pipeline and provides a risk score instantly and risk trends over time.
+ Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI and Github workflows.
+
+
+# A chart can be either an 'application' or a 'library' chart.
+#
+# Application charts are a collection of templates that can be packaged into versioned archives
+# to be deployed.
+#
+# Library charts provide useful utilities or functions for the chart developer. They're included as
+# a dependency of application charts to inject those utilities and functions into the rendering
+# pipeline. Library charts do not define any templates and therefore cannot be deployed.
+type: application
+
+# This is the chart version. This version number should be incremented each time you make changes
+# to the chart and its templates, including the app version.
+# Versions are expected to follow Semantic Versioning (https://semver.org/)
+version: 1.0.0
+
+# This is the version number of the application being deployed. This version number should be
+# incremented each time you make changes to the application. Versions are not expected to
+# follow Semantic Versioning. They should reflect the version the application is using.
+# It is recommended to use it with quotes.
+appVersion: "v1.0.128"
diff --git a/examples/helm_chart/README.md b/examples/helm_chart/README.md
new file mode 100644
index 00000000..d296b88c
--- /dev/null
+++ b/examples/helm_chart/README.md
@@ -0,0 +1,27 @@
+# kubescape
+
+  
+
+Kubescape is the first open-source tool for testing if Kubernetes is deployed securely according to multiple frameworks regulatory, customized company policies and DevSecOps best practices, such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) and the [MITRE ATT&CK®](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/) . Kubescape scans K8s clusters, YAML files, and HELM charts, and detect misconfigurations and software vulnerabilities at early stages of the CI/CD pipeline and provides a risk score instantly and risk trends over time. Kubescape integrates natively with other DevOps tools, including Jenkins, CircleCI and Github workflows.
+
+## Values
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| affinity | object | `{}` | |
+| configMap | object | `{"create":true,"params":{"clusterName":"","customerGUID":","}}` | ARMO customer information |
+| fullnameOverride | string | `""` | |
+| image | object | `{"imageName":"kubescape","pullPolicy":"IfNotPresent","repository":"quay.io/armosec","tag":"latest"}` | Image and version to deploy |
+| imagePullSecrets | list | `[]` | |
+| nameOverride | string | `""` | |
+| nodeSelector | object | `{}` | |
+| podAnnotations | object | `{}` | |
+| podSecurityContext | object | `{}` | |
+| resources | object | `{"limits":{"cpu":"500m","memory":"512Mi"},"requests":{"cpu":"200m","memory":"256Mi"}}` | Default resources for running the service in cluster |
+| schedule | string | `"0 0 * * *"` | Frequency of running the scan |
+| securityContext | object | `{}` | |
+| serviceAccount | object | `{"annotations":{},"create":true,"name":"kubescape-discovery"}` | Service account that runs the scan and has permissions to view the cluster |
+| tolerations | list | `[]` | |
+
+----------------------------------------------
+Autogenerated from chart metadata using [helm-docs v1.5.0](https://github.com/norwoodj/helm-docs/releases/v1.5.0)
diff --git a/examples/helm_chart/templates/_helpers.tpl b/examples/helm_chart/templates/_helpers.tpl
new file mode 100644
index 00000000..b933db8a
--- /dev/null
+++ b/examples/helm_chart/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "kubescape.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "kubescape.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "kubescape.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "kubescape.labels" -}}
+helm.sh/chart: {{ include "kubescape.chart" . }}
+{{ include "kubescape.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "kubescape.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "kubescape.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "kubescape.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "kubescape.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/examples/helm_chart/templates/clusterrole.yaml b/examples/helm_chart/templates/clusterrole.yaml
new file mode 100644
index 00000000..fa2bf7ee
--- /dev/null
+++ b/examples/helm_chart/templates/clusterrole.yaml
@@ -0,0 +1,11 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "kubescape.fullname" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+rules:
+ - apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["get", "list", "describe"]
+
diff --git a/examples/helm_chart/templates/clusterrolebinding.yaml b/examples/helm_chart/templates/clusterrolebinding.yaml
new file mode 100644
index 00000000..74ec8006
--- /dev/null
+++ b/examples/helm_chart/templates/clusterrolebinding.yaml
@@ -0,0 +1,16 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "kubescape.fullname" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "kubescape.fullname" . }}
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "kubescape.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace | quote }}
+
+
diff --git a/examples/helm_chart/templates/configmap.yaml b/examples/helm_chart/templates/configmap.yaml
new file mode 100644
index 00000000..861a8321
--- /dev/null
+++ b/examples/helm_chart/templates/configmap.yaml
@@ -0,0 +1,14 @@
+{{- if .Values.configMap.create -}}
+kind: ConfigMap
+apiVersion: v1
+metadata:
+ name: {{ include "kubescape.fullname" . }}-configmap
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+data:
+ config.json: |
+ {
+ "customerGUID": "{{ .Values.configMap.params.customerGUID }}",
+ "clusterName": "{{ .Values.configMap.params.clusterName }}"
+ }
+{{- end }}
diff --git a/examples/helm_chart/templates/cronjob.yaml b/examples/helm_chart/templates/cronjob.yaml
new file mode 100644
index 00000000..99800e68
--- /dev/null
+++ b/examples/helm_chart/templates/cronjob.yaml
@@ -0,0 +1,28 @@
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: {{ include "kubescape.fullname" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+spec:
+ schedule: "{{ .Values.schedule }}"
+ jobTemplate:
+ spec:
+ template:
+ spec:
+ containers:
+ - name: {{ .Chart.Name }}
+ image: "{{ .Values.image.repository }}/{{ .Values.image.imageName }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.image.pullPolicy }}
+ command: ["/bin/sh", "-c"]
+ args: ["kubescape scan framework nsa --submit"]
+ volumeMounts:
+ - name: kubescape-config-volume
+ mountPath: /root/.kubescape/config.json
+ subPath: config.json
+ restartPolicy: OnFailure
+ serviceAccountName: {{ include "kubescape.serviceAccountName" . }}
+ volumes:
+ - name: kubescape-config-volume
+ configMap:
+ name: {{ include "kubescape.fullname" . }}-configmap
diff --git a/examples/helm_chart/templates/role.yaml b/examples/helm_chart/templates/role.yaml
new file mode 100644
index 00000000..ecfc740a
--- /dev/null
+++ b/examples/helm_chart/templates/role.yaml
@@ -0,0 +1,11 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: {{ include "kubescape.fullname" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+rules:
+ - apiGroups: ["*"]
+ resources: ["*"]
+ verbs: ["get", "list", "describe"]
+
diff --git a/examples/helm_chart/templates/rolebinding.yaml b/examples/helm_chart/templates/rolebinding.yaml
new file mode 100644
index 00000000..0b392abd
--- /dev/null
+++ b/examples/helm_chart/templates/rolebinding.yaml
@@ -0,0 +1,16 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: {{ include "kubescape.fullname" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "kubescape.fullname" . }}
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "kubescape.serviceAccountName" . }}
+ namespace: {{ .Release.Namespace | quote }}
+
+
diff --git a/examples/helm_chart/templates/serviceaccount.yaml b/examples/helm_chart/templates/serviceaccount.yaml
new file mode 100644
index 00000000..50abf451
--- /dev/null
+++ b/examples/helm_chart/templates/serviceaccount.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "kubescape.serviceAccountName" . }}
+ labels:
+ {{- include "kubescape.labels" . | nindent 4 }}
+ {{- with .Values.serviceAccount.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+{{- end }}
diff --git a/examples/helm_chart/values.yaml b/examples/helm_chart/values.yaml
new file mode 100644
index 00000000..883c4bb7
--- /dev/null
+++ b/examples/helm_chart/values.yaml
@@ -0,0 +1,74 @@
+# Default values for kubescape.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+# -- Frequency of running the scan
+# ┌────────────── timezone (optional)
+# | ┌───────────── minute (0 - 59)
+# | │ ┌───────────── hour (0 - 23)
+# | │ │ ┌───────────── day of the month (1 - 31)
+# | │ │ │ ┌───────────── month (1 - 12)
+# | │ │ │ │ ┌───────────── day of the week (0 - 6) (Sunday to Saturday;
+# | │ │ │ │ │ 7 is also Sunday on some systems)
+# | │ │ │ │ │
+# | │ │ │ │ │
+# UTC * * * * *
+schedule: "* * 1 * *"
+
+# -- Image and version to deploy
+image:
+ repository: quay.io/armosec
+ imageName: kubescape
+ pullPolicy: Always
+ # Overrides the image tag whose default is the chart appVersion.
+ tag: latest
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+# -- Service account that runs the scan and has permissions to view the cluster
+serviceAccount:
+ # Specifies whether a service account should be created
+ create: true
+ # Annotations to add to the service account
+ annotations: {}
+ # The name of the service account to use.
+ # If not set and create is true, a name is generated using the fullname template
+ name: "kubescape-discovery"
+
+# -- ARMO customer information
+configMap:
+ create: false
+ params:
+ customerGUID:
+ clusterName:
+
+podAnnotations: {}
+
+podSecurityContext: {}
+ # fsGroup: 2000
+
+securityContext: {}
+ # capabilities:
+ # drop:
+ # - ALL
+ # readOnlyRootFilesystem: true
+ # runAsNonRoot: true
+ # runAsUser: 1000
+
+# -- Default resources for running the service in cluster
+resources:
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ requests:
+ cpu: 200m
+ memory: 256Mi
+
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/go.mod b/go.mod
index 0e3cbc03..ac553962 100644
--- a/go.mod
+++ b/go.mod
@@ -3,9 +3,10 @@ module github.com/armosec/kubescape
go 1.17
require (
- github.com/armosec/armoapi-go v0.0.8
- github.com/armosec/k8s-interface v0.0.5
- github.com/armosec/opa-utils v0.0.13
+ github.com/armosec/armoapi-go v0.0.23
+ github.com/armosec/k8s-interface v0.0.8
+ github.com/armosec/opa-utils v0.0.42
+ github.com/armosec/rbac-utils v0.0.1
github.com/armosec/utils-go v0.0.3
github.com/briandowns/spinner v1.16.0
github.com/enescakir/emoji v1.0.0
diff --git a/go.sum b/go.sum
index 842bf3e4..d298636d 100644
--- a/go.sum
+++ b/go.sum
@@ -84,13 +84,14 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qmSkApyq5xFs=
-github.com/armosec/armoapi-go v0.0.7/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
-github.com/armosec/armoapi-go v0.0.8 h1:JPa9rZynuE2RucamDh6dsy/sjCScmWDsyt1zagJFCDo=
-github.com/armosec/armoapi-go v0.0.8/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
-github.com/armosec/k8s-interface v0.0.5 h1:DWQXZNMSsYQeLQ6xpB21ueFMR9oFnz28iWQTNn31TAk=
-github.com/armosec/k8s-interface v0.0.5/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
-github.com/armosec/opa-utils v0.0.13 h1:QkmmYX0lzC7ZNGetyD8ysRKQHgJhjMfvRUW2cp+hz2o=
-github.com/armosec/opa-utils v0.0.13/go.mod h1:E0mFTVx+4BYAVvO2hxWnIniv/IZIogRCak8BkKd7KK4=
+github.com/armosec/armoapi-go v0.0.23 h1:jqoLIWM5CR7DCD9fpFgN0ePqtHvOCoZv/XzCwsUluJU=
+github.com/armosec/armoapi-go v0.0.23/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw=
+github.com/armosec/k8s-interface v0.0.8 h1:Eo3Qen4yFXxzVem49FNeij2ckyzHSAJ0w6PZMaSEIm8=
+github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM=
+github.com/armosec/opa-utils v0.0.42 h1:7YzQJNVBmM0+1nWOAiUgDt+mvlVEwApg80FjMh4oxXo=
+github.com/armosec/opa-utils v0.0.42/go.mod h1:OqewZoSqKD5udtQ4lGFixb8yyFNqLq9zqinlAL6KSjM=
+github.com/armosec/rbac-utils v0.0.1 h1:N2MI98F/0zbDjmRZ29CNElU1AXkFLk5csd/qAHOBdXY=
+github.com/armosec/rbac-utils v0.0.1/go.mod h1:pQ8CBiij8kSKV7aeZm9FMvtZN28VgA7LZcYyTWimq40=
github.com/armosec/utils-go v0.0.2/go.mod h1:itWmRLzRdsnwjpEOomL0mBWGnVNNIxSjDAdyc+b0iUo=
github.com/armosec/utils-go v0.0.3 h1:uyQI676yRciQM0sSN9uPoqHkbspTxHO0kmzXhBeE/xU=
github.com/armosec/utils-go v0.0.3/go.mod h1:itWmRLzRdsnwjpEOomL0mBWGnVNNIxSjDAdyc+b0iUo=
@@ -98,6 +99,7 @@ github.com/armosec/utils-k8s-go v0.0.1 h1:Ay3y7fW+4+FjVc0+obOWm8YsnEvM31vPAVoKTy
github.com/armosec/utils-k8s-go v0.0.1/go.mod h1:qrU4pmY2iZsOb39Eltpm0sTTNM3E4pmeyWx4dgDUC2U=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/aws/aws-sdk-go v1.41.1/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
+github.com/aws/aws-sdk-go v1.41.11/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
diff --git a/main.go b/main.go
index 956b6622..07bf089e 100644
--- a/main.go
+++ b/main.go
@@ -1,23 +1,9 @@
package main
import (
- "fmt"
- "os"
-
"github.com/armosec/kubescape/clihandler/cmd"
)
func main() {
- CheckLatestVersion()
cmd.Execute()
}
-
-func CheckLatestVersion() {
- latest, err := cmd.GetLatestVersion()
- if err != nil {
- fmt.Fprintf(os.Stderr, "error: %v\n", err)
- } else if latest != cmd.BuildNumber {
- fmt.Println("Warning: You are not updated to the latest release: " + latest)
- }
-
-}
diff --git a/opaprocessor/processorhandler.go b/opaprocessor/processorhandler.go
index ab79456e..31ede043 100644
--- a/opaprocessor/processorhandler.go
+++ b/opaprocessor/processorhandler.go
@@ -8,7 +8,6 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/exceptions"
"github.com/armosec/opa-utils/reporthandling"
- "github.com/armosec/opa-utils/score"
"github.com/armosec/k8s-interface/k8sinterface"
@@ -16,42 +15,37 @@ import (
"github.com/golang/glog"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
- "github.com/open-policy-agent/opa/storage"
uuid "github.com/satori/go.uuid"
)
const ScoreConfigPath = "/resources/config"
-var RegoK8sCredentials storage.Store
-
type OPAProcessorHandler struct {
- processedPolicy *chan *cautils.OPASessionObj
- reportResults *chan *cautils.OPASessionObj
- // componentConfig cautils.ComponentConfig
+ processedPolicy *chan *cautils.OPASessionObj
+ reportResults *chan *cautils.OPASessionObj
+ regoDependenciesData *resources.RegoDependenciesData
}
type OPAProcessor struct {
*cautils.OPASessionObj
+ regoDependenciesData *resources.RegoDependenciesData
}
-func NewOPAProcessor(sessionObj *cautils.OPASessionObj) *OPAProcessor {
+func NewOPAProcessor(sessionObj *cautils.OPASessionObj, regoDependenciesData *resources.RegoDependenciesData) *OPAProcessor {
+ if regoDependenciesData != nil && sessionObj != nil {
+ regoDependenciesData.PostureControlInputs = sessionObj.RegoInputData.PostureControlInputs
+ }
return &OPAProcessor{
- OPASessionObj: sessionObj,
+ OPASessionObj: sessionObj,
+ regoDependenciesData: regoDependenciesData,
}
}
func NewOPAProcessorHandler(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessorHandler {
-
- regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig(), cautils.ClusterName)
- store, err := regoDependenciesData.TOStorage()
- if err != nil {
- panic(err)
- }
- RegoK8sCredentials = store
-
return &OPAProcessorHandler{
- processedPolicy: processedPolicy,
- reportResults: reportResults,
+ processedPolicy: processedPolicy,
+ reportResults: reportResults,
+ regoDependenciesData: resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig(), cautils.ClusterName),
}
}
@@ -59,7 +53,7 @@ func (opaHandler *OPAProcessorHandler) ProcessRulesListenner() {
for {
opaSessionObj := <-*opaHandler.processedPolicy
- opap := NewOPAProcessor(opaSessionObj)
+ opap := NewOPAProcessor(opaSessionObj, opaHandler.regoDependenciesData)
// process
if err := opap.Process(); err != nil {
@@ -126,6 +120,8 @@ func (opap *OPAProcessor) processControl(control *reporthandling.Control) (*repo
controlReport := reporthandling.ControlReport{}
controlReport.PortalBase = control.PortalBase
controlReport.ControlID = control.ControlID
+ controlReport.BaseScore = control.BaseScore
+
controlReport.Control_ID = control.Control_ID // TODO: delete when 'id' is deprecated
controlReport.Name = control.Name
@@ -204,11 +200,16 @@ func (opap *OPAProcessor) runRegoOnK8s(rule *reporthandling.PolicyRule, k8sObjec
}
func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRego *ast.Compiler) ([]reporthandling.RuleResponse, error) {
+ store, err := opap.regoDependenciesData.TOStorage() // get store
+ if err != nil {
+ return nil, err
+ }
+
rego := rego.New(
rego.Query("data.armo_builtins"), // get package name from rule
rego.Compiler(compiledRego),
rego.Input(inputObj),
- rego.Store(RegoK8sCredentials),
+ rego.Store(store),
)
// Run evaluation
@@ -226,17 +227,6 @@ func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRe
return results, nil
}
-func (opap *OPAProcessor) updateScore() {
-
- if !k8sinterface.ConnectedToCluster {
- return
- }
-
- // calculate score
- s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath)
- s.Calculate(opap.PostureReport.FrameworkReports)
-}
-
func (opap *OPAProcessor) updateResults() {
for f := range opap.PostureReport.FrameworkReports {
// set exceptions
diff --git a/opaprocessor/processorhandler_test.go b/opaprocessor/processorhandler_test.go
index 49e0b4f2..08e11154 100644
--- a/opaprocessor/processorhandler_test.go
+++ b/opaprocessor/processorhandler_test.go
@@ -5,6 +5,7 @@ import (
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
+ "github.com/armosec/opa-utils/resources"
"github.com/armosec/k8s-interface/k8sinterface"
// _ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -24,7 +25,7 @@ func TestProcess(t *testing.T) {
opaSessionObj.Frameworks = []reporthandling.Framework{*reporthandling.MockFrameworkA()}
opaSessionObj.K8SResources = &k8sResources
- opap := NewOPAProcessor(opaSessionObj)
+ opap := NewOPAProcessor(opaSessionObj, resources.NewRegoDependenciesDataMock())
opap.Process()
opap.updateResults()
for _, f := range opap.PostureReport.FrameworkReports {
diff --git a/policyhandler/handlenotification.go b/policyhandler/handlenotification.go
index 76ecbb6e..8db8963e 100644
--- a/policyhandler/handlenotification.go
+++ b/policyhandler/handlenotification.go
@@ -4,30 +4,23 @@ import (
"fmt"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/resourcehandler"
"github.com/armosec/opa-utils/reporthandling"
-
- "github.com/armosec/armoapi-go/armotypes"
- "github.com/armosec/k8s-interface/k8sinterface"
)
-var supportedFrameworks = []reporthandling.PolicyIdentifier{
- {Kind: "Framework", Name: "nsa"},
- {Kind: "Framework", Name: "mitre"},
-}
-
// PolicyHandler -
type PolicyHandler struct {
- k8s *k8sinterface.KubernetesApi
+ resourceHandler resourcehandler.IResourceHandler
// we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func
processPolicy *chan *cautils.OPASessionObj
getters *cautils.Getters
}
// CreatePolicyHandler Create ws-handler obj
-func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterface.KubernetesApi) *PolicyHandler {
+func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, resourceHandler resourcehandler.IResourceHandler) *PolicyHandler {
return &PolicyHandler{
- k8s: k8s,
- processPolicy: processPolicy,
+ resourceHandler: resourceHandler,
+ processPolicy: processPolicy,
}
}
@@ -38,15 +31,9 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *repo
policyHandler.getters = &scanInfo.Getters
// get policies
- frameworks, exceptions, err := policyHandler.getPolicies(notification)
- if err != nil {
+ if err := policyHandler.getPolicies(notification, opaSessionObj); err != nil {
return err
}
- if len(frameworks) == 0 {
- return fmt.Errorf("empty list of frameworks")
- }
- opaSessionObj.Frameworks = frameworks
- opaSessionObj.Exceptions = exceptions
k8sResources, err := policyHandler.getResources(notification, opaSessionObj, scanInfo)
if err != nil {
@@ -62,36 +49,8 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *repo
return nil
}
-func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.PolicyNotification) ([]reporthandling.Framework, []armotypes.PostureExceptionPolicy, error) {
-
- cautils.ProgressTextDisplay("Downloading/Loading policy definitions")
-
- frameworks, exceptions, err := policyHandler.GetPoliciesFromBackend(notification)
- if err != nil {
- return frameworks, exceptions, err
- }
-
- if len(frameworks) == 0 {
- err := fmt.Errorf("could not download any policies, please check previous logs")
- return frameworks, exceptions, err
- }
- //if notification.Rules
- cautils.SuccessTextDisplay("Downloaded/Loaded policy")
-
- return frameworks, exceptions, nil
-}
-
func (policyHandler *PolicyHandler) getResources(notification *reporthandling.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
- var k8sResources *cautils.K8SResources
- var err error
- if k8sinterface.ConnectedToCluster { // TODO - use interface
- if opaSessionObj.PostureReport.ClusterAPIServerInfo, err = policyHandler.k8s.KubernetesClient.Discovery().ServerVersion(); err != nil {
- cautils.ErrorDisplay(fmt.Sprintf("Failed to discover API server inforamtion: %v", err))
- }
- k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces)
- } else {
- k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
- }
- return k8sResources, err
+ opaSessionObj.PostureReport.ClusterAPIServerInfo = policyHandler.resourceHandler.GetClusterAPIServerInfo()
+ return policyHandler.resourceHandler.GetResources(opaSessionObj.Frameworks, ¬ification.Designators)
}
diff --git a/policyhandler/handlepullpolicies.go b/policyhandler/handlepullpolicies.go
index 0762a05e..005fe228 100644
--- a/policyhandler/handlepullpolicies.go
+++ b/policyhandler/handlepullpolicies.go
@@ -2,90 +2,71 @@ package policyhandler
import (
"fmt"
- "strings"
- "github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
)
-func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *reporthandling.PolicyNotification) ([]reporthandling.Framework, []armotypes.PostureExceptionPolicy, error) {
- var errs error
+func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.PolicyNotification, policiesAndResources *cautils.OPASessionObj) error {
+ cautils.ProgressTextDisplay("Downloading/Loading policy definitions")
+
+ frameworks, err := policyHandler.getScanPolicies(notification)
+ if err != nil {
+ return err
+ }
+ if len(frameworks) == 0 {
+ return fmt.Errorf("failed to download policies, please ARMO team for more information")
+ }
+
+ policiesAndResources.Frameworks = frameworks
+
+ // get exceptions
+ exceptionPolicies, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
+ if err == nil {
+ policiesAndResources.Exceptions = exceptionPolicies
+ }
+
+ // get account configuration
+ controlsInputs, err := policyHandler.getters.ControlsInputsGetter.GetControlsInputs(cautils.CustomerGUID, cautils.ClusterName)
+ if err == nil {
+ policiesAndResources.RegoInputData.PostureControlInputs = controlsInputs
+ }
+
+ cautils.SuccessTextDisplay("Downloaded/Loaded policy")
+ return nil
+}
+
+func (policyHandler *PolicyHandler) getScanPolicies(notification *reporthandling.PolicyNotification) ([]reporthandling.Framework, error) {
frameworks := []reporthandling.Framework{}
- exceptionPolicies := []armotypes.PostureExceptionPolicy{}
- // Get - cacli opa get
- for _, rule := range notification.Rules {
- switch rule.Kind {
- case reporthandling.KindFramework:
- receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
+
+ switch getScanKind(notification) {
+ case reporthandling.KindFramework: // Download frameworks
+ for _, rule := range notification.Rules {
+ receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(rule.Name)
+ if err != nil {
+ return frameworks, policyDownloadError(err)
+ }
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
- if recExceptionPolicies != nil {
- exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
- }
- } else if err != nil {
- if strings.Contains(err.Error(), "unsupported protocol scheme") {
- err = fmt.Errorf("failed to download from GitHub release, try running with `--use-default` flag")
- }
- return nil, nil, fmt.Errorf("kind: %v, name: %s, error: %s", rule.Kind, rule.Name, err.Error())
}
- case reporthandling.KindControl:
- receivedControls, recExceptionPolicies, err := policyHandler.getControl(rule.Name)
- if receivedControls != nil {
- f := reporthandling.Framework{
- Controls: receivedControls,
- }
- frameworks = append(frameworks, f)
- if recExceptionPolicies != nil {
- exceptionPolicies = append(exceptionPolicies, recExceptionPolicies...)
- }
- } else if err != nil {
- if strings.Contains(err.Error(), "unsupported protocol scheme") {
- err = fmt.Errorf("failed to download from GitHub release, try running with `--use-default` flag")
- }
- return nil, nil, fmt.Errorf("error: %s", err.Error())
- }
- // TODO: add case for control from file
- default:
- err := fmt.Errorf("missing rule kind, expected: %s", reporthandling.KindFramework)
- errs = fmt.Errorf("%s", err.Error())
}
+ case reporthandling.KindControl: // Download controls
+ f := reporthandling.Framework{}
+ var receivedControl *reporthandling.Control
+ var err error
+ for _, rule := range notification.Rules {
+ receivedControl, err = policyHandler.getters.PolicyGetter.GetControl(rule.Name)
+ if err != nil {
+ return frameworks, policyDownloadError(err)
+ }
+ if receivedControl != nil {
+ f.Controls = append(f.Controls, *receivedControl)
+ }
+ }
+ frameworks = append(frameworks, f)
+ // TODO: add case for control from file
+ default:
+ return frameworks, fmt.Errorf("unknown policy kind")
}
- return frameworks, exceptionPolicies, errs
-}
-
-func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*reporthandling.Framework, []armotypes.PostureExceptionPolicy, error) {
- receivedFramework, err := policyHandler.getters.PolicyGetter.GetFramework(policyName)
- if err != nil {
- return nil, nil, err
- }
-
- receivedException, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
- if err != nil {
- return receivedFramework, nil, err
- }
-
- return receivedFramework, receivedException, nil
-}
-
-// Get control by name
-func (policyHandler *PolicyHandler) getControl(policyName string) ([]reporthandling.Control, []armotypes.PostureExceptionPolicy, error) {
-
- controls := []reporthandling.Control{}
-
- control, err := policyHandler.getters.PolicyGetter.GetControl(policyName)
- if err != nil {
- return nil, nil, err
- }
- if control == nil {
- return nil, nil, fmt.Errorf("control not found")
- }
- controls = append(controls, *control)
-
- exceptions, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
- if err != nil {
- return controls, nil, err
- }
-
- return controls, exceptions, nil
+ return frameworks, nil
}
diff --git a/policyhandler/handlepullpoliciesutils.go b/policyhandler/handlepullpoliciesutils.go
new file mode 100644
index 00000000..c4a4b321
--- /dev/null
+++ b/policyhandler/handlepullpoliciesutils.go
@@ -0,0 +1,21 @@
+package policyhandler
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/armosec/opa-utils/reporthandling"
+)
+
+func getScanKind(notification *reporthandling.PolicyNotification) reporthandling.NotificationPolicyKind {
+ if len(notification.Rules) > 0 {
+ return notification.Rules[0].Kind
+ }
+ return "unknown"
+}
+func policyDownloadError(err error) error {
+ if strings.Contains(err.Error(), "unsupported protocol scheme") {
+ err = fmt.Errorf("failed to download from GitHub release, try running with `--use-default` flag")
+ }
+ return err
+}
diff --git a/resourcehandler/fieldselector.go b/resourcehandler/fieldselector.go
new file mode 100644
index 00000000..fec72d4e
--- /dev/null
+++ b/resourcehandler/fieldselector.go
@@ -0,0 +1,61 @@
+package resourcehandler
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/armosec/k8s-interface/k8sinterface"
+ "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%s,", fieldSelector, operator, n)
+ }
+ return fieldSelectors
+
+}
diff --git a/policyhandler/filesloader.go b/resourcehandler/filesloader.go
similarity index 88%
rename from policyhandler/filesloader.go
rename to resourcehandler/filesloader.go
index 60f40f5a..eabb9109 100644
--- a/policyhandler/filesloader.go
+++ b/resourcehandler/filesloader.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"bytes"
@@ -8,7 +8,9 @@ import (
"path/filepath"
"strings"
+ "github.com/armosec/armoapi-go/armotypes"
"github.com/armosec/k8s-interface/workloadinterface"
+ "k8s.io/apimachinery/pkg/version"
"github.com/armosec/k8s-interface/k8sinterface"
"github.com/armosec/kubescape/cautils"
@@ -29,11 +31,22 @@ const (
JSON_FILE_FORMAT FileFormat = "json"
)
-func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
+// FileResourceHandler handle resources from files and URLs
+type FileResourceHandler struct {
+ inputPatterns []string
+}
+
+func NewFileResourceHandler(inputPatterns []string) *FileResourceHandler {
+ return &FileResourceHandler{
+ inputPatterns: inputPatterns,
+ }
+}
+
+func (fileHandler *FileResourceHandler) GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) {
workloads := []k8sinterface.IWorkload{}
// load resource from local file system
- w, err := loadResourcesFromFiles(scanInfo.InputPatterns)
+ w, err := loadResourcesFromFiles(fileHandler.inputPatterns)
if err != nil {
return nil, err
}
@@ -42,7 +55,7 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr
}
// load resources from url
- w, err = loadResourcesFromUrl(scanInfo.InputPatterns)
+ w, err = loadResourcesFromUrl(fileHandler.inputPatterns)
if err != nil {
return nil, err
}
@@ -59,7 +72,7 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr
// build resources map
// map resources based on framework required resources: map["/group/version/kind"][]
- k8sResources := setResourceMap(frameworks)
+ k8sResources := setResourceMap(frameworks) // TODO - support designators
// save only relevant resources
for i := range allResources {
@@ -72,6 +85,10 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr
}
+func (fileHandler *FileResourceHandler) GetClusterAPIServerInfo() *version.Info {
+ return nil
+}
+
func loadResourcesFromFiles(inputPatterns []string) ([]k8sinterface.IWorkload, error) {
files, errs := listFiles(inputPatterns)
if len(errs) > 0 {
diff --git a/policyhandler/filesloader_test.go b/resourcehandler/filesloader_test.go
similarity index 98%
rename from policyhandler/filesloader_test.go
rename to resourcehandler/filesloader_test.go
index f2bea56b..04d64082 100644
--- a/policyhandler/filesloader_test.go
+++ b/resourcehandler/filesloader_test.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"fmt"
diff --git a/policyhandler/k8sresources.go b/resourcehandler/k8sresources.go
similarity index 54%
rename from policyhandler/k8sresources.go
rename to resourcehandler/k8sresources.go
index 28f17577..5b6c0d08 100644
--- a/policyhandler/k8sresources.go
+++ b/resourcehandler/k8sresources.go
@@ -1,8 +1,8 @@
-package policyhandler
+package resourcehandler
import (
+ "context"
"fmt"
- "strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
@@ -15,12 +15,23 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8slabels "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
+ "k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/dynamic"
)
-const SelectAllResources = "*"
+type K8sResourceHandler struct {
+ k8s *k8sinterface.KubernetesApi
+ fieldSelector IFieldSelector
+}
-func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator, excludedNamespaces string) (*cautils.K8SResources, error) {
+func NewK8sResourceHandler(k8s *k8sinterface.KubernetesApi, fieldSelector IFieldSelector) *K8sResourceHandler {
+ return &K8sResourceHandler{
+ k8s: k8s,
+ fieldSelector: fieldSelector,
+ }
+}
+
+func (k8sHandler *K8sResourceHandler) GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) {
// get k8s resources
cautils.ProgressTextDisplay("Accessing Kubernetes objects")
@@ -31,7 +42,7 @@ func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling.
_, namespace, labels := armotypes.DigestPortalDesignator(designator)
// pull k8s recourses
- if err := policyHandler.pullResources(k8sResourcesMap, namespace, labels, excludedNamespaces); err != nil {
+ if err := k8sHandler.pullResources(k8sResourcesMap, namespace, labels); err != nil {
return k8sResourcesMap, err
}
@@ -39,13 +50,21 @@ func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling.
return k8sResourcesMap, nil
}
-func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string, excludedNamespaces string) error {
+func (k8sHandler *K8sResourceHandler) GetClusterAPIServerInfo() *version.Info {
+ clusterAPIServerInfo, err := k8sHandler.k8s.KubernetesClient.Discovery().ServerVersion()
+ if err != nil {
+ cautils.ErrorDisplay(fmt.Sprintf("Failed to discover API server information: %v", err))
+ return nil
+ }
+ return clusterAPIServerInfo
+}
+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 := policyHandler.pullSingleResource(&gvr, namespace, labels, excludedNamespaces)
+ result, err := k8sHandler.pullSingleResource(&gvr, namespace, labels)
if err != nil {
// handle error
if errs == nil {
@@ -61,13 +80,13 @@ func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResou
return errs
}
-func (policyHandler *PolicyHandler) 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()
@@ -76,13 +95,13 @@ func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVer
// set dynamic object
var clientResource dynamic.ResourceInterface
if namespace != "" && k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) {
- clientResource = policyHandler.k8s.DynamicClient.Resource(*resource).Namespace(namespace)
+ clientResource = k8sHandler.k8s.DynamicClient.Resource(*resource).Namespace(namespace)
} else {
- clientResource = policyHandler.k8s.DynamicClient.Resource(*resource)
+ clientResource = k8sHandler.k8s.DynamicClient.Resource(*resource)
}
// list resources
- result, err := clientResource.List(policyHandler.k8s.Context, listOptions)
+ result, err := clientResource.List(context.Background(), listOptions)
if err != nil {
return nil, fmt.Errorf("failed to get resource: %v, namespace: %s, labelSelector: %v, reason: %s", resource, namespace, listOptions.LabelSelector, err.Error())
}
@@ -90,18 +109,3 @@ func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVer
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)
- }
-}
diff --git a/policyhandler/k8sresourcesutils.go b/resourcehandler/k8sresourcesutils.go
similarity index 98%
rename from policyhandler/k8sresourcesutils.go
rename to resourcehandler/k8sresourcesutils.go
index 48932a42..2c14b05b 100644
--- a/policyhandler/k8sresourcesutils.go
+++ b/resourcehandler/k8sresourcesutils.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"github.com/armosec/kubescape/cautils"
diff --git a/policyhandler/k8sresourcesutils_test.go b/resourcehandler/k8sresourcesutils_test.go
similarity index 98%
rename from policyhandler/k8sresourcesutils_test.go
rename to resourcehandler/k8sresourcesutils_test.go
index 63239047..60f38936 100644
--- a/policyhandler/k8sresourcesutils_test.go
+++ b/resourcehandler/k8sresourcesutils_test.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"github.com/armosec/k8s-interface/k8sinterface"
diff --git a/policyhandler/repositoryscanner.go b/resourcehandler/repositoryscanner.go
similarity index 97%
rename from policyhandler/repositoryscanner.go
rename to resourcehandler/repositoryscanner.go
index a568e8c5..f4f7901c 100644
--- a/policyhandler/repositoryscanner.go
+++ b/resourcehandler/repositoryscanner.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"encoding/json"
@@ -95,7 +95,7 @@ func (g *GitHubRepository) setBranch(branchOptional string) error {
// By default it is "master", unless the branchOptional came with a value
if branchOptional == "" {
- body, err := getter.HttpGetter(&http.Client{}, g.defaultBranchAPI())
+ body, err := getter.HttpGetter(&http.Client{}, g.defaultBranchAPI(), nil)
if err != nil {
return err
}
@@ -117,7 +117,7 @@ func (g *GitHubRepository) defaultBranchAPI() string {
}
func (g *GitHubRepository) setTree() error {
- body, err := getter.HttpGetter(&http.Client{}, g.treeAPI())
+ body, err := getter.HttpGetter(&http.Client{}, g.treeAPI(), nil)
if err != nil {
return err
}
diff --git a/resourcehandler/resourceshandler.go b/resourcehandler/resourceshandler.go
new file mode 100644
index 00000000..c2ddf15f
--- /dev/null
+++ b/resourcehandler/resourceshandler.go
@@ -0,0 +1,13 @@
+package resourcehandler
+
+import (
+ "github.com/armosec/armoapi-go/armotypes"
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/opa-utils/reporthandling"
+ "k8s.io/apimachinery/pkg/version"
+)
+
+type IResourceHandler interface {
+ GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error)
+ GetClusterAPIServerInfo() *version.Info
+}
diff --git a/policyhandler/urlloader.go b/resourcehandler/urlloader.go
similarity index 98%
rename from policyhandler/urlloader.go
rename to resourcehandler/urlloader.go
index faa8f634..96fbdc0d 100644
--- a/policyhandler/urlloader.go
+++ b/resourcehandler/urlloader.go
@@ -1,4 +1,4 @@
-package policyhandler
+package resourcehandler
import (
"bytes"
diff --git a/resultshandling/printer/jsonprinter.go b/resultshandling/printer/jsonprinter.go
new file mode 100644
index 00000000..01acad7e
--- /dev/null
+++ b/resultshandling/printer/jsonprinter.go
@@ -0,0 +1,42 @@
+package printer
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/armosec/kubescape/cautils"
+)
+
+type JsonPrinter struct {
+ writer *os.File
+}
+
+func NewJsonPrinter() *JsonPrinter {
+ return &JsonPrinter{}
+}
+
+func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) {
+ jsonPrinter.writer = getWriter(outputFile)
+}
+
+func (jsonPrinter *JsonPrinter) Score(score float32) {
+ fmt.Printf("\nFinal score: %d", int(score*100))
+}
+
+func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
+ var postureReportStr []byte
+ var err error
+
+ if len(opaSessionObj.PostureReport.FrameworkReports) == 1 {
+ postureReportStr, err = json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
+ } else {
+ postureReportStr, err = json.Marshal(opaSessionObj.PostureReport.FrameworkReports)
+ }
+
+ if err != nil {
+ fmt.Println("Failed to convert posture report object!")
+ os.Exit(1)
+ }
+ jsonPrinter.writer.Write(postureReportStr)
+}
diff --git a/resultshandling/printer/junit.go b/resultshandling/printer/junit.go
index 370a5d0a..ac03753d 100644
--- a/resultshandling/printer/junit.go
+++ b/resultshandling/printer/junit.go
@@ -3,10 +3,42 @@ package printer
import (
"encoding/xml"
"fmt"
+ "os"
+ "github.com/armosec/kubescape/cautils"
"github.com/armosec/opa-utils/reporthandling"
)
+type JunitPrinter struct {
+ writer *os.File
+}
+
+func NewJunitPrinter() *JunitPrinter {
+ return &JunitPrinter{}
+}
+
+func (junitPrinter *JunitPrinter) SetWriter(outputFile string) {
+ junitPrinter.writer = getWriter(outputFile)
+}
+
+func (junitPrinter *JunitPrinter) Score(score float32) {
+ fmt.Printf("\nFinal score: %d", int(score*100))
+}
+
+func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
+ junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
+ if err != nil {
+ fmt.Println("Failed to convert posture report object!")
+ os.Exit(1)
+ }
+ postureReportStr, err := xml.Marshal(junitResult)
+ if err != nil {
+ fmt.Println("Failed to convert posture report object!")
+ os.Exit(1)
+ }
+ junitPrinter.writer.Write(postureReportStr)
+}
+
type JUnitTestSuites struct {
XMLName xml.Name `xml:"testsuites"`
Suites []JUnitTestSuite `xml:"testsuite"`
@@ -74,9 +106,9 @@ func convertPostureReportToJunitResult(postureResult *reporthandling.PostureRepo
testCase.Time = "0"
if 0 < len(controlReports.RuleReports[0].RuleResponses) {
- testCase.Resources = framework.GetNumberOfResources()
- testCase.Excluded = framework.GetNumberOfWarningResources()
- testCase.Failed = framework.GetNumberOfFailedResources()
+ testCase.Resources = controlReports.GetNumberOfResources()
+ testCase.Excluded = controlReports.GetNumberOfWarningResources()
+ testCase.Failed = controlReports.GetNumberOfFailedResources()
failure := JUnitFailure{}
failure.Message = fmt.Sprintf("%d resources failed", testCase.Failed)
for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses {
diff --git a/resultshandling/printer/prettyprinter.go b/resultshandling/printer/prettyprinter.go
new file mode 100644
index 00000000..e387eacf
--- /dev/null
+++ b/resultshandling/printer/prettyprinter.go
@@ -0,0 +1,239 @@
+package printer
+
+import (
+ "fmt"
+ "os"
+ "sort"
+
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/opa-utils/reporthandling"
+ "github.com/enescakir/emoji"
+ "github.com/olekukonko/tablewriter"
+)
+
+type PrettyPrinter struct {
+ writer *os.File
+ summary Summary
+ sortedControlNames []string
+ frameworkSummary ControlSummary
+}
+
+func NewPrettyPrinter() *PrettyPrinter {
+ return &PrettyPrinter{
+ summary: NewSummary(),
+ }
+}
+
+// Initializes empty printer for new table
+func (printer *PrettyPrinter) init() *PrettyPrinter {
+ printer.frameworkSummary = ControlSummary{}
+ printer.summary = Summary{}
+ printer.sortedControlNames = []string{}
+ return printer
+}
+
+func (printer *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
+ // score := calculatePostureScore(opaSessionObj.PostureReport)
+ for _, report := range opaSessionObj.PostureReport.FrameworkReports {
+ // Print summary table together for control scan
+ if report.Name != "" {
+ printer = printer.init()
+ }
+ printer.summarySetup(report)
+ printer.printResults()
+ printer.printSummaryTable(report.Name)
+ }
+
+ // return score
+}
+
+func (printer *PrettyPrinter) SetWriter(outputFile string) {
+ printer.writer = getWriter(outputFile)
+}
+
+func (printer *PrettyPrinter) Score(score float32) {
+}
+
+func (printer *PrettyPrinter) summarySetup(fr reporthandling.FrameworkReport) {
+ printer.frameworkSummary = ControlSummary{
+ TotalResources: fr.GetNumberOfResources(),
+ TotalFailed: fr.GetNumberOfFailedResources(),
+ TotalWarning: fr.GetNumberOfWarningResources(),
+ }
+ for _, cr := range fr.ControlReports {
+ if len(cr.RuleReports) == 0 {
+ continue
+ }
+ workloadsSummary := listResultSummary(cr.RuleReports)
+
+ printer.summary[cr.Name] = ControlSummary{
+ TotalResources: cr.GetNumberOfResources(),
+ TotalFailed: cr.GetNumberOfFailedResources(),
+ TotalWarning: cr.GetNumberOfWarningResources(),
+ FailedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryFailed),
+ ExcludedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryExclude),
+ Description: cr.Description,
+ Remediation: cr.Remediation,
+ ListInputKinds: cr.ListControlsInputKinds(),
+ }
+ }
+ printer.sortedControlNames = printer.getSortedControlsNames()
+}
+func (printer *PrettyPrinter) printResults() {
+ for i := 0; i < len(printer.sortedControlNames); i++ {
+ controlSummary := printer.summary[printer.sortedControlNames[i]]
+ printer.printTitle(printer.sortedControlNames[i], &controlSummary)
+ printer.printResources(&controlSummary)
+ if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 {
+ printer.printSummary(printer.sortedControlNames[i], &controlSummary)
+ }
+
+ }
+}
+
+func (printer *PrettyPrinter) printSummary(controlName string, controlSummary *ControlSummary) {
+ cautils.SimpleDisplay(printer.writer, "Summary - ")
+ cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarning)
+ cautils.WarningDisplay(printer.writer, "Excluded:%v ", controlSummary.TotalWarning)
+ cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
+ cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
+ if controlSummary.TotalFailed > 0 {
+ cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation)
+ }
+ cautils.DescriptionDisplay(printer.writer, "\n")
+
+}
+
+func (printer *PrettyPrinter) printTitle(controlName string, controlSummary *ControlSummary) {
+ cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName)
+ if controlSummary.TotalResources == 0 {
+ cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace)
+ } else if controlSummary.TotalFailed != 0 {
+ cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
+ } else if controlSummary.TotalWarning != 0 {
+ cautils.WarningDisplay(printer.writer, "excluded %v\n", emoji.NeutralFace)
+ } else {
+ cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
+ }
+
+ cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
+
+}
+func (printer *PrettyPrinter) printResources(controlSummary *ControlSummary) {
+
+ if len(controlSummary.FailedWorkloads) > 0 {
+ cautils.FailureDisplay(printer.writer, "Failed:\n")
+ printer.printGroupedResources(controlSummary.FailedWorkloads)
+ }
+ if len(controlSummary.ExcludedWorkloads) > 0 {
+ cautils.WarningDisplay(printer.writer, "Excluded:\n")
+ printer.printGroupedResources(controlSummary.ExcludedWorkloads)
+ }
+
+}
+
+func (printer *PrettyPrinter) printGroupedResources(workloads map[string][]WorkloadSummary) {
+
+ indent := INDENT
+
+ for ns, rsc := range workloads {
+ preIndent := indent
+ if ns != "" {
+ cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns)
+ }
+ preIndent2 := indent
+ for r := range rsc {
+ indent += indent
+ cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
+ indent = preIndent2
+ }
+ indent = preIndent
+ }
+
+}
+
+func generateRow(control string, cs ControlSummary) []string {
+ row := []string{control}
+ row = append(row, cs.ToSlice()...)
+ if cs.TotalResources != 0 {
+ row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
+ } else {
+ row = append(row, EmptyPercentage)
+ }
+ return row
+}
+
+func generateHeader() []string {
+ return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% success"}
+}
+
+func percentage(big, small int) int {
+ if big == 0 {
+ if small == 0 {
+ return 100
+ }
+ return 0
+ }
+ return int(float64(float64(big-small)/float64(big)) * 100)
+}
+func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string {
+ // Control name | # failed resources | all resources | % success
+ row := []string{}
+ row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers"))
+ row = append(row, fmt.Sprintf("%d", sumFailed))
+ row = append(row, fmt.Sprintf("%d", sumWarning))
+ row = append(row, fmt.Sprintf("%d", sumTotal))
+ if sumTotal != 0 {
+ row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
+ } else {
+ row = append(row, EmptyPercentage)
+ }
+ return row
+}
+func (printer *PrettyPrinter) printSummaryTable(framework string) {
+ // For control scan framework will be nil
+ printer.printFramework(framework)
+
+ summaryTable := tablewriter.NewWriter(printer.writer)
+ summaryTable.SetAutoWrapText(false)
+ summaryTable.SetHeader(generateHeader())
+ summaryTable.SetHeaderLine(true)
+ alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}
+ summaryTable.SetColumnAlignment(alignments)
+
+ for i := 0; i < len(printer.sortedControlNames); i++ {
+ controlSummary := printer.summary[printer.sortedControlNames[i]]
+ summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
+ }
+ summaryTable.SetFooter(generateFooter(len(printer.summary), printer.frameworkSummary.TotalFailed, printer.frameworkSummary.TotalWarning, printer.frameworkSummary.TotalResources))
+ summaryTable.Render()
+}
+
+func (printer *PrettyPrinter) printFramework(framework string) {
+ if framework != "" {
+ cautils.InfoTextDisplay(printer.writer, fmt.Sprintf("%s FRAMEWORK\n", framework))
+ }
+}
+
+func (printer *PrettyPrinter) getSortedControlsNames() []string {
+ controlNames := make([]string, 0, len(printer.summary))
+ for k := range printer.summary {
+ controlNames = append(controlNames, k)
+ }
+ sort.Strings(controlNames)
+ return controlNames
+}
+
+func getWriter(outputFile string) *os.File {
+ os.Remove(outputFile)
+ if outputFile != "" {
+ f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ fmt.Println("Error opening file")
+ return os.Stdout
+ }
+ return f
+ }
+ return os.Stdout
+
+}
diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go
index 50a624c4..eab0c5d9 100644
--- a/resultshandling/printer/printresults.go
+++ b/resultshandling/printer/printresults.go
@@ -1,17 +1,7 @@
package printer
import (
- "encoding/json"
- "encoding/xml"
- "fmt"
- "os"
- "sort"
-
"github.com/armosec/kubescape/cautils"
- "github.com/armosec/opa-utils/reporthandling"
-
- "github.com/enescakir/emoji"
- "github.com/olekukonko/tablewriter"
)
var INDENT = " "
@@ -19,253 +9,27 @@ var INDENT = " "
const EmptyPercentage = "NaN"
const (
- PrettyPrinter string = "pretty-printer"
- JsonPrinter string = "json"
- JunitResultPrinter string = "junit"
+ PrettyFormat string = "pretty-printer"
+ JsonFormat string = "json"
+ JunitResultFormat string = "junit"
+ PrometheusFormat string = "prometheus"
)
-type Printer struct {
- writer *os.File
- summary Summary
- sortedControlNames []string
- printerType string
- frameworkSummary ControlSummary
+type IPrinter interface {
+ ActionPrint(opaSessionObj *cautils.OPASessionObj)
+ SetWriter(outputFile string)
+ Score(score float32)
}
-func NewPrinter(printerType, outputFile string) *Printer {
- return &Printer{
- summary: NewSummary(),
- writer: getWriter(outputFile),
- printerType: printerType,
+func GetPrinter(printFormat string) IPrinter {
+ switch printFormat {
+ case JsonFormat:
+ return NewJsonPrinter()
+ case JunitResultFormat:
+ return NewJunitPrinter()
+ case PrometheusFormat:
+ return NewPrometheusPrinter()
+ default:
+ return NewPrettyPrinter()
}
}
-
-func calculatePostureScore(postureReport *reporthandling.PostureReport) float32 {
- totalResources := 0
- totalFailed := 0
- for _, frameworkReport := range postureReport.FrameworkReports {
- totalFailed += frameworkReport.GetNumberOfFailedResources()
- totalResources += frameworkReport.GetNumberOfResources()
- }
- if totalResources == 0 {
- return float32(0)
- }
- return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
-}
-
-func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 {
- score := calculatePostureScore(opaSessionObj.PostureReport)
-
- if printer.printerType == PrettyPrinter {
- printer.SummarySetup(opaSessionObj.PostureReport)
- printer.PrintResults()
- printer.PrintSummaryTable()
- } else if printer.printerType == JsonPrinter {
- postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
- if err != nil {
- fmt.Println("Failed to convert posture report object!")
- os.Exit(1)
- }
- printer.writer.Write(postureReportStr)
- fmt.Printf("\nFinal score: %d\n", int(score*100))
- } else if printer.printerType == JunitResultPrinter {
- junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport)
- if err != nil {
- fmt.Println("Failed to convert posture report object!")
- os.Exit(1)
- }
- postureReportStr, err := xml.Marshal(junitResult)
- if err != nil {
- fmt.Println("Failed to convert posture report object!")
- os.Exit(1)
- }
- printer.writer.Write(postureReportStr)
- fmt.Printf("\nFinal score: %d\n", int(score*100))
- } else if !cautils.IsSilent() {
- fmt.Println("unknown output printer")
- os.Exit(1)
- }
-
- return score
-}
-
-func (printer *Printer) SummarySetup(postureReport *reporthandling.PostureReport) {
- for _, fr := range postureReport.FrameworkReports {
- printer.frameworkSummary = ControlSummary{
- TotalResources: fr.GetNumberOfResources(),
- TotalFailed: fr.GetNumberOfFailedResources(),
- TotalWarnign: fr.GetNumberOfWarningResources(),
- }
- for _, cr := range fr.ControlReports {
- if len(cr.RuleReports) == 0 {
- continue
- }
- workloadsSummary := listResultSummary(cr.RuleReports)
-
- printer.summary[cr.Name] = ControlSummary{
- TotalResources: cr.GetNumberOfResources(),
- TotalFailed: cr.GetNumberOfFailedResources(),
- TotalWarnign: cr.GetNumberOfWarningResources(),
- FailedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryFailed),
- ExcludedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryExclude),
- Description: cr.Description,
- Remediation: cr.Remediation,
- ListInputKinds: cr.ListControlsInputKinds(),
- }
- }
- }
- printer.sortedControlNames = printer.getSortedControlsNames()
-}
-func (printer *Printer) PrintResults() {
- for i := 0; i < len(printer.sortedControlNames); i++ {
- controlSummary := printer.summary[printer.sortedControlNames[i]]
- printer.printTitle(printer.sortedControlNames[i], &controlSummary)
- printer.printResources(&controlSummary)
- if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 {
- printer.printSummary(printer.sortedControlNames[i], &controlSummary)
- }
-
- }
-}
-
-func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) {
- cautils.SimpleDisplay(printer.writer, "Summary - ")
- cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarnign)
- cautils.WarningDisplay(printer.writer, "Excluded:%v ", controlSummary.TotalWarnign)
- cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed)
- cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources)
- if controlSummary.TotalFailed > 0 {
- cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation)
- }
- cautils.DescriptionDisplay(printer.writer, "\n")
-
-}
-
-func (printer *Printer) printTitle(controlName string, controlSummary *ControlSummary) {
- cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName)
- if controlSummary.TotalResources == 0 {
- cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace)
- } else if controlSummary.TotalFailed != 0 {
- cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace)
- } else if controlSummary.TotalWarnign != 0 {
- cautils.WarningDisplay(printer.writer, "excluded %v\n", emoji.NeutralFace)
- } else {
- cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp)
- }
-
- cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description)
-
-}
-func (printer *Printer) printResources(controlSummary *ControlSummary) {
-
- if len(controlSummary.FailedWorkloads) > 0 {
- cautils.FailureDisplay(printer.writer, "Failed:\n")
- printer.printGroupedResources(controlSummary.FailedWorkloads)
- }
- if len(controlSummary.ExcludedWorkloads) > 0 {
- cautils.WarningDisplay(printer.writer, "Excluded:\n")
- printer.printGroupedResources(controlSummary.ExcludedWorkloads)
- }
-
-}
-
-func (printer *Printer) printGroupedResources(workloads map[string][]WorkloadSummary) {
-
- indent := INDENT
-
- for ns, rsc := range workloads {
- preIndent := indent
- if ns != "" {
- cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns)
- }
- preIndent2 := indent
- for r := range rsc {
- indent += indent
- cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name))
- indent = preIndent2
- }
- indent = preIndent
- }
-
-}
-
-func (printer *Printer) PrintUrl(url string) {
- cautils.InfoTextDisplay(printer.writer, url)
-}
-
-func generateRow(control string, cs ControlSummary) []string {
- row := []string{control}
- row = append(row, cs.ToSlice()...)
- if cs.TotalResources != 0 {
- row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%"))
- } else {
- row = append(row, EmptyPercentage)
- }
- return row
-}
-
-func generateHeader() []string {
- return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% success"}
-}
-
-func percentage(big, small int) int {
- if big == 0 {
- if small == 0 {
- return 100
- }
- return 0
- }
- return int(float64(float64(big-small)/float64(big)) * 100)
-}
-func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string {
- // Control name | # failed resources | all resources | % success
- row := []string{}
- row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers"))
- row = append(row, fmt.Sprintf("%d", sumFailed))
- row = append(row, fmt.Sprintf("%d", sumWarning))
- row = append(row, fmt.Sprintf("%d", sumTotal))
- if sumTotal != 0 {
- row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%"))
- } else {
- row = append(row, EmptyPercentage)
- }
- return row
-}
-func (printer *Printer) PrintSummaryTable() {
- summaryTable := tablewriter.NewWriter(printer.writer)
- summaryTable.SetAutoWrapText(false)
- summaryTable.SetHeader(generateHeader())
- summaryTable.SetHeaderLine(true)
- alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER}
- summaryTable.SetColumnAlignment(alignments)
-
- for i := 0; i < len(printer.sortedControlNames); i++ {
- controlSummary := printer.summary[printer.sortedControlNames[i]]
- summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary))
- }
- summaryTable.SetFooter(generateFooter(len(printer.summary), printer.frameworkSummary.TotalFailed, printer.frameworkSummary.TotalWarnign, printer.frameworkSummary.TotalResources))
- summaryTable.Render()
-}
-
-func (printer *Printer) getSortedControlsNames() []string {
- controlNames := make([]string, 0, len(printer.summary))
- for k := range printer.summary {
- controlNames = append(controlNames, k)
- }
- sort.Strings(controlNames)
- return controlNames
-}
-
-func getWriter(outputFile string) *os.File {
- os.Remove(outputFile)
- if outputFile != "" {
- f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if err != nil {
- fmt.Println("Error opening file")
- return os.Stdout
- }
- return f
- }
- return os.Stdout
-
-}
diff --git a/resultshandling/printer/prometheusprinter.go b/resultshandling/printer/prometheusprinter.go
new file mode 100644
index 00000000..0c5bbaaf
--- /dev/null
+++ b/resultshandling/printer/prometheusprinter.go
@@ -0,0 +1,100 @@
+package printer
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "github.com/armosec/kubescape/cautils"
+ "github.com/armosec/opa-utils/reporthandling"
+)
+
+type PrometheusPrinter struct {
+ writer *os.File
+}
+
+func NewPrometheusPrinter() *PrometheusPrinter {
+ return &PrometheusPrinter{}
+}
+
+func (prometheusPrinter *PrometheusPrinter) SetWriter(outputFile string) {
+ prometheusPrinter.writer = getWriter(outputFile)
+}
+
+func (prometheusPrinter *PrometheusPrinter) Score(score float32) {
+ fmt.Printf("\n# Overall score out of 100\nkubescape_score %f\n", score*100)
+}
+
+func (printer *PrometheusPrinter) printDetails(details []reporthandling.RuleResponse, frameworkName string, controlName string) error {
+ objs := make(map[string]map[string]map[string]int)
+ for _, ruleResponses := range details {
+ for _, k8sObj := range ruleResponses.AlertObject.K8SApiObjects {
+ kind, ok := k8sObj[`kind`].(string)
+ if (!ok) {
+ return errors.New("Found object with non string kind")
+ }
+ apiVersion,ok := k8sObj[`apiVersion`].(string)
+ if (!ok) {
+ return errors.New("Found object with non string apiVersion")
+ }
+ gvk := fmt.Sprintf("%s/%s",apiVersion,kind)
+ metadata,ok := k8sObj[`metadata`].(map[string]interface{})
+ if (!ok) {
+ return errors.New("Found object with non convertable metadata")
+ }
+ name,ok := metadata[`name`].(string)
+ if (!ok) {
+ return errors.New("Found metadata with non string name")
+ }
+ namespace,ok := metadata[`namespace`].(string)
+ if (!ok) {
+ namespace = ""
+ }
+ if (objs[gvk] == nil) {
+ objs[gvk] = make(map[string]map[string]int)
+ }
+ if (objs[gvk][namespace] == nil) {
+ objs[gvk][namespace] = make(map[string]int)
+ }
+ objs[gvk][namespace][name]++
+ }
+ }
+ for gvk, namespaces := range objs {
+ for namespace, names := range namespaces {
+ for name, value := range names {
+ fmt.Fprintf(printer.writer, "# Failed object from %s control %s\n", frameworkName, controlName)
+ if namespace != "" {
+ fmt.Fprintf(printer.writer, "kubescape_object_failed_count{framework=\"%s\",control=\"%s\",namespace=\"%s\",name=\"%s\",groupVersionKind=\"%s\"} %d\n", frameworkName, controlName, namespace, name, gvk, value)
+ } else {
+ fmt.Fprintf(printer.writer, "kubescape_object_failed_count{framework=\"%s\",control=\"%s\",name=\"%s\",groupVersionKind=\"%s\"} %d\n", frameworkName, controlName, name, gvk, value)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (printer *PrometheusPrinter) printReports(frameworks []reporthandling.FrameworkReport) error {
+ for _, framework := range frameworks {
+ for _, controlReports := range framework.ControlReports {
+ if len(controlReports.RuleReports[0].RuleResponses) > 0 {
+ fmt.Fprintf(printer.writer, "# Number of resources found as part of %s control %s\nkubescape_resources_found_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfResources())
+ fmt.Fprintf(printer.writer, "# Number of resources excluded as part of %s control %s\nkubescape_resources_excluded_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfWarningResources())
+ fmt.Fprintf(printer.writer, "# Number of resources failed as part of %s control %s\nkubescape_resources_failed_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfFailedResources())
+ err := printer.printDetails(controlReports.RuleReports[0].RuleResponses, framework.Name, controlReports.Name)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (printer *PrometheusPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
+ err := printer.printReports(opaSessionObj.PostureReport.FrameworkReports)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
diff --git a/resultshandling/printer/silentprinter.go b/resultshandling/printer/silentprinter.go
new file mode 100644
index 00000000..65737b5c
--- /dev/null
+++ b/resultshandling/printer/silentprinter.go
@@ -0,0 +1,11 @@
+package printer
+
+import (
+ "github.com/armosec/kubescape/cautils"
+)
+
+type SilentPrinter struct {
+}
+
+func (silentPrinter *SilentPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
+}
diff --git a/resultshandling/printer/summary.go b/resultshandling/printer/summary.go
index 9df245f6..056f6d23 100644
--- a/resultshandling/printer/summary.go
+++ b/resultshandling/printer/summary.go
@@ -15,7 +15,7 @@ func NewSummary() Summary {
type ControlSummary struct {
TotalResources int
TotalFailed int
- TotalWarnign int
+ TotalWarning int
Description string
Remediation string
ListInputKinds []string
@@ -34,7 +34,7 @@ type WorkloadSummary struct {
func (controlSummary *ControlSummary) ToSlice() []string {
s := []string{}
s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed))
- s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarnign))
+ s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarning))
s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources))
return s
}
diff --git a/resultshandling/reporter/mockreporter.go b/resultshandling/reporter/mockreporter.go
new file mode 100644
index 00000000..3059aeb2
--- /dev/null
+++ b/resultshandling/reporter/mockreporter.go
@@ -0,0 +1,19 @@
+package reporter
+
+import "github.com/armosec/kubescape/cautils"
+
+type ReportMock struct {
+}
+
+func NewReportMock() *ReportMock {
+ return &ReportMock{}
+}
+func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error {
+ return nil
+}
+
+func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) {
+}
+
+func (reportMock *ReportMock) SetClusterName(clusterName string) {
+}
diff --git a/resultshandling/reporter/reporteventreceiver.go b/resultshandling/reporter/reporteventreceiver.go
index 99364328..2bd98392 100644
--- a/resultshandling/reporter/reporteventreceiver.go
+++ b/resultshandling/reporter/reporteventreceiver.go
@@ -1,61 +1,64 @@
package reporter
import (
- "bytes"
"encoding/json"
"fmt"
"net/http"
- "net/url"
"github.com/armosec/kubescape/cautils"
+ "github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/opa-utils/reporthandling"
)
+type IReport interface {
+ ActionSendReport(opaSessionObj *cautils.OPASessionObj) error
+ SetCustomerGUID(customerGUID string)
+ SetClusterName(clusterName string)
+}
+
type ReportEventReceiver struct {
- httpClient http.Client
- host url.URL
+ httpClient *http.Client
+ clusterName string
+ customerGUID string
}
-func NewReportEventReceiver() *ReportEventReceiver {
- hostURL := initEventReceiverURL()
+func NewReportEventReceiver(customerGUID, clusterName string) *ReportEventReceiver {
return &ReportEventReceiver{
- httpClient: http.Client{},
- host: *hostURL,
+ httpClient: &http.Client{},
+ clusterName: clusterName,
+ customerGUID: customerGUID,
}
}
-func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *cautils.OPASessionObj) {
- if cautils.CustomerGUID == "" {
- return
- }
- //Add score
-
+func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error {
// Remove data before reporting
keepFields := []string{"kind", "apiVersion", "metadata"}
keepMetadataFields := []string{"name", "namespace", "labels"}
opaSessionObj.PostureReport.RemoveData(keepFields, keepMetadataFields)
- if err := report.Send(opaSessionObj.PostureReport); err != nil {
- fmt.Println(err)
+ if err := report.send(opaSessionObj.PostureReport); err != nil {
+ return err
}
+ return nil
}
-func (report *ReportEventReceiver) Send(postureReport *reporthandling.PostureReport) error {
+
+func (report *ReportEventReceiver) SetCustomerGUID(customerGUID string) {
+ report.customerGUID = customerGUID
+}
+
+func (report *ReportEventReceiver) SetClusterName(clusterName string) {
+ report.clusterName = clusterName
+}
+
+func (report *ReportEventReceiver) send(postureReport *reporthandling.PostureReport) error {
reqBody, err := json.Marshal(*postureReport)
if err != nil {
return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err)
}
- host := hostToString(&report.host, postureReport.ReportID)
+ host := hostToString(report.initEventReceiverURL(), postureReport.ReportID)
- req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody))
- if err != nil {
- return fmt.Errorf("in 'Send', http.NewRequest failed, host: %s, reason: %v", host, err)
- }
- res, err := report.httpClient.Do(req)
- if err != nil {
- return fmt.Errorf("httpClient.Do failed: %v", err)
- }
- msg, err := httpRespToString(res)
+ msg, err := getter.HttpPost(report.httpClient, host, nil, reqBody)
if err != nil {
return fmt.Errorf("%s, %v:%s", host, err, msg)
}
diff --git a/resultshandling/reporter/reporteventreceiverutils.go b/resultshandling/reporter/reporteventreceiverutils.go
index 96fa75be..0f90dbae 100644
--- a/resultshandling/reporter/reporteventreceiverutils.go
+++ b/resultshandling/reporter/reporteventreceiverutils.go
@@ -1,47 +1,21 @@
package reporter
import (
- "fmt"
- "io"
- "net/http"
"net/url"
- "strings"
- "github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/gofrs/uuid"
)
-// HTTPRespToString parses the body as string and checks the HTTP status code, it closes the body reader at the end
-func httpRespToString(resp *http.Response) (string, error) {
- if resp == nil || resp.Body == nil {
- return "", nil
- }
- strBuilder := strings.Builder{}
- defer resp.Body.Close()
- if resp.ContentLength > 0 {
- strBuilder.Grow(int(resp.ContentLength))
- }
- _, err := io.Copy(&strBuilder, resp.Body)
- if err != nil {
- return strBuilder.String(), err
- }
- if resp.StatusCode < 200 || resp.StatusCode >= 300 {
- err = fmt.Errorf("response status: %d. Content: %s", resp.StatusCode, strBuilder.String())
- }
-
- return strBuilder.String(), err
-}
-
-func initEventReceiverURL() *url.URL {
+func (report *ReportEventReceiver) initEventReceiverURL() *url.URL {
urlObj := url.URL{}
urlObj.Scheme = "https"
urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL()
urlObj.Path = "/k8s/postureReport"
q := urlObj.Query()
- q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String())
- q.Add("clusterName", cautils.ClusterName)
+ q.Add("customerGUID", uuid.FromStringOrNil(report.customerGUID).String())
+ q.Add("clusterName", report.clusterName)
urlObj.RawQuery = q.Encode()
diff --git a/resultshandling/results.go b/resultshandling/results.go
index 2db5e8f0..01ecea34 100644
--- a/resultshandling/results.go
+++ b/resultshandling/results.go
@@ -1,18 +1,21 @@
package resultshandling
import (
+ "fmt"
+
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/resultshandling/printer"
"github.com/armosec/kubescape/resultshandling/reporter"
+ "github.com/armosec/opa-utils/reporthandling"
)
type ResultsHandler struct {
opaSessionObj *chan *cautils.OPASessionObj
- reporterObj *reporter.ReportEventReceiver
- printerObj *printer.Printer
+ reporterObj reporter.IReport
+ printerObj printer.IPrinter
}
-func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *reporter.ReportEventReceiver, printerObj *printer.Printer) *ResultsHandler {
+func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj reporter.IReport, printerObj printer.IPrinter) *ResultsHandler {
return &ResultsHandler{
opaSessionObj: opaSessionObj,
reporterObj: reporterObj,
@@ -20,16 +23,38 @@ func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *
}
}
-func (resultsHandler *ResultsHandler) HandleResults(scanInfo cautils.ScanInfo) float32 {
+func (resultsHandler *ResultsHandler) HandleResults(scanInfo *cautils.ScanInfo) float32 {
opaSessionObj := <-*resultsHandler.opaSessionObj
- score := resultsHandler.printerObj.ActionPrint(opaSessionObj)
+ resultsHandler.printerObj.ActionPrint(opaSessionObj)
- // Don't send report for control scan
- if scanInfo.FrameworkScan { // TODO - use interface for ActionSendReportListenner
- resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj)
+ if err := resultsHandler.reporterObj.ActionSendReport(opaSessionObj); err != nil {
+ fmt.Println(err)
}
+ // TODO - get score from table
+ score := CalculatePostureScore(opaSessionObj.PostureReport)
+ resultsHandler.printerObj.Score(score)
+
return score
}
+
+// CalculatePostureScore calculate final score
+func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 {
+ lowestScore := float32(100)
+ for _, frameworkReport := range postureReport.FrameworkReports {
+ totalFailed := frameworkReport.GetNumberOfFailedResources()
+ totalResources := frameworkReport.GetNumberOfResources()
+
+ frameworkScore := float32(0)
+ if float32(totalResources) > 0 {
+ frameworkScore = (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
+ }
+ if lowestScore > frameworkScore {
+ lowestScore = frameworkScore
+ }
+ }
+
+ return lowestScore
+}
diff --git a/smoke_testing/init.py b/smoke_testing/init.py
new file mode 100644
index 00000000..573212c3
--- /dev/null
+++ b/smoke_testing/init.py
@@ -0,0 +1,47 @@
+"""
+Kubescape smoke testing
+
+Execute all tests:
+python init.py path/the/bin/kubescape
+
+Execute single test:
+python test_.py path/the/bin/kubescape
+
+Add a new test:
+1. Create a python file with test_ prefix
+2. Implement a function named run()
+
+"""
+
+import sys
+import smoke_utils
+import glob
+import os
+
+# get all python files in dir that begin with test_
+tests_pkg = list(map(lambda x: os.path.splitext(os.path.basename(x))[0], glob.glob(os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_*.py'))))
+
+
+def run(**kwargs):
+ for i in tests_pkg:
+ m = __import__(i)
+ m.run(**kwargs)
+
+
+if __name__ == "__main__":
+ # the first argument should be the kubescape binary path
+ run(kubescape_exec=smoke_utils.get_exec_from_args(sys.argv))
+
+'''
+Supported tests:
+1. Commands
+2. Version number
+3. E2E yaml scanning
+
+TODO:
+1. Test formats + output
+2. Test --fail-threshold
+3. Test known supported FW
+4. Test FW list
+5. Test Controls list
+'''
\ No newline at end of file
diff --git a/smoke_testing/smoke_utils.py b/smoke_testing/smoke_utils.py
new file mode 100644
index 00000000..3bff46d9
--- /dev/null
+++ b/smoke_testing/smoke_utils.py
@@ -0,0 +1,19 @@
+import subprocess
+
+
+def get_exec_from_args(args: list):
+ return args[1]
+
+
+def run_command(command):
+ try:
+ return f"{subprocess.check_output(command, stdin=subprocess.PIPE, stderr=subprocess.STDOUT)}"
+ except Exception as e:
+ return f"{e}"
+
+
+def assertion(msg):
+ errors = ["Error: invalid parameter", "exit status 1"]
+ for e in errors:
+ assert e not in msg, msg
+
diff --git a/smoke_testing/test_command.py b/smoke_testing/test_command.py
new file mode 100644
index 00000000..0bb00ff8
--- /dev/null
+++ b/smoke_testing/test_command.py
@@ -0,0 +1,31 @@
+import smoke_utils
+import sys
+
+
+def test_command(command: list):
+ print(f"Testing \"{' '.join(command[1:])}\" command")
+
+ msg = smoke_utils.run_command(command)
+ assert "unknown command" not in msg, f"{command[1:]} is missing: {msg}"
+ assert "invalid parameter" not in msg, f"{command[1:]} is invalid: {msg}"
+
+ print(f"Done testing \"{' '.join(command[1:])}\" command")
+
+
+def run(kubescape_exec:str):
+ print("Testing supported commands")
+
+ test_command(command=[kubescape_exec, "version"])
+ test_command(command=[kubescape_exec, "download"])
+ test_command(command=[kubescape_exec, "config"])
+ test_command(command=[kubescape_exec, "help"])
+ test_command(command=[kubescape_exec, "scan", "framework"])
+ test_command(command=[kubescape_exec, "scan", "control"])
+ test_command(command=[kubescape_exec, "submit", "results"])
+ test_command(command=[kubescape_exec, "submit", "rbac"])
+
+ print("Done testing commands")
+
+
+if __name__ == "__main__":
+ run(kubescape_exec=smoke_utils.get_exec_from_args(sys.argv))
diff --git a/smoke_testing/test_scan.py b/smoke_testing/test_scan.py
new file mode 100644
index 00000000..0e18fb8a
--- /dev/null
+++ b/smoke_testing/test_scan.py
@@ -0,0 +1,76 @@
+import os
+import smoke_utils
+import sys
+
+
+all_files = os.path.join("..", "*.yaml")
+# all_files = os.path.join("..", "examples", "online-boutique", "*.yaml")
+single_file = os.path.join("..", "examples", "online-boutique", "frontend.yaml")
+
+
+def scan_all(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", all_files])
+
+
+def scan_control_name(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", "control", 'Allowed hostPath', all_files])
+
+
+def scan_control_id(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", "control", 'C-0006', all_files])
+
+
+def scan_controls(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", "control", 'Allowed hostPath,Allow privilege escalation', all_files])
+
+
+def scan_framework(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", "framework", "nsa", all_files])
+
+
+def scan_frameworks(kubescape_exec: str):
+ return smoke_utils.run_command(command=[kubescape_exec, "scan", "framework", "nsa,mitre,armobest", all_files])
+
+
+def scan_from_stdin(kubescape_exec: str):
+ return smoke_utils.run_command(command=["cat", single_file, "|", kubescape_exec, "scan", "framework", "nsa", "-"])
+
+
+def run(kubescape_exec: str):
+ print("Testing E2E on yaml files")
+
+ # TODO - fix support
+ # print("Testing scan all yaml files")
+ # msg = scan_all(kubescape_exec=kubescape_exec)
+ # smoke_utils.assertion(msg)
+
+ print("Testing scan control name")
+ msg = scan_control_name(kubescape_exec=kubescape_exec)
+ smoke_utils.assertion(msg)
+
+ print("Testing scan control id")
+ msg = scan_control_id(kubescape_exec=kubescape_exec)
+ smoke_utils.assertion(msg)
+
+ print("Testing scan controls")
+ msg = scan_controls(kubescape_exec=kubescape_exec)
+ smoke_utils.assertion(msg)
+
+ print("Testing scan framework")
+ msg = scan_framework(kubescape_exec=kubescape_exec)
+ smoke_utils.assertion(msg)
+
+ print("Testing scan frameworks")
+ msg = scan_frameworks(kubescape_exec=kubescape_exec)
+ smoke_utils.assertion(msg)
+
+ # TODO - fix test
+ # print("Testing scan from stdin")
+ # msg = scan_from_stdin(kubescape_exec=kubescape_exec)
+ # smoke_utils.assertion(msg)
+
+ print("Done E2E yaml files")
+
+
+if __name__ == "__main__":
+ run(kubescape_exec=smoke_utils.get_exec_from_args(sys.argv))
diff --git a/smoke_testing/test_version.py b/smoke_testing/test_version.py
new file mode 100644
index 00000000..bf075027
--- /dev/null
+++ b/smoke_testing/test_version.py
@@ -0,0 +1,17 @@
+import os
+import smoke_utils
+import sys
+
+
+def run(kubescape_exec: str):
+ print("Testing version")
+
+ ver = os.getenv("RELEASE")
+ msg = smoke_utils.run_command(command=[kubescape_exec, "version"])
+ assert ver in msg, f"expected version: {ver}, found: {msg}"
+
+ print("Done testing version")
+
+
+if __name__ == "__main__":
+ run(kubescape_exec=smoke_utils.get_exec_from_args(sys.argv))