From 18aa975ae25663d482a89700309e0239a906da72 Mon Sep 17 00:00:00 2001 From: Andrei Aaron Date: Wed, 13 Dec 2023 19:16:31 +0200 Subject: [PATCH] feat(CVE): add CVE severity counters to returned images and CVE list calls (#2131) For CLI output is similar to: CRITICAL 0, HIGH 1, MEDIUM 1, LOW 0, UNKNOWN 0, TOTAL 2 ID SEVERITY TITLE CVE-2023-0464 HIGH openssl: Denial of service by excessive resou... CVE-2023-0465 MEDIUM openssl: Invalid certificate policies in leaf... Signed-off-by: Andrei Aaron --- pkg/cli/client/cve_cmd_internal_test.go | 14 +- pkg/cli/client/cve_cmd_test.go | 6 +- pkg/cli/client/image_cmd_internal_test.go | 29 +- pkg/cli/client/image_cmd_test.go | 39 +- pkg/cli/client/search_functions.go | 8 + .../client/search_functions_internal_test.go | 10 + pkg/cli/client/service.go | 11 +- pkg/common/model.go | 9 +- .../search/convert/convert_internal_test.go | 69 +++- pkg/extensions/search/convert/cve.go | 28 +- pkg/extensions/search/cve/cve.go | 93 +++-- pkg/extensions/search/cve/cve_test.go | 246 +++++++++++- pkg/extensions/search/cve/model/models.go | 9 +- pkg/extensions/search/cve/pagination_test.go | 117 +++++- .../search/gql_generated/generated.go | 373 +++++++++++++++++- .../search/gql_generated/models_gen.go | 12 + pkg/extensions/search/resolver.go | 12 +- pkg/extensions/search/schema.graphql | 25 ++ pkg/extensions/search/search_test.go | 97 +++-- pkg/test/mocks/cve_mock.go | 5 +- 20 files changed, 1077 insertions(+), 135 deletions(-) diff --git a/pkg/cli/client/cve_cmd_internal_test.go b/pkg/cli/client/cve_cmd_internal_test.go index 5728cbd9..0a59f9ac 100644 --- a/pkg/cli/client/cve_cmd_internal_test.go +++ b/pkg/cli/client/cve_cmd_internal_test.go @@ -141,7 +141,8 @@ func TestSearchCVECmd(t *testing.T) { err := cmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE") + So(strings.TrimSpace(str), ShouldEqual, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1 "+ + "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE") So(err, ShouldBeNil) }) @@ -207,7 +208,8 @@ func TestSearchCVECmd(t *testing.T) { err := cveCmd.Execute() space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") - So(strings.TrimSpace(str), ShouldEqual, "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE") + So(strings.TrimSpace(str), ShouldEqual, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1 "+ + "ID SEVERITY TITLE dummyCVEID HIGH Title of that CVE") So(err, ShouldBeNil) }) @@ -225,7 +227,9 @@ func TestSearchCVECmd(t *testing.T) { So(buff.String(), ShouldEqual, `{"Tag":"dummyImageName:tag","CVEList":`+ `[{"Id":"dummyCVEID","Severity":"HIGH","Title":"Title of that CVE",`+ `"Description":"Description of the CVE","PackageList":[{"Name":"packagename",`+ - `"InstalledVersion":"installedver","FixedVersion":"fixedver"}]}]}`+"\n") + `"InstalledVersion":"installedver","FixedVersion":"fixedver"}]}],"Summary":`+ + `{"maxSeverity":"HIGH","unknownCount":0,"lowCount":0,"mediumCount":0,"highCount":1,`+ + `"criticalCount":0,"count":1}}`+"\n") So(err, ShouldBeNil) }) @@ -243,7 +247,8 @@ func TestSearchCVECmd(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") So(strings.TrimSpace(str), ShouldEqual, `--- tag: dummyImageName:tag cvelist: - id: dummyCVEID`+ ` severity: HIGH title: Title of that CVE description: Description of the CVE packagelist: `+ - `- name: packagename installedversion: installedver fixedversion: fixedver`) + `- name: packagename installedversion: installedver fixedversion: fixedver `+ + `summary: maxseverity: HIGH unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 1 criticalcount: 0 count: 1`) So(err, ShouldBeNil) }) Convey("Test CVE by image name - invalid format", t, func() { @@ -508,6 +513,7 @@ func TestCVECommandGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1") So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") }) diff --git a/pkg/cli/client/cve_cmd_test.go b/pkg/cli/client/cve_cmd_test.go index d0876b28..4175a92a 100644 --- a/pkg/cli/client/cve_cmd_test.go +++ b/pkg/cli/client/cve_cmd_test.go @@ -634,7 +634,7 @@ func TestCVESort(t *testing.T) { str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) So(actual, ShouldResemble, - "ID SEVERITY TITLE "+ + "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ "CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+ "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ "CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+ @@ -652,7 +652,7 @@ func TestCVESort(t *testing.T) { str = space.ReplaceAllString(buff.String(), " ") actual = strings.TrimSpace(str) So(actual, ShouldResemble, - "ID SEVERITY TITLE "+ + "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ "CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat... "+ "CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+ "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ @@ -670,7 +670,7 @@ func TestCVESort(t *testing.T) { str = space.ReplaceAllString(buff.String(), " ") actual = strings.TrimSpace(str) So(actual, ShouldResemble, - "ID SEVERITY TITLE "+ + "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ "CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ... "+ "CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+ "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ diff --git a/pkg/cli/client/image_cmd_internal_test.go b/pkg/cli/client/image_cmd_internal_test.go index 2387ec61..cfbf36d3 100644 --- a/pkg/cli/client/image_cmd_internal_test.go +++ b/pkg/cli/client/image_cmd_internal_test.go @@ -384,11 +384,13 @@ func TestOutputFormat(t *testing.T) { `"lastUpdated":"0001-01-01T00:00:00Z","size":"123445","platform":{"os":"os","arch":"arch",`+ `"variant":""},"isSigned":false,"downloadCount":0,`+ `"layers":[{"size":"","digest":"sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6",`+ - `"score":0}],"history":null,"vulnerabilities":{"maxSeverity":"","count":0},`+ + `"score":0}],"history":null,"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,`+ + `"mediumCount":0,"highCount":0,"criticalCount":0,"count":0},`+ `"referrers":null,"artifactType":"","signatureInfo":null}],"size":"123445",`+ `"downloadCount":0,"lastUpdated":"0001-01-01T00:00:00Z","description":"","isSigned":false,"licenses":"",`+ `"labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",`+ - `"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}`+"\n") + `"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,"mediumCount":0,"highCount":0,`+ + `"criticalCount":0,"count":0},"referrers":null,"signatureInfo":null}`+"\n") So(err, ShouldBeNil) }) @@ -415,10 +417,13 @@ func TestOutputFormat(t *testing.T) { `lastupdated: 0001-01-01T00:00:00Z size: "123445" platform: os: os arch: arch variant: "" `+ `issigned: false downloadcount: 0 layers: - size: "" `+ `digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+ - `history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+ + `history: [] vulnerabilities: maxseverity: "" `+ + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 `+ + `referrers: [] artifacttype: "" `+ `signatureinfo: [] size: "123445" downloadcount: 0 `+ `lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+ `title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: "" `+ + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 `+ `count: 0 referrers: [] signatureinfo: []`, ) So(err, ShouldBeNil) @@ -449,11 +454,13 @@ func TestOutputFormat(t *testing.T) { `lastupdated: 0001-01-01T00:00:00Z size: "123445" platform: os: os arch: arch variant: "" `+ `issigned: false downloadcount: 0 layers: - size: "" `+ `digest: sha256:c122a146f0d02349be211bb95cc2530f4a5793f96edbdfa00860f741e5d8c0e6 score: 0 `+ - `history: [] vulnerabilities: maxseverity: "" count: 0 referrers: [] artifacttype: "" `+ + `history: [] vulnerabilities: maxseverity: "" unknowncount: 0 lowcount: 0 mediumcount: 0 `+ + `highcount: 0 criticalcount: 0 count: 0 referrers: [] artifacttype: "" `+ `signatureinfo: [] size: "123445" downloadcount: 0 `+ `lastupdated: 0001-01-01T00:00:00Z description: "" issigned: false licenses: "" labels: "" `+ - `title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: `+ - `"" count: 0 referrers: [] signatureinfo: []`, + `title: "" source: "" documentation: "" authors: "" vendor: "" vulnerabilities: maxseverity: "" `+ + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 `+ + `count: 0 referrers: [] signatureinfo: []`, ) So(err, ShouldBeNil) }) @@ -783,6 +790,7 @@ func TestImagesCommandGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1") So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") }) @@ -1342,6 +1350,15 @@ func (service mockService) getCveByImageGQL(ctx context.Context, config SearchCo }, }, }, + Summary: common.ImageVulnerabilitySummary{ + Count: 1, + UnknownCount: 0, + LowCount: 0, + MediumCount: 0, + HighCount: 1, + CriticalCount: 0, + MaxSeverity: "HIGH", + }, }, } diff --git a/pkg/cli/client/image_cmd_test.go b/pkg/cli/client/image_cmd_test.go index 496ff026..6c033c37 100644 --- a/pkg/cli/client/image_cmd_test.go +++ b/pkg/cli/client/image_cmd_test.go @@ -379,11 +379,13 @@ func TestOutputFormatGQL(t *testing.T) { `"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` + `"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` + `"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` + - `"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` + + `"history":null,"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,"mediumCount":0,` + + `"highCount":0,"criticalCount":0,"count":0},` + `"referrers":null,"artifactType":"","signatureInfo":null}],` + `"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` + `"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` + - `"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n" + + `"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,"mediumCount":0,` + + `"highCount":0,"criticalCount":0,"count":0},"referrers":null,"signatureInfo":null}` + "\n" + `{"repoName":"repo7","tag":"test:2.0",` + `"digest":"sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06",` + `"mediaType":"application/vnd.oci.image.manifest.v1+json",` + @@ -392,11 +394,13 @@ func TestOutputFormatGQL(t *testing.T) { `"lastUpdated":"2023-01-01T12:00:00Z","size":"528","platform":{"os":"linux","arch":"amd64",` + `"variant":""},"isSigned":false,"downloadCount":0,"layers":[{"size":"15","digest":` + `"sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6","score":0}],` + - `"history":null,"vulnerabilities":{"maxSeverity":"","count":0},` + + `"history":null,"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,"mediumCount":0,` + + `"highCount":0,"criticalCount":0,"count":0},` + `"referrers":null,"artifactType":"","signatureInfo":null}],` + `"size":"528","downloadCount":0,"lastUpdated":"2023-01-01T12:00:00Z","description":"","isSigned":false,` + `"licenses":"","labels":"","title":"","source":"","documentation":"","authors":"","vendor":"",` + - `"vulnerabilities":{"maxSeverity":"","count":0},"referrers":null,"signatureInfo":null}` + "\n" + `"vulnerabilities":{"maxSeverity":"","unknownCount":0,"lowCount":0,"mediumCount":0,` + + `"highCount":0,"criticalCount":0,"count":0},"referrers":null,"signatureInfo":null}` + "\n" // Output is supposed to be in json lines format, keep all spaces as is for verification So(buff.String(), ShouldEqual, expectedStr) So(err, ShouldBeNil) @@ -424,10 +428,13 @@ func TestOutputFormatGQL(t *testing.T) { `issigned: false downloadcount: 0 layers: - size: "15" ` + `digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` + `history: [] vulnerabilities: maxseverity: "" ` + - `count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] artifacttype: "" signatureinfo: [] ` + `size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` + `issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` + - `authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: [] ` + + `authors: "" vendor: "" vulnerabilities: maxseverity: "" ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] signatureinfo: [] ` + `--- reponame: repo7 tag: test:2.0 ` + `digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` + `mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` + @@ -437,10 +444,13 @@ func TestOutputFormatGQL(t *testing.T) { `issigned: false downloadcount: 0 layers: - size: "15" ` + `digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` + `history: [] vulnerabilities: maxseverity: "" ` + - `count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] artifacttype: "" signatureinfo: [] ` + `size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` + `issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` + - `authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []` + `authors: "" vendor: "" vulnerabilities: maxseverity: "" ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] signatureinfo: []` So(strings.TrimSpace(str), ShouldEqual, expectedStr) So(err, ShouldBeNil) }) @@ -467,11 +477,13 @@ func TestOutputFormatGQL(t *testing.T) { `issigned: false downloadcount: 0 layers: - size: "15" ` + `digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` + `history: [] vulnerabilities: maxseverity: "" ` + - `count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] artifacttype: "" signatureinfo: [] ` + `size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` + `issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` + `authors: "" vendor: "" vulnerabilities: maxseverity: "" ` + - `count: 0 referrers: [] signatureinfo: [] ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] signatureinfo: [] ` + `--- reponame: repo7 tag: test:2.0 ` + `digest: sha256:51e18f508fd7125b0831ff9a22ba74cd79f0b934e77661ff72cfb54896951a06 ` + `mediatype: application/vnd.oci.image.manifest.v1+json manifests: - ` + @@ -481,10 +493,13 @@ func TestOutputFormatGQL(t *testing.T) { `issigned: false downloadcount: 0 layers: - size: "15" ` + `digest: sha256:b8781e8844f5b7bf6f2f8fa343de18ec471c3b278027355bc34c120585ff04f6 score: 0 ` + `history: [] vulnerabilities: maxseverity: "" ` + - `count: 0 referrers: [] artifacttype: "" signatureinfo: [] ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] artifacttype: "" signatureinfo: [] ` + `size: "528" downloadcount: 0 lastupdated: 2023-01-01T12:00:00Z description: "" ` + `issigned: false licenses: "" labels: "" title: "" source: "" documentation: "" ` + - `authors: "" vendor: "" vulnerabilities: maxseverity: "" count: 0 referrers: [] signatureinfo: []` + `authors: "" vendor: "" vulnerabilities: maxseverity: "" ` + + `unknowncount: 0 lowcount: 0 mediumcount: 0 highcount: 0 criticalcount: 0 count: 0 ` + + `referrers: [] signatureinfo: []` So(strings.TrimSpace(str), ShouldEqual, expectedStr) So(err, ShouldBeNil) }) diff --git a/pkg/cli/client/search_functions.go b/pkg/cli/client/search_functions.go index 9cf5c34f..dc47b342 100644 --- a/pkg/cli/client/search_functions.go +++ b/pkg/cli/client/search_functions.go @@ -245,6 +245,14 @@ func SearchCVEForImageGQL(config SearchConfig, image, searchedCveID string) erro var builder strings.Builder if config.OutputFormat == defaultOutputFormat || config.OutputFormat == "" { + imageCVESummary := cveList.Data.CVEListForImage.Summary + + statsStr := fmt.Sprintf("CRITICAL %d, HIGH %d, MEDIUM %d, LOW %d, UNKNOWN %d, TOTAL %d\n\n", + imageCVESummary.CriticalCount, imageCVESummary.HighCount, imageCVESummary.MediumCount, + imageCVESummary.LowCount, imageCVESummary.UnknownCount, imageCVESummary.Count) + + fmt.Fprint(config.ResultWriter, statsStr) + printCVETableHeader(&builder) fmt.Fprint(config.ResultWriter, builder.String()) } diff --git a/pkg/cli/client/search_functions_internal_test.go b/pkg/cli/client/search_functions_internal_test.go index f8880e7e..4fcb6d15 100644 --- a/pkg/cli/client/search_functions_internal_test.go +++ b/pkg/cli/client/search_functions_internal_test.go @@ -346,6 +346,15 @@ func TestSearchCVEForImageGQL(t *testing.T) { }, }, }, + Summary: common.ImageVulnerabilitySummary{ + Count: 1, + UnknownCount: 0, + LowCount: 0, + MediumCount: 0, + HighCount: 1, + CriticalCount: 0, + MaxSeverity: "HIGH", + }, }, }, }, nil @@ -357,6 +366,7 @@ func TestSearchCVEForImageGQL(t *testing.T) { space := regexp.MustCompile(`\s+`) str := space.ReplaceAllString(buff.String(), " ") actual := strings.TrimSpace(str) + So(actual, ShouldContainSubstring, "CRITICAL 0, HIGH 1, MEDIUM 0, LOW 0, UNKNOWN 0, TOTAL 1") So(actual, ShouldContainSubstring, "dummyCVEID HIGH Title of that CVE") }) diff --git a/pkg/cli/client/service.go b/pkg/cli/client/service.go index a71ed759..9dbae1bb 100644 --- a/pkg/cli/client/service.go +++ b/pkg/cli/client/service.go @@ -305,10 +305,14 @@ func (service searchService) getCveByImageGQL(ctx context.Context, config Search query := fmt.Sprintf(` { CVEListForImage (image:"%s", searchedCVE:"%s", requestedPage: {sortBy: %s}) { - Tag CVEList { + Tag + CVEList { Id Title Severity Description PackageList {Name InstalledVersion FixedVersion} } + Summary { + Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity + } } }`, imageName, searchedCVE, Flag2SortCriteria(config.SortBy)) result := &cveResult{} @@ -743,8 +747,9 @@ type cve struct { //nolint:tagliatelle // graphQL schema type cveListForImage struct { - Tag string `json:"Tag"` - CVEList []cve `json:"CVEList"` + Tag string `json:"Tag"` + CVEList []cve `json:"CVEList"` + Summary common.ImageVulnerabilitySummary `json:"Summary"` } //nolint:tagliatelle // graphQL schema diff --git a/pkg/common/model.go b/pkg/common/model.go index cacfc1f5..6c0a7aef 100644 --- a/pkg/common/model.go +++ b/pkg/common/model.go @@ -84,8 +84,13 @@ type Platform struct { } type ImageVulnerabilitySummary struct { - MaxSeverity string `json:"maxSeverity"` - Count int `json:"count"` + MaxSeverity string `json:"maxSeverity"` + UnknownCount int `json:"unknownCount"` + LowCount int `json:"lowCount"` + MediumCount int `json:"mediumCount"` + HighCount int `json:"highCount"` + CriticalCount int `json:"criticalCount"` + Count int `json:"count"` } type LayerSummary struct { diff --git a/pkg/extensions/search/convert/convert_internal_test.go b/pkg/extensions/search/convert/convert_internal_test.go index d2305372..62826c3e 100644 --- a/pkg/extensions/search/convert/convert_internal_test.go +++ b/pkg/extensions/search/convert/convert_internal_test.go @@ -88,6 +88,11 @@ func TestCVEConvert(t *testing.T) { So(imageSummary.Vulnerabilities, ShouldNotBeNil) So(*imageSummary.Vulnerabilities.Count, ShouldEqual, 0) + So(*imageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(*imageSummary.Vulnerabilities.LowCount, ShouldEqual, 0) + So(*imageSummary.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(*imageSummary.Vulnerabilities.HighCount, ShouldEqual, 0) + So(*imageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(*imageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") So(graphql.GetErrors(ctx), ShouldBeNil) @@ -102,20 +107,35 @@ func TestCVEConvert(t *testing.T) { GetCVESummaryForImageMediaFn: func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) { return cvemodel.ImageCVESummary{ - Count: 1, - MaxSeverity: "HIGH", + Count: 30, + UnknownCount: 1, + LowCount: 2, + MediumCount: 3, + HighCount: 10, + CriticalCount: 14, + MaxSeverity: "HIGH", }, nil }, }, ) So(imageSummary.Vulnerabilities, ShouldNotBeNil) - So(*imageSummary.Vulnerabilities.Count, ShouldEqual, 1) + So(*imageSummary.Vulnerabilities.Count, ShouldEqual, 30) + So(*imageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 1) + So(*imageSummary.Vulnerabilities.LowCount, ShouldEqual, 2) + So(*imageSummary.Vulnerabilities.MediumCount, ShouldEqual, 3) + So(*imageSummary.Vulnerabilities.HighCount, ShouldEqual, 10) + So(*imageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 14) So(*imageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "HIGH") So(graphql.GetErrors(ctx), ShouldBeNil) So(len(imageSummary.Manifests), ShouldEqual, 1) So(imageSummary.Manifests[0].Vulnerabilities, ShouldNotBeNil) - So(*imageSummary.Manifests[0].Vulnerabilities.Count, ShouldEqual, 1) + So(*imageSummary.Manifests[0].Vulnerabilities.Count, ShouldEqual, 30) + So(*imageSummary.Manifests[0].Vulnerabilities.UnknownCount, ShouldEqual, 1) + So(*imageSummary.Manifests[0].Vulnerabilities.LowCount, ShouldEqual, 2) + So(*imageSummary.Manifests[0].Vulnerabilities.MediumCount, ShouldEqual, 3) + So(*imageSummary.Manifests[0].Vulnerabilities.HighCount, ShouldEqual, 10) + So(*imageSummary.Manifests[0].Vulnerabilities.CriticalCount, ShouldEqual, 14) So(*imageSummary.Manifests[0].Vulnerabilities.MaxSeverity, ShouldEqual, "HIGH") imageSummary.Vulnerabilities = nil @@ -152,8 +172,13 @@ func TestCVEConvert(t *testing.T) { GetCVESummaryForImageMediaFn: func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) { return cvemodel.ImageCVESummary{ - Count: 1, - MaxSeverity: "HIGH", + Count: 30, + UnknownCount: 1, + LowCount: 2, + MediumCount: 3, + HighCount: 10, + CriticalCount: 14, + MaxSeverity: "HIGH", }, nil }, }, @@ -182,15 +207,25 @@ func TestCVEConvert(t *testing.T) { GetCVESummaryForImageMediaFn: func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) { return cvemodel.ImageCVESummary{ - Count: 1, - MaxSeverity: "HIGH", + Count: 30, + UnknownCount: 1, + LowCount: 2, + MediumCount: 3, + HighCount: 10, + CriticalCount: 14, + MaxSeverity: "HIGH", }, nil }, }, ) So(repoSummary.NewestImage.Vulnerabilities, ShouldNotBeNil) - So(*repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 1) + So(*repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 30) + So(*repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 1) + So(*repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 2) + So(*repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 3) + So(*repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 10) + So(*repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 14) So(*repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "HIGH") So(graphql.GetErrors(ctx), ShouldBeNil) }) @@ -251,15 +286,25 @@ func TestCVEConvert(t *testing.T) { GetCVESummaryForImageMediaFn: func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) { return cvemodel.ImageCVESummary{ - Count: 1, - MaxSeverity: "HIGH", + Count: 30, + UnknownCount: 1, + LowCount: 2, + MediumCount: 3, + HighCount: 10, + CriticalCount: 14, + MaxSeverity: "HIGH", }, nil }, }, ) So(manifestSummary.Vulnerabilities, ShouldNotBeNil) - So(*manifestSummary.Vulnerabilities.Count, ShouldEqual, 1) + So(*manifestSummary.Vulnerabilities.Count, ShouldEqual, 30) + So(*manifestSummary.Vulnerabilities.UnknownCount, ShouldEqual, 1) + So(*manifestSummary.Vulnerabilities.LowCount, ShouldEqual, 2) + So(*manifestSummary.Vulnerabilities.MediumCount, ShouldEqual, 3) + So(*manifestSummary.Vulnerabilities.HighCount, ShouldEqual, 10) + So(*manifestSummary.Vulnerabilities.CriticalCount, ShouldEqual, 14) So(*manifestSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "HIGH") manifestSummary.Vulnerabilities = nil diff --git a/pkg/extensions/search/convert/cve.go b/pkg/extensions/search/convert/cve.go index ffda7208..282c0f1d 100644 --- a/pkg/extensions/search/convert/cve.go +++ b/pkg/extensions/search/convert/cve.go @@ -38,8 +38,13 @@ func updateImageSummaryVulnerabilities( imageCveSummary := cvemodel.ImageCVESummary{} imageSummary.Vulnerabilities = &gql_generated.ImageVulnerabilitySummary{ - MaxSeverity: &imageCveSummary.MaxSeverity, - Count: &imageCveSummary.Count, + MaxSeverity: &imageCveSummary.MaxSeverity, + UnknownCount: &imageCveSummary.UnknownCount, + LowCount: &imageCveSummary.LowCount, + MediumCount: &imageCveSummary.MediumCount, + HighCount: &imageCveSummary.HighCount, + CriticalCount: &imageCveSummary.CriticalCount, + Count: &imageCveSummary.Count, } // Check if vulnerability scanning is disabled @@ -61,6 +66,11 @@ func updateImageSummaryVulnerabilities( } imageSummary.Vulnerabilities.MaxSeverity = &imageCveSummary.MaxSeverity + imageSummary.Vulnerabilities.UnknownCount = &imageCveSummary.UnknownCount + imageSummary.Vulnerabilities.LowCount = &imageCveSummary.LowCount + imageSummary.Vulnerabilities.MediumCount = &imageCveSummary.MediumCount + imageSummary.Vulnerabilities.HighCount = &imageCveSummary.HighCount + imageSummary.Vulnerabilities.CriticalCount = &imageCveSummary.CriticalCount imageSummary.Vulnerabilities.Count = &imageCveSummary.Count for _, manifestSummary := range imageSummary.Manifests { @@ -82,8 +92,13 @@ func updateManifestSummaryVulnerabilities( imageCveSummary := cvemodel.ImageCVESummary{} manifestSummary.Vulnerabilities = &gql_generated.ImageVulnerabilitySummary{ - MaxSeverity: &imageCveSummary.MaxSeverity, - Count: &imageCveSummary.Count, + MaxSeverity: &imageCveSummary.MaxSeverity, + UnknownCount: &imageCveSummary.UnknownCount, + LowCount: &imageCveSummary.LowCount, + MediumCount: &imageCveSummary.MediumCount, + HighCount: &imageCveSummary.HighCount, + CriticalCount: &imageCveSummary.CriticalCount, + Count: &imageCveSummary.Count, } // Check if vulnerability scanning is disabled @@ -105,5 +120,10 @@ func updateManifestSummaryVulnerabilities( } manifestSummary.Vulnerabilities.MaxSeverity = &imageCveSummary.MaxSeverity + manifestSummary.Vulnerabilities.UnknownCount = &imageCveSummary.UnknownCount + manifestSummary.Vulnerabilities.LowCount = &imageCveSummary.LowCount + manifestSummary.Vulnerabilities.MediumCount = &imageCveSummary.MediumCount + manifestSummary.Vulnerabilities.HighCount = &imageCveSummary.HighCount + manifestSummary.Vulnerabilities.CriticalCount = &imageCveSummary.CriticalCount manifestSummary.Vulnerabilities.Count = &imageCveSummary.Count } diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index da2dcb71..c2899587 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -23,8 +23,8 @@ type CveInfo interface { GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixed(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE string, pageinput cvemodel.PageInput, - ) ([]cvemodel.CVE, zcommon.PageInfo, error) - GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string) (cvemodel.ImageCVESummary, error) + ) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) + GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string) (cvemodel.ImageCVESummary, error) } type Scanner interface { @@ -352,75 +352,67 @@ func filterCVEList(cveMap map[string]cvemodel.CVE, searchedCVE string, pageFinde func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref string, searchedCVE string, pageInput cvemodel.PageInput, ) ( - []cvemodel.CVE, zcommon.PageInfo, error, + []cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error, ) { + imageCVESummary := cvemodel.ImageCVESummary{ + MaxSeverity: cvemodel.SeverityNotScanned, + } + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(repo, ref) if !isValidImage { cveinfo.Log.Debug().Str("image", repo+":"+ref).Err(err).Msg("image is not scanable") - return []cvemodel.CVE{}, zcommon.PageInfo{}, err + return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err } image := zcommon.GetFullImageName(repo, ref) cveMap, err := cveinfo.Scanner.ScanImage(ctx, image) if err != nil { - return []cvemodel.CVE{}, zcommon.PageInfo{}, err + return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err } + imageCVESummary = initCVESummaryFromCVEMap(cveMap) + pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy) if err != nil { - return []cvemodel.CVE{}, zcommon.PageInfo{}, err + return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err } filterCVEList(cveMap, searchedCVE, pageFinder) cveList, pageInfo := pageFinder.Page() - return cveList, pageInfo, nil + return cveList, imageCVESummary, pageInfo, nil } -func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string, +func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string, ) (cvemodel.ImageCVESummary, error) { // There are several cases, expected returned values below: // not scanned yet - max severity "" - cve count 0 - no Errors // not scannable - max severity "" - cve count 0 - has Errors // scannable no issues found - max severity "NONE" - cve count 0 - no Errors // scannable issues found - max severity from Scanner - cve count >0 - no Errors - imageCVESummary := cvemodel.ImageCVESummary{ - Count: 0, - MaxSeverity: cvemodel.SeverityNotScanned, - } - // For this call we only look at the scanner cache, we skip the actual scanning to save time - if !cveinfo.Scanner.IsResultCached(digest) { - isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digest, mediaType) + if !cveinfo.Scanner.IsResultCached(digestStr) { + isValidImage, err := cveinfo.Scanner.IsImageMediaScannable(repo, digestStr, mediaType) if !isValidImage { - cveinfo.Log.Debug().Str("digest", digest).Str("mediaType", mediaType). + cveinfo.Log.Debug().Str("digest", digestStr).Str("mediaType", mediaType). Err(err).Msg("image is not scannable") } + // Counters are initialized with 0 by default + imageCVESummary := cvemodel.ImageCVESummary{ + MaxSeverity: cvemodel.SeverityNotScanned, + } + return imageCVESummary, err } // We will make due with cached results - cveMap := cveinfo.Scanner.GetCachedResult(digest) + cveMap := cveinfo.Scanner.GetCachedResult(digestStr) - imageCVESummary.Count = len(cveMap) - if imageCVESummary.Count == 0 { - imageCVESummary.MaxSeverity = cvemodel.SeverityNone - - return imageCVESummary, nil - } - - imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown - for _, cve := range cveMap { - if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 { - imageCVESummary.MaxSeverity = cve.Severity - } - } - - return imageCVESummary, nil + return initCVESummaryFromCVEMap(cveMap), nil } func GetFixedTags(allTags, vulnerableTags []cvemodel.TagInfo) []cvemodel.TagInfo { @@ -517,3 +509,40 @@ func containsDescriptorInfo(slice []cvemodel.DescriptorInfo, descriptorInfo cvem return false } + +func initCVESummaryFromCVEMap(cveMap map[string]cvemodel.CVE) cvemodel.ImageCVESummary { + // Counters are initialized with 0 by default + imageCVESummary := cvemodel.ImageCVESummary{ + MaxSeverity: cvemodel.SeverityNotScanned, + } + + imageCVESummary.Count = len(cveMap) + if imageCVESummary.Count == 0 { + imageCVESummary.MaxSeverity = cvemodel.SeverityNone + + return imageCVESummary + } + + imageCVESummary.MaxSeverity = cvemodel.SeverityUnknown + + for _, cve := range cveMap { + switch cve.Severity { + case cvemodel.SeverityUnknown: + imageCVESummary.UnknownCount += 1 + case cvemodel.SeverityLow: + imageCVESummary.LowCount += 1 + case cvemodel.SeverityMedium: + imageCVESummary.MediumCount += 1 + case cvemodel.SeverityHigh: + imageCVESummary.HighCount += 1 + case cvemodel.SeverityCritical: + imageCVESummary.CriticalCount += 1 + } + + if cvemodel.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 { + imageCVESummary.MaxSeverity = cve.Severity + } + } + + return imageCVESummary +} diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index fd9e277d..add2640e 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -754,6 +754,7 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo const repo5 = "repo5" const repo6 = "repo6" const repo7 = "repo7" + const repo8 = "repo8" const repo100 = "repo100" const repoMultiarch = "repoIndex" @@ -833,6 +834,13 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo err = metaDB.SetRepoReference(context.Background(), repo7, "1.0.0", image71.AsImageMeta()) So(err, ShouldBeNil) + // Create image with vulnerabilities of all severities + image81 := CreateImageWith().DefaultLayers(). + ImageConfig(ispec.Image{Created: DateRef(2020, 12, 1, 12, 0, 0, 0, time.UTC)}).Build() + + err = metaDB.SetRepoReference(context.Background(), repo8, "1.0.0", image81.AsImageMeta()) + So(err, ShouldBeNil) + // create multiarch image with vulnerabilities multiarchImage := CreateRandomMultiarch() @@ -891,6 +899,10 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo image71Media := image71.ManifestDescriptor.MediaType image71Name := repo7 + ":1.0.0" imageMap[image71Name] = image71Digest + image81Digest := image81.ManifestDescriptor.Digest.String() + image81Media := image81.ManifestDescriptor.MediaType + image81Name := repo8 + ":1.0.0" + imageMap[image81Name] = image81Digest indexDigest := multiarchImage.IndexDescriptor.Digest.String() indexMedia := multiarchImage.IndexDescriptor.MediaType indexName := repoMultiarch + ":tagIndex" @@ -1042,6 +1054,57 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo return result, nil } + if repo == repo8 && ref == image81Digest { + result := map[string]cvemodel.CVE{ + "CVE0": { + ID: "CVE0", + Severity: "UNKNOWN", + Title: "Title CVE0", + Description: "Description CVE0", + }, + "CVE1": { + ID: "CVE1", + Severity: "MEDIUM", + Title: "Title CVE1", + Description: "Description CVE1", + }, + "CVE2": { + ID: "CVE2", + Severity: "HIGH", + Title: "Title CVE2", + Description: "Description CVE2", + }, + "CVE3": { + ID: "CVE3", + Severity: "LOW", + Title: "Title CVE3", + Description: "Description CVE3", + }, + "CVE4": { + ID: "CVE4", + Severity: "CRITICAL", + Title: "Title CVE4", + Description: "Description CVE4", + }, + "CVE5": { + ID: "CVE5", + Severity: "CRITICAL", + Title: "Title CVE5", + Description: "Description CVE5", + }, + "CVE6": { + ID: "CVE6", + Severity: "LOW", + Title: "Title CVE6", + Description: "Description CVE6", + }, + } + + cache.Add(ref, result) + + return result, nil + } + // By default the image has no vulnerabilities result = map[string]cvemodel.CVE{} cache.Add(ref, result) @@ -1130,14 +1193,21 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo ctx := context.Background() // Image is found - cveList, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) + So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 3) So(cveList[0].ID, ShouldEqual, "CVE2") @@ -1145,116 +1215,249 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo So(cveList[2].ID, ShouldEqual, "CVE3") So(pageInfo.ItemCount, ShouldEqual, 3) So(pageInfo.TotalCount, ShouldEqual, 3) + So(cveSummary.Count, ShouldEqual, 3) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "HIGH") - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.1", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.0.1", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 2) So(cveList[0].ID, ShouldEqual, "CVE1") So(cveList[1].ID, ShouldEqual, "CVE3") So(pageInfo.ItemCount, ShouldEqual, 2) So(pageInfo.TotalCount, ShouldEqual, 2) + So(cveSummary.Count, ShouldEqual, 2) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "1.1.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE3") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) + So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "LOW") - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo6, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo6, "1.0.0", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "NONE") + + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo8, "1.0.0", "", pageInput) + So(err, ShouldBeNil) + So(len(cveList), ShouldEqual, 7) + So(pageInfo.ItemCount, ShouldEqual, 7) + So(pageInfo.TotalCount, ShouldEqual, 7) + So(cveSummary.Count, ShouldEqual, 7) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 2) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 2) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") // Image is multiarch - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", pageInput) So(err, ShouldBeNil) So(len(cveList), ShouldEqual, 1) So(cveList[0].ID, ShouldEqual, "CVE1") So(pageInfo.ItemCount, ShouldEqual, 1) So(pageInfo.TotalCount, ShouldEqual, 1) + So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") // Image is not scannable - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo2, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo2, "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrScanNotSupported) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") // Tag is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo3, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo3, "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrTagMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") // Scan failed - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo7, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo7, "1.0.0", "", pageInput) So(err, ShouldEqual, ErrFailedScan) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") // Tag is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo-with-bad-tag-digest", "tag", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo-with-bad-tag-digest", "tag", "", pageInput) So(err, ShouldEqual, zerr.ErrImageMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") // Repo is not found - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo100, "1.0.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo100, "1.0.0", "", pageInput) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(len(cveList), ShouldEqual, 0) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") // By this point the cache should already be pupulated by previous function calls t.Log("\nTest GetCVESummaryForImage\n") // Image is found - cveSummary, err := cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image11Digest, image11Media) + cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image11Digest, image11Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image12Digest, image12Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 3) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "HIGH") cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image14Digest, image14Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 2) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image13Digest, image13Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "LOW") cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo6, image61Digest, image61Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "NONE") + cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo8, image81Digest, image81Media) + So(err, ShouldBeNil) + So(cveSummary.Count, ShouldEqual, 7) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 2) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 2) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") + // Image is multiarch cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repoMultiarch, indexDigest, indexMedia) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") // Image is not scannable cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo2, image21Digest, image21Media) So(err, ShouldEqual, zerr.ErrScanNotSupported) So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") // Scan failed cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo5, image71Digest, image71Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") // Repo is not found @@ -1262,6 +1465,11 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo godigest.FromString("missing_digest").String(), ispec.MediaTypeImageManifest) So(err, ShouldEqual, zerr.ErrRepoMetaNotFound) So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") t.Log("\nTest GetImageListWithCVEFixed\n") @@ -1366,13 +1574,25 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo cveSummary, err = cveInfo.GetCVESummaryForImageMedia(ctx, repo1, image11Digest, image11Media) So(err, ShouldBeNil) So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) So(cveSummary.MaxSeverity, ShouldEqual, "") - cveList, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) + cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repo1, "0.1.0", "", pageInput) So(err, ShouldNotBeNil) So(cveList, ShouldBeEmpty) So(pageInfo.ItemCount, ShouldEqual, 0) So(pageInfo.TotalCount, ShouldEqual, 0) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.UnknownCount, ShouldEqual, 0) + So(cveSummary.LowCount, ShouldEqual, 0) + So(cveSummary.MediumCount, ShouldEqual, 0) + So(cveSummary.HighCount, ShouldEqual, 0) + So(cveSummary.CriticalCount, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "") tagList, err = cveInfo.GetImageListWithCVEFixed(ctx, repo1, "CVE1") // CVE is not considered fixed as scan is not possible diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go index fbc20b51..e85de88c 100644 --- a/pkg/extensions/search/cve/model/models.go +++ b/pkg/extensions/search/cve/model/models.go @@ -7,8 +7,13 @@ import ( ) type ImageCVESummary struct { - Count int - MaxSeverity string + Count int + UnknownCount int + LowCount int + MediumCount int + HighCount int + CriticalCount int + MaxSeverity string } //nolint:tagliatelle // graphQL schema diff --git a/pkg/extensions/search/cve/pagination_test.go b/pkg/extensions/search/cve/pagination_test.go index a154ddd4..08f3b899 100644 --- a/pkg/extensions/search/cve/pagination_test.go +++ b/pkg/extensions/search/cve/pagination_test.go @@ -140,22 +140,36 @@ func TestCVEPagination(t *testing.T) { Convey("Page", func() { Convey("defaults", func() { // By default expect unlimitted results sorted by severity - cves, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{}) + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) So(pageInfo.TotalCount, ShouldEqual, 5) + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") previousSeverity := 4 for _, cve := range cves { So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity) previousSeverity = severityToInt[cve.Severity] } - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{}) + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.TotalCount, ShouldEqual, 30) + So(cveSummary.Count, ShouldEqual, 30) + So(cveSummary.UnknownCount, ShouldEqual, 6) + So(cveSummary.LowCount, ShouldEqual, 6) + So(cveSummary.MediumCount, ShouldEqual, 6) + So(cveSummary.HighCount, ShouldEqual, 6) + So(cveSummary.CriticalCount, ShouldEqual, 6) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") previousSeverity = 4 for _, cve := range cves { So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity) @@ -169,44 +183,72 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) So(pageInfo.TotalCount, ShouldEqual, 5) + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") for i, cve := range cves { So(cve.ID, ShouldEqual, cveIds[i]) } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticAsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.TotalCount, ShouldEqual, 30) + So(cveSummary.Count, ShouldEqual, 30) + So(cveSummary.UnknownCount, ShouldEqual, 6) + So(cveSummary.LowCount, ShouldEqual, 6) + So(cveSummary.MediumCount, ShouldEqual, 6) + So(cveSummary.HighCount, ShouldEqual, 6) + So(cveSummary.CriticalCount, ShouldEqual, 6) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") for i, cve := range cves { So(cve.ID, ShouldEqual, cveIds[i]) } sort.Sort(sort.Reverse(sort.StringSlice(cveIds))) - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{SortBy: cveinfo.AlphabeticDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.TotalCount, ShouldEqual, 30) + So(cveSummary.Count, ShouldEqual, 30) + So(cveSummary.UnknownCount, ShouldEqual, 6) + So(cveSummary.LowCount, ShouldEqual, 6) + So(cveSummary.MediumCount, ShouldEqual, 6) + So(cveSummary.HighCount, ShouldEqual, 6) + So(cveSummary.CriticalCount, ShouldEqual, 6) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") for i, cve := range cves { So(cve.ID, ShouldEqual, cveIds[i]) } - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{SortBy: cveinfo.SeverityDsc}) So(err, ShouldBeNil) So(len(cves), ShouldEqual, 30) So(pageInfo.ItemCount, ShouldEqual, 30) So(pageInfo.TotalCount, ShouldEqual, 30) + So(cveSummary.Count, ShouldEqual, 30) + So(cveSummary.UnknownCount, ShouldEqual, 6) + So(cveSummary.LowCount, ShouldEqual, 6) + So(cveSummary.MediumCount, ShouldEqual, 6) + So(cveSummary.HighCount, ShouldEqual, 6) + So(cveSummary.CriticalCount, ShouldEqual, 6) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") previousSeverity := 4 for _, cve := range cves { So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity) @@ -220,7 +262,7 @@ func TestCVEPagination(t *testing.T) { cveIds = append(cveIds, fmt.Sprintf("CVE%d", i)) } - cves, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.AlphabeticAsc, @@ -233,8 +275,15 @@ func TestCVEPagination(t *testing.T) { So(cves[0].ID, ShouldEqual, "CVE1") // CVE0 is first ID and is not part of the page So(cves[1].ID, ShouldEqual, "CVE2") So(cves[2].ID, ShouldEqual, "CVE3") + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 2, Offset: 1, SortBy: cveinfo.AlphabeticDsc, @@ -246,8 +295,15 @@ func TestCVEPagination(t *testing.T) { So(pageInfo.TotalCount, ShouldEqual, 5) So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE2") + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 3, Offset: 1, SortBy: cveinfo.SeverityDsc, @@ -257,6 +313,13 @@ func TestCVEPagination(t *testing.T) { So(len(cves), ShouldEqual, 3) So(pageInfo.ItemCount, ShouldEqual, 3) So(pageInfo.TotalCount, ShouldEqual, 5) + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") previousSeverity := 4 for _, cve := range cves { So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity) @@ -264,7 +327,7 @@ func TestCVEPagination(t *testing.T) { } sort.Strings(cveIds) - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "1.0.0", "", cvemodel.PageInput{ Limit: 5, Offset: 20, SortBy: cveinfo.AlphabeticAsc, @@ -274,13 +337,20 @@ func TestCVEPagination(t *testing.T) { So(len(cves), ShouldEqual, 5) So(pageInfo.ItemCount, ShouldEqual, 5) So(pageInfo.TotalCount, ShouldEqual, 30) + So(cveSummary.Count, ShouldEqual, 30) + So(cveSummary.UnknownCount, ShouldEqual, 6) + So(cveSummary.LowCount, ShouldEqual, 6) + So(cveSummary.MediumCount, ShouldEqual, 6) + So(cveSummary.HighCount, ShouldEqual, 6) + So(cveSummary.CriticalCount, ShouldEqual, 6) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") for i, cve := range cves { So(cve.ID, ShouldEqual, cveIds[i+20]) } }) Convey("limit > len(cves)", func() { - cves, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticAsc, @@ -292,8 +362,15 @@ func TestCVEPagination(t *testing.T) { So(pageInfo.TotalCount, ShouldEqual, 5) So(cves[0].ID, ShouldEqual, "CVE3") So(cves[1].ID, ShouldEqual, "CVE4") + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.AlphabeticDsc, @@ -305,8 +382,15 @@ func TestCVEPagination(t *testing.T) { So(pageInfo.TotalCount, ShouldEqual, 5) So(cves[0].ID, ShouldEqual, "CVE1") So(cves[1].ID, ShouldEqual, "CVE0") + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") - cves, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ + cves, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, "repo1", "0.1.0", "", cvemodel.PageInput{ Limit: 6, Offset: 3, SortBy: cveinfo.SeverityDsc, @@ -316,6 +400,13 @@ func TestCVEPagination(t *testing.T) { So(len(cves), ShouldEqual, 2) So(pageInfo.ItemCount, ShouldEqual, 2) So(pageInfo.TotalCount, ShouldEqual, 5) + So(cveSummary.Count, ShouldEqual, 5) + So(cveSummary.UnknownCount, ShouldEqual, 1) + So(cveSummary.LowCount, ShouldEqual, 1) + So(cveSummary.MediumCount, ShouldEqual, 1) + So(cveSummary.HighCount, ShouldEqual, 1) + So(cveSummary.CriticalCount, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") previousSeverity := 4 for _, cve := range cves { So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity) diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index ef3258bd..9585fc77 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -62,6 +62,7 @@ type ComplexityRoot struct { CVEResultForImage struct { CVEList func(childComplexity int) int Page func(childComplexity int) int + Summary func(childComplexity int) int Tag func(childComplexity int) int } @@ -105,8 +106,13 @@ type ComplexityRoot struct { } ImageVulnerabilitySummary struct { - Count func(childComplexity int) int - MaxSeverity func(childComplexity int) int + Count func(childComplexity int) int + CriticalCount func(childComplexity int) int + HighCount func(childComplexity int) int + LowCount func(childComplexity int) int + MaxSeverity func(childComplexity int) int + MediumCount func(childComplexity int) int + UnknownCount func(childComplexity int) int } LayerHistory struct { @@ -318,6 +324,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.CVEResultForImage.Page(childComplexity), true + case "CVEResultForImage.Summary": + if e.complexity.CVEResultForImage.Summary == nil { + break + } + + return e.complexity.CVEResultForImage.Summary(childComplexity), true + case "CVEResultForImage.Tag": if e.complexity.CVEResultForImage.Tag == nil { break @@ -542,6 +555,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageVulnerabilitySummary.Count(childComplexity), true + case "ImageVulnerabilitySummary.CriticalCount": + if e.complexity.ImageVulnerabilitySummary.CriticalCount == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.CriticalCount(childComplexity), true + + case "ImageVulnerabilitySummary.HighCount": + if e.complexity.ImageVulnerabilitySummary.HighCount == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.HighCount(childComplexity), true + + case "ImageVulnerabilitySummary.LowCount": + if e.complexity.ImageVulnerabilitySummary.LowCount == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.LowCount(childComplexity), true + case "ImageVulnerabilitySummary.MaxSeverity": if e.complexity.ImageVulnerabilitySummary.MaxSeverity == nil { break @@ -549,6 +583,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageVulnerabilitySummary.MaxSeverity(childComplexity), true + case "ImageVulnerabilitySummary.MediumCount": + if e.complexity.ImageVulnerabilitySummary.MediumCount == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.MediumCount(childComplexity), true + + case "ImageVulnerabilitySummary.UnknownCount": + if e.complexity.ImageVulnerabilitySummary.UnknownCount == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.UnknownCount(childComplexity), true + case "LayerHistory.HistoryDescription": if e.complexity.LayerHistory.HistoryDescription == nil { break @@ -1164,6 +1212,7 @@ A timestamp """ scalar Time + """ Contains the tag of the image and a list of CVEs """ @@ -1177,6 +1226,10 @@ type CVEResultForImage { """ CVEList: [CVE] """ + Summary of the findings for this image + """ + Summary: ImageVulnerabilitySummary + """ The CVE pagination information, see PageInfo object for more details """ Page: PageInfo @@ -1430,6 +1483,26 @@ type ImageVulnerabilitySummary { Count of all CVEs found in this image """ Count: Int + """ + Coresponds to CVSS 3 score NONE + """ + UnknownCount: Int + """ + Coresponds to CVSS 3 score LOW + """ + LowCount: Int + """ + Coresponds to CVSS 3 score MEDIUM + """ + MediumCount: Int + """ + Coresponds to CVSS 3 score HIGH + """ + HighCount: Int + """ + Coresponds to CVSS 3 score CRITICAL + """ + CriticalCount: Int } """ @@ -2761,6 +2834,63 @@ func (ec *executionContext) fieldContext_CVEResultForImage_CVEList(ctx context.C return fc, nil } +func (ec *executionContext) _CVEResultForImage_Summary(ctx context.Context, field graphql.CollectedField, obj *CVEResultForImage) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_CVEResultForImage_Summary(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Summary, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*ImageVulnerabilitySummary) + fc.Result = res + return ec.marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_CVEResultForImage_Summary(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "CVEResultForImage", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "MaxSeverity": + return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) + case "Count": + return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + case "UnknownCount": + return ec.fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx, field) + case "LowCount": + return ec.fieldContext_ImageVulnerabilitySummary_LowCount(ctx, field) + case "MediumCount": + return ec.fieldContext_ImageVulnerabilitySummary_MediumCount(ctx, field) + case "HighCount": + return ec.fieldContext_ImageVulnerabilitySummary_HighCount(ctx, field) + case "CriticalCount": + return ec.fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) + }, + } + return fc, nil +} + func (ec *executionContext) _CVEResultForImage_Page(ctx context.Context, field graphql.CollectedField, obj *CVEResultForImage) (ret graphql.Marshaler) { fc, err := ec.fieldContext_CVEResultForImage_Page(ctx, field) if err != nil { @@ -4071,6 +4201,16 @@ func (ec *executionContext) fieldContext_ImageSummary_Vulnerabilities(ctx contex return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) case "Count": return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + case "UnknownCount": + return ec.fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx, field) + case "LowCount": + return ec.fieldContext_ImageVulnerabilitySummary_LowCount(ctx, field) + case "MediumCount": + return ec.fieldContext_ImageVulnerabilitySummary_MediumCount(ctx, field) + case "HighCount": + return ec.fieldContext_ImageVulnerabilitySummary_HighCount(ctx, field) + case "CriticalCount": + return ec.fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) }, @@ -4254,6 +4394,211 @@ func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_Count(ctx con return fc, nil } +func (ec *executionContext) _ImageVulnerabilitySummary_UnknownCount(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.UnknownCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_LowCount(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_LowCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.LowCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_LowCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_MediumCount(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_MediumCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.MediumCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_MediumCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_HighCount(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_HighCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.HighCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_HighCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_CriticalCount(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.CriticalCount, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _LayerHistory_Layer(ctx context.Context, field graphql.CollectedField, obj *LayerHistory) (ret graphql.Marshaler) { fc, err := ec.fieldContext_LayerHistory_Layer(ctx, field) if err != nil { @@ -4912,6 +5257,16 @@ func (ec *executionContext) fieldContext_ManifestSummary_Vulnerabilities(ctx con return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) case "Count": return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + case "UnknownCount": + return ec.fieldContext_ImageVulnerabilitySummary_UnknownCount(ctx, field) + case "LowCount": + return ec.fieldContext_ImageVulnerabilitySummary_LowCount(ctx, field) + case "MediumCount": + return ec.fieldContext_ImageVulnerabilitySummary_MediumCount(ctx, field) + case "HighCount": + return ec.fieldContext_ImageVulnerabilitySummary_HighCount(ctx, field) + case "CriticalCount": + return ec.fieldContext_ImageVulnerabilitySummary_CriticalCount(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) }, @@ -5599,6 +5954,8 @@ func (ec *executionContext) fieldContext_Query_CVEListForImage(ctx context.Conte return ec.fieldContext_CVEResultForImage_Tag(ctx, field) case "CVEList": return ec.fieldContext_CVEResultForImage_CVEList(ctx, field) + case "Summary": + return ec.fieldContext_CVEResultForImage_Summary(ctx, field) case "Page": return ec.fieldContext_CVEResultForImage_Page(ctx, field) } @@ -9553,6 +9910,8 @@ func (ec *executionContext) _CVEResultForImage(ctx context.Context, sel ast.Sele out.Values[i] = ec._CVEResultForImage_Tag(ctx, field, obj) case "CVEList": out.Values[i] = ec._CVEResultForImage_CVEList(ctx, field, obj) + case "Summary": + out.Values[i] = ec._CVEResultForImage_Summary(ctx, field, obj) case "Page": out.Values[i] = ec._CVEResultForImage_Page(ctx, field, obj) default: @@ -9755,6 +10114,16 @@ func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel out.Values[i] = ec._ImageVulnerabilitySummary_MaxSeverity(ctx, field, obj) case "Count": out.Values[i] = ec._ImageVulnerabilitySummary_Count(ctx, field, obj) + case "UnknownCount": + out.Values[i] = ec._ImageVulnerabilitySummary_UnknownCount(ctx, field, obj) + case "LowCount": + out.Values[i] = ec._ImageVulnerabilitySummary_LowCount(ctx, field, obj) + case "MediumCount": + out.Values[i] = ec._ImageVulnerabilitySummary_MediumCount(ctx, field, obj) + case "HighCount": + out.Values[i] = ec._ImageVulnerabilitySummary_HighCount(ctx, field, obj) + case "CriticalCount": + out.Values[i] = ec._ImageVulnerabilitySummary_CriticalCount(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 9e654c53..00a82c75 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -41,6 +41,8 @@ type CVEResultForImage struct { Tag *string `json:"Tag,omitempty"` // List of CVE objects which affect this specific image:tag CVEList []*Cve `json:"CVEList,omitempty"` + // Summary of the findings for this image + Summary *ImageVulnerabilitySummary `json:"Summary,omitempty"` // The CVE pagination information, see PageInfo object for more details Page *PageInfo `json:"Page,omitempty"` } @@ -144,6 +146,16 @@ type ImageVulnerabilitySummary struct { MaxSeverity *string `json:"MaxSeverity,omitempty"` // Count of all CVEs found in this image Count *int `json:"Count,omitempty"` + // Coresponds to CVSS 3 score NONE + UnknownCount *int `json:"UnknownCount,omitempty"` + // Coresponds to CVSS 3 score LOW + LowCount *int `json:"LowCount,omitempty"` + // Coresponds to CVSS 3 score MEDIUM + MediumCount *int `json:"MediumCount,omitempty"` + // Coresponds to CVSS 3 score HIGH + HighCount *int `json:"HighCount,omitempty"` + // Coresponds to CVSS 3 score CRITICAL + CriticalCount *int `json:"CriticalCount,omitempty"` } // Information about how/when a layer was built diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 86309b55..220663f3 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -216,7 +216,8 @@ func getCVEListForImage( return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided") } - cveList, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo, ref, searchedCVE, pageInput) + cveList, imageCveSummary, pageInfo, err := cveInfo.GetCVEListForImage(ctx, repo, ref, + searchedCVE, pageInput) if err != nil { return &gql_generated.CVEResultForImage{}, err } @@ -259,6 +260,15 @@ func getCVEListForImage( return &gql_generated.CVEResultForImage{ Tag: &ref, CVEList: cveids, + Summary: &gql_generated.ImageVulnerabilitySummary{ + MaxSeverity: &imageCveSummary.MaxSeverity, + UnknownCount: &imageCveSummary.UnknownCount, + LowCount: &imageCveSummary.LowCount, + MediumCount: &imageCveSummary.MediumCount, + HighCount: &imageCveSummary.HighCount, + CriticalCount: &imageCveSummary.CriticalCount, + Count: &imageCveSummary.Count, + }, Page: &gql_generated.PageInfo{ TotalCount: pageInfo.TotalCount, ItemCount: pageInfo.ItemCount, diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index c1b50453..9f7f19c0 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -10,6 +10,7 @@ A timestamp """ scalar Time + """ Contains the tag of the image and a list of CVEs """ @@ -23,6 +24,10 @@ type CVEResultForImage { """ CVEList: [CVE] """ + Summary of the findings for this image + """ + Summary: ImageVulnerabilitySummary + """ The CVE pagination information, see PageInfo object for more details """ Page: PageInfo @@ -276,6 +281,26 @@ type ImageVulnerabilitySummary { Count of all CVEs found in this image """ Count: Int + """ + Coresponds to CVSS 3 score NONE + """ + UnknownCount: Int + """ + Coresponds to CVSS 3 score LOW + """ + LowCount: Int + """ + Coresponds to CVSS 3 score MEDIUM + """ + MediumCount: Int + """ + Coresponds to CVSS 3 score HIGH + """ + HighCount: Int + """ + Coresponds to CVSS 3 score CRITICAL + """ + CriticalCount: Int } """ diff --git a/pkg/extensions/search/search_test.go b/pkg/extensions/search/search_test.go index b13dd893..6a4634bc 100644 --- a/pkg/extensions/search/search_test.go +++ b/pkg/extensions/search/search_test.go @@ -18,7 +18,6 @@ import ( "testing" "time" - dbTypes "github.com/aquasecurity/trivy-db/pkg/types" guuid "github.com/gofrs/uuid" regTypes "github.com/google/go-containerregistry/pkg/v1/types" notreg "github.com/notaryproject/notation-go/registry" @@ -530,6 +529,11 @@ func TestRepoListWithNewestImage(t *testing.T) { Tag Vulnerabilities{ MaxSeverity + UnknownCount + LowCount + MediumCount + HighCount + CriticalCount Count } } @@ -551,6 +555,11 @@ func TestRepoListWithNewestImage(t *testing.T) { images = responseStruct.Results So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1") So(images[0].NewestImage.Vulnerabilities.Count, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(images[0].NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") query = `{ @@ -741,6 +750,11 @@ func TestRepoListWithNewestImage(t *testing.T) { Digest Vulnerabilities{ MaxSeverity + UnknownCount + LowCount + MediumCount + HighCount + CriticalCount Count } } @@ -765,12 +779,12 @@ func TestRepoListWithNewestImage(t *testing.T) { So(vulnerabilities, ShouldNotBeNil) t.Logf("Found vulnerability summary %v", vulnerabilities) // Depends on test data, but current tested images contain hundreds - So(vulnerabilities.Count, ShouldBeGreaterThan, 1) - So( - dbTypes.CompareSeverityString(dbTypes.SeverityUnknown.String(), vulnerabilities.MaxSeverity), - ShouldBeGreaterThan, - 0, - ) + So(vulnerabilities.Count, ShouldEqual, 4) + So(vulnerabilities.UnknownCount, ShouldEqual, 0) + So(vulnerabilities.LowCount, ShouldEqual, 1) + So(vulnerabilities.MediumCount, ShouldEqual, 1) + So(vulnerabilities.HighCount, ShouldEqual, 1) + So(vulnerabilities.CriticalCount, ShouldEqual, 1) So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") } }) @@ -3178,7 +3192,7 @@ func TestGlobalSearch(t *testing.T) { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Vendor Vulnerabilities { Count MaxSeverity } @@ -3199,7 +3213,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } @@ -3256,6 +3270,11 @@ func TestGlobalSearch(t *testing.T) { // No vulnerabilities should be detected since trivy is disabled t.Logf("Found vulnerability summary %v", repoSummary.NewestImage.Vulnerabilities) So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") } @@ -3272,7 +3291,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size @@ -3288,7 +3307,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } @@ -3323,6 +3342,11 @@ func TestGlobalSearch(t *testing.T) { // 0 vulnerabilities should be detected since trivy is disabled t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities) So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.LowCount, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.HighCount, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") }) @@ -3500,7 +3524,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size @@ -3516,7 +3540,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } @@ -3575,10 +3599,20 @@ func TestGlobalSearch(t *testing.T) { if repoName == "repo1" { //nolint:goconst So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 4) // There are 4 vulnerabilities in the data used in tests + So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 1) + So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 1) + So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 1) + So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 1) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") } else { So(repoSummary.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) // There are 0 vulnerabilities this data used in tests + So(repoSummary.NewestImage.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.LowCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.HighCount, ShouldEqual, 0) + So(repoSummary.NewestImage.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(repoSummary.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "NONE") } } @@ -3596,7 +3630,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } Repos { Name LastUpdated Size @@ -3612,7 +3646,7 @@ func TestGlobalSearch(t *testing.T) { HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } } Layers { Digest Size } @@ -3647,6 +3681,11 @@ func TestGlobalSearch(t *testing.T) { t.Logf("Found vulnerability summary %v", actualImageSummary.Vulnerabilities) // There are 4 vulnerabilities in the data used in tests So(actualImageSummary.Vulnerabilities.Count, ShouldEqual, 4) + So(actualImageSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(actualImageSummary.Vulnerabilities.LowCount, ShouldEqual, 1) + So(actualImageSummary.Vulnerabilities.MediumCount, ShouldEqual, 1) + So(actualImageSummary.Vulnerabilities.HighCount, ShouldEqual, 1) + So(actualImageSummary.Vulnerabilities.CriticalCount, ShouldEqual, 1) So(actualImageSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") }) @@ -5949,7 +5988,7 @@ func TestImageSummary(t *testing.T) { Size Platform { Os Arch } Layers { Digest Size } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } @@ -5957,7 +5996,7 @@ func TestImageSummary(t *testing.T) { } LastUpdated Size - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } Referrers {MediaType ArtifactType Digest Annotations {Key Value}} } }` @@ -5976,7 +6015,7 @@ func TestImageSummary(t *testing.T) { Size Platform { Os Arch } Layers { Digest Size } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } @@ -6089,6 +6128,11 @@ func TestImageSummary(t *testing.T) { So(imgSummary.Manifests[0].History[0].HistoryDescription.Created, ShouldEqual, createdTime) // No vulnerabilities should be detected since trivy is disabled So(imgSummary.Vulnerabilities.Count, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.LowCount, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.MediumCount, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.HighCount, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.CriticalCount, ShouldEqual, 0) So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "") So(len(imgSummary.Referrers), ShouldEqual, 1) So(imgSummary.Referrers[0], ShouldResemble, zcommon.Referrer{ @@ -6177,7 +6221,7 @@ func TestImageSummary(t *testing.T) { Size Platform { Os Arch } Layers { Digest Size } - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } History { HistoryDescription { Created } Layer { Digest Size } @@ -6185,7 +6229,7 @@ func TestImageSummary(t *testing.T) { } LastUpdated Size - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } } }` @@ -6271,6 +6315,11 @@ func TestImageSummary(t *testing.T) { So(len(imgSummary.Manifests[0].History), ShouldEqual, 1) So(imgSummary.Manifests[0].History[0].HistoryDescription.Created, ShouldEqual, createdTime) So(imgSummary.Vulnerabilities.Count, ShouldEqual, 4) + So(imgSummary.Vulnerabilities.UnknownCount, ShouldEqual, 0) + So(imgSummary.Vulnerabilities.LowCount, ShouldEqual, 1) + So(imgSummary.Vulnerabilities.MediumCount, ShouldEqual, 1) + So(imgSummary.Vulnerabilities.HighCount, ShouldEqual, 1) + So(imgSummary.Vulnerabilities.CriticalCount, ShouldEqual, 1) // There are 0 vulnerabilities this data used in tests So(imgSummary.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") }) @@ -6800,11 +6849,11 @@ func GlobalSearchGQL(query, baseURL string) *zcommon.GlobalSearchResultResp { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } - Vulnerabilities {Count MaxSeverity} + Vulnerabilities {Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity} Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} } Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } SignatureInfo {Tool IsTrusted Author} } Repos { @@ -6824,11 +6873,11 @@ func GlobalSearchGQL(query, baseURL string) *zcommon.GlobalSearchResultResp { Layer { Size Digest } HistoryDescription { Author Comment Created CreatedBy EmptyLayer } } - Vulnerabilities {Count MaxSeverity} + Vulnerabilities {Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity} Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} } Referrers {MediaType ArtifactType Size Digest Annotations {Key Value}} - Vulnerabilities { Count MaxSeverity } + Vulnerabilities { Count UnknownCount LowCount MediumCount HighCount CriticalCount MaxSeverity } SignatureInfo {Tool IsTrusted Author} } } diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go index 03d17a0b..ad11abf5 100644 --- a/pkg/test/mocks/cve_mock.go +++ b/pkg/test/mocks/cve_mock.go @@ -11,7 +11,7 @@ type CveInfoMock struct { GetImageListForCVEFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListWithCVEFixedFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetCVEListForImageFn func(ctx context.Context, repo string, reference string, searchedCVE string, - pageInput cvemodel.PageInput) ([]cvemodel.CVE, common.PageInfo, error) + pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string, ) (cvemodel.ImageCVESummary, error) } @@ -37,6 +37,7 @@ func (cveInfo CveInfoMock) GetCVEListForImage(ctx context.Context, repo string, searchedCVE string, pageInput cvemodel.PageInput, ) ( []cvemodel.CVE, + cvemodel.ImageCVESummary, common.PageInfo, error, ) { @@ -44,7 +45,7 @@ func (cveInfo CveInfoMock) GetCVEListForImage(ctx context.Context, repo string, return cveInfo.GetCVEListForImageFn(ctx, repo, reference, searchedCVE, pageInput) } - return []cvemodel.CVE{}, common.PageInfo{}, nil + return []cvemodel.CVE{}, cvemodel.ImageCVESummary{}, common.PageInfo{}, nil } func (cveInfo CveInfoMock) GetCVESummaryForImageMedia(ctx context.Context, repo, digest, mediaType string,