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:
parent
752b9e87c1
commit
5039128723
20 changed files with 2555 additions and 11 deletions
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
DebugFlag = "debug"
|
||||
SearchedCVEID = "cve-id"
|
||||
SortByFlag = "sort-by"
|
||||
PlatformFlag = "platform"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue