feat(kubernetes): Add report flag for summary (#2112)

* feat(k8s): Add report flag for summary
* chore: add headings to the severity columns
* chore: make the default output of k8s summary table

Signed-off-by: Owen Rumney <owen.rumney@aquasec.com>
This commit is contained in:
Owen Rumney
2022-05-13 19:02:01 +01:00
committed by GitHub
parent 5f004f03d9
commit 2ae8faa7a8
9 changed files with 255 additions and 28 deletions

View File

@@ -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
```
<details>
<summary>Result</summary>
```
```json
{
"ClusterName": "minikube",
"Vulnerabilities": [

BIN
docs/imgs/k8s-summary.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

16
go.mod
View File

@@ -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

16
go.sum
View File

@@ -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=

View File

@@ -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,

View File

@@ -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)
}

View File

@@ -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"),
}
}

View File

@@ -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)
}

159
pkg/report/k8s/summary.go Normal file
View File

@@ -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("<bold><red>%s</red></bold>", value)
case "HIGH":
return tml.Sprintf("<red>%s</red>", value)
case "MEDIUM":
return tml.Sprintf("<yellow>%s</yellow>", value)
case "UNKNOWN":
return tml.Sprintf("<blue>%s</blue>", value)
default:
return tml.Sprintf("%s", value)
}
}