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) {
Getting status...
-
+
`) @@ -195,11 +201,13 @@ function showResources(namespace, chart, revision) { showDescribe(ns, res.kind, res.metadata.name, badge.clone()) }) - const btn2 = $(""); - resBlock.find(".res-actions").append(btn2) - btn2.click(function () { - scanResource(ns, res.kind, res.metadata.name, badge.clone()) - }) + if (scannableResKinds.has(res.kind)) { + const btn2 = $(""); + resBlock.find(".res-actions").append(btn2) + btn2.click(function () { + scanResource(ns, res.kind, res.metadata.name, badge.clone()) + }) + } } }) } @@ -237,25 +245,36 @@ function scanResource(ns, kind, name, badge) { body.append("No information from scanners. Make sure you have installed some and scanned object is supported.") } + const tabs = $('') + const content = $('
') + for (let name in data) { const res = data[name] - if (!res.OrigReport) continue - const hdr = $("

" + name + " Scan Results

"); + if (!res.OrigReport && !res.PassedCount) continue + + const hdr = $(``) if (res.FailedCount) { - hdr.append("" + res.FailedCount + " failed") + hdr.find('button').append("" + res.FailedCount + " failed") } if (res.PassedCount) { - hdr.append("" + res.PassedCount + " passed") + hdr.find('button').append("" + res.PassedCount + " passed") } - body.append(hdr) - const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value const pre = $("
").html(hl)
-            body.append(pre)
+            const div = $('
').append(pre) + + tabs.append(hdr) + content.append(div) } + + body.append(tabs) + body.append(content) + tabs.find('li').first().find('button').click() }) } diff --git a/pkg/dashboard/static/index.html b/pkg/dashboard/static/index.html index 133645c..3cbd699 100644 --- a/pkg/dashboard/static/index.html +++ b/pkg/dashboard/static/index.html @@ -90,14 +90,15 @@ +

REPOSITORY

name-of-repo

URL: http://somerepo/somepath
-
- +
+
@@ -390,7 +391,7 @@
diff --git a/pkg/dashboard/static/repo.js b/pkg/dashboard/static/repo.js index 0968dd3..ea71103 100644 --- a/pkg/dashboard/static/repo.js +++ b/pkg/dashboard/static/repo.js @@ -62,6 +62,19 @@ function loadRepoView() { }) } +$("#inputSearch").keyup(function () { + let val = $(this).val().toLowerCase(); + + $(".charts li").each(function () { + let chartName = $(this.firstElementChild).text().toLowerCase() + if (chartName.indexOf(val) >= 0) { + $(this).show() + } else { + $(this).hide() + } + }) +}) + $("#sectionRepo .repo-list .btn").click(function () { const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {}); myModal.show() diff --git a/pkg/dashboard/static/scripts.js b/pkg/dashboard/static/scripts.js index c4b98a3..0fe4668 100644 --- a/pkg/dashboard/static/scripts.js +++ b/pkg/dashboard/static/scripts.js @@ -17,8 +17,11 @@ $(function () { $.getJSON("/api/scanners").fail(function (xhr) { reportError("Failed to get list of scanners", xhr) }).done(function (data) { - if (!data || !data.length) { - $("#upgradeModal .btn-scan").hide() + $("body").data("scanners", data) + for (let k in data) { + if (data[k].ManifestScannable) { + $("#upgradeModal .btn-scan").show() // TODO: move this to install flow + } } }) diff --git a/pkg/dashboard/static/styles.css b/pkg/dashboard/static/styles.css index 40e57b1..56f1f25 100644 --- a/pkg/dashboard/static/styles.css +++ b/pkg/dashboard/static/styles.css @@ -26,10 +26,6 @@ body > .container-fluid { min-height: 100% !important; } -#topNav.navbar { -} - - .navbar-brand > a > img { vertical-align: middle; height: 3rem; @@ -211,17 +207,17 @@ span.link { position: static; } -.nav-tabs { +nav .nav-tabs { border: none; margin-bottom: 1rem; } -.nav-tabs .nav-link { +nav .nav-tabs .nav-link { padding-bottom: 0.25rem; color: #3B3D45; } -.nav-tabs .nav-link.active { +nav .nav-tabs .nav-link.active { border: none; border-bottom: 3px solid #3B3D45; background-color: transparent; diff --git a/pkg/dashboard/subproc/data.go b/pkg/dashboard/subproc/data.go index fd7a929..e939c3a 100644 --- a/pkg/dashboard/subproc/data.go +++ b/pkg/dashboard/subproc/data.go @@ -163,7 +163,7 @@ func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) { func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) { // TODO: there is `max` but there is no `offset` - out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json", "--max", "18") + out, err := d.runCommandHelm("history", chartName, "--namespace", namespace, "--output", "json") if err != nil { return nil, err } @@ -178,7 +178,7 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His if err != nil { return nil, err } - elm.ChartName = chartRepoName + elm.ChartName = chartRepoName // TODO: move it to frontend? elm.ChartVer = curVer elm.Updated.Time = elm.Updated.Time.Round(time.Second) } diff --git a/pkg/dashboard/subproc/helmTypes.go b/pkg/dashboard/subproc/helmTypes.go index eeebf53..dcbab61 100644 --- a/pkg/dashboard/subproc/helmTypes.go +++ b/pkg/dashboard/subproc/helmTypes.go @@ -24,8 +24,9 @@ type HistoryElement struct { Chart string `json:"chart"` AppVersion string `json:"app_version"` Description string `json:"description"` - ChartName string `json:"chart_name"` - ChartVer string `json:"chart_ver"` + + ChartName string `json:"chart_name"` // custom addition on top of Helm + ChartVer string `json:"chart_ver"` // custom addition on top of Helm } type RepoChartElement struct { diff --git a/pkg/dashboard/subproc/scan.go b/pkg/dashboard/subproc/scan.go index 0de6a08..5db8e44 100644 --- a/pkg/dashboard/subproc/scan.go +++ b/pkg/dashboard/subproc/scan.go @@ -5,6 +5,8 @@ type Scanner interface { Test() bool // test if the scanner is available ScanManifests(mnf string) (*ScanResults, error) // run the scanner on manifests ScanResource(ns string, kind string, name string) (*ScanResults, error) // run the scanner on k8s resource + SupportedResourceKinds() []string + ManifestIsScannable() bool } type ScanResults struct { diff --git a/pkg/dashboard/utils/utils.go b/pkg/dashboard/utils/utils.go index a43b95d..363c9b8 100644 --- a/pkg/dashboard/utils/utils.go +++ b/pkg/dashboard/utils/utils.go @@ -12,6 +12,8 @@ import ( "strings" ) +var FailLogLevel = log.WarnLevel // allows to suppress error logging in some situations + type ControlChan = chan struct{} func ChartAndVersion(x string) (string, string, error) { @@ -64,10 +66,10 @@ func RunCommand(cmd []string, env map[string]string) (string, error) { prog.Stderr = &stderr if err := prog.Run(); err != nil { - log.Warnf("Failed command: %s", cmd) + log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd) serr := stderr.Bytes() if serr != nil { - log.Warnf("STDERR:\n%s", serr) + log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr) } if eerr, ok := err.(*exec.ExitError); ok { return "", CmdError{