Improve resource scanning flow (#68)

* List supported resources for scanners

* Don't warn on scanner discovery commands

* Use scanner-to-resource map

* Save changes

* Scan result tabs

* Own table render for Checkov

* Scannable manifest flag for scanners
This commit is contained in:
Andrey Pokhilko
2022-11-06 15:56:34 +00:00
committed by GitHub
parent 612352d69f
commit 671fa949df
16 changed files with 191 additions and 78 deletions

2
go.mod
View File

@@ -28,8 +28,10 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.16 // 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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/pelletier/go-toml/v2 v2.0.3 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect golang.org/x/crypto v0.0.0-20220817201139-bc19a97f63c8 // indirect

4
go.sum
View File

@@ -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/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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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-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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 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/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 h1:h9JoA60e1dVEOpp0PFwJSmt1Htu057NUq9/bUwaO61s=
github.com/pelletier/go-toml/v2 v2.0.3/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= 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= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=

View File

@@ -35,7 +35,7 @@ func main() {
address, webServerDone := dashboard.StartServer(version, int(opts.Port), opts.Namespace, opts.Verbose, opts.NoTracking) address, webServerDone := dashboard.StartServer(version, int(opts.Port), opts.Namespace, opts.Verbose, opts.NoTracking)
if !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 { if opts.NoBrowser {

View File

@@ -12,11 +12,18 @@ type ScannersHandler struct {
} }
func (h *ScannersHandler) List(c *gin.Context) { func (h *ScannersHandler) List(c *gin.Context) {
res := make([]string, 0) type ScannerInfo struct {
for _, scanner := range h.Data.Scanners { SupportedResourceKinds []string
res = append(res, scanner.Name()) 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) { func (h *ScannersHandler) ScanDraftManifest(c *gin.Context) {

View File

@@ -1,11 +1,12 @@
package scanners package scanners
import ( import (
"encoding/json"
"github.com/komodorio/helm-dashboard/pkg/dashboard/subproc" "github.com/komodorio/helm-dashboard/pkg/dashboard/subproc"
"github.com/komodorio/helm-dashboard/pkg/dashboard/utils" "github.com/komodorio/helm-dashboard/pkg/dashboard/utils"
"github.com/olekukonko/tablewriter"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1" v1 "k8s.io/apimachinery/pkg/apis/testapigroup/v1"
"strconv"
"strings" "strings"
) )
@@ -13,11 +14,46 @@ type Checkov struct {
Data *subproc.DataLayer 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 { func (c *Checkov) Name() string {
return "Checkov" return "Checkov"
} }
func (c *Checkov) Test() bool { func (c *Checkov) Test() bool {
utils.FailLogLevel = log.DebugLevel
defer func() { utils.FailLogLevel = log.WarnLevel }()
res, err := utils.RunCommand([]string{"checkov", "--version"}, nil) res, err := utils.RunCommand([]string{"checkov", "--version"}, nil)
if err != nil { if err != nil {
return false return false
@@ -33,7 +69,7 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
} }
defer fclose() 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) out, err := utils.RunCommand(cmd, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -41,7 +77,10 @@ func (c *Checkov) ScanManifests(mnf string) (*subproc.ScanResults, error) {
res := &subproc.ScanResults{} res := &subproc.ScanResults{}
res.OrigReport = out err = json.Unmarshal([]byte(out), res.OrigReport)
if err != nil {
return nil, err
}
return res, nil return res, nil
} }
@@ -61,41 +100,46 @@ func (c *Checkov) ScanResource(ns string, kind string, name string) (*subproc.Sc
} }
defer fclose() 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) out, err := utils.RunCommand(cmd, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
res := subproc.ScanResults{} cr := CheckovReport{}
_, out, _ = strings.Cut(out, "\n") // kubernetes scan results: err = json.Unmarshal([]byte(out), &cr)
_, out, _ = strings.Cut(out, "\n") // empty line if err != nil {
line, out, found := strings.Cut(out, "\n") // status line return nil, err
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")
} }
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 { func checkovReportTable(c *CheckovReport) string {
Summary CheckovSummary 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 { type CheckovSummary struct {
@@ -105,3 +149,16 @@ type CheckovSummary struct {
// parsing errors? // parsing errors?
// skipped ? // 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"`
}

View File

@@ -12,11 +12,31 @@ type Trivy struct {
Data *subproc.DataLayer 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 { func (c *Trivy) Name() string {
return "Trivy" return "Trivy"
} }
func (c *Trivy) Test() bool { func (c *Trivy) Test() bool {
utils.FailLogLevel = log.DebugLevel
defer func() { utils.FailLogLevel = log.WarnLevel }()
res, err := utils.RunCommand([]string{"trivy", "--version"}, nil) res, err := utils.RunCommand([]string{"trivy", "--version"}, nil)
if err != nil { if err != nil {
return false return false

View File

@@ -350,17 +350,3 @@ $("#btnAddRepository").click(function () {
setHashParam("section", "repository") setHashParam("section", "repository")
window.location.reload() 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()
}
})
})

View File

@@ -150,6 +150,12 @@ function showResources(namespace, chart, revision) {
$.getJSON(url).fail(function (xhr) { $.getJSON(url).fail(function (xhr) {
reportError("Failed to get list of resources", xhr) reportError("Failed to get list of resources", xhr)
}).done(function (data) { }).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(); resBody.empty();
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
const res = data[i] const res = data[i]
@@ -159,7 +165,7 @@ function showResources(namespace, chart, revision) {
<div class="col-3 res-name text-break fw-bold"></div> <div class="col-3 res-name text-break fw-bold"></div>
<div class="col-1 res-status overflow-hidden"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></div> <div class="col-1 res-status overflow-hidden"><span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span></div>
<div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div> <div class="col-4 res-statusmsg"><span class="text-muted small">Getting status...</span></div>
<div class="col-2 res-actions"></div> <div class="col-2 res-actions"><button class='btn btn-sm ms-2 visually-hidden'>Vertical-sizer</button></div>
</div> </div>
`) `)
@@ -195,11 +201,13 @@ function showResources(namespace, chart, revision) {
showDescribe(ns, res.kind, res.metadata.name, badge.clone()) showDescribe(ns, res.kind, res.metadata.name, badge.clone())
}) })
const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>"); if (scannableResKinds.has(res.kind)) {
resBlock.find(".res-actions").append(btn2) const btn2 = $("<button class='btn btn-sm btn-white border-secondary ms-2'>Scan</button>");
btn2.click(function () { resBlock.find(".res-actions").append(btn2)
scanResource(ns, res.kind, res.metadata.name, badge.clone()) 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.") body.append("No information from scanners. Make sure you have installed some and scanned object is supported.")
} }
const tabs = $('<ul class="nav nav-tabs mt-3" role="tablist"></ul>')
const content = $('<div class="tab-content"></div>')
for (let name in data) { for (let name in data) {
const res = data[name] const res = data[name]
if (!res.OrigReport) continue if (!res.OrigReport && !res.PassedCount) continue
const hdr = $("<h3>" + name + " Scan Results</h3>");
const hdr = $(`<li class="nav-item" role="presentation">
<button class="nav-link" id="` + name + `-tab" data-bs-toggle="tab" data-bs-target="#` + name + `-tab-pane" type="button" role="tab">` + name + `</button>
</li>`)
if (res.FailedCount) { if (res.FailedCount) {
hdr.append("<span class='badge bg-danger ms-3'>" + res.FailedCount + " failed</span>") hdr.find('button').append("<span class='badge bg-danger ms-2'>" + res.FailedCount + " failed</span>")
} }
if (res.PassedCount) { if (res.PassedCount) {
hdr.append("<span class='badge bg-info ms-3'>" + res.PassedCount + " passed</span>") hdr.find('button').append("<span class='badge bg-info ms-2'>" + res.PassedCount + " passed</span>")
} }
body.append(hdr)
const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value const hl = hljs.highlight(res.OrigReport, {language: 'yaml'}).value
const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl) const pre = $("<pre class='bg-white rounded p-3' style='font-size: inherit; overflow: unset'></pre>").html(hl)
body.append(pre) const div = $('<div class="tab-pane fade" id="' + name + '-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0"></div>').append(pre)
tabs.append(hdr)
content.append(div)
} }
body.append(tabs)
body.append(content)
tabs.find('li').first().find('button').click()
}) })
} }

View File

@@ -90,14 +90,15 @@
<button class="btn btn-sm btn-light bg-white border border-secondary btn-remove"> <button class="btn btn-sm btn-light bg-white border border-secondary btn-remove">
<i class="bi-trash3"></i> Remove <i class="bi-trash3"></i> Remove
</button> </button>
<p class="my-3"><input class="form-control form-control-sm" type="text" placeholder="Filter..." id="inputSearch"></p>
</div> </div>
<div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div> <div><span class="text-muted small fw-bold me-3">REPOSITORY</span></div>
<h2 class="mb-3">name-of-repo</h2> <h2 class="mb-3">name-of-repo</h2>
<div class="mb-5"> <div class="mb-5">
<span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span> <span class="rounded rounded-1 me-2 p-1 px-2 bg-tag text-dark">URL: <span class="url fw-bold">http://somerepo/somepath</span></span>
</div> </div>
<div class="row py-2 mb-3"> <div class="py-2 mb-3 float-end">
<input class="form-control" type="text" placeholder="Search..." id="inputSearch">
</div> </div>
<div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small" <div class="row bg-secondary rounded px-3 py-2 mb-3 fw-bold small"
style="text-transform: uppercase"> style="text-transform: uppercase">
@@ -390,7 +391,7 @@
<div id="upgradeModalBody" class="small"></div> <div id="upgradeModalBody" class="small"></div>
</form> </form>
<div class="modal-footer d-flex"> <div class="modal-footer d-flex">
<button type="button" class="btn btn-scan bg-white border-secondary">Scan for Problems</button> <button type="button" class="btn btn-scan bg-white border-secondary display-none">Scan for Problems</button>
<button type="button" class="btn btn-primary btn-confirm">Confirm</button> <button type="button" class="btn btn-primary btn-confirm">Confirm</button>
</div> </div>
</div> </div>

View File

@@ -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 () { $("#sectionRepo .repo-list .btn").click(function () {
const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {}); const myModal = new bootstrap.Modal(document.getElementById('repoAddModal'), {});
myModal.show() myModal.show()

View File

@@ -17,8 +17,11 @@ $(function () {
$.getJSON("/api/scanners").fail(function (xhr) { $.getJSON("/api/scanners").fail(function (xhr) {
reportError("Failed to get list of scanners", xhr) reportError("Failed to get list of scanners", xhr)
}).done(function (data) { }).done(function (data) {
if (!data || !data.length) { $("body").data("scanners", data)
$("#upgradeModal .btn-scan").hide() for (let k in data) {
if (data[k].ManifestScannable) {
$("#upgradeModal .btn-scan").show() // TODO: move this to install flow
}
} }
}) })

View File

@@ -26,10 +26,6 @@ body > .container-fluid {
min-height: 100% !important; min-height: 100% !important;
} }
#topNav.navbar {
}
.navbar-brand > a > img { .navbar-brand > a > img {
vertical-align: middle; vertical-align: middle;
height: 3rem; height: 3rem;
@@ -211,17 +207,17 @@ span.link {
position: static; position: static;
} }
.nav-tabs { nav .nav-tabs {
border: none; border: none;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.nav-tabs .nav-link { nav .nav-tabs .nav-link {
padding-bottom: 0.25rem; padding-bottom: 0.25rem;
color: #3B3D45; color: #3B3D45;
} }
.nav-tabs .nav-link.active { nav .nav-tabs .nav-link.active {
border: none; border: none;
border-bottom: 3px solid #3B3D45; border-bottom: 3px solid #3B3D45;
background-color: transparent; background-color: transparent;

View File

@@ -163,7 +163,7 @@ func (d *DataLayer) ListInstalled() (res []ReleaseElement, err error) {
func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) { func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*HistoryElement, err error) {
// TODO: there is `max` but there is no `offset` // 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 { if err != nil {
return nil, err return nil, err
} }
@@ -178,7 +178,7 @@ func (d *DataLayer) ChartHistory(namespace string, chartName string) (res []*His
if err != nil { if err != nil {
return nil, err return nil, err
} }
elm.ChartName = chartRepoName elm.ChartName = chartRepoName // TODO: move it to frontend?
elm.ChartVer = curVer elm.ChartVer = curVer
elm.Updated.Time = elm.Updated.Time.Round(time.Second) elm.Updated.Time = elm.Updated.Time.Round(time.Second)
} }

View File

@@ -24,8 +24,9 @@ type HistoryElement struct {
Chart string `json:"chart"` Chart string `json:"chart"`
AppVersion string `json:"app_version"` AppVersion string `json:"app_version"`
Description string `json:"description"` 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 { type RepoChartElement struct {

View File

@@ -5,6 +5,8 @@ type Scanner interface {
Test() bool // test if the scanner is available Test() bool // test if the scanner is available
ScanManifests(mnf string) (*ScanResults, error) // run the scanner on manifests 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 ScanResource(ns string, kind string, name string) (*ScanResults, error) // run the scanner on k8s resource
SupportedResourceKinds() []string
ManifestIsScannable() bool
} }
type ScanResults struct { type ScanResults struct {

View File

@@ -12,6 +12,8 @@ import (
"strings" "strings"
) )
var FailLogLevel = log.WarnLevel // allows to suppress error logging in some situations
type ControlChan = chan struct{} type ControlChan = chan struct{}
func ChartAndVersion(x string) (string, string, error) { func ChartAndVersion(x string) (string, string, error) {
@@ -64,10 +66,10 @@ func RunCommand(cmd []string, env map[string]string) (string, error) {
prog.Stderr = &stderr prog.Stderr = &stderr
if err := prog.Run(); err != nil { if err := prog.Run(); err != nil {
log.Warnf("Failed command: %s", cmd) log.StandardLogger().Logf(FailLogLevel, "Failed command: %s", cmd)
serr := stderr.Bytes() serr := stderr.Bytes()
if serr != nil { if serr != nil {
log.Warnf("STDERR:\n%s", serr) log.StandardLogger().Logf(FailLogLevel, "STDERR:\n%s", serr)
} }
if eerr, ok := err.(*exec.ExitError); ok { if eerr, ok := err.(*exec.ExitError); ok {
return "", CmdError{ return "", CmdError{