0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-30 22:34:13 -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") ErrAPINotSupported = errors.New("registry at the given address doesn't implement the correct API")
ErrURLNotFound = errors.New("url not found") ErrURLNotFound = errors.New("url not found")
ErrInvalidSearchQuery = errors.New("invalid search query") 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 //nolint:dupl
func TestServerCVEResponse(t *testing.T) { func TestServerCVEResponse(t *testing.T) {
port := test.GetFreePort() port := test.GetFreePort()

View file

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

View file

@ -140,3 +140,139 @@ func NewFixedTagsCommand(searchService SearchService) *cobra.Command {
return fixedTagsCmd 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" DebugFlag = "debug"
SearchedCVEID = "cve-id" SearchedCVEID = "cve-id"
SortByFlag = "sort-by" SortByFlag = "sort-by"
PlatformFlag = "platform"
) )
const ( const (

View file

@ -25,6 +25,12 @@ func CVEResultForImage() GQLType {
} }
} }
func CVEDiffResult() GQLType {
return GQLType{
Name: "CVEDiffResult",
}
}
func PaginatedImagesResult() GQLType { func PaginatedImagesResult() GQLType {
return GQLType{ return GQLType{
Name: "PaginatedImagesResult", 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 { func ImageListForDigestQuery() GQLQuery {
return GQLQuery{ return GQLQuery{
Name: "ImageListForDigest", Name: "ImageListForDigest",

View file

@ -1077,6 +1077,20 @@ type mockService struct {
getFixedTagsForCVEGQLFn func(ctx context.Context, config SearchConfig, username, password, getFixedTagsForCVEGQLFn func(ctx context.Context, config SearchConfig, username, password,
imageName, cveID string, imageName, cveID string,
) (*common.ImageListWithCVEFixedResponse, error) ) (*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, 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 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 { func SearchImagesByCVEIDGQL(config SearchConfig, repo, cveid string) error {
username, password := getUsernameAndPassword(config.User) username, password := getUsernameAndPassword(config.User)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())

View file

@ -48,6 +48,9 @@ type SearchService interface { //nolint:interfacebloat
baseImage string) (*common.BaseImageListResponse, error) baseImage string) (*common.BaseImageListResponse, error)
getReferrersGQL(ctx context.Context, config SearchConfig, username, password string, getReferrersGQL(ctx context.Context, config SearchConfig, username, password string,
repo, digest string) (*common.ReferrersResp, error) 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, globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
query string) (*common.GlobalSearch, error) query string) (*common.GlobalSearch, error)
@ -146,6 +149,46 @@ func (service searchService) getReferrersGQL(ctx context.Context, config SearchC
return result, nil 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, func (service searchService) globalSearchGQL(ctx context.Context, config SearchConfig, username, password string,
query string, query string,
) (*common.GlobalSearch, error) { ) (*common.GlobalSearch, error) {
@ -746,6 +789,22 @@ type cve struct {
PackageList []packageList `json:"PackageList"` 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 //nolint:tagliatelle // graphQL schema
type cveListForImage struct { type cveListForImage struct {
Tag string `json:"Tag"` Tag string `json:"Tag"`
@ -755,7 +814,7 @@ type cveListForImage struct {
//nolint:tagliatelle // graphQL schema //nolint:tagliatelle // graphQL schema
type cveData struct { type cveData struct {
CVEListForImage cveListForImage `json:"CVEListForImage"` CVEListForImage cveListForImage `json:"cveListForImage"`
} }
func (cve cveResult) string(format string) (string, error) { func (cve cveResult) string(format string) (string, error) {

View file

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

View file

@ -21,8 +21,10 @@ import (
type CveInfo interface { type CveInfo interface {
GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixed(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) 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) 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 return manifestData.Manifests[0].Config, manifestDigest, err
} }
func filterCVEList( func filterCVEMap(cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string,
cveMap map[string]cvemodel.CVE, searchedCVE, excludedCVE, severity string, pageFinder *CvePageFinder, pageFinder *CvePageFinder,
) { ) {
searchedCVE = strings.ToUpper(searchedCVE) 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, 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, []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 return []cvemodel.CVE{}, imageCVESummary, zcommon.PageInfo{}, err
} }
filterCVEList(cveMap, searchedCVE, excludedCVE, severity, pageFinder) filterCVEMap(cveMap, searchedCVE, excludedCVE, severity, pageFinder)
cveList, pageInfo := pageFinder.Page() cveList, pageInfo := pageFinder.Page()
return cveList, imageCVESummary, pageInfo, nil 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, func (cveinfo BaseCveInfo) GetCVESummaryForImageMedia(ctx context.Context, repo, digestStr, mediaType string,
) (cvemodel.ImageCVESummary, error) { ) (cvemodel.ImageCVESummary, error) {
// There are several cases, expected returned values below: // 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.CriticalCount, ShouldEqual, 2)
So(cveSummary.MaxSeverity, ShouldEqual, "CRITICAL") 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 // Image is multiarch
cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", "", "", pageInput) cveList, cveSummary, pageInfo, err = cveInfo.GetCVEListForImage(ctx, repoMultiarch, "tagIndex", "", "", "", pageInput)
So(err, ShouldBeNil) So(err, ShouldBeNil)
@ -1625,6 +1630,35 @@ func TestCVEStruct(t *testing.T) { //nolint:gocyclo
_, err = cveInfo.GetImageListForCVE(ctx, repoMultiarch, "CVE1") _, err = cveInfo.GetImageListForCVE(ctx, repoMultiarch, "CVE1")
So(err, ShouldBeNil) 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"` 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 // Contains the tag of the image and a list of CVEs
type CVEResultForImage struct { type CVEResultForImage struct {
// Tag affected by the CVEs // Tag affected by the CVEs
@ -92,6 +106,30 @@ type HistoryDescription struct {
EmptyLayer *bool `json:"EmptyLayer,omitempty"` 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 // 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 // We define an image as a pairing or a repository and a tag belonging to that repository
type ImageSummary struct { type ImageSummary struct {
@ -264,6 +302,14 @@ type Platform struct {
Arch *string `json:"Arch,omitempty"` 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 // Queries supported by the zot server
type Query struct { type Query struct {
} }

View file

@ -279,6 +279,260 @@ func getCVEListForImage(
}, nil }, 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 { func FilterByTagInfo(tagsInfo []cvemodel.TagInfo) mTypes.FilterFunc {
return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool { return func(repoMeta mTypes.RepoMeta, imageMeta mTypes.ImageMeta) bool {
manifestDigest := imageMeta.Manifests[0].Digest.String() 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, func validateGlobalSearchInput(query string, filter *gql_generated.Filter,
requestedPage *gql_generated.PageInput, requestedPage *gql_generated.PageInput,
) error { ) error {
@ -1187,6 +1482,17 @@ func (p timeSlice) Swap(i, j int) {
p[i], p[j] = p[j], p[i] 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 { func deref[T any](pointer *T, defaultVal T) T {
if pointer != nil { if pointer != nil {
return *pointer return *pointer
@ -1195,6 +1501,12 @@ func deref[T any](pointer *T, defaultVal T) T {
return defaultVal 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, func getImageList(ctx context.Context, repo string, metaDB mTypes.MetaDB, cveInfo cveinfo.CveInfo,
requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam requestedPage *gql_generated.PageInput, log log.Logger, //nolint:unparam
) (*gql_generated.PaginatedImagesResult, error) { ) (*gql_generated.PaginatedImagesResult, error) {

View file

@ -14,6 +14,7 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1" ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
zerr "zotregistry.dev/zot/errors"
"zotregistry.dev/zot/pkg/common" "zotregistry.dev/zot/pkg/common"
"zotregistry.dev/zot/pkg/extensions/search/convert" "zotregistry.dev/zot/pkg/extensions/search/convert"
cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve" cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve"
@ -730,6 +731,50 @@ func TestQueryResolverErrors(t *testing.T) {
So(err, ShouldNotBeNil) 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() { Convey("ImageListForCve error in GetMultipleRepoMeta", func() {
resolverConfig := NewResolver( resolverConfig := NewResolver(
log, log,
@ -1883,6 +1928,272 @@ func TestCVEResolvers(t *testing.T) { //nolint:gocyclo
) )
So(err, ShouldNotBeNil) 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) { func TestMockedDerivedImageList(t *testing.T) {
@ -2352,10 +2663,67 @@ func TestExpandedRepoInfoErrors(t *testing.T) {
}) })
} }
func ref[T any](input T) *T { func TestUtils(t *testing.T) {
ref := input 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 { func getGQLPageInput(limit int, offset int) *gql_generated.PageInput {

View file

@ -33,6 +33,54 @@ type CVEResultForImage {
Page: PageInfo 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) Contains various details about the CVE (Common Vulnerabilities and Exposures)
and a list of PackageInfo about the affected packages and a list of PackageInfo about the affected packages
@ -567,6 +615,42 @@ input PageInput {
sortBy: SortCriteria 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 Paginated list of RepoSummary objects
""" """
@ -645,6 +729,22 @@ type Query {
severity: String severity: String
): CVEResultForImage! ): 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 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) 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. // 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) { 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 { if r.cveInfo == nil {

View file

@ -38,6 +38,10 @@ func AcceptAllRepoTag(repo, tag string) bool {
return true 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 { func AcceptAllImageMeta(repoMeta RepoMeta, imageMeta ImageMeta) bool {
return true return true
} }

View file

@ -8,12 +8,29 @@ import (
) )
type CveInfoMock struct { type CveInfoMock struct {
GetImageListForCVEFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) GetImageListForCVEFn func(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error)
GetImageListWithCVEFixedFn 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,
GetCVEListForImageFn func(ctx context.Context, repo, reference, searchedCVE, excludedCVE, severity string,
pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error) pageInput cvemodel.PageInput) ([]cvemodel.CVE, cvemodel.ImageCVESummary, common.PageInfo, error)
GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string, GetCVESummaryForImageMediaFn func(ctx context.Context, repo string, digest, mediaType string,
) (cvemodel.ImageCVESummary, error) ) (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) { func (cveInfo CveInfoMock) GetImageListForCVE(ctx context.Context, repo, cveID string) ([]cvemodel.TagInfo, error) {