0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2025-04-08 02:54:41 -05:00

feat(cve): graphql: paginate returned CVEs for a given image (#1136)

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron 2023-01-25 01:03:10 +02:00 committed by GitHub
parent 08983a845a
commit 58ec62b3e4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 724 additions and 74 deletions

View file

@ -18,8 +18,9 @@ import (
type CveInfo interface {
GetImageListForCVE(repo, cveID string) ([]common.TagInfo, error)
GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error)
GetCVEListForImage(image string) (map[string]cvemodel.CVE, error)
GetCVEListForImage(image string, pageinput PageInput) ([]cvemodel.CVE, PageInfo, error)
GetCVESummaryForImage(image string) (ImageCVESummary, error)
CompareSeverities(severity1, severity2 string) int
UpdateDB() error
}
@ -102,15 +103,11 @@ func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]common.TagI
continue
}
for id := range cveMap {
if id == cveID {
imgList = append(imgList, common.TagInfo{
Name: tag,
Digest: manifestDigest,
})
break
}
if _, hasCVE := cveMap[cveID]; hasCVE {
imgList = append(imgList, common.TagInfo{
Name: tag,
Digest: manifestDigest,
})
}
}
@ -208,15 +205,33 @@ func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]commo
return fixedTags, nil
}
func (cveinfo BaseCveInfo) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) {
cveMap := make(map[string]cvemodel.CVE)
func (cveinfo BaseCveInfo) GetCVEListForImage(image string, pageInput PageInput) (
[]cvemodel.CVE,
PageInfo,
error,
) {
isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image)
if !isValidImage {
return cveMap, err
return []cvemodel.CVE{}, PageInfo{}, err
}
return cveinfo.Scanner.ScanImage(image)
cveMap, err := cveinfo.Scanner.ScanImage(image)
if err != nil {
return []cvemodel.CVE{}, PageInfo{}, err
}
pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy, cveinfo)
if err != nil {
return []cvemodel.CVE{}, PageInfo{}, err
}
for _, cve := range cveMap {
pageFinder.Add(cve)
}
cveList, pageInfo := pageFinder.Page()
return cveList, pageInfo, nil
}
func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, error) {
@ -260,3 +275,7 @@ func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary,
func (cveinfo BaseCveInfo) UpdateDB() error {
return cveinfo.Scanner.UpdateDB()
}
func (cveinfo BaseCveInfo) CompareSeverities(severity1, severity2 string) int {
return cveinfo.Scanner.CompareSeverities(severity1, severity2)
}

View file

@ -1216,58 +1216,75 @@ func TestCVEStruct(t *testing.T) {
t.Log("Test GetCVEListForImage")
pageInput := cveinfo.PageInput{
SortBy: cveinfo.SeverityDsc,
}
// Image is found
cveMap, err := cveInfo.GetCVEListForImage("repo1:0.1.0")
cveList, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", pageInput)
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 1)
So(cveMap, ShouldContainKey, "CVE1")
So(cveMap, ShouldNotContainKey, "CVE2")
So(cveMap, ShouldNotContainKey, "CVE3")
So(len(cveList), ShouldEqual, 1)
So(cveList[0].ID, ShouldEqual, "CVE1")
So(pageInfo.ItemCount, ShouldEqual, 1)
So(pageInfo.TotalCount, ShouldEqual, 1)
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", pageInput)
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 3)
So(cveMap, ShouldContainKey, "CVE1")
So(cveMap, ShouldContainKey, "CVE2")
So(cveMap, ShouldContainKey, "CVE3")
So(len(cveList), ShouldEqual, 3)
So(cveList[0].ID, ShouldEqual, "CVE2")
So(cveList[1].ID, ShouldEqual, "CVE1")
So(cveList[2].ID, ShouldEqual, "CVE3")
So(pageInfo.ItemCount, ShouldEqual, 3)
So(pageInfo.TotalCount, ShouldEqual, 3)
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.1")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.1", pageInput)
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 2)
So(cveMap, ShouldContainKey, "CVE1")
So(cveMap, ShouldNotContainKey, "CVE2")
So(cveMap, ShouldContainKey, "CVE3")
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)
cveMap, err = cveInfo.GetCVEListForImage("repo1:1.1.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.1.0", pageInput)
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 1)
So(cveMap, ShouldNotContainKey, "CVE1")
So(cveMap, ShouldNotContainKey, "CVE2")
So(cveMap, ShouldContainKey, "CVE3")
So(len(cveList), ShouldEqual, 1)
So(cveList[0].ID, ShouldEqual, "CVE3")
So(pageInfo.ItemCount, ShouldEqual, 1)
So(pageInfo.TotalCount, ShouldEqual, 1)
cveMap, err = cveInfo.GetCVEListForImage("repo6:1.0.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo6:1.0.0", pageInput)
So(err, ShouldBeNil)
So(len(cveMap), ShouldEqual, 0)
So(len(cveList), ShouldEqual, 0)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
// Image is not scannable
cveMap, err = cveInfo.GetCVEListForImage("repo2:1.0.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo2:1.0.0", pageInput)
So(err, ShouldEqual, zerr.ErrScanNotSupported)
So(len(cveMap), ShouldEqual, 0)
So(len(cveList), ShouldEqual, 0)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
// Tag is not found
cveMap, err = cveInfo.GetCVEListForImage("repo3:1.0.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo3:1.0.0", pageInput)
So(err, ShouldEqual, zerr.ErrTagMetaNotFound)
So(len(cveMap), ShouldEqual, 0)
So(len(cveList), ShouldEqual, 0)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
// Manifest is not found
cveMap, err = cveInfo.GetCVEListForImage("repo5:nonexitent-manifest")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo5:nonexitent-manifest", pageInput)
So(err, ShouldEqual, zerr.ErrManifestDataNotFound)
So(len(cveMap), ShouldEqual, 0)
So(len(cveList), ShouldEqual, 0)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
// Repo is not found
cveMap, err = cveInfo.GetCVEListForImage("repo100:1.0.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo100:1.0.0", pageInput)
So(err, ShouldEqual, zerr.ErrRepoMetaNotFound)
So(len(cveMap), ShouldEqual, 0)
So(len(cveList), ShouldEqual, 0)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
t.Log("Test GetImageListWithCVEFixed")
@ -1383,9 +1400,11 @@ func TestCVEStruct(t *testing.T) {
So(cveSummary.Count, ShouldEqual, 0)
So(cveSummary.MaxSeverity, ShouldEqual, "")
cveMap, err = cveInfo.GetCVEListForImage("repo1:0.1.0")
cveList, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", pageInput)
So(err, ShouldNotBeNil)
So(cveMap, ShouldBeNil)
So(cveList, ShouldBeEmpty)
So(pageInfo.ItemCount, ShouldEqual, 0)
So(pageInfo.TotalCount, ShouldEqual, 0)
tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE1")
// CVE is not considered fixed as scan is not possible

View file

@ -0,0 +1,144 @@
package cveinfo
import (
"sort"
"github.com/pkg/errors"
zerr "zotregistry.io/zot/errors"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
)
type SortCriteria string
const (
AlphabeticAsc = SortCriteria("ALPHABETIC_ASC")
AlphabeticDsc = SortCriteria("ALPHABETIC_DSC")
SeverityDsc = SortCriteria("SEVERITY")
)
func SortFunctions() map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return map[SortCriteria]func(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool{
AlphabeticAsc: SortByAlphabeticAsc,
AlphabeticDsc: SortByAlphabeticDsc,
SeverityDsc: SortBySeverity,
}
}
func SortByAlphabeticAsc(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return func(i, j int) bool {
return pageBuffer[i].ID < pageBuffer[j].ID
}
}
func SortByAlphabeticDsc(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return func(i, j int) bool {
return pageBuffer[i].ID > pageBuffer[j].ID
}
}
func SortBySeverity(pageBuffer []cvemodel.CVE, cveInfo CveInfo) func(i, j int) bool {
return func(i, j int) bool {
return cveInfo.CompareSeverities(pageBuffer[i].Severity, pageBuffer[j].Severity) < 0
}
}
// PageFinder permits keeping a pool of objects using Add
// and returning a specific page.
type PageFinder interface {
Add(cve cvemodel.CVE)
Page() ([]cvemodel.CVE, PageInfo)
Reset()
}
// CvePageFinder implements PageFinder. It manages Cve objects and calculates the page
// using the given limit, offset and sortBy option.
type CvePageFinder struct {
limit int
offset int
sortBy SortCriteria
pageBuffer []cvemodel.CVE
cveInfo CveInfo
}
func NewCvePageFinder(limit, offset int, sortBy SortCriteria, cveInfo CveInfo) (*CvePageFinder, error) {
if sortBy == "" {
sortBy = SeverityDsc
}
if limit < 0 {
return nil, zerr.ErrLimitIsNegative
}
if offset < 0 {
return nil, zerr.ErrOffsetIsNegative
}
if _, found := SortFunctions()[sortBy]; !found {
return nil, errors.Wrapf(zerr.ErrSortCriteriaNotSupported, "sorting CVEs by '%s' is not supported", sortBy)
}
return &CvePageFinder{
limit: limit,
offset: offset,
sortBy: sortBy,
pageBuffer: make([]cvemodel.CVE, 0, limit),
cveInfo: cveInfo,
}, nil
}
func (bpt *CvePageFinder) Reset() {
bpt.pageBuffer = []cvemodel.CVE{}
}
func (bpt *CvePageFinder) Add(cve cvemodel.CVE) {
bpt.pageBuffer = append(bpt.pageBuffer, cve)
}
func (bpt *CvePageFinder) Page() ([]cvemodel.CVE, PageInfo) {
if len(bpt.pageBuffer) == 0 {
return []cvemodel.CVE{}, PageInfo{}
}
pageInfo := &PageInfo{}
sort.Slice(bpt.pageBuffer, SortFunctions()[bpt.sortBy](bpt.pageBuffer, bpt.cveInfo))
// the offset and limit are calculated in terms of CVEs counted
start := bpt.offset
end := bpt.offset + bpt.limit
// we'll return an empty array when the offset is greater than the number of elements
if start >= len(bpt.pageBuffer) {
start = len(bpt.pageBuffer)
end = start
}
if end >= len(bpt.pageBuffer) {
end = len(bpt.pageBuffer)
}
cves := bpt.pageBuffer[start:end]
pageInfo.ItemCount = len(cves)
if start == 0 && end == 0 {
cves = bpt.pageBuffer
pageInfo.ItemCount = len(cves)
}
pageInfo.TotalCount = len(bpt.pageBuffer)
return cves, *pageInfo
}
type PageInfo struct {
TotalCount int
ItemCount int
}
type PageInput struct {
Limit int
Offset int
SortBy SortCriteria
}

View file

@ -0,0 +1,355 @@
package cveinfo_test
import (
"encoding/json"
"fmt"
"sort"
"testing"
"time"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
cveinfo "zotregistry.io/zot/pkg/extensions/search/cve"
cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model"
"zotregistry.io/zot/pkg/log"
"zotregistry.io/zot/pkg/meta/repodb"
bolt "zotregistry.io/zot/pkg/meta/repodb/boltdb-wrapper"
"zotregistry.io/zot/pkg/test/mocks"
)
func TestCVEPagination(t *testing.T) {
Convey("CVE Pagination", t, func() {
repoDB, err := bolt.NewBoltDBWrapper(bolt.DBParameters{
RootDir: t.TempDir(),
})
So(err, ShouldBeNil)
// Create repodb data for scannable image with vulnerabilities
timeStamp11 := time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC)
configBlob11, err := json.Marshal(ispec.Image{
Created: &timeStamp11,
})
So(err, ShouldBeNil)
manifestBlob11, err := json.Marshal(ispec.Manifest{
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Size: 0,
Digest: godigest.FromBytes(configBlob11),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayerGzip,
Size: 0,
Digest: godigest.NewDigestFromEncoded(godigest.SHA256, "digest"),
},
},
})
So(err, ShouldBeNil)
repoMeta11 := repodb.ManifestMetadata{
ManifestBlob: manifestBlob11,
ConfigBlob: configBlob11,
}
digest11 := godigest.FromBytes(manifestBlob11)
err = repoDB.SetManifestMeta("repo1", digest11, repoMeta11)
So(err, ShouldBeNil)
err = repoDB.SetRepoTag("repo1", "0.1.0", digest11, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
timeStamp12 := time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC)
configBlob12, err := json.Marshal(ispec.Image{
Created: &timeStamp12,
})
So(err, ShouldBeNil)
manifestBlob12, err := json.Marshal(ispec.Manifest{
Config: ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Size: 0,
Digest: godigest.FromBytes(configBlob12),
},
Layers: []ispec.Descriptor{
{
MediaType: ispec.MediaTypeImageLayerGzip,
Size: 0,
Digest: godigest.NewDigestFromEncoded(godigest.SHA256, "digest"),
},
},
})
So(err, ShouldBeNil)
repoMeta12 := repodb.ManifestMetadata{
ManifestBlob: manifestBlob12,
ConfigBlob: configBlob12,
}
digest12 := godigest.FromBytes(manifestBlob12)
err = repoDB.SetManifestMeta("repo1", digest12, repoMeta12)
So(err, ShouldBeNil)
err = repoDB.SetRepoTag("repo1", "1.0.0", digest12, ispec.MediaTypeImageManifest)
So(err, ShouldBeNil)
// RepoDB loaded with initial data, mock the scanner
severityToInt := map[string]int{
"UNKNOWN": 0,
"LOW": 1,
"MEDIUM": 2,
"HIGH": 3,
"CRITICAL": 4,
}
intToSeverity := make(map[int]string, len(severityToInt))
for k, v := range severityToInt {
intToSeverity[v] = k
}
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) {
cveMap := map[string]cvemodel.CVE{}
if image == "repo1:0.1.0" {
for i := 0; i < 5; i++ {
cveMap[fmt.Sprintf("CVE%d", i)] = cvemodel.CVE{
ID: fmt.Sprintf("CVE%d", i),
Severity: intToSeverity[i%5],
Title: fmt.Sprintf("Title for CVE%d", i),
Description: fmt.Sprintf("Description for CVE%d", i),
}
}
}
if image == "repo1:1.0.0" {
for i := 0; i < 30; i++ {
cveMap[fmt.Sprintf("CVE%d", i)] = cvemodel.CVE{
ID: fmt.Sprintf("CVE%d", i),
Severity: intToSeverity[i%5],
Title: fmt.Sprintf("Title for CVE%d", i),
Description: fmt.Sprintf("Description for CVE%d", i),
}
}
}
// By default the image has no vulnerabilities
return cveMap, nil
},
CompareSeveritiesFn: func(severity1, severity2 string) int {
return severityToInt[severity2] - severityToInt[severity1]
},
}
log := log.NewLogger("debug", "")
cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, RepoDB: repoDB}
Convey("create new paginator errors", func() {
paginator, err := cveinfo.NewCvePageFinder(-1, 10, cveinfo.AlphabeticAsc, cveInfo)
So(paginator, ShouldBeNil)
So(err, ShouldNotBeNil)
paginator, err = cveinfo.NewCvePageFinder(2, -1, cveinfo.AlphabeticAsc, cveInfo)
So(paginator, ShouldBeNil)
So(err, ShouldNotBeNil)
paginator, err = cveinfo.NewCvePageFinder(2, 1, "wrong sorting criteria", cveInfo)
So(paginator, ShouldBeNil)
So(err, ShouldNotBeNil)
})
Convey("Reset", func() {
paginator, err := cveinfo.NewCvePageFinder(1, 0, cveinfo.AlphabeticAsc, cveInfo)
So(err, ShouldBeNil)
So(paginator, ShouldNotBeNil)
paginator.Add(cvemodel.CVE{})
paginator.Add(cvemodel.CVE{})
paginator.Add(cvemodel.CVE{})
paginator.Reset()
result, _ := paginator.Page()
So(result, ShouldBeEmpty)
})
Convey("Page", func() {
Convey("defaults", func() {
// By default expect unlimitted results sorted by severity
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5)
So(pageInfo.TotalCount, ShouldEqual, 5)
previousSeverity := 4
for _, cve := range cves {
So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity)
previousSeverity = severityToInt[cve.Severity]
}
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
So(pageInfo.TotalCount, ShouldEqual, 30)
previousSeverity = 4
for _, cve := range cves {
So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity)
previousSeverity = severityToInt[cve.Severity]
}
})
Convey("no limit or offset", func() {
cveIds := []string{}
for i := 0; i < 30; i++ {
cveIds = append(cveIds, fmt.Sprintf("CVE%d", i))
}
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5)
So(pageInfo.TotalCount, ShouldEqual, 5)
for i, cve := range cves {
So(cve.ID, ShouldEqual, cveIds[i])
}
sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticAsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
So(pageInfo.TotalCount, ShouldEqual, 30)
for i, cve := range cves {
So(cve.ID, ShouldEqual, cveIds[i])
}
sort.Sort(sort.Reverse(sort.StringSlice(cveIds)))
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.AlphabeticDsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
So(pageInfo.TotalCount, ShouldEqual, 30)
for i, cve := range cves {
So(cve.ID, ShouldEqual, cveIds[i])
}
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{SortBy: cveinfo.SeverityDsc})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 30)
So(pageInfo.ItemCount, ShouldEqual, 30)
So(pageInfo.TotalCount, ShouldEqual, 30)
previousSeverity := 4
for _, cve := range cves {
So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity)
previousSeverity = severityToInt[cve.Severity]
}
})
Convey("limit < len(cves)", func() {
cveIds := []string{}
for i := 0; i < 30; i++ {
cveIds = append(cveIds, fmt.Sprintf("CVE%d", i))
}
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 3,
Offset: 1,
SortBy: cveinfo.AlphabeticAsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 3)
So(pageInfo.ItemCount, ShouldEqual, 3)
So(pageInfo.TotalCount, ShouldEqual, 5)
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")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 2,
Offset: 1,
SortBy: cveinfo.AlphabeticDsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 2)
So(pageInfo.ItemCount, ShouldEqual, 2)
So(pageInfo.TotalCount, ShouldEqual, 5)
So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE2")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 3,
Offset: 1,
SortBy: cveinfo.SeverityDsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 3)
So(pageInfo.ItemCount, ShouldEqual, 3)
So(pageInfo.TotalCount, ShouldEqual, 5)
previousSeverity := 4
for _, cve := range cves {
So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity)
previousSeverity = severityToInt[cve.Severity]
}
sort.Strings(cveIds)
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:1.0.0", cveinfo.PageInput{
Limit: 5,
Offset: 20,
SortBy: cveinfo.AlphabeticAsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 5)
So(pageInfo.ItemCount, ShouldEqual, 5)
So(pageInfo.TotalCount, ShouldEqual, 30)
for i, cve := range cves {
So(cve.ID, ShouldEqual, cveIds[i+20])
}
})
Convey("limit > len(cves)", func() {
cves, pageInfo, err := cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.AlphabeticAsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 2)
So(pageInfo.ItemCount, ShouldEqual, 2)
So(pageInfo.TotalCount, ShouldEqual, 5)
So(cves[0].ID, ShouldEqual, "CVE3")
So(cves[1].ID, ShouldEqual, "CVE4")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.AlphabeticDsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 2)
So(pageInfo.ItemCount, ShouldEqual, 2)
So(pageInfo.TotalCount, ShouldEqual, 5)
So(cves[0].ID, ShouldEqual, "CVE1")
So(cves[1].ID, ShouldEqual, "CVE0")
cves, pageInfo, err = cveInfo.GetCVEListForImage("repo1:0.1.0", cveinfo.PageInput{
Limit: 6,
Offset: 3,
SortBy: cveinfo.SeverityDsc,
})
So(err, ShouldBeNil)
So(len(cves), ShouldEqual, 2)
So(pageInfo.ItemCount, ShouldEqual, 2)
So(pageInfo.TotalCount, ShouldEqual, 5)
previousSeverity := 4
for _, cve := range cves {
So(severityToInt[cve.Severity], ShouldBeLessThanOrEqualTo, previousSeverity)
previousSeverity = severityToInt[cve.Severity]
}
})
})
})
}

View file

@ -58,6 +58,7 @@ type ComplexityRoot struct {
CVEResultForImage struct {
CVEList func(childComplexity int) int
Page func(childComplexity int) int
Tag func(childComplexity int) int
}
@ -144,7 +145,7 @@ type ComplexityRoot struct {
Query struct {
BaseImageList func(childComplexity int, image string) int
CVEListForImage func(childComplexity int, image string) int
CVEListForImage func(childComplexity int, image string, requestedPage *PageInput) int
DerivedImageList func(childComplexity int, image string) int
ExpandedRepoInfo func(childComplexity int, repo string) int
GlobalSearch func(childComplexity int, query string, filter *Filter, requestedPage *PageInput) int
@ -186,7 +187,7 @@ type ComplexityRoot struct {
}
type QueryResolver interface {
CVEListForImage(ctx context.Context, image string) (*CVEResultForImage, error)
CVEListForImage(ctx context.Context, image string, requestedPage *PageInput) (*CVEResultForImage, error)
ImageListForCve(ctx context.Context, id string, requestedPage *PageInput) ([]*ImageSummary, error)
ImageListWithCVEFixed(ctx context.Context, id string, image string, requestedPage *PageInput) ([]*ImageSummary, error)
ImageListForDigest(ctx context.Context, id string, requestedPage *PageInput) ([]*ImageSummary, error)
@ -271,6 +272,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.CVEResultForImage.CVEList(childComplexity), true
case "CVEResultForImage.Page":
if e.complexity.CVEResultForImage.Page == nil {
break
}
return e.complexity.CVEResultForImage.Page(childComplexity), true
case "CVEResultForImage.Tag":
if e.complexity.CVEResultForImage.Tag == nil {
break
@ -636,7 +644,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return 0, false
}
return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string)), true
return e.complexity.Query.CVEListForImage(childComplexity, args["image"].(string), args["requestedPage"].(*PageInput)), true
case "Query.DerivedImageList":
if e.complexity.Query.DerivedImageList == nil {
@ -947,6 +955,7 @@ Contains the tag of the image and a list of CVEs
type CVEResultForImage {
Tag: String
CVEList: [CVE]
Page: PageInfo
}
"""
@ -1103,6 +1112,7 @@ enum SortCriteria {
UPDATE_TIME
ALPHABETIC_ASC
ALPHABETIC_DSC
SEVERITY
STARS
DOWNLOADS
}
@ -1141,9 +1151,9 @@ input Filter {
type Query {
"""
Returns a CVE list for the image specified in the arugment
Returns a CVE list for the image specified in the argument. Format image:tag
"""
CVEListForImage(image: String!): CVEResultForImage!
CVEListForImage(image: String!, requestedPage: PageInput): CVEResultForImage!
"""
Returns a list of images vulnerable to the CVE of the specified ID
@ -1236,6 +1246,15 @@ func (ec *executionContext) field_Query_CVEListForImage_args(ctx context.Context
}
}
args["image"] = arg0
var arg1 *PageInput
if tmp, ok := rawArgs["requestedPage"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("requestedPage"))
arg1, err = ec.unmarshalOPageInput2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInput(ctx, tmp)
if err != nil {
return nil, err
}
}
args["requestedPage"] = arg1
return args, nil
}
@ -1912,6 +1931,53 @@ func (ec *executionContext) fieldContext_CVEResultForImage_CVEList(ctx context.C
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 {
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.Page, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
return graphql.Null
}
res := resTmp.(*PageInfo)
fc.Result = res
return ec.marshalOPageInfo2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐPageInfo(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_CVEResultForImage_Page(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 "TotalCount":
return ec.fieldContext_PageInfo_TotalCount(ctx, field)
case "ItemCount":
return ec.fieldContext_PageInfo_ItemCount(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type PageInfo", field.Name)
},
}
return fc, nil
}
func (ec *executionContext) _GlobalSearchResult_Page(ctx context.Context, field graphql.CollectedField, obj *GlobalSearchResult) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_GlobalSearchResult_Page(ctx, field)
if err != nil {
@ -4114,7 +4180,7 @@ func (ec *executionContext) _Query_CVEListForImage(ctx context.Context, field gr
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().CVEListForImage(rctx, fc.Args["image"].(string))
return ec.resolvers.Query().CVEListForImage(rctx, fc.Args["image"].(string), fc.Args["requestedPage"].(*PageInput))
})
if err != nil {
ec.Error(ctx, err)
@ -4143,6 +4209,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 "Page":
return ec.fieldContext_CVEResultForImage_Page(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type CVEResultForImage", field.Name)
},
@ -8038,6 +8106,10 @@ func (ec *executionContext) _CVEResultForImage(ctx context.Context, sel ast.Sele
out.Values[i] = ec._CVEResultForImage_CVEList(ctx, field, obj)
case "Page":
out.Values[i] = ec._CVEResultForImage_Page(ctx, field, obj)
default:
panic("unknown field " + strconv.Quote(field.Name))
}

View file

@ -25,8 +25,9 @@ type Cve struct {
// Contains the tag of the image and a list of CVEs
type CVEResultForImage struct {
Tag *string `json:"Tag"`
CVEList []*Cve `json:"CVEList"`
Tag *string `json:"Tag"`
CVEList []*Cve `json:"CVEList"`
Page *PageInfo `json:"Page"`
}
type Filter struct {
@ -167,6 +168,7 @@ const (
SortCriteriaUpdateTime SortCriteria = "UPDATE_TIME"
SortCriteriaAlphabeticAsc SortCriteria = "ALPHABETIC_ASC"
SortCriteriaAlphabeticDsc SortCriteria = "ALPHABETIC_DSC"
SortCriteriaSeverity SortCriteria = "SEVERITY"
SortCriteriaStars SortCriteria = "STARS"
SortCriteriaDownloads SortCriteria = "DOWNLOADS"
)
@ -176,13 +178,14 @@ var AllSortCriteria = []SortCriteria{
SortCriteriaUpdateTime,
SortCriteriaAlphabeticAsc,
SortCriteriaAlphabeticDsc,
SortCriteriaSeverity,
SortCriteriaStars,
SortCriteriaDownloads,
}
func (e SortCriteria) IsValid() bool {
switch e {
case SortCriteriaRelevance, SortCriteriaUpdateTime, SortCriteriaAlphabeticAsc, SortCriteriaAlphabeticDsc, SortCriteriaStars, SortCriteriaDownloads:
case SortCriteriaRelevance, SortCriteriaUpdateTime, SortCriteriaAlphabeticAsc, SortCriteriaAlphabeticDsc, SortCriteriaSeverity, SortCriteriaStars, SortCriteriaDownloads:
return true
}
return false

View file

@ -195,23 +195,36 @@ func getCVEListForImage(
ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions
image string,
cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput,
log log.Logger, //nolint:unparam // may be used by devs for debugging
) (*gql_generated.CVEResultForImage, error) {
if requestedPage == nil {
requestedPage = &gql_generated.PageInput{}
}
pageInput := cveinfo.PageInput{
Limit: safeDerefferencing(requestedPage.Limit, 0),
Offset: safeDerefferencing(requestedPage.Offset, 0),
SortBy: cveinfo.SortCriteria(
safeDerefferencing(requestedPage.SortBy, gql_generated.SortCriteriaSeverity),
),
}
_, copyImgTag := common.GetImageDirAndTag(image)
if copyImgTag == "" {
return &gql_generated.CVEResultForImage{}, gqlerror.Errorf("no reference provided")
}
cveidMap, err := cveInfo.GetCVEListForImage(image)
cveList, pageInfo, err := cveInfo.GetCVEListForImage(image, pageInput)
if err != nil {
return &gql_generated.CVEResultForImage{}, err
}
cveids := []*gql_generated.Cve{}
for id, cveDetail := range cveidMap {
vulID := id
for _, cveDetail := range cveList {
vulID := cveDetail.ID
desc := cveDetail.Description
title := cveDetail.Title
severity := cveDetail.Severity
@ -241,7 +254,14 @@ func getCVEListForImage(
)
}
return &gql_generated.CVEResultForImage{Tag: &copyImgTag, CVEList: cveids}, nil
return &gql_generated.CVEResultForImage{
Tag: &copyImgTag,
CVEList: cveids,
Page: &gql_generated.PageInfo{
TotalCount: pageInfo.TotalCount,
ItemCount: pageInfo.ItemCount,
},
}, nil
}
func FilterByTagInfo(tagsInfo []common.TagInfo) repodb.FilterFunc {

View file

@ -1020,7 +1020,7 @@ func TestImageList(t *testing.T) {
},
Signatures: map[string]repodb.ManifestSignatures{
"digestTag1.0.1": {
"cosgin": []repodb.SignatureInfo{
"cosign": []repodb.SignatureInfo{
{SignatureManifestDigest: "digestSignature1"},
},
},
@ -1045,7 +1045,7 @@ func TestImageList(t *testing.T) {
ConfigBlob: configBlob,
DownloadCount: 0,
Signatures: repodb.ManifestSignatures{
"cosgin": []repodb.SignatureInfo{
"cosign": []repodb.SignatureInfo{
{SignatureManifestDigest: "digestSignature1"},
},
},
@ -1733,12 +1733,15 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
Convey("Get CVE list for image ", t, func() {
Convey("Unpaginated request to get all CVEs in an image", func() {
// CVE pagination will be implemented later
sortCriteria := gql_generated.SortCriteriaAlphabeticAsc
pageInput := &gql_generated.PageInput{
SortBy: &sortCriteria,
}
responseContext := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter,
graphql.DefaultRecover)
cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, log)
cveResult, err := getCVEListForImage(responseContext, "repo1:1.0.0", cveInfo, pageInput, log)
So(err, ShouldBeNil)
So(*cveResult.Tag, ShouldEqual, "1.0.0")
@ -1749,7 +1752,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
So(expectedCves, ShouldContain, *cve.ID)
}
cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.1", cveInfo, log)
cveResult, err = getCVEListForImage(responseContext, "repo1:1.0.1", cveInfo, pageInput, log)
So(err, ShouldBeNil)
So(*cveResult.Tag, ShouldEqual, "1.0.1")
@ -1760,7 +1763,7 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
So(expectedCves, ShouldContain, *cve.ID)
}
cveResult, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, log)
cveResult, err = getCVEListForImage(responseContext, "repo1:1.1.0", cveInfo, pageInput, log)
So(err, ShouldBeNil)
So(*cveResult.Tag, ShouldEqual, "1.1.0")

View file

@ -6,6 +6,7 @@ Contains the tag of the image and a list of CVEs
type CVEResultForImage {
Tag: String
CVEList: [CVE]
Page: PageInfo
}
"""
@ -162,6 +163,7 @@ enum SortCriteria {
UPDATE_TIME
ALPHABETIC_ASC
ALPHABETIC_DSC
SEVERITY
STARS
DOWNLOADS
}
@ -200,9 +202,9 @@ input Filter {
type Query {
"""
Returns a CVE list for the image specified in the arugment
Returns a CVE list for the image specified in the argument. Format image:tag
"""
CVEListForImage(image: String!): CVEResultForImage!
CVEListForImage(image: String!, requestedPage: PageInput): CVEResultForImage!
"""
Returns a list of images vulnerable to the CVE of the specified ID

View file

@ -14,12 +14,12 @@ import (
)
// CVEListForImage is the resolver for the CVEListForImage field.
func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql_generated.CVEResultForImage, error) {
func (r *queryResolver) CVEListForImage(ctx context.Context, image string, requestedPage *gql_generated.PageInput) (*gql_generated.CVEResultForImage, error) {
if r.cveInfo == nil {
return &gql_generated.CVEResultForImage{}, zerr.ErrCVESearchDisabled
}
return getCVEListForImage(ctx, image, r.cveInfo, r.log)
return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, r.log)
}
// ImageListForCve is the resolver for the ImageListForCVE field.

View file

@ -9,8 +9,9 @@ import (
type CveInfoMock struct {
GetImageListForCVEFn func(repo, cveID string) ([]common.TagInfo, error)
GetImageListWithCVEFixedFn func(repo, cveID string) ([]common.TagInfo, error)
GetCVEListForImageFn func(image string) (map[string]cvemodel.CVE, error)
GetCVEListForImageFn func(image string, pageInput cveinfo.PageInput) ([]cvemodel.CVE, cveinfo.PageInfo, error)
GetCVESummaryForImageFn func(image string) (cveinfo.ImageCVESummary, error)
CompareSeveritiesFn func(severity1, severity2 string) int
UpdateDBFn func() error
}
@ -30,12 +31,16 @@ func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]commo
return []common.TagInfo{}, nil
}
func (cveInfo CveInfoMock) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) {
func (cveInfo CveInfoMock) GetCVEListForImage(image string, pageInput cveinfo.PageInput) (
[]cvemodel.CVE,
cveinfo.PageInfo,
error,
) {
if cveInfo.GetCVEListForImageFn != nil {
return cveInfo.GetCVEListForImageFn(image)
return cveInfo.GetCVEListForImageFn(image, pageInput)
}
return map[string]cvemodel.CVE{}, nil
return []cvemodel.CVE{}, cveinfo.PageInfo{}, nil
}
func (cveInfo CveInfoMock) GetCVESummaryForImage(image string) (cveinfo.ImageCVESummary, error) {
@ -46,6 +51,14 @@ func (cveInfo CveInfoMock) GetCVESummaryForImage(image string) (cveinfo.ImageCVE
return cveinfo.ImageCVESummary{}, nil
}
func (cveInfo CveInfoMock) CompareSeverities(severity1, severity2 string) int {
if cveInfo.CompareSeveritiesFn != nil {
return cveInfo.CompareSeveritiesFn(severity1, severity2)
}
return 0
}
func (cveInfo CveInfoMock) UpdateDB() error {
if cveInfo.UpdateDBFn != nil {
return cveInfo.UpdateDBFn()