Compare commits

..

49 Commits

Author SHA1 Message Date
dwertent
0c5eb48fdb Merge remote-tracking branch 'upstream/master' 2021-09-02 13:21:13 +03:00
dwertent
2ae2c81e0b rm download 2021-09-02 13:18:23 +03:00
Benyamin Hirschberg
23090fbb9f Merge pull request #53 from armosec/dev
Supporting failure threshold
2021-09-02 00:00:17 +03:00
Benyamin Hirschberg
fcb6255f75 Merge pull request #52 from BenHirschbergCa/dev
Support failure threshold
2021-09-01 23:59:16 +03:00
Ben Hirschberg
abbc9c3e2e Support failure threshold 2021-09-01 23:41:12 +03:00
David Wertenteil
75f76fcecd Update README.md 2021-09-01 17:23:52 +03:00
dwertent
a00bba5fe4 support downloaded files 2021-09-01 17:18:26 +03:00
Daniel Grunberger
96473188ed Merge pull request #51 from Daniel-GrunbergerCA/master
scan with local file
2021-09-01 17:11:55 +03:00
Daniel-GrunbergerCA
56264df047 update scan with local file 2021-09-01 17:02:41 +03:00
Daniel-GrunbergerCA
c3008981e4 download from be 2021-09-01 16:51:43 +03:00
Daniel Grunberger
572130d797 Merge pull request #49 from Daniel-GrunbergerCA/master
support store download
2021-09-01 16:35:39 +03:00
Daniel-GrunbergerCA
7d8cf37532 support store download 2021-09-01 16:32:33 +03:00
Daniel-GrunbergerCA
ebd9661255 Merge remote-tracking branch 'upstream/dev' 2021-09-01 15:51:35 +03:00
Daniel-GrunbergerCA
05e108b47b add download json option 2021-09-01 15:51:24 +03:00
dwertent
8cb6824f3c ignore k8s config when running local yamls 2021-09-01 13:29:44 +03:00
dwertent
304017fc41 Merge branch 'dev' of ssh://github.com/armosec/kubescape into dev 2021-09-01 11:04:49 +03:00
dwertent
b82673f694 Merge branch 'master' of ssh://github.com/armosec/kubescape 2021-09-01 11:03:27 +03:00
dwertent
ac7e5219f1 interface support of framework 2021-09-01 11:03:20 +03:00
Daniel Grunberger
a4c4f9c6ed Update README.md 2021-08-31 17:22:27 +03:00
dwertent
222b154505 store file localy 2021-08-31 17:08:02 +03:00
dwertent
67c2de74f1 adding download script 2021-08-31 17:05:16 +03:00
dwertent
4a9b36807a remove sudo 2021-08-31 16:43:37 +03:00
dwertent
c6241fab38 remove sudo 2021-08-31 16:42:12 +03:00
dwertent
afbc69c6d2 Merge remote-tracking branch 'upstream/dev' 2021-08-31 16:41:44 +03:00
David Wertenteil
571a15bee8 Update README.md 2021-08-31 16:31:06 +03:00
dwertent
8a00a5c54b Add input table to readme 2021-08-31 16:29:11 +03:00
Daniel Grunberger
8f8aaf70d9 Update README.md 2021-08-31 13:00:17 +03:00
dwertent
2779cb4e25 update module 2021-08-31 11:47:44 +03:00
dwertent
f46ee93539 update modul name 2021-08-31 11:39:27 +03:00
dwertent
3eb087e5c1 Merge remote-tracking branch 'upstream/dev' 2021-08-31 11:38:14 +03:00
dwertent
59c935e723 update output f 2021-08-31 09:00:52 +03:00
dwertent
bae45d277f Merge remote-tracking branch 'upstream/dev' 2021-08-31 08:47:37 +03:00
dwertent
0b6dfa9cd0 Merge remote-tracking branch 'upstream/dev' 2021-08-30 18:47:07 +03:00
dwertent
1ff3a6c92c support output to file 2021-08-30 18:44:42 +03:00
dwertent
f75cee0d78 support stdin input 2021-08-30 14:54:01 +03:00
dwertent
229f16cb01 Merge remote-tracking branch 'upstream/dev' 2021-08-30 13:52:58 +03:00
dwertent
2c6b1a440f update glob function 2021-08-30 08:53:34 +03:00
dwertent
37afc1352f adding helm support to readme 2021-08-29 13:34:40 +03:00
dwertent
9943119033 recursive glob 2021-08-29 13:15:34 +03:00
dwertent
41457ff551 Merge remote-tracking branch 'upstream/dev' 2021-08-29 10:38:42 +03:00
dwertent
82b64b5828 Merge remote-tracking branch 'origin/dev' 2021-08-29 10:35:59 +03:00
dwertent
229e8acc74 Merge remote-tracking branch 'origin/yamlsupport' 2021-08-29 10:35:32 +03:00
David Wertenteil
30324e1c01 Merge branch 'dev' into yamlsupport 2021-08-29 10:19:09 +03:00
dwertent
8ca356eae7 Merge remote-tracking branch 'upstream/master' 2021-08-29 10:09:54 +03:00
dwertent
29f4ae368d support url input, update readme 2021-08-29 10:08:49 +03:00
dwertent
409080f51b update package name o kubescape 2021-08-29 08:17:09 +03:00
dwertent
0b24c46279 Merge remote-tracking branch 'upstream/dev' 2021-08-26 18:30:50 +03:00
dwertent
49596c5ac1 split to function 2021-08-26 18:29:32 +03:00
dwertent
9bf79db8f8 Merge branch 'Daniel-GrunbergerCA-master' into dev 2021-08-26 12:22:34 +03:00
18 changed files with 575 additions and 253 deletions

View File

@@ -28,20 +28,18 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
<img src="docs/summary.png">
### 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 |
## Usage & Examples
### Pre-Deployment Testing
Check your YAML files before you're deploying, simply add them at the end of command line:
```
kubescape scan framework nsa *.yaml
```
### Integration with other tools
Kubescape can produce output fitting for later processing:
* JSON (`-f json`)
* JUnit XML (`-f junit`)
### 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
@@ -49,32 +47,32 @@ Kubescape can produce output fitting for later processing:
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
* Scan local `yaml`/`json` files
* Scan local `yaml`/`json` files before deploying <img src="docs/new-feature.svg">
```
kubescape scan framework nsa examples/online-boutique/*
kubescape scan framework nsa *.yaml
```
* Scan `yaml`/`json` files from url
* Scan `yaml`/`json` files from url <img src="docs/new-feature.svg">
```
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
```
* Output in `json` format
* Output in `json` format <img src="docs/new-feature.svg">
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
```
* Output in `junit xml` format
* Output in `junit xml` format <img src="docs/new-feature.svg">
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
```
### Helm Support
Render the helm template and pass as stdout
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout <img src="docs/new-feature.svg">
```
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
```
for example:
@@ -82,6 +80,23 @@ for example:
helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa -
```
### Offline Support <img src="docs/new-feature.svg">
It is possible to run Kubescape offline!
First download the framework and then scan with `--use-from` flag
* Download and save in file, if file name not specified, will store save to `~/.kubescape/<framework name>.json`
```
kubescape download framework nsa --output nsa.json
```
* Scan using the downloaded framework
```
kubescape scan framework nsa --use-from nsa.json
```
# How to build
Note: development (and the release process) is done with Go `1.16`
@@ -126,7 +141,8 @@ Kubescape is running the following tests according to what is defined by [Kubern
* Linux hardening
* Ingress and Egress blocked
* Container hostPort
* Anonymous requests
* Network policies
## Technology

6
cautils/downloadinfo.go Normal file
View File

@@ -0,0 +1,6 @@
package cautils
type DownloadInfo struct {
Path string
FrameworkName string
}

View File

@@ -0,0 +1,160 @@
package getter
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/armosec/kubescape/cautils/opapolicy"
)
const DefaultLocalStore = ".kubescape"
type IPolicyGetter interface {
GetFramework(name string) (*opapolicy.Framework, error)
}
// =======================================================================================================================
// ======================================== DownloadReleasedPolicy =======================================================
// =======================================================================================================================
// Download released version
type DownloadReleasedPolicy struct {
hostURL string
httpClient *http.Client
}
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
return &DownloadReleasedPolicy{
hostURL: "",
httpClient: &http.Client{},
}
}
func (drp *DownloadReleasedPolicy) GetFramework(name string) (*opapolicy.Framework, error) {
drp.setURL(name)
respStr, err := HttpGetter(drp.httpClient, drp.hostURL)
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return framework, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
return framework, err
}
func (drp *DownloadReleasedPolicy) setURL(frameworkName string) error {
latestReleases := "https://api.github.com/repos/armosec/regolibrary/releases/latest"
resp, err := http.Get(latestReleases)
if err != nil {
return fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestReleases, err.Error())
}
defer resp.Body.Close()
if resp.StatusCode < 200 || 301 < resp.StatusCode {
return fmt.Errorf("failed to download file, status code: %s", resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response body from '%s', reason: %s", latestReleases, err.Error())
}
var data map[string]interface{}
err = json.Unmarshal(body, &data)
if err != nil {
return fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestReleases, err.Error())
}
if assets, ok := data["assets"].([]interface{}); ok {
for i := range assets {
if asset, ok := assets[i].(map[string]interface{}); ok {
if name, ok := asset["name"].(string); ok {
if name == frameworkName {
if url, ok := asset["browser_download_url"].(string); ok {
drp.hostURL = url
}
}
}
}
}
}
return nil
}
// =======================================================================================================================
// ============================================== LoadPolicy =============================================================
// =======================================================================================================================
// Load policies from a local repository
type LoadPolicy struct {
filePath string
}
func NewLoadPolicy(filePath string) *LoadPolicy {
return &LoadPolicy{
filePath: filePath,
}
}
func (lp *LoadPolicy) GetFramework(frameworkName string) (*opapolicy.Framework, error) {
framework := &opapolicy.Framework{}
f, err := ioutil.ReadFile(lp.filePath)
if err != nil {
return nil, err
}
err = json.Unmarshal(f, framework)
if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) {
return nil, fmt.Errorf("framework from file not matching")
}
return framework, err
}
// =======================================================================================================================
// =============================================== ArmoAPI ===============================================================
// =======================================================================================================================
// Armo API for downloading policies
type ArmoAPI struct {
httpClient *http.Client
hostURL string
}
func NewArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{},
hostURL: "https://dashbe.eustage2.cyberarmorsoft.com",
}
}
func (armoAPI *ArmoAPI) GetFramework(name string) (*opapolicy.Framework, error) {
armoAPI.setURL(name)
respStr, err := HttpGetter(armoAPI.httpClient, armoAPI.hostURL)
if err != nil {
return nil, err
}
framework := &opapolicy.Framework{}
if err = JSONDecoder(respStr).Decode(framework); err != nil {
return nil, err
}
SaveFrameworkInFile(framework, GetDefaultPath(name))
return framework, err
}
func (armoAPI *ArmoAPI) setURL(frameworkName string) {
requestURI := "v1/armoFrameworks"
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(frameworkName))
requestURI += "&getRules=true"
armoAPI.hostURL = urlEncoder(fmt.Sprintf("%s/%s", armoAPI.hostURL, requestURI))
}

View File

@@ -0,0 +1,114 @@
package getter
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils/opapolicy"
)
func GetDefaultPath(frameworkName string) string {
defaultfilePath := filepath.Join(DefaultLocalStore, frameworkName+".json")
if homeDir, err := os.UserHomeDir(); err == nil {
defaultfilePath = filepath.Join(homeDir, defaultfilePath)
}
return defaultfilePath
}
func SaveFrameworkInFile(framework *opapolicy.Framework, path string) error {
encodedData, err := json.Marshal(framework)
if err != nil {
return err
}
err = os.WriteFile(path, []byte(fmt.Sprintf("%v", string(encodedData))), 0644)
if err != nil {
return err
}
return nil
}
// JSONDecoder returns JSON decoder for given string
func JSONDecoder(origin string) *json.Decoder {
dec := json.NewDecoder(strings.NewReader(origin))
dec.UseNumber()
return dec
}
func HttpGetter(httpClient *http.Client, fullURL string) (string, error) {
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return "", err
}
resp, err := httpClient.Do(req)
if err != nil {
return "", err
}
respStr, err := httpRespToString(resp)
if err != nil {
return "", err
}
return respStr, nil
}
// 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))
}
bytesNum, err := io.Copy(&strBuilder, resp.Body)
respStr := strBuilder.String()
if err != nil {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
return respStr, err
}
// URLEncoder encode url
func urlEncoder(oldURL string) string {
fullURL := strings.Split(oldURL, "?")
baseURL, err := url.Parse(fullURL[0])
if err != nil {
return ""
}
// Prepare Query Parameters
if len(fullURL) > 1 {
params := url.Values{}
queryParams := strings.Split(fullURL[1], "&")
for _, i := range queryParams {
queryParam := strings.Split(i, "=")
val := ""
if len(queryParam) > 1 {
val = queryParam[1]
}
params.Add(queryParam[0], val)
}
baseURL.RawQuery = params.Encode()
}
return baseURL.String()
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"strings"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
@@ -52,7 +53,7 @@ var RunningIncluster bool
func LoadK8sConfig() error {
kubeconfig, err := config.GetConfig()
if err != nil {
return fmt.Errorf("Failed to load kubernetes config: %s\n", err)
return fmt.Errorf("failed to load kubernetes config: %s\n", strings.ReplaceAll(err.Error(), "KUBERNETES_MASTER", "KUBECONFIG"))
}
if _, err := restclient.InClusterConfig(); err == nil {
RunningIncluster = true
@@ -61,7 +62,7 @@ func LoadK8sConfig() error {
return nil
}
// GetK8sConfig get config. load if not loaded yer
// GetK8sConfig get config. load if not loaded yet
func GetK8sConfig() *restclient.Config {
if K8SConfig == nil {
if err := LoadK8sConfig(); err != nil {

View File

@@ -1,7 +1,6 @@
package opapolicy
import (
"path/filepath"
"time"
armotypes "github.com/armosec/kubescape/cautils/armotypes"
@@ -150,43 +149,3 @@ type PolicyIdentifier struct {
Kind NotificationPolicyKind `json:"kind"`
Name string `json:"name"`
}
type ScanInfo struct {
PolicyIdentifier PolicyIdentifier
Format string
Output string
ExcludedNamespaces string
InputPatterns []string
Silent bool
}
func (scanInfo *ScanInfo) Init() {
// scanInfo.setSilentMode()
scanInfo.setOutputFile()
}
func (scanInfo *ScanInfo) setSilentMode() {
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
scanInfo.Silent = true
}
if scanInfo.Output != "" {
scanInfo.Silent = true
}
}
func (scanInfo *ScanInfo) setOutputFile() {
if scanInfo.Output == "" {
return
}
if scanInfo.Format == "json" {
if filepath.Ext(scanInfo.Output) != "json" {
scanInfo.Output += ".json"
}
}
if scanInfo.Format == "junit" {
if filepath.Ext(scanInfo.Output) != "xml" {
scanInfo.Output += ".xml"
}
}
}

View File

@@ -42,9 +42,12 @@ func NewRegoDependenciesDataMock() *RegoDependenciesData {
func NewRegoDependenciesData(k8sConfig *rest.Config) *RegoDependenciesData {
regoDependenciesData := RegoDependenciesData{
K8sConfig: *NewRegoK8sConfig(k8sConfig),
regoDependenciesData := RegoDependenciesData{}
if k8sConfig != nil {
regoDependenciesData.K8sConfig = *NewRegoK8sConfig(k8sConfig)
}
return &regoDependenciesData
}
func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
@@ -61,19 +64,9 @@ func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
token = fmt.Sprintf("Bearer %s", k8sConfig.BearerToken)
}
// crtFile := os.Getenv("KUBERNETES_CRT_PATH")
// if crtFile == "" {
// crtFile = k8sConfig.CAFile
// // crtFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
// }
// glog.Infof("===========================================================================")
// glog.Infof(fmt.Sprintf("%v", k8sConfig.String()))
// glog.Infof("===========================================================================")
regoK8sConfig := RegoK8sConfig{
Token: token,
Host: k8sConfig.Host,
Host: host,
CrtFile: k8sConfig.CAFile,
ClientCrtFile: k8sConfig.CertFile,
ClientKeyFile: k8sConfig.KeyFile,

75
cautils/scaninfo.go Normal file
View File

@@ -0,0 +1,75 @@
package cautils
import (
"path/filepath"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/opapolicy"
)
type ScanInfo struct {
PolicyGetter getter.IPolicyGetter
PolicyIdentifier opapolicy.PolicyIdentifier
UseFrom string
UseDefault bool
Format string
Output string
ExcludedNamespaces string
InputPatterns []string
Silent bool
FailThreshold uint16
}
func (scanInfo *ScanInfo) Init() {
// scanInfo.setSilentMode()
scanInfo.setUseFrom()
scanInfo.setOutputFile()
scanInfo.setGetter()
}
func (scanInfo *ScanInfo) setUseFrom() {
if scanInfo.UseFrom != "" {
return
}
if scanInfo.UseDefault {
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name)
}
}
func (scanInfo *ScanInfo) setGetter() {
if scanInfo.UseFrom != "" {
// load from file
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
} else {
scanInfo.PolicyGetter = getter.NewArmoAPI()
}
}
func (scanInfo *ScanInfo) setSilentMode() {
if scanInfo.Format == "json" || scanInfo.Format == "junit" {
scanInfo.Silent = true
}
if scanInfo.Output != "" {
scanInfo.Silent = true
}
}
func (scanInfo *ScanInfo) setOutputFile() {
if scanInfo.Output == "" {
return
}
if scanInfo.Format == "json" {
if filepath.Ext(scanInfo.Output) != "json" {
scanInfo.Output += ".json"
}
}
if scanInfo.Format == "junit" {
if filepath.Ext(scanInfo.Output) != "xml" {
scanInfo.Output += ".xml"
}
}
}
func (scanInfo *ScanInfo) ScanRunningCluster() bool {
return len(scanInfo.InputPatterns) == 0
}

45
cmd/download.go Normal file
View File

@@ -0,0 +1,45 @@
package cmd
import (
"fmt"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/spf13/cobra"
)
var downloadInfo cautils.DownloadInfo
var downloadCmd = &cobra.Command{
Use: "download framework <framework-name>",
Short: "Download framework controls",
Long: ``,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) != 2 {
return fmt.Errorf("requires two arguments : framework <framework-name>")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
downloadInfo.FrameworkName = args[1]
g := getter.NewArmoAPI()
if downloadInfo.Path == "" {
downloadInfo.Path = getter.GetDefaultPath(downloadInfo.FrameworkName)
}
frameworks, err := g.GetFramework(downloadInfo.FrameworkName)
if err != nil {
return err
}
err = getter.SaveFrameworkInFile(frameworks, downloadInfo.Path)
if err != nil {
return err
}
return nil
},
}
func init() {
rootCmd.AddCommand(downloadCmd)
downloadInfo = cautils.DownloadInfo{}
downloadCmd.Flags().StringVarP(&downloadInfo.Path, "output", "o", "", "Output file. If specified, will store save to `~/.kubescape/<framework name>.json`")
}

View File

@@ -19,12 +19,12 @@ import (
"github.com/spf13/cobra"
)
var scanInfo opapolicy.ScanInfo
var scanInfo cautils.ScanInfo
var supportedFrameworks = []string{"nsa"}
type CLIHandler struct {
policyHandler *policyhandler.PolicyHandler
scanInfo *opapolicy.ScanInfo
scanInfo *cautils.ScanInfo
}
var frameworkCmd = &cobra.Command{
@@ -33,37 +33,45 @@ var frameworkCmd = &cobra.Command{
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
ValidArgs: supportedFrameworks,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
if len(args) < 1 && !(cmd.Flags().Lookup("use-from").Changed) {
return fmt.Errorf("requires at least one argument")
}
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
} else if len(args) > 0 {
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
}
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
scanInfo.PolicyIdentifier = opapolicy.PolicyIdentifier{}
scanInfo.PolicyIdentifier.Kind = opapolicy.KindFramework
scanInfo.PolicyIdentifier.Name = args[0]
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
} else { // store stout to file
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
if !(cmd.Flags().Lookup("use-from").Changed) {
scanInfo.PolicyIdentifier.Name = args[0]
}
if len(args) > 0 {
if len(args[1:]) == 0 || args[1] != "-" {
scanInfo.InputPatterns = args[1:]
} else { // store stout to file
tempFile, err := ioutil.TempFile(".", "tmp-kubescape*.yaml")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
return err
if _, err := io.Copy(tempFile, os.Stdin); err != nil {
return err
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
CliSetup()
err := CliSetup()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return nil
},
}
@@ -74,17 +82,29 @@ func isValidFramework(framework string) bool {
func init() {
scanCmd.AddCommand(frameworkCmd)
scanInfo = opapolicy.ScanInfo{}
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "output file. print output to file and not stdout")
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "silent progress output")
scanInfo = cautils.ScanInfo{}
frameworkCmd.Flags().StringVarP(&scanInfo.UseFrom, "use-from", "", "", "Path to load framework from")
frameworkCmd.Flags().BoolVarP(&scanInfo.UseDefault, "use-default", "", false, "Load framework from default path")
frameworkCmd.Flags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from check")
frameworkCmd.Flags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. supported formats: "pretty-printer"/"json"/"junit"`)
frameworkCmd.Flags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. print output to file and not stdout")
frameworkCmd.Flags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages")
frameworkCmd.Flags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 0, "Failure threshold is the percent bellow which the command fails and returns exit code -1")
}
func CliSetup() error {
flag.Parse()
k8s := k8sinterface.NewKubernetesApi()
if 100 < scanInfo.FailThreshold {
fmt.Println("bad argument: out of range threshold")
os.Exit(1)
}
var k8s *k8sinterface.KubernetesApi
if scanInfo.ScanRunningCluster() {
k8s = k8sinterface.NewKubernetesApi()
}
processNotification := make(chan *cautils.OPASessionObj)
reportResults := make(chan *cautils.OPASessionObj)
@@ -104,7 +124,12 @@ func CliSetup() error {
reporterObj.ProcessRulesListenner()
}()
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
p.ActionPrint()
score := p.ActionPrint()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {
return fmt.Errorf("Scan score is bellow threshold")
}
return nil
}

