Merge pull request #10 from armosec/dev

Dev
This commit is contained in:
Bezbran
2021-11-23 10:53:41 +02:00
committed by GitHub
91 changed files with 2876 additions and 816 deletions
+8 -2
View File
@@ -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
+7 -1
View File
@@ -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
+6 -5
View File
@@ -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
+1
View File
@@ -2,4 +2,5 @@
*kubescape*
*debug*
*vender*
*.pyc*
.idea
+97 -37
View File
@@ -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
```
<img src="docs/summary.png">
</br>
> Kubescape is an open source project, we welcome your feedback and ideas for improvement. Were also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
</br>
### Click [👍](https://github.com/armosec/kubescape/stargazers) if you want us to continue to develop and improve Kubescape 😀
</br>
# 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?
[<img src="docs/discord-banner.png" width="100" alt="logo" align="center">](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. Were 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 <name> --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
+3 -12
View File
@@ -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()
+5
View File
@@ -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
+14 -8
View File
@@ -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, "/", "-")
}
+7
View File
@@ -13,6 +13,7 @@ type OPASessionObj struct {
K8SResources *K8SResources
Exceptions []armotypes.PostureExceptionPolicy
PostureReport *reporthandling.PostureReport
RegoInputData RegoInputData // map[<control name>][<input arguments>]
}
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"`
}
+26
View File
@@ -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
}
+99 -7
View File
@@ -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[<control name>][<input arguments>]
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[<control name>][<input arguments>]
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"`
+37 -4
View File
@@ -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"
+17 -8
View File
@@ -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
}
+5 -1
View File
@@ -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)
}
+30 -1
View File
@@ -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 {
+57 -14
View File
@@ -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 ""
}
-5
View File
@@ -1,5 +0,0 @@
package cautils
const (
ComponentIdentifier = "Posture"
)
+38 -35
View File
@@ -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
}
+121
View File
@@ -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)
}
+17
View File
@@ -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
}
+1 -2
View File
@@ -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 <key>",
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")
+60 -9
View File
@@ -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 <control name>/<control id>",
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 <control names list>/<control ids list>.\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: <control-0>,<control-1>")
}
}
} 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
}
+1 -2
View File
@@ -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 <framework-name>/<control-name> [flags]\nSupported frameworks: %s", clihandler.ValidFrameworks),
Use: fmt.Sprintf("download framework/control <framework-name>/<control-name> [flags]\nSupported frameworks: %s", getter.NativeFrameworks),
Short: "Download framework/control",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
+46 -39
View File
@@ -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 <framework name> [`<glob pattern>`/`-`] [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 <framework names list> [`<glob pattern>`/`-`] [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: <framework-0>,<framework-1>")
}
}
} 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)
+80
View File
@@ -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)
}
+105
View File
@@ -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 <json file>\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
}
+1
View File
@@ -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")
+21 -11
View File
@@ -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")
}
+27
View File
@@ -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 <command>",
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
}
+4 -29
View File
@@ -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)
}
+110 -35
View File
@@ -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
}
+41
View File
@@ -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
}
+2 -4
View File
@@ -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. Were also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops.
+85
View File
@@ -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
</br>
<img src="screenshots/add-cluster.png" alt="add-cluster">
3. Copy the value of `--account` and set it in the `data.customerGUID` field
</br>
<img src="screenshots/account.png" alt="account">
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
```
@@ -0,0 +1,14 @@
# ------------------- Kubescape User/Customer ID ------------------- #
kind: ConfigMap
apiVersion: v1
metadata:
name: kubescape
labels:
app: kubescape
namespace: kubescape
data:
config.json: |
{
"customerGUID": "<ID>",
"clusterName": "<cluster name>"
}
@@ -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
@@ -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
@@ -0,0 +1,7 @@
# ------------------- Kubescape User/Customer ID ------------------- #
kind: Namespace
apiVersion: v1
metadata:
name: kubescape
labels:
app: kubescape
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

+7
View File
@@ -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
+179
View File
@@ -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"
}
]
}
]
```
@@ -0,0 +1,22 @@
[
{
"name": "exclude-allowed-hostPath-control",
"policyType": "postureExceptionPolicy",
"actions": [
"alertOnly"
],
"resources": [
{
"designatorType": "Attributes",
"attributes": {
"kind": ".*"
}
}
],
"posturePolicies": [
{
"controlName": "Allowed hostPath"
}
]
}
]
@@ -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"
}
]
}
]
@@ -28,6 +28,12 @@
"posturePolicies": [
{
"frameworkName": "NSA"
},
{
"frameworkName": "MITRE"
},
{
"frameworkName": "ArmoBest"
}
]
}
@@ -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"
}
]
}
]
+29
View File
@@ -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"
+27
View File
@@ -0,0 +1,27 @@
# kubescape
![Version: 1.0.0](https://img.shields.io/badge/Version-1.0.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v1.0.128](https://img.shields.io/badge/AppVersion-v1.0.128-informational?style=flat-square)
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":"<MyK8sClusterName>","customerGUID":"<MyGUID>,"}}` | 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)
@@ -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 }}
@@ -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"]
@@ -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 }}
@@ -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 }}
@@ -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
+11
View File
@@ -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"]
@@ -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 }}
@@ -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 }}
+74
View File
@@ -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: <MyGUID>
clusterName: <MyK8sClusterName>
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: {}
+4 -3
View File
@@ -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
+9 -7
View File
@@ -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=
-14
View File
@@ -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)
}
}
+22 -32
View File
@@ -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
+2 -1
View File
@@ -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 {
+8 -49
View File
@@ -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, &notification.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, &notification.Designators)
}
+56 -75
View File
@@ -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
}
+21
View File
@@ -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
}
+61
View File
@@ -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
}
@@ -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"][]<k8s workloads>
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 {
@@ -1,4 +1,4 @@
package policyhandler
package resourcehandler
import (
"fmt"
@@ -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)
}
}
@@ -1,4 +1,4 @@
package policyhandler
package resourcehandler
import (
"github.com/armosec/kubescape/cautils"
@@ -1,4 +1,4 @@
package policyhandler
package resourcehandler
import (
"github.com/armosec/k8s-interface/k8sinterface"
@@ -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
}
+13
View File
@@ -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
}
@@ -1,4 +1,4 @@
package policyhandler
package resourcehandler
import (
"bytes"
+42
View File
@@ -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)
}
+35 -3
View File
@@ -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 {
+239
View File
@@ -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
}
+18 -254
View File
@@ -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
}
@@ -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)
}
}
+11
View File
@@ -0,0 +1,11 @@
package printer
import (
"github.com/armosec/kubescape/cautils"
)
type SilentPrinter struct {
}
func (silentPrinter *SilentPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
}
+2 -2
View File
@@ -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
}
+19
View File
@@ -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) {
}
+30 -27
View File
@@ -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)
}
@@ -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()
+33 -8
View File
@@ -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
}
+47
View File
@@ -0,0 +1,47 @@
"""
Kubescape smoke testing
Execute all tests:
python init.py path/the/bin/kubescape
Execute single test:
python test_<name>.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
'''
+19
View File
@@ -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
+31
View File
@@ -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))
+76
View File
@@ -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))
+17
View File
@@ -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))