Compare commits

..

21 Commits

Author SHA1 Message Date
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
35 changed files with 308 additions and 640 deletions

View File

@@ -28,18 +28,20 @@ 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
@@ -47,32 +49,32 @@ If you wish to scan all namespaces in your cluster, remove the `--exclude-namesp
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public
```
* Scan local `yaml`/`json` files before deploying <img src="docs/new-feature.svg">
* Scan local `yaml`/`json` files
```
kubescape scan framework nsa *.yaml
kubescape scan framework nsa examples/online-boutique/*
```
* Scan `yaml`/`json` files from url <img src="docs/new-feature.svg">
* Scan `yaml`/`json` files from url
```
kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml
```
* Output in `json` format <img src="docs/new-feature.svg">
* Output in `json` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json
```
* Output in `junit xml` format <img src="docs/new-feature.svg">
* Output in `junit xml` format
```
kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml
```
### Helm Support
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout <img src="docs/new-feature.svg">
Render the helm template and pass as stdout
```
helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa -
helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa -
```
for example:
@@ -80,23 +82,6 @@ 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`
@@ -141,8 +126,7 @@ Kubescape is running the following tests according to what is defined by [Kubern
* Linux hardening
* Ingress and Egress blocked
* Container hostPort
* Network policies
* Anonymous requests
## Technology

View File

@@ -1,7 +1,7 @@
package cautils
import (
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy"
)
// K8SResources map[<api group>/<api version>/<resource>]<resource object>

View File

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

View File

@@ -1,160 +0,0 @@
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

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

View File

@@ -3,7 +3,7 @@ package k8sinterface
import (
"testing"
"github.com/armosec/kubescape/cautils/cautils"
"kubescape/cautils/cautils"
)
func TestGetGroupVersionResource(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/armosec/kubescape/cautils/cautils"
"kubescape/cautils/cautils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"

View File

@@ -3,7 +3,7 @@ package k8sinterface
import (
"context"
"github.com/armosec/kubescape/cautils/cautils"
"kubescape/cautils/cautils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@@ -3,7 +3,7 @@ package k8sinterface
import (
"encoding/json"
"github.com/armosec/kubescape/cautils/apis"
"kubescape/cautils/apis"
corev1 "k8s.io/api/core/v1"

View File

@@ -7,8 +7,8 @@ import (
"strings"
"time"
"github.com/armosec/kubescape/cautils/apis"
"github.com/armosec/kubescape/cautils/cautils"
"kubescape/cautils/apis"
"kubescape/cautils/cautils"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@@ -1,9 +1,10 @@
package opapolicy
import (
"path/filepath"
"time"
armotypes "github.com/armosec/kubescape/cautils/armotypes"
armotypes "kubescape/cautils/armotypes"
)
type AlertScore float32
@@ -149,3 +150,43 @@ 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

@@ -3,7 +3,7 @@ package opapolicy
import (
"time"
armotypes "github.com/armosec/kubescape/cautils/armotypes"
armotypes "kubescape/cautils/armotypes"
)
// Mock A

View File

@@ -8,7 +8,7 @@ import (
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils/k8sinterface"
"kubescape/cautils/k8sinterface"
"github.com/golang/glog"
"github.com/open-policy-agent/opa/storage"
@@ -42,12 +42,9 @@ func NewRegoDependenciesDataMock() *RegoDependenciesData {
func NewRegoDependenciesData(k8sConfig *rest.Config) *RegoDependenciesData {
regoDependenciesData := RegoDependenciesData{}
if k8sConfig != nil {
regoDependenciesData.K8sConfig = *NewRegoK8sConfig(k8sConfig)
regoDependenciesData := RegoDependenciesData{
K8sConfig: *NewRegoK8sConfig(k8sConfig),
}
return &regoDependenciesData
}
func NewRegoK8sConfig(k8sConfig *rest.Config) *RegoK8sConfig {
@@ -64,9 +61,19 @@ 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: host,
Host: k8sConfig.Host,
CrtFile: k8sConfig.CAFile,
ClientCrtFile: k8sConfig.CertFile,
ClientKeyFile: k8sConfig.KeyFile,

View File

@@ -1,75 +0,0 @@
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
}

View File

@@ -1,45 +0,0 @@
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

@@ -5,26 +5,25 @@ import (
"fmt"
"io"
"io/ioutil"
"kubescape/cautils"
"kubescape/cautils/armotypes"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
"kubescape/opaprocessor"
"kubescape/policyhandler"
"kubescape/printer"
"os"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/opaprocessor"
"github.com/armosec/kubescape/policyhandler"
"github.com/armosec/kubescape/printer"
"github.com/spf13/cobra"
)
var scanInfo cautils.ScanInfo
var scanInfo opapolicy.ScanInfo
var supportedFrameworks = []string{"nsa"}
type CLIHandler struct {
policyHandler *policyhandler.PolicyHandler
scanInfo *cautils.ScanInfo
scanInfo *opapolicy.ScanInfo
}
var frameworkCmd = &cobra.Command{
@@ -33,45 +32,37 @@ 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 && !(cmd.Flags().Lookup("use-from").Changed) {
if len(args) < 1 {
return fmt.Errorf("requires at least one argument")
} else if len(args) > 0 {
if !isValidFramework(args[0]) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(supportedFrameworks, ", ")))
}
}
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 !(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
}
scanInfo.InputPatterns = []string{tempFile.Name()}
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
}
scanInfo.InputPatterns = []string{tempFile.Name()}
}
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
err := CliSetup()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
CliSetup()
return nil
},
}
@@ -82,29 +73,17 @@ func isValidFramework(framework string) bool {
func init() {
scanCmd.AddCommand(frameworkCmd)
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")
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")
}
func CliSetup() error {
flag.Parse()
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()
}
k8s := k8sinterface.NewKubernetesApi()
processNotification := make(chan *cautils.OPASessionObj)
reportResults := make(chan *cautils.OPASessionObj)
@@ -124,12 +103,7 @@ func CliSetup() error {
reporterObj.ProcessRulesListenner()
}()
p := printer.NewPrinter(&reportResults, scanInfo.Format, scanInfo.Output)
score := p.ActionPrint()
adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100
if score < adjustedFailThreshold {
return fmt.Errorf("Scan score is bellow threshold")
}
p.ActionPrint()
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 specifications.`,
Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA and MITRE specifications.`,
}
func Execute() {

View File

@@ -50,18 +50,6 @@ 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.
@@ -76,16 +64,4 @@ 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

@@ -32,7 +32,7 @@ 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 $OUTPUT
rm -rf $BASE_DIR
echo -e "[V] Finished Installation"
echo

View File

@@ -1,6 +1,6 @@
package main
import "github.com/armosec/kubescape/cmd"
import "kubescape/cmd"
func main() {
cmd.Execute()

View File

@@ -3,14 +3,13 @@ package opaprocessor
import (
"context"
"fmt"
"kubescape/cautils"
"time"
"github.com/armosec/kubescape/cautils"
"kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/opapolicy/resources"
"kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy/resources"
"github.com/golang/glog"
"github.com/open-policy-agent/opa/ast"
@@ -26,7 +25,7 @@ type OPAProcessor struct {
func NewOPAProcessor(processedPolicy, reportResults *chan *cautils.OPASessionObj) *OPAProcessor {
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.K8SConfig)
regoDependenciesData := resources.NewRegoDependenciesData(k8sinterface.GetK8sConfig())
store, err := regoDependenciesData.TOStorage()
if err != nil {
panic(err)

View File

@@ -3,19 +3,18 @@ package opaprocessor
import (
"context"
"encoding/json"
"kubescape/cautils"
"os"
"path"
"strings"
"testing"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"kubescape/cautils/k8sinterface"
// _ "k8s.io/client-go/plugin/pkg/client/auth"
restclient "k8s.io/client-go/rest"
"github.com/armosec/kubescape/cautils/opapolicy"
"github.com/armosec/kubescape/cautils/opapolicy/resources"
"kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy/resources"
"github.com/open-policy-agent/opa/ast"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

View File

@@ -1,12 +1,12 @@
package opaprocessor
import (
"github.com/armosec/kubescape/cautils"
"kubescape/cautils"
pkgcautils "github.com/armosec/kubescape/cautils/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
resources "github.com/armosec/kubescape/cautils/opapolicy/resources"
pkgcautils "kubescape/cautils/cautils"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
resources "kubescape/cautils/opapolicy/resources"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

View File

@@ -5,14 +5,13 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
"kubescape/cautils"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
"os"
"path/filepath"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"gopkg.in/yaml.v2"
)
@@ -28,7 +27,7 @@ const (
JSON_FILE_FORMAT FileFormat = "json"
)
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) loadResources(frameworks []opapolicy.Framework, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
workloads := []k8sinterface.IWorkload{}
// load resource from local file system

View File

@@ -2,12 +2,11 @@ package policyhandler
import (
"fmt"
"kubescape/cautils"
"os"
"path/filepath"
"strings"
"testing"
"github.com/armosec/kubescape/cautils"
)
func combine(base, rel string) string {
@@ -25,7 +24,7 @@ func combine(base, rel string) string {
}
func onlineBoutiquePath() string {
o, _ := os.Getwd()
return combine(o, "github.com/armosec/kubescape/examples/online-boutique/*")
return combine(o, "kubescape/examples/online-boutique/*")
}
func TestListFiles(t *testing.T) {
files, errs := listFiles([]string{onlineBoutiquePath()})

View File

@@ -2,13 +2,11 @@ package policyhandler
import (
"fmt"
"kubescape/cautils"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/getter"
"kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy"
)
// PolicyHandler -
@@ -26,13 +24,13 @@ func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterf
}
}
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *cautils.ScanInfo) error {
func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opapolicy.PolicyNotification, scanInfo *opapolicy.ScanInfo) error {
opaSessionObj := cautils.NewOPASessionObj(nil, nil)
// validate notification
// TODO
// get policies
frameworks, err := policyHandler.getPolicies(notification, scanInfo.PolicyGetter)
frameworks, err := policyHandler.getPolicies(notification)
if err != nil {
return err
}
@@ -55,11 +53,12 @@ func (policyHandler *PolicyHandler) HandleNotificationRequest(notification *opap
return nil
}
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification, policyGetter getter.IPolicyGetter) ([]opapolicy.Framework, error) {
func (policyHandler *PolicyHandler) getPolicies(notification *opapolicy.PolicyNotification) ([]opapolicy.Framework, error) {
cautils.ProgressTextDisplay("Downloading/Loading framework definitions")
cautils.ProgressTextDisplay("Downloading framework definitions")
frameworks, err := policyHandler.GetPoliciesFromBackend(notification, policyGetter)
// TODO - support load policies from local file
frameworks, err := policyHandler.GetPoliciesFromBackend(notification)
if err != nil {
return frameworks, err
}
@@ -68,18 +67,19 @@ 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/Loaded framework")
cautils.SuccessTextDisplay("Downloaded framework")
return frameworks, nil
}
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) {
func (policyHandler *PolicyHandler) getResources(notification *opapolicy.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *opapolicy.ScanInfo) (*cautils.K8SResources, error) {
var k8sResources *cautils.K8SResources
var err error
if scanInfo.ScanRunningCluster() {
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, &notification.Designators, scanInfo.ExcludedNamespaces)
} else {
if len(scanInfo.InputPatterns) > 0 {
k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo)
} else {
k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, &notification.Designators, scanInfo.ExcludedNamespaces)
}
return k8sResources, err

View File

@@ -1,32 +1,151 @@
package policyhandler
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/armosec/kubescape/cautils/getter"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy"
)
func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *opapolicy.PolicyNotification, getPolicies getter.IPolicyGetter) ([]opapolicy.Framework, error) {
// 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) {
var errs error
// d := getter.NewArmoAPI()
d := NewArmoAPI()
frameworks := []opapolicy.Framework{}
// Get - cacli opa get
for _, rule := range notification.Rules {
switch rule.Kind {
case opapolicy.KindFramework:
// backend
receivedFramework, err := getPolicies.GetFramework(rule.Name)
receivedFrameworks, err := d.OPAFRAMEWORKGet(rule.Name)
if err != nil {
errs = err
}
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
errs = fmt.Errorf("Could not download framework, please check if this framework exists")
}
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

@@ -2,14 +2,13 @@ package policyhandler
import (
"fmt"
"kubescape/cautils"
"strings"
"github.com/armosec/kubescape/cautils"
"kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/armotypes"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/armotypes"
"kubescape/cautils/opapolicy"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

View File

@@ -1,10 +1,10 @@
package policyhandler
import (
"github.com/armosec/kubescape/cautils"
"kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
)
func setResourceMap(frameworks []opapolicy.Framework) *cautils.K8SResources {

View File

@@ -1,8 +1,8 @@
package policyhandler
import (
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
"testing"
)

View File

@@ -4,11 +4,10 @@ import (
"bytes"
"fmt"
"io"
"kubescape/cautils"
"kubescape/cautils/k8sinterface"
"net/http"
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
)
func loadResourcesFromUrl(inputPatterns []string) ([]k8sinterface.IWorkload, error) {

View File

@@ -3,8 +3,7 @@ package printer
import (
"encoding/xml"
"fmt"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/opapolicy"
)
type JUnitTestSuites struct {

View File

@@ -4,13 +4,12 @@ import (
"encoding/json"
"encoding/xml"
"fmt"
"kubescape/cautils"
"os"
"sort"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
"github.com/enescakir/emoji"
"github.com/olekukonko/tablewriter"
@@ -43,28 +42,8 @@ func NewPrinter(opaSessionObj *chan *cautils.OPASessionObj, printerType, outputF
}
}
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() {
func (printer *Printer) ActionPrint() float32 {
var score float32
for {
opaSessionObj := <-*printer.opaSessionObj
if printer.printerType == PrettyPrinter {
@@ -95,14 +74,10 @@ func (printer *Printer) ActionPrint() float32 {
os.Exit(1)
}
score = calculatePostureScore(opaSessionObj.PostureReport)
if !k8sinterface.RunningIncluster {
break
}
}
return score
}
func (printer *Printer) SummarySetup(postureReport *opapolicy.PostureReport) {

View File

@@ -3,8 +3,8 @@ package printer
import (
"fmt"
"github.com/armosec/kubescape/cautils/k8sinterface"
"github.com/armosec/kubescape/cautils/opapolicy"
"kubescape/cautils/k8sinterface"
"kubescape/cautils/opapolicy"
)
// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>}