Compare commits

...

27 Commits

Author SHA1 Message Date
dwertent
ca927dec30 update naming convention 2021-10-28 10:05:30 +03:00
dwertent
3a78ef46a3 Merge remote-tracking branch 'upstream/dev' 2021-10-28 09:40:26 +03:00
David Wertenteil
bdb1cd0905 Merge pull request #199 from Daniel-GrunbergerCA/master
Scan with multiple frameworks/control support
2021-10-28 09:35:07 +03:00
dwertent
ffb556a637 update readme 2021-10-28 09:15:22 +03:00
dwertent
40acfb5e9d Adding cronJob doc 2021-10-28 09:10:37 +03:00
Daniel-GrunbergerCA
de8bcfa0d2 enhance help msgs 2021-10-27 14:44:25 +03:00
Daniel-GrunbergerCA
9439f407da add env var to not check latest release 2021-10-27 13:39:03 +03:00
Daniel-GrunbergerCA
5095e62961 support scanning multiple frameworks from multiple files 2021-10-27 13:02:45 +03:00
Daniel-GrunbergerCA
3301907864 print only one table for controls & enhance help msg 2021-10-27 10:25:26 +03:00
Daniel-GrunbergerCA
151175c40f read single control from framework file 2021-10-27 08:49:40 +03:00
Daniel-GrunbergerCA
234d4fa537 Merge remote-tracking branch 'upstream/dev' 2021-10-27 08:27:21 +03:00
dwertent
66068757e1 update cluster name in mock struct 2021-10-26 20:39:18 +03:00
dwertent
8a7cda5dd1 adopt cluster name 2021-10-26 20:27:33 +03:00
Daniel-GrunbergerCA
a0ca68cc41 update json and junit for multiple frameworks 2021-10-26 13:55:12 +03:00
Daniel-GrunbergerCA
41cae0bc93 Merge remote-tracking branch 'upstream/dev' 2021-10-26 13:23:00 +03:00
David Wertenteil
b4198fde8c Merge pull request #198 from dwertent/master
update pkg tag
2021-10-26 12:28:02 +03:00
dwertent
bd24f35738 update tag 2021-10-26 12:26:44 +03:00
Daniel-GrunbergerCA
6fcbb757b5 Merge remote-tracking branch 'upstream/dev' 2021-10-25 17:41:15 +03:00
Daniel-GrunbergerCA
3b8825e5d2 scan multiple frameworks and controls 2021-10-25 17:41:04 +03:00
Rotem Refael
5cf3244918 Merge pull request #192 from dwertent/master
Update multiple score
2021-10-25 17:40:19 +03:00
dwertent
934c9ccc8b fixed lowest 2021-10-25 15:51:23 +03:00
dwertent
41dfdfd1e8 support more than score 2021-10-25 15:14:31 +03:00
David Wertenteil
427fb59c99 Merge pull request #190 from dwertent/master
Fixed submit and url
2021-10-25 12:08:23 +03:00
David Wertenteil
ae825800f6 Merge pull request #189 from Daniel-GrunbergerCA/master
Update tag for newest release of k8s-interface
2021-10-25 12:08:06 +03:00
dwertent
d72700acf6 update submit 2021-10-25 12:05:51 +03:00
Daniel-GrunbergerCA
04b55e764a fix k8s-interface pkg tag 2021-10-25 10:44:43 +03:00
Daniel-GrunbergerCA
beb4062bb1 update tag 2021-10-25 09:29:43 +03:00
27 changed files with 534 additions and 134 deletions

View File

@@ -87,13 +87,13 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser
### 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/News-Features/Feature-Stories/Article-View/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
```
@@ -130,6 +130,10 @@ kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --form
kubescape scan framework nsa --exceptions examples/exceptions.json
```
### Repeatedly Kubescape Scanning using a CronJob
For setting up a cronJob please follow the [instructions](examples/cronJob-support/README.md)
### Helm Support
* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout

View File

