diff --git a/README.md b/README.md index 2e52fd04..6bcc3cb6 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,21 @@ kubescape scan framework nsa --submit kubescape scan framework mitre --submit ``` + +* Scan a running Kubernetes cluster with a specific control using the control name or control ID. [List of controls](https://hub.armo.cloud/docs/controls) +``` +kubescape scan control "Privileged container" +``` + * Scan local `yaml`/`json` files before deploying. [Take a look at the demonstration](https://youtu.be/Ox6DaR7_4ZI) ``` kubescape scan framework nsa *.yaml ``` -* Scan `yaml`/`json` files from url +* Scan kubernetes manifest files from a public github repository ``` -kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml +kubescape scan framework nsa https://github.com/armosec/kubescape ``` * Output in `json` format @@ -193,7 +199,13 @@ go build -o kubescape . 4. Enjoy :zany_face: -## How to build in Docker +## Docker Support + +### Official Docker image +``` +quay.io/armosec/kubescape +``` +### Build your own Docker image 1. Clone Project ``` diff --git a/build.py b/build.py index 546b0084..2104775d 100644 --- a/build.py +++ b/build.py @@ -41,7 +41,7 @@ def main(): # Set some variables packageName = getPackageName() - buildUrl = "github.com/armosec/kubescape/cmd.BuildNumber" + buildUrl = "github.com/armosec/kubescape/clihandler/cmd.BuildNumber" releaseVersion = os.getenv("RELEASE") ArmoBEServer = os.getenv("ArmoBEServer") ArmoERServer = os.getenv("ArmoERServer") diff --git a/build/Dockerfile b/build/Dockerfile index 6385051f..f80803cb 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -24,4 +24,4 @@ COPY --from=builder /work/build/ubuntu-latest/kubescape /usr/bin/kubescape # # Download the frameworks. Use the "--use-default" flag when running kubescape # RUN kubescape download framework nsa && kubescape download framework mitre -CMD ["kubescape"] +ENTRYPOINT ["kubescape"] diff --git a/cautils/customerloader.go b/cautils/customerloader.go index 499e7630..c77347f0 100644 --- a/cautils/customerloader.go +++ b/cautils/customerloader.go @@ -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 } @@ -158,6 +160,7 @@ func (c *ClusterConfig) GetDefaultNS() string { return c.defau func (c *ClusterConfig) GetBackendAPI() getter.IBackend { return c.backendAPI } func (c *ClusterConfig) GenerateURL() { + message := "Checkout for more cool features: " u := url.URL{} u.Scheme = "https" @@ -165,9 +168,8 @@ func (c *ClusterConfig) GenerateURL() { if c.configObj == nil { return } - message := fmt.Sprintf("\nCheckout for more cool features: https://%s\n", getter.GetArmoAPIConnector().GetFrontendURL()) if c.configObj.CustomerAdminEMail != "" { - InfoTextDisplay(os.Stdout, message+"\n") + InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n") return } u.Path = "account/sign-up" @@ -176,8 +178,7 @@ func (c *ClusterConfig) GenerateURL() { q.Add("customerGUID", c.configObj.CustomerGUID) u.RawQuery = q.Encode() - InfoTextDisplay(os.Stdout, message+"\n") - + InfoTextDisplay(os.Stdout, "\n\n"+message+u.String()+"\n\n") } func (c *ClusterConfig) GetCustomerGUID() string { @@ -242,7 +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 @@ -471,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, "/", "-") +} diff --git a/cautils/getter/downloadreleasedpolicy.go b/cautils/getter/downloadreleasedpolicy.go index 944c1bd1..23ca29e5 100644 --- a/cautils/getter/downloadreleasedpolicy.go +++ b/cautils/getter/downloadreleasedpolicy.go @@ -18,7 +18,7 @@ type DownloadReleasedPolicy struct { func NewDownloadReleasedPolicy() *DownloadReleasedPolicy { return &DownloadReleasedPolicy{ - gs: gitregostore.InitDefaultGitRegoStore(), + gs: gitregostore.InitDefaultGitRegoStore(-1), } } diff --git a/cautils/getter/loadpolicy.go b/cautils/getter/loadpolicy.go index c976a478..223ee7ec 100644 --- a/cautils/getter/loadpolicy.go +++ b/cautils/getter/loadpolicy.go @@ -35,7 +35,10 @@ func (lp *LoadPolicy) GetControl(controlName string) (*reporthandling.Control, e 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") } @@ -50,7 +53,10 @@ func (lp *LoadPolicy) GetFramework(frameworkName string) (*reporthandling.Framew return nil, err } - err = json.Unmarshal(f, framework) + if err = json.Unmarshal(f, framework); err != nil { + return framework, err + } + if frameworkName != "" && !strings.EqualFold(frameworkName, framework.Name) { return nil, fmt.Errorf("framework from file not matching") } diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 8ca73ca6..305b400c 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -3,7 +3,6 @@ package cautils import ( "path/filepath" - "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/opa-utils/reporthandling" ) @@ -84,22 +83,3 @@ func (scanInfo *ScanInfo) setOutputFile() { func (scanInfo *ScanInfo) ScanRunningCluster() bool { return len(scanInfo.InputPatterns) == 0 } - -func (scanInfo *ScanInfo) SetClusterConfig() (IClusterConfig, *k8sinterface.KubernetesApi) { - var clusterConfig IClusterConfig - var k8s *k8sinterface.KubernetesApi - if !scanInfo.ScanRunningCluster() { - k8sinterface.ConnectedToCluster = false - clusterConfig = NewEmptyConfig() - } else { - k8s = k8sinterface.NewKubernetesApi() - // setup cluster config - clusterConfig = ClusterConfigSetup(scanInfo, k8s, getter.GetArmoAPIConnector()) - } - return clusterConfig, k8s -} - -// func (scanInfo *ScanInfo) ConnectedToCluster(k8s k8sinterface.) bool { -// _, err := k8s.KubernetesClient.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{}) -// return err == nil -// } diff --git a/clihandler/cmd/control.go b/clihandler/cmd/control.go index 0618e0c4..e89ba905 100644 --- a/clihandler/cmd/control.go +++ b/clihandler/cmd/control.go @@ -31,7 +31,7 @@ var controlCmd = &cobra.Command{ scanInfo.PolicyIdentifier.Kind = reporthandling.KindControl scanInfo.Init() cautils.SetSilentMode(scanInfo.Silent) - err := clihandler.CliSetup(scanInfo) + err := clihandler.CliSetup(&scanInfo) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) diff --git a/clihandler/cmd/framework.go b/clihandler/cmd/framework.go index bd0bbfe6..9461dce5 100644 --- a/clihandler/cmd/framework.go +++ b/clihandler/cmd/framework.go @@ -54,7 +54,7 @@ var frameworkCmd = &cobra.Command{ } scanInfo.Init() cautils.SetSilentMode(scanInfo.Silent) - err := clihandler.CliSetup(scanInfo) + err := clihandler.CliSetup(&scanInfo) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) diff --git a/clihandler/cmd/version.go b/clihandler/cmd/version.go index ebcc7d68..45ba5bc2 100644 --- a/clihandler/cmd/version.go +++ b/clihandler/cmd/version.go @@ -25,21 +25,21 @@ func GetLatestVersion() (string, error) { latestVersion := "https://api.github.com/repos/armosec/kubescape/releases/latest" resp, err := http.Get(latestVersion) if err != nil { - return "", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, err.Error()) + return "unknown", fmt.Errorf("failed to get latest releases from '%s', reason: %s", latestVersion, err.Error()) } defer resp.Body.Close() if resp.StatusCode < 200 || 301 < resp.StatusCode { - return "", fmt.Errorf("failed to download file, status code: %s", resp.Status) + return "unknown", fmt.Errorf("failed to download file, status code: %s", resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { - return "", fmt.Errorf("failed to read response body from '%s', reason: %s", latestVersion, err.Error()) + return "unknown", fmt.Errorf("failed to read response body from '%s', reason: %s", latestVersion, err.Error()) } var data map[string]interface{} err = json.Unmarshal(body, &data) if err != nil { - return "", fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestVersion, err.Error()) + return "unknown", fmt.Errorf("failed to unmarshal response body from '%s', reason: %s", latestVersion, err.Error()) } return fmt.Sprintf("%v", data["tag_name"]), nil } diff --git a/clihandler/initcli.go b/clihandler/initcli.go index c335702d..1fdaae44 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -6,9 +6,12 @@ import ( "strings" "github.com/armosec/armoapi-go/armotypes" + "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/kubescape/opaprocessor" "github.com/armosec/kubescape/policyhandler" + "github.com/armosec/kubescape/resourcehandler" "github.com/armosec/kubescape/resultshandling" "github.com/armosec/kubescape/resultshandling/printer" "github.com/armosec/kubescape/resultshandling/reporter" @@ -23,25 +26,77 @@ type CLIHandler struct { var SupportedFrameworks = []string{"nsa", "mitre"} var ValidFrameworks = strings.Join(SupportedFrameworks, ", ") -func CliSetup(scanInfo cautils.ScanInfo) error { +type componentInterfaces struct { + clusterConfig cautils.IClusterConfig + resourceHandler resourcehandler.IResourceHandler + report reporter.IReport + printerHandler printer.IPrinter +} - clusterConfig, k8s := scanInfo.SetClusterConfig() +func getReporter(scanInfo *cautils.ScanInfo) reporter.IReport { + if !scanInfo.Submit { + return reporter.NewReportMock() + } + if !scanInfo.FrameworkScan { + return reporter.NewReportMock() + } + + return reporter.NewReportEventReceiver() +} +func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { + var resourceHandler resourcehandler.IResourceHandler + var clusterConfig cautils.IClusterConfig + var reportHandler reporter.IReport + + if !scanInfo.ScanRunningCluster() { + k8sinterface.ConnectedToCluster = false + clusterConfig = cautils.NewEmptyConfig() + + // load fom file + resourceHandler = resourcehandler.NewFileResourceHandler(scanInfo.InputPatterns) + + // set mock report (do not send report) + reportHandler = reporter.NewReportMock() + } else { + k8s := k8sinterface.NewKubernetesApi() + resourceHandler = resourcehandler.NewK8sResourceHandler(k8s, scanInfo.ExcludedNamespaces) + clusterConfig = cautils.ClusterConfigSetup(scanInfo, k8s, getter.GetArmoAPIConnector()) + + // setup reporter + reportHandler = getReporter(scanInfo) + } + + // setup printer + printerHandler := printer.GetPrinter(scanInfo.Format) + printerHandler.SetWriter(scanInfo.Output) + + return componentInterfaces{ + clusterConfig: clusterConfig, + resourceHandler: resourceHandler, + report: reportHandler, + printerHandler: printerHandler, + } +} + +func CliSetup(scanInfo *cautils.ScanInfo) error { + + interfaces := getInterfaces(scanInfo) processNotification := make(chan *cautils.OPASessionObj) reportResults := make(chan *cautils.OPASessionObj) - // policy handler setup - policyHandler := policyhandler.NewPolicyHandler(&processNotification, k8s) - - if err := clusterConfig.SetConfig(scanInfo.Account); err != nil { + if err := interfaces.clusterConfig.SetConfig(scanInfo.Account); err != nil { fmt.Println(err) } - cautils.ClusterName = clusterConfig.GetClusterName() - cautils.CustomerGUID = clusterConfig.GetCustomerGUID() - + cautils.ClusterName = interfaces.clusterConfig.GetClusterName() // TODO - Deprecated + cautils.CustomerGUID = interfaces.clusterConfig.GetCustomerGUID() // TODO - Deprecated + interfaces.report.SetClusterName(interfaces.clusterConfig.GetClusterName()) + interfaces.report.SetCustomerGUID(interfaces.clusterConfig.GetCustomerGUID()) // cli handler setup go func() { + // policy handler setup + policyHandler := policyhandler.NewPolicyHandler(&processNotification, interfaces.resourceHandler) cli := NewCLIHandler(policyHandler, scanInfo) if err := cli.Scan(); err != nil { fmt.Println(err) @@ -55,13 +110,11 @@ func CliSetup(scanInfo cautils.ScanInfo) error { opaprocessorObj.ProcessRulesListenner() }() - resultsHandling := resultshandling.NewResultsHandler(&reportResults, reporter.NewReportEventReceiver(), printer.NewPrinter(scanInfo.Format, scanInfo.Output)) + resultsHandling := resultshandling.NewResultsHandler(&reportResults, interfaces.report, interfaces.printerHandler) score := resultsHandling.HandleResults(scanInfo) // print report url - if scanInfo.FrameworkScan { - clusterConfig.GenerateURL() - } + interfaces.clusterConfig.GenerateURL() adjustedFailThreshold := float32(scanInfo.FailThreshold) / 100 if score < adjustedFailThreshold { @@ -71,9 +124,9 @@ func CliSetup(scanInfo cautils.ScanInfo) error { return nil } -func NewCLIHandler(policyHandler *policyhandler.PolicyHandler, scanInfo cautils.ScanInfo) *CLIHandler { +func NewCLIHandler(policyHandler *policyhandler.PolicyHandler, scanInfo *cautils.ScanInfo) *CLIHandler { return &CLIHandler{ - scanInfo: &scanInfo, + scanInfo: scanInfo, policyHandler: policyHandler, } } diff --git a/examples/cronjob/ks-cronjob.yaml b/examples/cronjob/ks-cronjob.yaml index 49f46efe..b00b2aff 100644 --- a/examples/cronjob/ks-cronjob.yaml +++ b/examples/cronjob/ks-cronjob.yaml @@ -72,10 +72,8 @@ metadata: data: config.json: | { - "adminMail": "", - "customerGUID": "", - "clusterName": "", - "invitationParam": "" + "customerGUID": , + "clusterName": } --- @@ -104,12 +102,11 @@ spec: spec: containers: - name: kubescape - image: quay.io/armosec/kubescape:latset + image: quay.io/armosec/kubescape:latest imagePullPolicy: IfNotPresent command: ["/bin/sh","-c"] args: - - kubescape scan framework nsa --exclude-namespaces kube-system,kube-public - #- kubescape scan framework nsa --use-default --exclude-namespaces kube-system,kube-public + - kubescape scan framework nsa --submit volumeMounts: - name: kubescape-config-volume mountPath: /root/.kubescape/config.json diff --git a/go.mod b/go.mod index 0e3cbc03..508431cb 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 842bf3e4..2ef3ea74 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/opaprocessor/processorhandler.go b/opaprocessor/processorhandler.go index ab79456e..d4a6b759 100644 --- a/opaprocessor/processorhandler.go +++ b/opaprocessor/processorhandler.go @@ -8,7 +8,6 @@ import ( "github.com/armosec/kubescape/cautils" "github.com/armosec/opa-utils/exceptions" "github.com/armosec/opa-utils/reporthandling" - "github.com/armosec/opa-utils/score" "github.com/armosec/k8s-interface/k8sinterface" @@ -226,17 +225,6 @@ func (opap *OPAProcessor) regoEval(inputObj []map[string]interface{}, compiledRe return results, nil } -func (opap *OPAProcessor) updateScore() { - - if !k8sinterface.ConnectedToCluster { - return - } - - // calculate score - s := score.NewScore(k8sinterface.NewKubernetesApi(), ScoreConfigPath) - s.Calculate(opap.PostureReport.FrameworkReports) -} - func (opap *OPAProcessor) updateResults() { for f := range opap.PostureReport.FrameworkReports { // set exceptions diff --git a/policyhandler/handlenotification.go b/policyhandler/handlenotification.go index 76ecbb6e..3a3cfdd5 100644 --- a/policyhandler/handlenotification.go +++ b/policyhandler/handlenotification.go @@ -4,30 +4,25 @@ import ( "fmt" "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/resourcehandler" "github.com/armosec/opa-utils/reporthandling" "github.com/armosec/armoapi-go/armotypes" - "github.com/armosec/k8s-interface/k8sinterface" ) -var supportedFrameworks = []reporthandling.PolicyIdentifier{ - {Kind: "Framework", Name: "nsa"}, - {Kind: "Framework", Name: "mitre"}, -} - // PolicyHandler - type PolicyHandler struct { - k8s *k8sinterface.KubernetesApi + resourceHandler resourcehandler.IResourceHandler // we are listening on this chan in opaprocessor/processorhandler.go/ProcessRulesListenner func processPolicy *chan *cautils.OPASessionObj getters *cautils.Getters } // CreatePolicyHandler Create ws-handler obj -func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, k8s *k8sinterface.KubernetesApi) *PolicyHandler { +func NewPolicyHandler(processPolicy *chan *cautils.OPASessionObj, resourceHandler resourcehandler.IResourceHandler) *PolicyHandler { return &PolicyHandler{ - k8s: k8s, - processPolicy: processPolicy, + resourceHandler: resourceHandler, + processPolicy: processPolicy, } } @@ -82,16 +77,7 @@ func (policyHandler *PolicyHandler) getPolicies(notification *reporthandling.Pol } func (policyHandler *PolicyHandler) getResources(notification *reporthandling.PolicyNotification, opaSessionObj *cautils.OPASessionObj, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) { - var k8sResources *cautils.K8SResources - var err error - if k8sinterface.ConnectedToCluster { // TODO - use interface - if opaSessionObj.PostureReport.ClusterAPIServerInfo, err = policyHandler.k8s.KubernetesClient.Discovery().ServerVersion(); err != nil { - cautils.ErrorDisplay(fmt.Sprintf("Failed to discover API server inforamtion: %v", err)) - } - k8sResources, err = policyHandler.getK8sResources(opaSessionObj.Frameworks, ¬ification.Designators, scanInfo.ExcludedNamespaces) - } else { - k8sResources, err = policyHandler.loadResources(opaSessionObj.Frameworks, scanInfo) - } - return k8sResources, err + opaSessionObj.PostureReport.ClusterAPIServerInfo = policyHandler.resourceHandler.GetClusterAPIServerInfo() + return policyHandler.resourceHandler.GetResources(opaSessionObj.Frameworks, ¬ification.Designators) } diff --git a/policyhandler/filesloader.go b/resourcehandler/filesloader.go similarity index 88% rename from policyhandler/filesloader.go rename to resourcehandler/filesloader.go index 60f40f5a..eabb9109 100644 --- a/policyhandler/filesloader.go +++ b/resourcehandler/filesloader.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "bytes" @@ -8,7 +8,9 @@ import ( "path/filepath" "strings" + "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/k8s-interface/workloadinterface" + "k8s.io/apimachinery/pkg/version" "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/cautils" @@ -29,11 +31,22 @@ const ( JSON_FILE_FORMAT FileFormat = "json" ) -func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Framework, scanInfo *cautils.ScanInfo) (*cautils.K8SResources, error) { +// FileResourceHandler handle resources from files and URLs +type FileResourceHandler struct { + inputPatterns []string +} + +func NewFileResourceHandler(inputPatterns []string) *FileResourceHandler { + return &FileResourceHandler{ + inputPatterns: inputPatterns, + } +} + +func (fileHandler *FileResourceHandler) GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) { workloads := []k8sinterface.IWorkload{} // load resource from local file system - w, err := loadResourcesFromFiles(scanInfo.InputPatterns) + w, err := loadResourcesFromFiles(fileHandler.inputPatterns) if err != nil { return nil, err } @@ -42,7 +55,7 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr } // load resources from url - w, err = loadResourcesFromUrl(scanInfo.InputPatterns) + w, err = loadResourcesFromUrl(fileHandler.inputPatterns) if err != nil { return nil, err } @@ -59,7 +72,7 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr // build resources map // map resources based on framework required resources: map["/group/version/kind"][] - k8sResources := setResourceMap(frameworks) + k8sResources := setResourceMap(frameworks) // TODO - support designators // save only relevant resources for i := range allResources { @@ -72,6 +85,10 @@ func (policyHandler *PolicyHandler) loadResources(frameworks []reporthandling.Fr } +func (fileHandler *FileResourceHandler) GetClusterAPIServerInfo() *version.Info { + return nil +} + func loadResourcesFromFiles(inputPatterns []string) ([]k8sinterface.IWorkload, error) { files, errs := listFiles(inputPatterns) if len(errs) > 0 { diff --git a/policyhandler/filesloader_test.go b/resourcehandler/filesloader_test.go similarity index 98% rename from policyhandler/filesloader_test.go rename to resourcehandler/filesloader_test.go index f2bea56b..04d64082 100644 --- a/policyhandler/filesloader_test.go +++ b/resourcehandler/filesloader_test.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "fmt" diff --git a/policyhandler/k8sresources.go b/resourcehandler/k8sresources.go similarity index 61% rename from policyhandler/k8sresources.go rename to resourcehandler/k8sresources.go index 28f17577..74bc4922 100644 --- a/policyhandler/k8sresources.go +++ b/resourcehandler/k8sresources.go @@ -1,6 +1,7 @@ -package policyhandler +package resourcehandler import ( + "context" "fmt" "strings" @@ -15,12 +16,23 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/dynamic" ) -const SelectAllResources = "*" +type K8sResourceHandler struct { + k8s *k8sinterface.KubernetesApi + excludedNamespaces string // excluded namespaces (separated by comma) +} -func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator, excludedNamespaces string) (*cautils.K8SResources, error) { +func NewK8sResourceHandler(k8s *k8sinterface.KubernetesApi, excludedNamespaces string) *K8sResourceHandler { + return &K8sResourceHandler{ + k8s: k8s, + excludedNamespaces: excludedNamespaces, + } +} + +func (k8sHandler *K8sResourceHandler) GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) { // get k8s resources cautils.ProgressTextDisplay("Accessing Kubernetes objects") @@ -31,7 +43,7 @@ func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling. _, namespace, labels := armotypes.DigestPortalDesignator(designator) // pull k8s recourses - if err := policyHandler.pullResources(k8sResourcesMap, namespace, labels, excludedNamespaces); err != nil { + if err := k8sHandler.pullResources(k8sResourcesMap, namespace, labels, k8sHandler.excludedNamespaces); err != nil { return k8sResourcesMap, err } @@ -39,13 +51,21 @@ func (policyHandler *PolicyHandler) getK8sResources(frameworks []reporthandling. return k8sResourcesMap, nil } -func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string, excludedNamespaces string) error { +func (k8sHandler *K8sResourceHandler) GetClusterAPIServerInfo() *version.Info { + clusterAPIServerInfo, err := k8sHandler.k8s.KubernetesClient.Discovery().ServerVersion() + if err != nil { + cautils.ErrorDisplay(fmt.Sprintf("Failed to discover API server information: %v", err)) + return nil + } + return clusterAPIServerInfo +} +func (k8sHandler *K8sResourceHandler) pullResources(k8sResources *cautils.K8SResources, namespace string, labels map[string]string, excludedNamespaces string) error { var errs error for groupResource := range *k8sResources { apiGroup, apiVersion, resource := k8sinterface.StringToResourceGroup(groupResource) gvr := schema.GroupVersionResource{Group: apiGroup, Version: apiVersion, Resource: resource} - result, err := policyHandler.pullSingleResource(&gvr, namespace, labels, excludedNamespaces) + result, err := k8sHandler.pullSingleResource(&gvr, namespace, labels, excludedNamespaces) if err != nil { // handle error if errs == nil { @@ -61,7 +81,7 @@ func (policyHandler *PolicyHandler) pullResources(k8sResources *cautils.K8SResou return errs } -func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string, excludedNamespaces string) ([]unstructured.Unstructured, error) { +func (k8sHandler *K8sResourceHandler) pullSingleResource(resource *schema.GroupVersionResource, namespace string, labels map[string]string, excludedNamespaces string) ([]unstructured.Unstructured, error) { // set labels listOptions := metav1.ListOptions{} @@ -76,13 +96,13 @@ func (policyHandler *PolicyHandler) pullSingleResource(resource *schema.GroupVer // set dynamic object var clientResource dynamic.ResourceInterface if namespace != "" && k8sinterface.IsNamespaceScope(resource.Group, resource.Resource) { - clientResource = policyHandler.k8s.DynamicClient.Resource(*resource).Namespace(namespace) + clientResource = k8sHandler.k8s.DynamicClient.Resource(*resource).Namespace(namespace) } else { - clientResource = policyHandler.k8s.DynamicClient.Resource(*resource) + clientResource = k8sHandler.k8s.DynamicClient.Resource(*resource) } // list resources - result, err := clientResource.List(policyHandler.k8s.Context, listOptions) + result, err := clientResource.List(context.Background(), listOptions) if err != nil { return nil, fmt.Errorf("failed to get resource: %v, namespace: %s, labelSelector: %v, reason: %s", resource, namespace, listOptions.LabelSelector, err.Error()) } diff --git a/policyhandler/k8sresourcesutils.go b/resourcehandler/k8sresourcesutils.go similarity index 98% rename from policyhandler/k8sresourcesutils.go rename to resourcehandler/k8sresourcesutils.go index 48932a42..2c14b05b 100644 --- a/policyhandler/k8sresourcesutils.go +++ b/resourcehandler/k8sresourcesutils.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "github.com/armosec/kubescape/cautils" diff --git a/policyhandler/k8sresourcesutils_test.go b/resourcehandler/k8sresourcesutils_test.go similarity index 98% rename from policyhandler/k8sresourcesutils_test.go rename to resourcehandler/k8sresourcesutils_test.go index 63239047..60f38936 100644 --- a/policyhandler/k8sresourcesutils_test.go +++ b/resourcehandler/k8sresourcesutils_test.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "github.com/armosec/k8s-interface/k8sinterface" diff --git a/policyhandler/repositoryscanner.go b/resourcehandler/repositoryscanner.go similarity index 99% rename from policyhandler/repositoryscanner.go rename to resourcehandler/repositoryscanner.go index a568e8c5..7b44017d 100644 --- a/policyhandler/repositoryscanner.go +++ b/resourcehandler/repositoryscanner.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "encoding/json" diff --git a/resourcehandler/resourceshandler.go b/resourcehandler/resourceshandler.go new file mode 100644 index 00000000..c2ddf15f --- /dev/null +++ b/resourcehandler/resourceshandler.go @@ -0,0 +1,13 @@ +package resourcehandler + +import ( + "github.com/armosec/armoapi-go/armotypes" + "github.com/armosec/kubescape/cautils" + "github.com/armosec/opa-utils/reporthandling" + "k8s.io/apimachinery/pkg/version" +) + +type IResourceHandler interface { + GetResources(frameworks []reporthandling.Framework, designator *armotypes.PortalDesignator) (*cautils.K8SResources, error) + GetClusterAPIServerInfo() *version.Info +} diff --git a/policyhandler/urlloader.go b/resourcehandler/urlloader.go similarity index 98% rename from policyhandler/urlloader.go rename to resourcehandler/urlloader.go index faa8f634..96fbdc0d 100644 --- a/policyhandler/urlloader.go +++ b/resourcehandler/urlloader.go @@ -1,4 +1,4 @@ -package policyhandler +package resourcehandler import ( "bytes" diff --git a/resultshandling/printer/jsonprinter.go b/resultshandling/printer/jsonprinter.go new file mode 100644 index 00000000..ff80feb8 --- /dev/null +++ b/resultshandling/printer/jsonprinter.go @@ -0,0 +1,34 @@ +package printer + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" +) + +type JsonPrinter struct { + writer *os.File +} + +func NewJsonPrinter() *JsonPrinter { + return &JsonPrinter{} +} + +func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) { + jsonPrinter.writer = getWriter(outputFile) +} + +func (jsonPrinter *JsonPrinter) Score(score float32) { + fmt.Printf("\nFinal score: %d", int(score*100)) +} + +func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0]) + if err != nil { + fmt.Println("Failed to convert posture report object!") + os.Exit(1) + } + jsonPrinter.writer.Write(postureReportStr) +} diff --git a/resultshandling/printer/junit.go b/resultshandling/printer/junit.go index 370a5d0a..ac03753d 100644 --- a/resultshandling/printer/junit.go +++ b/resultshandling/printer/junit.go @@ -3,10 +3,42 @@ package printer import ( "encoding/xml" "fmt" + "os" + "github.com/armosec/kubescape/cautils" "github.com/armosec/opa-utils/reporthandling" ) +type JunitPrinter struct { + writer *os.File +} + +func NewJunitPrinter() *JunitPrinter { + return &JunitPrinter{} +} + +func (junitPrinter *JunitPrinter) SetWriter(outputFile string) { + junitPrinter.writer = getWriter(outputFile) +} + +func (junitPrinter *JunitPrinter) Score(score float32) { + fmt.Printf("\nFinal score: %d", int(score*100)) +} + +func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport) + if err != nil { + fmt.Println("Failed to convert posture report object!") + os.Exit(1) + } + postureReportStr, err := xml.Marshal(junitResult) + if err != nil { + fmt.Println("Failed to convert posture report object!") + os.Exit(1) + } + junitPrinter.writer.Write(postureReportStr) +} + type JUnitTestSuites struct { XMLName xml.Name `xml:"testsuites"` Suites []JUnitTestSuite `xml:"testsuite"` @@ -74,9 +106,9 @@ func convertPostureReportToJunitResult(postureResult *reporthandling.PostureRepo testCase.Time = "0" if 0 < len(controlReports.RuleReports[0].RuleResponses) { - testCase.Resources = framework.GetNumberOfResources() - testCase.Excluded = framework.GetNumberOfWarningResources() - testCase.Failed = framework.GetNumberOfFailedResources() + testCase.Resources = controlReports.GetNumberOfResources() + testCase.Excluded = controlReports.GetNumberOfWarningResources() + testCase.Failed = controlReports.GetNumberOfFailedResources() failure := JUnitFailure{} failure.Message = fmt.Sprintf("%d resources failed", testCase.Failed) for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses { diff --git a/resultshandling/printer/prettyprinter.go b/resultshandling/printer/prettyprinter.go new file mode 100644 index 00000000..1d4b366b --- /dev/null +++ b/resultshandling/printer/prettyprinter.go @@ -0,0 +1,219 @@ +package printer + +import ( + "fmt" + "os" + "sort" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/opa-utils/reporthandling" + "github.com/enescakir/emoji" + "github.com/olekukonko/tablewriter" +) + +type PrettyPrinter struct { + writer *os.File + summary Summary + sortedControlNames []string + frameworkSummary ControlSummary +} + +func NewPrettyPrinter() *PrettyPrinter { + return &PrettyPrinter{ + summary: NewSummary(), + } +} + +func (printer *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + // score := calculatePostureScore(opaSessionObj.PostureReport) + + printer.summarySetup(opaSessionObj.PostureReport) + printer.printResults() + printer.printSummaryTable() + + // return score +} + +func (printer *PrettyPrinter) SetWriter(outputFile string) { + printer.writer = getWriter(outputFile) +} + +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(), + } + for _, cr := range fr.ControlReports { + if len(cr.RuleReports) == 0 { + continue + } + workloadsSummary := listResultSummary(cr.RuleReports) + + printer.summary[cr.Name] = ControlSummary{ + TotalResources: cr.GetNumberOfResources(), + TotalFailed: cr.GetNumberOfFailedResources(), + TotalWarnign: cr.GetNumberOfWarningResources(), + FailedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryFailed), + ExcludedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryExclude), + Description: cr.Description, + Remediation: cr.Remediation, + ListInputKinds: cr.ListControlsInputKinds(), + } + } + } + printer.sortedControlNames = printer.getSortedControlsNames() +} +func (printer *PrettyPrinter) printResults() { + for i := 0; i < len(printer.sortedControlNames); i++ { + controlSummary := printer.summary[printer.sortedControlNames[i]] + printer.printTitle(printer.sortedControlNames[i], &controlSummary) + printer.printResources(&controlSummary) + if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 { + printer.printSummary(printer.sortedControlNames[i], &controlSummary) + } + + } +} + +func (printer *PrettyPrinter) printSummary(controlName string, controlSummary *ControlSummary) { + cautils.SimpleDisplay(printer.writer, "Summary - ") + cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarnign) + cautils.WarningDisplay(printer.writer, "Excluded:%v ", controlSummary.TotalWarnign) + cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed) + cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources) + if controlSummary.TotalFailed > 0 { + cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation) + } + cautils.DescriptionDisplay(printer.writer, "\n") + +} + +func (printer *PrettyPrinter) printTitle(controlName string, controlSummary *ControlSummary) { + cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName) + if controlSummary.TotalResources == 0 { + cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace) + } else if controlSummary.TotalFailed != 0 { + cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace) + } else if controlSummary.TotalWarnign != 0 { + cautils.WarningDisplay(printer.writer, "excluded %v\n", emoji.NeutralFace) + } else { + cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp) + } + + cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description) + +} +func (printer *PrettyPrinter) printResources(controlSummary *ControlSummary) { + + if len(controlSummary.FailedWorkloads) > 0 { + cautils.FailureDisplay(printer.writer, "Failed:\n") + printer.printGroupedResources(controlSummary.FailedWorkloads) + } + if len(controlSummary.ExcludedWorkloads) > 0 { + cautils.WarningDisplay(printer.writer, "Excluded:\n") + printer.printGroupedResources(controlSummary.ExcludedWorkloads) + } + +} + +func (printer *PrettyPrinter) printGroupedResources(workloads map[string][]WorkloadSummary) { + + indent := INDENT + + for ns, rsc := range workloads { + preIndent := indent + if ns != "" { + cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns) + } + preIndent2 := indent + for r := range rsc { + indent += indent + cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name)) + indent = preIndent2 + } + indent = preIndent + } + +} + +func generateRow(control string, cs ControlSummary) []string { + row := []string{control} + row = append(row, cs.ToSlice()...) + if cs.TotalResources != 0 { + row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%")) + } else { + row = append(row, EmptyPercentage) + } + return row +} + +func generateHeader() []string { + return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% success"} +} + +func percentage(big, small int) int { + if big == 0 { + if small == 0 { + return 100 + } + return 0 + } + return int(float64(float64(big-small)/float64(big)) * 100) +} +func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string { + // Control name | # failed resources | all resources | % success + row := []string{} + row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers")) + row = append(row, fmt.Sprintf("%d", sumFailed)) + row = append(row, fmt.Sprintf("%d", sumWarning)) + row = append(row, fmt.Sprintf("%d", sumTotal)) + if sumTotal != 0 { + row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%")) + } else { + row = append(row, EmptyPercentage) + } + return row +} +func (printer *PrettyPrinter) printSummaryTable() { + summaryTable := tablewriter.NewWriter(printer.writer) + summaryTable.SetAutoWrapText(false) + summaryTable.SetHeader(generateHeader()) + summaryTable.SetHeaderLine(true) + alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER} + summaryTable.SetColumnAlignment(alignments) + + for i := 0; i < len(printer.sortedControlNames); i++ { + controlSummary := printer.summary[printer.sortedControlNames[i]] + summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary)) + } + summaryTable.SetFooter(generateFooter(len(printer.summary), printer.frameworkSummary.TotalFailed, printer.frameworkSummary.TotalWarnign, printer.frameworkSummary.TotalResources)) + summaryTable.Render() +} + +func (printer *PrettyPrinter) getSortedControlsNames() []string { + controlNames := make([]string, 0, len(printer.summary)) + for k := range printer.summary { + controlNames = append(controlNames, k) + } + sort.Strings(controlNames) + return controlNames +} + +func getWriter(outputFile string) *os.File { + os.Remove(outputFile) + if outputFile != "" { + f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + fmt.Println("Error opening file") + return os.Stdout + } + return f + } + return os.Stdout + +} diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go index 50a624c4..8452c19f 100644 --- a/resultshandling/printer/printresults.go +++ b/resultshandling/printer/printresults.go @@ -1,17 +1,7 @@ package printer import ( - "encoding/json" - "encoding/xml" - "fmt" - "os" - "sort" - "github.com/armosec/kubescape/cautils" - "github.com/armosec/opa-utils/reporthandling" - - "github.com/enescakir/emoji" - "github.com/olekukonko/tablewriter" ) var INDENT = " " @@ -19,253 +9,24 @@ var INDENT = " " const EmptyPercentage = "NaN" const ( - PrettyPrinter string = "pretty-printer" - JsonPrinter string = "json" + PrettyFormat string = "pretty-printer" + JsonFormat string = "json" JunitResultPrinter string = "junit" ) -type Printer struct { - writer *os.File - summary Summary - sortedControlNames []string - printerType string - frameworkSummary ControlSummary +type IPrinter interface { + ActionPrint(opaSessionObj *cautils.OPASessionObj) + SetWriter(outputFile string) + Score(score float32) } -func NewPrinter(printerType, outputFile string) *Printer { - return &Printer{ - summary: NewSummary(), - writer: getWriter(outputFile), - printerType: printerType, +func GetPrinter(printFormat string) IPrinter { + switch printFormat { + case JsonFormat: + return NewJsonPrinter() + case JunitResultPrinter: + return NewJunitPrinter() + default: + return NewPrettyPrinter() } } - -func calculatePostureScore(postureReport *reporthandling.PostureReport) float32 { - totalResources := 0 - totalFailed := 0 - for _, frameworkReport := range postureReport.FrameworkReports { - totalFailed += frameworkReport.GetNumberOfFailedResources() - totalResources += frameworkReport.GetNumberOfResources() - } - if totalResources == 0 { - return float32(0) - } - return (float32(totalResources) - float32(totalFailed)) / float32(totalResources) -} - -func (printer *Printer) ActionPrint(opaSessionObj *cautils.OPASessionObj) float32 { - score := calculatePostureScore(opaSessionObj.PostureReport) - - if printer.printerType == PrettyPrinter { - printer.SummarySetup(opaSessionObj.PostureReport) - printer.PrintResults() - printer.PrintSummaryTable() - } else if printer.printerType == JsonPrinter { - postureReportStr, err := json.Marshal(opaSessionObj.PostureReport.FrameworkReports[0]) - if err != nil { - fmt.Println("Failed to convert posture report object!") - os.Exit(1) - } - printer.writer.Write(postureReportStr) - fmt.Printf("\nFinal score: %d\n", int(score*100)) - } else if printer.printerType == JunitResultPrinter { - junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport) - if err != nil { - fmt.Println("Failed to convert posture report object!") - os.Exit(1) - } - postureReportStr, err := xml.Marshal(junitResult) - if err != nil { - fmt.Println("Failed to convert posture report object!") - os.Exit(1) - } - printer.writer.Write(postureReportStr) - fmt.Printf("\nFinal score: %d\n", int(score*100)) - } else if !cautils.IsSilent() { - fmt.Println("unknown output printer") - os.Exit(1) - } - - return score -} - -func (printer *Printer) SummarySetup(postureReport *reporthandling.PostureReport) { - for _, fr := range postureReport.FrameworkReports { - printer.frameworkSummary = ControlSummary{ - TotalResources: fr.GetNumberOfResources(), - TotalFailed: fr.GetNumberOfFailedResources(), - TotalWarnign: fr.GetNumberOfWarningResources(), - } - for _, cr := range fr.ControlReports { - if len(cr.RuleReports) == 0 { - continue - } - workloadsSummary := listResultSummary(cr.RuleReports) - - printer.summary[cr.Name] = ControlSummary{ - TotalResources: cr.GetNumberOfResources(), - TotalFailed: cr.GetNumberOfFailedResources(), - TotalWarnign: cr.GetNumberOfWarningResources(), - FailedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryFailed), - ExcludedWorkloads: groupByNamespace(workloadsSummary, workloadSummaryExclude), - Description: cr.Description, - Remediation: cr.Remediation, - ListInputKinds: cr.ListControlsInputKinds(), - } - } - } - printer.sortedControlNames = printer.getSortedControlsNames() -} -func (printer *Printer) PrintResults() { - for i := 0; i < len(printer.sortedControlNames); i++ { - controlSummary := printer.summary[printer.sortedControlNames[i]] - printer.printTitle(printer.sortedControlNames[i], &controlSummary) - printer.printResources(&controlSummary) - if printer.summary[printer.sortedControlNames[i]].TotalResources > 0 { - printer.printSummary(printer.sortedControlNames[i], &controlSummary) - } - - } -} - -func (printer *Printer) printSummary(controlName string, controlSummary *ControlSummary) { - cautils.SimpleDisplay(printer.writer, "Summary - ") - cautils.SuccessDisplay(printer.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarnign) - cautils.WarningDisplay(printer.writer, "Excluded:%v ", controlSummary.TotalWarnign) - cautils.FailureDisplay(printer.writer, "Failed:%v ", controlSummary.TotalFailed) - cautils.InfoDisplay(printer.writer, "Total:%v\n", controlSummary.TotalResources) - if controlSummary.TotalFailed > 0 { - cautils.DescriptionDisplay(printer.writer, "Remediation: %v\n", controlSummary.Remediation) - } - cautils.DescriptionDisplay(printer.writer, "\n") - -} - -func (printer *Printer) printTitle(controlName string, controlSummary *ControlSummary) { - cautils.InfoDisplay(printer.writer, "[control: %s] ", controlName) - if controlSummary.TotalResources == 0 { - cautils.InfoDisplay(printer.writer, "resources not found %v\n", emoji.ConfusedFace) - } else if controlSummary.TotalFailed != 0 { - cautils.FailureDisplay(printer.writer, "failed %v\n", emoji.SadButRelievedFace) - } else if controlSummary.TotalWarnign != 0 { - cautils.WarningDisplay(printer.writer, "excluded %v\n", emoji.NeutralFace) - } else { - cautils.SuccessDisplay(printer.writer, "passed %v\n", emoji.ThumbsUp) - } - - cautils.DescriptionDisplay(printer.writer, "Description: %s\n", controlSummary.Description) - -} -func (printer *Printer) printResources(controlSummary *ControlSummary) { - - if len(controlSummary.FailedWorkloads) > 0 { - cautils.FailureDisplay(printer.writer, "Failed:\n") - printer.printGroupedResources(controlSummary.FailedWorkloads) - } - if len(controlSummary.ExcludedWorkloads) > 0 { - cautils.WarningDisplay(printer.writer, "Excluded:\n") - printer.printGroupedResources(controlSummary.ExcludedWorkloads) - } - -} - -func (printer *Printer) printGroupedResources(workloads map[string][]WorkloadSummary) { - - indent := INDENT - - for ns, rsc := range workloads { - preIndent := indent - if ns != "" { - cautils.SimpleDisplay(printer.writer, "%sNamespace %s\n", indent, ns) - } - preIndent2 := indent - for r := range rsc { - indent += indent - cautils.SimpleDisplay(printer.writer, fmt.Sprintf("%s%s - %s\n", indent, rsc[r].Kind, rsc[r].Name)) - indent = preIndent2 - } - indent = preIndent - } - -} - -func (printer *Printer) PrintUrl(url string) { - cautils.InfoTextDisplay(printer.writer, url) -} - -func generateRow(control string, cs ControlSummary) []string { - row := []string{control} - row = append(row, cs.ToSlice()...) - if cs.TotalResources != 0 { - row = append(row, fmt.Sprintf("%d%s", percentage(cs.TotalResources, cs.TotalFailed), "%")) - } else { - row = append(row, EmptyPercentage) - } - return row -} - -func generateHeader() []string { - return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% success"} -} - -func percentage(big, small int) int { - if big == 0 { - if small == 0 { - return 100 - } - return 0 - } - return int(float64(float64(big-small)/float64(big)) * 100) -} -func generateFooter(numControlers, sumFailed, sumWarning, sumTotal int) []string { - // Control name | # failed resources | all resources | % success - row := []string{} - row = append(row, "Resource Summary") //fmt.Sprintf(""%d", numControlers")) - row = append(row, fmt.Sprintf("%d", sumFailed)) - row = append(row, fmt.Sprintf("%d", sumWarning)) - row = append(row, fmt.Sprintf("%d", sumTotal)) - if sumTotal != 0 { - row = append(row, fmt.Sprintf("%d%s", percentage(sumTotal, sumFailed), "%")) - } else { - row = append(row, EmptyPercentage) - } - return row -} -func (printer *Printer) PrintSummaryTable() { - summaryTable := tablewriter.NewWriter(printer.writer) - summaryTable.SetAutoWrapText(false) - summaryTable.SetHeader(generateHeader()) - summaryTable.SetHeaderLine(true) - alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER} - summaryTable.SetColumnAlignment(alignments) - - for i := 0; i < len(printer.sortedControlNames); i++ { - controlSummary := printer.summary[printer.sortedControlNames[i]] - summaryTable.Append(generateRow(printer.sortedControlNames[i], controlSummary)) - } - summaryTable.SetFooter(generateFooter(len(printer.summary), printer.frameworkSummary.TotalFailed, printer.frameworkSummary.TotalWarnign, printer.frameworkSummary.TotalResources)) - summaryTable.Render() -} - -func (printer *Printer) getSortedControlsNames() []string { - controlNames := make([]string, 0, len(printer.summary)) - for k := range printer.summary { - controlNames = append(controlNames, k) - } - sort.Strings(controlNames) - return controlNames -} - -func getWriter(outputFile string) *os.File { - os.Remove(outputFile) - if outputFile != "" { - f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - fmt.Println("Error opening file") - return os.Stdout - } - return f - } - return os.Stdout - -} diff --git a/resultshandling/printer/silentprinter.go b/resultshandling/printer/silentprinter.go new file mode 100644 index 00000000..65737b5c --- /dev/null +++ b/resultshandling/printer/silentprinter.go @@ -0,0 +1,11 @@ +package printer + +import ( + "github.com/armosec/kubescape/cautils" +) + +type SilentPrinter struct { +} + +func (silentPrinter *SilentPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { +} diff --git a/resultshandling/reporter/mockreporter.go b/resultshandling/reporter/mockreporter.go new file mode 100644 index 00000000..21e37bb5 --- /dev/null +++ b/resultshandling/reporter/mockreporter.go @@ -0,0 +1,17 @@ +package reporter + +import "github.com/armosec/kubescape/cautils" + +type ReportMock struct { +} + +func NewReportMock() *ReportMock { + return &ReportMock{} +} +func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) {} + +func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) { +} + +func (reportMock *ReportMock) SetClusterName(clusterName string) { +} diff --git a/resultshandling/reporter/reporteventreceiver.go b/resultshandling/reporter/reporteventreceiver.go index 99364328..070ba745 100644 --- a/resultshandling/reporter/reporteventreceiver.go +++ b/resultshandling/reporter/reporteventreceiver.go @@ -5,47 +5,55 @@ import ( "encoding/json" "fmt" "net/http" - "net/url" "github.com/armosec/kubescape/cautils" "github.com/armosec/opa-utils/reporthandling" ) +type IReport interface { + ActionSendReport(opaSessionObj *cautils.OPASessionObj) + SetCustomerGUID(customerGUID string) + SetClusterName(clusterName string) +} + type ReportEventReceiver struct { - httpClient http.Client - host url.URL + httpClient http.Client + clusterName string + customerGUID string } func NewReportEventReceiver() *ReportEventReceiver { - hostURL := initEventReceiverURL() return &ReportEventReceiver{ httpClient: http.Client{}, - host: *hostURL, } } -func (report *ReportEventReceiver) ActionSendReportListenner(opaSessionObj *cautils.OPASessionObj) { - if cautils.CustomerGUID == "" { - return - } - //Add score - +func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASessionObj) { // Remove data before reporting keepFields := []string{"kind", "apiVersion", "metadata"} keepMetadataFields := []string{"name", "namespace", "labels"} opaSessionObj.PostureReport.RemoveData(keepFields, keepMetadataFields) - if err := report.Send(opaSessionObj.PostureReport); err != nil { + if err := report.send(opaSessionObj.PostureReport); err != nil { fmt.Println(err) } } -func (report *ReportEventReceiver) Send(postureReport *reporthandling.PostureReport) error { + +func (report *ReportEventReceiver) SetCustomerGUID(customerGUID string) { + report.customerGUID = customerGUID +} + +func (report *ReportEventReceiver) SetClusterName(clusterName string) { + report.clusterName = clusterName +} + +func (report *ReportEventReceiver) send(postureReport *reporthandling.PostureReport) error { reqBody, err := json.Marshal(*postureReport) if err != nil { return fmt.Errorf("in 'Send' failed to json.Marshal, reason: %v", err) } - host := hostToString(&report.host, postureReport.ReportID) + host := hostToString(report.initEventReceiverURL(), postureReport.ReportID) req, err := http.NewRequest("POST", host, bytes.NewReader(reqBody)) if err != nil { diff --git a/resultshandling/reporter/reporteventreceiverutils.go b/resultshandling/reporter/reporteventreceiverutils.go index 96fa75be..a32a3c22 100644 --- a/resultshandling/reporter/reporteventreceiverutils.go +++ b/resultshandling/reporter/reporteventreceiverutils.go @@ -7,7 +7,6 @@ import ( "net/url" "strings" - "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/getter" "github.com/gofrs/uuid" ) @@ -33,15 +32,15 @@ func httpRespToString(resp *http.Response) (string, error) { return strBuilder.String(), err } -func initEventReceiverURL() *url.URL { +func (report *ReportEventReceiver) initEventReceiverURL() *url.URL { urlObj := url.URL{} urlObj.Scheme = "https" urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL() urlObj.Path = "/k8s/postureReport" q := urlObj.Query() - q.Add("customerGUID", uuid.FromStringOrNil(cautils.CustomerGUID).String()) - q.Add("clusterName", cautils.ClusterName) + q.Add("customerGUID", uuid.FromStringOrNil(report.customerGUID).String()) + q.Add("clusterName", report.clusterName) urlObj.RawQuery = q.Encode() diff --git a/resultshandling/results.go b/resultshandling/results.go index 2db5e8f0..1ba9aacf 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -4,15 +4,16 @@ import ( "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/resultshandling/printer" "github.com/armosec/kubescape/resultshandling/reporter" + "github.com/armosec/opa-utils/reporthandling" ) type ResultsHandler struct { opaSessionObj *chan *cautils.OPASessionObj - reporterObj *reporter.ReportEventReceiver - printerObj *printer.Printer + reporterObj reporter.IReport + printerObj printer.IPrinter } -func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj *reporter.ReportEventReceiver, printerObj *printer.Printer) *ResultsHandler { +func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj reporter.IReport, printerObj printer.IPrinter) *ResultsHandler { return &ResultsHandler{ opaSessionObj: opaSessionObj, reporterObj: reporterObj, @@ -20,16 +21,36 @@ func NewResultsHandler(opaSessionObj *chan *cautils.OPASessionObj, reporterObj * } } -func (resultsHandler *ResultsHandler) HandleResults(scanInfo cautils.ScanInfo) float32 { +func (resultsHandler *ResultsHandler) HandleResults(scanInfo *cautils.ScanInfo) float32 { opaSessionObj := <-*resultsHandler.opaSessionObj - score := resultsHandler.printerObj.ActionPrint(opaSessionObj) + resultsHandler.printerObj.ActionPrint(opaSessionObj) - // Don't send report for control scan - if scanInfo.FrameworkScan { // TODO - use interface for ActionSendReportListenner - resultsHandler.reporterObj.ActionSendReportListenner(opaSessionObj) - } + resultsHandler.reporterObj.ActionSendReport(opaSessionObj) + + // TODO - get score from table + score := CalculatePostureScore(opaSessionObj.PostureReport) + resultsHandler.printerObj.Score(score) return score } + +// CalculatePostureScore calculate final score +func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 { + lowestScore := float32(100) + for _, frameworkReport := range postureReport.FrameworkReports { + totalFailed := frameworkReport.GetNumberOfFailedResources() + totalResources := frameworkReport.GetNumberOfResources() + + frameworkScore := float32(0) + if float32(totalResources) > 0 { + frameworkScore = (float32(totalResources) - float32(totalFailed)) / float32(totalResources) + } + if lowestScore > frameworkScore { + lowestScore = frameworkScore + } + } + + return lowestScore +}