From c7472a2dda8ca4371b69bc076cdecf1ea2321b9a Mon Sep 17 00:00:00 2001 From: Vishwas R <30438425+vrajashkr@users.noreply.github.com> Date: Wed, 13 Mar 2024 02:08:48 +0530 Subject: [PATCH] feat: add verbose mode for cves for image listing (#2308) Signed-off-by: Vishwas Rajashekar --- pkg/cli/client/cve_cmd_internal_test.go | 37 +++++ pkg/cli/client/search_functions.go | 10 +- .../client/search_functions_internal_test.go | 126 +++++++++++++++++- pkg/cli/client/service.go | 50 ++++++- 4 files changed, 214 insertions(+), 9 deletions(-) diff --git a/pkg/cli/client/cve_cmd_internal_test.go b/pkg/cli/client/cve_cmd_internal_test.go index 028fe229..1a97271f 100644 --- a/pkg/cli/client/cve_cmd_internal_test.go +++ b/pkg/cli/client/cve_cmd_internal_test.go @@ -213,6 +213,43 @@ func TestSearchCVECmd(t *testing.T) { So(err, ShouldBeNil) }) + Convey("Test CVE by image name - in text format - in verbose mode", t, func() { + args := []string{"list", "dummyImageName:tag", "--url", baseURL, "--verbose"} + configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) + defer os.Remove(configPath) + cveCmd := NewCVECommand(new(mockService)) + buff := bytes.NewBufferString("") + cveCmd.SetOut(buff) + cveCmd.SetErr(buff) + cveCmd.SetArgs(args) + err := cveCmd.Execute() + + outputLines := strings.Split(buff.String(), "\n") + expected := []string{ + "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1", + "", + "dummyCVEID", + "Severity: HIGH", + "Title: Title of that CVE", + "Description:", + "Description of the CVE", + "", + "Vulnerable Packages:", + " Package Name: packagename", + " Package Path: ", + " Installed Version: installedver", + " Fixed Version: fixedver", + "", + "", + } + + for index, expectedLine := range expected { + So(outputLines[index], ShouldEqual, expectedLine) + } + + So(err, ShouldBeNil) + }) + Convey("Test CVE by image name - in json format", t, func() { args := []string{"list", "dummyImageName:tag", "--url", baseURL, "-f", "json"} configPath := makeConfigFile(`{"configs":[{"_name":"cvetest","showspinner":false}]}`) diff --git a/pkg/cli/client/search_functions.go b/pkg/cli/client/search_functions.go index a4f2fca9..5b9c55fc 100644 --- a/pkg/cli/client/search_functions.go +++ b/pkg/cli/client/search_functions.go @@ -253,11 +253,13 @@ func SearchCVEForImageGQL(config SearchConfig, image, searchedCveID string) erro fmt.Fprint(config.ResultWriter, statsStr) - printCVETableHeader(&builder) - fmt.Fprint(config.ResultWriter, builder.String()) + if !config.Verbose { + printCVETableHeader(&builder) + fmt.Fprint(config.ResultWriter, builder.String()) + } } - out, err := cveList.string(config.OutputFormat) + out, err := cveList.string(config.OutputFormat, config.Verbose) if err != nil { return err } @@ -303,7 +305,7 @@ func SearchCVEDiffList(config SearchConfig, minuend, subtrahend ImageIdentifier) fmt.Fprint(config.ResultWriter, builder.String()) } - out, err := result.string(config.OutputFormat) + out, err := result.string(config.OutputFormat, config.Verbose) if err != nil { return err } diff --git a/pkg/cli/client/search_functions_internal_test.go b/pkg/cli/client/search_functions_internal_test.go index 36683b05..8d54f69c 100644 --- a/pkg/cli/client/search_functions_internal_test.go +++ b/pkg/cli/client/search_functions_internal_test.go @@ -322,7 +322,7 @@ func TestSearchImagesForDigestGQL(t *testing.T) { } func TestSearchCVEForImageGQL(t *testing.T) { - Convey("SearchCVEForImageGQL", t, func() { + Convey("SearchCVEForImageGQL normal mode", t, func() { buff := bytes.NewBufferString("") searchConfig := getMockSearchConfig(buff, mockService{ getCveByImageGQLFn: func(ctx context.Context, config SearchConfig, username string, password string, @@ -403,6 +403,130 @@ func TestSearchCVEForImageGQL(t *testing.T) { } }) + Convey("SearchCVEForImageGQL verbose mode", t, func() { + buff := bytes.NewBufferString("") + searchConfig := getMockSearchConfig(buff, mockService{ + getCveByImageGQLFn: func(ctx context.Context, config SearchConfig, username string, password string, + imageName string, searchedCVE string) (*cveResult, error, + ) { + return &cveResult{ + Data: cveData{ + CVEListForImage: cveListForImage{ + CVEList: []cve{ + { + ID: "CVE-100", + Description: "", + Title: "CVE-100 Title", + Severity: "HIGH", + PackageList: []packageList{}, + }, + { + ID: "CVE-101", + Description: "Desc 101\n", + Title: "CVE-101 Title", + Severity: "HIGH", + PackageList: []packageList{ + { + Name: "Pkg1", + FixedVersion: "2.0.0", + InstalledVersion: "1.0.0", + }, + }, + }, + { + ID: "CVE-102", + Description: "Desc 102", + Title: "CVE-102 Title", + Severity: "HIGH", + PackageList: []packageList{ + { + Name: "dummy-java", + PackagePath: "/usr/bin/dummy.jar", + FixedVersion: "4.0.0", + InstalledVersion: "3.0.0", + }, + { + Name: "dummy-ruby", + PackagePath: "/usr/bin/dummy.gem", + FixedVersion: "5.0.0", + InstalledVersion: "1.0.0", + }, + }, + }, + }, + Summary: common.ImageVulnerabilitySummary{ + Count: 3, + UnknownCount: 0, + LowCount: 0, + MediumCount: 0, + HighCount: 3, + CriticalCount: 0, + MaxSeverity: "HIGH", + }, + }, + }, + }, nil + }, + }) + + searchConfig.Verbose = true + err := SearchCVEForImageGQL(searchConfig, "repo-test", "dummyCVEID") + So(err, ShouldBeNil) + bufferContent := buff.String() + bufferLines := strings.Split(bufferContent, "\n") + + // Expected result - each row indicates a line in the output + expected := []string{ + "CRITICAL 0, HIGH 3, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 3", + "", + "CVE-100", + "Severity: HIGH", + "Title: CVE-100 Title", + "Description:", + "Not Specified", + "", + "Vulnerable Packages:", + "No Vulnerable Packages", + "", + "", + "CVE-101", + "Severity: HIGH", + "Title: CVE-101 Title", + "Description:", + "Desc 101", + "", + "Vulnerable Packages:", + " Package Name: Pkg1", + " Package Path: ", + " Installed Version: 1.0.0", + " Fixed Version: 2.0.0", + "", + "", + "CVE-102", + "Severity: HIGH", + "Title: CVE-102 Title", + "Description:", + "Desc 102", + "", + "Vulnerable Packages:", + " Package Name: dummy-java", + " Package Path: /usr/bin/dummy.jar", + " Installed Version: 3.0.0", + " Fixed Version: 4.0.0", + "", + " Package Name: dummy-ruby", + " Package Path: /usr/bin/dummy.gem", + " Installed Version: 1.0.0", + " Fixed Version: 5.0.0", + "", + "", + } + + for index, expectedLine := range expected { + So(bufferLines[index], ShouldEqual, expectedLine) + } + }) + Convey("SearchCVEForImageGQL with injected error", t, func() { buff := bytes.NewBufferString("") searchConfig := getMockSearchConfig(buff, mockService{ diff --git a/pkg/cli/client/service.go b/pkg/cli/client/service.go index 2b406a02..48ee82d6 100644 --- a/pkg/cli/client/service.go +++ b/pkg/cli/client/service.go @@ -817,10 +817,19 @@ type cveData struct { CVEListForImage cveListForImage `json:"cveListForImage"` } -func (cve cveResult) string(format string) (string, error) { +func (cve cveResult) string(format string, verbose bool) (string, error) { switch strings.ToLower(format) { case "", defaultOutputFormat: - return cve.stringPlainText() + { + var out string + if verbose { + out = cve.stringPlainTextDetailed() + } else { + out = cve.stringPlainText() + } + + return out, nil + } case jsonFormat: return cve.stringJSON() case ymlFormat, yamlFormat: @@ -830,7 +839,40 @@ func (cve cveResult) string(format string) (string, error) { } } -func (cve cveResult) stringPlainText() (string, error) { +func (cve cveResult) stringPlainTextDetailed() string { + var builder strings.Builder + + for _, cveListItem := range cve.Data.CVEListForImage.CVEList { + cveDesc := strings.TrimSpace(cveListItem.Description) + if len(cveDesc) == 0 { + cveDesc = "Not Specified" + } + cveMetaData := fmt.Sprintf( + "%s\nSeverity: %s\nTitle: %s\nDescription:\n%s\n\n", + cveListItem.ID, cveListItem.Severity, cveListItem.Title, cveDesc, + ) + fmt.Fprint(&builder, cveMetaData) + fmt.Fprint(&builder, "Vulnerable Packages:\n") + + for _, pkg := range cveListItem.PackageList { + pkgMetaData := fmt.Sprintf( + " Package Name: %s\n Package Path: %s\n Installed Version: %s\n Fixed Version: %s\n\n", + pkg.Name, pkg.PackagePath, pkg.InstalledVersion, pkg.FixedVersion, + ) + fmt.Fprint(&builder, pkgMetaData) + } + + if len(cveListItem.PackageList) == 0 { + fmt.Fprintf(&builder, "No Vulnerable Packages\n\n") + } + + fmt.Fprint(&builder, "\n") + } + + return builder.String() +} + +func (cve cveResult) stringPlainText() string { var builder strings.Builder table := getCVETableWriter(&builder) @@ -849,7 +891,7 @@ func (cve cveResult) stringPlainText() (string, error) { table.Render() - return builder.String(), nil + return builder.String() } func (cve cveResult) stringJSON() (string, error) {