@@ -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,7 +129,7 @@ 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())
InfoTextDisplay(os.Stdout, fmt.Sprintf("\n%s\n", message))
@@ -158,7 +160,7 @@ func (c *ClusterConfig) GetDefaultNS() string { return c.defau
func (c *ClusterConfig) GetBackendAPI() getter.IBackend { return c.backendAPI }
func (c *ClusterConfig) GenerateURL() {
message := "\nCheckout for more cool features: "
message := "Checkout for more cool features: "
u := url.URL{}
u.Scheme = "https"
@@ -167,7 +169,7 @@ func (c *ClusterConfig) GenerateURL() {
return
}
if c.configObj.CustomerAdminEMail != "" {
InfoTextDisplay(os.Stdout, message+u.String()+"\n")
InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n")
return
}
u.Path = "account/sign-up"
@@ -176,7 +178,7 @@ func (c *ClusterConfig) GenerateURL() {
q.Add("customerGUID", c.configObj.CustomerGUID)
u.RawQuery = q.Encode()
InfoTextDisplay(os.Stdout, message+u.String()+"\n")
InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n")
}
func (c *ClusterConfig) GetCustomerGUID() string {
@@ -241,7 +243,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
@@ -470,3 +472,7 @@ func DeleteConfigMap(k8s *k8sinterface.KubernetesApi) error {
func DeleteConfigFile() error {
return os.Remove(ConfigFileFullPath())
}
func adoptClusterName(clusterName string) string {
return strings.ReplaceAll(clusterName, "/", "-")
}

View File

@@ -18,7 +18,7 @@ type DownloadReleasedPolicy struct {
func NewDownloadReleasedPolicy() *DownloadReleasedPolicy {
return &DownloadReleasedPolicy{
gs: gitregostore.InitDefaultGitRegoStore(),
gs: gitregostore.InitDefaultGitRegoStore(-1),
}
}

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.getFileForControl()
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(controlName)
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.getFileForException()
exception := []armotypes.PostureExceptionPolicy{}
f, err := os.ReadFile(lp.filePath)
f, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
@@ -68,3 +89,11 @@ func (lp *LoadPolicy) GetExceptions(customerGUID, clusterName string) ([]armotyp
err = json.Unmarshal(f, &exception)
return exception, err
}
func (lp *LoadPolicy) getFileForException() string {
return lp.filePaths[0]
}
func (lp *LoadPolicy) getFileForControl() string {
return lp.filePaths[0]
}

View File

@@ -9,9 +9,9 @@ import (
type ScanInfo struct {
Getters
PolicyIdentifier reporthandling.PolicyIdentifier
PolicyIdentifier []reporthandling.PolicyIdentifier
UseExceptions string // Load exceptions configuration
UseFrom string // Load framework from local file (instead of download). Use when running offline
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
@@ -41,22 +41,21 @@ func (scanInfo *ScanInfo) Init() {
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) setUseFrom() {
if scanInfo.UseFrom != "" {
return
}
if scanInfo.UseDefault {
scanInfo.UseFrom = getter.GetDefaultPath(scanInfo.PolicyIdentifier.Name + ".json")
for _, policy := range scanInfo.PolicyIdentifier {
scanInfo.UseFrom = append(scanInfo.UseFrom, getter.GetDefaultPath(policy.Name+".json"))
}
}
}
func (scanInfo *ScanInfo) setGetter() {
if scanInfo.UseFrom != "" {
if len(scanInfo.UseFrom) > 0 {
// load from file
scanInfo.PolicyGetter = getter.NewLoadPolicy(scanInfo.UseFrom)
} else {

View File

@@ -13,22 +13,40 @@ import (
// controlCmd represents the control command
var controlCmd = &cobra.Command{
Use: "control <control name>/<control id>",
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", clihandler.ValidFrameworks),
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_one>,<control_two>")
}
}
} 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) < 1 {
scanInfo.PolicyIdentifier = SetScanForGivenFrameworks(clihandler.SupportedFrameworks)
} else {
var controls []string
if len(args) > 0 {
controls = strings.Split(args[0], ",")
scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
scanInfo.PolicyIdentifier = setScanForFirstControl(controls)
}
if len(controls) > 1 {
scanInfo.PolicyIdentifier = SetScanForGivenControls(controls[1:])
}
}
scanInfo.FrameworkScan = false
scanInfo.PolicyIdentifier.Kind = reporthandling.KindControl
scanInfo.Init()
cautils.SetSilentMode(scanInfo.Silent)
err := clihandler.CliSetup(&scanInfo)
@@ -51,3 +69,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
}

View File

@@ -9,32 +9,42 @@ import (
"github.com/armosec/kubescape/cautils"
"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),
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", clihandler.ValidFrameworks),
Short: fmt.Sprintf("The framework you wish to use. Supported frameworks: %s", strings.Join(clihandler.SupportedFrameworks, ", ")),
Long: "Execute a scan on a running Kubernetes cluster or `yaml`/`json` files (use glob) or `-` for stdin",
ValidArgs: clihandler.SupportedFrameworks,
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 {
// "nsa, mitre" -> ["nsa", "mitre"] and nsa,mitre -> ["nsa", "mitre"]
frameworks := strings.Split(strings.Join(strings.Fields(args[0]), ""), ",")
for _, framework := range frameworks {
if !isValidFramework(strings.ToLower(framework)) {
return fmt.Errorf(fmt.Sprintf("supported frameworks: %s", strings.Join(clihandler.SupportedFrameworks, ", ")))
}
}
} 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])
scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
// If no framework provided, use all
if len(args) < 1 {
scanInfo.PolicyIdentifier = SetScanForGivenFrameworks(clihandler.SupportedFrameworks)
} else {
// Read frameworks from input args
scanInfo.PolicyIdentifier = []reporthandling.PolicyIdentifier{}
frameworks := strings.Split(strings.Join(strings.Fields(args[0]), ""), ",")
scanInfo.PolicyIdentifier = SetScanForFirstFramework(frameworks)
if len(frameworks) > 1 {
scanInfo.PolicyIdentifier = SetScanForGivenFrameworks(frameworks[1:])
}
}
if len(args) > 0 {
if len(args[1:]) == 0 || args[1] != "-" {
@@ -74,11 +84,26 @@ func init() {
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 SetScanForGivenFrameworks(frameworks []string) []reporthandling.PolicyIdentifier {
for _, framework := range frameworks {
newPolicy := reporthandling.PolicyIdentifier{}
newPolicy.Kind = reporthandling.KindFramework
newPolicy.Name = framework
scanInfo.PolicyIdentifier = append(scanInfo.PolicyIdentifier, newPolicy)
}
return scanInfo.PolicyIdentifier
}
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)

View File

@@ -5,6 +5,7 @@ import (
"strings"
"github.com/armosec/kubescape/cautils"
"github.com/armosec/kubescape/clihandler"
"github.com/spf13/cobra"
)
@@ -16,15 +17,19 @@ 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 {
frameworkArgs := []string{clihandler.ValidFrameworks}
frameworkArgs = append(frameworkArgs, args...)
frameworkCmd.RunE(cmd, frameworkArgs)
}
},
}
@@ -35,7 +40,7 @@ func init() {
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().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")
}

View File

@@ -29,7 +29,7 @@ func GetLatestVersion() (string, 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)
return "unknown", nil
}
body, err := io.ReadAll(resp.Body)

View File

@@ -34,7 +34,7 @@ type componentInterfaces struct {
}
func getReporter(scanInfo *cautils.ScanInfo) reporter.IReport {
if scanInfo.Local {
if !scanInfo.Submit {
return reporter.NewReportMock()
}
if !scanInfo.FrameworkScan {
@@ -135,10 +135,8 @@ func (clihandler *CLIHandler) Scan() error {
cautils.ScanStartDisplay()
policyNotification := &reporthandling.PolicyNotification{
NotificationType: reporthandling.TypeExecPostureScan,
Rules: []reporthandling.PolicyIdentifier{
clihandler.scanInfo.PolicyIdentifier,
},
Designators: armotypes.PortalDesignator{},
Rules: clihandler.scanInfo.PolicyIdentifier,
Designators: armotypes.PortalDesignator{},
}
switch policyNotification.NotificationType {
case reporthandling.TypeExecPostureScan:

View File

@@ -0,0 +1,83 @@
# Repeatedly Kubescape Scanning
You can scan your cluster repeatedly by adding a `CronJob` that will repeatedly trigger kubescape
* Setup [scanning & submitting](#scanning-&-submitting)
* Setup [scanning without submitting](#scanning-without-submitting)
## Scanning & Submitting
If you wish to repeatedly 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
<img src="screenshots/add-cluster.png" alt="add-cluster">
3. Copy the value of `--account` and set it in the `data.customerGUID` field
<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 repeatedly 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
```

View File

@@ -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>"
}

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,7 @@
# ------------------- Kubescape User/Customer ID ------------------- #
kind: Namespace
apiVersion: v1
metadata:
name: kubescape
labels:
app: kubescape

View File

@@ -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

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

4
go.mod
View File

@@ -4,8 +4,8 @@ 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/k8s-interface v0.0.8
github.com/armosec/opa-utils v0.0.18
github.com/armosec/utils-go v0.0.3
github.com/briandowns/spinner v1.16.0
github.com/enescakir/emoji v1.0.0

7
go.sum
View File

@@ -87,10 +87,11 @@ github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qm
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/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.18 h1:1hL5v2KCD8yStuwzul+gq1zg9+RCV9N3kHoRepKnrg0=
github.com/armosec/opa-utils v0.0.18/go.mod h1:E0mFTVx+4BYAVvO2hxWnIniv/IZIogRCak8BkKd7KK4=
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=

14
main.go
View File

@@ -5,19 +5,25 @@ import (
"os"
"github.com/armosec/kubescape/clihandler/cmd"
pkgutils "github.com/armosec/utils-go/utils"
)
const SKIP_VERSION_CHECK = "KUBESCAPE_SKIP_UPDATE_CHECK"
func main() {
CheckLatestVersion()
cmd.Execute()
}
func CheckLatestVersion() {
if v, ok := os.LookupEnv(SKIP_VERSION_CHECK); ok && pkgutils.StringToBool(v) {
return
}
latest, err := cmd.GetLatestVersion()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
} else if latest != cmd.BuildNumber {
if err != nil || latest == "unknown" {
return
}
if latest != cmd.BuildNumber {
fmt.Println("Warning: You are not updated to the latest release: " + latest)
}
}

View File

@@ -14,9 +14,11 @@ func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *reporth
frameworks := []reporthandling.Framework{}
exceptionPolicies := []armotypes.PostureExceptionPolicy{}
// Get - cacli opa get
for _, rule := range notification.Rules {
switch rule.Kind {
case reporthandling.KindFramework:
rule := GetScanKind(notification)
switch rule.Kind {
case reporthandling.KindFramework:
for _, rule := range notification.Rules {
receivedFramework, recExceptionPolicies, err := policyHandler.getFrameworkPolicies(rule.Name)
if receivedFramework != nil {
frameworks = append(frameworks, *receivedFramework)
@@ -29,27 +31,32 @@ func (policyHandler *PolicyHandler) GetPoliciesFromBackend(notification *reporth
}
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)
}
case reporthandling.KindControl:
f := reporthandling.Framework{}
var receivedControl *reporthandling.Control
var recExceptionPolicies []armotypes.PostureExceptionPolicy
var err error
for _, rule := range notification.Rules {
receivedControl, recExceptionPolicies, err = policyHandler.getControl(rule.Name)
if receivedControl != nil {
f.Controls = append(f.Controls, *receivedControl)
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())
}
frameworks = append(frameworks, f)
// TODO: add case for control from file
default:
err := fmt.Errorf("missing rule kind, expected: %s", reporthandling.KindFramework)
errs = fmt.Errorf("%s", err.Error())
}
return frameworks, exceptionPolicies, errs
}
@@ -68,24 +75,30 @@ func (policyHandler *PolicyHandler) getFrameworkPolicies(policyName string) (*re
return receivedFramework, receivedException, nil
}
func GetScanKind(notification *reporthandling.PolicyNotification) *reporthandling.PolicyIdentifier {
if len(notification.Rules) > 0 {
return &notification.Rules[0]
}
return nil
}
// Get control by name
func (policyHandler *PolicyHandler) getControl(policyName string) ([]reporthandling.Control, []armotypes.PostureExceptionPolicy, error) {
func (policyHandler *PolicyHandler) getControl(policyName string) (*reporthandling.Control, []armotypes.PostureExceptionPolicy, error) {
controls := []reporthandling.Control{}
control, err := policyHandler.getters.PolicyGetter.GetControl(policyName)
control := &reporthandling.Control{}
var err error
control, err = policyHandler.getters.PolicyGetter.GetControl(policyName)
if err != nil {
return nil, nil, err
return control, nil, err
}
if control == nil {
return nil, nil, fmt.Errorf("control not found")
}
controls = append(controls, *control)
// if control == nil {
// return control, nil, fmt.Errorf("control not found")
// }
exceptions, err := policyHandler.getters.ExceptionsGetter.GetExceptions(cautils.CustomerGUID, cautils.ClusterName)
if err != nil {
return controls, nil, err
return control, nil, err
}
return controls, exceptions, nil
return control, exceptions, nil
}

View File

@@ -21,11 +21,19 @@ func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) {
}
func (jsonPrinter *JsonPrinter) Score(score float32) {
fmt.Printf("\nFinal score: %d\n", int(score))
fmt.Printf("\nFinal score: %d", int(score*100))
}
func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {
postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0])
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)

View File

@@ -22,7 +22,7 @@ func (junitPrinter *JunitPrinter) SetWriter(outputFile string) {
}
func (junitPrinter *JunitPrinter) Score(score float32) {
fmt.Printf("\nFinal score: %d\n", int(score))
fmt.Printf("\nFinal score: %d", int(score*100))
}
func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) {

View File

@@ -24,12 +24,25 @@ func NewPrettyPrinter() *PrettyPrinter {
}
}
// 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)
printer.summarySetup(opaSessionObj.PostureReport)
printer.printResults()
printer.printSummaryTable()
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
}
@@ -41,29 +54,27 @@ func (printer *PrettyPrinter) SetWriter(outputFile string) {
func (printer *PrettyPrinter) Score(score float32) {
}
func (printer *PrettyPrinter) summarySetup(postureReport *reporthandling.PostureReport) {
for _, fr := range postureReport.FrameworkReports {
printer.frameworkSummary = ControlSummary{
TotalResources: fr.GetNumberOfResources(),
TotalFailed: fr.GetNumberOfFailedResources(),
TotalWarnign: fr.GetNumberOfWarningResources(),
func (printer *PrettyPrinter) summarySetup(fr reporthandling.FrameworkReport) {
printer.frameworkSummary = ControlSummary{
TotalResources: fr.GetNumberOfResources(),
TotalFailed: fr.GetNumberOfFailedResources(),
TotalWarnign: fr.GetNumberOfWarningResources(),
}
for _, cr := range fr.ControlReports {
if len(cr.RuleReports) == 0 {
continue
}
for _, cr := range fr.ControlReports {
if len(cr.RuleReports) == 0 {
continue
}
workloadsSummary := listResultSummary(cr.RuleReports)
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.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()
@@ -179,7 +190,10 @@ func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string
}
return row
}
func (printer *PrettyPrinter) printSummaryTable() {
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())
@@ -195,6 +209,12 @@ func (printer *PrettyPrinter) printSummaryTable() {
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 {

View File

@@ -38,14 +38,19 @@ func (resultsHandler *ResultsHandler) HandleResults(scanInfo *cautils.ScanInfo)
// CalculatePostureScore calculate final score
func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 {
totalResources := 0
totalFailed := 0
lowestScore := float32(100)
for _, frameworkReport := range postureReport.FrameworkReports {
totalFailed += frameworkReport.GetNumberOfFailedResources()
totalResources += frameworkReport.GetNumberOfResources()
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
}
}
if totalResources == 0 {
return float32(0)
}
return (float32(totalResources) - float32(totalFailed)) / float32(totalResources)
return lowestScore
}