View File

@@ -9,7 +9,7 @@ var cfgFile string
var rootCmd = &cobra.Command{
Use: "kubescape",
Short: "Kubescape is a tool for testing Kubernetes security posture",
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA and MITRE specifications.`,
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA specifications.`,
}
func Execute() {

View File

@@ -50,6 +50,18 @@ kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatfo
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
```
### Scan on-prem (offline)
* Scan using a framework from the local file system
```
kubescape scan framework --use-from <path>
```
* Scan using the framework from the default location in file system
```
kubescape scan framework --use-default
```
## Output formats
By default, the output is user friendly.
@@ -64,4 +76,16 @@ kubescape scan framework nsa --format json --output results.json
* Output in `junit xml` format <img src="new-feature.svg">
```
kubescape scan framework nsa --format junit --output results.xml
```
```
## Download
* Download and save in file <img src="new-feature.svg">
```
kubescape download framework nsa --output nsa.json
```
* Download and save in default file (`~/.kubescape/<framework name>.json`)
```
kubescape download framework nsa
```

View File

@@ -29,10 +29,10 @@ OUTPUT=$BASE_DIR/$KUBESCAPE_EXEC
curl --progress-bar -L $DOWNLOAD_URL -o $OUTPUT
echo -e "\033[32m[V] Downloaded Kubescape"
sudo chmod +x $OUTPUT
sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
sudo cp $OUTPUT /usr/local/bin
rm -rf $BASE_DIR
chmod +x $OUTPUT || sudo chmod +x $OUTPUT
rm -f /usr/local/bin/$KUBESCAPE_EXEC || sudo rm -f /usr/local/bin/$KUBESCAPE_EXEC
cp $OUTPUT /usr/local/bin || sudo cp $OUTPUT /usr/local/bin
rm -rf $OUTPUT
echo -e "[V] Finished Installation"
echo

View File

@@ -26,7 +26,7 @@ type OPAProcessor struct {
func NewOPAProcessor(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessor {
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig())
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.K8SConfig)
store, err := regoDependenciesData.TOStorage()
if err != nil {
panic(err)

View File

@@ -28,7 +28,7 @@ const (
JSON_FILE_FORMAT FileFormat = "json"
)
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
workloads := []k8sinterface.IWorkload{}
// load resource from local file system

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/k8sinterface"
@@ -25,13 +26,13 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
}
}
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *opapolicy.ScanInfo) error {
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *cautils.ScanInfo) error {
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
// validate notification
// TODO
// get policies
frameworks, err := policyHandler.getPolicies(notification)
frameworks, err := policyHandler.getPolicies(notification, scanInfo.PolicyGetter)
if err != nil {
return err
}
@@ -54,12 +55,11 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return nil
}
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification, policyGetter getter.IPolicyGetter) ([]opapolicy.Framework, error) {
cautils.ProgressTextDisplay("Downloading framework definitions")
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
// TODO - support load policies from local file
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
frameworks, err := policyHandler.GetPoliciesFromBackend(notification, policyGetter)
if err != nil {
return frameworks, err
}
@@ -68,19 +68,18 @@ func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNo
err := fmt.Errorf("could not download any policies, please check previous logs")
return frameworks, err
}
cautils.SuccessTextDisplay("Downloaded framework")
cautils.SuccessTextDisplay("Downloaded/Loaded framework")
return frameworks, nil
}
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
var k8sResources *cautils.K8SResources
var err error
if len(scanInfo.InputPatterns) > 0 {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
} else {
if scanInfo.ScanRunningCluster() {
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, &notification.Designators, scanInfo.ExcludedNamespaces)
} else {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
}
return k8sResources, err

