diff --git a/go.mod b/go.mod index 489b82f..c14d18b 100644 --- a/go.mod +++ b/go.mod @@ -28,8 +28,10 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.0.3 // indirect github.com/ugorji/go/codec v1.2.7 // indirect golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect diff --git a/go.sum b/go.sum index 72bf46a..bf8e012 100644 --- a/go.sum +++ b/go.sum @@ -54,12 +54,16 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.0.3 h1:h9JoA60e1dVEOpp0PFwJSmt1Htu057NUq9/bUwaO61s= github.com/pelletier/go-toml/v2 v2.0.3/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= diff --git a/main.go b/main.go index 4176147..f0ba970 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { address, webServerDone := dashboard.StartServer(version, int(opts.Port), opts.Namespace, opts.Verbose, opts.NoTracking) if !opts.NoTracking { - log.Infof("User analytics collected to improve the quality, disable it with --no-analytics") + log.Infof("User analytics is collected to improve the quality, disable it with --no-analytics") } if opts.NoBrowser { diff --git a/pkg/dashboard/handlers/scannerHandlers.go b/pkg/dashboard/handlers/scannerHandlers.go index e5d8fba..c59654b 100644 --- a/pkg/dashboard/handlers/scannerHandlers.go +++ b/pkg/dashboard/handlers/scannerHandlers.go @@ -12,11 +12,18 @@ type ScannersHandler struct { } func (h *ScannersHandler) List(c *gin.Context) { - res := make([]string, 0) - for _, scanner := range h.Data.Scanners { - res = append(res, scanner.Name()) + type ScannerInfo struct { + SupportedResourceKinds []string + ManifestScannable bool } - c.JSON(http.StatusOK, res) + res := map[string]ScannerInfo{} + for _, scanner := range h.Data.Scanners { + res[scanner.Name()] = ScannerInfo{ + SupportedResourceKinds: scanner.SupportedResourceKinds(), + ManifestScannable: scanner.ManifestIsScannable(), + } + } + c.IndentedJSON(http.StatusOK, res) } func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) { diff --git a/pkg/dashboard/scanners/checkov.go b/pkg/dashboard/scanners/checkov.go index 6791cb7..ab01530 100644 --- a/pkg/dashboard/scanners/checkov.go +++ b/pkg/dashboard/scanners/checkov.go @@ -1,11 +1,12 @@ package scanners import ( + "encoding/json" "github.com/komodorio/helm-dashboard/pkg/dashboard/subproc" "github.com/komodorio/helm-dashboard/pkg/dashboard/utils" + "github.com/olekukonko/tablewriter" log "github.com/sirupsen/logrus" v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" - "strconv" "strings" ) @@ -13,11 +14,46 @@ type Checkov struct { Data *subproc.DataLayer } +func (c *Checkov) ManifestIsScannable() bool { + return true +} + +func (c *Checkov) SupportedResourceKinds() []string { + // from https://github.com/bridgecrewio/checkov//blob/master/docs/5.Policy%20Index/kubernetes.md + return []string{ + "AdmissionConfiguration", + "ClusterRole", + "ClusterRoleBinding", + "ConfigMap", + "CronJob", + "DaemonSet", + "Deployment", + "DeploymentConfig", + "Ingress", + "Job", + "Pod", + "PodSecurityPolicy", + "PodTemplate", + "Policy", + "ReplicaSet", + "ReplicationController", + "Role", + "RoleBinding", + "Secret", + "Service", + "ServiceAccount", + "StatefulSet", + } +} + func (c *Checkov) Name() string { return "Checkov" } func (c *Checkov) Test() bool { + utils.FailLogLevel = log.DebugLevel + defer func() { utils.FailLogLevel = log.WarnLevel }() + res, err := utils.RunCommand([]string{"checkov", "--version"}, nil) if err != nil { return false @@ -33,7 +69,7 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) { } defer fclose() - cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname} + cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname} out, err := utils.RunCommand(cmd, nil) if err != nil { return nil, err @@ -41,7 +77,10 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) { res := &subproc.ScanResults{} - res.OrigReport = out + err = json.Unmarshal([]byte(out), res.OrigReport) + if err != nil { + return nil, err + } return res, nil } @@ -61,41 +100,46 @@ func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.Sc } defer fclose() - cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "cli", "--file", fname} + cmd := []string{"checkov", "--quiet", "--soft-fail", "--framework", "kubernetes", "--output", "json", "--file", fname} out, err := utils.RunCommand(cmd, nil) if err != nil { return nil, err } - res := subproc.ScanResults{} - _, out, _ = strings.Cut(out, "\n") // kubernetes scan results: - _, out, _ = strings.Cut(out, "\n") // empty line - line, out, found := strings.Cut(out, "\n") // status line - if found { - parts := strings.FieldsFunc(line, func(r rune) bool { - return r == ':' || r == ',' - }) - if cnt, err := strconv.Atoi(strings.TrimSpace(parts[1])); err == nil { - res.PassedCount = cnt - } else { - log.Warnf("Failed to parse Checkov output: %s", err) - } - if cnt, err := strconv.Atoi(strings.TrimSpace(parts[3])); err == nil { - res.FailedCount = cnt - } else { - log.Warnf("Failed to parse Checkov output: %s", err) - } - } else { - log.Warnf("Failed to parse Checkov output") + cr := CheckovReport{} + err = json.Unmarshal([]byte(out), &cr) + if err != nil { + return nil, err } - res.OrigReport = strings.TrimSpace(out) + res := &subproc.ScanResults{ + PassedCount: cr.Summary.Passed, + FailedCount: cr.Summary.Failed, + OrigReport: checkovReportTable(&cr), + } - return &res, nil + return res, nil } -type CheckovResults struct { - Summary CheckovSummary +func checkovReportTable(c *CheckovReport) string { + data := [][]string{} + for _, item := range c.Results.FailedChecks { + data = append(data, []string{item.Id, item.Name + "\n", item.Guideline}) + } + + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + table.SetHeader([]string{"ID", "Name", "Guideline"}) + table.SetBorder(false) + table.SetColWidth(64) + table.AppendBulk(data) + table.Render() + return tableString.String() +} + +type CheckovReport struct { + Summary CheckovSummary `json:"summary"` + Results CheckovResults `json:"results"` } type CheckovSummary struct { @@ -105,3 +149,16 @@ type CheckovSummary struct { // parsing errors? // skipped ? } + +type CheckovResults struct { + FailedChecks []CheckovCheck `json:"failed_checks"` +} + +type CheckovCheck struct { + Id string `json:"check_id"` + BcId string `json:"bc_check_id"` + Name string `json:"check_name"` + Resource string `json:"resource"` + Guideline string `json:"guideline"` + FileLineRange []int `json:"file_line_range"` +} diff --git a/pkg/dashboard/scanners/trivy.go b/pkg/dashboard/scanners/trivy.go index e3f174f..ff0a19e 100644 --- a/pkg/dashboard/scanners/trivy.go +++ b/pkg/dashboard/scanners/trivy.go @@ -12,11 +12,31 @@ type Trivy struct { Data *subproc.DataLayer } +func (c *Trivy) ManifestIsScannable() bool { + return false +} + +func (c *Trivy) SupportedResourceKinds() []string { + // from https://github.com/aquasecurity/trivy-kubernetes/blob/main/pkg/k8s/k8s.go#L190 + return []string{ + "ReplicaSet", + "ReplicationController", + "StatefulSet", + "Deployment", + "CronJob", + "DaemonSet", + "Job", + } +} + func (c *Trivy) Name() string { return "Trivy" } func (c *Trivy) Test() bool { + utils.FailLogLevel = log.DebugLevel + defer func() { utils.FailLogLevel = log.WarnLevel }() + res, err := utils.RunCommand([]string{"trivy", "--version"}, nil) if err != nil { return false diff --git a/pkg/dashboard/static/actions.js b/pkg/dashboard/static/actions.js index f6ba7aa..11e1a25 100644 --- a/pkg/dashboard/static/actions.js +++ b/pkg/dashboard/static/actions.js @@ -350,17 +350,3 @@ $("#btnAddRepository").click(function () { setHashParam("section", "repository") window.location.reload() }) - -$("#inputSearch").keyup(function() { - var val = $(this).val().toLowerCase(); - - $(".charts li").hide() - - $(".charts li").each(function(){ - var chartNameElem = this.firstElementChild - var chartName = $(chartNameElem).text().toLowerCase() - if(chartName.indexOf(val) != -1) { - $(this).show() - } - }) -}) \ No newline at end of file diff --git a/pkg/dashboard/static/details-view.js b/pkg/dashboard/static/details-view.js index c206e8e..728d954 100644 --- a/pkg/dashboard/static/details-view.js +++ b/pkg/dashboard/static/details-view.js @@ -150,6 +150,12 @@ function showResources(namespace, chart, revision) { $.getJSON(url).fail(function (xhr) { reportError("Failed to get list of resources", xhr) }).done(function (data) { + const scanners = $("body").data("scanners"); + const scannableResKinds = new Set(); + for (let k in scanners) { + scanners[k].SupportedResourceKinds.forEach(scannableResKinds.add, scannableResKinds) + } + resBody.empty(); for (let i = 0; i < data.length; i++) { const res = data[i] @@ -159,7 +165,7 @@ function showResources(namespace, chart, revision) {