diff --git a/docs/docs/kubernetes/scanning.md b/docs/docs/kubernetes/scanning.md index 72fd142d71..dff0501ae1 100644 --- a/docs/docs/kubernetes/scanning.md +++ b/docs/docs/kubernetes/scanning.md @@ -6,6 +6,8 @@ This feature might change without preserving backwards compatibility. Scan your Kubernetes cluster for both Vulnerabilities and Misconfigurations. +Trivy uses your local kubectl configuration to access the API server to list artifacts. + Scan a full cluster: ``` @@ -24,13 +26,25 @@ Scan a namespace for only `CRITICAL` Vulnerabilities and Misconfigurations: $ trivy k8s -n default -o results.json --severity CRITICAL ``` -Trivy uses your local kubectl configuration to access the API server to list artifacts. -At this time, JSON is the only supported output and is intended to be used for automation, other reports will be implemented soon. +Scan a cluster and generate a simple summary report. The only outputs currently supported are `all` and `summary`. The default report format is `summary` + +``` +$ trivy k8s +``` + +![k8s Summary Report](../../imgs/k8s-summary.png) + +To get all of the detail the output contains, use `--report all`, to get JSON output: + +``` +$ trivy k8s --report all +``` +
Result -``` +```json { "ClusterName": "minikube", "Vulnerabilities": [ diff --git a/docs/imgs/k8s-summary.png b/docs/imgs/k8s-summary.png new file mode 100644 index 0000000000..3089dc2f05 Binary files /dev/null and b/docs/imgs/k8s-summary.png differ diff --git a/go.mod b/go.mod index e4aba14f0f..30f51a779d 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,12 @@ require ( github.com/Masterminds/sprig/v3 v3.2.2 github.com/NYTimes/gziphandler v1.1.1 github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 - github.com/aquasecurity/fanal v0.0.0-20220513163515-33f2cd8392ee + github.com/aquasecurity/fanal v0.0.0-20220511115204-32614d79a234 github.com/aquasecurity/go-dep-parser v0.0.0-20220503151658-d316f5cc2cff github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 - github.com/aquasecurity/table v1.2.0 github.com/aquasecurity/trivy-db v0.0.0-20220327074450-74195d9604b2 github.com/caarlos0/env/v6 v6.9.1 github.com/cenkalti/backoff v2.2.1+incompatible @@ -54,7 +53,7 @@ require ( require ( cloud.google.com/go v0.99.0 // indirect cloud.google.com/go/storage v1.14.0 // indirect - github.com/Azure/azure-sdk-for-go v64.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v63.0.0+incompatible // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.27 // indirect @@ -77,10 +76,9 @@ require ( github.com/VividCortex/ewma v1.1.1 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/alecthomas/chroma v0.10.0 // indirect github.com/apparentlymart/go-cidr v1.1.0 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/aquasecurity/defsec v0.57.5 + github.com/aquasecurity/defsec v0.57.3 github.com/aws/aws-sdk-go v1.44.5 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect @@ -93,7 +91,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect - github.com/dlclark/regexp2 v1.4.0 // indirect github.com/docker/cli v20.10.13+incompatible // indirect github.com/docker/distribution v2.8.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.6.4 // indirect @@ -242,6 +239,13 @@ require gopkg.in/yaml.v2 v2.4.0 require github.com/aquasecurity/trivy-kubernetes v0.1.0 +require github.com/aquasecurity/table v1.5.1 + +require ( + github.com/alecthomas/chroma v0.10.0 // indirect + github.com/dlclark/regexp2 v1.4.0 // indirect +) + // To resolve CVE-2022-23648 replace github.com/containerd/containerd v1.5.9 => github.com/containerd/containerd v1.5.10 diff --git a/go.sum b/go.sum index 04f4ce1428..3f89153fe8 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v64.0.0+incompatible h1:WAA77WBDWYtNfCC95V70VvkdzHe+wM/r2MQ9mG7fnQs= -github.com/Azure/azure-sdk-for-go v64.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v63.0.0+incompatible h1:whPsa+jCHQSo5wGMPNLw4bz8q9Co2+vnXHzXGctoTaQ= +github.com/Azure/azure-sdk-for-go v63.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= @@ -180,10 +180,10 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6 github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM= github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= -github.com/aquasecurity/defsec v0.57.5 h1:kOsRgMlQMxdOHYNEF0SCblZevrsdoz7c4fz5qYTTUFY= -github.com/aquasecurity/defsec v0.57.5/go.mod h1:42FxKif2itz+MHFlJ3TJjdroL9Jzj3THoexlueBTU5w= -github.com/aquasecurity/fanal v0.0.0-20220513163515-33f2cd8392ee h1:O7cN19V4W7u7s7M3kME21/IDUA4iQULDeo9g3DS4gdU= -github.com/aquasecurity/fanal v0.0.0-20220513163515-33f2cd8392ee/go.mod h1:1FqeeQo0AKRIgYgv60r0SOBNMBenxBQF3jAnnICEFIE= +github.com/aquasecurity/defsec v0.57.3 h1:oiATfUTxOAcxAuXSH31RdgjtXJdQznlVzMJWdVYGmXY= +github.com/aquasecurity/defsec v0.57.3/go.mod h1:42FxKif2itz+MHFlJ3TJjdroL9Jzj3THoexlueBTU5w= +github.com/aquasecurity/fanal v0.0.0-20220511115204-32614d79a234 h1:NG9Qs4hocUWcGytaA0yhArPRoPmo12EPAUERwYCgvLA= +github.com/aquasecurity/fanal v0.0.0-20220511115204-32614d79a234/go.mod h1:bqz0H4eqstkngJB0TJCk39GLXZcUtobMpuNr4ScC1vk= github.com/aquasecurity/go-dep-parser v0.0.0-20220503151658-d316f5cc2cff h1:YNlzRYB0n4mZtfuWx6AWaGEjnLVNekchyoFDlYFZegs= github.com/aquasecurity/go-dep-parser v0.0.0-20220503151658-d316f5cc2cff/go.mod h1:7EOQWQmyavVPY3fScbbPdd3dB/b0Q4ZbJ/NZCvNKrLs= github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= @@ -195,8 +195,8 @@ github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go. github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M= github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= -github.com/aquasecurity/table v1.2.0 h1:26N9hFB5qttCjWoBgeKIlBtmlCpSwfL01BK7N+IOBN0= -github.com/aquasecurity/table v1.2.0/go.mod h1:1MFKrEPJ8NchM917BrVGvsqoXJo1OL1Ja7dF3PgUea4= +github.com/aquasecurity/table v1.5.1 h1:y05AuHM3p4BGybbGn/XbcTX3RxpyzeTXAXYMcJve4IE= +github.com/aquasecurity/table v1.5.1/go.mod h1:1MFKrEPJ8NchM917BrVGvsqoXJo1OL1Ja7dF3PgUea4= github.com/aquasecurity/testdocker v0.0.0-20210911155206-e1e85f5a1516 h1:moQmzbpLo5dxHQCyEhqzizsDSNrNhn/7uRTCZzo4A1o= github.com/aquasecurity/trivy-db v0.0.0-20220327074450-74195d9604b2 h1:q2Gza4V8uO5C1COzC2HeTbQgJIrmC6dTWaXZ8ujiWu0= github.com/aquasecurity/trivy-db v0.0.0-20220327074450-74195d9604b2/go.mod h1:EwiQRdzVq6k7cKOMjkss8LjWMt2FUW7NaYwE7HfZZvk= diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 6ec860cd87..e40df8eba7 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -12,6 +12,7 @@ import ( "github.com/aquasecurity/trivy-db/pkg/metadata" dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/commands/option" "github.com/aquasecurity/trivy/pkg/commands/plugin" @@ -215,6 +216,12 @@ var ( EnvVars: []string{"TRIVY_K8S_NAMESPACE"}, } + reportFlag = cli.StringFlag{ + Name: "report", + Value: "summary", + Usage: "specify a report format for the output. (all,summary default: all)", + } + // TODO: remove this flag after a sufficient deprecation period. lightFlag = cli.BoolFlag{ Name: "light", @@ -789,6 +796,7 @@ func NewK8sCommand() *cli.Command { Action: artifact.K8sRun, Flags: []cli.Flag{ &namespaceFlag, + &reportFlag, &outputFlag, &severityFlag, &exitCodeFlag, diff --git a/pkg/commands/artifact/k8s.go b/pkg/commands/artifact/k8s.go index 7acca7d3f0..7a22558521 100644 --- a/pkg/commands/artifact/k8s.go +++ b/pkg/commands/artifact/k8s.go @@ -15,6 +15,7 @@ import ( "github.com/aquasecurity/fanal/analyzer" "github.com/aquasecurity/fanal/cache" "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" pkgReport "github.com/aquasecurity/trivy/pkg/report" k8sReport "github.com/aquasecurity/trivy/pkg/report/k8s" @@ -76,9 +77,9 @@ func K8sRun(ctx *cli.Context) error { report.ClusterName = cluster.GetCurrentContext() if err = k8sReport.Write(report, pkgReport.Option{ - Format: "json", // for now json is the default + Format: opt.KubernetesOption.ReportFormat, // for now json is the default Output: opt.Output, - }); err != nil { + }, opt.Severities); err != nil { return xerrors.Errorf("unable to write results: %w", err) } diff --git a/pkg/commands/option/kubernetes.go b/pkg/commands/option/kubernetes.go index af13ee6fdb..04ab417c43 100644 --- a/pkg/commands/option/kubernetes.go +++ b/pkg/commands/option/kubernetes.go @@ -6,12 +6,14 @@ import ( // KubernetesOption holds the options for Kubernetes scanning type KubernetesOption struct { - Namespace string + Namespace string + ReportFormat string } // NewKubernetesOption is the factory method to return Kubernetes options func NewKubernetesOption(c *cli.Context) KubernetesOption { return KubernetesOption{ - Namespace: c.String("namespace"), + Namespace: c.String("namespace"), + ReportFormat: c.String("report"), } } diff --git a/pkg/report/k8s/k8s.go b/pkg/report/k8s/k8s.go index 4526ead6ef..f25e331564 100644 --- a/pkg/report/k8s/k8s.go +++ b/pkg/report/k8s/k8s.go @@ -3,6 +3,8 @@ package k8s import ( "golang.org/x/xerrors" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy/pkg/types" ) @@ -15,26 +17,33 @@ type Report struct { Misconfigurations []Resource `json:",omitempty"` } +// ConsolidatedReport represents a kubernetes scan report with consolidated findings +type ConsolidatedReport struct { + SchemaVersion int `json:",omitempty"` + ClusterName string + Findings []Resource `json:",omitempty"` +} + // Resource represents a kubernetes resource report type Resource struct { Namespace string `json:",omitempty"` Kind string Name string - //TODO(josedonizetti): should add metadata? per report? per Result? - //Metadata Metadata `json:",omitempty"` + // TODO(josedonizetti): should add metadata? per report? per Result? + // Metadata Metadata `json:",omitempty"` Results types.Results `json:",omitempty"` Error string `json:",omitempty"` } // Failed returns whether the k8s report includes any vulnerabilities or misconfigurations -func (report Report) Failed() bool { - for _, r := range report.Vulnerabilities { +func (r Report) Failed() bool { + for _, r := range r.Vulnerabilities { if r.Results.Failed() { return true } } - for _, r := range report.Misconfigurations { + for _, r := range r.Misconfigurations { if r.Results.Failed() { return true } @@ -43,17 +52,47 @@ func (report Report) Failed() bool { return false } +func (r Report) consolidate() ConsolidatedReport { + consolidated := ConsolidatedReport{ + SchemaVersion: r.SchemaVersion, + ClusterName: r.ClusterName, + } + + for _, m := range r.Misconfigurations { + found := false + for _, v := range r.Vulnerabilities { + if v.Kind == m.Kind && v.Name == m.Name && v.Namespace == m.Namespace { + consolidated.Findings = append(consolidated.Findings, Resource{ + Namespace: v.Namespace, + Kind: v.Kind, + Name: v.Name, + Results: append(v.Results, m.Results...), + Error: v.Error, + }) + found = true + continue + } + } + if !found { + consolidated.Findings = append(consolidated.Findings, m) + } + } + return consolidated +} + // Writer defines the result write operation type Writer interface { Write(Report) error } // Write writes the results in the give format -func Write(report Report, option report.Option) error { +func Write(report Report, option report.Option, severities []dbTypes.Severity) error { var writer Writer switch option.Format { - case "json": + case "all": writer = &JSONWriter{Output: option.Output} + case "summary": + writer = NewSummaryWriter(option.Output, severities) default: return xerrors.Errorf("unknown format: %v", option.Format) } diff --git a/pkg/report/k8s/summary.go b/pkg/report/k8s/summary.go new file mode 100644 index 0000000000..8d53af2e87 --- /dev/null +++ b/pkg/report/k8s/summary.go @@ -0,0 +1,159 @@ +package k8s + +import ( + "fmt" + "io" + "sort" + "strconv" + "strings" + + "github.com/aquasecurity/table" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + + "github.com/liamg/tml" +) + +type SummaryWriter struct { + Output io.Writer + Severities []string + SeverityHeadings []string +} + +func NewSummaryWriter(output io.Writer, requiredSevs []dbTypes.Severity) SummaryWriter { + var severities []string + var severityHeadings []string + severities, severityHeadings = getRequiredSeverities(requiredSevs) + return SummaryWriter{ + Output: output, + Severities: severities, + SeverityHeadings: severityHeadings, + } +} + +// Write writes the results in a summarized table format +func (s SummaryWriter) Write(report Report) error { + consolidated := report.consolidate() + _, _ = fmt.Fprintln(s.Output) + _, _ = fmt.Fprintf(s.Output, "Summary Report for %s\n", consolidated.ClusterName) + + t := table.New(s.Output) + t.SetRowLines(false) + configureHeader(s, t) + + sort.Slice(consolidated.Findings, func(i, j int) bool { + return consolidated.Findings[i].Namespace > consolidated.Findings[j].Namespace + }) + + for _, finding := range consolidated.Findings { + if !finding.Results.Failed() { + continue + } + vCount, mCount, sCount := accumulateSeverityCounts(finding) + name := fmt.Sprintf("%s/%s", finding.Kind, finding.Name) + rowParts := []string{finding.Namespace, name} + rowParts = append(rowParts, s.generateSummary(vCount)...) + rowParts = append(rowParts, s.generateSummary(mCount)...) + rowParts = append(rowParts, s.generateSummary(sCount)...) + + t.AddRow(rowParts...) + } + + t.Render() + + keyParts := []string{"Severities:"} + for _, s := range s.Severities { + keyParts = append(keyParts, fmt.Sprintf("%s=%s", s[:1], colourSeverityValue(s, s))) + } + + _, _ = fmt.Fprintln(s.Output, strings.Join(keyParts, " ")) + _, _ = fmt.Fprintln(s.Output) + return nil +} + +func (s SummaryWriter) generateSummary(sevCount map[string]int) []string { + var parts []string + + for _, sev := range s.Severities { + if count, ok := sevCount[sev]; ok { + parts = append(parts, colourSeverityValue(strconv.Itoa(count), sev)) + } else { + parts = append(parts, " ") + } + } + + return parts +} + +func getRequiredSeverities(requiredSevs []dbTypes.Severity) ([]string, []string) { + requiredSevOrder := []dbTypes.Severity{dbTypes.SeverityCritical, + dbTypes.SeverityHigh, dbTypes.SeverityMedium, + dbTypes.SeverityLow, dbTypes.SeverityUnknown} + var severities []string + var severityHeadings []string + for _, sev := range requiredSevOrder { + for _, p := range requiredSevs { + if p == sev { + severities = append(severities, sev.String()) + severityHeadings = append(severityHeadings, strings.ToUpper(sev.String()[:1])) + continue + } + } + } + return severities, severityHeadings +} + +func accumulateSeverityCounts(finding Resource) (map[string]int, map[string]int, map[string]int) { + vCount := make(map[string]int) + mCount := make(map[string]int) + sCount := make(map[string]int) + for _, r := range finding.Results { + for _, rv := range r.Vulnerabilities { + vCount[rv.Severity] = vCount[rv.Severity] + 1 + } + for _, rv := range r.Misconfigurations { + mCount[rv.Severity] = mCount[rv.Severity] + 1 + } + for _, rv := range r.Secrets { + sCount[rv.Severity] = sCount[rv.Severity] + 1 + } + } + return vCount, mCount, sCount +} + +func configureHeader(s SummaryWriter, t *table.Table) { + sevCount := len(s.Severities) + + headerRow := []string{"Namespace", "Resource"} + // vulnerabilities headings + headerRow = append(headerRow, s.SeverityHeadings...) + // misconfig headings + headerRow = append(headerRow, s.SeverityHeadings...) + // secrets headings + headerRow = append(headerRow, s.SeverityHeadings...) + headerAlignment := []table.Alignment{table.AlignLeft, table.AlignLeft} + + for i := 0; i < len(headerRow)-2; i++ { + headerAlignment = append(headerAlignment, table.AlignCenter) + } + + t.SetHeaders("Namespace", "Resource", "Vulnerabilities", "Misconfigurations", "Secrets") + t.AddHeaders(headerRow...) + t.SetAlignment(headerAlignment...) + t.SetAutoMergeHeaders(true) + t.SetHeaderColSpans(0, 1, 1, sevCount, sevCount, sevCount) +} + +func colourSeverityValue(value string, severity string) string { + switch severity { + case "CRITICAL": + return tml.Sprintf("%s", value) + case "HIGH": + return tml.Sprintf("%s", value) + case "MEDIUM": + return tml.Sprintf("%s", value) + case "UNKNOWN": + return tml.Sprintf("%s", value) + default: + return tml.Sprintf("%s", value) + } +}