From 3ef450d9a49398519a6bd49327af4a88a25b216c Mon Sep 17 00:00:00 2001 From: Jose Donizetti Date: Sun, 15 May 2022 13:01:58 -0300 Subject: [PATCH] feat: k8s resource scanning (#2118) --- docs/docs/kubernetes/scanning.md | 34 +++++++------ go.mod | 11 ++-- go.sum | 16 +++--- pkg/commands/app.go | 15 +++++- pkg/k8s/json.go | 15 +++++- pkg/k8s/report.go | 35 ++++++++++--- pkg/k8s/run.go | 86 ++++++++++++++++++++++++++++---- pkg/k8s/scanner.go | 1 - pkg/k8s/table.go | 41 +++++++++++++++ 9 files changed, 207 insertions(+), 47 deletions(-) create mode 100644 pkg/k8s/table.go diff --git a/docs/docs/kubernetes/scanning.md b/docs/docs/kubernetes/scanning.md index 7c3e838a54..cc71577e8c 100644 --- a/docs/docs/kubernetes/scanning.md +++ b/docs/docs/kubernetes/scanning.md @@ -8,39 +8,41 @@ 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: +Scan a full cluster and generate a simple summary report: ``` -$ trivy k8s +$ trivy k8s --report=summary +``` + +![k8s Summary Report](../../imgs/k8s-summary.png) + +The summary report is the default. To get all of the detail the output contains, use `--report all`. + +Filter by severity: + +``` +$ trivy k8s --severity=CRITICAL --report=all ``` Scan a specific namespace: ``` -$ trivy k8s -n default +$ trivy k8s -n kube-system --report=summary ``` -Scan a namespace for only `CRITICAL` Vulnerabilities and Misconfigurations: +Scan a specific resource and get all the output: ``` -$ trivy k8s -n default --severity CRITICAL +$ trivy k8s deployment/appname ``` -Scan a cluster and generate a simple summary report. The only outputs currently supported are `all` and `summary`. The default report format is `summary` +The supported formats are `table`, which is the default, and `json`. +To get a JSON output on a full cluster scan: ``` -$ trivy k8s +$ trivy k8s --format json -o results.json ``` -![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 diff --git a/go.mod b/go.mod index adf90ab7fa..83c07b874e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ 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-20220511115204-32614d79a234 + github.com/aquasecurity/fanal v0.0.0-20220513163515-33f2cd8392ee 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 @@ -54,7 +54,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 v63.0.0+incompatible // indirect + github.com/Azure/azure-sdk-for-go v64.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,7 +77,7 @@ require ( github.com/agext/levenshtein v1.2.3 // 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.3 + github.com/aquasecurity/defsec v0.57.5 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 @@ -198,7 +198,7 @@ require ( require ( github.com/aquasecurity/table v1.5.1 - github.com/aquasecurity/trivy-kubernetes v0.1.0 + github.com/aquasecurity/trivy-kubernetes v0.2.1 ) require ( @@ -251,3 +251,6 @@ replace github.com/containerd/containerd v1.5.9 => github.com/containerd/contain // See https://github.com/moby/moby/issues/42939#issuecomment-1114255529 replace github.com/docker/docker => github.com/docker/docker v20.10.3-0.20220224222438-c78f6963a1c0+incompatible + +// TODO: remove once the feature is merged upstream +replace github.com/aquasecurity/trivy-kubernetes v0.1.0 => github.com/josedonizetti/trivy-kubernetes v0.0.0-20220515001536-b6e8afada9c5 diff --git a/go.sum b/go.sum index ef4043220a..59cf28059d 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 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/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/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.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/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/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= @@ -200,8 +200,8 @@ github.com/aquasecurity/table v1.5.1/go.mod h1:1MFKrEPJ8NchM917BrVGvsqoXJo1OL1Ja github.com/aquasecurity/testdocker v0.0.0-20210911155206-e1e85f5a1516 h1:moQmzbpLo5dxHQCyEhqzizsDSNrNhn/7uRTCZzo4A1o= github.com/aquasecurity/trivy-db v0.0.0-20220510190819-8ca06716f46e h1:NLm5KWGcnkwaUR1GODPePyhNsbuFiT6lgKYcCcW9c10= github.com/aquasecurity/trivy-db v0.0.0-20220510190819-8ca06716f46e/go.mod h1:/nULgnDeq/JMPMVwE1dmf4kWlYn++7VrM3O2naj4BHA= -github.com/aquasecurity/trivy-kubernetes v0.1.0 h1:eE7JSdqo83Kn87c86DcUIsPAtW0K9UnkkHEQ4sGI030= -github.com/aquasecurity/trivy-kubernetes v0.1.0/go.mod h1:9fU3sHz/wXN5ruZ5snUEJpzm2X6pUndKucv1mz9Walc= +github.com/aquasecurity/trivy-kubernetes v0.2.1 h1:h7MeJpwZXd4+9f6mWRslrYQWSY/NXede50zUDx4kwLk= +github.com/aquasecurity/trivy-kubernetes v0.2.1/go.mod h1:9fU3sHz/wXN5ruZ5snUEJpzm2X6pUndKucv1mz9Walc= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= diff --git a/pkg/commands/app.go b/pkg/commands/app.go index 3c583ea6c4..741ebf1314 100644 --- a/pkg/commands/app.go +++ b/pkg/commands/app.go @@ -219,7 +219,7 @@ var ( reportFlag = cli.StringFlag{ Name: "report", - Value: "summary", + Value: "all", Usage: "specify a report format for the output. (all,summary default: all)", } @@ -804,10 +804,21 @@ func NewK8sCommand() *cli.Command { Name: "kubernetes", Aliases: []string{"k8s"}, Usage: "scan kubernetes vulnerabilities and misconfigurations", - Action: k8s.Run, + CustomHelpTemplate: cli.CommandHelpTemplate + `EXAMPLES: + - cluster scanning: + $ trivy k8s --report summary + + - namespace scanning: + $ trivy k8s -n kube-system --report summary + + - resource scanning: + $ trivy k8s deployment/orion +`, + Action: k8s.Run, Flags: []cli.Flag{ &namespaceFlag, &reportFlag, + &formatFlag, &outputFlag, &severityFlag, &exitCodeFlag, diff --git a/pkg/k8s/json.go b/pkg/k8s/json.go index 71047ab2e3..6e9c305419 100644 --- a/pkg/k8s/json.go +++ b/pkg/k8s/json.go @@ -10,11 +10,23 @@ import ( type JSONWriter struct { Output io.Writer + Report string } // Write writes the results in JSON format func (jw JSONWriter) Write(report Report) error { - output, err := json.MarshalIndent(report, "", " ") + var output []byte + var err error + + switch jw.Report { + case allReport: + output, err = json.MarshalIndent(report, "", " ") + case summaryReport: + output, err = json.MarshalIndent(report.consolidate(), "", " ") + default: + err = fmt.Errorf("report %s not supported", jw.Report) + } + if err != nil { return xerrors.Errorf("failed to marshal json: %w", err) } @@ -22,5 +34,6 @@ func (jw JSONWriter) Write(report Report) error { if _, err = fmt.Fprintln(jw.Output, string(output)); err != nil { return xerrors.Errorf("failed to write json: %w", err) } + return nil } diff --git a/pkg/k8s/report.go b/pkg/k8s/report.go index 69a7c25818..53b2707bb7 100644 --- a/pkg/k8s/report.go +++ b/pkg/k8s/report.go @@ -2,6 +2,7 @@ package k8s import ( "fmt" + "io" "golang.org/x/xerrors" @@ -9,10 +10,24 @@ import ( dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" - "github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy/pkg/types" ) +const ( + allReport = "all" + summaryReport = "summary" + + tableFormat = "table" + jsonFormat = "json" +) + +type Option struct { + Format string + Report string + Output io.Writer + Severities []dbTypes.Severity +} + // Report represents a kubernetes scan report type Report struct { SchemaVersion int `json:",omitempty"` @@ -37,6 +52,9 @@ type Resource struct { // Metadata Metadata `json:",omitempty"` Results types.Results `json:",omitempty"` Error string `json:",omitempty"` + + // original report + Report types.Report `json:"-"` } // Failed returns whether the k8s report includes any vulnerabilities or misconfigurations @@ -90,13 +108,17 @@ type Writer interface { } // write writes the results in the give format -func write(report Report, option report.Option, severities []dbTypes.Severity) error { +func write(report Report, option Option) error { var writer Writer switch option.Format { - case "all": - writer = &JSONWriter{Output: option.Output} - case "summary": - writer = NewSummaryWriter(option.Output, severities) + case jsonFormat: + writer = &JSONWriter{Output: option.Output, Report: option.Report} + case tableFormat: + writer = &TableWriter{ + Output: option.Output, + Report: option.Report, + Severities: option.Severities, + } default: return xerrors.Errorf("unknown format: %v", option.Format) } @@ -121,6 +143,7 @@ func createResource(artifact *artifacts.Artifact, report types.Report, err error Kind: artifact.Kind, Name: artifact.Name, Results: results, + Report: report, } // if there was any error during the scan diff --git a/pkg/k8s/run.go b/pkg/k8s/run.go index 1e55f482e7..9da95d6104 100644 --- a/pkg/k8s/run.go +++ b/pkg/k8s/run.go @@ -3,13 +3,13 @@ package k8s import ( "context" "errors" + "strings" "github.com/urfave/cli/v2" "golang.org/x/xerrors" cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" "github.com/aquasecurity/trivy/pkg/log" - pkgReport "github.com/aquasecurity/trivy/pkg/report" "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" @@ -17,12 +17,36 @@ import ( ) // Run runs scan on kubernetes cluster + func Run(cliCtx *cli.Context) error { opt, err := cmd.InitOption(cliCtx) if err != nil { return xerrors.Errorf("option error: %w", err) } + // Full-cluster scanning with '--format table' without explicit '--report all' is not allowed so that it won't mess up user's terminal. + // To show all the results, user needs to specify "--report all" explicitly + // even though the default value of "--report" is "all". + // + // e.g. $ trivy k8s --report all + // + // Or they can use "--format json" with implicit "--report all". + // + // e.g. $ trivy k8s --format json // All the results are shown in JSON + // + // Single resource scanning is allowed with implicit "--report all". + // + // e.g. $ trivy k8s pod myapp + if cliCtx.String("report") == allReport && + !cliCtx.IsSet("report") && + cliCtx.String("format") == tableFormat && + !cliCtx.Args().Present() { + + m := "All the results in the table format can mess up your terminal. Use \"--report all\" to tell Trivy to output it to your terminal anyway, or consider \"--report summary\" to show the summary output." + + return xerrors.New(m) + } + ctx, cancel := context.WithTimeout(cliCtx.Context, opt.Timeout) defer cancel() @@ -39,7 +63,11 @@ func Run(cliCtx *cli.Context) error { } return xerrors.Errorf("init error: %w", err) } - defer runner.Close() + defer func() { + if err := runner.Close(); err != nil { + log.Logger.Errorf("failed to close runner: %s", err) + } + }() cluster, err := k8s.GetCluster() if err != nil { @@ -47,7 +75,7 @@ func Run(cliCtx *cli.Context) error { } // get kubernetes scannable artifacts - artifacts, err := getArtifacts(ctx, cluster, opt.KubernetesOption.Namespace) + artifacts, err := getArtifacts(ctx, cliCtx.Args(), cluster, opt.KubernetesOption.Namespace) if err != nil { return xerrors.Errorf("get k8s artifacts error: %w", err) } @@ -67,10 +95,12 @@ func run(ctx context.Context, s *scanner, opt cmd.Option, artifacts []*artifacts return xerrors.Errorf("k8s scan error: %w", err) } - if err = write(report, pkgReport.Option{ - Format: opt.KubernetesOption.ReportFormat, - Output: opt.Output, - }, opt.Severities); err != nil { + if err = write(report, Option{ + Format: opt.Format, + Report: opt.KubernetesOption.ReportFormat, + Output: opt.Output, + Severities: opt.Severities, + }); err != nil { return xerrors.Errorf("unable to write results: %w", err) } @@ -79,6 +109,44 @@ func run(ctx context.Context, s *scanner, opt cmd.Option, artifacts []*artifacts return nil } -func getArtifacts(ctx context.Context, cluster k8s.Cluster, namespace string) ([]*artifacts.Artifact, error) { - return trivyk8s.New(cluster).Namespace(namespace).ListArtifacts(ctx) +func getArtifacts(ctx context.Context, args cli.Args, cluster k8s.Cluster, namespace string) ([]*artifacts.Artifact, error) { + trivyk8s := trivyk8s.New(cluster) + + if !args.Present() { + return trivyk8s.Namespace(namespace).ListArtifacts(ctx) + } + + // if scanning single resource, and namespace is empty + // uses default namespace + if len(namespace) == 0 { + namespace = cluster.GetCurrentNamespace() + } + + kind, name, err := extractKindAndName(args) + if err != nil { + return nil, err + } + + artifact, err := trivyk8s.Namespace(namespace).GetArtifact(ctx, kind, name) + if err != nil { + return nil, err + } + + return []*artifacts.Artifact{artifact}, nil +} + +func extractKindAndName(args cli.Args) (string, string, error) { + switch args.Len() { + case 1: + s := strings.Split(args.Get(0), "/") + if len(s) != 2 { + return "", "", xerrors.Errorf("can't parse arguments: %v", args.Slice()) + } + + return s[0], s[1], nil + case 2: + return args.Get(0), args.Get(1), nil + } + + return "", "", xerrors.Errorf("can't parse arguments: %v", args.Slice()) } diff --git a/pkg/k8s/scanner.go b/pkg/k8s/scanner.go index 0cec0ed69c..572488ed86 100644 --- a/pkg/k8s/scanner.go +++ b/pkg/k8s/scanner.go @@ -123,5 +123,4 @@ func (s *scanner) filter(ctx context.Context, report types.Report, artifact *art } return createResource(artifact, report, nil), nil - } diff --git a/pkg/k8s/table.go b/pkg/k8s/table.go new file mode 100644 index 0000000000..8310adedd3 --- /dev/null +++ b/pkg/k8s/table.go @@ -0,0 +1,41 @@ +package k8s + +import ( + "io" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + pkgReport "github.com/aquasecurity/trivy/pkg/report" +) + +type TableWriter struct { + Report string + Output io.Writer + Severities []dbTypes.Severity +} + +func (tw TableWriter) Write(report Report) error { + switch tw.Report { + case allReport: + t := pkgReport.TableWriter{Output: tw.Output, Severities: tw.Severities} + for _, r := range report.Vulnerabilities { + if r.Report.Results.Failed() { + err := t.Write(r.Report) + if err != nil { + return err + } + } + } + for _, r := range report.Misconfigurations { + if r.Report.Results.Failed() { + err := t.Write(r.Report) + if err != nil { + return err + } + } + } + case summaryReport: + writer := NewSummaryWriter(tw.Output, tw.Severities) + return writer.Write(report) + } + return nil +}