From 7e7191ab1aa4b765081c91573df307d5c9113f9c Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Mon, 6 Jul 2020 16:21:22 -0600 Subject: [PATCH] feat(cli): new vulnerability evaluation command Here is a quick summary, with suppressed help output, of the changes to the vulnerability commands: Main Vulnerability Command: ``` $ lacework vulnerability -h Available Commands: evaluation review evaluations from assessed container images report (deprecated) use 'evaluation show ' instead scan manage on-demand vulnerability scans ``` Vulnerability Evaluation Sub-Command: ``` $ lacework vulnerability evaluation -h Available Commands: list list all evaluations from a time range (default last 7 days) show show results of a container image evaluation ``` Vulnerability Scan Sub-Command: ``` $ lacework vulnerability scan -h Available Commands: run request an on-demand vulnerability scan show return results about an on-demand vulnerability scan ``` (DEPRECATED) Vulnerability Report Sub-Command: ``` $ lacework vulnerability report -h (DEPRECATED) This command has been moved, use now the following command: $ lacework vulnerability evaluation show ``` Adds Deprecations https://github.com/lacework/go-sdk/issues/162 Closes https://github.com/lacework/go-sdk/issues/156 Signed-off-by: Salim Afiune Maya --- api/vulnerabilities.go | 1 - cli/cmd/event.go | 27 +--- cli/cmd/flags.go | 50 ++++++ cli/cmd/vulnerability.go | 318 ++++++++++++++++++++++++++++++--------- integration/help_test.go | 2 +- 5 files changed, 298 insertions(+), 100 deletions(-) create mode 100644 cli/cmd/flags.go diff --git a/api/vulnerabilities.go b/api/vulnerabilities.go index e728a1905..fdf01a1e3 100644 --- a/api/vulnerabilities.go +++ b/api/vulnerabilities.go @@ -271,7 +271,6 @@ func (svc *VulnerabilitiesService) ListEvaluationsDateRange(start, end time.Time start.UTC().Format(time.RFC3339), end.UTC().Format(time.RFC3339), ) - fmt.Println(apiPath) err = svc.client.RequestDecoder("GET", apiPath, nil, &response) return } diff --git a/cli/cmd/event.go b/cli/cmd/event.go index f04a4363d..f5049c48e 100644 --- a/cli/cmd/event.go +++ b/cli/cmd/event.go @@ -68,7 +68,7 @@ time range.`, err error ) if eventsCmdState.Start != "" || eventsCmdState.End != "" { - start, end, errT := parseStartAndEndTime() + start, end, errT := parseStartAndEndTime(eventsCmdState.Start, eventsCmdState.End) if errT != nil { return errors.Wrap(errT, "unable to parse time range") } @@ -899,28 +899,3 @@ func eventMachineEntitiesTable(machines []api.EventMachineEntity) string { return r.String() } - -// parse the start and end time provided by the user -func parseStartAndEndTime() (start time.Time, end time.Time, err error) { - if eventsCmdState.Start == "" { - err = errors.New("when providing an end time, start time should be provided (--start)") - return - } - start, err = time.Parse(time.RFC3339, eventsCmdState.Start) - if err != nil { - err = errors.Wrap(err, "unable to parse start time") - return - } - - if eventsCmdState.End == "" { - err = errors.New("when providing a start time, end time should be provided (--end)") - return - } - end, err = time.Parse(time.RFC3339, eventsCmdState.End) - if err != nil { - err = errors.Wrap(err, "unable to parse end time") - return - } - - return -} diff --git a/cli/cmd/flags.go b/cli/cmd/flags.go new file mode 100644 index 000000000..0f71b4d83 --- /dev/null +++ b/cli/cmd/flags.go @@ -0,0 +1,50 @@ +// +// Author:: Salim Afiune Maya () +// Copyright:: Copyright 2020, Lacework Inc. +// License:: Apache License, Version 2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package cmd + +import ( + "time" + + "github.com/pkg/errors" +) + +// parse the start and end time provided by the user +func parseStartAndEndTime(s, e string) (start time.Time, end time.Time, err error) { + if s == "" { + err = errors.New("when providing an end time, start time should be provided (--start)") + return + } + start, err = time.Parse(time.RFC3339, s) + if err != nil { + err = errors.Wrap(err, "unable to parse start time") + return + } + + if e == "" { + err = errors.New("when providing a start time, end time should be provided (--end)") + return + } + end, err = time.Parse(time.RFC3339, e) + if err != nil { + err = errors.Wrap(err, "unable to parse end time") + return + } + + return +} diff --git a/cli/cmd/vulnerability.go b/cli/cmd/vulnerability.go index db6c5098a..dbfae2646 100644 --- a/cli/cmd/vulnerability.go +++ b/cli/cmd/vulnerability.go @@ -45,7 +45,7 @@ var ( // when enabled we tread the provided sha256 hash as image id ImageID bool - // display extended details about a vulnerability scan/report + // display extended details about a vulnerability scan/evaluation Details bool // display only fixable vulnerabilities @@ -53,15 +53,21 @@ var ( // show a list of packages by number of CVEs Packages bool + + // start time for listing evaluations + Start string + + // end time for listing evaluations + End string }{PollInterval: time.Second * 5} // vulnerability represents the vulnerability command vulnerabilityCmd = &cobra.Command{ Use: "vulnerability", - Aliases: []string{"vul"}, - Short: "view vulnerability reports and run on-demand scans", + Aliases: []string{"vuln", "vul"}, + Short: "view vulnerability evaluations and run on-demand scans", Long: ` -Request on-demand vulnerability scans and vizualize previous reports +Request on-demand vulnerability scans and vizualize previous evaluations from published images. (*) PREREQUISITE: Your Lacework account should already be configured @@ -83,7 +89,7 @@ Then navigate to Settings > Integrations > Container Registry.`, vulScanCmd = &cobra.Command{ Use: "scan", Short: "manage on-demand vulnerability scans", - Long: `Request on-demand vulnerability scans and view the generated report. + Long: `Request on-demand vulnerability scans and view the generated evaluations. NOTE: Scans can take up to 15 minutes to return results.`, } @@ -156,13 +162,13 @@ Arguments: return pollScanStatus(args[0]) } - report, err, scanning := checkScanStatus(args[0]) + results, err, scanning := checkScanStatus(args[0]) if err != nil { return err } if cli.JSONOutput() { - return cli.OutputJSON(report) + return cli.OutputJSON(results) } // if the scan is still running, display a nice message @@ -175,85 +181,105 @@ Arguments: return nil } - cli.OutputHuman(buildVulnerabilityReport(report)) + cli.OutputHuman(buildVulnerabilityReport(results)) return nil }, } - // vulReportCmd represents the report sub-command inside the vulnerability command + // + // TODO: @afiune marking this command as DEPRECATED. + // TO-BE-REMOVED with issue https://github.com/lacework/go-sdk/issues/162 vulReportCmd = &cobra.Command{ Use: "report ", - Short: "show vulnerability reports of a container image", - Long: `Review vulnerability reports from container image scans that run previously either -by the periodic scan mechanism that Lacework runs every hour, or a requested -on-demand vulnerability scan. - -Arguments: - a sha256 hash of a container image (format: sha256:1ee...1d3b) + Short: "(deprecated) use 'evaluation show ' instead", + Long: `(DEPRECATED) This command has been moved, use now the following command: -By default, this command treads the provided sha256 as image digest, when trying to -lookup a report by its image id, provided the flag '--image_id'. + $ lacework vulnerability evaluation show `, Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + cli.OutputHuman("(DEPRECATED) This command has been moved.\n") + cli.OutputHuman("(DEPRECATED) Use now the command 'lacework vulnerability evaluation show %s'\n\n", args[0]) + return showEvaluationWithSha256(args[0]) + }, + } + + // vulEvaluationCmd represents the evaluation sub-command inside the vulnerability command + vulEvaluationCmd = &cobra.Command{ + Use: "evaluation", + Aliases: []string{"evaluations", "eval"}, + Short: "review evaluations from assessed container images", + Long: `Review evaluations from container image scans that run previously either +by the periodic scan mechanism that Lacework runs every hour, or a requested +on-demand vulnerability scan. +`, + } + + vulEvaluationListCmd = &cobra.Command{ + Use: "list", + Short: "list all evaluations from a time range (default last 7 days)", + Long: `List of images evaluated by the Lacework container vulnerability assessments +during the specified time range, by default this command displays the +evaluations from the last 7 days, but it is possible to specify a different +time range.`, + Args: cobra.NoArgs, RunE: func(_ *cobra.Command, args []string) error { var ( - report api.VulContainerReportResponse - searchField string - err error + response api.VulContainerEvaluationsResponse + err error ) - if vulCmdState.ImageID { - searchField = "image_id" - cli.Log.Debugw("retrieve vulnerability report", searchField, args[0]) - report, err = cli.LwApi.Vulnerabilities.ReportFromID(args[0]) + if vulCmdState.Start != "" || vulCmdState.End != "" { + start, end, errT := parseStartAndEndTime(vulCmdState.Start, vulCmdState.End) + if errT != nil { + return errors.Wrap(errT, "unable to parse time range") + } + + cli.Log.Infow("requesting list of evaluations from custom time range", + "start_time", start, "end_time", end, + ) + response, err = cli.LwApi.Vulnerabilities.ListEvaluationsDateRange(start, end) } else { - searchField = "digest" - cli.Log.Debugw("retrieve vulnerability report", searchField, args[0]) - report, err = cli.LwApi.Vulnerabilities.ReportFromDigest(args[0]) + cli.Log.Info("requesting list of evaluations from the last 7 days") + response, err = cli.LwApi.Vulnerabilities.ListEvaluations() } + if err != nil { - return errors.Wrap(err, "unable to show vulnerability report") + return errors.Wrap(err, "unable to get evaluations") } - cli.Log.Debugw("vulnerability report", "details", report) - status := report.CheckStatus() - switch status { - case "Success": - if cli.JSONOutput() { - return cli.OutputJSON(report.Data) - } + cli.Log.Debugw("evaluations", "raw", response) + // Sort the evaluations from the response by date + sort.Slice(response.Evaluations, func(i, j int) bool { + return response.Evaluations[i].StartTime.ToTime().After(response.Evaluations[j].StartTime.ToTime()) + }) - cli.OutputHuman(buildVulnerabilityReport(&report.Data)) - case "Unsupported": - return errors.Errorf( - `unable to retrieve report for the provided container image. (unsupported distribution) + if cli.JSONOutput() { + return cli.OutputJSON(response.Evaluations) + } -For more information about supported distributions, visit: - https://support.lacework.com/hc/en-us/articles/360035472393-Container-Vulnerability-Assessment-Overview -`, - ) - case "NotFound": - msg := fmt.Sprintf( - "unable to find any container vulnerability report with %s '%s'", - searchField, args[0], - ) + cli.OutputHuman(vulEvaluationsToTableReport(response.Evaluations)) + return nil + }, + } - // add a suggestion to the user in regards of the image_id vs digest - if !vulCmdState.ImageID { - msg = fmt.Sprintf("%s\n\n(?) Are you trying to lookup a report using an image id? Try adding '--image_id'", msg) - } + vulEvaluationShowCmd = &cobra.Command{ + Use: "show ", + Short: "show results of a container image evaluation", + Long: `Review the results from an evaluation of a container image. - return errors.New(msg) - case "Failed": - return errors.New( - "the vulnerability report failed to execute. Use '--debug' to troubleshoot.", - ) - default: - return errors.New( - "unable to get status from vulnerability report. Use '--debug' to troubleshoot.", - ) - } +Arguments: + a sha256 hash of a container image (format: sha256:1ee...1d3b) - return nil +By default, this command treads the provided sha256 as image digest, when trying to +lookup an evaluation by its image id, provided the flag '--image_id'. + +To request an on-demand vulnerability scan: + + $ lacework vulnerability scan run +`, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + return showEvaluationWithSha256(args[0]) }, } ) @@ -264,12 +290,26 @@ func init() { // add sub-commands to the vulnerability command vulnerabilityCmd.AddCommand(vulScanCmd) - vulnerabilityCmd.AddCommand(vulReportCmd) + vulnerabilityCmd.AddCommand(vulReportCmd) // TO-BE-REMOVED + vulnerabilityCmd.AddCommand(vulEvaluationCmd) // add sub-commands to the 'vulnerability scan' command vulScanCmd.AddCommand(vulScanRunCmd) vulScanCmd.AddCommand(vulScanShowCmd) + // add sub-commands to the 'vulnerability evaluation' command + vulEvaluationCmd.AddCommand(vulEvaluationListCmd) + vulEvaluationCmd.AddCommand(vulEvaluationShowCmd) + + // add start flag to evaluation list command + vulEvaluationListCmd.Flags().StringVar(&vulCmdState.Start, + "start", "", "start of the time range in UTC (format: yyyy-MM-ddTHH:mm:ssZ)", + ) + // add end flag to evaluation list command + vulEvaluationListCmd.Flags().StringVar(&vulCmdState.End, + "end", "", "end of the time range in UTC (format: yyyy-MM-ddTHH:mm:ssZ)", + ) + setPollFlag( vulScanRunCmd.Flags(), vulScanShowCmd.Flags(), @@ -278,25 +318,95 @@ func init() { setDetailsFlag( vulScanRunCmd.Flags(), vulScanShowCmd.Flags(), - vulReportCmd.Flags(), + vulReportCmd.Flags(), // TO-BE-REMOVED + vulEvaluationShowCmd.Flags(), ) setFixableFlag( vulScanRunCmd.Flags(), vulScanShowCmd.Flags(), - vulReportCmd.Flags(), + vulReportCmd.Flags(), // TO-BE-REMOVED + vulEvaluationShowCmd.Flags(), ) setPackagesFlag( vulScanRunCmd.Flags(), vulScanShowCmd.Flags(), - vulReportCmd.Flags(), + vulReportCmd.Flags(), // TO-BE-REMOVED + vulEvaluationShowCmd.Flags(), ) + // TO-BE-REMOVED vulReportCmd.Flags().BoolVar( &vulCmdState.ImageID, "image_id", false, "tread the provided sha256 hash as image id", ) + + vulEvaluationShowCmd.Flags().BoolVar( + &vulCmdState.ImageID, "image_id", false, + "tread the provided sha256 hash as image id", + ) +} + +func showEvaluationWithSha256(sha string) error { + var ( + evaluation api.VulContainerReportResponse + searchField string + err error + ) + if vulCmdState.ImageID { + searchField = "image_id" + cli.Log.Debugw("retrieve image evaluation", searchField, sha) + evaluation, err = cli.LwApi.Vulnerabilities.ReportFromID(sha) + } else { + searchField = "digest" + cli.Log.Debugw("retrieve image evaluation", searchField, sha) + evaluation, err = cli.LwApi.Vulnerabilities.ReportFromDigest(sha) + } + if err != nil { + return errors.Wrap(err, "unable to show vulnerability evaluation") + } + + cli.Log.Debugw("image evaluation", "details", evaluation) + status := evaluation.CheckStatus() + switch status { + case "Success": + if cli.JSONOutput() { + return cli.OutputJSON(evaluation.Data) + } + + cli.OutputHuman(buildVulnerabilityReport(&evaluation.Data)) + case "Unsupported": + return errors.Errorf( + `unable to retrieve evaluation for the provided container image. (unsupported distribution) + +For more information about supported distributions, visit: + https://support.lacework.com/hc/en-us/articles/360035472393-Container-Vulnerability-Assessment-Overview +`, + ) + case "NotFound": + msg := fmt.Sprintf( + "unable to find any evaluation from a container image with %s '%s'", + searchField, sha, + ) + + // add a suggestion to the user in regards of the image_id vs digest + if !vulCmdState.ImageID { + msg = fmt.Sprintf("%s\n\n(?) Are you trying to lookup an evaluation using an image id? Try adding '--image_id'", msg) + } + + return errors.New(msg) + case "Failed": + return errors.New( + "the evaluation failed to execute. Use '--debug' to troubleshoot.", + ) + default: + return errors.New( + "unable to get evaluation status from the container image. Use '--debug' to troubleshoot.", + ) + } + + return nil } func setPollFlag(cmds ...*flag.FlagSet) { @@ -334,7 +444,7 @@ func setDetailsFlag(cmds ...*flag.FlagSet) { for _, cmd := range cmds { if cmd != nil { cmd.BoolVar(&vulCmdState.Details, "details", false, - "increase details about the vulnerability report", + "increase details of an evaluation", ) } } @@ -344,7 +454,7 @@ func pollScanStatus(requestID string) error { cli.StartProgress(" Scan running...") for { - report, err, retry := checkScanStatus(requestID) + evaluation, err, retry := checkScanStatus(requestID) if err != nil { return err } @@ -355,11 +465,11 @@ func pollScanStatus(requestID string) error { } if cli.JSONOutput() { - return cli.OutputJSON(report) + return cli.OutputJSON(evaluation) } cli.StopProgress() - cli.OutputHuman(buildVulnerabilityReport(report)) + cli.OutputHuman(buildVulnerabilityReport(evaluation)) return nil } } @@ -658,3 +768,67 @@ func byteCountBinary(b int64) string { } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) } + +func vulEvaluationsToTableReport(evaluations []api.VulContainerEvaluation) string { + var ( + evaluationsTable = &strings.Builder{} + t = tablewriter.NewWriter(evaluationsTable) + ) + + t.SetHeader([]string{ + "Registry", + "Repository", + "Tags", + "Last Run", + "Status", + "Containers", + "Vulnerabilities", + "Image Digest", + }) + t.SetBorder(false) + t.AppendBulk(vulEvaluationsToTable(evaluations)) + t.Render() + + return evaluationsTable.String() +} + +func vulEvaluationsToTable(evaluations []api.VulContainerEvaluation) [][]string { + out := [][]string{} + for _, eval := range evaluations { + out = append(out, []string{ + eval.ImageRegistry, + eval.ImageRepo, + strings.Join(eval.ImageTags, ","), + eval.StartTime.UTC().Format(time.RFC3339), + eval.ImageScanStatus, + eval.NdvContainers, + vulSummaryFromEvaluation(&eval), + eval.ImageDigest, + }) + } + return out +} + +func vulSummaryFromEvaluation(eval *api.VulContainerEvaluation) string { + summary := []string{} + + summary = addToEvaluationSummary(summary, eval.NumVulnerabilitiesSeverity1, "Critical") + summary = addToEvaluationSummary(summary, eval.NumVulnerabilitiesSeverity2, "High") + summary = addToEvaluationSummary(summary, eval.NumVulnerabilitiesSeverity3, "Medium") + summary = addToEvaluationSummary(summary, eval.NumVulnerabilitiesSeverity4, "Low") + summary = addToEvaluationSummary(summary, eval.NumVulnerabilitiesSeverity5, "Info") + + if eval.NumFixes != "" { + summary = append(summary, fmt.Sprintf("%s Fixable", eval.NumFixes)) + } + return strings.Join(summary, " ") +} + +func addToEvaluationSummary(text []string, num, severity string) []string { + if len(text) == 0 { + if num != "" && num != "0" { + return append(text, fmt.Sprintf("%s %s", num, severity)) + } + } + return text +} diff --git a/integration/help_test.go b/integration/help_test.go index c2771b9b6..c8ecb3f90 100644 --- a/integration/help_test.go +++ b/integration/help_test.go @@ -142,7 +142,7 @@ Available Commands: event inspect Lacework events integration manage external integrations version print the Lacework CLI version - vulnerability view vulnerability reports and run on-demand scans + vulnerability view vulnerability evaluations and run on-demand scans Flags: -a, --account string account subdomain of URL (i.e. .lacework.net)