From 0c9da9ddc8d45bd74e28cdd6c62a70f9b1515ed2 Mon Sep 17 00:00:00 2001 From: Alan Clucas Date: Tue, 26 Oct 2021 14:56:55 +0100 Subject: [PATCH 1/2] Add a prometheus metrics style output Output per control results and also per object counts This can lead to running this as a service that prometheus can collect from --- resultshandling/printer/printresults.go | 7 +- resultshandling/printer/prometheusprinter.go | 100 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 resultshandling/printer/prometheusprinter.go diff --git a/resultshandling/printer/printresults.go b/resultshandling/printer/printresults.go index 8452c19f..eab0c5d9 100644 --- a/resultshandling/printer/printresults.go +++ b/resultshandling/printer/printresults.go @@ -11,7 +11,8 @@ const EmptyPercentage = "NaN" const ( PrettyFormat string = "pretty-printer" JsonFormat string = "json" - JunitResultPrinter string = "junit" + JunitResultFormat string = "junit" + PrometheusFormat string = "prometheus" ) type IPrinter interface { @@ -24,8 +25,10 @@ func GetPrinter(printFormat string) IPrinter { switch printFormat { case JsonFormat: return NewJsonPrinter() - case JunitResultPrinter: + case JunitResultFormat: return NewJunitPrinter() + case PrometheusFormat: + return NewPrometheusPrinter() default: return NewPrettyPrinter() } diff --git a/resultshandling/printer/prometheusprinter.go b/resultshandling/printer/prometheusprinter.go new file mode 100644 index 00000000..0c5bbaaf --- /dev/null +++ b/resultshandling/printer/prometheusprinter.go @@ -0,0 +1,100 @@ +package printer + +import ( + "errors" + "fmt" + "os" + + "github.com/armosec/kubescape/cautils" + "github.com/armosec/opa-utils/reporthandling" +) + +type PrometheusPrinter struct { + writer *os.File +} + +func NewPrometheusPrinter() *PrometheusPrinter { + return &PrometheusPrinter{} +} + +func (prometheusPrinter *PrometheusPrinter) SetWriter(outputFile string) { + prometheusPrinter.writer = getWriter(outputFile) +} + +func (prometheusPrinter *PrometheusPrinter) Score(score float32) { + fmt.Printf("\n# Overall score out of 100\nkubescape_score %f\n", score*100) +} + +func (printer *PrometheusPrinter) printDetails(details []reporthandling.RuleResponse, frameworkName string, controlName string) error { + objs := make(map[string]map[string]map[string]int) + for _, ruleResponses := range details { + for _, k8sObj := range ruleResponses.AlertObject.K8SApiObjects { + kind, ok := k8sObj[`kind`].(string) + if (!ok) { + return errors.New("Found object with non string kind") + } + apiVersion,ok := k8sObj[`apiVersion`].(string) + if (!ok) { + return errors.New("Found object with non string apiVersion") + } + gvk := fmt.Sprintf("%s/%s",apiVersion,kind) + metadata,ok := k8sObj[`metadata`].(map[string]interface{}) + if (!ok) { + return errors.New("Found object with non convertable metadata") + } + name,ok := metadata[`name`].(string) + if (!ok) { + return errors.New("Found metadata with non string name") + } + namespace,ok := metadata[`namespace`].(string) + if (!ok) { + namespace = "" + } + if (objs[gvk] == nil) { + objs[gvk] = make(map[string]map[string]int) + } + if (objs[gvk][namespace] == nil) { + objs[gvk][namespace] = make(map[string]int) + } + objs[gvk][namespace][name]++ + } + } + for gvk, namespaces := range objs { + for namespace, names := range namespaces { + for name, value := range names { + fmt.Fprintf(printer.writer, "# Failed object from %s control %s\n", frameworkName, controlName) + if namespace != "" { + fmt.Fprintf(printer.writer, "kubescape_object_failed_count{framework=\"%s\",control=\"%s\",namespace=\"%s\",name=\"%s\",groupVersionKind=\"%s\"} %d\n", frameworkName, controlName, namespace, name, gvk, value) + } else { + fmt.Fprintf(printer.writer, "kubescape_object_failed_count{framework=\"%s\",control=\"%s\",name=\"%s\",groupVersionKind=\"%s\"} %d\n", frameworkName, controlName, name, gvk, value) + } + } + } + } + return nil +} + +func (printer *PrometheusPrinter) printReports(frameworks []reporthandling.FrameworkReport) error { + for _, framework := range frameworks { + for _, controlReports := range framework.ControlReports { + if len(controlReports.RuleReports[0].RuleResponses) > 0 { + fmt.Fprintf(printer.writer, "# Number of resources found as part of %s control %s\nkubescape_resources_found_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfResources()) + fmt.Fprintf(printer.writer, "# Number of resources excluded as part of %s control %s\nkubescape_resources_excluded_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfWarningResources()) + fmt.Fprintf(printer.writer, "# Number of resources failed as part of %s control %s\nkubescape_resources_failed_count{framework=\"%s\",control=\"%s\"} %d\n", framework.Name, controlReports.Name, framework.Name, controlReports.Name, controlReports.GetNumberOfFailedResources()) + err := printer.printDetails(controlReports.RuleReports[0].RuleResponses, framework.Name, controlReports.Name) + if err != nil { + return err + } + } + } + } + return nil +} + +func (printer *PrometheusPrinter) ActionPrint(opaSessionObj *cautils.OPASessionObj) { + err := printer.printReports(opaSessionObj.PostureReport.FrameworkReports) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} From 8e67104ba4396ca44c4a6370763cabc5a58c3c56 Mon Sep 17 00:00:00 2001 From: Alan Clucas Date: Tue, 26 Oct 2021 16:23:05 +0100 Subject: [PATCH 2/2] Add prometheus to readme --- README.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6bcc3cb6..eabf319e 100644 --- a/README.md +++ b/README.md @@ -69,19 +69,19 @@ Set-ExecutionPolicy RemoteSigned -scope CurrentUser ## Flags -| flag | default | description | options | -| --- | --- | --- | --- | -| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | -| `-s`/`--silent` | Display progress messages | Silent progress messages | -| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold| `0` -> `100` | -| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit` | -| `-o`/`--output` | print to stdout | Save scan result in file | -| `--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 | -| `--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 | | +| flag | default | description | options | +|-----------------------------|---------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------| +| `-e`/`--exclude-namespaces` | Scan all namespaces | Namespaces to exclude from scanning. Recommended to exclude `kube-system` and `kube-public` namespaces | | +| `-s`/`--silent` | Display progress messages | Silent progress messages | | +| `-t`/`--fail-threshold` | `0` (do not fail) | fail command (return exit code 1) if result bellow threshold | `0` -> `100` | +| `-f`/`--format` | `pretty-printer` | Output format | `pretty-printer`/`json`/`junit`/`prometheus` | +| `-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 | | +| `--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 | | ## Usage & Examples @@ -125,6 +125,11 @@ kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --form kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format junit --output results.xml ``` +* Output in `prometheus` metrics format +``` +kubescape scan framework nsa --exclude-namespaces kube-system,kube-public --format prometheus +``` + * Scan with exceptions, objects with exceptions will be presented as `exclude` and not `fail` ``` kubescape scan framework nsa --exceptions examples/exceptions.json