0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-16 21:56:37 -05:00

feat(cve): cli cve diff (#2242)

* feat(gql): add new query for diff of cves for 2 images

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>

* feat(cli): add cli for cve diff

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>

---------

Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
This commit is contained in:
LaurentiuNiculae 2024-03-06 00:40:29 -08:00 committed by GitHub
parent 752b9e87c1
commit 5039128723
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 2555 additions and 11 deletions

View file

@ -168,4 +168,6 @@ var (
ErrAPINotSupported = errors.New("registry at the given address doesn't implement the correct API")
ErrURLNotFound = errors.New("url not found")
ErrInvalidSearchQuery = errors.New("invalid search query")
ErrImageNotFound = errors.New("image not found")
ErrAmbiguousInput = errors.New("input is not specific enough")
)

View file

@ -198,6 +198,276 @@ func TestNegativeServerResponse(t *testing.T) {
})
}
func TestCVEDiffList(t *testing.T) {
port := test.GetFreePort()
url := test.GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
dir := t.TempDir()
conf.Storage.RootDirectory = dir
trivyConfig := &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
}
cveConfig := &extconf.CVEConfig{
UpdateInterval: 2,
Trivy: trivyConfig,
}
defaultVal := true
searchConfig := &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
CVE: cveConfig,
}
conf.Extensions = &extconf.ExtensionConfig{
Search: searchConfig,
}
logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt")
if err != nil {
panic(err)
}
logPath := logFile.Name()
defer os.Remove(logPath)
writers := io.MultiWriter(os.Stdout, logFile)
ctlr := api.NewController(conf)
ctlr.Log.Logger = ctlr.Log.Output(writers)
if err := ctlr.Init(); err != nil {
panic(err)
}
layer1 := []byte{10, 20, 30}
layer2 := []byte{11, 21, 31}
layer3 := []byte{12, 22, 23}
otherImage := CreateImageWith().LayerBlobs([][]byte{
layer1,
}).DefaultConfig().Build()
baseImage := CreateImageWith().LayerBlobs([][]byte{
layer1,
layer2,
}).PlatformConfig("testArch", "testOs").Build()
image := CreateImageWith().LayerBlobs([][]byte{
layer1,
layer2,
layer3,
}).PlatformConfig("testArch", "testOs").Build()
multiArchBase := CreateMultiarchWith().Images([]Image{baseImage, CreateRandomImage(), CreateRandomImage()}).
Build()
multiArchImage := CreateMultiarchWith().Images([]Image{image, CreateRandomImage(), CreateRandomImage()}).
Build()
getCveResults := func(digestStr string) map[string]cvemodel.CVE {
switch digestStr {
case image.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
PackageList: []cvemodel.Package{{}},
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
PackageList: []cvemodel.Package{{}},
},
}
case baseImage.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
PackageList: []cvemodel.Package{{}},
},
}
case otherImage.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
}
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}
}
// MetaDB loaded with initial data, now mock the scanner
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
repo, ref, _, _ := zcommon.GetRepoReference(image)
if zcommon.IsDigest(ref) {
return getCveResults(ref), nil
}
repoMeta, _ := ctlr.MetaDB.GetRepoMeta(ctx, repo)
if _, ok := repoMeta.Tags[ref]; !ok {
panic("unexpected tag '" + ref + "', test might be wrong")
}
return getCveResults(repoMeta.Tags[ref].Digest), nil
},
GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE {
return getCveResults(digestStr)
},
IsResultCachedFn: func(digestStr string) bool {
return true
},
}
ctlr.CveScanner = scanner
go func() {
if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
defer ctlr.Shutdown()
test.WaitTillServerReady(url)
ctx := context.Background()
_, err = ociutils.InitializeTestMetaDB(ctx, ctlr.MetaDB,
ociutils.Repo{
Name: "repo",
Images: []ociutils.RepoImage{
{Image: otherImage, Reference: "other-image"},
{Image: baseImage, Reference: "base-image"},
{Image: image, Reference: "image"},
},
},
ociutils.Repo{
Name: "repo-multi",
MultiArchImages: []ociutils.RepoMultiArchImage{
{MultiarchImage: CreateRandomMultiarch(), Reference: "multi-rand"},
{MultiarchImage: multiArchBase, Reference: "multi-base"},
{MultiarchImage: multiArchImage, Reference: "multi-img"},
},
},
)
Convey("Test CVE by image name - GQL - positive", t, func() {
args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := client.NewCVECommand(client.NewSearchService())
buff := bytes.NewBufferString("")
cveCmd.SetOut(buff)
cveCmd.SetErr(buff)
cveCmd.SetArgs(args)
err = cveCmd.Execute()
fmt.Println(buff.String())
space := regexp.MustCompile(`\s+`)
str := space.ReplaceAllString(buff.String(), " ")
str = strings.TrimSpace(str)
So(str, ShouldContainSubstring, "CVE3")
So(str, ShouldNotContainSubstring, "CVE1")
So(str, ShouldNotContainSubstring, "CVE2")
})
Convey("Errors", t, func() {
// args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"}
configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url))
defer os.Remove(configPath)
cveCmd := client.NewCVECommand(client.NewSearchService())
Convey("Set wrong number of params", func() {
args := []string{"diff", "repo:image", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("First input is not a repo:tag", func() {
args := []string{"diff", "bad-input", "repo:base-image", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("Second input is arch but not enough args", func() {
args := []string{"diff", "repo:base-image", "linux/amd64", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("Second input is arch 3rd is repo:tag", func() {
args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldBeNil)
})
Convey("Second input is repo:tag 3rd is repo:tag", func() {
args := []string{"diff", "repo:base-image", "repo:base-image", "repo:base-image", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("Second input is arch 3rd is arch as well", func() {
args := []string{"diff", "repo:base-image", "linux/amd64", "linux/amd64", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("Second input is repo:tag 3rd is arch", func() {
args := []string{"diff", "repo:base-image", "repo:base-image", "linux/amd64", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldBeNil)
})
Convey("Second input is repo:tag 3rd is arch, 4th is repo:tag", func() {
args := []string{
"diff", "repo:base-image", "repo:base-image", "linux/amd64", "repo:base-image",
"--config", "cvetest",
}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("Second input is arch 3rd is repo:tag, 4th is arch", func() {
args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "linux/amd64", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldBeNil)
})
Convey("input is with digest ref", func() {
args := []string{"diff", "repo@sha256:123123", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
Convey("input is with just repo no ref", func() {
args := []string{"diff", "repo", "--config", "cvetest"}
cveCmd.SetArgs(args)
So(cveCmd.Execute(), ShouldNotBeNil)
})
})
}
//nolint:dupl
func TestServerCVEResponse(t *testing.T) {
port := test.GetFreePort()

View file

@ -30,6 +30,7 @@ func NewCVECommand(searchService SearchService) *cobra.Command {
cvesCmd.AddCommand(NewCveForImageCommand(searchService))
cvesCmd.AddCommand(NewImagesByCVEIDCommand(searchService))
cvesCmd.AddCommand(NewFixedTagsCommand(searchService))
cvesCmd.AddCommand(NewCVEDiffCommand(searchService))
return cvesCmd
}

View file

@ -140,3 +140,139 @@ func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
return fixedTagsCmd
}
func NewCVEDiffCommand(searchService SearchService) *cobra.Command {
var (
minuendStr, minuendArch string
subtrahendStr, subtrahendArch string
)
imagesByCVEIDCmd := &cobra.Command{
Use: "diff [minuend] ([minuend-platform]) [subtrahend] ([subtrahend-platform])",
Short: "List the CVE's present in minuend that are not present in subtrahend",
Long: `List the CVE's present in minuend that are not present in subtrahend`,
Args: func(cmd *cobra.Command, args []string) error {
const (
twoArgs = 2
threeArgs = 3
fourArgs = 4
)
if err := cobra.RangeArgs(twoArgs, fourArgs)(cmd, args); err != nil {
return err
}
if !isRepoTag(args[0]) {
return fmt.Errorf("%w: first parameter should be a repo:tag", zerr.ErrInvalidArgs)
}
minuendStr = args[0]
if isRepoTag(args[1]) {
subtrahendStr = args[1]
} else {
minuendArch = args[1]
if len(args) == twoArgs {
return fmt.Errorf("%w: not enough arguments, specified only 1 image with arch", zerr.ErrInvalidArgs)
}
}
if len(args) == twoArgs {
return nil
}
if isRepoTag(args[2]) {
if subtrahendStr == "" {
subtrahendStr = args[2]
} else {
return fmt.Errorf("%w: too many repo:tag inputs", zerr.ErrInvalidArgs)
}
} else {
if subtrahendStr == "" {
return fmt.Errorf("%w: 3rd argument should be a repo:tag", zerr.ErrInvalidArgs)
} else {
subtrahendArch = args[2]
}
}
if len(args) == threeArgs {
return nil
}
if isRepoTag(args[3]) {
return fmt.Errorf("%w: 4th argument should not be a repo:tag but an arch", zerr.ErrInvalidArgs)
} else {
subtrahendArch = args[3]
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
searchConfig, err := GetSearchConfigFromFlags(cmd, searchService)
if err != nil {
return err
}
err = CheckExtEndPointQuery(searchConfig, CVEDiffListForImagesQuery())
if err != nil {
return fmt.Errorf("%w: '%s'", err, CVEDiffListForImagesQuery().Name)
}
// parse the args and determine the input
minuend := getImageIdentifier(minuendStr, minuendArch)
subtrahend := getImageIdentifier(subtrahendStr, subtrahendArch)
return SearchCVEDiffList(searchConfig, minuend, subtrahend)
},
}
return imagesByCVEIDCmd
}
func isRepoTag(arg string) bool {
_, _, _, err := zcommon.GetRepoReference(arg) //nolint:dogsled
return err == nil
}
type osArch struct {
Os string
Arch string
}
type ImageIdentifier struct {
Repo string `json:"repo"`
Tag string `json:"tag"`
Digest string `json:"digest"`
Platform *osArch `json:"platform"`
}
func getImageIdentifier(repoTagStr, platformStr string) ImageIdentifier {
var tag, digest string
repo, ref, isTag, err := zcommon.GetRepoReference(repoTagStr)
if err != nil {
return ImageIdentifier{}
}
if isTag {
tag = ref
} else {
digest = ref
}
// check if the following input is a repo:tag or repo@digest, if not then it's a platform
var platform *osArch
if platformStr != "" {
os, arch, _ := strings.Cut(platformStr, "/")
platform = &osArch{Os: os, Arch: arch}
}
return ImageIdentifier{
Repo: repo,
Tag: tag,
Digest: digest,
Platform: platform,
}
}

View file

@ -22,6 +22,7 @@ const (
DebugFlag = "debug"
SearchedCVEID = "cve-id"
SortByFlag = "sort-by"
PlatformFlag = "platform"
)
const (

View file

@ -25,6 +25,12 @@ func CVEResultForImage() GQLType {
}
}
func CVEDiffResult() GQLType {
return GQLType{
Name: "CVEDiffResult",
}
}
func PaginatedImagesResult() GQLType {
return GQLType{
Name: "PaginatedImagesResult",
@ -51,6 +57,14 @@ func ImageListQuery() GQLQuery {
}
}
func CVEDiffListForImagesQuery() GQLQuery {
return GQLQuery{
Name: "CVEDiffListForImages",
Args: []string{"minuend", "subtrahend", "requestedPage", "searchedCVE", "excludedCVE"},
ReturnType: CVEDiffResult(),
}
}
func ImageListForDigestQuery() GQLQuery {
return GQLQuery{
Name: "ImageListForDigest",

View file

@ -1077,6 +1077,20 @@ type mockService struct {
getFixedTagsForCVEGQLFn func(ctx context.Context, config SearchConfig, username, password,
imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error)
getCVEDiffListGQLFn func(ctx context.Context, config SearchConfig, username, password string,
minuend, subtrahend ImageIdentifier,
) (*cveDiffListResp, error)
}
func (service mockService) getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string,
minuend, subtrahend ImageIdentifier,
) (*cveDiffListResp, error) {
if service.getCVEDiffListGQLFn != nil {
return service.getCVEDiffListGQLFn(ctx, config, username, password, minuend, subtrahend)
}
return &cveDiffListResp{}, nil
}
func (service mockService) getRepos(ctx context.Context, config SearchConfig, username,

View file

@ -267,6 +267,52 @@ func SearchCVEForImageGQL(config SearchConfig, image, searchedCveID string) erro
return nil
}
func SearchCVEDiffList(config SearchConfig, minuend, subtrahend ImageIdentifier) error {
username, password := getUsernameAndPassword(config.User)
response, err := config.SearchService.getCVEDiffListGQL(context.Background(), config, username, password,
minuend, subtrahend)
if err != nil {
return err
}
cveDiffResult := response.Data.CveDiffResult
result := cveResult{
Data: cveData{
CVEListForImage: cveListForImage{
Tag: cveDiffResult.Minuend.Tag,
CVEList: cveDiffResult.CVEList,
Summary: cveDiffResult.Summary,
},
},
}
var builder strings.Builder
if config.OutputFormat == defaultOutputFormat || config.OutputFormat == "" {
imageCVESummary := result.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())
}
out, err := result.string(config.OutputFormat)
if err != nil {
return err
}
fmt.Fprint(config.ResultWriter, out)
return nil
}
func SearchImagesByCVEIDGQL(config SearchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(config.User)
ctx, cancel := context.WithCancel(context.Background())

View file

@ -48,6 +48,9 @@ type SearchService interface { //nolint:interfacebloat
baseImage string) (*common.BaseImageListResponse, error)
getReferrersGQL(ctx context.Context, config SearchConfig, username, password string,
repo, digest string) (*common.ReferrersResp, error)
getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string,
minuend, subtrahend ImageIdentifier,
) (*cveDiffListResp, error)
globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
query string) (*common.GlobalSearch, error)
@ -146,6 +149,46 @@ func (service searchService) getReferrersGQL(ctx context.Context, config SearchC
return result, nil
}
func (service searchService) getCVEDiffListGQL(ctx context.Context, config SearchConfig, username, password string,
minuend, subtrahend ImageIdentifier,
) (*cveDiffListResp, error) {
minuendInput := getImageInput(minuend)
subtrahendInput := getImageInput(subtrahend)
query := fmt.Sprintf(`
{
CVEDiffListForImages( minuend: %s, subtrahend: %s ) {
Minuend {Repo Tag}
Subtrahend {Repo Tag}
CVEList {
Id Title Description Severity Reference
PackageList {Name InstalledVersion FixedVersion}
}
Summary {
Count UnknownCount LowCount MediumCount HighCount CriticalCount
}
Page {TotalCount ItemCount}
}
}`, minuendInput, subtrahendInput)
result := &cveDiffListResp{}
err := service.makeGraphQLQuery(ctx, config, username, password, query, result)
if errResult := checkResultGraphQLQuery(ctx, err, result.Errors); errResult != nil {
return nil, errResult
}
return result, nil
}
func getImageInput(img ImageIdentifier) string {
platform := ""
if img.Platform != nil {
platform = fmt.Sprintf(`, Platform: {Os: "%s", Arch: "%s"}`, img.Platform.Os, img.Platform.Arch)
}
return fmt.Sprintf(`{Repo: "%s", Tag: "%s", Digest: "%s"%s}`, img.Repo, img.Tag, img.Digest, platform)
}
func (service searchService) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
query string,
) (*common.GlobalSearch, error) {
@ -746,6 +789,22 @@ type cve struct {
PackageList []packageList `json:"PackageList"`
}
type cveDiffListResp struct {
Data cveDiffResultsForImages `json:"data"`
Errors []common.ErrorGQL `json:"errors"`
}
type cveDiffResultsForImages struct {
CveDiffResult cveDiffResult `json:"cveDiffListForImages"`
}
type cveDiffResult struct {
Minuend ImageIdentifier `json:"minuend"`
Subtrahend ImageIdentifier `json:"subtrahend"`
CVEList []cve `json:"cveList"`
Summary common.ImageVulnerabilitySummary `json:"summary"`
}
//nolint:tagliatelle // graphQL schema
type cveListForImage struct {
Tag string `json:"Tag"`
@ -755,7 +814,7 @@ type cveListForImage struct {
//nolint:tagliatelle // graphQL schema
type cveData struct {
CVEListForImage cveListForImage `json:"CVEListForImage"`
CVEListForImage cveListForImage `json:"cveListForImage"`
}
func (cve cveResult) string(format string) (string, error) {

View file

@ -112,6 +112,17 @@ type HistoryDescription struct {
EmptyLayer bool `json:"emptyLayer"`
}
type OsArch struct {
Os, Arch string
}
type ImageIdentifier struct {
Repo string
Tag string
Digest string
Platform OsArch
}
type Referrer struct {
MediaType string `json:"mediatype"`
ArtifactType string `json:"artifacttype"`

View file

@ -21,8 +21,10 @@ import (
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, excludedCVE string, severity string,
GetCVEListForImage(ctx context.Context, repo, tag string, searchedCVE, excludedCVE string, severity string,
pageinput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error)
GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE, excludedCVE string,
pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error)
GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string) (cvemodel.ImageCVESummary, error)
}
@ -329,8 +331,8 @@ func getConfigAndDigest(metaDB mTypes.MetaDB, manifestDigestStr string) (ispec.I
return manifestData.Manifests[0].Config, manifestDigest, err
}
func filterCVEList(
cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string, pageFinder *CvePageFinder,
func filterCVEMap(cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string,
pageFinder *CvePageFinder,
) {
searchedCVE = strings.ToUpper(searchedCVE)
@ -349,8 +351,26 @@ func filterCVEList(
}
}
func filterCVEList(cveList []cvemodel.CVE, searchedCVE, excludedCVE, severity string, pageFinder *CvePageFinder) {
searchedCVE = strings.ToUpper(searchedCVE)
for _, cve := range cveList {
if severity != "" && (cvemodel.CompareSeverities(cve.Severity, severity) != 0) {
continue
}
if excludedCVE != "" && cve.ContainsStr(excludedCVE) {
continue
}
if cve.ContainsStr(searchedCVE) {
pageFinder.Add(cve)
}
}
}
func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref string, searchedCVE string,
excludedCVE string, severity string, pageInput cvemodel.PageInput,
excludedCVE, severity string, pageInput cvemodel.PageInput,
) (
[]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error,
) {
@ -379,13 +399,98 @@ func (cveinfo BaseCveInfo) GetCVEListForImage(ctx context.Context, repo, ref str
return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err
}
filterCVEList(cveMap, searchedCVE, excludedCVE, severity, pageFinder)
filterCVEMap(cveMap, searchedCVE, excludedCVE, severity, pageFinder)
cveList, pageInfo := pageFinder.Page()
return cveList, imageCVESummary, pageInfo, nil
}
func (cveinfo BaseCveInfo) GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE string,
excludedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, cvemodel.ImageCVESummary, zcommon.PageInfo, error) {
minuendRepo, minuendRef, _ := zcommon.GetImageDirAndReference(minuend)
subtrahendRepo, subtrahendRef, _ := zcommon.GetImageDirAndReference(subtrahend)
// get the CVEs of image and comparedImage
minuendCVEList, _, _, err := cveinfo.GetCVEListForImage(ctx, minuendRepo, minuendRef, searchedCVE, excludedCVE,
"", cvemodel.PageInput{})
if err != nil {
return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err
}
subtrahendCVEList, _, _, err := cveinfo.GetCVEListForImage(ctx, subtrahendRepo, subtrahendRef,
searchedCVE, excludedCVE, "", cvemodel.PageInput{})
if err != nil {
return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err
}
subtrahendCVEMap := map[string]cvemodel.CVE{}
for _, cve := range subtrahendCVEList {
cve := cve
subtrahendCVEMap[cve.ID] = cve
}
var (
count int
unknownCount int
lowCount int
mediumCount int
highCount int
criticalCount int
maxSeverity string
diffCVEs = []cvemodel.CVE{}
)
for i := range minuendCVEList {
if _, ok := subtrahendCVEMap[minuendCVEList[i].ID]; !ok {
diffCVEs = append(diffCVEs, minuendCVEList[i])
switch minuendCVEList[i].Severity {
case cvemodel.SeverityUnknown:
unknownCount++
case cvemodel.SeverityLow:
lowCount++
case cvemodel.SeverityMedium:
mediumCount++
case cvemodel.SeverityHigh:
highCount++
case cvemodel.SeverityCritical:
criticalCount++
}
if cvemodel.CompareSeverities(maxSeverity, minuendCVEList[i].Severity) > 0 {
maxSeverity = minuendCVEList[i].Severity
}
}
}
pageFinder, err := NewCvePageFinder(pageInput.Limit, pageInput.Offset, pageInput.SortBy)
if err != nil {
return nil, cvemodel.ImageCVESummary{}, zcommon.PageInfo{}, err
}
filterCVEList(diffCVEs, "", "", "", pageFinder)
cveList, pageInfo := pageFinder.Page()
count = unknownCount + lowCount + mediumCount + highCount + criticalCount
diffCVESummary := cvemodel.ImageCVESummary{
Count: count,
UnknownCount: unknownCount,
LowCount: lowCount,
MediumCount: mediumCount,
HighCount: highCount,
CriticalCount: criticalCount,
MaxSeverity: maxSeverity,
}
return cveList, diffCVESummary, pageInfo, nil
}
func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string,
) (cvemodel.ImageCVESummary, error) {
// There are several cases, expected returned values below:

View file

@ -1277,6 +1277,11 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo
So(cveSummary.CriticalCount, ShouldEqual, 2)
So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL")
_, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1@"+image13Digest, "", "", pageInput)
So(err, ShouldBeNil)
_, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1:0.1.0", "", "", pageInput)
So(err, ShouldBeNil)
// Image is multiarch
cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", "", "", pageInput)
So(err, ShouldBeNil)
@ -1625,6 +1630,35 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo
_, err = cveInfo.GetImageListForCVE(ctx, repoMultiarch, "CVE1")
So(err, ShouldBeNil)
cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{
IsImageFormatScannableFn: func(repo, reference string) (bool, error) {
return true, nil
},
ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
return nil, zerr.ErrTypeAssertionFailed
},
}, MetaDB: metaDB}
_, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo1:0.1.0", "", "", pageInput)
So(err, ShouldNotBeNil)
try := 0
cveInfo = cveinfo.BaseCveInfo{Log: log, Scanner: mocks.CveScannerMock{
IsImageFormatScannableFn: func(repo, reference string) (bool, error) {
return true, nil
},
ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
if try == 1 {
return nil, zerr.ErrTypeAssertionFailed
}
try++
return make(map[string]cvemodel.CVE), nil
},
}, MetaDB: metaDB}
_, _, _, err = cveInfo.GetCVEDiffListForImages(ctx, "repo8:1.0.0", "repo6:0.1.0", "", "", pageInput)
So(err, ShouldNotBeNil)
})
}

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,20 @@ type Cve struct {
PackageList []*PackageInfo `json:"PackageList,omitempty"`
}
// Contains the diff results of subtracting Subtrahend's CVEs from Minuend's CVEs
type CVEDiffResult struct {
// Minuend is the image from which CVE's we subtract
Minuend *ImageIdentifier `json:"Minuend"`
// Subtrahend is the image which CVE's are subtracted
Subtrahend *ImageIdentifier `json:"Subtrahend"`
// List of CVE objects which are present in minuend but not in subtrahend
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"`
}
// Contains the tag of the image and a list of CVEs
type CVEResultForImage struct {
// Tag affected by the CVEs
@ -92,6 +106,30 @@ type HistoryDescription struct {
EmptyLayer *bool `json:"EmptyLayer,omitempty"`
}
// ImageIdentifier
type ImageIdentifier struct {
// Repo name of the image
Repo string `json:"Repo"`
// The tag of the image
Tag string `json:"Tag"`
// The digest of the image
Digest *string `json:"Digest,omitempty"`
// The platform of the image
Platform *Platform `json:"Platform,omitempty"`
}
// ImageInput
type ImageInput struct {
// Repo name of the image
Repo string `json:"Repo"`
// The tag of the image
Tag string `json:"Tag"`
// The digest of the image
Digest *string `json:"Digest,omitempty"`
// The platform of the image
Platform *PlatformInput `json:"Platform,omitempty"`
}
// Details about a specific image, it is used by queries returning a list of images
// We define an image as a pairing or a repository and a tag belonging to that repository
type ImageSummary struct {
@ -264,6 +302,14 @@ type Platform struct {
Arch *string `json:"Arch,omitempty"`
}
// PlatformInput contains the Os and the Arch of the input image
type PlatformInput struct {
// The os of the image
Os *string `json:"Os,omitempty"`
// The arch of the image
Arch *string `json:"Arch,omitempty"`
}
// Queries supported by the zot server
type Query struct {
}

View file

@ -279,6 +279,260 @@ func getCVEListForImage(
}, nil
}
func getCVEDiffListForImages(
ctx context.Context, //nolint:unparam // may be used in the future to filter by permissions
minuend gql_generated.ImageInput,
subtrahend gql_generated.ImageInput,
metaDB mTypes.MetaDB,
cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput,
searchedCVE string,
excludedCVE string,
log log.Logger, //nolint:unparam // may be used by devs for debugging
) (*gql_generated.CVEDiffResult, error) {
minuend, err := resolveImageData(ctx, minuend, metaDB)
if err != nil {
return nil, err
}
resultMinuend := getImageIdentifier(minuend)
resultSubtrahend := gql_generated.ImageIdentifier{}
if subtrahend.Repo != "" {
subtrahend, err = resolveImageData(ctx, subtrahend, metaDB)
if err != nil {
return nil, err
}
resultSubtrahend = getImageIdentifier(subtrahend)
} else {
// search for base images
// get minuend image meta
minuendSummary, err := metaDB.GetImageMeta(godigest.Digest(deref(minuend.Digest, "")))
if err != nil {
return &gql_generated.CVEDiffResult{}, err
}
// get the base images for the minuend
minuendBaseImages, err := metaDB.FilterTags(ctx, mTypes.AcceptOnlyRepo(minuend.Repo),
filterBaseImagesForMeta(minuendSummary))
if err != nil {
return &gql_generated.CVEDiffResult{}, err
}
// get the best base image as subtrahend
// get the one with most layers in common
imgLayers := map[string]struct{}{}
for _, layer := range minuendSummary.Manifests[0].Manifest.Layers {
imgLayers[layer.Digest.String()] = struct{}{}
}
bestMatchingScore := 0
for _, baseImage := range minuendBaseImages {
for _, baseManifest := range baseImage.Manifests {
currentMatchingScore := 0
for _, layer := range baseManifest.Manifest.Layers {
if _, ok := imgLayers[layer.Digest.String()]; ok {
currentMatchingScore++
}
}
if currentMatchingScore > bestMatchingScore {
bestMatchingScore = currentMatchingScore
resultSubtrahend = gql_generated.ImageIdentifier{
Repo: baseImage.Repo,
Tag: baseImage.Tag,
Digest: ref(baseImage.Manifests[0].Digest.String()),
Platform: &gql_generated.Platform{
Os: ref(baseImage.Manifests[0].Config.OS),
Arch: ref(getArch(baseImage.Manifests[0].Config.Platform)),
},
}
subtrahend.Repo = baseImage.Repo
subtrahend.Tag = baseImage.Tag
subtrahend.Digest = ref(baseImage.Manifests[0].Digest.String())
}
}
}
}
minuendRepoRef := minuend.Repo + "@" + deref(minuend.Digest, "")
subtrahendRepoRef := subtrahend.Repo + "@" + deref(subtrahend.Digest, "")
page := dderef(requestedPage)
diffCVEs, diffSummary, _, err := cveInfo.GetCVEDiffListForImages(ctx, minuendRepoRef, subtrahendRepoRef, searchedCVE,
excludedCVE, cvemodel.PageInput{
Limit: deref(page.Limit, 0),
Offset: deref(page.Offset, 0),
SortBy: cvemodel.SortCriteria(deref(page.SortBy, gql_generated.SortCriteriaSeverity)),
})
if err != nil {
return nil, err
}
cveids := []*gql_generated.Cve{}
for _, cveDetail := range diffCVEs {
vulID := cveDetail.ID
desc := cveDetail.Description
title := cveDetail.Title
severity := cveDetail.Severity
referenceURL := cveDetail.Reference
pkgList := make([]*gql_generated.PackageInfo, 0)
for _, pkg := range cveDetail.PackageList {
pkg := pkg
pkgList = append(pkgList,
&gql_generated.PackageInfo{
Name: &pkg.Name,
InstalledVersion: &pkg.InstalledVersion,
FixedVersion: &pkg.FixedVersion,
},
)
}
cveids = append(cveids,
&gql_generated.Cve{
ID: &vulID,
Title: &title,
Description: &desc,
Severity: &severity,
Reference: &referenceURL,
PackageList: pkgList,
},
)
}
return &gql_generated.CVEDiffResult{
Minuend: &resultMinuend,
Subtrahend: &resultSubtrahend,
Summary: &gql_generated.ImageVulnerabilitySummary{
Count: &diffSummary.Count,
UnknownCount: &diffSummary.UnknownCount,
LowCount: &diffSummary.LowCount,
MediumCount: &diffSummary.MediumCount,
HighCount: &diffSummary.HighCount,
CriticalCount: &diffSummary.CriticalCount,
MaxSeverity: &diffSummary.MaxSeverity,
},
CVEList: cveids,
Page: &gql_generated.PageInfo{},
}, nil
}
func getImageIdentifier(img gql_generated.ImageInput) gql_generated.ImageIdentifier {
return gql_generated.ImageIdentifier{
Repo: img.Repo,
Tag: img.Tag,
Digest: img.Digest,
Platform: getIdentifierPlatform(img.Platform),
}
}
func getIdentifierPlatform(platform *gql_generated.PlatformInput) *gql_generated.Platform {
if platform == nil {
return nil
}
return &gql_generated.Platform{
Os: platform.Os,
Arch: platform.Arch,
}
}
// rename idea: identify image from input.
func resolveImageData(ctx context.Context, imageInput gql_generated.ImageInput, metaDB mTypes.MetaDB,
) (gql_generated.ImageInput, error) {
if imageInput.Repo == "" {
return gql_generated.ImageInput{}, zerr.ErrEmptyRepoName
}
if imageInput.Tag == "" {
return gql_generated.ImageInput{}, zerr.ErrEmptyTag
}
// try checking if the tag is a simple image first
repoMeta, err := metaDB.GetRepoMeta(ctx, imageInput.Repo)
if err != nil {
return gql_generated.ImageInput{}, err
}
descriptor, ok := repoMeta.Tags[imageInput.Tag]
if !ok {
return gql_generated.ImageInput{}, zerr.ErrImageNotFound
}
switch descriptor.MediaType {
case ispec.MediaTypeImageManifest:
imageInput.Digest = ref(descriptor.Digest)
return imageInput, nil
case ispec.MediaTypeImageIndex:
if dderef(imageInput.Digest) == "" && !isPlatformSpecified(imageInput.Platform) {
return gql_generated.ImageInput{},
fmt.Errorf("%w: platform or specific manifest digest needed", zerr.ErrAmbiguousInput)
}
imageMeta, err := metaDB.GetImageMeta(godigest.Digest(descriptor.Digest))
if err != nil {
return gql_generated.ImageInput{}, err
}
for _, manifest := range imageMeta.Manifests {
if manifest.Digest.String() == dderef(imageInput.Digest) ||
isMatchingPlatform(manifest.Config.Platform, dderef(imageInput.Platform)) {
imageInput.Digest = ref(manifest.Digest.String())
imageInput.Platform = &gql_generated.PlatformInput{
Os: ref(manifest.Config.OS),
Arch: ref(getArch(manifest.Config.Platform)),
}
return imageInput, nil
}
}
return imageInput, zerr.ErrImageNotFound
}
return imageInput, nil
}
func isPlatformSpecified(platformInput *gql_generated.PlatformInput) bool {
if platformInput == nil {
return false
}
if dderef(platformInput.Os) == "" || dderef(platformInput.Arch) == "" {
return false
}
return true
}
func isMatchingPlatform(platform ispec.Platform, platformInput gql_generated.PlatformInput) bool {
if platform.OS != deref(platformInput.Os, "") {
return false
}
arch := getArch(platform)
return arch == deref(platformInput.Arch, "")
}
func getArch(platform ispec.Platform) string {
arch := platform.Architecture
if arch != "" && platform.Variant != "" {
arch += "/" + platform.Variant
}
return arch
}
func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
manifestDigest := imageMeta.Manifests[0].Digest.String()
@ -1001,6 +1255,47 @@ func filterBaseImages(image *gql_generated.ImageSummary) mTypes.FilterFunc {
}
}
func filterBaseImagesForMeta(image mTypes.ImageMeta) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
var addImageToList bool
manifest := imageMeta.Manifests[0]
for i := range image.Manifests {
manifestDigest := manifest.Digest.String()
if manifestDigest == image.Manifests[i].Digest.String() {
return false
}
addImageToList = true
for _, l := range manifest.Manifest.Layers {
foundLayer := false
for _, k := range image.Manifests[i].Manifest.Layers {
if l.Digest.String() == k.Digest.String() {
foundLayer = true
break
}
}
if !foundLayer {
addImageToList = false
break
}
}
if addImageToList {
return true
}
}
return false
}
}
func validateGlobalSearchInput(query string, filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput,
) error {
@ -1187,6 +1482,17 @@ func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i]
}
// dderef is a default deref.
func dderef[T any](pointer *T) T {
var defValue T
if pointer != nil {
return *pointer
}
return defValue
}
func deref[T any](pointer *T, defaultVal T) T {
if pointer != nil {
return *pointer
@ -1195,6 +1501,12 @@ func deref[T any](pointer *T, defaultVal T) T {
return defaultVal
}
func ref[T any](input T) *T {
ref := input
return &ref
}
func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam
) (*gql_generated.PaginatedImagesResult, error) {

View file

@ -14,6 +14,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/errors"
"zotregistry.dev/zot/pkg/common"
"zotregistry.dev/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve"
@ -730,6 +731,50 @@ func TestQueryResolverErrors(t *testing.T) {
So(err, ShouldNotBeNil)
})
Convey("CVEDiffListForImages nill cveinfo", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{},
},
mocks.MetaDBMock{
GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool,
) ([]mTypes.RepoMeta, error) {
return []mTypes.RepoMeta{}, ErrTestError
},
},
nil,
)
qr := queryResolver{resolverConfig}
_, err := qr.CVEDiffListForImages(ctx, gql_generated.ImageInput{}, gql_generated.ImageInput{},
&gql_generated.PageInput{}, nil, nil)
So(err, ShouldNotBeNil)
})
Convey("CVEDiffListForImages error", func() {
resolverConfig := NewResolver(
log,
storage.StoreController{
DefaultStore: mocks.MockedImageStore{},
},
mocks.MetaDBMock{
GetMultipleRepoMetaFn: func(ctx context.Context, filter func(repoMeta mTypes.RepoMeta) bool,
) ([]mTypes.RepoMeta, error) {
return []mTypes.RepoMeta{}, ErrTestError
},
},
mocks.CveInfoMock{},
)
qr := queryResolver{resolverConfig}
_, err := qr.CVEDiffListForImages(ctx, gql_generated.ImageInput{}, gql_generated.ImageInput{},
&gql_generated.PageInput{}, nil, nil)
So(err, ShouldNotBeNil)
})
Convey("ImageListForCve error in GetMultipleRepoMeta", func() {
resolverConfig := NewResolver(
log,
@ -1883,6 +1928,272 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
)
So(err, ShouldNotBeNil)
})
Convey("CVE Diff between images", t, func() {
// image := "image:tag"
// baseImage := "base:basetag"
ctx := context.Background()
pageInput := &gql_generated.PageInput{
SortBy: ref(gql_generated.SortCriteriaAlphabeticAsc),
}
boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: t.TempDir()})
if err != nil {
panic(err)
}
metaDB, err := boltdb.New(boltDriver, log)
if err != nil {
panic(err)
}
layer1 := []byte{10, 20, 30}
layer2 := []byte{11, 21, 31}
layer3 := []byte{12, 22, 23}
otherImage := CreateImageWith().LayerBlobs([][]byte{
layer1,
}).DefaultConfig().Build()
baseImage := CreateImageWith().LayerBlobs([][]byte{
layer1,
layer2,
}).PlatformConfig("testArch", "testOs").Build()
image := CreateImageWith().LayerBlobs([][]byte{
layer1,
layer2,
layer3,
}).PlatformConfig("testArch", "testOs").Build()
multiArchBase := CreateMultiarchWith().Images([]Image{baseImage, CreateRandomImage(), CreateRandomImage()}).
Build()
multiArchImage := CreateMultiarchWith().Images([]Image{image, CreateRandomImage(), CreateRandomImage()}).
Build()
getCveResults := func(digestStr string) map[string]cvemodel.CVE {
switch digestStr {
case image.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
PackageList: []cvemodel.Package{{}},
},
"CVE3": {
ID: "CVE3",
Severity: "LOW",
Title: "Title CVE3",
Description: "Description CVE3",
PackageList: []cvemodel.Package{{}},
},
}
case baseImage.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
"CVE2": {
ID: "CVE2",
Severity: "MEDIUM",
Title: "Title CVE2",
Description: "Description CVE2",
PackageList: []cvemodel.Package{{}},
},
}
case otherImage.DigestStr():
return map[string]cvemodel.CVE{
"CVE1": {
ID: "CVE1",
Severity: "HIGH",
Title: "Title CVE1",
Description: "Description CVE1",
PackageList: []cvemodel.Package{{}},
},
}
}
// By default the image has no vulnerabilities
return map[string]cvemodel.CVE{}
}
// MetaDB loaded with initial data, now mock the scanner
// Setup test CVE data in mock scanner
scanner := mocks.CveScannerMock{
ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) {
repo, ref, _, _ := common.GetRepoReference(image)
if common.IsDigest(ref) {
return getCveResults(ref), nil
}
repoMeta, _ := metaDB.GetRepoMeta(ctx, repo)
if _, ok := repoMeta.Tags[ref]; !ok {
panic("unexpected tag '" + ref + "', test might be wrong")
}
return getCveResults(repoMeta.Tags[ref].Digest), nil
},
GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE {
return getCveResults(digestStr)
},
IsResultCachedFn: func(digestStr string) bool {
return true
},
}
cveInfo := &cveinfo.BaseCveInfo{
Log: log,
Scanner: scanner,
MetaDB: metaDB,
}
ctx, err = ociutils.InitializeTestMetaDB(ctx, metaDB,
ociutils.Repo{
Name: "repo",
Images: []ociutils.RepoImage{
{Image: otherImage, Reference: "other-image"},
{Image: baseImage, Reference: "base-image"},
{Image: image, Reference: "image"},
},
},
ociutils.Repo{
Name: "repo-multi",
MultiArchImages: []ociutils.RepoMultiArchImage{
{MultiarchImage: CreateRandomMultiarch(), Reference: "multi-rand"},
{MultiarchImage: multiArchBase, Reference: "multi-base"},
{MultiarchImage: multiArchImage, Reference: "multi-img"},
},
},
)
So(err, ShouldBeNil)
minuend := gql_generated.ImageInput{Repo: "repo", Tag: "image"}
subtrahend := gql_generated.ImageInput{Repo: "repo", Tag: "image"}
diffResult, err := getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldBeNil)
So(len(diffResult.CVEList), ShouldEqual, 0)
minuend = gql_generated.ImageInput{Repo: "repo", Tag: "image"}
subtrahend = gql_generated.ImageInput{}
diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldBeNil)
So(len(diffResult.CVEList), ShouldEqual, 1)
minuend = gql_generated.ImageInput{Repo: "repo", Tag: "base-image"}
subtrahend = gql_generated.ImageInput{Repo: "repo", Tag: "image"}
diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldBeNil)
So(len(diffResult.CVEList), ShouldEqual, 0)
minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{}}
subtrahend = gql_generated.ImageInput{}
_, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldNotBeNil)
minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{
Os: ref("testOs"),
Arch: ref("testArch"),
}}
subtrahend = gql_generated.ImageInput{}
diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldBeNil)
So(len(diffResult.CVEList), ShouldEqual, 1)
So(diffResult.Subtrahend.Repo, ShouldEqual, "repo-multi")
So(diffResult.Subtrahend.Tag, ShouldEqual, "multi-base")
So(dderef(diffResult.Subtrahend.Platform.Os), ShouldResemble, "testOs")
So(dderef(diffResult.Subtrahend.Platform.Arch), ShouldResemble, "testArch")
minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{
Os: ref("testOs"),
Arch: ref("testArch"),
}}
subtrahend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-base", Platform: &gql_generated.PlatformInput{
Os: ref("testOs"),
Arch: ref("testArch"),
}}
diffResult, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldBeNil)
So(len(diffResult.CVEList), ShouldEqual, 1)
minuend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-img", Platform: &gql_generated.PlatformInput{
Os: ref("testOs"),
Arch: ref("testArch"),
}}
subtrahend = gql_generated.ImageInput{Repo: "repo-multi", Tag: "multi-base", Platform: &gql_generated.PlatformInput{}}
_, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, pageInput, "", "", log)
So(err, ShouldNotBeNil)
})
Convey("CVE Diff Errors", t, func() {
ctx := context.Background()
metaDB := mocks.MetaDBMock{}
cveInfo := mocks.CveInfoMock{}
emptyImage := gql_generated.ImageInput{}
Convey("minuend is empty", func() {
_, err := getCVEDiffListForImages(ctx, emptyImage, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
})
Convey("no ", func() {
minuend := gql_generated.ImageInput{Repo: "repo", Tag: "bad-tag"}
_, err := getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
})
Convey("getImageSummary for subtrahend errors", func() {
metaDB.GetRepoMetaFn = func(ctx context.Context, repo string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{}, ErrTestError
}
minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"}
_, err := getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
metaDB.GetRepoMetaFn = func(ctx context.Context, repo string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{}, zerr.ErrRepoMetaNotFound
}
minuend = gql_generated.ImageInput{Repo: "test", Tag: "img"}
_, err = getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
})
Convey("FilterTags for subtrahend errors", func() {
metaDB.FilterTagsFn = func(ctx context.Context, filterRepoTag mTypes.FilterRepoTagFunc, filterFunc mTypes.FilterFunc,
) ([]mTypes.FullImageMeta, error) {
return nil, ErrTestError
}
minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"}
_, err = getCVEDiffListForImages(ctx, minuend, emptyImage, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
})
Convey("GetCVEDiffListForImages errors", func() {
cveInfo.GetCVEDiffListForImagesFn = func(ctx context.Context, minuend, subtrahend, searchedCVE, excluded string,
pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) {
return nil, cvemodel.ImageCVESummary{}, common.PageInfo{}, ErrTestError
}
minuend := gql_generated.ImageInput{Repo: "test", Tag: "img"}
subtrahend := gql_generated.ImageInput{Repo: "sub", Tag: "img"}
_, err = getCVEDiffListForImages(ctx, minuend, subtrahend, metaDB, cveInfo, getGQLPageInput(0, 0), "", "", log)
So(err, ShouldNotBeNil)
})
})
}
func TestMockedDerivedImageList(t *testing.T) {
@ -2352,10 +2663,67 @@ func TestExpandedRepoInfoErrors(t *testing.T) {
})
}
func ref[T any](input T) *T {
ref := input
func TestUtils(t *testing.T) {
Convey("utils", t, func() {
Convey("", func() {
So(isMatchingPlatform(ispec.Platform{OS: "test"}, gql_generated.PlatformInput{Os: ref("t")}), ShouldBeFalse)
So(getArch(ispec.Platform{OS: "t", Architecture: "e", Variant: "st"}), ShouldResemble, "e/st")
})
return &ref
Convey("checkImageInput", func() {
_, err := resolveImageData(context.Background(), gql_generated.ImageInput{Repo: "test"}, mocks.MetaDBMock{})
So(err, ShouldNotBeNil)
})
Convey("checkImageInput can't find index data", func() {
_, err := resolveImageData(context.Background(), gql_generated.ImageInput{
Repo: "test", Tag: "test", Digest: ref("dig"),
},
mocks.MetaDBMock{
GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{
"test": {MediaType: ispec.MediaTypeImageIndex},
}}, nil
},
GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) {
return mTypes.ImageMeta{}, ErrTestError
},
})
So(err, ShouldNotBeNil)
})
Convey("checkImageInput image meta not found", func() {
_, err := resolveImageData(context.Background(), gql_generated.ImageInput{
Repo: "test", Tag: "test", Digest: ref("dig"),
},
mocks.MetaDBMock{
GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{
"test": {MediaType: ispec.MediaTypeImageIndex},
}}, nil
},
GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) {
return mTypes.ImageMeta{}, nil
},
})
So(err, ShouldNotBeNil)
})
Convey("checkImageInput image meta bad media type", func() {
_, err := resolveImageData(context.Background(), gql_generated.ImageInput{
Repo: "test", Tag: "test", Digest: ref("dig"),
},
mocks.MetaDBMock{
GetRepoMetaFn: func(ctx context.Context, repo string) (mTypes.RepoMeta, error) {
return mTypes.RepoMeta{Tags: map[string]mTypes.Descriptor{
"test": {MediaType: "bad-type"},
}}, nil
},
GetImageMetaFn: func(digest godigest.Digest) (mTypes.ImageMeta, error) {
return mTypes.ImageMeta{}, nil
},
})
So(err, ShouldBeNil)
})
})
}
func getGQLPageInput(limit int, offset int) *gql_generated.PageInput {

View file

@ -33,6 +33,54 @@ type CVEResultForImage {
Page: PageInfo
}
"""
ImageIdentifier
"""
type ImageIdentifier {
"""
Repo name of the image
"""
Repo: String!
"""
The tag of the image
"""
Tag: String!
"""
The digest of the image
"""
Digest: String
"""
The platform of the image
"""
Platform: Platform
}
"""
Contains the diff results of subtracting Subtrahend's CVEs from Minuend's CVEs
"""
type CVEDiffResult {
"""
Minuend is the image from which CVE's we subtract
"""
Minuend: ImageIdentifier!
"""
Subtrahend is the image which CVE's are subtracted
"""
Subtrahend: ImageIdentifier!
"""
List of CVE objects which are present in minuend but not in subtrahend
"""
CVEList: [CVE]
"""
Summary of the findings for this image
"""
Summary: ImageVulnerabilitySummary
"""
The CVE pagination information, see PageInfo object for more details
"""
Page: PageInfo
}
"""
Contains various details about the CVE (Common Vulnerabilities and Exposures)
and a list of PackageInfo about the affected packages
@ -567,6 +615,42 @@ input PageInput {
sortBy: SortCriteria
}
"""
PlatformInput contains the Os and the Arch of the input image
"""
input PlatformInput {
"""
The os of the image
"""
Os: String
"""
The arch of the image
"""
Arch: String
}
"""
ImageInput
"""
input ImageInput {
"""
Repo name of the image
"""
Repo: String!
"""
The tag of the image
"""
Tag: String!
"""
The digest of the image
"""
Digest: String
"""
The platform of the image
"""
Platform: PlatformInput
}
"""
Paginated list of RepoSummary objects
"""
@ -645,6 +729,22 @@ type Query {
severity: String
): CVEResultForImage!
"""
Returns a list with CVE's that are present in `image` but not in `comparedImage`
"""
CVEDiffListForImages(
"Image name in format `repository:tag` or `repository@digest`"
minuend: ImageInput!,
"Compared image name in format `repository:tag` or `repository@digest`"
subtrahend: ImageInput!,
"Sets the parameters of the requested page"
requestedPage: PageInput
"Search term for specific CVE by title/id"
searchedCVE: String
"Search term that must not be present in the returned results"
excludedCVE: String
): CVEDiffResult!
"""
Returns a list of images vulnerable to the CVE of the specified ID
"""

View file

@ -23,6 +23,16 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string, reque
return getCVEListForImage(ctx, image, r.cveInfo, requestedPage, deref(searchedCve, ""), deref(excludedCve, ""), deref(severity, ""), r.log)
}
// CVEDiffListForImages is the resolver for the CVEDiffListForImages field.
func (r *queryResolver) CVEDiffListForImages(ctx context.Context, minuend gql_generated.ImageInput, subtrahend gql_generated.ImageInput, requestedPage *gql_generated.PageInput, searchedCve *string, excludedCve *string) (*gql_generated.CVEDiffResult, error) {
if r.cveInfo == nil {
return &gql_generated.CVEDiffResult{}, zerr.ErrCVESearchDisabled
}
return getCVEDiffListForImages(ctx, minuend, subtrahend, r.metaDB, r.cveInfo, requestedPage,
dderef(searchedCve), dderef(excludedCve), r.log)
}
// ImageListForCve is the resolver for the ImageListForCVE field.
func (r *queryResolver) ImageListForCve(ctx context.Context, id string, filter *gql_generated.Filter, requestedPage *gql_generated.PageInput) (*gql_generated.PaginatedImagesResult, error) {
if r.cveInfo == nil {

View file

@ -38,6 +38,10 @@ func AcceptAllRepoTag(repo, tag string) bool {
return true
}
func AcceptOnlyRepo(repo string) func(repo, tag string) bool {
return func(r, t string) bool { return repo == r }
}
func AcceptAllImageMeta(repoMeta RepoMeta, imageMeta ImageMeta) bool {
return true
}

View file

@ -9,11 +9,28 @@ import (
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, reference, searchedCVE, excludedCVE, severity string,
pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error)
GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error)
GetCVEDiffListForImagesFn func(ctx context.Context, minuend, subtrahend, searchedCVE string,
excludedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error)
}
func (cveInfo CveInfoMock) GetCVEDiffListForImages(ctx context.Context, minuend, subtrahend, searchedCVE string,
excludedCVE string, pageInput cvemodel.PageInput,
) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) {
if cveInfo.GetCVEDiffListForImagesFn != nil {
return cveInfo.GetCVEDiffListForImagesFn(ctx, minuend, subtrahend, searchedCVE, excludedCVE, pageInput)
}
return []cvemodel.CVE{}, cvemodel.ImageCVESummary{}, common.PageInfo{}, nil
}
func (cveInfo CveInfoMock) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) {