View File

@@ -1,151 +1,32 @@
package policyhandler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/opapolicy"
)
// URLEncoder encode url
func URLEncoder(oldURL string) string {
fullURL := strings.Split(oldURL, "?")
baseURL, err := url.Parse(fullURL[0])
if err != nil {
return ""
}
// Prepare Query Parameters
if len(fullURL) > 1 {
params := url.Values{}
queryParams := strings.Split(fullURL[1], "&")
for _, i := range queryParams {
queryParam := strings.Split(i, "=")
val := ""
if len(queryParam) > 1 {
val = queryParam[1]
}
params.Add(queryParam[0], val)
}
baseURL.RawQuery = params.Encode()
}
return baseURL.String()
}
type IArmoAPI interface {
OPAFRAMEWORKGet(string) ([]opapolicy.Framework, error)
}
type ArmoAPI struct {
httpClient *http.Client
hostURL string
}
func NewArmoAPI() *ArmoAPI {
return &ArmoAPI{
httpClient: &http.Client{},
hostURL: "https://dashbe.eustage2.cyberarmorsoft.com",
}
}
func (db *ArmoAPI) GetServerAddress() string {
return db.hostURL
}
func (db *ArmoAPI) GetHttpClient() *http.Client {
return db.httpClient
}
func (db *ArmoAPI) OPAFRAMEWORKGet(name string) ([]opapolicy.Framework, error) {
requestURI := "v1/armoFrameworks"
requestURI += fmt.Sprintf("?customerGUID=%s", "11111111-1111-1111-1111-111111111111")
requestURI += fmt.Sprintf("&frameworkName=%s", strings.ToUpper(name))
requestURI += "&getRules=true"
fullURL := URLEncoder(fmt.Sprintf("%s/%s", db.GetServerAddress(), requestURI))
frameworkList := []opapolicy.Framework{}
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return frameworkList, err
}
c := http.Client{}
resp, err := c.Do(req)
if err != nil {
return frameworkList, err
}
respStr, err := HTTPRespToString(resp)
if err != nil {
return frameworkList, err
}
if name != "" {
frameworkSingle := opapolicy.Framework{}
err = JSONDecoder(respStr).Decode(&frameworkSingle)
frameworkList = append(frameworkList, frameworkSingle)
} else {
err = JSONDecoder(respStr).Decode(&frameworkList)
}
return frameworkList, err
}
// JSONDecoder returns JSON decoder for given string
func JSONDecoder(origin string) *json.Decoder {
dec := json.NewDecoder(strings.NewReader(origin))
dec.UseNumber()
return dec
}
// 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))
}
bytesNum, err := io.Copy(&strBuilder, resp.Body)
respStr := strBuilder.String()
if err != nil {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
return "", fmt.Errorf("HTTP request failed. URL: '%s', Read-ERROR: '%s', HTTP-CODE: '%s', BODY(top): '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), err, resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
respStrNewLen := len(respStr)
if respStrNewLen > 1024 {
respStrNewLen = 1024
}
err = fmt.Errorf("HTTP request failed. URL: '%s', HTTP-ERROR: '%s', BODY: '%s', HTTP-HEADERS: %v, HTTP-BODY-BUFFER-LENGTH: %v", resp.Request.URL.RequestURI(), resp.Status, respStr[:respStrNewLen], resp.Header, bytesNum)
}
return respStr, err
}
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification, getPolicies getter.IPolicyGetter) ([]opapolicy.Framework, error) {
var errs error
d := NewArmoAPI()
// d := getter.NewArmoAPI()
frameworks := []opapolicy.Framework{}
// Get - cacli opa get
for _, rule := range notification.Rules {
switch rule.Kind {
case opapolicy.KindFramework:
// backend
receivedFrameworks, err := d.OPAFRAMEWORKGet(rule.Name)
receivedFramework, err := getPolicies.GetFramework(rule.Name)
if err != nil {
errs = fmt.Errorf("Could not download framework, please check if this framework exists")
errs = err
}
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
}
frameworks = append(frameworks, receivedFrameworks...)
default:
err := fmt.Errorf("Missing rule kind, expected: %s", opapolicy.KindFramework)
err := fmt.Errorf("missing rule kind, expected: %s", opapolicy.KindFramework)
errs = fmt.Errorf("%s", err.Error())
}
}
return frameworks, errs

View File

@@ -43,8 +43,28 @@ func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj, printerType, outputF
}
}
func (printer *Printer) ActionPrint() {
func calculatePostureScore(postureReport *opapolicy.PostureReport) float32 {
totalResources := 0
totalFailed := 0
for _, frameworkReport := range postureReport.FrameworkReports {
for _, controlReport := range frameworkReport.ControlReports {
for _, ruleReport := range controlReport.RuleReports {
for _, ruleResponses := range ruleReport.RuleResponses {
totalFailed += len(ruleResponses.AlertObject.K8SApiObjects)
totalFailed += len(ruleResponses.AlertObject.ExternalObjects)
}
}
totalResources += controlReport.GetNumberOfResources()
}
}
if totalResources == 0 {
return float32(0)
}
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
}
func (printer *Printer) ActionPrint() float32 {
var score float32
for {
opaSessionObj := <-*printer.opaSessionObj
if printer.printerType == PrettyPrinter {
@@ -75,10 +95,14 @@ func (printer *Printer) ActionPrint() {
os.Exit(1)
}
score = calculatePostureScore(opaSessionObj.PostureReport)
if !k8sinterface.RunningIncluster {
break
}
}
return score
}
func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {