From b4d712fcb157f939e15ec9eac81348368c2db6be Mon Sep 17 00:00:00 2001 From: Bezalel Brandwine Date: Thu, 10 Feb 2022 17:38:13 +0200 Subject: [PATCH 01/44] add explicit protocol to solve known K8s issue: https://github.com/kubernetes/kubernetes/issues/92332 --- hostsensorutils/hostsensor.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/hostsensorutils/hostsensor.yaml b/hostsensorutils/hostsensor.yaml index bc03b94c..5647a162 100644 --- a/hostsensorutils/hostsensor.yaml +++ b/hostsensorutils/hostsensor.yaml @@ -40,6 +40,7 @@ spec: - name: http hostPort: 7888 containerPort: 7888 + protocol: TCP resources: limits: cpu: 0.1m From 46001e4761f1c8e6f74a585880c26765e9de1e7d Mon Sep 17 00:00:00 2001 From: avicoder Date: Fri, 11 Feb 2022 03:17:10 +0800 Subject: [PATCH 02/44] [Minor] Fixed a Typo Useage to Usage --- clihandler/clilist.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clihandler/clilist.go b/clihandler/clilist.go index dda856ee..928d262b 100644 --- a/clihandler/clilist.go +++ b/clihandler/clilist.go @@ -32,7 +32,7 @@ func CliList(listPolicies *cliobjects.ListPolicies) error { sep := "\n * " usageCmd := strings.TrimSuffix(listPolicies.Target, "s") fmt.Printf("Supported %s:%s%s\n", listPolicies.Target, sep, strings.Join(policies, sep)) - fmt.Printf("\nUseage:\n") + fmt.Printf("\nUsage:\n") fmt.Printf("$ kubescape scan %s \"name\"\n", usageCmd) fmt.Printf("$ kubescape scan %s \"name-0\",\"name-1\"\n\n", usageCmd) return nil From bf7505934749394006f4af174f4843e69ddc4c14 Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 15 Feb 2022 14:25:28 +0200 Subject: [PATCH 03/44] update after scan messgae --- cautils/getter/armoapi.go | 2 ++ clihandler/initcliutils.go | 6 ++-- resourcehandler/k8sresources.go | 16 ++++++++++- resultshandling/reporter/v1/mockreporter.go | 2 +- .../reporter/v1/reporteventreceiver.go | 2 +- .../reporter/v2/reporteventreceiver.go | 28 +++++++++++-------- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/cautils/getter/armoapi.go b/cautils/getter/armoapi.go index cc6a1fd8..0534f12b 100644 --- a/cautils/getter/armoapi.go +++ b/cautils/getter/armoapi.go @@ -11,6 +11,7 @@ import ( "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/opa-utils/reporthandling" ) @@ -50,6 +51,7 @@ type ArmoAPI struct { var globalArmoAPIConnector *ArmoAPI func SetARMOAPIConnector(armoAPI *ArmoAPI) { + logger.L().Debug("Armo URLs", helpers.String("api", armoAPI.apiURL), helpers.String("auth", armoAPI.authURL), helpers.String("report", armoAPI.erURL), helpers.String("UI", armoAPI.feURL)) globalArmoAPIConnector = armoAPI } diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index a03a5d62..1a364ea1 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -216,8 +216,8 @@ func getDefaultFrameworksPaths() []string { func listFrameworksNames(policyGetter getter.IPolicyGetter) []string { fw, err := policyGetter.ListFrameworks() - if err != nil { - fw = getDefaultFrameworksPaths() + if err == nil { + return fw } - return fw + return getter.NativeFrameworks } diff --git a/resourcehandler/k8sresources.go b/resourcehandler/k8sresources.go index 2e41daea..0a1fbba2 100644 --- a/resourcehandler/k8sresources.go +++ b/resourcehandler/k8sresources.go @@ -168,6 +168,18 @@ func ConvertMapListToMeta(resourceMap []map[string]interface{}) []workloadinterf return workloads } +// func (k8sHandler *K8sResourceHandler) collectHostResourcesAPI(allResources map[string]workloadinterface.IMetadata, resourcesMap *cautils.K8SResources) error { + +// HostSensorAPI := map[string]string{ +// "bla/v1": "", +// } +// for apiVersion := range allResources { +// if HostSensorAPI == apiVersion { +// k8sHandler.collectHostResources() +// } +// } +// return nil +// } func (k8sHandler *K8sResourceHandler) collectHostResources(allResources map[string]workloadinterface.IMetadata, resourcesMap *cautils.K8SResources) error { logger.L().Debug("Collecting host sensor resources") @@ -175,6 +187,7 @@ func (k8sHandler *K8sResourceHandler) collectHostResources(allResources map[stri if err != nil { return err } + for rscIdx := range hostResources { group, version := getGroupNVersion(hostResources[rscIdx].GetApiVersion()) groupResource := k8sinterface.JoinResourceTriplets(group, version, hostResources[rscIdx].GetKind()) @@ -220,9 +233,10 @@ func getCloudProviderDescription(allResources map[string]workloadinterface.IMeta if err != nil { return err } - logger.L().Debug("cloud", helpers.String("cluster", cluster), helpers.String("clusterName", clusterName), helpers.String("provider", provider), helpers.String("region", region), helpers.String("project", project)) if provider != "" { + logger.L().Debug("cloud", helpers.String("cluster", cluster), helpers.String("clusterName", clusterName), helpers.String("provider", provider), helpers.String("region", region), helpers.String("project", project)) + wl, err := cloudsupport.GetDescriptiveInfoFromCloudProvider(clusterName, provider, region, project) if err != nil { // Return error with useful info on how to configure credentials for getting cloud provider info diff --git a/resultshandling/reporter/v1/mockreporter.go b/resultshandling/reporter/v1/mockreporter.go index 451a59f4..8ffc01fe 100644 --- a/resultshandling/reporter/v1/mockreporter.go +++ b/resultshandling/reporter/v1/mockreporter.go @@ -25,6 +25,6 @@ func (reportMock *ReportMock) SetClusterName(clusterName string) { } func (reportMock *ReportMock) DisplayReportURL() { - message := fmt.Sprintf("\nYou can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here: https://%s/cli-signup \n", getter.GetArmoAPIConnector().GetFrontendURL()) + message := fmt.Sprintf("\nScan results have not been submitted.\nYou can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here: https://%s/cli-signup \n", getter.GetArmoAPIConnector().GetFrontendURL()) cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n%s\n", message)) } diff --git a/resultshandling/reporter/v1/reporteventreceiver.go b/resultshandling/reporter/v1/reporteventreceiver.go index 4b815b65..cc2cd34a 100644 --- a/resultshandling/reporter/v1/reporteventreceiver.go +++ b/resultshandling/reporter/v1/reporteventreceiver.go @@ -144,7 +144,7 @@ func (report *ReportEventReceiver) generateMessage() { if report.customerAdminEMail != "" { logger.L().Debug("", helpers.String("account ID", report.customerGUID)) - report.message = fmt.Sprintf("%s %s/risk/%s", message, u.String(), report.clusterName) + report.message = fmt.Sprintf("%s %s/configuration-scanning/%s", message, u.String(), report.clusterName) return } u.Path = "account/sign-up" diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index db29b77f..77ba546e 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -60,6 +60,8 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe } else { report.generateMessage() } + logger.L().Debug("", helpers.String("account ID", report.customerGUID)) + return nil } @@ -202,24 +204,28 @@ func (report *ReportEventReceiver) sendReport(host string, postureReport *report } func (report *ReportEventReceiver) generateMessage() { - message := "You can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here:" u := url.URL{} u.Scheme = "https" u.Host = getter.GetArmoAPIConnector().GetFrontendURL() - if report.customerAdminEMail != "" { - logger.L().Debug("", helpers.String("account ID", report.customerGUID)) - report.message = fmt.Sprintf("%s %s/risk/%s", message, u.String(), report.clusterName) - return + if report.customerAdminEMail != "" { // data has been submitted + u.Path = fmt.Sprintf("configuration-scanning/%s", report.clusterName) + } else { + u.Path = "account/sign-up" + q := u.Query() + q.Add("invitationToken", report.token) + q.Add("customerGUID", report.customerGUID) + + u.RawQuery = q.Encode() } - u.Path = "account/sign-up" - q := u.Query() - q.Add("invitationToken", report.token) - q.Add("customerGUID", report.customerGUID) + + sep := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + report.message = sep + report.message += " << WOW! Now you can see the scan results on the web >>\n\n" + report.message += fmt.Sprintf(" %s\n", u.String()) + report.message += sep - u.RawQuery = q.Encode() - report.message = fmt.Sprintf("%s %s", message, u.String()) } func (report *ReportEventReceiver) DisplayReportURL() { From 9de73dab29ad943a978acdfd012772c2f840371e Mon Sep 17 00:00:00 2001 From: dwertent Date: Wed, 16 Feb 2022 13:15:44 +0200 Subject: [PATCH 04/44] updalead sha --- .github/workflows/build.yaml | 14 +++++++++++++- build.py | 19 +++++++++++++------ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d5bba495..7523cbb4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -53,7 +53,7 @@ jobs: KUBESCAPE_SKIP_UPDATE_CHECK: "true" run: python3 smoke_testing/init.py ${PWD}/build/${{ matrix.os }}/kubescape - - name: Upload Release binaries + - name: Upload release binaries id: upload-release-asset uses: actions/upload-release-asset@v1 env: @@ -64,6 +64,18 @@ jobs: asset_name: kubescape-${{ matrix.os }} asset_content_type: application/octet-stream + - name: Upload release hash + id: upload-release-hash + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.once.outputs.upload_url }} + asset_path: build/${{ matrix.os }}/kubescape.sha256 + asset_name: kubescape-${{ matrix.os }}-sha256 + asset_content_type: application/octet-stream + + build-docker: name: Build docker container, tag and upload to registry diff --git a/build.py b/build.py index fc8a19d3..abf816ff 100644 --- a/build.py +++ b/build.py @@ -49,6 +49,9 @@ def main(): ArmoWebsite = os.getenv("ArmoWebsite") ArmoAuthServer = os.getenv("ArmoAuthServer") + ks_file = os.path.join(buildDir, packageName) + hash_file = os.path.join(buildDir, packageName + ".sha256") + # Create build directory buildDir = getBuildDir() @@ -60,14 +63,18 @@ def main(): % (buildUrl, releaseVersion, BE_SERVER_CONST, ArmoBEServer, ER_SERVER_CONST, ArmoERServer, WEBSITE_CONST, ArmoWebsite, AUTH_SERVER_CONST, ArmoAuthServer) - status = subprocess.call(["go", "build", "-o", "%s/%s" % (buildDir, packageName), "-ldflags" ,ldflags]) + + print("Building kubescape and saving here: {}".format(ks_file)) + status = subprocess.call(["go", "build", "-o", ks_file, "-ldflags" ,ldflags]) checkStatus(status, "Failed to build kubescape") - sha1 = hashlib.sha1() - with open(buildDir + "/" + packageName, "rb") as kube: - sha1.update(kube.read()) - with open(buildDir + "/" + packageName + ".sha1", "w") as kube_sha: - kube_sha.write(sha1.hexdigest()) + sha256 = hashlib.sha256() + with open(ks_file, "rb") as kube: + sha256.update(kube.read()) + with open(hash_file, "w") as kube_sha: + hash = sha256.hexdigest() + print("kubescape hash: {}, file: {}".format(hash, hash_file)) + kube_sha.write(sha256.hexdigest()) print("Build Done") From d288fdc7f2df8d5746a7a181f94ade3266ef0bb9 Mon Sep 17 00:00:00 2001 From: dwertent Date: Wed, 16 Feb 2022 13:59:29 +0200 Subject: [PATCH 05/44] set tenant only when submiiting data --- cautils/customerloader.go | 12 +++++++----- cautils/getter/armoapiutils.go | 17 ++++++++++++----- clihandler/clidownload.go | 4 ++++ clihandler/clilist.go | 1 + clihandler/clisubmit.go | 3 ++- clihandler/cmd/rbac.go | 1 + clihandler/cmd/results.go | 1 + 7 files changed, 28 insertions(+), 11 deletions(-) diff --git a/cautils/customerloader.go b/cautils/customerloader.go index adec4e65..174b3380 100644 --- a/cautils/customerloader.go +++ b/cautils/customerloader.go @@ -118,6 +118,7 @@ func NewLocalConfig(backendAPI getter.IBackend, customerGUID, clusterName string logger.L().Error(err.Error()) } } + lc.UpdateCachedConfig() return lc } @@ -228,11 +229,12 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken c.backendAPI.SetClientID(c.configObj.ClientID) c.backendAPI.SetSecretKey(c.configObj.SecretKey) - if c.configObj.AccountID != "" { - if err := c.SetTenant(); err != nil { - logger.L().Error(err.Error()) - } - } + // if c.configObj.AccountID != "" { + // if err := c.SetTenant(); err != nil { + // logger.L().Error(err.Error()) + // } + // } + c.UpdateCachedConfig() return c } diff --git a/cautils/getter/armoapiutils.go b/cautils/getter/armoapiutils.go index 39674036..0b213b48 100644 --- a/cautils/getter/armoapiutils.go +++ b/cautils/getter/armoapiutils.go @@ -17,7 +17,7 @@ func (armoAPI *ArmoAPI) getFrameworkURL(frameworkName string) string { u.Host = armoAPI.apiURL u.Path = "api/v1/armoFrameworks" q := u.Query() - q.Add("customerGUID", armoAPI.accountID) + q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) if isNativeFramework(frameworkName) { q.Add("frameworkName", strings.ToUpper(frameworkName)) } else { @@ -35,7 +35,7 @@ func (armoAPI *ArmoAPI) getListFrameworkURL() string { u.Host = armoAPI.apiURL u.Path = "api/v1/armoFrameworks" q := u.Query() - q.Add("customerGUID", armoAPI.accountID) + q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) u.RawQuery = q.Encode() return u.String() @@ -47,7 +47,7 @@ func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string { u.Path = "api/v1/armoPostureExceptions" q := u.Query() - q.Add("customerGUID", armoAPI.accountID) + q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) // if clusterName != "" { // TODO - fix customer name support in Armo BE // q.Add("clusterName", clusterName) // } @@ -63,7 +63,7 @@ func (armoAPI *ArmoAPI) postExceptionsURL() string { u.Path = "api/v1/postureExceptionPolicy" q := u.Query() - q.Add("customerGUID", armoAPI.accountID) + q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) u.RawQuery = q.Encode() return u.String() @@ -76,7 +76,7 @@ func (armoAPI *ArmoAPI) getAccountConfig(clusterName string) string { u.Path = "api/v1/armoCustomerConfiguration" q := u.Query() - q.Add("customerGUID", armoAPI.accountID) + q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) if clusterName != "" { // TODO - fix customer name support in Armo BE q.Add("clusterName", clusterName) } @@ -156,3 +156,10 @@ func (armoAPI *ArmoAPI) appendAuthHeaders(headers map[string]string) { headers["Cookie"] = fmt.Sprintf("auth=%s", armoAPI.authCookie) } } + +func (armoAPI *ArmoAPI) getCustomerGUIDFallBack() string { + if armoAPI.accountID != "" { + return armoAPI.accountID + } + return "11111111-1111-1111-1111-111111111111" +} diff --git a/clihandler/clidownload.go b/clihandler/clidownload.go index 35c1fdf9..ec9b02d3 100644 --- a/clihandler/clidownload.go +++ b/clihandler/clidownload.go @@ -77,6 +77,7 @@ func downloadArtifacts(downloadInfo *cautils.DownloadInfo) error { func downloadConfigInputs(downloadInfo *cautils.DownloadInfo) error { tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi()) + controlsInputsGetter := getConfigInputsGetter(downloadInfo.Name, tenant.GetAccountID(), nil) controlInputs, err := controlsInputsGetter.GetControlsInputs(tenant.GetClusterName()) if err != nil { @@ -97,6 +98,7 @@ func downloadConfigInputs(downloadInfo *cautils.DownloadInfo) error { func downloadExceptions(downloadInfo *cautils.DownloadInfo) error { var err error tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi()) + exceptionsGetter := getExceptionsGetter("") exceptions := []armotypes.PostureExceptionPolicy{} if tenant.GetAccountID() != "" { @@ -120,6 +122,7 @@ func downloadExceptions(downloadInfo *cautils.DownloadInfo) error { func downloadFramework(downloadInfo *cautils.DownloadInfo) error { tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi()) + g := getPolicyGetter(nil, tenant.GetAccountID(), true, nil) if downloadInfo.Name == "" { @@ -156,6 +159,7 @@ func downloadFramework(downloadInfo *cautils.DownloadInfo) error { func downloadControl(downloadInfo *cautils.DownloadInfo) error { tenant := getTenantConfig(downloadInfo.Account, "", getKubernetesApi()) + g := getPolicyGetter(nil, tenant.GetAccountID(), false, nil) if downloadInfo.Name == "" { diff --git a/clihandler/clilist.go b/clihandler/clilist.go index dda856ee..128537fc 100644 --- a/clihandler/clilist.go +++ b/clihandler/clilist.go @@ -49,6 +49,7 @@ func listFrameworks(listPolicies *cliobjects.ListPolicies) ([]string, error) { func listControls(listPolicies *cliobjects.ListPolicies) ([]string, error) { tenant := getTenantConfig(listPolicies.Account, "", getKubernetesApi()) // change k8sinterface + g := getPolicyGetter(nil, tenant.GetAccountID(), false, nil) l := getter.ListName if listPolicies.ListIDs { diff --git a/clihandler/clisubmit.go b/clihandler/clisubmit.go index 2f19aff0..c8370335 100644 --- a/clihandler/clisubmit.go +++ b/clihandler/clisubmit.go @@ -33,7 +33,8 @@ func SubmitExceptions(accountID, excPath string) error { logger.L().Info("submitting exceptions", helpers.String("path", excPath)) // load cached config - getTenantConfig(accountID, "", getKubernetesApi()) + tenantConfig := getTenantConfig(accountID, "", getKubernetesApi()) + tenantConfig.SetTenant() // load exceptions from file loader := getter.NewLoadPolicy([]string{excPath}) diff --git a/clihandler/cmd/rbac.go b/clihandler/cmd/rbac.go index 938b73b9..8d8fbec0 100644 --- a/clihandler/cmd/rbac.go +++ b/clihandler/cmd/rbac.go @@ -23,6 +23,7 @@ var rabcCmd = &cobra.Command{ // get config clusterConfig := getTenantConfig(submitInfo.Account, "", k8s) + clusterConfig.SetTenant() // list RBAC rbacObjects := cautils.NewRBACObjects(rbacscanner.NewRbacScannerFromK8sAPI(k8s, clusterConfig.GetAccountID(), clusterConfig.GetClusterName())) diff --git a/clihandler/cmd/results.go b/clihandler/cmd/results.go index ccf2d536..d6b25cfa 100644 --- a/clihandler/cmd/results.go +++ b/clihandler/cmd/results.go @@ -63,6 +63,7 @@ var resultsCmd = &cobra.Command{ // get config clusterConfig := getTenantConfig(submitInfo.Account, "", k8s) + clusterConfig.SetTenant() resultsObjects := NewResultsObject(clusterConfig.GetAccountID(), clusterConfig.GetClusterName(), args[0]) From e47eb9cb4e7bfce9d52196457f25e958cee7c213 Mon Sep 17 00:00:00 2001 From: dwertent Date: Wed, 16 Feb 2022 14:02:28 +0200 Subject: [PATCH 06/44] adding logs to python file --- build.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build.py b/build.py index abf816ff..e695e811 100644 --- a/build.py +++ b/build.py @@ -49,11 +49,12 @@ def main(): ArmoWebsite = os.getenv("ArmoWebsite") ArmoAuthServer = os.getenv("ArmoAuthServer") - ks_file = os.path.join(buildDir, packageName) - hash_file = os.path.join(buildDir, packageName + ".sha256") # Create build directory buildDir = getBuildDir() + + ks_file = os.path.join(buildDir, packageName) + hash_file = ks_file + ".sha256" if not os.path.isdir(buildDir): os.makedirs(buildDir) From fd135e9e49d2ff1bcafbcace52b4108c5aec3f01 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 17 Feb 2022 09:23:17 +0200 Subject: [PATCH 07/44] Cache accountID only when submitting data --- README.md | 3 +++ cautils/customerloader.go | 15 --------------- clihandler/clisubmit.go | 4 +++- clihandler/cmd/rbac.go | 5 ++++- clihandler/cmd/results.go | 5 ++++- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index ebaa5320..91011846 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![build](https://github.com/armosec/kubescape/actions/workflows/build.yaml/badge.svg)](https://github.com/armosec/kubescape/actions/workflows/build.yaml) [![Go Report Card](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://goreportcard.com/report/github.com/armosec/kubescape) + + Kubescape is a K8s open-source tool providing a multi-cloud K8s single pane of glass, including risk analysis, security compliance, RBAC visualizer and image vulnerabilities scanning. Kubescape scans K8s clusters, YAML files, and HELM charts, detecting misconfigurations according to multiple frameworks (such as the [NSA-CISA](https://www.armosec.io/blog/kubernetes-hardening-guidance-summary-by-armo) , [MITRE ATT&CKยฎ](https://www.microsoft.com/security/blog/2021/03/23/secure-containerized-environments-with-updated-threat-matrix-for-kubernetes/)), software vulnerabilities, and RBAC (role-based-access-control) violations at early stages of the CI/CD pipeline, calculates risk score instantly and shows risk trends over time. It became one of the fastest-growing Kubernetes tools among developers due to its easy-to-use CLI interface, flexible output formats, and automated scanning capabilities, saving Kubernetes users and adminsโ€™ precious time, effort, and resources. @@ -48,6 +50,7 @@ Want to contribute? Want to discuss something? Have an issue? * Open a issue, we are trying to respond within 48 hours * [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! +[![discord](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://img.shields.io/discord/893048809884643379) [logo](https://armosec.github.io/kubescape/) diff --git a/cautils/customerloader.go b/cautils/customerloader.go index 174b3380..b156e2b2 100644 --- a/cautils/customerloader.go +++ b/cautils/customerloader.go @@ -11,7 +11,6 @@ import ( "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/cautils/getter" - "github.com/armosec/kubescape/cautils/logger" corev1 "k8s.io/api/core/v1" ) @@ -113,13 +112,6 @@ func NewLocalConfig(backendAPI getter.IBackend, customerGUID, clusterName string lc.backendAPI.SetClientID(lc.configObj.ClientID) lc.backendAPI.SetSecretKey(lc.configObj.SecretKey) - if lc.configObj.AccountID != "" { - if err := lc.SetTenant(); err != nil { - logger.L().Error(err.Error()) - } - } - lc.UpdateCachedConfig() - return lc } @@ -229,13 +221,6 @@ func NewClusterConfig(k8s *k8sinterface.KubernetesApi, backendAPI getter.IBacken c.backendAPI.SetClientID(c.configObj.ClientID) c.backendAPI.SetSecretKey(c.configObj.SecretKey) - // if c.configObj.AccountID != "" { - // if err := c.SetTenant(); err != nil { - // logger.L().Error(err.Error()) - // } - // } - c.UpdateCachedConfig() - return c } diff --git a/clihandler/clisubmit.go b/clihandler/clisubmit.go index c8370335..878fc5f7 100644 --- a/clihandler/clisubmit.go +++ b/clihandler/clisubmit.go @@ -34,7 +34,9 @@ func SubmitExceptions(accountID, excPath string) error { // load cached config tenantConfig := getTenantConfig(accountID, "", getKubernetesApi()) - tenantConfig.SetTenant() + if err := tenantConfig.SetTenant(); err != nil { + logger.L().Error("failed setting account ID", helpers.Error(err)) + } // load exceptions from file loader := getter.NewLoadPolicy([]string{excPath}) diff --git a/clihandler/cmd/rbac.go b/clihandler/cmd/rbac.go index 8d8fbec0..a666a58e 100644 --- a/clihandler/cmd/rbac.go +++ b/clihandler/cmd/rbac.go @@ -5,6 +5,7 @@ import ( "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/clihandler" "github.com/armosec/kubescape/clihandler/cliinterfaces" reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" @@ -23,7 +24,9 @@ var rabcCmd = &cobra.Command{ // get config clusterConfig := getTenantConfig(submitInfo.Account, "", k8s) - clusterConfig.SetTenant() + if err := clusterConfig.SetTenant(); err != nil { + logger.L().Error("failed setting account ID", helpers.Error(err)) + } // list RBAC rbacObjects := cautils.NewRBACObjects(rbacscanner.NewRbacScannerFromK8sAPI(k8s, clusterConfig.GetAccountID(), clusterConfig.GetClusterName())) diff --git a/clihandler/cmd/results.go b/clihandler/cmd/results.go index d6b25cfa..f7c09896 100644 --- a/clihandler/cmd/results.go +++ b/clihandler/cmd/results.go @@ -8,6 +8,7 @@ import ( "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/clihandler" "github.com/armosec/kubescape/clihandler/cliinterfaces" reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" @@ -63,7 +64,9 @@ var resultsCmd = &cobra.Command{ // get config clusterConfig := getTenantConfig(submitInfo.Account, "", k8s) - clusterConfig.SetTenant() + if err := clusterConfig.SetTenant(); err != nil { + logger.L().Error("failed setting account ID", helpers.Error(err)) + } resultsObjects := NewResultsObject(clusterConfig.GetAccountID(), clusterConfig.GetClusterName(), args[0]) From e95b0f840a53de47bf79fbc1d9f0e0657192f7c7 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 17 Feb 2022 17:47:57 +0200 Subject: [PATCH 08/44] Support zap logger --- cautils/logger/methods.go | 12 +++++---- cautils/logger/zaplogger/logger.go | 26 ++++++++++++++++--- .../reporter/v1/reporteventreceiver.go | 4 +-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/cautils/logger/methods.go b/cautils/logger/methods.go index 959abe42..e767775e 100644 --- a/cautils/logger/methods.go +++ b/cautils/logger/methods.go @@ -5,6 +5,8 @@ import ( "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/cautils/logger/prettylogger" + "github.com/armosec/kubescape/cautils/logger/zaplogger" + "github.com/mattn/go-isatty" ) type ILogger interface { @@ -32,10 +34,10 @@ func L() ILogger { } func InitializeLogger() { - initializeLogger() -} -func initializeLogger() { - // TODO - support zap logger - l = prettylogger.NewPrettyLogger() + if isatty.IsTerminal(os.Stdout.Fd()) { + l = prettylogger.NewPrettyLogger() + } else { + l = zaplogger.NewZapLogger() + } } diff --git a/cautils/logger/zaplogger/logger.go b/cautils/logger/zaplogger/logger.go index 5300ccfb..bd2c33c5 100644 --- a/cautils/logger/zaplogger/logger.go +++ b/cautils/logger/zaplogger/logger.go @@ -10,20 +10,40 @@ import ( type ZapLogger struct { zapL *zap.Logger + cfg zap.Config } func NewZapLogger() *ZapLogger { + ec := zap.NewProductionEncoderConfig() + ec.EncodeTime = zapcore.RFC3339TimeEncoder + cfg := zap.NewProductionConfig() + cfg.DisableCaller = true + cfg.DisableStacktrace = true + cfg.Encoding = "json" + cfg.EncoderConfig = ec + + zapLogger, err := cfg.Build() + if err != nil { + panic(err) + } return &ZapLogger{ - zapL: zap.L(), + zapL: zapLogger, + cfg: cfg, } } -func (zl *ZapLogger) GetLevel() string { return "" } +func (zl *ZapLogger) GetLevel() string { return zl.cfg.Level.Level().String() } func (zl *ZapLogger) SetWriter(w *os.File) {} +func (zl *ZapLogger) GetWriter() *os.File { return nil } func GetWriter() *os.File { return nil } func (zl *ZapLogger) SetLevel(level string) error { - return nil + l := zapcore.Level(1) + err := l.Set(level) + if err == nil { + zl.cfg.Level.SetLevel(l) + } + return err } func (zl *ZapLogger) Fatal(msg string, details ...helpers.IDetails) { zl.zapL.Fatal(msg, detailsToZapFields(details)...) diff --git a/resultshandling/reporter/v1/reporteventreceiver.go b/resultshandling/reporter/v1/reporteventreceiver.go index cc2cd34a..abdbdcc9 100644 --- a/resultshandling/reporter/v1/reporteventreceiver.go +++ b/resultshandling/reporter/v1/reporteventreceiver.go @@ -44,11 +44,11 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe } if report.customerGUID == "" { - report.message = "WARNING: Failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account ' flag. Contact ARMO team for more details" + report.message = "WARNING: Failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account ' flag. Please feel free to contact ARMO team for more details" return nil } if report.clusterName == "" { - report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS" + report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS.Please feel free to contact ARMO team for more details" return nil } From cf4317b5f67e222f03ec74ba5faa8da74aaf397a Mon Sep 17 00:00:00 2001 From: alegrey91 Date: Thu, 17 Feb 2022 18:16:29 +0100 Subject: [PATCH 09/44] feat(resulthandling): add support for pdf format output --- clihandler/cmd/scan.go | 2 +- go.mod | 5 + go.sum | 10 + resultshandling/printer/printresults.go | 1 + resultshandling/printer/v2/pdf.go | 246 +++++++++++++++++++++ resultshandling/printer/v2/pdf/logo.png | Bin 0 -> 54270 bytes resultshandling/printer/v2/printresults.go | 2 + 7 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 resultshandling/printer/v2/pdf.go create mode 100644 resultshandling/printer/v2/pdf/logo.png diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index cdb62449..b2861228 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -48,7 +48,7 @@ func init() { scanCmd.PersistentFlags().StringVar(&scanInfo.UseArtifactsFrom, "use-artifacts-from", "", "Load artifacts from local directory. If not used will download them") scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system,kube-public") scanCmd.PersistentFlags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1") - scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer"/"json"/"junit"/"prometheus"`) + scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer","json","junit","prometheus","pdf"`) scanCmd.PersistentFlags().StringVar(&scanInfo.IncludeNamespaces, "include-namespaces", "", "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to Armo backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") scanCmd.PersistentFlags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") diff --git a/go.mod b/go.mod index 35cdc24c..ca9009c0 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/fatih/color v1.13.0 github.com/francoispqt/gojay v1.2.13 github.com/gofrs/uuid v4.1.0+incompatible + github.com/johnfercher/maroto v0.34.0 github.com/mattn/go-isatty v0.0.14 github.com/olekukonko/tablewriter v0.0.5 github.com/open-policy-agent/opa v0.33.1 @@ -50,6 +51,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.13.0 // indirect github.com/aws/smithy-go v1.9.1 // indirect + github.com/boombuler/barcode v1.0.0 // indirect github.com/coreos/go-oidc v2.2.1+incompatible // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/docker v20.10.9+incompatible // indirect @@ -66,12 +68,14 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.1.2 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.11 // indirect + github.com/jung-kurt/gofpdf v1.4.2 // indirect github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -82,6 +86,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect diff --git a/go.sum b/go.sum index c1d4f78f..6c8dffcb 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,7 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/briandowns/spinner v1.18.0 h1:SJs0maNOs4FqhBwiJ3Gr7Z1D39/rukIVGQvpNZVHVcM= @@ -362,6 +363,7 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= @@ -416,6 +418,8 @@ github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9Y github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/johnfercher/maroto v0.34.0 h1:kmqlO280WbjzeaPn8HtqUE3gooauVwqO/3cmrBtCL4A= +github.com/johnfercher/maroto v0.34.0/go.mod h1:UeLY7evCe2Au8KwHFzaSGffKGADEZK+u6O8C74mdudM= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -431,6 +435,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.4.2 h1:3u2ojTwxPPu3ysIOc5iTwcECpvkFCAe2RJ/tQrvfLi0= +github.com/jung-kurt/gofpdf v1.4.2/go.mod h1:rZsO0wEsunjT/L9stF3fJjYbAHgqNYuQB4B8FWvBck0= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -573,6 +579,7 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= @@ -636,6 +643,8 @@ github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -742,6 +751,7 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6 h1:QE6XYQK6naiK1EPAe1g/ILLxN golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190507092727-e4e5bf290fec/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go index 1ac75455..30aaf302 100644 --- a/resultshandling/printer/printresults.go +++ b/resultshandling/printer/printresults.go @@ -15,6 +15,7 @@ const ( JsonFormat string = "json" JunitResultFormat string = "junit" PrometheusFormat string = "prometheus" + PdfFormat string = "pdf" ) type IPrinter interface { diff --git a/resultshandling/printer/v2/pdf.go b/resultshandling/printer/v2/pdf.go new file mode 100644 index 00000000..d594b4e6 --- /dev/null +++ b/resultshandling/printer/v2/pdf.go @@ -0,0 +1,246 @@ +package v2 + +import ( + _ "embed" + b64 "encoding/base64" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/resultshandling/printer" + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/johnfercher/maroto/pkg/color" + "github.com/johnfercher/maroto/pkg/consts" + "github.com/johnfercher/maroto/pkg/pdf" + "github.com/johnfercher/maroto/pkg/props" +) + +const ( + pdfOutputFile = "report" + pdfOutputExt = ".pdf" +) + +var ( + //go:embed pdf/logo.png + kubescapeLogo []byte +) + +type PdfPrinter struct { + writer *os.File + sortedControlNames []string +} + +func NewPdfPrinter() *PdfPrinter { + return &PdfPrinter{} +} + +func (pdfPrinter *PdfPrinter) SetWriter(outputFile string) { + // Ensure to have an available output file, otherwise create it. + if outputFile == "" { + outputFile = pdfOutputFile + } + // Ensure to have the right file extension. + if filepath.Ext(strings.TrimSpace(outputFile)) != pdfOutputExt { + outputFile = outputFile + pdfOutputExt + } + pdfPrinter.writer = printer.GetWriter(outputFile) +} + +func (pdfPrinter *PdfPrinter) Score(score float32) { + fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) +} + +func (pdfPrinter *PdfPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + pdfPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) + + m := pdf.NewMaroto(consts.Portrait, consts.A4) + pdfPrinter.printHeader(m) + pdfPrinter.printFramework(m, opaSessionObj.Report.SummaryDetails.ListFrameworks().All()) + pdfPrinter.printTable(m, &opaSessionObj.Report.SummaryDetails) + pdfPrinter.printFinalResult(m, &opaSessionObj.Report.SummaryDetails) + + // Extrat output buffer. + outBuff, err := m.Output() + if err != nil { + fmt.Println("Could not save PDF:", err) + os.Exit(1) + } + pdfPrinter.writer.Write(outBuff.Bytes()) +} + +// Print Kubescape logo and report date. +func (pdfPrinter *PdfPrinter) printHeader(m pdf.Maroto) { + // Retrieve current time (we need it for the report timestamp). + t := time.Now() + // Enconde PNG into Base64 to embed it into the pdf. + kubescapeLogoEnc := b64.StdEncoding.EncodeToString(kubescapeLogo) + + m.SetPageMargins(10, 15, 10) + m.Row(40, func() { + //m.Text(fmt.Sprintf("Security Assessment"), props.Text{ + // Align: consts.Center, + // Size: 24, + // Family: consts.Arial, + // Style: consts.Bold, + //}) + _ = m.Base64Image(kubescapeLogoEnc, consts.Png, props.Rect{ + Center: true, + Percent: 100, + }) + }) + m.Row(6, func() { + m.Text(fmt.Sprintf("Report date: %d-%02d-%02dT%02d:%02d:%02d", + t.Year(), + t.Month(), + t.Day(), + t.Hour(), + t.Minute(), + t.Second()), props.Text{ + Align: consts.Left, + Size: 6.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + m.Line(1) +} + +// Print pdf frameworks after pdf header. +func (pdfPrinter *PdfPrinter) printFramework(m pdf.Maroto, frameworks []reportsummary.IPolicies) { + var p string + if len(frameworks) == 1 { + if frameworks[0].GetName() != "" { + p = fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName()) + } + } else if len(frameworks) > 1 { + p = "FRAMEWORKS: " + i := 0 + for ; i < len(frameworks)-1; i++ { + p += fmt.Sprintf("%s (risk: %.2f), ", frameworks[i].GetName(), frameworks[i].GetScore()) + } + p += fmt.Sprintf("%s (risk: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetScore()) + } + m.Row(10, func() { + m.Text(fmt.Sprintf(p), props.Text{ + Align: consts.Center, + Size: 8, + Family: consts.Arial, + Style: consts.Bold, + }) + }) +} + +// Create pdf table +func (pdfPrinter *PdfPrinter) printTable(m pdf.Maroto, summaryDetails *reportsummary.SummaryDetails) { + headers := []string{"CONTROL NAME", "FAILED RESOURCES", "EXCLUDED RESOURCES", "ALL RESOURCES", "% RISK-SCORE"} + + controls := make([][]string, len(pdfPrinter.sortedControlNames)) + for i := range controls { + controls[i] = make([]string, len(headers)) + } + for i := 0; i < len(pdfPrinter.sortedControlNames); i++ { + controls[i] = generateRow(summaryDetails.Controls.GetControl(reportsummary.EControlCriteriaName, pdfPrinter.sortedControlNames[i])) + } + + m.TableList(headers, controls, props.TableList{ + HeaderProp: props.TableListContent{ + Family: consts.Arial, + Style: consts.Bold, + Size: 8.0, + }, + ContentProp: props.TableListContent{ + Family: consts.Courier, + Style: consts.Normal, + Size: 8.0, + }, + Align: consts.Center, + AlternatedBackground: &color.Color{ + Red: 224, + Green: 224, + Blue: 224, + }, + HeaderContentSpace: 2.0, + Line: false, + }) + m.Line(1) + m.Row(2, func(){}) +} + +// Add final results. +func (pdfPrinter *PdfPrinter) printFinalResult(m pdf.Maroto, summaryDetails *reportsummary.SummaryDetails) { + m.Row(5, func() { + m.Col(3, func() { + m.Text("Resource summary", props.Text{ + Align: consts.Left, + Size: 8.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + m.Col(2, func() { + m.Text(fmt.Sprintf("%d", summaryDetails.NumberOfResources().Failed()), props.Text{ + Align: consts.Left, + Size: 8.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + m.Col(2, func() { + m.Text(fmt.Sprintf("%d", summaryDetails.NumberOfResources().Excluded()), props.Text{ + Align: consts.Left, + Size: 8.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + m.Col(2, func() { + m.Text(fmt.Sprintf("%d", summaryDetails.NumberOfResources().All()), props.Text{ + Align: consts.Left, + Size: 8.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + m.Col(2, func() { + m.Text(fmt.Sprintf("%.2f%s", summaryDetails.Score, "%"), props.Text{ + Align: consts.Left, + Size: 8.0, + Style: consts.Bold, + Family: consts.Arial, + }) + }) + }) +} + +// Return sorted control names. +func getSortedControlsNames(controls reportsummary.ControlSummaries) []string { + controlNames := make([]string, 0, len(controls)) + for k := range controls { + c := controls[k] + controlNames = append(controlNames, c.GetName()) + } + sort.Strings(controlNames) + return controlNames +} + +// Generate rows to be appended to the table. +func generateRow(controlSummary reportsummary.IControlSummary) []string { + // We use ReplaceAll because some control name + // has a too long name that unformat the pdf view. + controlName := strings.ReplaceAll(controlSummary.GetName(), "-", " ") + row := []string{controlName} + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed())) + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Excluded())) + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().All())) + + if !controlSummary.GetStatus().IsSkipped() { + row = append(row, fmt.Sprintf("%d", int(controlSummary.GetScore()))+"%") + } else { + row = append(row, "skipped") + } + return row +} diff --git a/resultshandling/printer/v2/pdf/logo.png b/resultshandling/printer/v2/pdf/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cfa8ffc1a75c7f9774b6105cc50f752d7f94eec4 GIT binary patch literal 54270 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 literal 0 HcmV?d00001 diff --git a/resultshandling/printer/v2/printresults.go b/resultshandling/printer/v2/printresults.go index 04b0aa28..8dcda6c4 100644 --- a/resultshandling/printer/v2/printresults.go +++ b/resultshandling/printer/v2/printresults.go @@ -15,6 +15,8 @@ func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { return NewJunitPrinter() // case printer.PrometheusFormat: // return NewPrometheusPrinter(verboseMode) + case printer.PdfFormat: + return NewPdfPrinter() default: return resourcemapping.NewPrettyPrinter(verboseMode) } From 5f2e5c6f4ec232b03928eb8b1c5492e54e3a5772 Mon Sep 17 00:00:00 2001 From: alegrey91 Date: Thu, 17 Feb 2022 18:17:12 +0100 Subject: [PATCH 10/44] docs: describe support for pdf format output --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ebaa5320..3d6ce3bb 100644 --- a/README.md +++ b/README.md @@ -96,12 +96,12 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `--include-namespaces` | Scan all namespaces | Scan specific namespaces | | | `-s`/`--silent` | Display progress messages | Silent progress messages | | | `-t`/`--fail-threshold` | `100` (do not fail) | fail command (return exit code 1) if result is above threshold | `0` -> `100` | -| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus` | +| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus`/`pdf` | | `-o`/`--output` | print to stdout | Save scan result in file | | -| `--use-from` | | Load local framework object from specified path. If not used will download latest | +| `--use-from` | | Load local framework object from specified path. If not used will download latest || | `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | | | `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | -| `--exceptions` | | Path to an exceptions obj, [examples](examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS | +| `--exceptions` | | Path to an exceptions obj, [examples](examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || | `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal | | | `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` | | `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` | @@ -173,7 +173,14 @@ kubescape scan --format json --output results.json kubescape scan --format junit --output results.xml ``` +#### Output in `pdf` format + +``` +kubescape scan --format pdf --output results.pdf +``` + #### Output in `prometheus` metrics format - Contributed by [@Joibel](https://github.com/Joibel) + ``` kubescape scan --format prometheus ``` From 830ee271690f342267848ed7ffd34f3c5c435bae Mon Sep 17 00:00:00 2001 From: Avinash Upadhyaya Date: Sun, 20 Feb 2022 10:26:40 +0530 Subject: [PATCH 11/44] build: fix release version in Dockerfile --- build/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index 20474310..e3e6ff6c 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -3,7 +3,7 @@ FROM golang:1.17-alpine as builder ARG image_version -ENV RELEASE=image_version +ENV RELEASE=$image_version ENV GO111MODULE= From 9d528a80755b466e410d7db9f59d5d06466d38dd Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 20 Feb 2022 13:30:05 +0200 Subject: [PATCH 12/44] support setting default cach --- cautils/getter/armoapi.go | 16 +++++++++++++++ cautils/getter/getpoliciesutils.go | 6 +----- cautils/getter/loadpolicy.go | 11 +++++++++- cautils/logger/methods.go | 18 +++++++++++----- cautils/scaninfo.go | 1 + clihandler/cmd/root.go | 33 +++++++++++++++++++++++++++--- 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/cautils/getter/armoapi.go b/cautils/getter/armoapi.go index 0534f12b..dc482553 100644 --- a/cautils/getter/armoapi.go +++ b/cautils/getter/armoapi.go @@ -27,6 +27,11 @@ var ( armoFEURL = "portal.armo.cloud" armoAUTHURL = "auth.armo.cloud" + armoStageERURL = "report-ks.eustage2.cyberarmorsoft" + armoStageBEURL = "api-stage.armo.cloud" + armoStageFEURL = "armoui.eustage2.cyberarmorsoft.com" + armoStageAUTHURL = "eggauth.eustage2.cyberarmorsoft.com" + armoDevERURL = "report.eudev3.cyberarmorsoft.com" armoDevBEURL = "api-dev.armo.cloud" armoDevFEURL = "armoui-dev.eudev3.cyberarmorsoft.com" @@ -84,6 +89,17 @@ func NewARMOAPIProd() *ArmoAPI { return apiObj } +func NewARMOAPIStaging() *ArmoAPI { + apiObj := newArmoAPI() + + apiObj.apiURL = armoStageBEURL + apiObj.erURL = armoStageERURL + apiObj.feURL = armoStageFEURL + apiObj.authURL = armoStageAUTHURL + + return apiObj +} + func NewARMOAPICustomized(armoERURL, armoBEURL, armoFEURL, armoAUTHURL string) *ArmoAPI { apiObj := newArmoAPI() diff --git a/cautils/getter/getpoliciesutils.go b/cautils/getter/getpoliciesutils.go index 30b82e80..d577234a 100644 --- a/cautils/getter/getpoliciesutils.go +++ b/cautils/getter/getpoliciesutils.go @@ -13,11 +13,7 @@ import ( ) func GetDefaultPath(name string) string { - defaultfilePath := filepath.Join(DefaultLocalStore, name) - if homeDir, err := os.UserHomeDir(); err == nil { - defaultfilePath = filepath.Join(homeDir, defaultfilePath) - } - return defaultfilePath + return filepath.Join(DefaultLocalStore, name) } func SaveInFile(policy interface{}, pathStr string) error { diff --git a/cautils/getter/loadpolicy.go b/cautils/getter/loadpolicy.go index 7ae8c8f5..12d56555 100644 --- a/cautils/getter/loadpolicy.go +++ b/cautils/getter/loadpolicy.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "github.com/armosec/armoapi-go/armotypes" @@ -13,7 +14,15 @@ import ( // ======================================================================================================================= // ============================================== LoadPolicy ============================================================= // ======================================================================================================================= -const DefaultLocalStore = ".kubescape" +var DefaultLocalStore = getCacheDir() + +func getCacheDir() string { + defaultDirPath := ".kubescape" + if homeDir, err := os.UserHomeDir(); err == nil { + defaultDirPath = filepath.Join(homeDir, defaultDirPath) + } + return defaultDirPath +} // Load policies from a local repository type LoadPolicy struct { diff --git a/cautils/logger/methods.go b/cautils/logger/methods.go index e767775e..be208fd2 100644 --- a/cautils/logger/methods.go +++ b/cautils/logger/methods.go @@ -2,6 +2,7 @@ package logger import ( "os" + "strings" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/cautils/logger/prettylogger" @@ -28,16 +29,23 @@ var l ILogger func L() ILogger { if l == nil { - InitializeLogger() + InitializeLogger("") } return l } -func InitializeLogger() { +func InitializeLogger(loggerName string) { - if isatty.IsTerminal(os.Stdout.Fd()) { - l = prettylogger.NewPrettyLogger() - } else { + switch strings.ToLower(loggerName) { + case "zap": l = zaplogger.NewZapLogger() + case "pretty": + l = prettylogger.NewPrettyLogger() + default: + if isatty.IsTerminal(os.Stdout.Fd()) { + l = prettylogger.NewPrettyLogger() + } else { + l = zaplogger.NewZapLogger() + } } } diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index eb0c4a4c..128d4ce1 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -73,6 +73,7 @@ type ScanInfo struct { Local bool // Do not submit results Account string // account ID Logger string // logger level + CacheDir string // cached dir KubeContext string // context name FrameworkScan bool // false if scanning control ScanAll bool // true if scan all frameworks diff --git a/clihandler/cmd/root.go b/clihandler/cmd/root.go index 8c20df28..96245611 100644 --- a/clihandler/cmd/root.go +++ b/clihandler/cmd/root.go @@ -3,8 +3,10 @@ package cmd import ( "flag" "fmt" + "os" "strings" + "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" @@ -31,6 +33,7 @@ var ksExamples = ` var rootCmd = &cobra.Command{ Use: "kubescape", + Version: cautils.BuildNumber, Short: "Kubescape is a tool for testing Kubernetes security posture", Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA \ MITRE ATT&CKยฎ and other frameworks specifications`, Example: ksExamples, @@ -40,20 +43,42 @@ func Execute() { rootCmd.Execute() } func init() { - cobra.OnInitialize(initLogger, initEnvironment) + cobra.OnInitialize(initLogger, initLoggerLevel, initEnvironment, initCacheDir) flag.CommandLine.StringVar(&armoBEURLs, "environment", "", envFlagUsage) rootCmd.PersistentFlags().StringVar(&armoBEURLs, "environment", "", envFlagUsage) rootCmd.PersistentFlags().MarkHidden("environment") - rootCmd.PersistentFlags().StringVar(&scanInfo.Logger, "logger", "info", fmt.Sprintf("Logger level. Supported: %s", strings.Join(helpers.SupportedLevels(), "/"))) + rootCmd.PersistentFlags().StringVarP(&scanInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/"))) + rootCmd.PersistentFlags().StringVar(&scanInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]") flag.Parse() } func initLogger() { + if l := os.Getenv("KS_LOGGER_NAME"); l != "" { + logger.InitializeLogger(l) + } +} +func initLoggerLevel() { + if scanInfo.Logger != helpers.InfoLevel.String() { + } else if l := os.Getenv("KS_LOGGER"); l != "" { + scanInfo.Logger = l + } if err := logger.L().SetLevel(scanInfo.Logger); err != nil { logger.L().Fatal(fmt.Sprintf("supported levels: %s", strings.Join(helpers.SupportedLevels(), "/")), helpers.Error(err)) } } + +func initCacheDir() { + if scanInfo.CacheDir != getter.DefaultLocalStore { + getter.DefaultLocalStore = scanInfo.CacheDir + } else if cacheDir := os.Getenv("KS_CACHE_DIR"); cacheDir != "" { + getter.DefaultLocalStore = cacheDir + } else { + return // using default cache di location + } + + logger.L().Debug("cache dir updated", helpers.String("path", getter.DefaultLocalStore)) +} func initEnvironment() { urlSlices := strings.Split(armoBEURLs, ",") if len(urlSlices) != 1 && len(urlSlices) < 3 { @@ -62,8 +87,10 @@ func initEnvironment() { switch len(urlSlices) { case 1: switch urlSlices[0] { - case "dev": + case "dev", "development": getter.SetARMOAPIConnector(getter.NewARMOAPIDev()) + case "stage", "staging": + getter.SetARMOAPIConnector(getter.NewARMOAPIStaging()) case "": getter.SetARMOAPIConnector(getter.NewARMOAPIProd()) default: From a6497c1252b3bc21cd00027df204940f19f33ef7 Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 20 Feb 2022 19:04:16 +0200 Subject: [PATCH 13/44] fixed python build file --- build.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/build.py b/build.py index e695e811..eeeee049 100644 --- a/build.py +++ b/build.py @@ -49,10 +49,9 @@ def main(): ArmoWebsite = os.getenv("ArmoWebsite") ArmoAuthServer = os.getenv("ArmoAuthServer") - # Create build directory buildDir = getBuildDir() - + ks_file = os.path.join(buildDir, packageName) hash_file = ks_file + ".sha256" @@ -60,13 +59,24 @@ def main(): os.makedirs(buildDir) # Build kubescape - ldflags = "-w -s -X %s=%s -X %s=%s -X %s=%s -X %s=%s -X %s=%s" \ - % (buildUrl, releaseVersion, BE_SERVER_CONST, ArmoBEServer, - ER_SERVER_CONST, ArmoERServer, WEBSITE_CONST, ArmoWebsite, - AUTH_SERVER_CONST, ArmoAuthServer) + ldflags = "-w -s" + if releaseVersion: + ldflags += " -X {}={}".format(buildUrl, releaseVersion) + if ArmoBEServer: + ldflags += " -X {}={}".format(BE_SERVER_CONST, ArmoBEServer) + if ArmoERServer: + ldflags += " -X {}={}".format(ER_SERVER_CONST, ArmoERServer) + if ArmoWebsite: + ldflags += " -X {}={}".format(WEBSITE_CONST, ArmoWebsite) + if ArmoAuthServer: + ldflags += " -X {}={}".format(AUTH_SERVER_CONST, ArmoAuthServer) + + build_command = ["go", "build", "-o", ks_file, "-ldflags" ,ldflags] print("Building kubescape and saving here: {}".format(ks_file)) - status = subprocess.call(["go", "build", "-o", ks_file, "-ldflags" ,ldflags]) + print("Build command: {}".format(" ".join(build_command))) + + status = subprocess.call(build_command) checkStatus(status, "Failed to build kubescape") sha256 = hashlib.sha256() From 990be3afe8ed5e450c05754a8689104d531029db Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 20 Feb 2022 20:55:22 +0200 Subject: [PATCH 14/44] remove cat command --- build/Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/build/Dockerfile b/build/Dockerfile index e3e6ff6c..600bcb5e 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -21,7 +21,6 @@ ADD . . RUN python build.py RUN ls -ltr build/ubuntu-latest -RUN cat /work/build/ubuntu-latest/kubescape.sha1 FROM alpine COPY --from=builder /work/build/ubuntu-latest/kubescape /usr/bin/kubescape From 606f5cfb6215b481d66a94e336abd29001d65506 Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 21 Feb 2022 09:00:34 +0200 Subject: [PATCH 15/44] fixed discord banner --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 91011846..d127aea6 100644 --- a/README.md +++ b/README.md @@ -49,10 +49,11 @@ We invite you to our team! We are excited about this project and want to return Want to contribute? Want to discuss something? Have an issue? * Open a issue, we are trying to respond within 48 hours -* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! -[![discord](https://goreportcard.com/badge/github.com/armosec/kubescape)](https://img.shields.io/discord/893048809884643379) +* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! + [logo](https://armosec.github.io/kubescape/) +![discord](https://img.shields.io/discord/893048809884643379) # Options and examples From fef85a446783ff94eb5da584f41f393b0634083d Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 21 Feb 2022 14:48:48 +0200 Subject: [PATCH 16/44] init printer for all pkg --- clihandler/initcli.go | 6 +----- resultshandling/results.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/clihandler/initcli.go b/clihandler/initcli.go index a3398fc1..7979867c 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -8,9 +8,6 @@ import ( "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/resultshandling/printer" - printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1" - - // printerv2 "github.com/armosec/kubescape/resultshandling/printer/v2" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/getter" @@ -86,8 +83,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler := getReporter(tenantConfig, scanInfo.Submit) // setup printer - printerHandler := printerv1.GetPrinter(scanInfo.Format, scanInfo.VerboseMode) - // printerHandler = printerv2.GetPrinter(scanInfo.Format, scanInfo.VerboseMode) + printerHandler := resultshandling.GetPrinter(scanInfo.Format, scanInfo.VerboseMode) printerHandler.SetWriter(scanInfo.Output) // ================== return interface ====================================== diff --git a/resultshandling/results.go b/resultshandling/results.go index c9278292..6e45680a 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -4,6 +4,9 @@ import ( "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/resultshandling/printer" + printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1" + printerv2 "github.com/armosec/kubescape/resultshandling/printer/v2" + "github.com/armosec/kubescape/resultshandling/reporter" "github.com/armosec/opa-utils/reporthandling" ) @@ -49,3 +52,19 @@ func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 return (float32(len(allResources)) - float32(len(failedResources))) / float32(len(allResources)) } + +func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { + + switch printFormat { + case printer.JsonFormat: + return printerv1.NewJsonPrinter() + case printer.JunitResultFormat: + return printerv1.NewJunitPrinter() + case printer.PrometheusFormat: + return printerv1.NewPrometheusPrinter(verboseMode) + case printer.PdfFormat: + return printerv2.NewPdfPrinter() + default: + return printerv1.NewPrettyPrinter(verboseMode) + } +} From 88a5128c036ec39d97e7f70136620f62a3a8ac5b Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 21 Feb 2022 17:22:01 +0200 Subject: [PATCH 17/44] fixed pretty-print v1, expand list support --- README.md | 2 +- cautils/reportv2tov1.go | 36 ++++----------------- cautils/scaninfo.go | 5 +++ clihandler/clilist.go | 28 ++++++++++++---- clihandler/cliobjects/listpolicies.go | 1 + clihandler/cmd/list.go | 3 +- clihandler/initcli.go | 2 +- resultshandling/printer/v1/prettyprinter.go | 5 +-- resultshandling/printer/v1/printresults.go | 18 ----------- resultshandling/printer/v2/pdf.go | 6 ++-- resultshandling/printer/v2/printresults.go | 20 ------------ resultshandling/results.go | 5 +-- 12 files changed, 45 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index b97d6b25..79e94282 100644 --- a/README.md +++ b/README.md @@ -176,7 +176,7 @@ kubescape scan --format json --output results.json kubescape scan --format junit --output results.xml ``` -#### Output in `pdf` format +#### Output in `pdf` format - Contributed by [@alegrey91](https://github.com/alegrey91) ``` kubescape scan --format pdf --output results.pdf diff --git a/cautils/reportv2tov1.go b/cautils/reportv2tov1.go index 9b006339..af19aaf7 100644 --- a/cautils/reportv2tov1.go +++ b/cautils/reportv2tov1.go @@ -5,7 +5,6 @@ import ( "github.com/armosec/opa-utils/reporthandling" helpersv1 "github.com/armosec/opa-utils/reporthandling/helpers/v1" "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" - "github.com/armosec/opa-utils/score" ) func ReportV2ToV1(opaSessionObj *OPASessionObj) { @@ -22,7 +21,6 @@ func ReportV2ToV1(opaSessionObj *OPASessionObj) { fwv1 := reporthandling.FrameworkReport{} fwv1.Name = fwv2.GetName() fwv1.Score = fwv2.GetScore() - fwv1.ControlReports = append(fwv1.ControlReports, controlReportV2ToV1(opaSessionObj, fwv2.GetName(), fwv2.Controls)...) frameworks = append(frameworks, fwv1) @@ -30,10 +28,10 @@ func ReportV2ToV1(opaSessionObj *OPASessionObj) { } else { fwv1 := reporthandling.FrameworkReport{} fwv1.Name = "" - fwv1.Score = 0 fwv1.ControlReports = append(fwv1.ControlReports, controlReportV2ToV1(opaSessionObj, "", opaSessionObj.Report.SummaryDetails.Controls)...) frameworks = append(frameworks, fwv1) + fwv1.Score = opaSessionObj.Report.SummaryDetails.Score } // // remove unused data @@ -49,36 +47,14 @@ func ReportV2ToV1(opaSessionObj *OPASessionObj) { reporthandling.SetUniqueResourcesCounter(&frameworks[f]) // set default score - reporthandling.SetDefaultScore(&frameworks[f]) + // reporthandling.SetDefaultScore(&frameworks[f]) } - // update score - scoreutil := score.NewScore(opaSessionObj.AllResources) - scoreutil.Calculate(frameworks) + // // update score + // scoreutil := score.NewScore(opaSessionObj.AllResources) + // scoreutil.Calculate(frameworks) opaSessionObj.PostureReport.FrameworkReports = frameworks - - // opaSessionObj.Report.SummaryDetails.Score = 0 - // for i := range frameworks { - // for j := range frameworks[i].ControlReports { - // // frameworks[i].ControlReports[j].Score - // for w := range opaSessionObj.Report.SummaryDetails.Frameworks { - // if opaSessionObj.Report.SummaryDetails.Frameworks[w].Name == frameworks[i].Name { - // opaSessionObj.Report.SummaryDetails.Frameworks[w].Score = frameworks[i].Score - // } - // if c, ok := opaSessionObj.Report.SummaryDetails.Frameworks[w].Controls[frameworks[i].ControlReports[j].ControlID]; ok { - // c.Score = frameworks[i].ControlReports[j].Score - // opaSessionObj.Report.SummaryDetails.Frameworks[w].Controls[frameworks[i].ControlReports[j].ControlID] = c - // } - // } - // if c, ok := opaSessionObj.Report.SummaryDetails.Controls[frameworks[i].ControlReports[j].ControlID]; ok { - // c.Score = frameworks[i].ControlReports[j].Score - // opaSessionObj.Report.SummaryDetails.Controls[frameworks[i].ControlReports[j].ControlID] = c - // } - // } - // opaSessionObj.Report.SummaryDetails.Score += opaSessionObj.PostureReport.FrameworkReports[i].Score - // } - // opaSessionObj.Report.SummaryDetails.Score /= float32(len(opaSessionObj.Report.SummaryDetails.Frameworks)) } func controlReportV2ToV1(opaSessionObj *OPASessionObj, frameworkName string, controls map[string]reportsummary.ControlSummary) []reporthandling.ControlReport { @@ -88,9 +64,9 @@ func controlReportV2ToV1(opaSessionObj *OPASessionObj, frameworkName string, con crv1.ControlID = controlID crv1.BaseScore = crv2.ScoreFactor crv1.Name = crv2.GetName() + crv1.Score = crv2.GetScore() crv1.Control_ID = controlID // crv1.Attributes = crv2. - crv1.Score = crv2.GetScore() // TODO - add fields crv1.Description = crv2.Description diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 128d4ce1..e6ea9198 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -154,6 +154,11 @@ func (scanInfo *ScanInfo) setOutputFile() { scanInfo.Output += ".xml" } } + if scanInfo.Format == "pdf" { + if filepath.Ext(scanInfo.Output) != ".pdf" { + scanInfo.Output += ".pdf" + } + } } func (scanInfo *ScanInfo) GetScanningEnvironment() string { diff --git a/clihandler/clilist.go b/clihandler/clilist.go index 852c0000..aa6920d3 100644 --- a/clihandler/clilist.go +++ b/clihandler/clilist.go @@ -1,6 +1,7 @@ package clihandler import ( + "encoding/json" "fmt" "sort" "strings" @@ -14,6 +15,11 @@ var listFunc = map[string]func(*cliobjects.ListPolicies) ([]string, error){ "frameworks": listFrameworks, } +var listFormatFunc = map[string]func(*cliobjects.ListPolicies, []string){ + "pretty-print": prettyPrintListFormat, + "json": jsonListFormat, +} + func ListSupportCommands() []string { commands := []string{} for k := range listFunc { @@ -29,12 +35,8 @@ func CliList(listPolicies *cliobjects.ListPolicies) error { } sort.Strings(policies) - sep := "\n * " - usageCmd := strings.TrimSuffix(listPolicies.Target, "s") - fmt.Printf("Supported %s:%s%s\n", listPolicies.Target, sep, strings.Join(policies, sep)) - fmt.Printf("\nUsage:\n") - fmt.Printf("$ kubescape scan %s \"name\"\n", usageCmd) - fmt.Printf("$ kubescape scan %s \"name-0\",\"name-1\"\n\n", usageCmd) + listFormatFunc[listPolicies.Format](listPolicies, policies) + return nil } return fmt.Errorf("unknown command to download") @@ -57,3 +59,17 @@ func listControls(listPolicies *cliobjects.ListPolicies) ([]string, error) { } return g.ListControls(l) } + +func prettyPrintListFormat(listPolicies *cliobjects.ListPolicies, policies []string) { + sep := "\n * " + usageCmd := strings.TrimSuffix(listPolicies.Target, "s") + fmt.Printf("Supported %s:%s%s\n", listPolicies.Target, sep, strings.Join(policies, sep)) + fmt.Printf("\nUsage:\n") + fmt.Printf("$ kubescape scan %s \"name\"\n", usageCmd) + fmt.Printf("$ kubescape scan %s \"name-0\",\"name-1\"\n\n", usageCmd) +} + +func jsonListFormat(listPolicies *cliobjects.ListPolicies, policies []string) { + j, _ := json.MarshalIndent(policies, "", " ") + fmt.Printf("%s\n", j) +} diff --git a/clihandler/cliobjects/listpolicies.go b/clihandler/cliobjects/listpolicies.go index c8385b0c..1b92540f 100644 --- a/clihandler/cliobjects/listpolicies.go +++ b/clihandler/cliobjects/listpolicies.go @@ -4,4 +4,5 @@ type ListPolicies struct { Target string ListIDs bool Account string + Format string } diff --git a/clihandler/cmd/list.go b/clihandler/cmd/list.go index 0c9706b8..a84af881 100644 --- a/clihandler/cmd/list.go +++ b/clihandler/cmd/list.go @@ -62,6 +62,7 @@ func init() { // cobra.OnInitialize(initConfig) rootCmd.AddCommand(listCmd) - listCmd.PersistentFlags().StringVarP(&listPolicies.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file") + listCmd.PersistentFlags().StringVar(&listPolicies.Account, "account", "", "Armo portal account ID. Default will load account ID from configMap or config file") + listCmd.PersistentFlags().StringVar(&listPolicies.Format, "format", "pretty-print", "output format. supported: 'pretty-printer'/'json'") listCmd.PersistentFlags().BoolVarP(&listPolicies.ListIDs, "id", "", false, "List control ID's instead of controls names") } diff --git a/clihandler/initcli.go b/clihandler/initcli.go index 7979867c..a0dab3c3 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -83,7 +83,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler := getReporter(tenantConfig, scanInfo.Submit) // setup printer - printerHandler := resultshandling.GetPrinter(scanInfo.Format, scanInfo.VerboseMode) + printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.VerboseMode) printerHandler.SetWriter(scanInfo.Output) // ================== return interface ====================================== diff --git a/resultshandling/printer/v1/prettyprinter.go b/resultshandling/printer/v1/prettyprinter.go index 38e046ac..13c5eea6 100644 --- a/resultshandling/printer/v1/prettyprinter.go +++ b/resultshandling/printer/v1/prettyprinter.go @@ -31,6 +31,7 @@ func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { } func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + overallRiskScore := opaSessionObj.Report.SummaryDetails.Score cautils.ReportV2ToV1(opaSessionObj) // score := calculatePostureScore(opaSessionObj.PostureReport) @@ -40,7 +41,6 @@ func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessio frameworkNames := []string{} frameworkScores := []float32{} - var overallRiskScore float32 = 0 for _, frameworkReport := range opaSessionObj.PostureReport.FrameworkReports { frameworkNames = append(frameworkNames, frameworkReport.Name) frameworkScores = append(frameworkScores, frameworkReport.Score) @@ -48,11 +48,8 @@ func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessio warningResources = reporthandling.GetUniqueResourcesIDs(append(warningResources, frameworkReport.ListResourcesIDs().GetWarningResources()...)) allResources = reporthandling.GetUniqueResourcesIDs(append(allResources, frameworkReport.ListResourcesIDs().GetAllResources()...)) prettyPrinter.summarySetup(frameworkReport, opaSessionObj.AllResources) - overallRiskScore += frameworkReport.Score } - overallRiskScore /= float32(len(opaSessionObj.PostureReport.FrameworkReports)) - prettyPrinter.frameworkSummary = ResultSummary{ RiskScore: overallRiskScore, TotalResources: len(allResources), diff --git a/resultshandling/printer/v1/printresults.go b/resultshandling/printer/v1/printresults.go index 035c374f..043020dc 100644 --- a/resultshandling/printer/v1/printresults.go +++ b/resultshandling/printer/v1/printresults.go @@ -1,21 +1,3 @@ package v1 -import ( - "github.com/armosec/kubescape/resultshandling/printer" - "github.com/armosec/kubescape/resultshandling/printer/v2/controlmapping" -) - var INDENT = " " - -func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { - switch printFormat { - case printer.JsonFormat: - return NewJsonPrinter() - case printer.JunitResultFormat: - return NewJunitPrinter() - case printer.PrometheusFormat: - return NewPrometheusPrinter(verboseMode) - default: - return controlmapping.NewPrettyPrinter(verboseMode) - } -} diff --git a/resultshandling/printer/v2/pdf.go b/resultshandling/printer/v2/pdf.go index d594b4e6..4504a541 100644 --- a/resultshandling/printer/v2/pdf.go +++ b/resultshandling/printer/v2/pdf.go @@ -21,7 +21,7 @@ import ( const ( pdfOutputFile = "report" - pdfOutputExt = ".pdf" + pdfOutputExt = ".pdf" ) var ( @@ -125,7 +125,7 @@ func (pdfPrinter *PdfPrinter) printFramework(m pdf.Maroto, frameworks []reportsu p += fmt.Sprintf("%s (risk: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetScore()) } m.Row(10, func() { - m.Text(fmt.Sprintf(p), props.Text{ + m.Text(p, props.Text{ Align: consts.Center, Size: 8, Family: consts.Arial, @@ -167,7 +167,7 @@ func (pdfPrinter *PdfPrinter) printTable(m pdf.Maroto, summaryDetails *reportsum Line: false, }) m.Line(1) - m.Row(2, func(){}) + m.Row(2, func() {}) } // Add final results. diff --git a/resultshandling/printer/v2/printresults.go b/resultshandling/printer/v2/printresults.go index 8dcda6c4..6bed88f0 100644 --- a/resultshandling/printer/v2/printresults.go +++ b/resultshandling/printer/v2/printresults.go @@ -1,23 +1,3 @@ package v2 -import ( - "github.com/armosec/kubescape/resultshandling/printer" - "github.com/armosec/kubescape/resultshandling/printer/v2/resourcemapping" -) - var INDENT = " " - -func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { - switch printFormat { - case printer.JsonFormat: - return resourcemapping.NewJsonPrinter() - case printer.JunitResultFormat: - return NewJunitPrinter() - // case printer.PrometheusFormat: - // return NewPrometheusPrinter(verboseMode) - case printer.PdfFormat: - return NewPdfPrinter() - default: - return resourcemapping.NewPrettyPrinter(verboseMode) - } -} diff --git a/resultshandling/results.go b/resultshandling/results.go index 6e45680a..86662250 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -6,6 +6,7 @@ import ( "github.com/armosec/kubescape/resultshandling/printer" printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1" printerv2 "github.com/armosec/kubescape/resultshandling/printer/v2" + "github.com/armosec/kubescape/resultshandling/printer/v2/controlmapping" "github.com/armosec/kubescape/resultshandling/reporter" "github.com/armosec/opa-utils/reporthandling" @@ -53,7 +54,7 @@ func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 return (float32(len(allResources)) - float32(len(failedResources))) / float32(len(allResources)) } -func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { +func NewPrinter(printFormat string, verboseMode bool) printer.IPrinter { switch printFormat { case printer.JsonFormat: @@ -65,6 +66,6 @@ func GetPrinter(printFormat string, verboseMode bool) printer.IPrinter { case printer.PdfFormat: return printerv2.NewPdfPrinter() default: - return printerv1.NewPrettyPrinter(verboseMode) + return controlmapping.NewPrettyPrinter(verboseMode) } } From 3887ec809162a46a4137f39fc6307501542785d0 Mon Sep 17 00:00:00 2001 From: fsl <1171313930@qq.com> Date: Mon, 21 Feb 2022 16:40:28 +0800 Subject: [PATCH 18/44] fix uuid high-risk vulnerability --- cautils/rbac.go | 4 ++-- clihandler/cmd/results.go | 5 ++--- go.mod | 10 +++++----- resultshandling/reporter/v1/reporteventreceiver.go | 5 ++--- .../reporter/v1/reporteventreceiverutils.go | 4 ++-- resultshandling/reporter/v2/reporteventreceiver.go | 4 ++-- .../reporter/v2/reporteventreceiverutils.go | 4 ++-- 7 files changed, 17 insertions(+), 19 deletions(-) diff --git a/cautils/rbac.go b/cautils/rbac.go index a9d8bfbc..3f729f49 100644 --- a/cautils/rbac.go +++ b/cautils/rbac.go @@ -8,7 +8,7 @@ import ( "github.com/armosec/opa-utils/reporthandling" "github.com/armosec/rbac-utils/rbacscanner" "github.com/armosec/rbac-utils/rbacutils" - uuid "github.com/satori/go.uuid" + "github.com/google/uuid" ) type RBACObjects struct { @@ -21,7 +21,7 @@ func NewRBACObjects(scanner *rbacscanner.RbacScannerFromK8sAPI) *RBACObjects { func (rbacObjects *RBACObjects) SetResourcesReport() (*reporthandling.PostureReport, error) { return &reporthandling.PostureReport{ - ReportID: uuid.NewV4().String(), + ReportID: uuid.NewString(), ReportGenerationTime: time.Now().UTC(), CustomerGUID: rbacObjects.scanner.CustomerGUID, ClusterName: rbacObjects.scanner.ClusterName, diff --git a/clihandler/cmd/results.go b/clihandler/cmd/results.go index f7c09896..73b090ac 100644 --- a/clihandler/cmd/results.go +++ b/clihandler/cmd/results.go @@ -13,7 +13,7 @@ import ( "github.com/armosec/kubescape/clihandler/cliinterfaces" reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" "github.com/armosec/opa-utils/reporthandling" - uuid "github.com/satori/go.uuid" + "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -37,10 +37,9 @@ func (resultsObject *ResultsObject) SetResourcesReport() (*reporthandling.Postur if err != nil { return nil, err } - return &reporthandling.PostureReport{ FrameworkReports: frameworkReports, - ReportID: uuid.NewV4().String(), + ReportID: uuid.NewString(), ReportGenerationTime: time.Now().UTC(), CustomerGUID: resultsObject.customerGUID, ClusterName: resultsObject.clusterName, diff --git a/go.mod b/go.mod index ca9009c0..9988151e 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/armosec/kubescape go 1.17 require ( - github.com/armosec/armoapi-go v0.0.49 + github.com/armosec/armoapi-go v0.0.54 github.com/armosec/k8s-interface v0.0.60 github.com/armosec/opa-utils v0.0.110 github.com/armosec/rbac-utils v0.0.14 @@ -13,12 +13,11 @@ require ( github.com/enescakir/emoji v1.0.0 github.com/fatih/color v1.13.0 github.com/francoispqt/gojay v1.2.13 - github.com/gofrs/uuid v4.1.0+incompatible + github.com/google/uuid v1.3.0 github.com/johnfercher/maroto v0.34.0 github.com/mattn/go-isatty v0.0.14 github.com/olekukonko/tablewriter v0.0.5 github.com/open-policy-agent/opa v0.33.1 - github.com/satori/go.uuid v1.2.0 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 go.uber.org/zap v1.19.1 @@ -62,13 +61,13 @@ require ( github.com/go-gota/gota v0.12.0 // indirect github.com/go-logr/logr v0.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/gofrs/uuid v4.1.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.1.2 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/imdario/mergo v0.3.12 // indirect @@ -87,6 +86,7 @@ require ( github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 // indirect + github.com/satori/go.uuid v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -99,7 +99,7 @@ require ( golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect - golang.org/x/text v0.3.6 // indirect + golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gonum.org/v1/gonum v0.9.1 // indirect google.golang.org/api v0.44.0 // indirect diff --git a/resultshandling/reporter/v1/reporteventreceiver.go b/resultshandling/reporter/v1/reporteventreceiver.go index abdbdcc9..a7c67f9e 100644 --- a/resultshandling/reporter/v1/reporteventreceiver.go +++ b/resultshandling/reporter/v1/reporteventreceiver.go @@ -13,7 +13,7 @@ import ( "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/opa-utils/reporthandling" - uuid "github.com/satori/go.uuid" + "github.com/google/uuid" ) const MAX_REPORT_SIZE = 2097152 // 2 MB @@ -51,8 +51,7 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS.Please feel free to contact ARMO team for more details" return nil } - - opaSessionObj.PostureReport.ReportID = uuid.NewV4().String() + opaSessionObj.PostureReport.ReportID = uuid.NewString() opaSessionObj.PostureReport.CustomerGUID = report.customerGUID opaSessionObj.PostureReport.ClusterName = report.clusterName diff --git a/resultshandling/reporter/v1/reporteventreceiverutils.go b/resultshandling/reporter/v1/reporteventreceiverutils.go index 6b816aec..f16bd1ef 100644 --- a/resultshandling/reporter/v1/reporteventreceiverutils.go +++ b/resultshandling/reporter/v1/reporteventreceiverutils.go @@ -6,7 +6,7 @@ import ( "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/opa-utils/reporthandling" - "github.com/gofrs/uuid" + "github.com/google/uuid" ) func (report *ReportEventReceiver) initEventReceiverURL() { @@ -16,7 +16,7 @@ func (report *ReportEventReceiver) initEventReceiverURL() { urlObj.Host = getter.GetArmoAPIConnector().GetReportReceiverURL() urlObj.Path = "/k8s/postureReport" q := urlObj.Query() - q.Add("customerGUID", uuid.FromStringOrNil(report.customerGUID).String()) + q.Add("customerGUID", uuid.MustParse(report.customerGUID).String()) q.Add("clusterName", report.clusterName) urlObj.RawQuery = q.Encode() diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index 77ba546e..bbc84779 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -11,7 +11,7 @@ import ( "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" - uuid "github.com/satori/go.uuid" + "github.com/google/uuid" "github.com/armosec/opa-utils/reporthandling" "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" @@ -51,7 +51,7 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS" return nil } - opaSessionObj.Report.ReportID = uuid.NewV4().String() + opaSessionObj.Report.ReportID = uuid.NewString() opaSessionObj.Report.CustomerGUID = report.customerGUID opaSessionObj.Report.ClusterName = report.clusterName diff --git a/resultshandling/reporter/v2/reporteventreceiverutils.go b/resultshandling/reporter/v2/reporteventreceiverutils.go index 66c823f3..d1f0f06c 100644 --- a/resultshandling/reporter/v2/reporteventreceiverutils.go +++ b/resultshandling/reporter/v2/reporteventreceiverutils.go @@ -7,7 +7,7 @@ import ( "github.com/armosec/kubescape/cautils/getter" "github.com/armosec/opa-utils/reporthandling" reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2" - "github.com/gofrs/uuid" + "github.com/google/uuid" ) func (report *ReportEventReceiver) initEventReceiverURL() { @@ -18,7 +18,7 @@ func (report *ReportEventReceiver) initEventReceiverURL() { urlObj.Path = "/k8s/v2/postureReport" q := urlObj.Query() - q.Add("customerGUID", uuid.FromStringOrNil(report.customerGUID).String()) + q.Add("customerGUID", uuid.MustParse(report.customerGUID).String()) q.Add("clusterName", report.clusterName) urlObj.RawQuery = q.Encode() From 890c13a91f07538f3b9479be76f999d9f9b731b2 Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 22 Feb 2022 09:53:40 +0200 Subject: [PATCH 19/44] stage url --- cautils/getter/armoapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cautils/getter/armoapi.go b/cautils/getter/armoapi.go index dc482553..ddc9708a 100644 --- a/cautils/getter/armoapi.go +++ b/cautils/getter/armoapi.go @@ -27,7 +27,7 @@ var ( armoFEURL = "portal.armo.cloud" armoAUTHURL = "auth.armo.cloud" - armoStageERURL = "report-ks.eustage2.cyberarmorsoft" + armoStageERURL = "report-ks.eustage2.cyberarmorsoft.com" armoStageBEURL = "api-stage.armo.cloud" armoStageFEURL = "armoui.eustage2.cyberarmorsoft.com" armoStageAUTHURL = "eggauth.eustage2.cyberarmorsoft.com" From cddf7dd8f6c82f261bea94e77460878c435d4b0a Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 22 Feb 2022 11:03:44 +0200 Subject: [PATCH 20/44] handle errors --- resultshandling/reporter/v2/reporteventreceiver.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index bbc84779..5d50403e 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -44,11 +44,12 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe finalizeReport(opaSessionObj) if report.customerGUID == "" { - report.message = "WARNING: Failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account ' flag. Contact ARMO team for more details" + logger.L().Warning("failed to publish results. Reason: Unknown accout ID. Run kubescape with the '--account ' flag. Contact ARMO team for more details") + return nil } if report.clusterName == "" { - report.message = "WARNING: Failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS" + logger.L().Warning("failed to publish results because the cluster name is Unknown. If you are scanning YAML files the results are not submitted to the Kubescape SaaS") return nil } opaSessionObj.Report.ReportID = uuid.NewString() @@ -56,7 +57,7 @@ func (report *ReportEventReceiver) ActionSendReport(opaSessionObj *cautils.OPASe opaSessionObj.Report.ClusterName = report.clusterName if err := report.prepareReport(opaSessionObj.Report); err != nil { - report.message = err.Error() + logger.L().Error("failed to publish results", helpers.Error(err)) } else { report.generateMessage() } @@ -229,5 +230,7 @@ func (report *ReportEventReceiver) generateMessage() { } func (report *ReportEventReceiver) DisplayReportURL() { - cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s\n\n", report.message)) + if report.message != "" { + cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n\n%s\n\n", report.message)) + } } From 2c5bb977cba4229bec0703f56075ea22278f6066 Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 22 Feb 2022 14:46:25 +0200 Subject: [PATCH 21/44] ignore empty framewprks --- cautils/datastructuresmethods.go | 3 ++- cautils/scaninfo.go | 5 +++-- clihandler/clidownload.go | 15 +++++++++------ clihandler/cmd/config.go | 11 ++++------- clihandler/cmd/list.go | 5 ++--- resourcehandler/ekssupport.go | 4 ++-- 6 files changed, 22 insertions(+), 21 deletions(-) diff --git a/cautils/datastructuresmethods.go b/cautils/datastructuresmethods.go index cee394d7..655445bf 100644 --- a/cautils/datastructuresmethods.go +++ b/cautils/datastructuresmethods.go @@ -15,7 +15,7 @@ func NewPolicies() *Policies { func (policies *Policies) Set(frameworks []reporthandling.Framework, version string) { for i := range frameworks { - if frameworks[i].Name != "" { + if frameworks[i].Name != "" && len(frameworks[i].Controls) > 0 { policies.Frameworks = append(policies.Frameworks, frameworks[i].Name) } for j := range frameworks[i].Controls { @@ -30,6 +30,7 @@ func (policies *Policies) Set(frameworks []reporthandling.Framework, version str policies.Controls[frameworks[i].Controls[j].ControlID] = frameworks[i].Controls[j] } } + } } diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index e6ea9198..de242233 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -4,12 +4,13 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "os" "path/filepath" "strings" "github.com/armosec/kubescape/cautils/getter" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/opa-utils/reporthandling" ) @@ -105,7 +106,7 @@ func (scanInfo *ScanInfo) setUseArtifactsFrom() { // set frameworks files files, err := ioutil.ReadDir(scanInfo.UseArtifactsFrom) if err != nil { - log.Fatal(err) + logger.L().Fatal("failed to read files from directory", helpers.String("dir", scanInfo.UseArtifactsFrom), helpers.Error(err)) } framework := &reporthandling.Framework{} for _, f := range files { diff --git a/clihandler/clidownload.go b/clihandler/clidownload.go index ec9b02d3..f3d272e5 100644 --- a/clihandler/clidownload.go +++ b/clihandler/clidownload.go @@ -132,11 +132,12 @@ func downloadFramework(downloadInfo *cautils.DownloadInfo) error { return err } for _, fw := range frameworks { - err = getter.SaveInFile(fw, filepath.Join(downloadInfo.Path, (strings.ToLower(fw.Name)+".json"))) + downloadTo := filepath.Join(downloadInfo.Path, (strings.ToLower(fw.Name) + ".json")) + err = getter.SaveInFile(fw, downloadTo) if err != nil { return err } - logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", fw.Name), helpers.String("path", filepath.Join(downloadInfo.Path, downloadInfo.FileName))) + logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", fw.Name), helpers.String("path", downloadTo)) } // return fmt.Errorf("missing framework name") } else { @@ -147,11 +148,12 @@ func downloadFramework(downloadInfo *cautils.DownloadInfo) error { if err != nil { return err } - err = getter.SaveInFile(framework, filepath.Join(downloadInfo.Path, downloadInfo.FileName)) + downloadTo := filepath.Join(downloadInfo.Path, downloadInfo.FileName) + err = getter.SaveInFile(framework, downloadTo) if err != nil { return err } - logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", framework.Name), helpers.String("path", filepath.Join(downloadInfo.Path, downloadInfo.FileName))) + logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", framework.Name), helpers.String("path", downloadTo)) } return nil } @@ -173,10 +175,11 @@ func downloadControl(downloadInfo *cautils.DownloadInfo) error { if err != nil { return err } - err = getter.SaveInFile(controls, filepath.Join(downloadInfo.Path, downloadInfo.FileName)) + downloadTo := filepath.Join(downloadInfo.Path, downloadInfo.FileName) + err = getter.SaveInFile(controls, downloadTo) if err != nil { return err } - logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", downloadInfo.Name), helpers.String("path", filepath.Join(downloadInfo.Path, downloadInfo.FileName))) + logger.L().Success("Downloaded", helpers.String("artifact", downloadInfo.Target), helpers.String("name", downloadInfo.Name), helpers.String("path", downloadTo)) return nil } diff --git a/clihandler/cmd/config.go b/clihandler/cmd/config.go index bd759f4f..3bb9dc3f 100644 --- a/clihandler/cmd/config.go +++ b/clihandler/cmd/config.go @@ -2,9 +2,9 @@ package cmd import ( "fmt" - "os" "strings" + "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/clihandler" "github.com/armosec/kubescape/clihandler/cliobjects" "github.com/spf13/cobra" @@ -53,8 +53,7 @@ var configSetCmd = &cobra.Command{ return err } if err := clihandler.CliSetConfig(&setConfig); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + logger.L().Fatal(err.Error()) } return nil }, @@ -100,8 +99,7 @@ var configDeleteCmd = &cobra.Command{ Long: ``, Run: func(cmd *cobra.Command, args []string) { if err := clihandler.CliDelete(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + logger.L().Fatal(err.Error()) } }, } @@ -113,8 +111,7 @@ var configViewCmd = &cobra.Command{ Long: ``, Run: func(cmd *cobra.Command, args []string) { if err := clihandler.CliView(); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + logger.L().Fatal(err.Error()) } }, } diff --git a/clihandler/cmd/list.go b/clihandler/cmd/list.go index a84af881..b5004943 100644 --- a/clihandler/cmd/list.go +++ b/clihandler/cmd/list.go @@ -2,10 +2,10 @@ package cmd import ( "fmt" - "os" "strings" "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/clihandler" "github.com/armosec/kubescape/clihandler/cliobjects" "github.com/spf13/cobra" @@ -51,8 +51,7 @@ var listCmd = &cobra.Command{ listPolicies.Target = args[0] if err := clihandler.CliList(&listPolicies); err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) + logger.L().Fatal(err.Error()) } return nil }, diff --git a/resourcehandler/ekssupport.go b/resourcehandler/ekssupport.go index 21efc221..29c9998d 100644 --- a/resourcehandler/ekssupport.go +++ b/resourcehandler/ekssupport.go @@ -42,7 +42,7 @@ func (eksProviderEnvVar *EKSProviderEnvVar) getRegionForEKS(cluster string) (str } splittedClusterContext := strings.Split(cluster, ".") if len(splittedClusterContext) < 2 { - return "", fmt.Errorf("error: failed to get region") + return "", fmt.Errorf("failed to get region") } region = splittedClusterContext[1] return region, nil @@ -90,7 +90,7 @@ func (eksProviderContext *EKSProviderContext) getKubeCluster() string { func (eksProviderContext *EKSProviderContext) getRegion(cluster string, provider string) (string, error) { splittedClusterContext := strings.Split(cluster, ".") if len(splittedClusterContext) < 2 { - return "", fmt.Errorf("error: failed to get region") + return "", fmt.Errorf("failed to get region") } region := splittedClusterContext[1] return region, nil From 0d1c4cdc028e5dc4987f7c99c178f70e7292ca3e Mon Sep 17 00:00:00 2001 From: Avinash Upadhyaya Date: Wed, 23 Feb 2022 10:24:14 +0530 Subject: [PATCH 22/44] fix: argument validation for framework/control --- clihandler/cmd/control.go | 6 ++++-- clihandler/cmd/framework.go | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/clihandler/cmd/control.go b/clihandler/cmd/control.go index 827fd4f8..15c7f083 100644 --- a/clihandler/cmd/control.go +++ b/clihandler/cmd/control.go @@ -40,8 +40,10 @@ var controlCmd = &cobra.Command{ if len(args) > 0 { controls := strings.Split(args[0], ",") if len(controls) > 1 { - if controls[1] == "" { - return fmt.Errorf("usage: ,") + for _, control := range controls { + if control == "" { + return fmt.Errorf("usage: ,") + } } } } else { diff --git a/clihandler/cmd/framework.go b/clihandler/cmd/framework.go index f06c9c80..4cd17c86 100644 --- a/clihandler/cmd/framework.go +++ b/clihandler/cmd/framework.go @@ -51,8 +51,10 @@ var frameworkCmd = &cobra.Command{ if len(args) > 0 { frameworks := strings.Split(args[0], ",") if len(frameworks) > 1 { - if frameworks[1] == "" { - return fmt.Errorf("usage: ,") + for _, framework := range frameworks { + if framework == "" { + return fmt.Errorf("usage: ,") + } } } } else { From 155349dac0265b4c317577108164e5d6cdf294a5 Mon Sep 17 00:00:00 2001 From: dwertent Date: Wed, 23 Feb 2022 15:54:59 +0200 Subject: [PATCH 23/44] adding resource table --- hostsensorutils/hostsensordeploy.go | 2 +- resultshandling/printer/v2/controltable.go | 36 +++ resultshandling/printer/v2/pdf.go | 48 +--- .../v2/{controlmapping => }/prettyprinter.go | 43 +--- .../v2/resourcemapping/prettyprinter.go | 215 ------------------ resultshandling/printer/v2/resourcetable.go | 93 ++++++++ .../v2/{controlmapping => }/summeryhelpers.go | 2 +- resultshandling/results.go | 3 +- 8 files changed, 143 insertions(+), 299 deletions(-) create mode 100644 resultshandling/printer/v2/controltable.go rename resultshandling/printer/v2/{controlmapping => }/prettyprinter.go (85%) delete mode 100644 resultshandling/printer/v2/resourcemapping/prettyprinter.go create mode 100644 resultshandling/printer/v2/resourcetable.go rename resultshandling/printer/v2/{controlmapping => }/summeryhelpers.go (99%) diff --git a/hostsensorutils/hostsensordeploy.go b/hostsensorutils/hostsensordeploy.go index ca919dd8..0fd080c3 100644 --- a/hostsensorutils/hostsensordeploy.go +++ b/hostsensorutils/hostsensordeploy.go @@ -151,7 +151,7 @@ func (hsh *HostSensorHandler) checkPodForEachNode() error { hsh.podListLock.RLock() podsMap := hsh.HostSensorPodNames hsh.podListLock.RUnlock() - return fmt.Errorf("host-sensor pods number (%d) differ than nodes number (%d) after deadline exceded. We will take data only from the pods below: %v", + return fmt.Errorf("host-sensor pods number (%d) differ than nodes number (%d) after deadline exceeded. Kubescape will take data only from the pods below: %v", podsNum, len(nodesList.Items), podsMap) } time.Sleep(100 * time.Millisecond) diff --git a/resultshandling/printer/v2/controltable.go b/resultshandling/printer/v2/controltable.go new file mode 100644 index 00000000..ea26300b --- /dev/null +++ b/resultshandling/printer/v2/controltable.go @@ -0,0 +1,36 @@ +package v2 + +import ( + "fmt" + "sort" + + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" +) + +func generateRow(controlSummary reportsummary.IControlSummary) []string { + row := []string{controlSummary.GetName()} + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed())) + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Excluded())) + row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().All())) + + if !controlSummary.GetStatus().IsSkipped() { + row = append(row, fmt.Sprintf("%d", int(controlSummary.GetScore()))+"%") + } else { + row = append(row, "skipped") + } + return row +} + +func getSortedControlsNames(controls reportsummary.ControlSummaries) []string { + controlNames := make([]string, 0, len(controls)) + for k := range controls { + c := controls[k] + controlNames = append(controlNames, c.GetName()) + } + sort.Strings(controlNames) + return controlNames +} + +func getControlTableHeaders() []string { + return []string{"CONTROL NAME", "FAILED RESOURCES", "EXCLUDED RESOURCES", "ALL RESOURCES", "% RISK-SCORE"} +} diff --git a/resultshandling/printer/v2/pdf.go b/resultshandling/printer/v2/pdf.go index 4504a541..449fbacd 100644 --- a/resultshandling/printer/v2/pdf.go +++ b/resultshandling/printer/v2/pdf.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "sort" "strings" "time" @@ -111,21 +110,8 @@ func (pdfPrinter *PdfPrinter) printHeader(m pdf.Maroto) { // Print pdf frameworks after pdf header. func (pdfPrinter *PdfPrinter) printFramework(m pdf.Maroto, frameworks []reportsummary.IPolicies) { - var p string - if len(frameworks) == 1 { - if frameworks[0].GetName() != "" { - p = fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName()) - } - } else if len(frameworks) > 1 { - p = "FRAMEWORKS: " - i := 0 - for ; i < len(frameworks)-1; i++ { - p += fmt.Sprintf("%s (risk: %.2f), ", frameworks[i].GetName(), frameworks[i].GetScore()) - } - p += fmt.Sprintf("%s (risk: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetScore()) - } m.Row(10, func() { - m.Text(p, props.Text{ + m.Text(frameworksScoresToString(frameworks), props.Text{ Align: consts.Center, Size: 8, Family: consts.Arial, @@ -136,8 +122,7 @@ func (pdfPrinter *PdfPrinter) printFramework(m pdf.Maroto, frameworks []reportsu // Create pdf table func (pdfPrinter *PdfPrinter) printTable(m pdf.Maroto, summaryDetails *reportsummary.SummaryDetails) { - headers := []string{"CONTROL NAME", "FAILED RESOURCES", "EXCLUDED RESOURCES", "ALL RESOURCES", "% RISK-SCORE"} - + headers := getControlTableHeaders() controls := make([][]string, len(pdfPrinter.sortedControlNames)) for i := range controls { controls[i] = make([]string, len(headers)) @@ -215,32 +200,3 @@ func (pdfPrinter *PdfPrinter) printFinalResult(m pdf.Maroto, summaryDetails *rep }) }) } - -// Return sorted control names. -func getSortedControlsNames(controls reportsummary.ControlSummaries) []string { - controlNames := make([]string, 0, len(controls)) - for k := range controls { - c := controls[k] - controlNames = append(controlNames, c.GetName()) - } - sort.Strings(controlNames) - return controlNames -} - -// Generate rows to be appended to the table. -func generateRow(controlSummary reportsummary.IControlSummary) []string { - // We use ReplaceAll because some control name - // has a too long name that unformat the pdf view. - controlName := strings.ReplaceAll(controlSummary.GetName(), "-", " ") - row := []string{controlName} - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed())) - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Excluded())) - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().All())) - - if !controlSummary.GetStatus().IsSkipped() { - row = append(row, fmt.Sprintf("%d", int(controlSummary.GetScore()))+"%") - } else { - row = append(row, "skipped") - } - return row -} diff --git a/resultshandling/printer/v2/controlmapping/prettyprinter.go b/resultshandling/printer/v2/prettyprinter.go similarity index 85% rename from resultshandling/printer/v2/controlmapping/prettyprinter.go rename to resultshandling/printer/v2/prettyprinter.go index dec73734..b380e680 100644 --- a/resultshandling/printer/v2/controlmapping/prettyprinter.go +++ b/resultshandling/printer/v2/prettyprinter.go @@ -1,4 +1,4 @@ -package controlmapping +package v2 import ( "fmt" @@ -31,6 +31,7 @@ func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All()) + // prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails) @@ -152,25 +153,6 @@ func generateRelatedObjectsStr(workload WorkloadSummary) string { } return relatedStr } - -func generateRow(controlSummary reportsummary.IControlSummary) []string { - row := []string{controlSummary.GetName()} - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Failed())) - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().Excluded())) - row = append(row, fmt.Sprintf("%d", controlSummary.NumberOfResources().All())) - - if !controlSummary.GetStatus().IsSkipped() { - row = append(row, fmt.Sprintf("%d", int(controlSummary.GetScore()))+"%") - } else { - row = append(row, "skipped") - } - return row -} - -func generateHeader() []string { - return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% risk-score"} -} - func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string { // Control name | # failed resources | all resources | % success row := []string{} @@ -184,11 +166,11 @@ func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string { } func (prettyPrinter *PrettyPrinter) printSummaryTable(summaryDetails *reportsummary.SummaryDetails) { // For control scan framework will be nil - prettyPrinter.printFramework(summaryDetails.ListFrameworks().All()) + cautils.InfoTextDisplay(prettyPrinter.writer, frameworksScoresToString(summaryDetails.ListFrameworks().All())) summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(false) - summaryTable.SetHeader(generateHeader()) + summaryTable.SetHeader(getControlTableHeaders()) summaryTable.SetHeaderLine(true) alignments := []int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_CENTER} summaryTable.SetColumnAlignment(alignments) @@ -203,10 +185,11 @@ func (prettyPrinter *PrettyPrinter) printSummaryTable(summaryDetails *reportsumm summaryTable.Render() } -func (prettyPrinter *PrettyPrinter) printFramework(frameworks []reportsummary.IPolicies) { +func frameworksScoresToString(frameworks []reportsummary.IPolicies) string { if len(frameworks) == 1 { if frameworks[0].GetName() != "" { - cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName())) + return fmt.Sprintf("FRAMEWORK %s\n", frameworks[0].GetName()) + // cautils.InfoTextDisplay(prettyPrinter.writer, )) } } else if len(frameworks) > 1 { p := "FRAMEWORKS: " @@ -215,17 +198,9 @@ func (prettyPrinter *PrettyPrinter) printFramework(frameworks []reportsummary.IP p += fmt.Sprintf("%s (risk: %.2f), ", frameworks[i].GetName(), frameworks[i].GetScore()) } p += fmt.Sprintf("%s (risk: %.2f)\n", frameworks[i].GetName(), frameworks[i].GetScore()) - cautils.InfoTextDisplay(prettyPrinter.writer, p) + return p } -} -func getSortedControlsNames(controls reportsummary.ControlSummaries) []string { - controlNames := make([]string, 0, len(controls)) - for k := range controls { - c := controls[k] - controlNames = append(controlNames, c.GetName()) - } - sort.Strings(controlNames) - return controlNames + return "" } // func getSortedControlsNames(controls []reportsummary.IPolicies) []string { diff --git a/resultshandling/printer/v2/resourcemapping/prettyprinter.go b/resultshandling/printer/v2/resourcemapping/prettyprinter.go deleted file mode 100644 index 49b29002..00000000 --- a/resultshandling/printer/v2/resourcemapping/prettyprinter.go +++ /dev/null @@ -1,215 +0,0 @@ -package resourcemapping - -import ( - "fmt" - "os" - "strings" - - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/resultshandling/printer" - "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" - "github.com/olekukonko/tablewriter" -) - -type PrettyPrinter struct { - writer *os.File - verboseMode bool -} - -func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { - return &PrettyPrinter{ - verboseMode: verboseMode, - } -} - -func (prettyPrinter *PrettyPrinter) printResourceTable(results []resourcesresults.Result) { - - summaryTable := tablewriter.NewWriter(prettyPrinter.writer) - summaryTable.SetAutoWrapText(true) - summaryTable.SetAutoMergeCells(false) - // summaryTable.SetCenterSeparator("=") - // summaryTable.SetRowSeparator("*") - // summaryTable. - summaryTable.SetHeader(generateResourceHeader()) - summaryTable.SetHeaderLine(true) - - // For control scan framework will be nil - for i := range results { - // status := result.GetStatus(nil).Status() - resourceID := results[i].GetResourceID() - control := results[i].ListControls() - if raw := generateResourceRow(resourceID, control, prettyPrinter.verboseMode); len(raw) > 0 { - summaryTable.Append(raw) - } - } - - // 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(prettyPrinter.sortedControlNames); i++ { - // controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]] - // summaryTable.Append(generateRow(prettyPrinter.sortedControlNames[i], controlSummary)) - // } - - // summaryTable.SetFooter(generateFooter(prettyPrinter)) - - summaryTable.Render() -} - -func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - - prettyPrinter.printResourceTable(opaSessionObj.Report.Results) - // var overallRiskScore float32 = 0 - // for _, frameworkReport := range opaSessionObj.PostureReport.FrameworkReports { - // frameworkNames = append(frameworkNames, frameworkReport.Name) - // frameworkScores = append(frameworkScores, frameworkReport.Score) - // failedResources = reporthandling.GetUniqueResourcesIDs(append(failedResources, frameworkReport.ListResourcesIDs().GetFailedResources()...)) - // warningResources = reporthandling.GetUniqueResourcesIDs(append(warningResources, frameworkReport.ListResourcesIDs().GetWarningResources()...)) - // allResources = reporthandling.GetUniqueResourcesIDs(append(allResources, frameworkReport.ListResourcesIDs().GetAllResources()...)) - // prettyPrinter.summarySetup(frameworkReport, opaSessionObj.AllResources) - // overallRiskScore += frameworkReport.Score - // } - - // overallRiskScore /= float32(len(opaSessionObj.PostureReport.FrameworkReports)) - - // prettyPrinter.frameworkSummary = ResultSummary{ - // RiskScore: overallRiskScore, - // TotalResources: len(allResources), - // TotalFailed: len(failedResources), - // TotalWarning: len(warningResources), - // } - - // prettyPrinter.printResults() - // prettyPrinter.printSummaryTable(frameworkNames, frameworkScores) - -} - -func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) { - prettyPrinter.writer = printer.GetWriter(outputFile) -} - -func (prettyPrinter *PrettyPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) { - // finalizeReport(opaSessionObj) -} -func (prettyPrinter *PrettyPrinter) Score(score float32) { -} - -// func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSummary *ResultSummary) { -// // cautils.SimpleDisplay(prettyPrinter.writer, "Summary - ") -// // cautils.SuccessDisplay(prettyPrinter.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarning) -// // cautils.WarningDisplay(prettyPrinter.writer, "Excluded:%v ", controlSummary.TotalWarning) -// // cautils.FailureDisplay(prettyPrinter.writer, "Failed:%v ", controlSummary.TotalFailed) -// // cautils.InfoDisplay(prettyPrinter.writer, "Total:%v\n", controlSummary.TotalResources) -// // if controlSummary.TotalFailed > 0 { -// // cautils.DescriptionDisplay(prettyPrinter.writer, "Remediation: %v\n", controlSummary.Remediation) -// // } -// // cautils.DescriptionDisplay(prettyPrinter.writer, "\n") - -// } - -func generateResourceRow(resourceID string, controls []resourcesresults.ResourceAssociatedControl, verboseMode bool) []string { - row := []string{} - - controlsNames := []string{} - statuses := []string{} - - for i := range controls { - if !verboseMode && controls[i].GetStatus(nil).IsPassed() { - continue - } - if controls[i].GetName() == "" { - continue - } - controlsNames = append(controlsNames, controls[i].GetName()) - statuses = append(statuses, string(controls[i].GetStatus(nil).Status())) - } - - splitted := strings.Split(resourceID, "/") - if len(splitted) < 5 || len(controlsNames) == 0 { - return row - } - - row = append(row, splitted[3]) - row = append(row, splitted[4]) - row = append(row, splitted[2]) - - row = append(row, strings.Join(controlsNames, "\n")) - row = append(row, strings.Join(statuses, "\n")) - - return row -} - -// func generateRow(control string, cs ResultSummary) []string { -// row := []string{control} -// row = append(row, cs.ToSlice()...) -// if cs.TotalResources != 0 { -// row = append(row, fmt.Sprintf("%d", int(cs.RiskScore))+"%") -// } else { -// row = append(row, "skipped") -// } -// return row -// } - -func generateResourceHeader() []string { - return []string{"Kind", "Name", "Namespace", "Controls", "Statues"} -} -func generateHeader() []string { - return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% risk-score"} -} - -func generateFooter(prettyPrinter *PrettyPrinter) []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", prettyPrinter.frameworkSummary.TotalFailed)) - // row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalWarning)) - // row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalResources)) - // row = append(row, fmt.Sprintf("%.2f%s", prettyPrinter.frameworkSummary.RiskScore, "%")) - - return row -} -func (prettyPrinter *PrettyPrinter) printSummaryTable(frameworksNames []string, frameworkScores []float32) { - // For control scan framework will be nil - prettyPrinter.printFramework(frameworksNames, frameworkScores) - - summaryTable := tablewriter.NewWriter(prettyPrinter.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(prettyPrinter.sortedControlNames); i++ { - // controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]] - // summaryTable.Append(generateRow(prettyPrinter.sortedControlNames[i], controlSummary)) - // } - - // summaryTable.SetFooter(generateFooter(prettyPrinter)) - - summaryTable.Render() -} - -func (prettyPrinter *PrettyPrinter) printFramework(frameworksNames []string, frameworkScores []float32) { - if len(frameworksNames) == 1 { - cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworksNames[0])) - } else if len(frameworksNames) > 1 { - p := "FRAMEWORKS: " - for i := 0; i < len(frameworksNames)-1; i++ { - p += fmt.Sprintf("%s (risk: %.2f), ", frameworksNames[i], frameworkScores[i]) - } - p += fmt.Sprintf("%s (risk: %.2f)\n", frameworksNames[len(frameworksNames)-1], frameworkScores[len(frameworkScores)-1]) - cautils.InfoTextDisplay(prettyPrinter.writer, p) - } -} - -// func (prettyPrinter *PrettyPrinter) getSortedControlsNames() []string { -// controlNames := make([]string, 0, len(prettyPrinter.summary)) -// for k := range prettyPrinter.summary { -// controlNames = append(controlNames, k) -// } -// sort.Strings(controlNames) -// return controlNames -// } -// func getControlURL(controlID string) string { -// return fmt.Sprintf("https://hub.armo.cloud/docs/%s", strings.ToLower(controlID)) -// } diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go new file mode 100644 index 00000000..bfaaa15e --- /dev/null +++ b/resultshandling/printer/v2/resourcetable.go @@ -0,0 +1,93 @@ +package v2 + +import ( + "fmt" + "strings" + + "github.com/armosec/k8s-interface/workloadinterface" + "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" + "github.com/olekukonko/tablewriter" +) + +func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { + + summaryTable := tablewriter.NewWriter(prettyPrinter.writer) + summaryTable.SetAutoWrapText(true) + summaryTable.SetAutoMergeCells(true) + summaryTable.SetHeader(generateResourceHeader()) + summaryTable.SetHeaderLine(true) + summaryTable.SetRowLine(true) + // summaryTable.SetFooter([]string{"", "", "Total", "", "$146.93"}) + // For control scan framework will be nil + data := [][]string{} + for i := range results { + resource, ok := allResources[i] + if !ok { + continue + } + s := results[i] + if raw := generateResourceRows(resource, s.ListControls(), prettyPrinter.verboseMode); len(raw) > 0 { + data = append(data, raw...) + } + } + sortTable(data) + summaryTable.AppendBulk(data) + + summaryTable.Render() +} + +func generateResourceRows(resource workloadinterface.IMetadata, controls []resourcesresults.ResourceAssociatedControl, verboseMode bool) [][]string { + rows := [][]string{} + + for i := range controls { + + if controls[i].GetName() == "" { + continue + } + row := []string{} + + if !verboseMode && controls[i].GetStatus(nil).IsPassed() { + continue + } + + row = append(row, fmt.Sprintf("%s\nhttps://hub.armo.cloud/docs/%s", controls[i].GetName(), strings.ToLower(controls[i].GetID()))) + row = append(row, resource.GetNamespace()) + var paths []string + for j := range controls[i].ResourceAssociatedRules { + for k := range controls[i].ResourceAssociatedRules[j].Paths { + if p := controls[i].ResourceAssociatedRules[j].Paths[k].FailedPath; p != "" { + paths = append(paths, p) + } + if p := controls[i].ResourceAssociatedRules[j].Paths[k].FixPath.Path; p != "" { + v := controls[i].ResourceAssociatedRules[j].Paths[k].FixPath.Value + paths = append(paths, fmt.Sprintf("%s=%s", p, v)) + } + } + } + row = append(row, fmt.Sprintf("%s/%s\n%s", resource.GetKind(), resource.GetName(), strings.Join(paths, ";\n"))) + row = append(row, string(controls[i].GetStatus(nil).Status())) + rows = append(rows, row) + } + + return rows +} + +func generateResourceHeader() []string { + return []string{"Control", "Namespace", "Kind/Name", "Statues"} +} + +func sortTable(data [][]string) { + + for j := len(data[0]) - 1; j >= 0; j-- { + for k := 0; k < len(data)-2; { + if data[k][j] > data[k+1][j] { + tmp := data[k] + data[k] = data[k+1] + data[k+1] = tmp + k = 0 + } else { + k++ + } + } + } +} diff --git a/resultshandling/printer/v2/controlmapping/summeryhelpers.go b/resultshandling/printer/v2/summeryhelpers.go similarity index 99% rename from resultshandling/printer/v2/controlmapping/summeryhelpers.go rename to resultshandling/printer/v2/summeryhelpers.go index cc606f8a..09d0f2e8 100644 --- a/resultshandling/printer/v2/controlmapping/summeryhelpers.go +++ b/resultshandling/printer/v2/summeryhelpers.go @@ -1,4 +1,4 @@ -package controlmapping +package v2 import ( "github.com/armosec/k8s-interface/k8sinterface" diff --git a/resultshandling/results.go b/resultshandling/results.go index 86662250..1736a9f8 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -6,7 +6,6 @@ import ( "github.com/armosec/kubescape/resultshandling/printer" printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1" printerv2 "github.com/armosec/kubescape/resultshandling/printer/v2" - "github.com/armosec/kubescape/resultshandling/printer/v2/controlmapping" "github.com/armosec/kubescape/resultshandling/reporter" "github.com/armosec/opa-utils/reporthandling" @@ -66,6 +65,6 @@ func NewPrinter(printFormat string, verboseMode bool) printer.IPrinter { case printer.PdfFormat: return printerv2.NewPdfPrinter() default: - return controlmapping.NewPrettyPrinter(verboseMode) + return printerv2.NewPrettyPrinter(verboseMode) } } From d5fcbe842fc62943c68c44ee2b341f2430e7341f Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 24 Feb 2022 09:50:12 +0200 Subject: [PATCH 24/44] adding autocompletion --- clihandler/cmd/completion.go | 43 ++++++++++++++++++++++++++++++++++++ go.sum | 9 +++++--- 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 clihandler/cmd/completion.go diff --git a/clihandler/cmd/completion.go b/clihandler/cmd/completion.go new file mode 100644 index 00000000..7613441f --- /dev/null +++ b/clihandler/cmd/completion.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "os" + "strings" + + "github.com/spf13/cobra" +) + +var completionCmdExamples = ` + + # Enable BASH shell autocompletion + echo 'source <(kubescape completion bash)' >> ~/.bashrc + + # Enable ZSH shell autocompletion + echo 'source <(kubectl completion zsh)' >> "${fpath[1]}/_kubectl" + +` +var completionCmd = &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell]", + Short: "Generate autocompletion script", + Long: "To load completions", + Example: completionCmdExamples, + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + Args: cobra.ExactValidArgs(1), + Run: func(cmd *cobra.Command, args []string) { + switch strings.ToLower(args[0]) { + case "bash": + cmd.Root().GenBashCompletion(os.Stdout) + case "zsh": + cmd.Root().GenZshCompletion(os.Stdout) + case "fish": + cmd.Root().GenFishCompletion(os.Stdout, true) + case "powershell": + cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout) + } + }, +} + +func init() { + rootCmd.AddCommand(completionCmd) +} diff --git a/go.sum b/go.sum index 6c8dffcb..99dfc09a 100644 --- a/go.sum +++ b/go.sum @@ -85,8 +85,9 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qmSkApyq5xFs= github.com/armosec/armoapi-go v0.0.23/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= -github.com/armosec/armoapi-go v0.0.49 h1:b3gvZ5YB5DSEfk8pt7x0705b4Pcuahd3wI/ZmGYmB3Y= github.com/armosec/armoapi-go v0.0.49/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= +github.com/armosec/armoapi-go v0.0.54 h1:8PjT7qpvt5YsR0OxRRoQ891IOaWThlTJvVgRVReBBLA= +github.com/armosec/armoapi-go v0.0.54/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM= github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU= github.com/armosec/k8s-interface v0.0.50/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU= @@ -363,8 +364,9 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK9YM6zKx4D2qucQU= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= @@ -947,8 +949,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 36b384036266d9fc7b4851d003a422d9f5037604 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 24 Feb 2022 12:09:47 +0200 Subject: [PATCH 25/44] support host sensor from local file --- cautils/scaninfo.go | 3 ++- clihandler/cmd/root.go | 3 ++- clihandler/cmd/scan.go | 5 ++++- clihandler/initcliutils.go | 8 ++++---- hostsensorutils/hostsensordeploy.go | 19 ++++++++++++++++++- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index de242233..0030fc36 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -70,7 +70,8 @@ type ScanInfo struct { Silent bool // Silent mode - Do not print progress logs FailThreshold uint16 // Failure score threshold Submit bool // Submit results to Armo BE - HostSensor BoolPtrFlag // Deploy ARMO K8s host sensor to collect data from certain controls + HostSensorEnabled BoolPtrFlag // Deploy ARMO K8s host sensor to collect data from certain controls + HostSensorYamlPath string // Path to hostsensor file Local bool // Do not submit results Account string // account ID Logger string // logger level diff --git a/clihandler/cmd/root.go b/clihandler/cmd/root.go index 96245611..8fc1d615 100644 --- a/clihandler/cmd/root.go +++ b/clihandler/cmd/root.go @@ -35,13 +35,14 @@ var rootCmd = &cobra.Command{ Use: "kubescape", Version: cautils.BuildNumber, Short: "Kubescape is a tool for testing Kubernetes security posture", - Long: `Kubescape is a tool for testing Kubernetes security posture based on NSA \ MITRE ATT&CKยฎ and other frameworks specifications`, + Long: `Based on NSA \ MITRE ATT&CKยฎ and other frameworks specifications`, Example: ksExamples, } func Execute() { rootCmd.Execute() } + func init() { cobra.OnInitialize(initLogger, initLoggerLevel, initEnvironment, initCacheDir) diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index b2861228..ef1587d2 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -58,7 +58,10 @@ func init() { scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") - hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensor, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls") + scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") + scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") + + hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls") hostF.NoOptDefVal = "true" hostF.DefValue = "false, for no TTY in stdin" diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index 1a364ea1..a466690d 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -74,12 +74,12 @@ func getHostSensorHandler(scanInfo *cautils.ScanInfo, k8s *k8sinterface.Kubernet hasHostSensorControls := true // we need to determined which controls needs host sensor - if scanInfo.HostSensor.Get() == nil && hasHostSensorControls { - scanInfo.HostSensor.SetBool(askUserForHostSensor()) + if scanInfo.HostSensorEnabled.Get() == nil && hasHostSensorControls { + scanInfo.HostSensorEnabled.SetBool(askUserForHostSensor()) logger.L().Warning("Kubernetes cluster nodes scanning is disabled. This is required to collect valuable data for certain controls. You can enable it using the --enable-host-scan flag") } - if hostSensorVal := scanInfo.HostSensor.Get(); hostSensorVal != nil && *hostSensorVal { - hostSensorHandler, err := hostsensorutils.NewHostSensorHandler(k8s) + if hostSensorVal := scanInfo.HostSensorEnabled.Get(); hostSensorVal != nil && *hostSensorVal { + hostSensorHandler, err := hostsensorutils.NewHostSensorHandler(k8s, scanInfo.HostSensorYamlPath) if err != nil { logger.L().Warning(fmt.Sprintf("failed to create host sensor: %s", err.Error())) return &hostsensorutils.HostSensorHandlerMock{} diff --git a/hostsensorutils/hostsensordeploy.go b/hostsensorutils/hostsensordeploy.go index 0fd080c3..efe6adf0 100644 --- a/hostsensorutils/hostsensordeploy.go +++ b/hostsensorutils/hostsensordeploy.go @@ -4,6 +4,7 @@ import ( _ "embed" "fmt" "io" + "os" "strings" "sync" "time" @@ -37,11 +38,18 @@ type HostSensorHandler struct { gracePeriod int64 } -func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi) (*HostSensorHandler, error) { +func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi, hostSensorYAMLFile string) (*HostSensorHandler, error) { if k8sObj == nil { return nil, fmt.Errorf("nil k8s interface received") } + if hostSensorYAMLFile != "" { + d, err := loadHostSensorFromFile(hostSensorYAMLFile) + if err != nil { + return nil, fmt.Errorf("failed to load host-scan yaml file, reason: %s", err.Error()) + } + hostSensorYAML = d + } hsh := &HostSensorHandler{ k8sObj: k8sObj, HostSensorPodNames: map[string]string{}, @@ -249,3 +257,12 @@ func (hsh *HostSensorHandler) GetNamespace() string { } return hsh.DaemonSet.Namespace } + +func loadHostSensorFromFile(hostSensorYAMLFile string) (string, error) { + dat, err := os.ReadFile(hostSensorYAMLFile) + if err != nil { + return "", err + } + // TODO - Add file validation + return string(dat), err +} From 8694a929cf271cd7be52d79da952bc260011119e Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 24 Feb 2022 13:57:33 +0200 Subject: [PATCH 26/44] support output versions --- README.md | 5 ++- cautils/scaninfo.go | 1 + clihandler/cmd/framework.go | 11 +------ clihandler/cmd/scan.go | 35 ++++++++++++++++++--- clihandler/initcli.go | 2 +- resultshandling/printer/v2/prettyprinter.go | 17 +++++++--- resultshandling/printer/v2/resourcetable.go | 4 ++- resultshandling/results.go | 4 +-- 8 files changed, 54 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 309e161e..06e4cab8 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ Want to contribute? Want to discuss something? Have an issue? # Options and examples +[Kubescape docs](https://hub.armo.cloud/docs) + ## Playground * [Kubescape playground](https://www.katacoda.com/pathaksaiyam/scenarios/kubescape) @@ -65,9 +67,10 @@ Want to contribute? Want to discuss something? Have an issue? * [Overview](https://youtu.be/wdBkt_0Qhbg) * [How To Secure Kubernetes Clusters With Kubescape And Armo](https://youtu.be/ZATGiDIDBQk) -* [Scanning Kubernetes YAML files](https://youtu.be/Ox6DaR7_4ZI) +* [Scan Kubernetes YAML files](https://youtu.be/Ox6DaR7_4ZI) * [Scan Kubescape on an air-gapped environment (offline support)](https://youtu.be/IGXL9s37smM) * [Managing exceptions in the Kubescape SaaS version](https://youtu.be/OzpvxGmCR80) +* [Configure and run customized frameworks](https://youtu.be/12Sanq_rEhs) ## Install on Windows diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 0030fc36..9bef29b3 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -64,6 +64,7 @@ type ScanInfo struct { VerboseMode bool // Display all of the input resources and not only failed resources Format string // Format results (table, json, junit ...) Output string // Store results in an output file, Output file name + OutputVersion string // Output object can be differnet between versions, this is for testing and backward compatibility ExcludedNamespaces string // used for host sensor namespace IncludeNamespaces string // DEPRECATED? InputPatterns []string // Yaml files input patterns diff --git a/clihandler/cmd/framework.go b/clihandler/cmd/framework.go index f06c9c80..ad978473 100644 --- a/clihandler/cmd/framework.go +++ b/clihandler/cmd/framework.go @@ -16,7 +16,7 @@ import ( var ( frameworkExample = ` # Scan all frameworks and submit the results - kubescape scan --submit + kubescape scan framework all --submit # Scan the NSA framework kubescape scan framework nsa @@ -30,15 +30,6 @@ var ( # Scan kubernetes YAML manifest files kubescape scan framework nsa *.yaml - # Scan and save the results in the JSON format - kubescape scan --format json --output results.json - - # Save scan results in JSON format - kubescape scan --format json --output results.json - - # Display all resources - kubescape scan --verbose - Run 'kubescape list frameworks' for the list of supported frameworks ` ) diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index ef1587d2..55e7ead1 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -8,11 +8,32 @@ import ( var scanInfo cautils.ScanInfo +var scanCmdExamples = ` + Scan command is for scanning an existing cluster or kubernetes manifest files based on pre-defind frameworks + + # Scan current cluster with all frameworks + kubescape scan --submit --enable-host-scan + + # Scan kubernetes YAML manifest files + kubescape scan *.yaml + + # Scan and save the results in the JSON format + kubescape scan --format json --output results.json + + # Display all resources + kubescape scan --verbose + + # Scan different clusters from the kubectl context + kubescape scan --kube-context + +` + // scanCmd represents the scan command var scanCmd = &cobra.Command{ - Use: "scan [command]", - Short: "Scan the current running cluster or yaml files", - Long: `The action you want to perform`, + Use: "scan", + Short: "Scan the current running cluster or yaml files", + Long: `The action you want to perform`, + Example: scanCmdExamples, Args: func(cmd *cobra.Command, args []string) error { if len(args) > 0 { if args[0] != "framework" && args[0] != "control" { @@ -57,9 +78,13 @@ func init() { scanCmd.PersistentFlags().StringSliceVar(&scanInfo.UseFrom, "use-from", nil, "Load local policy object from specified path. If not used will download latest") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") - scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") - scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") + scanCmd.PersistentFlags().StringVar(&scanInfo.OutputVersion, "output-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") + + // hidden flags + scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") // this flag should be used very cautiously. We prefer users will not use it at all unless the DaemoSet can not run pods on the nodes + scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support + scanCmd.PersistentFlags().MarkHidden("output-version") // meant for testing different output approaches and not for common use hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls") hostF.NoOptDefVal = "true" diff --git a/clihandler/initcli.go b/clihandler/initcli.go index a0dab3c3..0a96a51f 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -83,7 +83,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler := getReporter(tenantConfig, scanInfo.Submit) // setup printer - printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.VerboseMode) + printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.OutputVersion, scanInfo.VerboseMode) printerHandler.SetWriter(scanInfo.Output) // ================== return interface ====================================== diff --git a/resultshandling/printer/v2/prettyprinter.go b/resultshandling/printer/v2/prettyprinter.go index b380e680..75e7053b 100644 --- a/resultshandling/printer/v2/prettyprinter.go +++ b/resultshandling/printer/v2/prettyprinter.go @@ -17,21 +17,23 @@ import ( ) type PrettyPrinter struct { + outputVersion string writer *os.File verboseMode bool sortedControlNames []string } -func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { +func NewPrettyPrinter(verboseMode bool, outputVersion string) *PrettyPrinter { return &PrettyPrinter{ - verboseMode: verboseMode, + verboseMode: verboseMode, + outputVersion: outputVersion, } } func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All()) - // prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) + prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails) @@ -45,6 +47,10 @@ func (prettyPrinter *PrettyPrinter) Score(score float32) { } func (prettyPrinter *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries, allResources map[string]workloadinterface.IMetadata) { + if prettyPrinter.outputVersion != "v1" { + return + } + for i := 0; i < len(prettyPrinter.sortedControlNames); i++ { controlSummary := controls.GetControl(reportsummary.EControlCriteriaName, prettyPrinter.sortedControlNames[i]) // summaryDetails.Controls ListControls().All() Controls.GetControl(ca) @@ -165,8 +171,6 @@ func generateFooter(summaryDetails *reportsummary.SummaryDetails) []string { return row } func (prettyPrinter *PrettyPrinter) printSummaryTable(summaryDetails *reportsummary.SummaryDetails) { - // For control scan framework will be nil - cautils.InfoTextDisplay(prettyPrinter.writer, frameworksScoresToString(summaryDetails.ListFrameworks().All())) summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(false) @@ -183,6 +187,9 @@ func (prettyPrinter *PrettyPrinter) printSummaryTable(summaryDetails *reportsumm // summaryTable.SetFooter(generateFooter()) summaryTable.Render() + + // For control scan framework will be nil + cautils.InfoTextDisplay(prettyPrinter.writer, frameworksScoresToString(summaryDetails.ListFrameworks().All())) } func frameworksScoresToString(frameworks []reportsummary.IPolicies) string { diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go index bfaaa15e..cc291e8e 100644 --- a/resultshandling/printer/v2/resourcetable.go +++ b/resultshandling/printer/v2/resourcetable.go @@ -10,7 +10,9 @@ import ( ) func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { - + if prettyPrinter.outputVersion != "v2" { + return + } summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(true) summaryTable.SetAutoMergeCells(true) diff --git a/resultshandling/results.go b/resultshandling/results.go index 1736a9f8..f6f4917d 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -53,7 +53,7 @@ func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 return (float32(len(allResources)) - float32(len(failedResources))) / float32(len(allResources)) } -func NewPrinter(printFormat string, verboseMode bool) printer.IPrinter { +func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPrinter { switch printFormat { case printer.JsonFormat: @@ -65,6 +65,6 @@ func NewPrinter(printFormat string, verboseMode bool) printer.IPrinter { case printer.PdfFormat: return printerv2.NewPdfPrinter() default: - return printerv2.NewPrettyPrinter(verboseMode) + return printerv2.NewPrettyPrinter(verboseMode, outputVersion) } } From dc172a147645ce31d0774ea40c097730392f1137 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 24 Feb 2022 18:22:29 +0200 Subject: [PATCH 27/44] fixed help message --- cautils/scaninfo.go | 6 ++++-- cautils/versioncheck.go | 6 +++--- clihandler/cmd/root.go | 19 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 9bef29b3..7f21ff3d 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -53,6 +53,10 @@ func (bpf *BoolPtrFlag) Set(val string) error { return nil } +type RootInfo struct { + Logger string // logger level + CacheDir string // cached dir +} type ScanInfo struct { Getters PolicyIdentifier []reporthandling.PolicyIdentifier @@ -75,8 +79,6 @@ type ScanInfo struct { HostSensorYamlPath string // Path to hostsensor file Local bool // Do not submit results Account string // account ID - Logger string // logger level - CacheDir string // cached dir KubeContext string // context name FrameworkScan bool // false if scanning control ScanAll bool // true if scan all frameworks diff --git a/cautils/versioncheck.go b/cautils/versioncheck.go index b53bced7..83385df1 100644 --- a/cautils/versioncheck.go +++ b/cautils/versioncheck.go @@ -98,7 +98,7 @@ func (v *VersionCheckHandler) CheckLatestVersion(versionData *VersionCheckReques if latestVersion.ClientUpdate != "" { if BuildNumber != "" && BuildNumber < latestVersion.ClientUpdate { - logger.L().Warning(warningMessage(latestVersion.Client, latestVersion.ClientUpdate)) + logger.L().Warning(warningMessage(latestVersion.ClientUpdate)) } } @@ -133,6 +133,6 @@ func (v *VersionCheckHandler) getLatestVersion(versionData *VersionCheckRequest) return vResp, nil } -func warningMessage(kind, release string) string { - return fmt.Sprintf("'%s' is not updated to the latest release: '%s'", kind, release) +func warningMessage(release string) string { + return fmt.Sprintf("current version '%s' is not updated to the latest release: '%s'", BuildNumber, release) } diff --git a/clihandler/cmd/root.go b/clihandler/cmd/root.go index 8fc1d615..8c3dff7c 100644 --- a/clihandler/cmd/root.go +++ b/clihandler/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "flag" "fmt" "os" "strings" @@ -14,6 +13,7 @@ import ( ) var armoBEURLs = "" +var rootInfo cautils.RootInfo const envFlagUsage = "Send report results to specific URL. Format:,,.\n\t\tExample:report.armo.cloud,api.armo.cloud,portal.armo.cloud" @@ -44,14 +44,13 @@ func Execute() { } func init() { + cobra.OnInitialize(initLogger, initLoggerLevel, initEnvironment, initCacheDir) - flag.CommandLine.StringVar(&armoBEURLs, "environment", "", envFlagUsage) rootCmd.PersistentFlags().StringVar(&armoBEURLs, "environment", "", envFlagUsage) rootCmd.PersistentFlags().MarkHidden("environment") - rootCmd.PersistentFlags().StringVarP(&scanInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/"))) - rootCmd.PersistentFlags().StringVar(&scanInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]") - flag.Parse() + rootCmd.PersistentFlags().StringVarP(&rootInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/"))) + rootCmd.PersistentFlags().StringVar(&rootInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]") } func initLogger() { @@ -60,18 +59,18 @@ func initLogger() { } } func initLoggerLevel() { - if scanInfo.Logger != helpers.InfoLevel.String() { + if rootInfo.Logger != helpers.InfoLevel.String() { } else if l := os.Getenv("KS_LOGGER"); l != "" { - scanInfo.Logger = l + rootInfo.Logger = l } - if err := logger.L().SetLevel(scanInfo.Logger); err != nil { + if err := logger.L().SetLevel(rootInfo.Logger); err != nil { logger.L().Fatal(fmt.Sprintf("supported levels: %s", strings.Join(helpers.SupportedLevels(), "/")), helpers.Error(err)) } } func initCacheDir() { - if scanInfo.CacheDir != getter.DefaultLocalStore { - getter.DefaultLocalStore = scanInfo.CacheDir + if rootInfo.CacheDir != getter.DefaultLocalStore { + getter.DefaultLocalStore = rootInfo.CacheDir } else if cacheDir := os.Getenv("KS_CACHE_DIR"); cacheDir != "" { getter.DefaultLocalStore = cacheDir } else { From db95da37424aa2ff16e1a153374be4a43e1655db Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 24 Feb 2022 18:43:29 +0200 Subject: [PATCH 28/44] udpate packages --- go.mod | 19 +++++++------ go.sum | 85 +++++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 82 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 9988151e..5a61d8ea 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/armosec/kubescape go 1.17 require ( - github.com/armosec/armoapi-go v0.0.54 - github.com/armosec/k8s-interface v0.0.60 + github.com/armosec/armoapi-go v0.0.57 + github.com/armosec/k8s-interface v0.0.63 github.com/armosec/opa-utils v0.0.110 github.com/armosec/rbac-utils v0.0.14 github.com/armosec/utils-go v0.0.3 @@ -29,7 +29,8 @@ require ( ) require ( - cloud.google.com/go v0.81.0 // indirect + cloud.google.com/go v0.97.0 // indirect + cloud.google.com/go/container v1.0.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.18 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect @@ -61,14 +62,13 @@ require ( github.com/go-gota/gota v0.12.0 // indirect github.com/go-logr/logr v0.4.0 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/gofrs/uuid v4.1.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/googleapis/gax-go/v2 v2.1.1 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect @@ -86,7 +86,6 @@ require ( github.com/pquerna/cachecontrol v0.1.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 // indirect - github.com/satori/go.uuid v1.2.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -97,15 +96,15 @@ require ( golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect golang.org/x/net v0.0.0-20210825183410-e898025ed96a // indirect golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 // indirect - golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect + golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect gonum.org/v1/gonum v0.9.1 // indirect - google.golang.org/api v0.44.0 // indirect + google.golang.org/api v0.59.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect - google.golang.org/grpc v1.38.0 // indirect + google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca // indirect + google.golang.org/grpc v1.40.0 // indirect google.golang.org/protobuf v1.27.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect diff --git a/go.sum b/go.sum index 99dfc09a..56b612fa 100644 --- a/go.sum +++ b/go.sum @@ -19,14 +19,23 @@ cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKP cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/container v1.0.0 h1:k3IMcHEEJR4pQmj4/gNeUuA4azhW7i2RPX8xGhyneZw= +cloud.google.com/go/container v1.0.0/go.mod h1:EQLhTDFhzVXTX6TmjfNqa3/ikbCzjGlZcGY67exUnlY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= @@ -86,13 +95,13 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armosec/armoapi-go v0.0.2/go.mod h1:vIK17yoKbJRQyZXWWLe3AqfqCRITxW8qmSkApyq5xFs= github.com/armosec/armoapi-go v0.0.23/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= github.com/armosec/armoapi-go v0.0.49/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= -github.com/armosec/armoapi-go v0.0.54 h1:8PjT7qpvt5YsR0OxRRoQ891IOaWThlTJvVgRVReBBLA= -github.com/armosec/armoapi-go v0.0.54/go.mod h1:iaVVGyc23QGGzAdv4n+szGQg3Rbpixn9yQTU3qWRpaw= +github.com/armosec/armoapi-go v0.0.57 h1:uAYohbzo+ZXZ4FR9R0i6181susGllWI4auUXQQhKQdQ= +github.com/armosec/armoapi-go v0.0.57/go.mod h1:U/Axd+D5N00x9Ekr7t+5HXqLCMO+98NfJSVAggqJftI= github.com/armosec/k8s-interface v0.0.8/go.mod h1:xxS+V5QT3gVQTwZyAMMDrYLWGrfKOpiJ7Jfhfa0w9sM= github.com/armosec/k8s-interface v0.0.37/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU= github.com/armosec/k8s-interface v0.0.50/go.mod h1:vHxGWqD/uh6+GQb9Sqv7OGMs+Rvc2dsFVc0XtgRh1ZU= -github.com/armosec/k8s-interface v0.0.60 h1:jTCiO15QQbHVuxFQ928rp4srf1rQoUzeybfcbv/cuss= -github.com/armosec/k8s-interface v0.0.60/go.mod h1:g0jv/fG+VqpT5ivO6D2gJcJ/w68BiffDz+PcU9YFbL4= +github.com/armosec/k8s-interface v0.0.63 h1:RuMqsYcneVUD7HJkMVxjc8N6bpWd6rhJ5K85USMq1Sg= +github.com/armosec/k8s-interface v0.0.63/go.mod h1:vwprS8qn/iowd5yf0JHpqDsLA5I8W2muqX9AxKhkb0Q= github.com/armosec/opa-utils v0.0.64/go.mod h1:6tQP8UDq2EvEfSqh8vrUdr/9QVSCG4sJfju1SXQOn4c= github.com/armosec/opa-utils v0.0.110 h1:qncGcbnYjiGULP3yK+4geRNNpRoWqKXQL+Xg+iXc1cM= github.com/armosec/opa-utils v0.0.110/go.mod h1:Wc1P4gkB6UQeGW8I76zCuitGGl15Omp0bKw7N0tR9dk= @@ -165,6 +174,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= @@ -216,6 +226,7 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= @@ -277,7 +288,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.1.0+incompatible h1:sIa2eCvUTwgjbqXrPLfNwUf9S3i3mpH1O1atV+iL/Wk= github.com/gofrs/uuid v4.1.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -303,6 +313,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -351,6 +362,7 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -362,6 +374,9 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -371,8 +386,10 @@ github.com/googleapis/gax-go v2.0.0+incompatible h1:j0GKcs05QVmm7yesiZq2+9cxHkNK github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= @@ -584,7 +601,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58 h1:nlG4Wa5+minh3S9LVFtNoY+GVRiudA2e3EVfcCi3RCA= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= @@ -833,6 +849,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -853,6 +870,9 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 h1:B333XXssMuKQeBwiNODx4TupZy7bf4sxFZnN2ZOcvUE= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= @@ -931,13 +951,19 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -1020,7 +1046,10 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1060,8 +1089,17 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0 h1:fPfFO7gttlXYo2ALuD3HxJzh8vaF++4youI0BkFL6GE= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1118,8 +1156,26 @@ google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca h1:+e+aQDO4/c9KaG8PXWHTc6/+Du6kz+BKcXCSnV4SSTE= +google.golang.org/genproto v0.0.0-20211027162914-98a5263abeca/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -1143,8 +1199,13 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= From fdcc5e9a6665c080bad31671974426933f299bf0 Mon Sep 17 00:00:00 2001 From: Tomasz Majer Date: Sun, 27 Feb 2022 11:14:05 +0100 Subject: [PATCH 29/44] Support float in fail-threshold --- cautils/scaninfo.go | 2 +- clihandler/cmd/scan.go | 2 +- clihandler/initcli.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 7f21ff3d..f98ee4c5 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -73,7 +73,7 @@ type ScanInfo struct { IncludeNamespaces string // DEPRECATED? InputPatterns []string // Yaml files input patterns Silent bool // Silent mode - Do not print progress logs - FailThreshold uint16 // Failure score threshold + FailThreshold float32 // Failure score threshold Submit bool // Submit results to Armo BE HostSensorEnabled BoolPtrFlag // Deploy ARMO K8s host sensor to collect data from certain controls HostSensorYamlPath string // Path to hostsensor file diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 55e7ead1..cd04189d 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -68,7 +68,7 @@ func init() { scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from ARMO management portal") scanCmd.PersistentFlags().StringVar(&scanInfo.UseArtifactsFrom, "use-artifacts-from", "", "Load artifacts from local directory. If not used will download them") scanCmd.PersistentFlags().StringVarP(&scanInfo.ExcludedNamespaces, "exclude-namespaces", "e", "", "Namespaces to exclude from scanning. Recommended: kube-system,kube-public") - scanCmd.PersistentFlags().Uint16VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1") + scanCmd.PersistentFlags().Float32VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1") scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer","json","junit","prometheus","pdf"`) scanCmd.PersistentFlags().StringVar(&scanInfo.IncludeNamespaces, "include-namespaces", "", "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to Armo backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") diff --git a/clihandler/initcli.go b/clihandler/initcli.go index 0a96a51f..1ccb5e36 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -153,7 +153,7 @@ func ScanCliSetup(scanInfo *cautils.ScanInfo) error { interfaces.report.DisplayReportURL() if score > float32(scanInfo.FailThreshold) { - return fmt.Errorf("scan risk-score %.2f is above permitted threshold %d", score, scanInfo.FailThreshold) + return fmt.Errorf("scan risk-score %.2f is above permitted threshold %.2f", score, scanInfo.FailThreshold) } return nil From 23e73f5e88bdd2b19074bd6681451396ede7781f Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 27 Feb 2022 14:30:32 +0200 Subject: [PATCH 30/44] extent host-sensor support --- README.md | 2 +- cautils/fileutils.go | 239 ++++++++++++++++++ .../fileutils_test.go | 2 +- clihandler/cmd/scan.go | 2 +- examples/exceptions/README.md | 12 +- hostsensorutils/hostsensor.yaml | 24 +- hostsensorutils/hostsensordeploy.go | 159 +++++++----- resourcehandler/filesloader.go | 234 +---------------- resourcehandler/urlloader.go | 5 +- resultshandling/printer/v1/prettyprinter.go | 1 + 10 files changed, 359 insertions(+), 321 deletions(-) create mode 100644 cautils/fileutils.go rename resourcehandler/filesloader_test.go => cautils/fileutils_test.go (97%) diff --git a/README.md b/README.md index 06e4cab8..4608f65b 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `--use-from` | | Load local framework object from specified path. If not used will download latest || | `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | | | `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | -| `--exceptions` | | Path to an exceptions obj, [examples](examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || +| `--exceptions` | | Path to an exceptions obj, [examples](https://github.com/armosec/kubescape/tree/master/examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || | `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal | | | `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` | | `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` | diff --git a/cautils/fileutils.go b/cautils/fileutils.go new file mode 100644 index 00000000..7f74899e --- /dev/null +++ b/cautils/fileutils.go @@ -0,0 +1,239 @@ +package cautils + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/armosec/k8s-interface/workloadinterface" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/opa-utils/objectsenvelopes" + "gopkg.in/yaml.v2" +) + +var ( + YAML_PREFIX = []string{".yaml", ".yml"} + JSON_PREFIX = []string{".json"} +) + +type FileFormat string + +const ( + YAML_FILE_FORMAT FileFormat = "yaml" + JSON_FILE_FORMAT FileFormat = "json" +) + +func LoadResourcesFromFiles(inputPatterns []string) ([]workloadinterface.IMetadata, error) { + files, errs := listFiles(inputPatterns) + if len(errs) > 0 { + logger.L().Error(fmt.Sprintf("%v", errs)) + } + if len(files) == 0 { + return nil, nil + } + + workloads, errs := loadFiles(files) + if len(errs) > 0 { + logger.L().Error(fmt.Sprintf("%v", errs)) + } + return workloads, nil +} + +func loadFiles(filePaths []string) ([]workloadinterface.IMetadata, []error) { + workloads := []workloadinterface.IMetadata{} + errs := []error{} + for i := range filePaths { + f, err := loadFile(filePaths[i]) + if err != nil { + errs = append(errs, err) + continue + } + w, e := ReadFile(f, GetFileFormat(filePaths[i])) + errs = append(errs, e...) + if w != nil { + workloads = append(workloads, w...) + } + } + return workloads, errs +} + +func loadFile(filePath string) ([]byte, error) { + return os.ReadFile(filePath) +} +func ReadFile(fileContent []byte, fileFromat FileFormat) ([]workloadinterface.IMetadata, []error) { + + switch fileFromat { + case YAML_FILE_FORMAT: + return readYamlFile(fileContent) + case JSON_FILE_FORMAT: + return readJsonFile(fileContent) + default: + return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)} + } + +} + +func listFiles(patterns []string) ([]string, []error) { + files := []string{} + errs := []error{} + for i := range patterns { + if strings.HasPrefix(patterns[i], "http") { + continue + } + if !filepath.IsAbs(patterns[i]) { + o, _ := os.Getwd() + patterns[i] = filepath.Join(o, patterns[i]) + } + if IsFile(patterns[i]) { + files = append(files, patterns[i]) + } else { + f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i]) + if err != nil { + errs = append(errs, err) + } else { + files = append(files, f...) + } + } + } + return files, errs +} + +func readYamlFile(yamlFile []byte) ([]workloadinterface.IMetadata, []error) { + errs := []error{} + + r := bytes.NewReader(yamlFile) + dec := yaml.NewDecoder(r) + yamlObjs := []workloadinterface.IMetadata{} + + var t interface{} + for dec.Decode(&t) == nil { + j := convertYamlToJson(t) + if j == nil { + continue + } + if obj, ok := j.(map[string]interface{}); ok { + if o := objectsenvelopes.NewObject(obj); o != nil { + if o.GetKind() == "List" { + yamlObjs = append(yamlObjs, handleListObject(o)...) + } else { + yamlObjs = append(yamlObjs, o) + } + } + } else { + errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j)) + } + } + + return yamlObjs, errs +} + +func readJsonFile(jsonFile []byte) ([]workloadinterface.IMetadata, []error) { + workloads := []workloadinterface.IMetadata{} + var jsonObj interface{} + if err := json.Unmarshal(jsonFile, &jsonObj); err != nil { + return workloads, []error{err} + } + + convertJsonToWorkload(jsonObj, &workloads) + + return workloads, nil +} +func convertJsonToWorkload(jsonObj interface{}, workloads *[]workloadinterface.IMetadata) { + + switch x := jsonObj.(type) { + case map[string]interface{}: + if o := objectsenvelopes.NewObject(x); o != nil { + (*workloads) = append(*workloads, o) + } + case []interface{}: + for i := range x { + convertJsonToWorkload(x[i], workloads) + } + } +} +func convertYamlToJson(i interface{}) interface{} { + switch x := i.(type) { + case map[interface{}]interface{}: + m2 := map[string]interface{}{} + for k, v := range x { + if s, ok := k.(string); ok { + m2[s] = convertYamlToJson(v) + } + } + return m2 + case []interface{}: + for i, v := range x { + x[i] = convertYamlToJson(v) + } + } + return i +} + +func IsYaml(filePath string) bool { + return StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != ValueNotFound +} + +func IsJson(filePath string) bool { + return StringInSlice(JSON_PREFIX, filepath.Ext(filePath)) != ValueNotFound +} + +func glob(root, pattern string) ([]string, error) { + var matches []string + + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil { + return err + } else if matched { + matches = append(matches, path) + } + return nil + }) + if err != nil { + return nil, err + } + return matches, nil +} +func IsFile(name string) bool { + if fi, err := os.Stat(name); err == nil { + if fi.Mode().IsRegular() { + return true + } + } + return false +} + +func GetFileFormat(filePath string) FileFormat { + if IsYaml(filePath) { + return YAML_FILE_FORMAT + } else if IsJson(filePath) { + return JSON_FILE_FORMAT + } else { + return FileFormat(filePath) + } +} + +// handleListObject handle a List manifest +func handleListObject(obj workloadinterface.IMetadata) []workloadinterface.IMetadata { + yamlObjs := []workloadinterface.IMetadata{} + if i, ok := workloadinterface.InspectMap(obj.GetObject(), "items"); ok && i != nil { + if items, ok := i.([]interface{}); ok && items != nil { + for item := range items { + if m, ok := items[item].(map[string]interface{}); ok && m != nil { + if o := objectsenvelopes.NewObject(m); o != nil { + yamlObjs = append(yamlObjs, o) + } + } + } + } + } + return yamlObjs +} diff --git a/resourcehandler/filesloader_test.go b/cautils/fileutils_test.go similarity index 97% rename from resourcehandler/filesloader_test.go rename to cautils/fileutils_test.go index 5d2006c5..1b0b5e06 100644 --- a/resourcehandler/filesloader_test.go +++ b/cautils/fileutils_test.go @@ -1,4 +1,4 @@ -package resourcehandler +package cautils import ( "os" diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 55e7ead1..1835321d 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -86,7 +86,7 @@ func init() { scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support scanCmd.PersistentFlags().MarkHidden("output-version") // meant for testing different output approaches and not for common use - hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls") + hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") hostF.NoOptDefVal = "true" hostF.DefValue = "false, for no TTY in stdin" diff --git a/examples/exceptions/README.md b/examples/exceptions/README.md index 4fd13019..6cad5627 100644 --- a/examples/exceptions/README.md +++ b/examples/exceptions/README.md @@ -19,10 +19,10 @@ e.g. When a `kube-system` resource fails and it is ok, simply add the resource t * `cluster`: k8s cluster name (usually it is the `current-context`) (case-sensitive, regex supported) * resource labels as key value (case-sensitive, regex NOT supported) * `posturePolicies`- An attribute-based declaration {key: value} - * `frameworkName` - Framework names can be find [here](https://github.com/armosec/regolibrary/tree/master/frameworks) - * `controlName` - Control names can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) - * `controlID` - Not yet supported - * `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules) + * `frameworkName` - Framework names can be find [here](https://github.com/armosec/regolibrary/tree/master/frameworks) (regex supported) + * `controlName` - Control names can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported) + * `controlID` - Control ID can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported) + * `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules) (regex supported) ## Usage @@ -92,7 +92,7 @@ Here are some examples demonstrating the different ways the exceptions file can ### Exclude control -Exclude the ["Allowed hostPath" control](https://github.com/armosec/regolibrary/blob/master/controls/allowedhostpath.json#L2) by declaring the control in the `"posturePolicies"` section. +Exclude the [C-0060 control](https://github.com/armosec/regolibrary/blob/master/controls/allowedhostpath.json#L2) by declaring the control ID in the `"posturePolicies"` section. The resources @@ -114,7 +114,7 @@ The resources ], "posturePolicies": [ { - "controlName": "Allowed hostPath" + "controlID": "C-0060" } ] } diff --git a/hostsensorutils/hostsensor.yaml b/hostsensorutils/hostsensor.yaml index 5647a162..b2aea6e0 100644 --- a/hostsensorutils/hostsensor.yaml +++ b/hostsensorutils/hostsensor.yaml @@ -2,29 +2,31 @@ apiVersion: v1 kind: Namespace metadata: labels: - app: host-sensor - kubernetes.io/metadata.name: armo-kube-host-sensor - tier: armo-kube-host-sensor-control-plane - name: armo-kube-host-sensor + app: kubescape-host-scanner + k8s-app: kubescape-host-scanner + kubernetes.io/metadata.name: kubescape-host-scanner + tier: kubescape-host-scanner-control-plane + name: kubescape-host-scanner --- apiVersion: apps/v1 kind: DaemonSet metadata: - name: host-sensor - namespace: armo-kube-host-sensor + name: host-scanner + namespace: kubescape-host-scanner labels: - k8s-app: armo-kube-host-sensor + app: host-scanner + k8s-app: kubescape-host-scanner spec: selector: matchLabels: - name: host-sensor + name: host-scanner template: metadata: labels: - name: host-sensor + name: host-scanner spec: tolerations: - # this toleration is to have the daemonset runnable on master nodes + # this toleration is to have the DaemonDet runnable on master nodes # remove it if your masters can't run pods - key: node-role.kubernetes.io/master operator: Exists @@ -37,7 +39,7 @@ spec: readOnlyRootFilesystem: true procMount: Unmasked ports: - - name: http + - name: scanner # Do not change port name hostPort: 7888 containerPort: 7888 protocol: TCP diff --git a/hostsensorutils/hostsensordeploy.go b/hostsensorutils/hostsensordeploy.go index efe6adf0..3157346a 100644 --- a/hostsensorutils/hostsensordeploy.go +++ b/hostsensorutils/hostsensordeploy.go @@ -2,24 +2,21 @@ package hostsensorutils import ( _ "embed" + "encoding/json" "fmt" - "io" "os" - "strings" "sync" "time" "github.com/armosec/k8s-interface/k8sinterface" + "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/watch" - appsapplyv1 "k8s.io/client-go/applyconfigurations/apps/v1" - coreapplyv1 "k8s.io/client-go/applyconfigurations/core/v1" ) var ( @@ -27,15 +24,17 @@ var ( hostSensorYAML string ) +const PortName string = "scanner" + type HostSensorHandler struct { - HostSensorPort int32 - HostSensorPodNames map[string]string //map from pod names to node names - HostSensorUnshedulePodNames map[string]string //map from pod names to node names - IsReady <-chan bool //readonly chan - k8sObj *k8sinterface.KubernetesApi - DaemonSet *appsv1.DaemonSet - podListLock sync.RWMutex - gracePeriod int64 + HostSensorPort int32 + HostSensorPodNames map[string]string //map from pod names to node names + HostSensorUnscheduledPodNames map[string]string //map from pod names to node names + IsReady <-chan bool //readonly chan + k8sObj *k8sinterface.KubernetesApi + DaemonSet *appsv1.DaemonSet + podListLock sync.RWMutex + gracePeriod int64 } func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi, hostSensorYAMLFile string) (*HostSensorHandler, error) { @@ -51,10 +50,10 @@ func NewHostSensorHandler(k8sObj *k8sinterface.KubernetesApi, hostSensorYAMLFile hostSensorYAML = d } hsh := &HostSensorHandler{ - k8sObj: k8sObj, - HostSensorPodNames: map[string]string{}, - HostSensorUnshedulePodNames: map[string]string{}, - gracePeriod: int64(15), + k8sObj: k8sObj, + HostSensorPodNames: map[string]string{}, + HostSensorUnscheduledPodNames: map[string]string{}, + gracePeriod: int64(15), } // Don't deploy on cluster with no nodes. Some cloud providers prevents termination of K8s objects for cluster with no nodes!!! if nodeList, err := k8sObj.KubernetesClient.CoreV1().Nodes().List(k8sObj.Context, metav1.ListOptions{}); err != nil || len(nodeList.Items) == 0 { @@ -78,7 +77,7 @@ func (hsh *HostSensorHandler) Init() error { defer cautils.StopSpinner() if err := hsh.applyYAML(); err != nil { - return fmt.Errorf("in HostSensorHandler init failed to apply YAML: %v", err) + return fmt.Errorf("failed to apply host sensor YAML, reason: %v", err) } hsh.populatePodNamesToNodeNames() if err := hsh.checkPodForEachNode(); err != nil { @@ -88,55 +87,83 @@ func (hsh *HostSensorHandler) Init() error { } func (hsh *HostSensorHandler) applyYAML() error { - dec := yaml.NewDocumentDecoder(io.NopCloser(strings.NewReader(hostSensorYAML))) - // apply namespace - singleYAMLBytes := make([]byte, 4096) - if readLen, err := dec.Read(singleYAMLBytes); err != nil { - return fmt.Errorf("failed to read YAML of namespace: %v", err) - } else { - singleYAMLBytes = singleYAMLBytes[:readLen] + workloads, err := cautils.ReadFile([]byte(hostSensorYAML), cautils.YAML_FILE_FORMAT) + if len(err) != 0 { + return fmt.Errorf("failed to read YAML files, reason: %v", err) } - namespaceAC := &coreapplyv1.NamespaceApplyConfiguration{} - if err := yaml.Unmarshal(singleYAMLBytes, namespaceAC); err != nil { - return fmt.Errorf("failed to Unmarshal YAML of namespace: %v", err) - } - namespaceName := "" - if ns, err := hsh.k8sObj.KubernetesClient.CoreV1().Namespaces().Apply(hsh.k8sObj.Context, namespaceAC, metav1.ApplyOptions{ - FieldManager: "kubescape", - }); err != nil { - return fmt.Errorf("failed to apply YAML of namespace: %v", err) - } else { - namespaceName = ns.Name - } - // apply DaemonSet - daemonAC := &appsapplyv1.DaemonSetApplyConfiguration{} - singleYAMLBytes = make([]byte, 4096) - if readLen, err := dec.Read(singleYAMLBytes); err != nil { - if erra := hsh.tearDownNamesapce(namespaceName); erra != nil { - err = fmt.Errorf("%v; In addidtion %v", err, erra) + // Get namespace name + namespaceName := "" + for i := range workloads { + if workloads[i].GetKind() == "Namespace" { + namespaceName = workloads[i].GetName() + break } - return fmt.Errorf("failed to read YAML of DaemonSet: %v", err) - } else { - singleYAMLBytes = singleYAMLBytes[:readLen] } - if err := yaml.Unmarshal(singleYAMLBytes, daemonAC); err != nil { - if erra := hsh.tearDownNamesapce(namespaceName); erra != nil { - err = fmt.Errorf("%v; In addidtion %v", err, erra) + + // Update workload data before applying + for i := range workloads { + w := workloadinterface.NewWorkloadObj(workloads[i].GetObject()) + if w == nil { + return fmt.Errorf("invalid workload: %v", workloads[i].GetObject()) } - return fmt.Errorf("failed to Unmarshal YAML of DaemonSet: %v", err) - } - daemonAC.Namespace = &namespaceName - if ds, err := hsh.k8sObj.KubernetesClient.AppsV1().DaemonSets(namespaceName).Apply(hsh.k8sObj.Context, daemonAC, metav1.ApplyOptions{ - FieldManager: "kubescape", - }); err != nil { - if erra := hsh.tearDownNamesapce(namespaceName); erra != nil { - err = fmt.Errorf("%v; In addidtion %v", err, erra) + // set namespace in all objects + if w.GetKind() != "Namespace" { + w.SetNamespace(namespaceName) + } + // Get container port + if w.GetKind() == "DaemonSet" { + containers, err := w.GetContainers() + if err != nil { + if erra := hsh.tearDownNamespace(namespaceName); erra != nil { + logger.L().Warning("failed to tear down namespace", helpers.Error(erra)) + } + return fmt.Errorf("container not found in DaemonSet: %v", err) + } + for j := range containers { + for k := range containers[j].Ports { + if containers[j].Ports[k].Name == PortName { + hsh.HostSensorPort = containers[j].Ports[k].ContainerPort + } + } + } + + } + + // Apply workload + var newWorkload k8sinterface.IWorkload + var e error + + if g, err := hsh.k8sObj.GetWorkload(w.GetNamespace(), w.GetKind(), w.GetName()); err == nil && g != nil { + newWorkload, e = hsh.k8sObj.UpdateWorkload(w) + } else { + newWorkload, e = hsh.k8sObj.CreateWorkload(w) + } + if e != nil { + if erra := hsh.tearDownNamespace(namespaceName); erra != nil { + logger.L().Warning("failed to tear down namespace", helpers.Error(erra)) + } + return fmt.Errorf("failed to create/update YAML, reason: %v", e) + } + + // Save DaemonSet + if newWorkload.GetKind() == "DaemonSet" { + b, err := json.Marshal(newWorkload.GetObject()) + if err != nil { + if erra := hsh.tearDownNamespace(namespaceName); erra != nil { + logger.L().Warning("failed to tear down namespace", helpers.Error(erra)) + } + return fmt.Errorf("failed to Marshal YAML of DaemonSet, reason: %v", err) + } + var ds appsv1.DaemonSet + if err := json.Unmarshal(b, &ds); err != nil { + if erra := hsh.tearDownNamespace(namespaceName); erra != nil { + logger.L().Warning("failed to tear down namespace", helpers.Error(erra)) + } + return fmt.Errorf("failed to Unmarshal YAML of DaemonSet, reason: %v", err) + } + hsh.DaemonSet = &ds } - return fmt.Errorf("failed to apply YAML of DaemonSet: %v", err) - } else { - hsh.HostSensorPort = ds.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort - hsh.DaemonSet = ds } return nil } @@ -150,7 +177,7 @@ func (hsh *HostSensorHandler) checkPodForEachNode() error { } hsh.podListLock.RLock() podsNum := len(hsh.HostSensorPodNames) - unschedPodNum := len(hsh.HostSensorUnshedulePodNames) + unschedPodNum := len(hsh.HostSensorUnscheduledPodNames) hsh.podListLock.RUnlock() if len(nodesList.Items) <= podsNum+unschedPodNum { break @@ -202,7 +229,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p if podObj.Status.Phase == corev1.PodRunning && len(podObj.Status.ContainerStatuses) > 0 && podObj.Status.ContainerStatuses[0].Ready { hsh.HostSensorPodNames[podObj.ObjectMeta.Name] = podObj.Spec.NodeName - delete(hsh.HostSensorUnshedulePodNames, podObj.ObjectMeta.Name) + delete(hsh.HostSensorUnscheduledPodNames, podObj.ObjectMeta.Name) } else { if podObj.Status.Phase == corev1.PodPending && len(podObj.Status.Conditions) > 0 && podObj.Status.Conditions[0].Reason == corev1.PodReasonUnschedulable { @@ -219,7 +246,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p helpers.String("nodeName", nodeName), helpers.String("podName", podObj.ObjectMeta.Name)) if nodeName != "" { - hsh.HostSensorUnshedulePodNames[podObj.ObjectMeta.Name] = nodeName + hsh.HostSensorUnscheduledPodNames[podObj.ObjectMeta.Name] = nodeName } } else { delete(hsh.HostSensorPodNames, podObj.ObjectMeta.Name) @@ -230,7 +257,7 @@ func (hsh *HostSensorHandler) updatePodInListAtomic(eventType watch.EventType, p } } -func (hsh *HostSensorHandler) tearDownNamesapce(namespace string) error { +func (hsh *HostSensorHandler) tearDownNamespace(namespace string) error { if err := hsh.k8sObj.KubernetesClient.CoreV1().Namespaces().Delete(hsh.k8sObj.Context, namespace, metav1.DeleteOptions{GracePeriodSeconds: &hsh.gracePeriod}); err != nil { return fmt.Errorf("failed to delete host-sensor namespace: %v", err) @@ -243,7 +270,7 @@ func (hsh *HostSensorHandler) TearDown() error { if err := hsh.k8sObj.KubernetesClient.AppsV1().DaemonSets(hsh.GetNamespace()).Delete(hsh.k8sObj.Context, hsh.DaemonSet.Name, metav1.DeleteOptions{GracePeriodSeconds: &hsh.gracePeriod}); err != nil { return fmt.Errorf("failed to delete host-sensor daemonset: %v", err) } - if err := hsh.tearDownNamesapce(namespace); err != nil { + if err := hsh.tearDownNamespace(namespace); err != nil { return fmt.Errorf("failed to delete host-sensor daemonset: %v", err) } // TODO: wait for termination? may take up to 120 seconds!!! diff --git a/resourcehandler/filesloader.go b/resourcehandler/filesloader.go index 28b0540c..f0cca705 100644 --- a/resourcehandler/filesloader.go +++ b/resourcehandler/filesloader.go @@ -1,12 +1,8 @@ package resourcehandler import ( - "bytes" - "encoding/json" "fmt" "os" - "path/filepath" - "strings" "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/k8s-interface/workloadinterface" @@ -14,23 +10,7 @@ import ( "github.com/armosec/k8s-interface/k8sinterface" "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/logger" - "github.com/armosec/opa-utils/objectsenvelopes" "github.com/armosec/opa-utils/reporthandling" - - "gopkg.in/yaml.v2" -) - -var ( - YAML_PREFIX = []string{".yaml", ".yml"} - JSON_PREFIX = []string{".json"} -) - -type FileFormat string - -const ( - YAML_FILE_FORMAT FileFormat = "yaml" - JSON_FILE_FORMAT FileFormat = "json" ) // FileResourceHandler handle resources from files and URLs @@ -57,7 +37,7 @@ func (fileHandler *FileResourceHandler) GetResources(frameworks []reporthandling workloads := []workloadinterface.IMetadata{} // load resource from local file system - w, err := loadResourcesFromFiles(fileHandler.inputPatterns) + w, err := cautils.LoadResourcesFromFiles(fileHandler.inputPatterns) if err != nil { return nil, allResources, err } @@ -105,22 +85,6 @@ func (fileHandler *FileResourceHandler) GetClusterAPIServerInfo() *version.Info return nil } -func loadResourcesFromFiles(inputPatterns []string) ([]workloadinterface.IMetadata, error) { - files, errs := listFiles(inputPatterns) - if len(errs) > 0 { - logger.L().Error(fmt.Sprintf("%v", errs)) - } - if len(files) == 0 { - return nil, nil - } - - workloads, errs := loadFiles(files) - if len(errs) > 0 { - logger.L().Error(fmt.Sprintf("%v", errs)) - } - return workloads, nil -} - // build resources map func mapResources(workloads []workloadinterface.IMetadata) map[string][]workloadinterface.IMetadata { @@ -149,199 +113,3 @@ func mapResources(workloads []workloadinterface.IMetadata) map[string][]workload return allResources } - -func loadFiles(filePaths []string) ([]workloadinterface.IMetadata, []error) { - workloads := []workloadinterface.IMetadata{} - errs := []error{} - for i := range filePaths { - f, err := loadFile(filePaths[i]) - if err != nil { - errs = append(errs, err) - continue - } - w, e := readFile(f, getFileFormat(filePaths[i])) - errs = append(errs, e...) - if w != nil { - workloads = append(workloads, w...) - } - } - return workloads, errs -} - -func loadFile(filePath string) ([]byte, error) { - return os.ReadFile(filePath) -} -func readFile(fileContent []byte, fileFromat FileFormat) ([]workloadinterface.IMetadata, []error) { - - switch fileFromat { - case YAML_FILE_FORMAT: - return readYamlFile(fileContent) - case JSON_FILE_FORMAT: - return readJsonFile(fileContent) - default: - return nil, nil // []error{fmt.Errorf("file extension %s not supported", fileFromat)} - } - -} - -func listFiles(patterns []string) ([]string, []error) { - files := []string{} - errs := []error{} - for i := range patterns { - if strings.HasPrefix(patterns[i], "http") { - continue - } - if !filepath.IsAbs(patterns[i]) { - o, _ := os.Getwd() - patterns[i] = filepath.Join(o, patterns[i]) - } - if isFile(patterns[i]) { - files = append(files, patterns[i]) - } else { - f, err := glob(filepath.Split(patterns[i])) //filepath.Glob(patterns[i]) - if err != nil { - errs = append(errs, err) - } else { - files = append(files, f...) - } - } - } - return files, errs -} - -func readYamlFile(yamlFile []byte) ([]workloadinterface.IMetadata, []error) { - errs := []error{} - - r := bytes.NewReader(yamlFile) - dec := yaml.NewDecoder(r) - yamlObjs := []workloadinterface.IMetadata{} - - var t interface{} - for dec.Decode(&t) == nil { - j := convertYamlToJson(t) - if j == nil { - continue - } - if obj, ok := j.(map[string]interface{}); ok { - if o := objectsenvelopes.NewObject(obj); o != nil { - if o.GetKind() == "List" { - yamlObjs = append(yamlObjs, handleListObject(o)...) - } else { - yamlObjs = append(yamlObjs, o) - } - } - } else { - errs = append(errs, fmt.Errorf("failed to convert yaml file to map[string]interface, file content: %v", j)) - } - } - - return yamlObjs, errs -} - -func readJsonFile(jsonFile []byte) ([]workloadinterface.IMetadata, []error) { - workloads := []workloadinterface.IMetadata{} - var jsonObj interface{} - if err := json.Unmarshal(jsonFile, &jsonObj); err != nil { - return workloads, []error{err} - } - - convertJsonToWorkload(jsonObj, &workloads) - - return workloads, nil -} -func convertJsonToWorkload(jsonObj interface{}, workloads *[]workloadinterface.IMetadata) { - - switch x := jsonObj.(type) { - case map[string]interface{}: - if o := objectsenvelopes.NewObject(x); o != nil { - (*workloads) = append(*workloads, o) - } - case []interface{}: - for i := range x { - convertJsonToWorkload(x[i], workloads) - } - } -} -func convertYamlToJson(i interface{}) interface{} { - switch x := i.(type) { - case map[interface{}]interface{}: - m2 := map[string]interface{}{} - for k, v := range x { - if s, ok := k.(string); ok { - m2[s] = convertYamlToJson(v) - } - } - return m2 - case []interface{}: - for i, v := range x { - x[i] = convertYamlToJson(v) - } - } - return i -} - -func isYaml(filePath string) bool { - return cautils.StringInSlice(YAML_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound -} - -func isJson(filePath string) bool { - return cautils.StringInSlice(JSON_PREFIX, filepath.Ext(filePath)) != cautils.ValueNotFound -} - -func glob(root, pattern string) ([]string, error) { - var matches []string - - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil - } - if matched, err := filepath.Match(pattern, filepath.Base(path)); err != nil { - return err - } else if matched { - matches = append(matches, path) - } - return nil - }) - if err != nil { - return nil, err - } - return matches, nil -} -func isFile(name string) bool { - if fi, err := os.Stat(name); err == nil { - if fi.Mode().IsRegular() { - return true - } - } - return false -} - -func getFileFormat(filePath string) FileFormat { - if isYaml(filePath) { - return YAML_FILE_FORMAT - } else if isJson(filePath) { - return JSON_FILE_FORMAT - } else { - return FileFormat(filePath) - } -} - -// handleListObject handle a List manifest -func handleListObject(obj workloadinterface.IMetadata) []workloadinterface.IMetadata { - yamlObjs := []workloadinterface.IMetadata{} - if i, ok := workloadinterface.InspectMap(obj.GetObject(), "items"); ok && i != nil { - if items, ok := i.([]interface{}); ok && items != nil { - for item := range items { - if m, ok := items[item].(map[string]interface{}); ok && m != nil { - if o := objectsenvelopes.NewObject(m); o != nil { - yamlObjs = append(yamlObjs, o) - } - } - } - } - } - return yamlObjs -} diff --git a/resourcehandler/urlloader.go b/resourcehandler/urlloader.go index 27b2a332..9cfe9acd 100644 --- a/resourcehandler/urlloader.go +++ b/resourcehandler/urlloader.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/armosec/k8s-interface/workloadinterface" + "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" ) @@ -28,7 +29,7 @@ func listUrls(patterns []string) []string { urls := []string{} for i := range patterns { if strings.HasPrefix(patterns[i], "http") { - if !isYaml(patterns[i]) && !isJson(patterns[i]) { // if url of repo + if !cautils.IsYaml(patterns[i]) && !cautils.IsJson(patterns[i]) { // if url of repo if yamls, err := ScanRepository(patterns[i], ""); err == nil { // TODO - support branch urls = append(urls, yamls...) } else { @@ -52,7 +53,7 @@ func downloadFiles(urls []string) ([]workloadinterface.IMetadata, []error) { errs = append(errs, err) continue } - w, e := readFile(f, getFileFormat(urls[i])) + w, e := cautils.ReadFile(f, cautils.GetFileFormat(urls[i])) errs = append(errs, e...) if w != nil { workloads = append(workloads, w...) diff --git a/resultshandling/printer/v1/prettyprinter.go b/resultshandling/printer/v1/prettyprinter.go index 13c5eea6..d6cb8580 100644 --- a/resultshandling/printer/v1/prettyprinter.go +++ b/resultshandling/printer/v1/prettyprinter.go @@ -30,6 +30,7 @@ func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { } } +// !!!! DEPRECATED !!!! use v2 package func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { overallRiskScore := opaSessionObj.Report.SummaryDetails.Score cautils.ReportV2ToV1(opaSessionObj) From 208bb251185198dba4dee2c0d1f6bea08423161c Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 27 Feb 2022 16:33:21 +0200 Subject: [PATCH 31/44] fixed junit support --- resultshandling/printer/v1/junit.go | 125 -------- resultshandling/printer/v1/prettyprinter.go | 271 ------------------ resultshandling/printer/v1/printresults.go | 3 - resultshandling/printer/v1/silentprinter.go | 11 - resultshandling/printer/v1/summary.go | 54 ---- resultshandling/printer/v1/summeryhelpers.go | 85 ------ resultshandling/printer/v2/junit.go | 154 +++++----- .../printer/v2/resourcemapping/jsonprinter.go | 41 --- resultshandling/results.go | 2 +- 9 files changed, 76 insertions(+), 670 deletions(-) delete mode 100644 resultshandling/printer/v1/junit.go delete mode 100644 resultshandling/printer/v1/prettyprinter.go delete mode 100644 resultshandling/printer/v1/printresults.go delete mode 100644 resultshandling/printer/v1/silentprinter.go delete mode 100644 resultshandling/printer/v1/summary.go delete mode 100644 resultshandling/printer/v1/summeryhelpers.go delete mode 100644 resultshandling/printer/v2/resourcemapping/jsonprinter.go diff --git a/resultshandling/printer/v1/junit.go b/resultshandling/printer/v1/junit.go deleted file mode 100644 index baee79fd..00000000 --- a/resultshandling/printer/v1/junit.go +++ /dev/null @@ -1,125 +0,0 @@ -package v1 - -import ( - "encoding/xml" - "fmt" - "os" - - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/logger" - "github.com/armosec/kubescape/resultshandling/printer" - "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 = printer.GetWriter(outputFile) -} - -func (junitPrinter *JunitPrinter) Score(score float32) { - fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) -} - -func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - cautils.ReportV2ToV1(opaSessionObj) - - junitResult, err := convertPostureReportToJunitResult(opaSessionObj.PostureReport) - if err != nil { - logger.L().Fatal("failed to convert posture report object") - } - postureReportStr, err := xml.Marshal(junitResult) - if err != nil { - logger.L().Fatal("failed to convert posture report object") - } - junitPrinter.writer.Write(postureReportStr) -} - -type JUnitTestSuites struct { - XMLName xml.Name `xml:"testsuites"` - Suites []JUnitTestSuite `xml:"testsuite"` -} - -// JUnitTestSuite is a single JUnit test suite which may contain many -// testcases. -type JUnitTestSuite struct { - XMLName xml.Name `xml:"testsuite"` - Tests int `xml:"tests,attr"` - Time string `xml:"time,attr"` - Name string `xml:"name,attr"` - Resources int `xml:"resources,attr"` - Excluded int `xml:"excluded,attr"` - Failed int `xml:"filed,attr"` - Properties []JUnitProperty `xml:"properties>property,omitempty"` - TestCases []JUnitTestCase `xml:"testcase"` -} - -// JUnitTestCase is a single test case with its result. -type JUnitTestCase struct { - XMLName xml.Name `xml:"testcase"` - Classname string `xml:"classname,attr"` - Name string `xml:"name,attr"` - Time string `xml:"time,attr"` - Resources int `xml:"resources,attr"` - Excluded int `xml:"excluded,attr"` - Failed int `xml:"filed,attr"` - SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` - Failure *JUnitFailure `xml:"failure,omitempty"` -} - -// JUnitSkipMessage contains the reason why a testcase was skipped. -type JUnitSkipMessage struct { - Message string `xml:"message,attr"` -} - -// JUnitProperty represents a key/value pair used to define properties. -type JUnitProperty struct { - Name string `xml:"name,attr"` - Value string `xml:"value,attr"` -} - -// JUnitFailure contains data related to a failed test. -type JUnitFailure struct { - Message string `xml:"message,attr"` - Type string `xml:"type,attr"` - Contents string `xml:",chardata"` -} - -func convertPostureReportToJunitResult(postureResult *reporthandling.PostureReport) (*JUnitTestSuites, error) { - juResult := JUnitTestSuites{XMLName: xml.Name{Local: "Kubescape scan results"}} - for _, framework := range postureResult.FrameworkReports { - suite := JUnitTestSuite{ - Name: framework.Name, - Resources: framework.GetNumberOfResources(), - Excluded: framework.GetNumberOfWarningResources(), - Failed: framework.GetNumberOfFailedResources(), - } - for _, controlReports := range framework.ControlReports { - suite.Tests = suite.Tests + 1 - testCase := JUnitTestCase{} - testCase.Name = controlReports.Name - testCase.Classname = "Kubescape" - testCase.Time = postureResult.ReportGenerationTime.String() - if 0 < len(controlReports.RuleReports) && 0 < len(controlReports.RuleReports[0].RuleResponses) { - 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 { - failure.Contents = fmt.Sprintf("%s\n%s", failure.Contents, ruleResponses.AlertMessage) - } - testCase.Failure = &failure - } - suite.TestCases = append(suite.TestCases, testCase) - } - juResult.Suites = append(juResult.Suites, suite) - } - return &juResult, nil -} diff --git a/resultshandling/printer/v1/prettyprinter.go b/resultshandling/printer/v1/prettyprinter.go deleted file mode 100644 index d6cb8580..00000000 --- a/resultshandling/printer/v1/prettyprinter.go +++ /dev/null @@ -1,271 +0,0 @@ -package v1 - -import ( - "fmt" - "os" - "sort" - "strings" - - "github.com/armosec/k8s-interface/workloadinterface" - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/resultshandling/printer" - "github.com/armosec/opa-utils/objectsenvelopes" - "github.com/armosec/opa-utils/reporthandling" - "github.com/enescakir/emoji" - "github.com/olekukonko/tablewriter" -) - -type PrettyPrinter struct { - writer *os.File - summary Summary - verboseMode bool - sortedControlNames []string - frameworkSummary ResultSummary -} - -func NewPrettyPrinter(verboseMode bool) *PrettyPrinter { - return &PrettyPrinter{ - verboseMode: verboseMode, - summary: NewSummary(), - } -} - -// !!!! DEPRECATED !!!! use v2 package -func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - overallRiskScore := opaSessionObj.Report.SummaryDetails.Score - cautils.ReportV2ToV1(opaSessionObj) - - // score := calculatePostureScore(opaSessionObj.PostureReport) - failedResources := []string{} - warningResources := []string{} - allResources := []string{} - frameworkNames := []string{} - frameworkScores := []float32{} - - for _, frameworkReport := range opaSessionObj.PostureReport.FrameworkReports { - frameworkNames = append(frameworkNames, frameworkReport.Name) - frameworkScores = append(frameworkScores, frameworkReport.Score) - failedResources = reporthandling.GetUniqueResourcesIDs(append(failedResources, frameworkReport.ListResourcesIDs().GetFailedResources()...)) - warningResources = reporthandling.GetUniqueResourcesIDs(append(warningResources, frameworkReport.ListResourcesIDs().GetWarningResources()...)) - allResources = reporthandling.GetUniqueResourcesIDs(append(allResources, frameworkReport.ListResourcesIDs().GetAllResources()...)) - prettyPrinter.summarySetup(frameworkReport, opaSessionObj.AllResources) - } - - prettyPrinter.frameworkSummary = ResultSummary{ - RiskScore: overallRiskScore, - TotalResources: len(allResources), - TotalFailed: len(failedResources), - TotalWarning: len(warningResources), - } - - prettyPrinter.printResults() - prettyPrinter.printSummaryTable(frameworkNames, frameworkScores) - -} - -func (prettyPrinter *PrettyPrinter) SetWriter(outputFile string) { - prettyPrinter.writer = printer.GetWriter(outputFile) -} - -func (prettyPrinter *PrettyPrinter) Score(score float32) { -} - -func (prettyPrinter *PrettyPrinter) summarySetup(fr reporthandling.FrameworkReport, allResources map[string]workloadinterface.IMetadata) { - - for _, cr := range fr.ControlReports { - // if len(cr.RuleReports) == 0 { - // continue - // } - workloadsSummary := listResultSummary(cr.RuleReports, allResources) - - var passedWorkloads map[string][]WorkloadSummary - if prettyPrinter.verboseMode { - passedWorkloads = groupByNamespaceOrKind(workloadsSummary, workloadSummaryPassed) - } - - //controlSummary - prettyPrinter.summary[cr.Name] = ResultSummary{ - ID: cr.ControlID, - RiskScore: cr.Score, - TotalResources: cr.GetNumberOfResources(), - TotalFailed: cr.GetNumberOfFailedResources(), - TotalWarning: cr.GetNumberOfWarningResources(), - FailedWorkloads: groupByNamespaceOrKind(workloadsSummary, workloadSummaryFailed), - ExcludedWorkloads: groupByNamespaceOrKind(workloadsSummary, workloadSummaryExclude), - PassedWorkloads: passedWorkloads, - Description: cr.Description, - Remediation: cr.Remediation, - ListInputKinds: cr.ListControlsInputKinds(), - } - - } - prettyPrinter.sortedControlNames = prettyPrinter.getSortedControlsNames() -} -func (prettyPrinter *PrettyPrinter) printResults() { - for i := 0; i < len(prettyPrinter.sortedControlNames); i++ { - controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]] - prettyPrinter.printTitle(prettyPrinter.sortedControlNames[i], &controlSummary) - prettyPrinter.printResources(&controlSummary) - if prettyPrinter.summary[prettyPrinter.sortedControlNames[i]].TotalResources > 0 { - prettyPrinter.printSummary(prettyPrinter.sortedControlNames[i], &controlSummary) - } - - } -} - -func (prettyPrinter *PrettyPrinter) printSummary(controlName string, controlSummary *ResultSummary) { - cautils.SimpleDisplay(prettyPrinter.writer, "Summary - ") - cautils.SuccessDisplay(prettyPrinter.writer, "Passed:%v ", controlSummary.TotalResources-controlSummary.TotalFailed-controlSummary.TotalWarning) - cautils.WarningDisplay(prettyPrinter.writer, "Excluded:%v ", controlSummary.TotalWarning) - cautils.FailureDisplay(prettyPrinter.writer, "Failed:%v ", controlSummary.TotalFailed) - cautils.InfoDisplay(prettyPrinter.writer, "Total:%v\n", controlSummary.TotalResources) - if controlSummary.TotalFailed > 0 { - cautils.DescriptionDisplay(prettyPrinter.writer, "Remediation: %v\n", controlSummary.Remediation) - } - cautils.DescriptionDisplay(prettyPrinter.writer, "\n") - -} -func (prettyPrinter *PrettyPrinter) printTitle(controlName string, controlSummary *ResultSummary) { - cautils.InfoDisplay(prettyPrinter.writer, "[control: %s - %s] ", controlName, getControlURL(controlSummary.ID)) - if controlSummary.TotalResources == 0 { - cautils.InfoDisplay(prettyPrinter.writer, "skipped %v\n", emoji.ConfusedFace) - } else if controlSummary.TotalFailed != 0 { - cautils.FailureDisplay(prettyPrinter.writer, "failed %v\n", emoji.SadButRelievedFace) - } else if controlSummary.TotalWarning != 0 { - cautils.WarningDisplay(prettyPrinter.writer, "excluded %v\n", emoji.NeutralFace) - } else { - cautils.SuccessDisplay(prettyPrinter.writer, "passed %v\n", emoji.ThumbsUp) - } - - cautils.DescriptionDisplay(prettyPrinter.writer, "Description: %s\n", controlSummary.Description) - -} -func (prettyPrinter *PrettyPrinter) printResources(controlSummary *ResultSummary) { - - if len(controlSummary.FailedWorkloads) > 0 { - cautils.FailureDisplay(prettyPrinter.writer, "Failed:\n") - prettyPrinter.printGroupedResources(controlSummary.FailedWorkloads) - } - if len(controlSummary.ExcludedWorkloads) > 0 { - cautils.WarningDisplay(prettyPrinter.writer, "Excluded:\n") - prettyPrinter.printGroupedResources(controlSummary.ExcludedWorkloads) - } - if len(controlSummary.PassedWorkloads) > 0 { - cautils.SuccessDisplay(prettyPrinter.writer, "Passed:\n") - prettyPrinter.printGroupedResources(controlSummary.PassedWorkloads) - } - -} - -func (prettyPrinter *PrettyPrinter) printGroupedResources(workloads map[string][]WorkloadSummary) { - indent := INDENT - for title, rsc := range workloads { - prettyPrinter.printGroupedResource(indent, title, rsc) - } -} - -func (prettyPrinter *PrettyPrinter) printGroupedResource(indent string, title string, rsc []WorkloadSummary) { - preIndent := indent - if title != "" { - cautils.SimpleDisplay(prettyPrinter.writer, "%s%s\n", indent, title) - indent += indent - } - - for r := range rsc { - relatedObjectsStr := generateRelatedObjectsStr(rsc[r]) - cautils.SimpleDisplay(prettyPrinter.writer, fmt.Sprintf("%s%s - %s %s\n", indent, rsc[r].resource.GetKind(), rsc[r].resource.GetName(), relatedObjectsStr)) - } - indent = preIndent -} - -func generateRelatedObjectsStr(workload WorkloadSummary) string { - relatedStr := "" - if workload.resource.GetObjectType() == workloadinterface.TypeWorkloadObject { - relatedObjects := objectsenvelopes.NewRegoResponseVectorObject(workload.resource.GetObject()).GetRelatedObjects() - for i, related := range relatedObjects { - if ns := related.GetNamespace(); i == 0 && ns != "" { - relatedStr += fmt.Sprintf("Namespace - %s, ", ns) - } - relatedStr += fmt.Sprintf("%s - %s, ", related.GetKind(), related.GetName()) - } - } - if relatedStr != "" { - relatedStr = fmt.Sprintf(" [%s]", relatedStr[:len(relatedStr)-2]) - } - return relatedStr -} - -func generateRow(control string, cs ResultSummary) []string { - row := []string{control} - row = append(row, cs.ToSlice()...) - if cs.TotalResources != 0 { - row = append(row, fmt.Sprintf("%d", int(cs.RiskScore))+"%") - } else { - row = append(row, "skipped") - } - return row -} - -func generateHeader() []string { - return []string{"Control Name", "Failed Resources", "Excluded Resources", "All Resources", "% risk-score"} -} - -func generateFooter(prettyPrinter *PrettyPrinter) []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", prettyPrinter.frameworkSummary.TotalFailed)) - row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalWarning)) - row = append(row, fmt.Sprintf("%d", prettyPrinter.frameworkSummary.TotalResources)) - row = append(row, fmt.Sprintf("%.2f%s", prettyPrinter.frameworkSummary.RiskScore, "%")) - - return row -} -func (prettyPrinter *PrettyPrinter) printSummaryTable(frameworksNames []string, frameworkScores []float32) { - // For control scan framework will be nil - prettyPrinter.printFramework(frameworksNames, frameworkScores) - - summaryTable := tablewriter.NewWriter(prettyPrinter.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(prettyPrinter.sortedControlNames); i++ { - controlSummary := prettyPrinter.summary[prettyPrinter.sortedControlNames[i]] - summaryTable.Append(generateRow(prettyPrinter.sortedControlNames[i], controlSummary)) - } - - summaryTable.SetFooter(generateFooter(prettyPrinter)) - - // summaryTable.SetFooter(generateFooter()) - summaryTable.Render() -} - -func (prettyPrinter *PrettyPrinter) printFramework(frameworksNames []string, frameworkScores []float32) { - if len(frameworksNames) == 1 { - if frameworksNames[0] != "" { - cautils.InfoTextDisplay(prettyPrinter.writer, fmt.Sprintf("FRAMEWORK %s\n", frameworksNames[0])) - } - } else if len(frameworksNames) > 1 { - p := "FRAMEWORKS: " - for i := 0; i < len(frameworksNames)-1; i++ { - p += fmt.Sprintf("%s (risk: %.2f), ", frameworksNames[i], frameworkScores[i]) - } - p += fmt.Sprintf("%s (risk: %.2f)\n", frameworksNames[len(frameworksNames)-1], frameworkScores[len(frameworkScores)-1]) - cautils.InfoTextDisplay(prettyPrinter.writer, p) - } -} - -func (prettyPrinter *PrettyPrinter) getSortedControlsNames() []string { - controlNames := make([]string, 0, len(prettyPrinter.summary)) - for k := range prettyPrinter.summary { - controlNames = append(controlNames, k) - } - sort.Strings(controlNames) - return controlNames -} -func getControlURL(controlID string) string { - return fmt.Sprintf("https://hub.armo.cloud/docs/%s", strings.ToLower(controlID)) -} diff --git a/resultshandling/printer/v1/printresults.go b/resultshandling/printer/v1/printresults.go deleted file mode 100644 index 043020dc..00000000 --- a/resultshandling/printer/v1/printresults.go +++ /dev/null @@ -1,3 +0,0 @@ -package v1 - -var INDENT = " " diff --git a/resultshandling/printer/v1/silentprinter.go b/resultshandling/printer/v1/silentprinter.go deleted file mode 100644 index 37476b59..00000000 --- a/resultshandling/printer/v1/silentprinter.go +++ /dev/null @@ -1,11 +0,0 @@ -package v1 - -import ( - "github.com/armosec/kubescape/cautils" -) - -type SilentPrinter struct { -} - -func (silentPrinter *SilentPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { -} diff --git a/resultshandling/printer/v1/summary.go b/resultshandling/printer/v1/summary.go deleted file mode 100644 index 18eeed58..00000000 --- a/resultshandling/printer/v1/summary.go +++ /dev/null @@ -1,54 +0,0 @@ -package v1 - -import ( - "fmt" - - "github.com/armosec/k8s-interface/workloadinterface" - "github.com/armosec/opa-utils/reporthandling" -) - -type Summary map[string]ResultSummary - -func NewSummary() Summary { - return make(map[string]ResultSummary) -} - -type ResultSummary struct { - ID string - RiskScore float32 - TotalResources int - TotalFailed int - TotalWarning int - Description string - Remediation string - Framework []string - ListInputKinds []string - FailedWorkloads map[string][]WorkloadSummary // :[] - ExcludedWorkloads map[string][]WorkloadSummary // :[] - PassedWorkloads map[string][]WorkloadSummary // :[] -} - -type WorkloadSummary struct { - resource workloadinterface.IMetadata - status string -} - -func (controlSummary *ResultSummary) ToSlice() []string { - s := []string{} - s = append(s, fmt.Sprintf("%d", controlSummary.TotalFailed)) - s = append(s, fmt.Sprintf("%d", controlSummary.TotalWarning)) - s = append(s, fmt.Sprintf("%d", controlSummary.TotalResources)) - return s -} - -func workloadSummaryFailed(workloadSummary *WorkloadSummary) bool { - return workloadSummary.status == reporthandling.StatusFailed -} - -func workloadSummaryExclude(workloadSummary *WorkloadSummary) bool { - return workloadSummary.status == reporthandling.StatusWarning -} - -func workloadSummaryPassed(workloadSummary *WorkloadSummary) bool { - return workloadSummary.status == reporthandling.StatusPassed -} diff --git a/resultshandling/printer/v1/summeryhelpers.go b/resultshandling/printer/v1/summeryhelpers.go deleted file mode 100644 index cff9c74d..00000000 --- a/resultshandling/printer/v1/summeryhelpers.go +++ /dev/null @@ -1,85 +0,0 @@ -package v1 - -import ( - "github.com/armosec/k8s-interface/k8sinterface" - "github.com/armosec/k8s-interface/workloadinterface" - "github.com/armosec/opa-utils/objectsenvelopes" - "github.com/armosec/opa-utils/reporthandling" -) - -// Group workloads by namespace - return {"namespace": <[]WorkloadSummary>} -func groupByNamespaceOrKind(resources []WorkloadSummary, status func(workloadSummary *WorkloadSummary) bool) map[string][]WorkloadSummary { - mapResources := make(map[string][]WorkloadSummary) - for i := range resources { - if !status(&resources[i]) { - continue - } - t := resources[i].resource.GetObjectType() - if t == objectsenvelopes.TypeRegoResponseVectorObject && !isKindToBeGrouped(resources[i].resource.GetKind()) { - t = workloadinterface.TypeWorkloadObject - } - switch t { // TODO - find a better way to defind the groups - case workloadinterface.TypeWorkloadObject: - ns := "" - if resources[i].resource.GetNamespace() != "" { - ns = "Namespace " + resources[i].resource.GetNamespace() - } - if r, ok := mapResources[ns]; ok { - r = append(r, resources[i]) - mapResources[ns] = r - } else { - mapResources[ns] = []WorkloadSummary{resources[i]} - } - case objectsenvelopes.TypeRegoResponseVectorObject: - group := resources[i].resource.GetKind() + "s" - if r, ok := mapResources[group]; ok { - r = append(r, resources[i]) - mapResources[group] = r - } else { - mapResources[group] = []WorkloadSummary{resources[i]} - } - default: - group, _ := k8sinterface.SplitApiVersion(resources[i].resource.GetApiVersion()) - if r, ok := mapResources[group]; ok { - r = append(r, resources[i]) - mapResources[group] = r - } else { - mapResources[group] = []WorkloadSummary{resources[i]} - } - } - } - return mapResources -} - -func isKindToBeGrouped(kind string) bool { - if kind == "Group" || kind == "User" { - return true - } - return false -} - -func listResultSummary(ruleReports []reporthandling.RuleReport, allResources map[string]workloadinterface.IMetadata) []WorkloadSummary { - workloadsSummary := []WorkloadSummary{} - - for c := range ruleReports { - resourcesIDs := ruleReports[c].ListResourcesIDs() - workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetFailedResources(), reporthandling.StatusFailed)...) - workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetWarningResources(), reporthandling.StatusWarning)...) - workloadsSummary = append(workloadsSummary, newListWorkloadsSummary(allResources, resourcesIDs.GetPassedResources(), reporthandling.StatusPassed)...) - } - return workloadsSummary -} - -func newListWorkloadsSummary(allResources map[string]workloadinterface.IMetadata, resourcesIDs []string, status string) []WorkloadSummary { - workloadsSummary := []WorkloadSummary{} - - for _, i := range resourcesIDs { - if r, ok := allResources[i]; ok { - workloadsSummary = append(workloadsSummary, WorkloadSummary{ - resource: r, - status: status, - }) - } - } - return workloadsSummary -} diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 84cbd757..f1b0317f 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -5,18 +5,22 @@ import ( "fmt" "os" + "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" - reporthandlingv2 "github.com/armosec/opa-utils/reporthandling/v2" ) type JunitPrinter struct { - writer *os.File + writer *os.File + verbose bool } -func NewJunitPrinter() *JunitPrinter { - return &JunitPrinter{} +func NewJunitPrinter(verbose bool) *JunitPrinter { + return &JunitPrinter{ + verbose: verbose, + } } func (junitPrinter *JunitPrinter) SetWriter(outputFile string) { @@ -32,97 +36,89 @@ func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASession } func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - junitResult, err := convertPostureReportToJunitResult(opaSessionObj.Report) + junitResult, err := junitPrinter.convertPostureReportToJunitResult(opaSessionObj) if err != nil { - logger.L().Fatal("failed to convert posture report object") + logger.L().Fatal("failed to build xml result object", helpers.Error(err)) } postureReportStr, err := xml.Marshal(junitResult) if err != nil { - logger.L().Fatal("failed to convert posture report object") + logger.L().Fatal("failed to Marshal xml result object", helpers.Error(err)) } junitPrinter.writer.Write(postureReportStr) } type JUnitTestSuites struct { - XMLName xml.Name `xml:"testsuites"` - Suites []JUnitTestSuite `xml:"testsuite"` -} - -// JUnitTestSuite is a single JUnit test suite which may contain many -// testcases. -type JUnitTestSuite struct { - XMLName xml.Name `xml:"testsuite"` - Tests int `xml:"tests,attr"` - Time string `xml:"time,attr"` - Name string `xml:"name,attr"` - Resources int `xml:"resources,attr"` - Excluded int `xml:"excluded,attr"` - Failed int `xml:"filed,attr"` - Properties []JUnitProperty `xml:"properties>property,omitempty"` - TestCases []JUnitTestCase `xml:"testcase"` + XMLName xml.Name `xml:"testsuite"` + Suites []JUnitTestCase `xml:"testsuites"` + RiskScore float32 `xml:"riskScore,attr"` // test risk score + Time string `xml:"time,attr"` // scanning time + Controls int `xml:"tests,attr"` // number of controls } // JUnitTestCase is a single test case with its result. -type JUnitTestCase struct { - XMLName xml.Name `xml:"testcase"` - Classname string `xml:"classname,attr"` - Name string `xml:"name,attr"` - Time string `xml:"time,attr"` - Resources int `xml:"resources,attr"` - Excluded int `xml:"excluded,attr"` - Failed int `xml:"filed,attr"` - SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` - Failure *JUnitFailure `xml:"failure,omitempty"` +type JUnitTestCase struct { // Control + XMLName xml.Name `xml:"testcase"` + RiskScore float32 `xml:"riskScore,attr"` + Status string `xml:"status,attr"` + Name string `xml:"name,attr"` + AllResources int `xml:"allResources,attr"` + Excluded int `xml:"excludedResources,attr"` + Failed int `xml:"filedResources,attr"` + Resources []JUnitResource `xml:"resources"` } -// JUnitSkipMessage contains the reason why a testcase was skipped. -type JUnitSkipMessage struct { - Message string `xml:"message,attr"` +type JUnitResource struct { // Single resource + Name string `xml:"name,attr"` + Namespace string `xml:"namespace,attr"` + Kind string `xml:"kind,attr"` + ApiVersion string `xml:"apiVersion,attr"` + FailedPaths []armotypes.PosturePaths `xml:"jsonPaths"` } -// JUnitProperty represents a key/value pair used to define properties. -type JUnitProperty struct { - Name string `xml:"name,attr"` - Value string `xml:"value,attr"` -} - -// JUnitFailure contains data related to a failed test. -type JUnitFailure struct { - Message string `xml:"message,attr"` - Type string `xml:"type,attr"` - Contents string `xml:",chardata"` -} - -func convertPostureReportToJunitResult(postureResult *reporthandlingv2.PostureReport) (*JUnitTestSuites, error) { - juResult := JUnitTestSuites{XMLName: xml.Name{Local: "Kubescape scan results"}} - for _, framework := range postureResult.ListFrameworks().All() { - suite := JUnitTestSuite{ - Name: framework.GetName(), - Resources: framework.NumberOfResources().All(), - Excluded: framework.NumberOfResources().Excluded(), - Failed: framework.NumberOfResources().Failed(), - } - for _, controlReports := range postureResult.ListControls().All() { - suite.Tests = suite.Tests + 1 - testCase := JUnitTestCase{} - testCase.Name = controlReports.GetName() - testCase.Classname = "Kubescape" - testCase.Time = postureResult.ReportGenerationTime.String() - // if 0 < len(controlReports.RuleReports[0].RuleResponses) { - - // testCase.Resources = controlReports.NumberOfResources().All() - // testCase.Excluded = controlReports.NumberOfResources().Excluded() - // testCase.Failed = controlReports.NumberOfResources().Failed() - // failure := JUnitFailure{} - // failure.Message = fmt.Sprintf("%d resources failed", testCase.Failed) - // for _, ruleResponses := range controlReports.RuleReports[0].RuleResponses { - // failure.Contents = fmt.Sprintf("%s\n%s", failure.Contents, ruleResponses.AlertMessage) - // } - // testCase.Failure = &failure - // } - suite.TestCases = append(suite.TestCases, testCase) - } - juResult.Suites = append(juResult.Suites, suite) +func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { + juResult := JUnitTestSuites{ + XMLName: xml.Name{ + Local: "Kubescape scan results", + }, + RiskScore: results.Report.SummaryDetails.Score, + Time: results.Report.GetTimestamp().String(), + Controls: len(results.Report.ListControls().All()), } + + // controls + for _, controlReports := range results.Report.ListControls().All() { + testCase := JUnitTestCase{} + testCase.Name = controlReports.GetName() + testCase.Status = string(controlReports.GetStatus().Status()) + + // resources + testCase.AllResources = controlReports.NumberOfResources().All() + testCase.Excluded = controlReports.NumberOfResources().Excluded() + testCase.Failed = controlReports.NumberOfResources().Failed() + + var jUnitResources []JUnitResource + for _, resourceID := range controlReports.ListResourcesIDs().All() { + if !junitPrinter.verbose { + continue + } + jUnitResource := JUnitResource{} + if resource, ok := results.AllResources[resourceID]; ok { + jUnitResource.Name = resource.GetName() + jUnitResource.Namespace = resource.GetNamespace() + jUnitResource.Kind = resource.GetKind() + jUnitResource.ApiVersion = resource.GetApiVersion() + } + if result, ok := results.ResourcesResult[resourceID]; ok { + rules := result.ListRulesOfControl("", controlReports.GetName()) + for _, rule := range rules { + jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) + } + } + jUnitResources = append(jUnitResources, jUnitResource) + } + testCase.Resources = jUnitResources + juResult.Suites = append(juResult.Suites, testCase) + } + return &juResult, nil } diff --git a/resultshandling/printer/v2/resourcemapping/jsonprinter.go b/resultshandling/printer/v2/resourcemapping/jsonprinter.go deleted file mode 100644 index 38194cb5..00000000 --- a/resultshandling/printer/v2/resourcemapping/jsonprinter.go +++ /dev/null @@ -1,41 +0,0 @@ -package resourcemapping - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/logger" - "github.com/armosec/kubescape/resultshandling/printer" -) - -type JsonPrinter struct { - writer *os.File -} - -func NewJsonPrinter() *JsonPrinter { - return &JsonPrinter{} -} - -func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) { - jsonPrinter.writer = printer.GetWriter(outputFile) -} - -func (jsonPrinter *JsonPrinter) Score(score float32) { - fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) -} - -func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - - postureReportStr, err := json.Marshal(opaSessionObj.Report) - - if err != nil { - logger.L().Fatal("failed to convert posture report object") - } - jsonPrinter.writer.Write(postureReportStr) -} - -func (jsonPrinter *JsonPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) { - // finalizeReport(opaSessionObj) -} diff --git a/resultshandling/results.go b/resultshandling/results.go index f6f4917d..e5ec8ec5 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -59,7 +59,7 @@ func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPr case printer.JsonFormat: return printerv1.NewJsonPrinter() case printer.JunitResultFormat: - return printerv1.NewJunitPrinter() + return printerv2.NewJunitPrinter(verboseMode) case printer.PrometheusFormat: return printerv1.NewPrometheusPrinter(verboseMode) case printer.PdfFormat: From 544a19906e149086341235818794db26f4c7cc2a Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 27 Feb 2022 17:29:42 +0200 Subject: [PATCH 32/44] update junit --- resultshandling/printer/v2/junit.go | 124 ++++++++++++-------- resultshandling/printer/v2/resourcetable.go | 27 +++-- 2 files changed, 92 insertions(+), 59 deletions(-) diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index f1b0317f..987a4f79 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -10,6 +10,7 @@ import ( "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" ) type JunitPrinter struct { @@ -17,6 +18,44 @@ type JunitPrinter struct { verbose bool } +type JUnitTestSuites struct { + XMLName xml.Name `xml:"testsuite"` + Suites []JUnitTestCase `xml:"testsuites"` + Frameworks []JUnitFrameworks `xml:"framework"` + RiskScore float32 `xml:"riskScore,attr"` // test risk score + Time string `xml:"time,attr"` // scanning time + Controls int `xml:"testcases,attr"` // number of controls +} + +type JUnitFrameworks struct { // Frameworks + Name string `xml:"name,attr"` + RiskScore float32 `xml:"riskscore,attr"` + Status string `xml:"status,attr"` +} + +// JUnitTestCase is a single test case with its result. +type JUnitTestCase struct { // Control + XMLName xml.Name `xml:"testcase"` + Name string `xml:"name,attr"` + ID string `xml:"id,attr"` + Url string `xml:"url,attr"` + RiskScore float32 `xml:"riskScore,attr"` + Status string `xml:"status,attr"` + Info string `xml:"info,attr"` + AllResources int `xml:"allResources,attr"` + Excluded int `xml:"excludedResources,attr"` + Failed int `xml:"filedResources,attr"` + Resources []JUnitResource `xml:"resource"` +} + +type JUnitResource struct { // Single resource + Name string `xml:"name,attr"` + Namespace string `xml:"namespace,attr"` + Kind string `xml:"kind,attr"` + ApiVersion string `xml:"apiVersion,attr"` + FailedPaths []armotypes.PosturePaths `xml:"jsonpath"` +} + func NewJunitPrinter(verbose bool) *JunitPrinter { return &JunitPrinter{ verbose: verbose, @@ -47,73 +86,66 @@ func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionO junitPrinter.writer.Write(postureReportStr) } -type JUnitTestSuites struct { - XMLName xml.Name `xml:"testsuite"` - Suites []JUnitTestCase `xml:"testsuites"` - RiskScore float32 `xml:"riskScore,attr"` // test risk score - Time string `xml:"time,attr"` // scanning time - Controls int `xml:"tests,attr"` // number of controls -} - -// JUnitTestCase is a single test case with its result. -type JUnitTestCase struct { // Control - XMLName xml.Name `xml:"testcase"` - RiskScore float32 `xml:"riskScore,attr"` - Status string `xml:"status,attr"` - Name string `xml:"name,attr"` - AllResources int `xml:"allResources,attr"` - Excluded int `xml:"excludedResources,attr"` - Failed int `xml:"filedResources,attr"` - Resources []JUnitResource `xml:"resources"` -} - -type JUnitResource struct { // Single resource - Name string `xml:"name,attr"` - Namespace string `xml:"namespace,attr"` - Kind string `xml:"kind,attr"` - ApiVersion string `xml:"apiVersion,attr"` - FailedPaths []armotypes.PosturePaths `xml:"jsonPaths"` -} - func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { juResult := JUnitTestSuites{ XMLName: xml.Name{ Local: "Kubescape scan results", }, - RiskScore: results.Report.SummaryDetails.Score, - Time: results.Report.GetTimestamp().String(), - Controls: len(results.Report.ListControls().All()), + RiskScore: results.Report.SummaryDetails.Score, + Time: results.Report.GetTimestamp().String(), + Controls: len(results.Report.ListControls().All()), + Frameworks: []JUnitFrameworks{}, + } + + // Frameworks + for _, frameworksReports := range results.Report.ListFrameworks().All() { + fw := JUnitFrameworks{} + fw.Name = frameworksReports.GetName() + fw.RiskScore = frameworksReports.GetScore() + fw.Status = string(frameworksReports.GetStatus().Status()) + juResult.Frameworks = append(juResult.Frameworks, fw) } // controls - for _, controlReports := range results.Report.ListControls().All() { + for _, controlIDs := range results.Report.ListControlsIDs().All() { + controlReport := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controlIDs) + + // control data testCase := JUnitTestCase{} - testCase.Name = controlReports.GetName() - testCase.Status = string(controlReports.GetStatus().Status()) + testCase.Name = controlReport.GetName() + testCase.ID = controlReport.GetID() + testCase.Url = getControlURL(controlReport.GetID()) + testCase.Name = controlReport.GetName() + testCase.Status = string(controlReport.GetStatus().Status()) + + // resources counters + testCase.AllResources = controlReport.NumberOfResources().All() + testCase.Excluded = controlReport.NumberOfResources().Excluded() + testCase.Failed = controlReport.NumberOfResources().Failed() // resources - testCase.AllResources = controlReports.NumberOfResources().All() - testCase.Excluded = controlReports.NumberOfResources().Excluded() - testCase.Failed = controlReports.NumberOfResources().Failed() - var jUnitResources []JUnitResource - for _, resourceID := range controlReports.ListResourcesIDs().All() { - if !junitPrinter.verbose { + for _, resourceID := range controlReport.ListResourcesIDs().All() { + result, ok := results.ResourcesResult[resourceID] + if !ok { continue } + if result.GetStatus(nil).IsPassed() && !junitPrinter.verbose { // add passed resources only in verbose mode + continue + } + jUnitResource := JUnitResource{} + rules := result.ListRulesOfControl(controlReport.GetID(), "") + for _, rule := range rules { + jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) + } if resource, ok := results.AllResources[resourceID]; ok { jUnitResource.Name = resource.GetName() jUnitResource.Namespace = resource.GetNamespace() jUnitResource.Kind = resource.GetKind() jUnitResource.ApiVersion = resource.GetApiVersion() } - if result, ok := results.ResourcesResult[resourceID]; ok { - rules := result.ListRulesOfControl("", controlReports.GetName()) - for _, rule := range rules { - jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) - } - } + jUnitResources = append(jUnitResources, jUnitResource) } testCase.Resources = jUnitResources diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go index cc291e8e..843963bf 100644 --- a/resultshandling/printer/v2/resourcetable.go +++ b/resultshandling/printer/v2/resourcetable.go @@ -2,6 +2,7 @@ package v2 import ( "fmt" + "sort" "strings" "github.com/armosec/k8s-interface/workloadinterface" @@ -21,7 +22,7 @@ func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesre summaryTable.SetRowLine(true) // summaryTable.SetFooter([]string{"", "", "Total", "", "$146.93"}) // For control scan framework will be nil - data := [][]string{} + data := Matrix{} for i := range results { resource, ok := allResources[i] if !ok { @@ -32,7 +33,7 @@ func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesre data = append(data, raw...) } } - sortTable(data) + sort.Sort(data) summaryTable.AppendBulk(data) summaryTable.Render() @@ -78,18 +79,18 @@ func generateResourceHeader() []string { return []string{"Control", "Namespace", "Kind/Name", "Statues"} } -func sortTable(data [][]string) { +type Matrix [][]string - for j := len(data[0]) - 1; j >= 0; j-- { - for k := 0; k < len(data)-2; { - if data[k][j] > data[k+1][j] { - tmp := data[k] - data[k] = data[k+1] - data[k+1] = tmp - k = 0 - } else { - k++ - } +func (a Matrix) Len() int { return len(a) } +func (a Matrix) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a Matrix) Less(i, j int) bool { + l := len(a[i]) + for k := 0; k < l; k++ { + if a[i][k] < a[j][k] { + return true + } else if a[i][k] > a[j][k] { + return false } } + return true } From ff0264ee15e7a6eb1f960fdea43ae2f2eb67c2a8 Mon Sep 17 00:00:00 2001 From: dwertent Date: Mon, 28 Feb 2022 18:19:17 +0200 Subject: [PATCH 33/44] extend exceptions support --- README.md | 3 +- cautils/getter/armoapi.go | 17 +- cautils/getter/armoapiutils.go | 6 +- cautils/getter/getpoliciesutils.go | 18 ++ clihandler/cliconfigdelete.go | 7 + clihandler/clidelete.go | 34 +- clihandler/clilist.go | 21 +- clihandler/cliobjects/submit.go | 4 + clihandler/cmd/delete.go | 57 ++++ clihandler/cmd/root.go | 10 +- .../exceptions/exclude-kube-namespaces.json | 11 - resultshandling/printer/v2/junit.go | 301 +++++++++++++----- resultshandling/printer/v2/resourcetable.go | 31 +- 13 files changed, 398 insertions(+), 122 deletions(-) create mode 100644 clihandler/cliconfigdelete.go create mode 100644 clihandler/cmd/delete.go diff --git a/README.md b/README.md index 4608f65b..ef679e9d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Want to contribute? Want to discuss something? Have an issue? * [Scan Kubescape on an air-gapped environment (offline support)](https://youtu.be/IGXL9s37smM) * [Managing exceptions in the Kubescape SaaS version](https://youtu.be/OzpvxGmCR80) * [Configure and run customized frameworks](https://youtu.be/12Sanq_rEhs) +* Customize controls configurations. [Kubescape CLI](https://youtu.be/955psg6TVu4), [Kubescape SaaS](https://youtu.be/lIMVSVhH33o) ## Install on Windows @@ -109,7 +110,7 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser | `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | | | `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | | `--exceptions` | | Path to an exceptions obj, [examples](https://github.com/armosec/kubescape/tree/master/examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || -| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal | | +| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal. [docs](https://hub.armo.cloud/docs/configuration-parameters) | | | `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` | | `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` | | `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | | diff --git a/cautils/getter/armoapi.go b/cautils/getter/armoapi.go index ddc9708a..e789b165 100644 --- a/cautils/getter/armoapi.go +++ b/cautils/getter/armoapi.go @@ -126,6 +126,13 @@ func (armoAPI *ArmoAPI) Post(fullURL string, headers map[string]string, body []b return HttpPost(armoAPI.httpClient, fullURL, headers, body) } +func (armoAPI *ArmoAPI) Delete(fullURL string, headers map[string]string) (string, error) { + if headers == nil { + headers = make(map[string]string) + } + armoAPI.appendAuthHeaders(headers) + return HttpDelete(armoAPI.httpClient, fullURL, headers) +} func (armoAPI *ArmoAPI) Get(fullURL string, headers map[string]string) (string, error) { if headers == nil { headers = make(map[string]string) @@ -293,7 +300,7 @@ func (armoAPI *ArmoAPI) PostExceptions(exceptions []armotypes.PostureExceptionPo if err != nil { return err } - _, err = armoAPI.Post(armoAPI.postExceptionsURL(), map[string]string{"Content-Type": "application/json"}, ex) + _, err = armoAPI.Post(armoAPI.exceptionsURL(""), map[string]string{"Content-Type": "application/json"}, ex) if err != nil { return err } @@ -301,6 +308,14 @@ func (armoAPI *ArmoAPI) PostExceptions(exceptions []armotypes.PostureExceptionPo return nil } +func (armoAPI *ArmoAPI) DeleteException(exceptionName string) error { + + _, err := armoAPI.Delete(armoAPI.exceptionsURL(exceptionName), nil) + if err != nil { + return err + } + return nil +} func (armoAPI *ArmoAPI) Login() error { if armoAPI.accountID == "" { return fmt.Errorf("failed to login, missing accountID") diff --git a/cautils/getter/armoapiutils.go b/cautils/getter/armoapiutils.go index 0b213b48..ce55e6f5 100644 --- a/cautils/getter/armoapiutils.go +++ b/cautils/getter/armoapiutils.go @@ -56,7 +56,7 @@ func (armoAPI *ArmoAPI) getExceptionsURL(clusterName string) string { return u.String() } -func (armoAPI *ArmoAPI) postExceptionsURL() string { +func (armoAPI *ArmoAPI) exceptionsURL(exceptionsPolicyName string) string { u := url.URL{} u.Scheme = "https" u.Host = armoAPI.apiURL @@ -64,6 +64,10 @@ func (armoAPI *ArmoAPI) postExceptionsURL() string { q := u.Query() q.Add("customerGUID", armoAPI.getCustomerGUIDFallBack()) + if exceptionsPolicyName != "" { // for delete + q.Add("policyName", exceptionsPolicyName) + } + u.RawQuery = q.Encode() return u.String() diff --git a/cautils/getter/getpoliciesutils.go b/cautils/getter/getpoliciesutils.go index d577234a..7d822eb3 100644 --- a/cautils/getter/getpoliciesutils.go +++ b/cautils/getter/getpoliciesutils.go @@ -47,6 +47,24 @@ func JSONDecoder(origin string) *json.Decoder { return dec } +func HttpDelete(httpClient *http.Client, fullURL string, headers map[string]string) (string, error) { + + req, err := http.NewRequest("DELETE", fullURL, nil) + if err != nil { + return "", err + } + setHeaders(req, headers) + + resp, err := httpClient.Do(req) + if err != nil { + return "", err + } + respStr, err := httpRespToString(resp) + if err != nil { + return "", err + } + return respStr, nil +} func HttpGetter(httpClient *http.Client, fullURL string, headers map[string]string) (string, error) { req, err := http.NewRequest("GET", fullURL, nil) diff --git a/clihandler/cliconfigdelete.go b/clihandler/cliconfigdelete.go new file mode 100644 index 00000000..77e9f7ba --- /dev/null +++ b/clihandler/cliconfigdelete.go @@ -0,0 +1,7 @@ +package clihandler + +func CliDelete() error { + + tenant := getTenantConfig("", "", getKubernetesApi()) // change k8sinterface + return tenant.DeleteCachedConfig() +} diff --git a/clihandler/clidelete.go b/clihandler/clidelete.go index 77e9f7ba..531b34d4 100644 --- a/clihandler/clidelete.go +++ b/clihandler/clidelete.go @@ -1,7 +1,35 @@ package clihandler -func CliDelete() error { +import ( + "fmt" - tenant := getTenantConfig("", "", getKubernetesApi()) // change k8sinterface - return tenant.DeleteCachedConfig() + "github.com/armosec/kubescape/cautils/getter" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" +) + +func DeleteExceptions(accountID string, exceptions []string) error { + + // load cached config + getTenantConfig(accountID, "", getKubernetesApi()) + + // login kubescape SaaS + armoAPI := getter.GetArmoAPIConnector() + if err := armoAPI.Login(); err != nil { + return err + } + + for i := range exceptions { + exceptionName := exceptions[i] + if exceptionName == "" { + continue + } + logger.L().Info("Deleting exception", helpers.String("name", exceptionName)) + if err := armoAPI.DeleteException(exceptionName); err != nil { + return fmt.Errorf("failed to delete exception '%s', reason: %s", exceptionName, err.Error()) + } + logger.L().Success("Exception deleted successfully") + } + + return nil } diff --git a/clihandler/clilist.go b/clihandler/clilist.go index aa6920d3..b2c81cee 100644 --- a/clihandler/clilist.go +++ b/clihandler/clilist.go @@ -13,6 +13,7 @@ import ( var listFunc = map[string]func(*cliobjects.ListPolicies) ([]string, error){ "controls": listControls, "frameworks": listFrameworks, + "exceptions": listExceptions, } var listFormatFunc = map[string]func(*cliobjects.ListPolicies, []string){ @@ -60,13 +61,25 @@ func listControls(listPolicies *cliobjects.ListPolicies) ([]string, error) { return g.ListControls(l) } +func listExceptions(listPolicies *cliobjects.ListPolicies) ([]string, error) { + // load tenant config + getTenantConfig(listPolicies.Account, "", getKubernetesApi()) + + var exceptionsNames []string + armoAPI := getExceptionsGetter("") + exceptions, err := armoAPI.GetExceptions("") + if err != nil { + return exceptionsNames, err + } + for i := range exceptions { + exceptionsNames = append(exceptionsNames, exceptions[i].Name) + } + return exceptionsNames, nil +} + func prettyPrintListFormat(listPolicies *cliobjects.ListPolicies, policies []string) { sep := "\n * " - usageCmd := strings.TrimSuffix(listPolicies.Target, "s") fmt.Printf("Supported %s:%s%s\n", listPolicies.Target, sep, strings.Join(policies, sep)) - fmt.Printf("\nUsage:\n") - fmt.Printf("$ kubescape scan %s \"name\"\n", usageCmd) - fmt.Printf("$ kubescape scan %s \"name-0\",\"name-1\"\n\n", usageCmd) } func jsonListFormat(listPolicies *cliobjects.ListPolicies, policies []string) { diff --git a/clihandler/cliobjects/submit.go b/clihandler/cliobjects/submit.go index e250e880..9e8ad02e 100644 --- a/clihandler/cliobjects/submit.go +++ b/clihandler/cliobjects/submit.go @@ -3,3 +3,7 @@ package cliobjects type Submit struct { Account string } + +type Delete struct { + Account string +} diff --git a/clihandler/cmd/delete.go b/clihandler/cmd/delete.go new file mode 100644 index 00000000..92bdedb4 --- /dev/null +++ b/clihandler/cmd/delete.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/clihandler" + "github.com/armosec/kubescape/clihandler/cliobjects" + "github.com/spf13/cobra" +) + +var deleteInfo cliobjects.Delete + +var deleteExceptionsExamples = ` + # Delete single exception + kubescape delete exceptions "exception name" + + # Delete multiple exceptions + kubescape delete exceptions "first exception;second exception;third exception" +` + +var deleteCmd = &cobra.Command{ + Use: "delete ", + Short: "Delete configurations in Kubescape SaaS version", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + }, +} + +var deleteExceptionsCmd = &cobra.Command{ + Use: "exceptions ", + Short: "Delete exceptions from Kubescape SaaS version. Run 'kubescape list exceptions' for all exceptions names", + Example: deleteExceptionsExamples, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("missing exceptions names") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + exceptionsNames := strings.Split(args[0], ";") + if len(exceptionsNames) == 0 { + logger.L().Fatal("missing exceptions names") + } + if err := clihandler.DeleteExceptions(deleteInfo.Account, exceptionsNames); err != nil { + logger.L().Fatal(err.Error()) + } + }, +} + +func init() { + deleteCmd.PersistentFlags().StringVarP(&deleteInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file") + rootCmd.AddCommand(deleteCmd) + + deleteCmd.AddCommand(deleteExceptionsCmd) +} diff --git a/clihandler/cmd/root.go b/clihandler/cmd/root.go index 8c3dff7c..7bc135b7 100644 --- a/clihandler/cmd/root.go +++ b/clihandler/cmd/root.go @@ -13,6 +13,7 @@ import ( ) var armoBEURLs = "" +var armoBEURLsDep = "" var rootInfo cautils.RootInfo const envFlagUsage = "Send report results to specific URL. Format:,,.\n\t\tExample:report.armo.cloud,api.armo.cloud,portal.armo.cloud" @@ -47,8 +48,12 @@ func init() { cobra.OnInitialize(initLogger, initLoggerLevel, initEnvironment, initCacheDir) - rootCmd.PersistentFlags().StringVar(&armoBEURLs, "environment", "", envFlagUsage) + rootCmd.PersistentFlags().StringVar(&armoBEURLsDep, "environment", "", envFlagUsage) + rootCmd.PersistentFlags().StringVar(&armoBEURLs, "env", "", envFlagUsage) + rootCmd.PersistentFlags().MarkDeprecated("environment", "use 'env' instead") rootCmd.PersistentFlags().MarkHidden("environment") + rootCmd.PersistentFlags().MarkHidden("env") + rootCmd.PersistentFlags().StringVarP(&rootInfo.Logger, "logger", "l", helpers.InfoLevel.String(), fmt.Sprintf("Logger level. Supported: %s [$KS_LOGGER]", strings.Join(helpers.SupportedLevels(), "/"))) rootCmd.PersistentFlags().StringVar(&rootInfo.CacheDir, "cache-dir", getter.DefaultLocalStore, "Cache directory [$KS_CACHE_DIR]") } @@ -80,6 +85,9 @@ func initCacheDir() { logger.L().Debug("cache dir updated", helpers.String("path", getter.DefaultLocalStore)) } func initEnvironment() { + if armoBEURLsDep != "" { + armoBEURLs = armoBEURLsDep + } urlSlices := strings.Split(armoBEURLs, ",") if len(urlSlices) != 1 && len(urlSlices) < 3 { logger.L().Fatal("expected at least 3 URLs (report, api, frontend, auth)") diff --git a/examples/exceptions/exclude-kube-namespaces.json b/examples/exceptions/exclude-kube-namespaces.json index a8aecd4a..d2aee52b 100644 --- a/examples/exceptions/exclude-kube-namespaces.json +++ b/examples/exceptions/exclude-kube-namespaces.json @@ -24,17 +24,6 @@ "namespace": "kube-node-lease" } } - ], - "posturePolicies": [ - { - "frameworkName": "NSA" - }, - { - "frameworkName": "MITRE" - }, - { - "frameworkName": "ArmoBest" - } ] } ] \ No newline at end of file diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 987a4f79..7b06e503 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -4,56 +4,85 @@ import ( "encoding/xml" "fmt" "os" + "strings" - "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" - "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" ) +/* +riskScore +status + +*/ type JunitPrinter struct { writer *os.File verbose bool } +// https://llg.cubic.org/docs/junit/ + +type JUnitXML struct { + TestSuites JUnitTestSuites `xml:"testsuites"` +} + +// JUnitTestSuites represents the test summary type JUnitTestSuites struct { - XMLName xml.Name `xml:"testsuite"` - Suites []JUnitTestCase `xml:"testsuites"` - Frameworks []JUnitFrameworks `xml:"framework"` - RiskScore float32 `xml:"riskScore,attr"` // test risk score - Time string `xml:"time,attr"` // scanning time - Controls int `xml:"testcases,attr"` // number of controls + XMLName xml.Name `xml:"testsuites"` + Suites []JUnitTestSuite `xml:"testsuite"` // list of controls + Errors int `xml:"errors,attr"` // total number of tests with error result from all testsuites + Disabled int `xml:"disabled,attr"` // total number of disabled tests from all testsuites + Failures int `xml:"failures,attr"` // total number of failed tests from all testsuites + Tests int `xml:"tests,attr"` // total number of tests from all testsuites. Some software may expect to only see the number of successful tests from all testsuites though + Time string `xml:"time,attr"` // time in seconds to execute all test suites + Name string `xml:"name,attr"` // ? Add framework names ? } -type JUnitFrameworks struct { // Frameworks - Name string `xml:"name,attr"` - RiskScore float32 `xml:"riskscore,attr"` - Status string `xml:"status,attr"` +// JUnitTestSuite represents a single control +type JUnitTestSuite struct { + XMLName xml.Name `xml:"testsuite"` + Name string `xml:"name,attr"` // Full (class) name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents. Required + Disabled int `xml:"disabled,attr"` // The total number of disabled tests in the suite. optional. not supported by maven surefire. + Errors int `xml:"errors,attr"` // The total number of tests in the suite that errored + Failures int `xml:"failures,attr"` // The total number of tests in the suite that failed + Hostname string `xml:"hostname,attr"` // Host on which the tests were executed ? cluster name ? + ID int `xml:"id,attr"` // Starts at 0 for the first testsuite and is incremented by 1 for each following testsuite + Skipped string `xml:"skipped,attr"` // The total number of skipped tests + Time string `xml:"time,attr"` // Time taken (in seconds) to execute the tests in the suite + Timestamp string `xml:"timestamp,attr"` // when the test was executed in ISO 8601 format (2014-01-21T16:17:18) + Properties []JUnitProperty `xml:"properties>property,omitempty"` + TestCases []JUnitTestCase `xml:"testcase"` } -// JUnitTestCase is a single test case with its result. -type JUnitTestCase struct { // Control - XMLName xml.Name `xml:"testcase"` - Name string `xml:"name,attr"` - ID string `xml:"id,attr"` - Url string `xml:"url,attr"` - RiskScore float32 `xml:"riskScore,attr"` - Status string `xml:"status,attr"` - Info string `xml:"info,attr"` - AllResources int `xml:"allResources,attr"` - Excluded int `xml:"excludedResources,attr"` - Failed int `xml:"filedResources,attr"` - Resources []JUnitResource `xml:"resource"` +// JUnitTestCase represents a single resource +type JUnitTestCase struct { + XMLName xml.Name `xml:"testcase"` + Classname string `xml:"classname,attr"` // Full class name for the class the test method is in. required + Status string `xml:"status,attr"` // Status + Name string `xml:"name,attr"` // Name of the test method, required + Time string `xml:"time,attr"` // Time taken (in seconds) to execute the test. optional + SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"` + Failure *JUnitFailure `xml:"failure,omitempty"` } -type JUnitResource struct { // Single resource - Name string `xml:"name,attr"` - Namespace string `xml:"namespace,attr"` - Kind string `xml:"kind,attr"` - ApiVersion string `xml:"apiVersion,attr"` - FailedPaths []armotypes.PosturePaths `xml:"jsonpath"` +// JUnitSkipMessage contains the reason why a testcase was skipped. +type JUnitSkipMessage struct { + Message string `xml:"message,attr"` +} + +// JUnitProperty represents a key/value pair used to define properties. +type JUnitProperty struct { + Name string `xml:"name,attr"` + Value string `xml:"value,attr"` +} + +// JUnitFailure contains data related to a failed test. +type JUnitFailure struct { + Message string `xml:"message,attr"` + Type string `xml:"type,attr"` + Contents string `xml:",chardata"` } func NewJunitPrinter(verbose bool) *JunitPrinter { @@ -87,70 +116,166 @@ func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionO } func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { - juResult := JUnitTestSuites{ + + // // Frameworks + // for _, frameworksReports := range results.Report.ListFrameworks().All() { + // fw := JUnitFrameworks{} + // fw.Name = frameworksReports.GetName() + // fw.RiskScore = frameworksReports.GetScore() + // fw.Status = string(frameworksReports.GetStatus().Status()) + // juResult.Frameworks = append(juResult.Frameworks, fw) + // } + testSuites := JUnitTestSuites{ XMLName: xml.Name{ Local: "Kubescape scan results", }, - RiskScore: results.Report.SummaryDetails.Score, - Time: results.Report.GetTimestamp().String(), - Controls: len(results.Report.ListControls().All()), - Frameworks: []JUnitFrameworks{}, } + testSuites.Failures = results.Report.SummaryDetails.NumberOfResources().Failed() + testSuites.Tests = results.Report.SummaryDetails.NumberOfResources().All() + testSuites.Disabled = results.Report.SummaryDetails.NumberOfResources().Skipped() + // summary.errors = + // summary.Name = "?" - // Frameworks - for _, frameworksReports := range results.Report.ListFrameworks().All() { - fw := JUnitFrameworks{} - fw.Name = frameworksReports.GetName() - fw.RiskScore = frameworksReports.GetScore() - fw.Status = string(frameworksReports.GetStatus().Status()) - juResult.Frameworks = append(juResult.Frameworks, fw) - } + // resources + counter := 0 + for resourceID, resourceResult := range results.ResourcesResult { + counter++ - // controls - for _, controlIDs := range results.Report.ListControlsIDs().All() { - controlReport := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controlIDs) - - // control data - testCase := JUnitTestCase{} - testCase.Name = controlReport.GetName() - testCase.ID = controlReport.GetID() - testCase.Url = getControlURL(controlReport.GetID()) - testCase.Name = controlReport.GetName() - testCase.Status = string(controlReport.GetStatus().Status()) - - // resources counters - testCase.AllResources = controlReport.NumberOfResources().All() - testCase.Excluded = controlReport.NumberOfResources().Excluded() - testCase.Failed = controlReport.NumberOfResources().Failed() - - // resources - var jUnitResources []JUnitResource - for _, resourceID := range controlReport.ListResourcesIDs().All() { - result, ok := results.ResourcesResult[resourceID] - if !ok { - continue - } - if result.GetStatus(nil).IsPassed() && !junitPrinter.verbose { // add passed resources only in verbose mode - continue - } - - jUnitResource := JUnitResource{} - rules := result.ListRulesOfControl(controlReport.GetID(), "") - for _, rule := range rules { - jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) - } - if resource, ok := results.AllResources[resourceID]; ok { - jUnitResource.Name = resource.GetName() - jUnitResource.Namespace = resource.GetNamespace() - jUnitResource.Kind = resource.GetKind() - jUnitResource.ApiVersion = resource.GetApiVersion() - } - - jUnitResources = append(jUnitResources, jUnitResource) + // resource data + testSuite := JUnitTestSuite{ + XMLName: xml.Name{ + Local: resourceID, + }, } - testCase.Resources = jUnitResources - juResult.Suites = append(juResult.Suites, testCase) + testSuite.Name = resourceID + testSuite.Disabled = 0 + testSuite.Errors = 0 + testSuite.Failures = len(resourceResult.ListControlsIDs(nil).Failed()) + testSuite.Hostname = "" + testSuite.ID = counter + // testSuite.Skipped = "" + testSuite.Time = "" + testSuite.Timestamp = results.PostureReport.ReportGenerationTime.String() + + testSuite.Properties = []JUnitProperty{ + { + Name: "ID", + Value: resourceID, + }, + } + + // controls + for _, control := range resourceResult.ListControls() { + testCase := JUnitTestCase{ + XMLName: xml.Name{ + Local: control.GetName(), + Space: getControlURL(control.GetID()), + }, + } + testCase.Name = control.GetName() + testCase.Classname = control.GetID() + testCase.Status = string(control.GetStatus(nil).Status()) + + if control.GetStatus(nil).IsFailed() { + paths := failedPathsToString(&control) + + testCaseFailure := JUnitFailure{} + testCaseFailure.Contents = fmt.Sprintf("More deatiles: %s", getControlURL(control.GetID())) + testCaseFailure.Message = strings.Join(paths, ";") + testCaseFailure.Type = "" // TODO - suppot add/modify + + testCase.Failure = &testCaseFailure + } + + testSuite.TestCases = append(testSuite.TestCases, testCase) + } + + testSuites.Suites = append(testSuites.Suites, testSuite) } - return &juResult, nil + return &testSuites, nil } + +// func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { + +// // // Frameworks +// // for _, frameworksReports := range results.Report.ListFrameworks().All() { +// // fw := JUnitFrameworks{} +// // fw.Name = frameworksReports.GetName() +// // fw.RiskScore = frameworksReports.GetScore() +// // fw.Status = string(frameworksReports.GetStatus().Status()) +// // juResult.Frameworks = append(juResult.Frameworks, fw) +// // } +// testSuites := JUnitTestSuites{ +// XMLName: xml.Name{ +// Local: "Kubescape scan results", +// }, +// } +// testSuites.Failures = results.Report.SummaryDetails.NumberOfControls().Failed() +// testSuites.Tests = results.Report.SummaryDetails.NumberOfControls().All() +// testSuites.Disabled = results.Report.SummaryDetails.NumberOfControls().Skipped() +// // summary.errors = +// // summary.Name = "?" + +// // controls +// for _, controlIDs := range results.Report.ListControlsIDs().All() { +// controlReport := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controlIDs) + +// // control data +// testSuite := JUnitTestSuite{ +// XMLName: xml.Name{ +// Local: controlReport.GetName(), +// Space: getControlURL(controlReport.GetID()), +// }, +// } +// testSuite.Name = controlReport.GetName() +// testSuite.Disabled = 0 +// testSuite.Errors = 0 +// testSuite.Failures = controlReport.NumberOfResources().Failed() +// testSuite.Hostname = "" +// testSuite.ID = 0 +// // testSuite.Skipped = "" +// testSuite.Time = "" +// testSuite.Timestamp = "" + +// testCase.Classname = controlReport.GetID() +// testCase.Url = getControlURL(controlReport.GetID()) +// testCase.Name = controlReport.GetName() +// testCase.Status = string(controlReport.GetStatus().Status()) + +// // resources counters +// testCase.AllResources = controlReport.NumberOfResources().All() +// testCase.Excluded = controlReport.NumberOfResources().Excluded() +// testCase.Failed = controlReport.NumberOfResources().Failed() + +// // resources +// var jUnitResources []JUnitResource +// for _, resourceID := range controlReport.ListResourcesIDs().All() { +// result, ok := results.ResourcesResult[resourceID] +// if !ok { +// continue +// } +// if result.GetStatus(nil).IsPassed() && !junitPrinter.verbose { // add passed resources only in verbose mode +// continue +// } + +// jUnitResource := JUnitResource{} +// rules := result.ListRulesOfControl(controlReport.GetID(), "") +// for _, rule := range rules { +// jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) +// } +// if resource, ok := results.AllResources[resourceID]; ok { +// jUnitResource.Name = resource.GetName() +// jUnitResource.Namespace = resource.GetNamespace() +// jUnitResource.Kind = resource.GetKind() +// jUnitResource.ApiVersion = resource.GetApiVersion() +// } + +// jUnitResources = append(jUnitResources, jUnitResource) +// } +// testCase.Resources = jUnitResources +// juResult.Suites = append(juResult.Suites, testCase) +// } + +// return &juResult, nil +// } diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go index 843963bf..1689ed3d 100644 --- a/resultshandling/printer/v2/resourcetable.go +++ b/resultshandling/printer/v2/resourcetable.go @@ -55,18 +55,8 @@ func generateResourceRows(resource workloadinterface.IMetadata, controls []resou row = append(row, fmt.Sprintf("%s\nhttps://hub.armo.cloud/docs/%s", controls[i].GetName(), strings.ToLower(controls[i].GetID()))) row = append(row, resource.GetNamespace()) - var paths []string - for j := range controls[i].ResourceAssociatedRules { - for k := range controls[i].ResourceAssociatedRules[j].Paths { - if p := controls[i].ResourceAssociatedRules[j].Paths[k].FailedPath; p != "" { - paths = append(paths, p) - } - if p := controls[i].ResourceAssociatedRules[j].Paths[k].FixPath.Path; p != "" { - v := controls[i].ResourceAssociatedRules[j].Paths[k].FixPath.Value - paths = append(paths, fmt.Sprintf("%s=%s", p, v)) - } - } - } + paths := failedPathsToString(&controls[i]) + row = append(row, fmt.Sprintf("%s/%s\n%s", resource.GetKind(), resource.GetName(), strings.Join(paths, ";\n"))) row = append(row, string(controls[i].GetStatus(nil).Status())) rows = append(rows, row) @@ -94,3 +84,20 @@ func (a Matrix) Less(i, j int) bool { } return true } + +func failedPathsToString(control *resourcesresults.ResourceAssociatedControl) []string { + var paths []string + + for j := range control.ResourceAssociatedRules { + for k := range control.ResourceAssociatedRules[j].Paths { + if p := control.ResourceAssociatedRules[j].Paths[k].FailedPath; p != "" { + paths = append(paths, p) + } + if p := control.ResourceAssociatedRules[j].Paths[k].FixPath.Path; p != "" { + v := control.ResourceAssociatedRules[j].Paths[k].FixPath.Value + paths = append(paths, fmt.Sprintf("%s=%s", p, v)) + } + } + } + return paths +} From 98079ec1eca931dc5393595a93c153aba27d4a21 Mon Sep 17 00:00:00 2001 From: Ben Hirschberg Date: Tue, 1 Mar 2022 17:26:26 +0200 Subject: [PATCH 34/44] added updated roadmap --- docs/roadmap.md | 52 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index f5586f23..224e3fd7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,19 +1,49 @@ # Kubescape project roadmap +## Planning principles -## Proposals -* [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) +Kubescape roadmap items are labeled based on where the feature is used and by their maturity. -## Planed features -* Image vulnerablity scanning based controls -* Assited remidiation (telling where/what to fix) -* Git integration for pull requests -* Integration with container registries -* Custom controls and regos -* API server configuration validation +The features serve different stages of the workflow of the users: +* development phase (writing Kubernetes manifests) - example: VS Code extension is used while editing YAMLs +* CI phase (integrating manifests to GIT repo) - example: GitHub action validating HELM charts on PRs +* delivery phase (deploying applications in Kubernetes) - example: running cluster scan after a new deployment +* monitoring phase (scanning application in Kubernetes) - example: Prometheus scraping the cluster security risk + +Items in Kubescape roadmap are split to 3 major groups based on the feature planning maturity: + +* Planning - we have tickets open for these issues with more or less clear vision of design +* Backlog - feature which were discussed at a high level but are not ready for development +* Wishlist - features we are dreaming of ๐Ÿ˜€ and want to push them gradually forward + + +## Planning ๐Ÿ‘ท +* **Integration with image registries**: we want to expand Kubescape to integrate with differnet image registries and read image vulnerability information from there. This will allow Kubescape to give contextual security information about vulnerabilities [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) +* **Kubescape as a microservice**: create a REST API for Kubescape so it can run constantly in a cluster and other components like Prometheus can scrape results +* **Kubescape CLI control over cluster operations**: add functionality to Kubescape CLI to trigger operations in Kubescape cluster components (example: trigger images scans and etc.) +* **Produce md/HTML reports**: create scan reports for different output formats +* **Git integration for pull requests**: create insightful GitHub actions for Kubescape + +## Backlog ๐Ÿ“… +* **JSON path for HELM charts**: today Kubescape can point to issues in the Kubernetes object, we want to develop this feature so Kubescape will be able to point to the misconfigured source file (HELM) +* **Create Kubescape HELM plugin** +* **Kubescape based admission controller**: Implement admission controller API for Kubescape microservice to enable users to use Kubescape rules as policies + +## Wishlist ๐Ÿ’ญ +* **Integrate with other Kubernetes CLI tools** use Kubescape as a YAML validator for `kubectl` and others. +* **Kubernetes audit log integration**: connect Kubescape to audit log stream to enable it to produce more contextual security information based on how the API service is used. +* **TUI for Kubescape**: interactive terminal based user interface which helps to analyze and fix issues +* **Scanning images with GO for vulnerabilities**: Images scanners cannot determine which packages were used to build Go executables and we want to scan them for vulnerabilities +* **Scanning Dockerfile-s for security best practices**: Scan image or Dockerfile to determine whether it is using security best practices (like root containers) +* **Custom controls and rules**: enable users to define their own Rego base rules +* **More CI/CD tool integration**: Jenkins and etc. ๐Ÿ˜€ + + +## Completed features ๐ŸŽ“ * Kubelet configuration validation - -## Completed features +* API server configuration validation +* Image vulnerability scanning based controls +* Assisted remediation (telling where/what to fix) * Integration with Prometheus * Confiugration of controls (customizing rules for a given environment) * Installation in the cluster for continous monitoring From cda9bb0e45de9b8e859762900453ee2564cac212 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 3 Mar 2022 12:34:03 +0200 Subject: [PATCH 35/44] Update junit support --- README.md | 23 -- clihandler/initcliutils.go | 7 +- examples/exceptions/README.md | 1 + resultshandling/printer/printresults.go | 2 +- resultshandling/printer/v2/junit.go | 256 +++++++----------- resultshandling/reporter/v1/mockreporter.go | 30 -- resultshandling/reporter/v2/mockreporter.go | 44 +++ .../reporter/v2/reporteventreceiver.go | 38 --- 8 files changed, 145 insertions(+), 256 deletions(-) delete mode 100644 resultshandling/reporter/v1/mockreporter.go create mode 100644 resultshandling/reporter/v2/mockreporter.go diff --git a/README.md b/README.md index ef679e9d..0d194e32 100644 --- a/README.md +++ b/README.md @@ -96,29 +96,6 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser brew install kubescape ``` -## Flags - -| flag | default | description | options | -|-----------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------| -| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | | -| `--include-namespaces` | Scan all namespaces | Scan specific namespaces | | -| `-s`/`--silent` | Display progress messages | Silent progress messages | | -| `-t`/`--fail-threshold` | `100` (do not fail) | fail command (return exit code 1) if result is above threshold | `0` -> `100` | -| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus`/`pdf` | -| `-o`/`--output` | print to stdout | Save scan result in file | | -| `--use-from` | | Load local framework object from specified path. If not used will download latest || -| `--use-artifacts-from` | | Load artifacts (frameworks, control-config, exceptions) from local directory. If not used will download them | | -| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | -| `--exceptions` | | Path to an exceptions obj, [examples](https://github.com/armosec/kubescape/tree/master/examples/exceptions/README.md). Default will download exceptions from Kubescape SaaS || -| `--controls-config` | | Path to a controls-config obj. If not set will download controls-config from ARMO management portal. [docs](https://hub.armo.cloud/docs/configuration-parameters) | | -| `--submit` | `false` | If set, Kubescape will send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not sent | `true`/`false` | -| `--keep-local` | `false` | Kubescape will not send scan results to Armo management portal. Use this flag if you ran with the `--submit` flag in the past and you do not want to submit your current scan results | `true`/`false` | -| `--account` | | Armo portal account ID. Default will load account ID from configMap or config file | | -| `--kube-context` | current-context | Cluster context to scan | | -| `--verbose` | `false` | Display all of the input resources and not only failed resources | `true`/`false` | -| `--logger` | `info` | Set the logger level | `debug`/`info`/`success`/`warning`/`error`/`fatal` | - - ## Usage & Examples ### Examples diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index a466690d..cbb68eaf 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -12,7 +12,6 @@ import ( "github.com/armosec/kubescape/hostsensorutils" "github.com/armosec/kubescape/resourcehandler" "github.com/armosec/kubescape/resultshandling/reporter" - reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" reporterv2 "github.com/armosec/kubescape/resultshandling/reporter/v2" "github.com/armosec/opa-utils/reporthandling" @@ -51,10 +50,12 @@ func getRBACHandler(tenantConfig cautils.ITenantConfig, k8s *k8sinterface.Kubern func getReporter(tenantConfig cautils.ITenantConfig, submit bool) reporter.IReport { if submit { - // return reporterv1.NewReportEventReceiver(tenantConfig.GetConfigObj()) return reporterv2.NewReportEventReceiver(tenantConfig.GetConfigObj()) } - return reporterv1.NewReportMock() + if tenantConfig.GetAccountID() == "" { + return reporterv2.NewReportMock(reporterv2.NO_SUBMIT_QUERY) + } + return reporterv2.NewReportMock("") } func getResourceHandler(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, hostSensorHandler hostsensorutils.IHostSensor, registryAdaptors *resourcehandler.RegistryAdaptors) resourcehandler.IResourceHandler { diff --git a/examples/exceptions/README.md b/examples/exceptions/README.md index 6cad5627..2edb3d29 100644 --- a/examples/exceptions/README.md +++ b/examples/exceptions/README.md @@ -24,6 +24,7 @@ e.g. When a `kube-system` resource fails and it is ok, simply add the resource t * `controlID` - Control ID can be find [here](https://github.com/armosec/regolibrary/tree/master/controls) (regex supported) * `ruleName` - Rule names can be find [here](https://github.com/armosec/regolibrary/tree/master/rules) (regex supported) +You can find [here](https://github.com/armosec/kubescape/tree/master/examples/exceptions) some examples of exceptions files ## Usage diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go index 30aaf302..5f4b948a 100644 --- a/resultshandling/printer/printresults.go +++ b/resultshandling/printer/printresults.go @@ -30,7 +30,7 @@ func GetWriter(outputFile string) *os.File { f, err := os.OpenFile(outputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { logger.L().Error(fmt.Sprintf("failed to open file for writing, reason: %s", err.Error())) - return os.Stderr + return os.Stdout } return f } diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 7b06e503..4069c597 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -4,12 +4,16 @@ import ( "encoding/xml" "fmt" "os" + "sort" "strings" + "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" + "github.com/armosec/opa-utils/reporthandling/results/v1/reportsummary" + "github.com/armosec/opa-utils/shared" ) /* @@ -104,178 +108,108 @@ func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASession } func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { - junitResult, err := junitPrinter.convertPostureReportToJunitResult(opaSessionObj) - if err != nil { - logger.L().Fatal("failed to build xml result object", helpers.Error(err)) - } + junitResult := testsSuites(opaSessionObj) postureReportStr, err := xml.Marshal(junitResult) if err != nil { logger.L().Fatal("failed to Marshal xml result object", helpers.Error(err)) } + + logOUtputFile(junitPrinter.writer.Name()) + junitPrinter.writer.Write(postureReportStr) } -func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { - - // // Frameworks - // for _, frameworksReports := range results.Report.ListFrameworks().All() { - // fw := JUnitFrameworks{} - // fw.Name = frameworksReports.GetName() - // fw.RiskScore = frameworksReports.GetScore() - // fw.Status = string(frameworksReports.GetStatus().Status()) - // juResult.Frameworks = append(juResult.Frameworks, fw) - // } - testSuites := JUnitTestSuites{ - XMLName: xml.Name{ - Local: "Kubescape scan results", - }, +func testsSuites(results *cautils.OPASessionObj) *JUnitTestSuites { + return &JUnitTestSuites{ + Suites: listTestsSuite(results), + Tests: results.Report.SummaryDetails.NumberOfControls().All(), + Name: "Kubescape Scanning", + Failures: results.Report.SummaryDetails.NumberOfControls().Failed(), } - testSuites.Failures = results.Report.SummaryDetails.NumberOfResources().Failed() - testSuites.Tests = results.Report.SummaryDetails.NumberOfResources().All() - testSuites.Disabled = results.Report.SummaryDetails.NumberOfResources().Skipped() - // summary.errors = - // summary.Name = "?" +} +func listTestsSuite(results *cautils.OPASessionObj) []JUnitTestSuite { + var testSuites []JUnitTestSuite - // resources - counter := 0 - for resourceID, resourceResult := range results.ResourcesResult { - counter++ - - // resource data - testSuite := JUnitTestSuite{ - XMLName: xml.Name{ - Local: resourceID, - }, - } - testSuite.Name = resourceID - testSuite.Disabled = 0 - testSuite.Errors = 0 - testSuite.Failures = len(resourceResult.ListControlsIDs(nil).Failed()) - testSuite.Hostname = "" - testSuite.ID = counter - // testSuite.Skipped = "" - testSuite.Time = "" - testSuite.Timestamp = results.PostureReport.ReportGenerationTime.String() - - testSuite.Properties = []JUnitProperty{ - { - Name: "ID", - Value: resourceID, - }, - } - - // controls - for _, control := range resourceResult.ListControls() { - testCase := JUnitTestCase{ - XMLName: xml.Name{ - Local: control.GetName(), - Space: getControlURL(control.GetID()), - }, - } - testCase.Name = control.GetName() - testCase.Classname = control.GetID() - testCase.Status = string(control.GetStatus(nil).Status()) - - if control.GetStatus(nil).IsFailed() { - paths := failedPathsToString(&control) - - testCaseFailure := JUnitFailure{} - testCaseFailure.Contents = fmt.Sprintf("More deatiles: %s", getControlURL(control.GetID())) - testCaseFailure.Message = strings.Join(paths, ";") - testCaseFailure.Type = "" // TODO - suppot add/modify - - testCase.Failure = &testCaseFailure - } - - testSuite.TestCases = append(testSuite.TestCases, testCase) - } - - testSuites.Suites = append(testSuites.Suites, testSuite) + // control scan + if len(results.Report.SummaryDetails.ListFrameworks().All()) == 0 { + testSuite := JUnitTestSuite{} + testSuite.Failures = results.Report.SummaryDetails.NumberOfControls().Failed() + testSuite.Timestamp = results.Report.ReportGenerationTime.String() + testSuite.ID = 0 + testSuite.Name = "kubescape" + testSuite.Properties = properties(results.Report.SummaryDetails.Score) + testSuite.TestCases = testsCases(results, &results.Report.SummaryDetails.Controls, "Kubescape") + testSuites = append(testSuites, testSuite) + return testSuites } - return &testSuites, nil + for i, f := range results.Report.SummaryDetails.Frameworks { + testSuite := JUnitTestSuite{} + testSuite.Failures = f.NumberOfControls().Failed() + testSuite.Timestamp = results.Report.ReportGenerationTime.String() + testSuite.ID = i + testSuite.Name = f.Name + testSuite.Properties = properties(f.Score) + testSuite.TestCases = testsCases(results, f.ListControls(), f.GetName()) + testSuites = append(testSuites, testSuite) + } + + return testSuites +} +func testsCases(results *cautils.OPASessionObj, controls reportsummary.IControlsSummaries, classname string) []JUnitTestCase { + var testCases []JUnitTestCase + + for _, cID := range controls.ListControlsIDs().All() { + testCase := JUnitTestCase{} + control := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, cID) + + testCase.Name = control.GetName() + testCase.Classname = classname + testCase.Status = string(control.GetStatus().Status()) + + if control.GetStatus().IsFailed() { + resources := map[string]interface{}{} + resourceIDs := control.ListResourcesIDs().Failed() + for j := range resourceIDs { + resource := results.AllResources[resourceIDs[j]] + resources[resourceToString(resource)] = nil + } + resourcesStr := shared.MapStringToSlice(resources) + sort.Strings(resourcesStr) + testCaseFailure := JUnitFailure{} + testCaseFailure.Type = "Control" + // testCaseFailure.Contents = + testCaseFailure.Message = fmt.Sprintf("Remediation: %s\nMore details: %s\n\n%s", control.GetRemediation(), getControlURL(control.GetID()), strings.Join(resourcesStr, "\n")) + + testCase.Failure = &testCaseFailure + } else if control.GetStatus().IsSkipped() { + testCase.SkipMessage = &JUnitSkipMessage{ + Message: "", // TODO - fill after statusInfo is supportred + } + + } + testCases = append(testCases, testCase) + } + return testCases } -// func (junitPrinter *JunitPrinter) convertPostureReportToJunitResult(results *cautils.OPASessionObj) (*JUnitTestSuites, error) { +func resourceToString(resource workloadinterface.IMetadata) string { + sep := "; " + s := "" + s += fmt.Sprintf("apiVersion: %s", resource.GetApiVersion()) + sep + s += fmt.Sprintf("kind: %s", resource.GetKind()) + sep + if resource.GetNamespace() != "" { + s += fmt.Sprintf("namespace: %s", resource.GetNamespace()) + sep + } + s += fmt.Sprintf("name: %s", resource.GetName()) + return s +} -// // // Frameworks -// // for _, frameworksReports := range results.Report.ListFrameworks().All() { -// // fw := JUnitFrameworks{} -// // fw.Name = frameworksReports.GetName() -// // fw.RiskScore = frameworksReports.GetScore() -// // fw.Status = string(frameworksReports.GetStatus().Status()) -// // juResult.Frameworks = append(juResult.Frameworks, fw) -// // } -// testSuites := JUnitTestSuites{ -// XMLName: xml.Name{ -// Local: "Kubescape scan results", -// }, -// } -// testSuites.Failures = results.Report.SummaryDetails.NumberOfControls().Failed() -// testSuites.Tests = results.Report.SummaryDetails.NumberOfControls().All() -// testSuites.Disabled = results.Report.SummaryDetails.NumberOfControls().Skipped() -// // summary.errors = -// // summary.Name = "?" - -// // controls -// for _, controlIDs := range results.Report.ListControlsIDs().All() { -// controlReport := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, controlIDs) - -// // control data -// testSuite := JUnitTestSuite{ -// XMLName: xml.Name{ -// Local: controlReport.GetName(), -// Space: getControlURL(controlReport.GetID()), -// }, -// } -// testSuite.Name = controlReport.GetName() -// testSuite.Disabled = 0 -// testSuite.Errors = 0 -// testSuite.Failures = controlReport.NumberOfResources().Failed() -// testSuite.Hostname = "" -// testSuite.ID = 0 -// // testSuite.Skipped = "" -// testSuite.Time = "" -// testSuite.Timestamp = "" - -// testCase.Classname = controlReport.GetID() -// testCase.Url = getControlURL(controlReport.GetID()) -// testCase.Name = controlReport.GetName() -// testCase.Status = string(controlReport.GetStatus().Status()) - -// // resources counters -// testCase.AllResources = controlReport.NumberOfResources().All() -// testCase.Excluded = controlReport.NumberOfResources().Excluded() -// testCase.Failed = controlReport.NumberOfResources().Failed() - -// // resources -// var jUnitResources []JUnitResource -// for _, resourceID := range controlReport.ListResourcesIDs().All() { -// result, ok := results.ResourcesResult[resourceID] -// if !ok { -// continue -// } -// if result.GetStatus(nil).IsPassed() && !junitPrinter.verbose { // add passed resources only in verbose mode -// continue -// } - -// jUnitResource := JUnitResource{} -// rules := result.ListRulesOfControl(controlReport.GetID(), "") -// for _, rule := range rules { -// jUnitResource.FailedPaths = append(jUnitResource.FailedPaths, rule.Paths...) -// } -// if resource, ok := results.AllResources[resourceID]; ok { -// jUnitResource.Name = resource.GetName() -// jUnitResource.Namespace = resource.GetNamespace() -// jUnitResource.Kind = resource.GetKind() -// jUnitResource.ApiVersion = resource.GetApiVersion() -// } - -// jUnitResources = append(jUnitResources, jUnitResource) -// } -// testCase.Resources = jUnitResources -// juResult.Suites = append(juResult.Suites, testCase) -// } - -// return &juResult, nil -// } +func properties(riskScore float32) []JUnitProperty { + return []JUnitProperty{ + { + Name: "riskScore", + Value: fmt.Sprintf("%.2f", riskScore), + }, + } +} diff --git a/resultshandling/reporter/v1/mockreporter.go b/resultshandling/reporter/v1/mockreporter.go deleted file mode 100644 index 8ffc01fe..00000000 --- a/resultshandling/reporter/v1/mockreporter.go +++ /dev/null @@ -1,30 +0,0 @@ -package v1 - -import ( - "fmt" - "os" - - "github.com/armosec/kubescape/cautils" - "github.com/armosec/kubescape/cautils/getter" -) - -type ReportMock struct { -} - -func NewReportMock() *ReportMock { - return &ReportMock{} -} -func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error { - return nil -} - -func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) { -} - -func (reportMock *ReportMock) SetClusterName(clusterName string) { -} - -func (reportMock *ReportMock) DisplayReportURL() { - message := fmt.Sprintf("\nScan results have not been submitted.\nYou can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more by registering here: https://%s/cli-signup \n", getter.GetArmoAPIConnector().GetFrontendURL()) - cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n%s\n", message)) -} diff --git a/resultshandling/reporter/v2/mockreporter.go b/resultshandling/reporter/v2/mockreporter.go new file mode 100644 index 00000000..7837f3fd --- /dev/null +++ b/resultshandling/reporter/v2/mockreporter.go @@ -0,0 +1,44 @@ +package v2 + +import ( + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/getter" +) + +const NO_SUBMIT_QUERY = "utm_source=GitHub&utm_medium=CLI&utm_campaign=no_submit" + +type ReportMock struct { + query string +} + +func NewReportMock(query string) *ReportMock { + return &ReportMock{ + query: query, + } +} +func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error { + return nil +} + +func (reportMock *ReportMock) SetCustomerGUID(customerGUID string) { +} + +func (reportMock *ReportMock) SetClusterName(clusterName string) { +} + +func (reportMock *ReportMock) DisplayReportURL() { + u := fmt.Sprintf("https://%s/account/login", getter.GetArmoAPIConnector().GetFrontendURL()) + if reportMock.query != "" { + u += fmt.Sprintf("?%s", reportMock.query) + } + sep := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + message := sep + "\n" + message += "Scan results have not been submitted." + "\n" + message += "Sign up for free: " + message += u + "\n" + message += sep + "\n" + cautils.InfoTextDisplay(os.Stderr, fmt.Sprintf("\n%s\n", message)) +} diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index 5d50403e..421cf6a5 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -87,12 +87,6 @@ func (report *ReportEventReceiver) prepareReport(postureReport *reporthandlingv2 if err := report.sendResources(host, postureReport, &reportCounter, false); err != nil { return err } - // reportCounter++ - - // // send results - // if err := report.sendResults(host, postureReport, &reportCounter, true); err != nil { - // return err - // } return nil } @@ -156,38 +150,6 @@ func (report *ReportEventReceiver) sendResources(host string, postureReport *rep return report.sendReport(host, splittedPostureReport, *reportCounter, true) } -// func (report *ReportEventReceiver) sendResults(host string, postureReport *reporthandlingv2.PostureReport, reportCounter *int, isLastReport bool) error { -// splittedPostureReport := setSubReport(postureReport) -// counter := 0 - -// for _, v := range postureReport.Results { -// r, err := json.Marshal(v) -// if err != nil { -// return fmt.Errorf("failed to unmarshal resource '%s', reason: %v", v.GetResourceID(), err) -// } - -// if counter+len(r) >= MAX_REPORT_SIZE && len(splittedPostureReport.Resources) > 0 { - -// // send report -// if err := report.sendReport(host, splittedPostureReport, *reportCounter, false); err != nil { -// return err -// } -// *reportCounter++ - -// // delete results -// splittedPostureReport.Results = []resourcesresults.Result{} - -// // restart counter -// counter = 0 -// } - -// counter += len(r) -// splittedPostureReport.Results = append(splittedPostureReport.Results, v) -// } - -// return report.sendReport(host, splittedPostureReport, *reportCounter, isLastReport) -// } - func (report *ReportEventReceiver) sendReport(host string, postureReport *reporthandlingv2.PostureReport, counter int, isLastReport bool) error { postureReport.PaginationInfo = reporthandlingv2.PaginationMarks{ ReportNumber: counter, From ec30ed8439a52d1002ef36fbb8f11e3c8ce5a20f Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 3 Mar 2022 13:05:36 +0200 Subject: [PATCH 36/44] json v2 support --- README.md | 2 +- cautils/scaninfo.go | 2 +- clihandler/cmd/scan.go | 4 +-- clihandler/initcli.go | 2 +- resultshandling/printer/v2/jsonprinter.go | 36 +++++++++++++++++++++ resultshandling/printer/v2/junit.go | 4 --- resultshandling/printer/v2/prettyprinter.go | 17 +++++----- resultshandling/printer/v2/resourcetable.go | 4 +-- resultshandling/printer/v2/utils.go | 23 ++++++++----- resultshandling/results.go | 11 +++++-- 10 files changed, 73 insertions(+), 32 deletions(-) create mode 100644 resultshandling/printer/v2/jsonprinter.go diff --git a/README.md b/README.md index 0d194e32..07c4d229 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ kubescape scan --verbose #### Output in `json` format ``` -kubescape scan --format json --output results.json +kubescape scan --format json --format-version v2 --output results.json ``` #### Output in `junit xml` format diff --git a/cautils/scaninfo.go b/cautils/scaninfo.go index 7f21ff3d..f8b7e7f7 100644 --- a/cautils/scaninfo.go +++ b/cautils/scaninfo.go @@ -68,7 +68,7 @@ type ScanInfo struct { VerboseMode bool // Display all of the input resources and not only failed resources Format string // Format results (table, json, junit ...) Output string // Store results in an output file, Output file name - OutputVersion string // Output object can be differnet between versions, this is for testing and backward compatibility + FormatVersion string // Output object can be differnet between versions, this is for testing and backward compatibility ExcludedNamespaces string // used for host sensor namespace IncludeNamespaces string // DEPRECATED? InputPatterns []string // Yaml files input patterns diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 1835321d..d2aabd74 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -79,12 +79,12 @@ func init() { scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") - scanCmd.PersistentFlags().StringVar(&scanInfo.OutputVersion, "output-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") + scanCmd.PersistentFlags().StringVar(&scanInfo.FormatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") // hidden flags scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") // this flag should be used very cautiously. We prefer users will not use it at all unless the DaemoSet can not run pods on the nodes scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support - scanCmd.PersistentFlags().MarkHidden("output-version") // meant for testing different output approaches and not for common use + scanCmd.PersistentFlags().MarkHidden("format-version") // meant for testing different output approaches and not for common use hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") hostF.NoOptDefVal = "true" diff --git a/clihandler/initcli.go b/clihandler/initcli.go index 0a96a51f..def16384 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -83,7 +83,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { reportHandler := getReporter(tenantConfig, scanInfo.Submit) // setup printer - printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.OutputVersion, scanInfo.VerboseMode) + printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.FormatVersion, scanInfo.VerboseMode) printerHandler.SetWriter(scanInfo.Output) // ================== return interface ====================================== diff --git a/resultshandling/printer/v2/jsonprinter.go b/resultshandling/printer/v2/jsonprinter.go new file mode 100644 index 00000000..d9b1235c --- /dev/null +++ b/resultshandling/printer/v2/jsonprinter.go @@ -0,0 +1,36 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/resultshandling/printer" +) + +type JsonPrinter struct { + writer *os.File +} + +func NewJsonPrinter() *JsonPrinter { + return &JsonPrinter{} +} + +func (jsonPrinter *JsonPrinter) SetWriter(outputFile string) { + jsonPrinter.writer = printer.GetWriter(outputFile) +} + +func (jsonPrinter *JsonPrinter) Score(score float32) { + fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) +} + +func (jsonPrinter *JsonPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + finalizeJson(opaSessionObj) + r, err := json.Marshal(opaSessionObj.Report) + if err != nil { + logger.L().Fatal("failed to Marshal posture report object") + } + jsonPrinter.writer.Write(r) +} diff --git a/resultshandling/printer/v2/junit.go b/resultshandling/printer/v2/junit.go index 4069c597..503b7eaf 100644 --- a/resultshandling/printer/v2/junit.go +++ b/resultshandling/printer/v2/junit.go @@ -103,10 +103,6 @@ func (junitPrinter *JunitPrinter) Score(score float32) { fmt.Fprintf(os.Stderr, "\nOverall risk-score (0- Excellent, 100- All failed): %d\n", int(score)) } -func (junitPrinter *JunitPrinter) FinalizeData(opaSessionObj *cautils.OPASessionObj) { - finalizeReport(opaSessionObj) -} - func (junitPrinter *JunitPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { junitResult := testsSuites(opaSessionObj) postureReportStr, err := xml.Marshal(junitResult) diff --git a/resultshandling/printer/v2/prettyprinter.go b/resultshandling/printer/v2/prettyprinter.go index 75e7053b..314149f5 100644 --- a/resultshandling/printer/v2/prettyprinter.go +++ b/resultshandling/printer/v2/prettyprinter.go @@ -17,24 +17,27 @@ import ( ) type PrettyPrinter struct { - outputVersion string + formatVersion string writer *os.File verboseMode bool sortedControlNames []string } -func NewPrettyPrinter(verboseMode bool, outputVersion string) *PrettyPrinter { +func NewPrettyPrinter(verboseMode bool, formatVersion string) *PrettyPrinter { return &PrettyPrinter{ verboseMode: verboseMode, - outputVersion: outputVersion, + formatVersion: formatVersion, } } func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All()) - prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) - prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) + if prettyPrinter.formatVersion == "v1" { + prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) + } else if prettyPrinter.formatVersion == "v2" { + prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) + } prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails) } @@ -47,10 +50,6 @@ func (prettyPrinter *PrettyPrinter) Score(score float32) { } func (prettyPrinter *PrettyPrinter) printResults(controls *reportsummary.ControlSummaries, allResources map[string]workloadinterface.IMetadata) { - if prettyPrinter.outputVersion != "v1" { - return - } - for i := 0; i < len(prettyPrinter.sortedControlNames); i++ { controlSummary := controls.GetControl(reportsummary.EControlCriteriaName, prettyPrinter.sortedControlNames[i]) // summaryDetails.Controls ListControls().All() Controls.GetControl(ca) diff --git a/resultshandling/printer/v2/resourcetable.go b/resultshandling/printer/v2/resourcetable.go index 1689ed3d..b1555fc1 100644 --- a/resultshandling/printer/v2/resourcetable.go +++ b/resultshandling/printer/v2/resourcetable.go @@ -11,9 +11,7 @@ import ( ) func (prettyPrinter *PrettyPrinter) resourceTable(results map[string]resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { - if prettyPrinter.outputVersion != "v2" { - return - } + summaryTable := tablewriter.NewWriter(prettyPrinter.writer) summaryTable.SetAutoWrapText(true) summaryTable.SetAutoMergeCells(true) diff --git a/resultshandling/printer/v2/utils.go b/resultshandling/printer/v2/utils.go index c432bcac..31c4372b 100644 --- a/resultshandling/printer/v2/utils.go +++ b/resultshandling/printer/v2/utils.go @@ -3,22 +3,22 @@ package v2 import ( "github.com/armosec/k8s-interface/workloadinterface" "github.com/armosec/kubescape/cautils" + "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/opa-utils/reporthandling" "github.com/armosec/opa-utils/reporthandling/results/v1/resourcesresults" ) // finalizeV2Report finalize the results objects by copying data from map to lists -func finalizeReport(opaSessionObj *cautils.OPASessionObj) { +func finalizeJson(opaSessionObj *cautils.OPASessionObj) { if len(opaSessionObj.Report.Results) == 0 { opaSessionObj.Report.Results = make([]resourcesresults.Result, len(opaSessionObj.ResourcesResult)) finalizeResults(opaSessionObj.Report.Results, opaSessionObj.ResourcesResult) - opaSessionObj.ResourcesResult = nil } if len(opaSessionObj.Report.Resources) == 0 { opaSessionObj.Report.Resources = make([]reporthandling.Resource, len(opaSessionObj.AllResources)) - finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.AllResources) - opaSessionObj.AllResources = nil + finalizeResources(opaSessionObj.Report.Resources, opaSessionObj.Report.Results, opaSessionObj.AllResources) } } @@ -30,15 +30,22 @@ func finalizeResults(results []resourcesresults.Result, resourcesResult map[stri } } -func finalizeResources(resources []reporthandling.Resource, allResources map[string]workloadinterface.IMetadata) { +func finalizeResources(resources []reporthandling.Resource, results []resourcesresults.Result, allResources map[string]workloadinterface.IMetadata) { index := 0 - for resourceID := range allResources { - if obj, ok := allResources[resourceID]; ok { + for i := range results { + if obj, ok := allResources[results[i].ResourceID]; ok { r := *reporthandling.NewResource(obj.GetObject()) - r.ResourceID = resourceID + r.ResourceID = results[i].ResourceID resources[index] = r } index++ } } + +func logOUtputFile(fileName string) { + if fileName != "/dev/stdout" && fileName != "/dev/stderr" { + logger.L().Success("Scan results saved", helpers.String("filename", fileName)) + } + +} diff --git a/resultshandling/results.go b/resultshandling/results.go index e5ec8ec5..b74f04d9 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -53,11 +53,16 @@ func CalculatePostureScore(postureReport *reporthandling.PostureReport) float32 return (float32(len(allResources)) - float32(len(failedResources))) / float32(len(allResources)) } -func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPrinter { +func NewPrinter(printFormat, formatVersion string, verboseMode bool) printer.IPrinter { switch printFormat { case printer.JsonFormat: - return printerv1.NewJsonPrinter() + switch formatVersion { + case "v2": + return printerv2.NewJsonPrinter() + default: + return printerv1.NewJsonPrinter() + } case printer.JunitResultFormat: return printerv2.NewJunitPrinter(verboseMode) case printer.PrometheusFormat: @@ -65,6 +70,6 @@ func NewPrinter(printFormat, outputVersion string, verboseMode bool) printer.IPr case printer.PdfFormat: return printerv2.NewPdfPrinter() default: - return printerv2.NewPrettyPrinter(verboseMode, outputVersion) + return printerv2.NewPrettyPrinter(verboseMode, formatVersion) } } From 0b01eb5ee4bd014a2861f81eea1245110e01c9b5 Mon Sep 17 00:00:00 2001 From: dwertent Date: Thu, 3 Mar 2022 14:26:34 +0200 Subject: [PATCH 37/44] swapped print versions --- resultshandling/printer/v2/prettyprinter.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resultshandling/printer/v2/prettyprinter.go b/resultshandling/printer/v2/prettyprinter.go index 314149f5..c3d2aed5 100644 --- a/resultshandling/printer/v2/prettyprinter.go +++ b/resultshandling/printer/v2/prettyprinter.go @@ -34,9 +34,9 @@ func (prettyPrinter *PrettyPrinter) ActionPrint(opaSessionObj *cautils.OPASessio prettyPrinter.sortedControlNames = getSortedControlsNames(opaSessionObj.Report.SummaryDetails.Controls) // ListControls().All()) if prettyPrinter.formatVersion == "v1" { - prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) - } else if prettyPrinter.formatVersion == "v2" { prettyPrinter.printResults(&opaSessionObj.Report.SummaryDetails.Controls, opaSessionObj.AllResources) + } else if prettyPrinter.formatVersion == "v2" { + prettyPrinter.resourceTable(opaSessionObj.ResourcesResult, opaSessionObj.AllResources) } prettyPrinter.printSummaryTable(&opaSessionObj.Report.SummaryDetails) From 9ae21b064a9754ffc7d2b23cfed9a8566720954e Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 09:32:26 +0200 Subject: [PATCH 38/44] update readme --- README.md | 100 +++----------------- build/README.md | 13 +++ clihandler/initcli.go | 2 +- clihandler/initcliutils.go | 18 +++- resultshandling/reporter/v2/mockreporter.go | 10 +- 5 files changed, 47 insertions(+), 96 deletions(-) create mode 100644 build/README.md diff --git a/README.md b/README.md index 07c4d229..09404b16 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,11 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser #### Scan a running Kubernetes cluster and submit results to the [Kubescape SaaS version](https://portal.armo.cloud/) ``` -kubescape scan --submit +kubescape scan --submit --enable-host-scan ``` +> Read [here](https://hub.armo.cloud/docs/host-sensor) more about the `enable-host-scan` flag + #### Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework and submit results to the [Kubescape SaaS version](https://portal.armo.cloud/) ``` kubescape scan framework nsa --submit @@ -149,8 +151,11 @@ kubescape scan --verbose ``` #### Output in `json` format + +> Add the `--format-version v2` flag + ``` -kubescape scan --format json --format-version v2 --output results.json +kubescape scan --format json --format-version v2 --output results.json ``` #### Output in `junit xml` format @@ -233,106 +238,29 @@ Official Docker image `quay.io/armosec/kubescape` docker run -v "$(pwd)/example.yaml:/app/example.yaml quay.io/armosec/kubescape scan /app/example.yaml ``` +If you wish, you can [build the docker image on your own](build/README.md) + # Submit data manually Use the `submit` command if you wish to submit data manually ## Submit scan results manually -First, scan your cluster using the `json` format flag: `kubescape scan framework --format json --output path/to/results.json`. +> Support forward compatibility by using the `--format-version v2` flag -Now you can submit the results to the Kubaescape SaaS version - +First, scan your cluster using the `json` format flag: `kubescape scan framework --format json --format-version v2 --output path/to/results.json`. + +Now you can submit the results to the Kubescape SaaS version - ``` kubescape submit results path/to/results.json ``` -# How to build - -## Build using python (3.7^) script - -Kubescape can be built using: - -``` sh -python build.py -``` - -Note: In order to built using the above script, one must set the environment -variables in this script: - -+ RELEASE -+ ArmoBEServer -+ ArmoERServer -+ ArmoWebsite -+ ArmoAuthServer - - -## Build using go - -Note: development (and the release process) is done with Go `1.17` - -1. Clone Project -``` -git clone https://github.com/armosec/kubescape.git kubescape && cd "$_" -``` - -2. Build -``` -go build -o kubescape . -``` - -3. Run -``` -./kubescape scan --submit --enable-host-scan -``` - -4. Enjoy :zany_face: - -## Docker Build - -### Build your own Docker image - -1. Clone Project -``` -git clone https://github.com/armosec/kubescape.git kubescape && cd "$_" -``` - -2. Build -``` -docker build -t kubescape -f build/Dockerfile . -``` - # Under the hood -## Tests -Kubescape is running the following tests according to what is defined by [Kubernetes Hardening Guidance by NSA and CISA](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) -* Non-root containers -* Immutable container filesystem -* Privileged containers -* hostPID, hostIPC privileges -* hostNetwork access -* allowedHostPaths field -* Protecting pod service account tokens -* Resource policies -* Control plane hardening -* Exposed dashboard -* Allow privilege escalation -* Applications credentials in configuration files -* Cluster-admin binding -* Exec into container -* Dangerous capabilities -* Insecure capabilities -* Linux hardening -* Ingress and Egress blocked -* Container hostPort -* Network policies -* Symlink Exchange Can Allow Host Filesystem Access (CVE-2021-25741) - - - ## Technology Kubescape based on OPA engine: https://github.com/open-policy-agent/opa and ARMO's posture controls. -The tools retrieves Kubernetes objects from the API server and runs a set of [regos snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/). +The tools retrieves Kubernetes objects from the API server and runs a set of [rego's snippets](https://www.openpolicyagent.org/docs/latest/policy-language/) developed by [ARMO](https://www.armosec.io/). The results by default printed in a pretty "console friendly" manner, but they can be retrieved in JSON format for further processing. diff --git a/build/README.md b/build/README.md new file mode 100644 index 00000000..581416f2 --- /dev/null +++ b/build/README.md @@ -0,0 +1,13 @@ +## Docker Build + +### Build your own Docker image + +1. Clone Project +``` +git clone https://github.com/armosec/kubescape.git kubescape && cd "$_" +``` + +2. Build +``` +docker build -t kubescape -f build/Dockerfile . +``` \ No newline at end of file diff --git a/clihandler/initcli.go b/clihandler/initcli.go index f7cfac5f..c3d400b8 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -80,7 +80,7 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { // ================== setup reporter & printer objects ====================================== // reporting behavior - setup reporter - reportHandler := getReporter(tenantConfig, scanInfo.Submit) + reportHandler := getReporter(tenantConfig, scanInfo.Submit, scanInfo.FrameworkScan, len(scanInfo.InputPatterns) == 0) // setup printer printerHandler := resultshandling.NewPrinter(scanInfo.Format, scanInfo.FormatVersion, scanInfo.VerboseMode) diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index cbb68eaf..a3b3217a 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -48,14 +48,22 @@ func getRBACHandler(tenantConfig cautils.ITenantConfig, k8s *k8sinterface.Kubern return nil } -func getReporter(tenantConfig cautils.ITenantConfig, submit bool) reporter.IReport { - if submit { +func getReporter(tenantConfig cautils.ITenantConfig, submit, fwScan, clusterScan bool) reporter.IReport { + if submit && clusterScan { return reporterv2.NewReportEventReceiver(tenantConfig.GetConfigObj()) } - if tenantConfig.GetAccountID() == "" { - return reporterv2.NewReportMock(reporterv2.NO_SUBMIT_QUERY) + if tenantConfig.GetAccountID() == "" && fwScan && clusterScan { + // Add link only when scanning a cluster using a framework + return reporterv2.NewReportMock(reporterv2.NO_SUBMIT_QUERY, "run kubescape with the '--submit' flag") } - return reporterv2.NewReportMock("") + var message string + if !fwScan { + message = "Kubescape does not submit scan results when scanning controls" + } + if !clusterScan { + message = "Kubescape will submit scan results only when scanning a cluster (not YAML files)" + } + return reporterv2.NewReportMock("", message) } func getResourceHandler(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantConfig, k8s *k8sinterface.KubernetesApi, hostSensorHandler hostsensorutils.IHostSensor, registryAdaptors *resourcehandler.RegistryAdaptors) resourcehandler.IResourceHandler { diff --git a/resultshandling/reporter/v2/mockreporter.go b/resultshandling/reporter/v2/mockreporter.go index 7837f3fd..16e9ea8f 100644 --- a/resultshandling/reporter/v2/mockreporter.go +++ b/resultshandling/reporter/v2/mockreporter.go @@ -11,12 +11,14 @@ import ( const NO_SUBMIT_QUERY = "utm_source=GitHub&utm_medium=CLI&utm_campaign=no_submit" type ReportMock struct { - query string + query string + message string } -func NewReportMock(query string) *ReportMock { +func NewReportMock(query, message string) *ReportMock { return &ReportMock{ - query: query, + query: query, + message: message, } } func (reportMock *ReportMock) ActionSendReport(opaSessionObj *cautils.OPASessionObj) error { @@ -36,7 +38,7 @@ func (reportMock *ReportMock) DisplayReportURL() { } sep := "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" message := sep + "\n" - message += "Scan results have not been submitted." + "\n" + message += "Scan results have not been submitted: " + reportMock.message + "\n" message += "Sign up for free: " message += u + "\n" message += sep + "\n" From 4c0e358afc5dafa351cbbe7cf7927f49d54b03ec Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 09:51:05 +0200 Subject: [PATCH 39/44] support submitting v2 --- clihandler/cmd/results.go | 16 ++++++++++++++-- clihandler/cmd/scan.go | 5 ++--- resultshandling/results.go | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/clihandler/cmd/results.go b/clihandler/cmd/results.go index 73b090ac..ed3d1d02 100644 --- a/clihandler/cmd/results.go +++ b/clihandler/cmd/results.go @@ -11,12 +11,16 @@ import ( "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/clihandler" "github.com/armosec/kubescape/clihandler/cliinterfaces" + "github.com/armosec/kubescape/resultshandling/reporter" reporterv1 "github.com/armosec/kubescape/resultshandling/reporter/v1" + reporterv2 "github.com/armosec/kubescape/resultshandling/reporter/v2" "github.com/armosec/opa-utils/reporthandling" "github.com/google/uuid" "github.com/spf13/cobra" ) +var formatVersion string + type ResultsObject struct { filePath string customerGUID string @@ -51,7 +55,7 @@ func (resultsObject *ResultsObject) ListAllResources() (map[string]workloadinter } var resultsCmd = &cobra.Command{ - Use: "results \nExample:\n$ kubescape submit results path/to/results.json", + Use: "results \nExample:\n$ kubescape submit results path/to/results.json --format-version v2", Short: "Submit a pre scanned results file. The file must be in json format", Long: ``, RunE: func(cmd *cobra.Command, args []string) error { @@ -70,7 +74,14 @@ var resultsCmd = &cobra.Command{ resultsObjects := NewResultsObject(clusterConfig.GetAccountID(), clusterConfig.GetClusterName(), args[0]) // submit resources - r := reporterv1.NewReportEventReceiver(clusterConfig.GetConfigObj()) + var r reporter.IReport + switch formatVersion { + case "v2": + r = reporterv2.NewReportEventReceiver(clusterConfig.GetConfigObj()) + default: + logger.L().Warning("Deprecated results version. run with '--format-version' flag", helpers.String("your version", formatVersion), helpers.String("latest version", "v2")) + r = reporterv1.NewReportEventReceiver(clusterConfig.GetConfigObj()) + } submitInterfaces := cliinterfaces.SubmitInterfaces{ ClusterConfig: clusterConfig, @@ -87,6 +98,7 @@ var resultsCmd = &cobra.Command{ func init() { submitCmd.AddCommand(resultsCmd) + resultsCmd.PersistentFlags().StringVar(&formatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for maintaining backward and forward compatibility. Supported:'v1'/'v2'") } func loadResultsFromFile(filePath string) ([]reporthandling.FrameworkReport, error) { diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 6e39856e..5a0f66eb 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -55,7 +55,6 @@ var scanCmd = &cobra.Command{ func frameworkInitConfig() { k8sinterface.SetClusterContextName(scanInfo.KubeContext) } - func init() { cobra.OnInitialize(frameworkInitConfig) @@ -79,12 +78,12 @@ func init() { scanCmd.PersistentFlags().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") - scanCmd.PersistentFlags().StringVar(&scanInfo.FormatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for testing and backward compatibility") + scanCmd.PersistentFlags().StringVar(&scanInfo.FormatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for maintaining backward and forward compatibility. Supported:'v1'/'v2'") // hidden flags scanCmd.PersistentFlags().MarkHidden("host-scan-yaml") // this flag should be used very cautiously. We prefer users will not use it at all unless the DaemoSet can not run pods on the nodes scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support - scanCmd.PersistentFlags().MarkHidden("format-version") // meant for testing different output approaches and not for common use + // scanCmd.PersistentFlags().MarkHidden("format-version") // meant for testing different output approaches and not for common use hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") hostF.NoOptDefVal = "true" diff --git a/resultshandling/results.go b/resultshandling/results.go index b74f04d9..a93dd63d 100644 --- a/resultshandling/results.go +++ b/resultshandling/results.go @@ -3,6 +3,7 @@ package resultshandling import ( "github.com/armosec/kubescape/cautils" "github.com/armosec/kubescape/cautils/logger" + "github.com/armosec/kubescape/cautils/logger/helpers" "github.com/armosec/kubescape/resultshandling/printer" printerv1 "github.com/armosec/kubescape/resultshandling/printer/v1" printerv2 "github.com/armosec/kubescape/resultshandling/printer/v2" @@ -61,6 +62,7 @@ func NewPrinter(printFormat, formatVersion string, verboseMode bool) printer.IPr case "v2": return printerv2.NewJsonPrinter() default: + logger.L().Warning("Deprecated format version. run with '--format-version' flag", helpers.String("your version", formatVersion), helpers.String("latest version", "v2")) return printerv1.NewJsonPrinter() } case printer.JunitResultFormat: From 4e9c6f34b38e3c42f97d31f2c6052369f4b04ce8 Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 10:38:03 +0200 Subject: [PATCH 40/44] Update maintainers and roadmap --- MAINTAINERS.md | 9 +++++---- README.md | 4 ++-- docs/roadmap.md | 30 +++++++++++++++--------------- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index ac29e207..c9bd8989 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,8 +2,9 @@ The following table lists Kubescape project maintainers -| Name | GitHub | Email | Organization | Repositories/Area of Expertise | Added/Renewed On | +| Name | GitHub | Email | Organization | Roll | Added/Renewed On | | --- | --- | --- | --- | --- | --- | -| Ben Hirschberg | @slashben | ben@armosec.io | ARMO | Kubescape CLI | 2021-09-01 | -| Rotem Refael | @rotemamsa | rrefael@armosec.io | ARMO | Kubescape CLI | 2021-10-11 | -| David Wertenteil | @dwertent | dwertent@armosec.io | ARMO | Kubescape CLI | 2021-09-01 | +| [Ben Hirschberg](https://www.linkedin.com/in/benyamin-ben-hirschberg-66141890) | [@slashben](https://github.com/slashben) | ben@armosec.io | [ARMO](https://www.armosec.io/) | VP R&D | 2021-09-01 | +| [Rotem Refael](https://www.linkedin.com/in/rotem-refael) | [@rotemamsa](https://github.com/rotemamsa) | rrefael@armosec.io | [ARMO](https://www.armosec.io/) | Team Leader | 2021-10-11 | +| [David Wertenteil](https://www.linkedin.com/in/david-wertenteil-0ba277b9) | [@dwertent](https://github.com/dwertent) | dwertent@armosec.io | [ARMO](https://www.armosec.io/) | Kubescape CLI Developer | 2021-09-01 | +| [Bezalel Brandwine](https://www.linkedin.com/in/bezalel-brandwine) | [@Bezbran](https://github.com/Bezbran) | bbrandwine@armosec.io | [ARMO](https://www.armosec.io/) | Kubescape SaaS Developer | 2021-09-01 | diff --git a/README.md b/README.md index 09404b16..ca0c88af 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ We invite you to our team! We are excited about this project and want to return Want to contribute? Want to discuss something? Have an issue? +* Feel free to pick a task from the [roadmap](docs/roadmap.md) or suggest a feature of your own. [Contact us](MAINTAINERS.md) directly for more information :) * Open a issue, we are trying to respond within 48 hours -* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! - +* [Join us](https://armosec.github.io/kubescape/) in a discussion on our discord server! [logo](https://armosec.github.io/kubescape/) ![discord](https://img.shields.io/discord/893048809884643379) diff --git a/docs/roadmap.md b/docs/roadmap.md index 224e3fd7..ceee96b5 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -18,25 +18,25 @@ Items in Kubescape roadmap are split to 3 major groups based on the feature plan ## Planning ๐Ÿ‘ท -* **Integration with image registries**: we want to expand Kubescape to integrate with differnet image registries and read image vulnerability information from there. This will allow Kubescape to give contextual security information about vulnerabilities [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) -* **Kubescape as a microservice**: create a REST API for Kubescape so it can run constantly in a cluster and other components like Prometheus can scrape results -* **Kubescape CLI control over cluster operations**: add functionality to Kubescape CLI to trigger operations in Kubescape cluster components (example: trigger images scans and etc.) -* **Produce md/HTML reports**: create scan reports for different output formats -* **Git integration for pull requests**: create insightful GitHub actions for Kubescape +* ###### **Integration with image registries**: we want to expand Kubescape to integrate with differnet image registries and read image vulnerability information from there. This will allow Kubescape to give contextual security information about vulnerabilities [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) +* ###### **Kubescape as a microservice**: create a REST API for Kubescape so it can run constantly in a cluster and other components like Prometheus can scrape results +* ###### **Kubescape CLI control over cluster operations**: add functionality to Kubescape CLI to trigger operations in Kubescape cluster components (example: trigger images scans and etc.) +* ###### **Produce md/HTML reports**: create scan reports for different output formats +* ###### **Git integration for pull requests**: create insightful GitHub actions for Kubescape ## Backlog ๐Ÿ“… -* **JSON path for HELM charts**: today Kubescape can point to issues in the Kubernetes object, we want to develop this feature so Kubescape will be able to point to the misconfigured source file (HELM) -* **Create Kubescape HELM plugin** -* **Kubescape based admission controller**: Implement admission controller API for Kubescape microservice to enable users to use Kubescape rules as policies +* ###### **JSON path for HELM charts**: today Kubescape can point to issues in the Kubernetes object, we want to develop this feature so Kubescape will be able to point to the misconfigured source file (HELM) +* ###### **Create Kubescape HELM plugin** +* ###### **Kubescape based admission controller**: Implement admission controller API for Kubescape microservice to enable users to use Kubescape rules as policies ## Wishlist ๐Ÿ’ญ -* **Integrate with other Kubernetes CLI tools** use Kubescape as a YAML validator for `kubectl` and others. -* **Kubernetes audit log integration**: connect Kubescape to audit log stream to enable it to produce more contextual security information based on how the API service is used. -* **TUI for Kubescape**: interactive terminal based user interface which helps to analyze and fix issues -* **Scanning images with GO for vulnerabilities**: Images scanners cannot determine which packages were used to build Go executables and we want to scan them for vulnerabilities -* **Scanning Dockerfile-s for security best practices**: Scan image or Dockerfile to determine whether it is using security best practices (like root containers) -* **Custom controls and rules**: enable users to define their own Rego base rules -* **More CI/CD tool integration**: Jenkins and etc. ๐Ÿ˜€ +* ###### **Integrate with other Kubernetes CLI tools** use Kubescape as a YAML validator for `kubectl` and others. +* ###### **Kubernetes audit log integration**: connect Kubescape to audit log stream to enable it to produce more contextual security information based on how the API service is used. +* ###### **TUI for Kubescape**: interactive terminal based user interface which helps to analyze and fix issues +* ###### **Scanning images with GO for vulnerabilities**: Images scanners cannot determine which packages were used to build Go executables and we want to scan them for vulnerabilities +* ###### **Scanning Dockerfile-s for security best practices**: Scan image or Dockerfile to determine whether it is using security best practices (like root containers) +* ###### **Custom controls and rules**: enable users to define their own Rego base rules +* ###### **More CI/CD tool integration**: Jenkins and etc. ๐Ÿ˜€ ## Completed features ๐ŸŽ“ From a2e1fb36dfb8c192bbe429f57d11198e40da1e0c Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 10:42:30 +0200 Subject: [PATCH 41/44] update maintainers --- MAINTAINERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index c9bd8989..9b21e6d8 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -2,7 +2,7 @@ The following table lists Kubescape project maintainers -| Name | GitHub | Email | Organization | Roll | Added/Renewed On | +| Name | GitHub | Email | Organization | Role | Added/Renewed On | | --- | --- | --- | --- | --- | --- | | [Ben Hirschberg](https://www.linkedin.com/in/benyamin-ben-hirschberg-66141890) | [@slashben](https://github.com/slashben) | ben@armosec.io | [ARMO](https://www.armosec.io/) | VP R&D | 2021-09-01 | | [Rotem Refael](https://www.linkedin.com/in/rotem-refael) | [@rotemamsa](https://github.com/rotemamsa) | rrefael@armosec.io | [ARMO](https://www.armosec.io/) | Team Leader | 2021-10-11 | From bf215a0f96cdfbdb4ca566ccce9db90488d92bf2 Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 10:50:45 +0200 Subject: [PATCH 42/44] update roadmap --- docs/release.md | 91 --------------------------------------------- docs/roadmap.md | 60 ++++++++++++++++++------------ docs/run-options.md | 87 ------------------------------------------- examples/example.md | 5 --- 4 files changed, 37 insertions(+), 206 deletions(-) delete mode 100644 docs/release.md delete mode 100644 docs/run-options.md delete mode 100644 examples/example.md diff --git a/docs/release.md b/docs/release.md deleted file mode 100644 index ad80d4f6..00000000 --- a/docs/release.md +++ /dev/null @@ -1,91 +0,0 @@ -# Kubescape Release - - -## Input - -### Scan a running Kubernetes cluster - -* Scan your Kubernetes cluster. Ignore `kube-system` and `kube-public` namespaces -``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public -``` - -* Scan your Kubernetes cluster -``` -kubescape scan framework nsa -``` - -### Scan a local Kubernetes manifest - -* Scan single Kubernetes manifest file -``` -kubescape scan framework nsa -``` - -* Scan many Kubernetes manifest files -``` -kubescape scan framework nsa -``` - -* Scan all Kubernetes manifest files in directory -``` -kubescape scan framework nsa *.yaml -``` - -* Scan Kubernetes manifest from stdout -``` -cat | kubescape scan framework nsa - -``` - - -* Scan Kubernetes manifest url -``` -kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml -``` - -### Scan HELM chart - -* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout -``` -helm template [CHART] [flags] --generate-name --dry-run | kubescape scan framework nsa - -``` - -### Scan on-prem (offline) - -* Scan using a framework from the local file system -``` -kubescape scan framework --use-from -``` - -* Scan using the framework from the default location in file system -``` -kubescape scan framework --use-default -``` - -## Output formats - -By default, the output is user friendly. - -For the sake of automation, it is possible to receive the result in a `json` or `junit xml` format. - -* Output in `json` format -``` -kubescape scan framework nsa --format json --output results.json -``` - -* Output in `junit xml` format -``` -kubescape scan framework nsa --format junit --output results.xml -``` - -## Download - -* Download and save in file -``` -kubescape download framework nsa --output nsa.json -``` - -* Download and save in default file (`~/.kubescape/.json`) -``` -kubescape download framework nsa -``` diff --git a/docs/roadmap.md b/docs/roadmap.md index ceee96b5..45b18317 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -5,38 +5,52 @@ Kubescape roadmap items are labeled based on where the feature is used and by their maturity. The features serve different stages of the workflow of the users: -* development phase (writing Kubernetes manifests) - example: VS Code extension is used while editing YAMLs -* CI phase (integrating manifests to GIT repo) - example: GitHub action validating HELM charts on PRs -* delivery phase (deploying applications in Kubernetes) - example: running cluster scan after a new deployment -* monitoring phase (scanning application in Kubernetes) - example: Prometheus scraping the cluster security risk +* **Development phase** (writing Kubernetes manifests) - example: VS Code extension is used while editing YAMLs +* **CI phase** (integrating manifests to GIT repo) - example: GitHub action validating HELM charts on PRs +* **CD phase** (deploying applications in Kubernetes) - example: running cluster scan after a new deployment +* **Monitoring phase** (scanning application in Kubernetes) - example: Prometheus scraping the cluster security risk -Items in Kubescape roadmap are split to 3 major groups based on the feature planning maturity: +The items in Kubescape roadmap are split to 3 major groups based on the feature planning maturity: -* Planning - we have tickets open for these issues with more or less clear vision of design -* Backlog - feature which were discussed at a high level but are not ready for development -* Wishlist - features we are dreaming of ๐Ÿ˜€ and want to push them gradually forward +* [Planning](#planning) - we have tickets open for these issues with more or less clear vision of design +* [Backlog](#backlog) - feature which were discussed at a high level but are not ready for development +* [Wishlist](#wishlist) - features we are dreaming of ๐Ÿ˜€ and want to push them gradually forward ## Planning ๐Ÿ‘ท -* ###### **Integration with image registries**: we want to expand Kubescape to integrate with differnet image registries and read image vulnerability information from there. This will allow Kubescape to give contextual security information about vulnerabilities [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) -* ###### **Kubescape as a microservice**: create a REST API for Kubescape so it can run constantly in a cluster and other components like Prometheus can scrape results -* ###### **Kubescape CLI control over cluster operations**: add functionality to Kubescape CLI to trigger operations in Kubescape cluster components (example: trigger images scans and etc.) -* ###### **Produce md/HTML reports**: create scan reports for different output formats -* ###### **Git integration for pull requests**: create insightful GitHub actions for Kubescape +* ##### Integration with image registries + We want to expand Kubescape to integrate with differnet image registries and read image vulnerability information from there. This will allow Kubescape to give contextual security information about vulnerabilities [Container registry integration](/docs/proposals/container-image-vulnerability-adaptor.md) +* ##### Kubescape as a microservice + Create a REST API for Kubescape so it can run constantly in a cluster and other components like Prometheus can scrape results +* ##### Kubescape CLI control over cluster operations + Add functionality to Kubescape CLI to trigger operations in Kubescape cluster components (example: trigger images scans and etc.) +* ##### Produce md/HTML reports + Create scan reports for different output formats +* ##### Git integration for pull requests + Create insightful GitHub actions for Kubescape ## Backlog ๐Ÿ“… -* ###### **JSON path for HELM charts**: today Kubescape can point to issues in the Kubernetes object, we want to develop this feature so Kubescape will be able to point to the misconfigured source file (HELM) -* ###### **Create Kubescape HELM plugin** -* ###### **Kubescape based admission controller**: Implement admission controller API for Kubescape microservice to enable users to use Kubescape rules as policies +* ##### JSON path for HELM charts + Today Kubescape can point to issues in the Kubernetes object, we want to develop this feature so Kubescape will be able to point to the misconfigured source file (HELM) +* ##### Create Kubescape HELM plugin +* ##### Kubescape based admission controller + Implement admission controller API for Kubescape microservice to enable users to use Kubescape rules as policies ## Wishlist ๐Ÿ’ญ -* ###### **Integrate with other Kubernetes CLI tools** use Kubescape as a YAML validator for `kubectl` and others. -* ###### **Kubernetes audit log integration**: connect Kubescape to audit log stream to enable it to produce more contextual security information based on how the API service is used. -* ###### **TUI for Kubescape**: interactive terminal based user interface which helps to analyze and fix issues -* ###### **Scanning images with GO for vulnerabilities**: Images scanners cannot determine which packages were used to build Go executables and we want to scan them for vulnerabilities -* ###### **Scanning Dockerfile-s for security best practices**: Scan image or Dockerfile to determine whether it is using security best practices (like root containers) -* ###### **Custom controls and rules**: enable users to define their own Rego base rules -* ###### **More CI/CD tool integration**: Jenkins and etc. ๐Ÿ˜€ +* ##### Integrate with other Kubernetes CLI tools + Use Kubescape as a YAML validator for `kubectl` and others. +* ##### Kubernetes audit log integration + Connect Kubescape to audit log stream to enable it to produce more contextual security information based on how the API service is used. +* ##### TUI for Kubescape + Interactive terminal based user interface which helps to analyze and fix issues +* ##### Scanning images with GO for vulnerabilities + Images scanners cannot determine which packages were used to build Go executables and we want to scan them for vulnerabilities +* ##### Scanning Dockerfile-s for security best practices + Scan image or Dockerfile to determine whether it is using security best practices (like root containers) +* ##### Custom controls and rules + Enable users to define their own Rego base rules +* ##### More CI/CD tool integration + Jenkins and etc. ๐Ÿ˜€ ## Completed features ๐ŸŽ“ diff --git a/docs/run-options.md b/docs/run-options.md deleted file mode 100644 index f2e0adf2..00000000 --- a/docs/run-options.md +++ /dev/null @@ -1,87 +0,0 @@ -logo - -# More detailed look on command line arguments and options - -## Simple run: -``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public -``` - -## Flags - -| flag | default | description | options | -| --- | --- | --- | --- | -| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | -| `-s`/`--silent` | Display progress messages | Silent progress messages | -| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result is below threshold| `0` -> `100` | -| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` | -| `-o`/`--output` | print to stdout | Save scan result in file | -| `--use-from` | | Load local framework object from specified path. If not used will download latest | -| `--use-default` | `false` | Load local framework object from default path. If not used will download latest | `true`/`false` | -| `--exceptions` | | Path to an [exceptions obj](examples/exceptions.json). If not set will download exceptions from Armo management portal | -| `--results-locally` | `false` | Kubescape sends scan results to Armo management portal to allow users to control exceptions and maintain chronological scan results. Use this flag if you do not wish to use these features | `true`/`false`| - -## Usage & Examples - -### Examples - -* Scan a running Kubernetes cluster with [`nsa`](https://www.nsa.gov/Press-Room/News-Highlights/Article/Article/2716980/nsa-cisa-release-kubernetes-hardening-guidance/) framework -``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public -``` - -* Scan local `yaml`/`json` files before deploying -``` -kubescape scan framework nsa *.yaml -``` - - -* Scan `yaml`/`json` files from url -``` -kubescape scan framework nsa https://raw.githubusercontent.com/GoogleCloudPlatform/microservices-demo/master/release/kubernetes-manifests.yaml -``` - -* Output in `json` format -``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format json --output results.json -``` - -* Output in `junit xml` format -``` -kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml -``` - -* Scan with exceptions, objects with exceptions will be presented as `warning` and not `fail` -``` -kubescape scan framework nsa --exceptions examples/exceptions.json -``` - -### Helm Support - -* Render the helm chart using [`helm template`](https://helm.sh/docs/helm/helm_template/) and pass to stdout -``` -helm template [NAME] [CHART] [flags] --dry-run | kubescape scan framework nsa - -``` - -for example: -``` -helm template bitnami/mysql --generate-name --dry-run | kubescape scan framework nsa - -``` - -### Offline Support - -It is possible to run Kubescape offline! - -First download the framework and then scan with `--use-from` flag - -* Download and save in file, if file name not specified, will store save to `~/.kubescape/.json` -``` -kubescape download framework nsa --output nsa.json -``` - -* Scan using the downloaded framework -``` -kubescape scan framework nsa --use-from nsa.json -``` - -Kubescape is an open source project, we welcome your feedback and ideas for improvement. Weโ€™re also aiming to collaborate with the Kubernetes community to help make the tests themselves more robust and complete as Kubernetes develops. diff --git a/examples/example.md b/examples/example.md deleted file mode 100644 index 7fdc9084..00000000 --- a/examples/example.md +++ /dev/null @@ -1,5 +0,0 @@ -#! /bin/bash - -echo "Testing Online Boutique yamls (https://github.com/GoogleCloudPlatform/microservices-demo)" - -kubescape scan framework nsa online-boutique/* From a768d22a1d5203f2732fd960d8286a1186387309 Mon Sep 17 00:00:00 2001 From: dwertent Date: Sun, 6 Mar 2022 11:25:16 +0200 Subject: [PATCH 43/44] call setTenant when submitting results --- clihandler/initcli.go | 7 +++++++ clihandler/initcliutils.go | 8 +------- resultshandling/reporter/v2/reporteventreceiver.go | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/clihandler/initcli.go b/clihandler/initcli.go index c3d400b8..a41df98d 100644 --- a/clihandler/initcli.go +++ b/clihandler/initcli.go @@ -49,6 +49,13 @@ func getInterfaces(scanInfo *cautils.ScanInfo) componentInterfaces { // Set submit behavior AFTER loading tenant config setSubmitBehavior(scanInfo, tenantConfig) + if scanInfo.Submit { + // submit - Create tenant & Submit report + if err := tenantConfig.SetTenant(); err != nil { + logger.L().Error(err.Error()) + } + } + // ================== version testing ====================================== v := cautils.NewIVersionCheckHandler() diff --git a/clihandler/initcliutils.go b/clihandler/initcliutils.go index a3b3217a..98514a2d 100644 --- a/clihandler/initcliutils.go +++ b/clihandler/initcliutils.go @@ -148,14 +148,8 @@ func setSubmitBehavior(scanInfo *cautils.ScanInfo, tenantConfig cautils.ITenantC // Submit report scanInfo.Submit = true } - } else { // config not found in cache (not submitted) - if scanInfo.Submit { - // submit - Create tenant & Submit report - if err := tenantConfig.SetTenant(); err != nil { - logger.L().Error(err.Error()) - } - } } + } // setPolicyGetter set the policy getter - local file/github release/ArmoAPI diff --git a/resultshandling/reporter/v2/reporteventreceiver.go b/resultshandling/reporter/v2/reporteventreceiver.go index 421cf6a5..6480212d 100644 --- a/resultshandling/reporter/v2/reporteventreceiver.go +++ b/resultshandling/reporter/v2/reporteventreceiver.go @@ -172,7 +172,7 @@ func (report *ReportEventReceiver) generateMessage() { u.Scheme = "https" u.Host = getter.GetArmoAPIConnector().GetFrontendURL() - if report.customerAdminEMail != "" { // data has been submitted + if report.customerAdminEMail != "" || report.token == "" { // data has been submitted u.Path = fmt.Sprintf("configuration-scanning/%s", report.clusterName) } else { u.Path = "account/sign-up" From 4b07469bb272a7cb7320dc428c2c9ee1c1eb4516 Mon Sep 17 00:00:00 2001 From: dwertent Date: Tue, 8 Mar 2022 10:10:06 +0200 Subject: [PATCH 44/44] use semver.Compare for version check --- cautils/datastructuresmethods.go | 6 ++- cautils/versioncheck.go | 3 +- cautils/versioncheck_test.go | 38 +++++++++++++++++ clihandler/cmd/scan.go | 8 ++-- go.mod | 1 + go.sum | 1 + opaprocessor/processorhandlerutils.go | 23 ----------- opaprocessor/processorhandlerutils_test.go | 48 ---------------------- registryadaptors/README.md | 2 +- 9 files changed, 51 insertions(+), 79 deletions(-) create mode 100644 cautils/versioncheck_test.go diff --git a/cautils/datastructuresmethods.go b/cautils/datastructuresmethods.go index 655445bf..d6b97477 100644 --- a/cautils/datastructuresmethods.go +++ b/cautils/datastructuresmethods.go @@ -2,6 +2,7 @@ package cautils import ( pkgcautils "github.com/armosec/utils-go/utils" + "golang.org/x/mod/semver" "github.com/armosec/opa-utils/reporthandling" ) @@ -50,14 +51,15 @@ func ruleWithArmoOpaDependency(attributes map[string]interface{}) bool { func isRuleKubescapeVersionCompatible(attributes map[string]interface{}, version string) bool { if from, ok := attributes["useFromKubescapeVersion"]; ok && from != nil { if version != "" { - if from.(string) > BuildNumber { + + if semver.Compare(from.(string), BuildNumber) > 0 { return false } } } if until, ok := attributes["useUntilKubescapeVersion"]; ok && until != nil { if version != "" { - if until.(string) <= BuildNumber { + if semver.Compare(BuildNumber, until.(string)) >= 0 { return false } } else { diff --git a/cautils/versioncheck.go b/cautils/versioncheck.go index 83385df1..d5a2af5a 100644 --- a/cautils/versioncheck.go +++ b/cautils/versioncheck.go @@ -10,6 +10,7 @@ import ( "github.com/armosec/kubescape/cautils/logger" "github.com/armosec/kubescape/cautils/logger/helpers" pkgutils "github.com/armosec/utils-go/utils" + "golang.org/x/mod/semver" ) const SKIP_VERSION_CHECK = "KUBESCAPE_SKIP_UPDATE_CHECK" @@ -97,7 +98,7 @@ func (v *VersionCheckHandler) CheckLatestVersion(versionData *VersionCheckReques } if latestVersion.ClientUpdate != "" { - if BuildNumber != "" && BuildNumber < latestVersion.ClientUpdate { + if BuildNumber != "" && semver.Compare(BuildNumber, latestVersion.ClientUpdate) >= 0 { logger.L().Warning(warningMessage(latestVersion.ClientUpdate)) } } diff --git a/cautils/versioncheck_test.go b/cautils/versioncheck_test.go new file mode 100644 index 00000000..bb90dbe8 --- /dev/null +++ b/cautils/versioncheck_test.go @@ -0,0 +1,38 @@ +package cautils + +import ( + "testing" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/armosec/opa-utils/reporthandling" + "github.com/stretchr/testify/assert" +) + +func TestGetKubernetesObjects(t *testing.T) { +} + +var rule_v1_0_131 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ + Attributes: map[string]interface{}{"useUntilKubescapeVersion": "v1.0.132"}}} +var rule_v1_0_132 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ + Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.132", "useUntilKubescapeVersion": "v1.0.133"}}} +var rule_v1_0_133 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ + Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.133", "useUntilKubescapeVersion": "v1.0.134"}}} +var rule_v1_0_134 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ + Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.134"}}} + +func TestIsRuleKubescapeVersionCompatible(t *testing.T) { + // local build- no build number + // should use only rules that don't have "until" + buildNumberMock := "" + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock)) + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock)) + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock)) + assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock)) + + // should only use rules that version is in range of use + buildNumberMock = "v1.0.133" + assert.True(t, isRuleKubescapeVersionCompatible(rule_v1_0_131.Attributes, buildNumberMock)) + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_132.Attributes, buildNumberMock)) + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_133.Attributes, buildNumberMock)) + assert.False(t, isRuleKubescapeVersionCompatible(rule_v1_0_134.Attributes, buildNumberMock)) +} diff --git a/clihandler/cmd/scan.go b/clihandler/cmd/scan.go index 5a0f66eb..defe322b 100644 --- a/clihandler/cmd/scan.go +++ b/clihandler/cmd/scan.go @@ -61,7 +61,7 @@ func init() { rootCmd.AddCommand(scanCmd) - scanCmd.PersistentFlags().StringVarP(&scanInfo.Account, "account", "", "", "Armo portal account ID. Default will load account ID from configMap or config file") + scanCmd.PersistentFlags().StringVarP(&scanInfo.Account, "account", "", "", "ARMO portal account ID. Default will load account ID from configMap or config file") scanCmd.PersistentFlags().StringVarP(&scanInfo.KubeContext, "kube-context", "", "", "Kube context. Default will use the current-context") scanCmd.PersistentFlags().StringVar(&scanInfo.ControlsInputs, "controls-config", "", "Path to an controls-config obj. If not set will download controls-config from ARMO management portal") scanCmd.PersistentFlags().StringVar(&scanInfo.UseExceptions, "exceptions", "", "Path to an exceptions obj. If not set will download exceptions from ARMO management portal") @@ -70,13 +70,13 @@ func init() { scanCmd.PersistentFlags().Float32VarP(&scanInfo.FailThreshold, "fail-threshold", "t", 100, "Failure threshold is the percent above which the command fails and returns exit code 1") scanCmd.PersistentFlags().StringVarP(&scanInfo.Format, "format", "f", "pretty-printer", `Output format. Supported formats: "pretty-printer","json","junit","prometheus","pdf"`) scanCmd.PersistentFlags().StringVar(&scanInfo.IncludeNamespaces, "include-namespaces", "", "scan specific namespaces. e.g: --include-namespaces ns-a,ns-b") - scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to Armo backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") + scanCmd.PersistentFlags().BoolVarP(&scanInfo.Local, "keep-local", "", false, "If you do not want your Kubescape results reported to ARMO backend. Use this flag if you ran with the '--submit' flag in the past and you do not want to submit your current scan results") scanCmd.PersistentFlags().StringVarP(&scanInfo.Output, "output", "o", "", "Output file. Print output to file and not stdout") scanCmd.PersistentFlags().BoolVar(&scanInfo.VerboseMode, "verbose", false, "Display all of the input resources and not only failed resources") scanCmd.PersistentFlags().BoolVar(&scanInfo.UseDefault, "use-default", false, "Load local policy 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().BoolVarP(&scanInfo.Silent, "silent", "s", false, "Silent progress messages") - scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to Armo management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") + scanCmd.PersistentFlags().BoolVarP(&scanInfo.Submit, "submit", "", false, "Send the scan results to ARMO management portal where you can see the results in a user-friendly UI, choose your preferred compliance framework, check risk results history and trends, manage exceptions, get remediation recommendations and much more. By default the results are not submitted") scanCmd.PersistentFlags().StringVar(&scanInfo.HostSensorYamlPath, "host-scan-yaml", "", "Override default host sensor DaemonSet. Use this flag cautiously") scanCmd.PersistentFlags().StringVar(&scanInfo.FormatVersion, "format-version", "v1", "Output object can be differnet between versions, this is for maintaining backward and forward compatibility. Supported:'v1'/'v2'") @@ -85,7 +85,7 @@ func init() { scanCmd.PersistentFlags().MarkHidden("silent") // this flag should be deprecated since we added the --logger support // scanCmd.PersistentFlags().MarkHidden("format-version") // meant for testing different output approaches and not for common use - hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valueable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") + hostF := scanCmd.PersistentFlags().VarPF(&scanInfo.HostSensorEnabled, "enable-host-scan", "", "Deploy ARMO K8s host-sensor daemonset in the scanned cluster. Deleting it right after we collecting the data. Required to collect valuable data from cluster nodes for certain controls. Yaml file: https://raw.githubusercontent.com/armosec/kubescape/master/hostsensorutils/hostsensor.yaml") hostF.NoOptDefVal = "true" hostF.DefValue = "false, for no TTY in stdin" diff --git a/go.mod b/go.mod index 5a61d8ea..fce8b07e 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 go.uber.org/zap v1.19.1 + golang.org/x/mod v0.4.2 gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.22.2 k8s.io/apimachinery v0.22.2 diff --git a/go.sum b/go.sum index 56b612fa..4e9dfab3 100644 --- a/go.sum +++ b/go.sum @@ -800,6 +800,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/opaprocessor/processorhandlerutils.go b/opaprocessor/processorhandlerutils.go index 5c89fad5..ffe1b10b 100644 --- a/opaprocessor/processorhandlerutils.go +++ b/opaprocessor/processorhandlerutils.go @@ -137,29 +137,6 @@ func ruleWithArmoOpaDependency(annotations map[string]interface{}) bool { return false } -// Checks that kubescape version is in range of use for this rule -// In local build (BuildNumber = ""): -// returns true only if rule doesn't have the "until" attribute -func isRuleKubescapeVersionCompatible(rule *reporthandling.PolicyRule) bool { - if from, ok := rule.Attributes["useFromKubescapeVersion"]; ok { - if cautils.BuildNumber != "" { - if from.(string) > cautils.BuildNumber { - return false - } - } - } - if until, ok := rule.Attributes["useUntilKubescapeVersion"]; ok { - if cautils.BuildNumber != "" { - if until.(string) <= cautils.BuildNumber { - return false - } - } else { - return false - } - } - return true -} - func removeData(obj workloadinterface.IMetadata) { if !k8sinterface.IsTypeWorkload(obj.GetObject()) { return // remove data only from kubernetes objects diff --git a/opaprocessor/processorhandlerutils_test.go b/opaprocessor/processorhandlerutils_test.go index 45a48092..3297e720 100644 --- a/opaprocessor/processorhandlerutils_test.go +++ b/opaprocessor/processorhandlerutils_test.go @@ -5,57 +5,9 @@ import ( "github.com/stretchr/testify/assert" - "github.com/armosec/armoapi-go/armotypes" "github.com/armosec/k8s-interface/workloadinterface" - "github.com/armosec/kubescape/cautils" - "github.com/armosec/opa-utils/reporthandling" ) -func TestGetKubernetesObjects(t *testing.T) { -} - -var rule_v1_0_131 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ - Attributes: map[string]interface{}{"useUntilKubescapeVersion": "v1.0.132"}}} -var rule_v1_0_132 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ - Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.132", "useUntilKubescapeVersion": "v1.0.133"}}} -var rule_v1_0_133 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ - Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.133", "useUntilKubescapeVersion": "v1.0.134"}}} -var rule_v1_0_134 = &reporthandling.PolicyRule{PortalBase: armotypes.PortalBase{ - Attributes: map[string]interface{}{"useFromKubescapeVersion": "v1.0.134"}}} - -func TestIsRuleKubescapeVersionCompatible(t *testing.T) { - // local build- no build number - // should use only rules that don't have "until" - cautils.BuildNumber = "" - if isRuleKubescapeVersionCompatible(rule_v1_0_131) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if isRuleKubescapeVersionCompatible(rule_v1_0_132) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if isRuleKubescapeVersionCompatible(rule_v1_0_133) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if !isRuleKubescapeVersionCompatible(rule_v1_0_134) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - - // should only use rules that version is in range of use - cautils.BuildNumber = "v1.0.133" - if isRuleKubescapeVersionCompatible(rule_v1_0_131) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if isRuleKubescapeVersionCompatible(rule_v1_0_132) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if !isRuleKubescapeVersionCompatible(rule_v1_0_133) { - t.Error("error in isRuleKubescapeVersionCompatible") - } - if isRuleKubescapeVersionCompatible(rule_v1_0_134) { - t.Error("error in isRuleKubescapeVersionCompatible") - } -} - func TestRemoveData(t *testing.T) { w := `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"demoservice-server"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"demoservice-server"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app":"demoservice-server"}},"spec":{"containers":[{"env":[{"name":"SERVER_PORT","value":"8089"},{"name":"SLEEP_DURATION","value":"1"},{"name":"DEMO_FOLDERS","value":"/app"},{"name":"ARMO_TEST_NAME","value":"auto_attach_deployment"},{"name":"CAA_ENABLE_CRASH_REPORTER","value":"1"}],"image":"quay.io/armosec/demoservice:v25","imagePullPolicy":"IfNotPresent","name":"demoservice","ports":[{"containerPort":8089,"protocol":"TCP"}],"resources":{},"terminationMessagePath":"/dev/termination-log","terminationMessagePolicy":"File"}],"dnsPolicy":"ClusterFirst","restartPolicy":"Always","schedulerName":"default-scheduler","securityContext":{},"terminationGracePeriodSeconds":30}}}}` diff --git a/registryadaptors/README.md b/registryadaptors/README.md index f0de4892..ea5ea4d3 100644 --- a/registryadaptors/README.md +++ b/registryadaptors/README.md @@ -1,7 +1,7 @@ # Integrate With Vulnerability Server There are some controls that check the relation between the kubernetes manifest and vulnerabilities. -For these controls to work properly, it is necasery to +For these controls to work properly, it is necessary to ## Supported Servers * Armosec