diff --git a/pkg/api/controller.go b/pkg/api/controller.go index 51ac71a0..136c55be 100644 --- a/pkg/api/controller.go +++ b/pkg/api/controller.go @@ -455,7 +455,7 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { // Enable extensions if extension config is provided for DefaultStore if c.Config != nil && c.Config.Extensions != nil { ext.EnableMetricsExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) - ext.EnableSearchExtension(c.Config, c.Log, c.Config.Storage.RootDirectory) + ext.EnableSearchExtension(c.Config, c.Log, c.StoreController) } if c.Config.Storage.SubPaths != nil { @@ -468,7 +468,6 @@ func (c *Controller) StartBackgroundTasks(reloadCtx context.Context) { // Enable extensions if extension config is provided for subImageStore if c.Config != nil && c.Config.Extensions != nil { ext.EnableMetricsExtension(c.Config, c.Log, storageConfig.RootDirectory) - ext.EnableSearchExtension(c.Config, c.Log, storageConfig.RootDirectory) } } } diff --git a/pkg/extensions/extension_search.go b/pkg/extensions/extension_search.go index 6aa0005b..906377b9 100644 --- a/pkg/extensions/extension_search.go +++ b/pkg/extensions/extension_search.go @@ -18,7 +18,12 @@ import ( "zotregistry.io/zot/pkg/storage" ) -func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) { +// We need this object to be a singleton as read/writes in the CVE DB may +// occur at any time via DB downloads as well as during scanning. +// The library doesn't seem to handle concurrency very well internally. +var cveInfo cveinfo.CveInfo // nolint:gochecknoglobals + +func EnableSearchExtension(config *config.Config, log log.Logger, storeController storage.StoreController) { if config.Extensions.Search != nil && *config.Extensions.Search.Enable && config.Extensions.Search.CVE != nil { defaultUpdateInterval, _ := time.ParseDuration("2h") @@ -28,9 +33,10 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string log.Warn().Msg("CVE update interval set to too-short interval < 2h, changing update duration to 2 hours and continuing.") //nolint:lll // gofumpt conflicts with lll } + cveInfo = cveinfo.NewCVEInfo(storeController, log) + go func() { - err := downloadTrivyDB(rootDir, log, - config.Extensions.Search.CVE.UpdateInterval) + err := downloadTrivyDB(log, config.Extensions.Search.CVE.UpdateInterval) if err != nil { log.Error().Err(err).Msg("error while downloading TrivyDB") } @@ -40,11 +46,11 @@ func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string } } -func downloadTrivyDB(dbDir string, log log.Logger, updateInterval time.Duration) error { +func downloadTrivyDB(log log.Logger, updateInterval time.Duration) error { for { log.Info().Msg("updating the CVE database") - err := cveinfo.UpdateCVEDb(dbDir, log) + err := cveInfo.UpdateDB() if err != nil { return err } @@ -66,9 +72,15 @@ func SetupSearchRoutes(config *config.Config, router *mux.Router, storeControlle var resConfig gql_generated.Config if config.Extensions.Search.CVE != nil { - resConfig = search.GetResolverConfig(log, storeController, true) + // cveinfo should already be initialized by this time + // as EnableSearchExtension is supposed to be called earlier, but let's be sure + if cveInfo == nil { + cveInfo = cveinfo.NewCVEInfo(storeController, log) + } + + resConfig = search.GetResolverConfig(log, storeController, cveInfo) } else { - resConfig = search.GetResolverConfig(log, storeController, false) + resConfig = search.GetResolverConfig(log, storeController, nil) } graphqlPrefix := router.PathPrefix(constants.ExtSearchPrefix).Methods("OPTIONS", "GET", "POST") diff --git a/pkg/extensions/extension_search_disabled.go b/pkg/extensions/extension_search_disabled.go index ce757b81..6e8b48fe 100644 --- a/pkg/extensions/extension_search_disabled.go +++ b/pkg/extensions/extension_search_disabled.go @@ -12,7 +12,7 @@ import ( ) // EnableSearchExtension ... -func EnableSearchExtension(config *config.Config, log log.Logger, rootDir string) { +func EnableSearchExtension(config *config.Config, log log.Logger, storeController storage.StoreController) { log.Warn().Msg("skipping enabling search extension because given zot binary doesn't include this feature," + "please build a binary that does so") } diff --git a/pkg/extensions/search/common/common.go b/pkg/extensions/search/common/common.go index 877b5707..709d2456 100644 --- a/pkg/extensions/search/common/common.go +++ b/pkg/extensions/search/common/common.go @@ -61,23 +61,39 @@ func GetRepo(image string) string { return image } -func GetFixedTags(allTags, infectedTags []TagInfo) []TagInfo { +func GetFixedTags(allTags, vulnerableTags []TagInfo) []TagInfo { sort.Slice(allTags, func(i, j int) bool { return allTags[i].Timestamp.Before(allTags[j].Timestamp) }) - latestInfected := TagInfo{} + earliestVulnerable := vulnerableTags[0] + vulnerableTagMap := make(map[string]TagInfo, len(vulnerableTags)) - for _, tag := range infectedTags { - if !tag.Timestamp.Before(latestInfected.Timestamp) { - latestInfected = tag + for _, tag := range vulnerableTags { + vulnerableTagMap[tag.Name] = tag + + if tag.Timestamp.Before(earliestVulnerable.Timestamp) { + earliestVulnerable = tag } } var fixedTags []TagInfo + // There are some downsides to this logic + // We assume there can't be multiple "branches" of the same + // image built at different times containing different fixes + // There may be older images which have a fix or + // newer images which don't for _, tag := range allTags { - if tag.Timestamp.After(latestInfected.Timestamp) { + if tag.Timestamp.Before(earliestVulnerable.Timestamp) { + // The vulnerability did not exist at the time this + // image was built + continue + } + // If the image is old enough for the vulnerability to + // exist, but it was not detected, it means it contains + // the fix + if _, ok := vulnerableTagMap[tag.Name]; !ok { fixedTags = append(fixedTags, tag) } } diff --git a/pkg/extensions/search/common/common_test.go b/pkg/extensions/search/common/common_test.go index 590c8eaa..f0b62b70 100644 --- a/pkg/extensions/search/common/common_test.go +++ b/pkg/extensions/search/common/common_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/specs-go" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -88,16 +89,17 @@ type GlobalSearch struct { } type ImageSummary struct { - RepoName string `json:"repoName"` - Tag string `json:"tag"` - LastUpdated time.Time `json:"lastUpdated"` - Size string `json:"size"` - Platform OsArch `json:"platform"` - Vendor string `json:"vendor"` - Score int `json:"score"` - IsSigned bool `json:"isSigned"` - History []LayerHistory `json:"history"` - Layers []LayerSummary `json:"layers"` + RepoName string `json:"repoName"` + Tag string `json:"tag"` + LastUpdated time.Time `json:"lastUpdated"` + Size string `json:"size"` + Platform OsArch `json:"platform"` + Vendor string `json:"vendor"` + Score int `json:"score"` + IsSigned bool `json:"isSigned"` + History []LayerHistory `json:"history"` + Layers []LayerSummary `json:"layers"` + Vulnerabilities ImageVulnerabilitySummary `json:"vulnerabilities"` } type LayerHistory struct { @@ -113,6 +115,11 @@ type HistoryDescription struct { EmptyLayer bool `json:"emptyLayer"` } +type ImageVulnerabilitySummary struct { + MaxSeverity string `json:"maxSeverity"` + Count int `json:"count"` +} + type RepoSummary struct { Name string `json:"name"` LastUpdated time.Time `json:"lastUpdated"` @@ -278,71 +285,31 @@ func getTags() ([]common.TagInfo, []common.TagInfo) { tags = append(tags, firstTag, secondTag, thirdTag, fourthTag) - infectedTags := make([]common.TagInfo, 0) - infectedTags = append(infectedTags, secondTag) + vulnerableTags := make([]common.TagInfo, 0) + vulnerableTags = append(vulnerableTags, secondTag) - return tags, infectedTags + return tags, vulnerableTags } -func TestImageFormat(t *testing.T) { - Convey("Test valid image", t, func() { - log := log.NewLogger("debug", "") - dbDir := "../../../../test/data" +func readFileAndSearchString(filePath string, stringToMatch string, timeout time.Duration) (bool, error) { + ctx, cancelFunc := context.WithTimeout(context.Background(), timeout) + defer cancelFunc() - conf := config.New() - conf.Extensions = &extconf.ExtensionConfig{} - conf.Extensions.Lint = &extconf.LintConfig{} + for { + select { + case <-ctx.Done(): + return false, nil + default: + content, err := os.ReadFile(filePath) + if err != nil { + return false, err + } - metrics := monitoring.NewMetricsServer(false, log) - defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay, - false, false, log, metrics, nil) - storeController := storage.StoreController{DefaultStore: defaultStore} - olu := common.NewBaseOciLayoutUtils(storeController, log) - - isValidImage, err := olu.IsValidImageFormat("zot-test") - So(err, ShouldBeNil) - So(isValidImage, ShouldEqual, true) - - isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.1") - So(err, ShouldBeNil) - So(isValidImage, ShouldEqual, true) - - isValidImage, err = olu.IsValidImageFormat("zot-test:0.0.") - So(err, ShouldBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-noindex-test") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot--tet") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-noindex-test") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-squashfs-noblobs") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-index") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-squashfs-invalid-blob") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-squashfs-test:0.3.22-squashfs") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - - isValidImage, err = olu.IsValidImageFormat("zot-nonreadable-test") - So(err, ShouldNotBeNil) - So(isValidImage, ShouldEqual, false) - }) + if strings.Contains(string(content), stringToMatch) { + return true, nil + } + } + } } func TestRepoListWithNewestImage(t *testing.T) { @@ -536,6 +503,22 @@ func TestRepoListWithNewestImage(t *testing.T) { images := responseStruct.RepoListWithNewestImage.Repos So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1") + // Verify we don't return any vulnerabilities if CVE scanning is disabled + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + + "?query={RepoListWithNewestImage{Name%20NewestImage{Tag%20Vulnerabilities{MaxSeverity%20Count}}}}") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.RepoListWithNewestImage.Repos), ShouldEqual, 4) + + images = responseStruct.RepoListWithNewestImage.Repos + So(images[0].NewestImage.Tag, ShouldEqual, "0.0.1") + So(images[0].NewestImage.Vulnerabilities.Count, ShouldEqual, 0) + So(images[0].NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query={RepoListWithNewestImage{Name%20NewestImage{Tag}}}") So(resp, ShouldNotBeNil) @@ -612,6 +595,120 @@ func TestRepoListWithNewestImage(t *testing.T) { So(err, ShouldBeNil) So(resp.StatusCode(), ShouldEqual, 200) }) + + Convey("Test repoListWithNewestImage with vulnerability scan enabled", t, func() { + subpath := "/a" + err := testSetup(t, subpath) + if err != nil { + panic(err) + } + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = rootDir + conf.Storage.SubPaths = make(map[string]config.StorageConfig) + conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} + defaultVal := true + + updateDuration, _ := time.ParseDuration("1h") + cveConfig := &extconf.CVEConfig{ + UpdateInterval: updateDuration, + } + searchConfig := &extconf.SearchConfig{ + Enable: &defaultVal, + CVE: cveConfig, + } + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + // we won't use the logging config feature as we want logs in both + // stdout and a file + logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") + So(err, ShouldBeNil) + logPath := logFile.Name() + defer os.Remove(logPath) + + writers := io.MultiWriter(os.Stdout, logFile) + + ctlr := api.NewController(conf) + ctlr.Log.Logger = ctlr.Log.Output(writers) + + go func() { + // this blocks + if err := ctlr.Run(context.Background()); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(baseURL) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + + // shut down server + defer func() { + ctx := context.Background() + _ = ctlr.Server.Shutdown(ctx) + }() + + substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll + found, err := readFileAndSearchString(logPath, substring, 2*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + found, err = readFileAndSearchString(logPath, "updating the CVE database", 2*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 4*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + resp, err := resty.R().Get(baseURL + "/v2/") + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 422) + + query := "?query={RepoListWithNewestImage{Name%20NewestImage{Tag%20Vulnerabilities{MaxSeverity%20Count}}}}" + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + query) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + var responseStruct RepoWithNewestImageResponse + err = json.Unmarshal(resp.Body(), &responseStruct) + So(err, ShouldBeNil) + So(len(responseStruct.RepoListWithNewestImage.Repos), ShouldEqual, 4) + + repos := responseStruct.RepoListWithNewestImage.Repos + So(repos[0].NewestImage.Tag, ShouldEqual, "0.0.1") + + for _, repo := range repos { + vulnerabilities := repo.NewestImage.Vulnerabilities + So(vulnerabilities, ShouldNotBeNil) + t.Logf("Found vulnerability summary %v", vulnerabilities) + // Depends on test data, but current tested images contain hundreds + So(vulnerabilities.Count, ShouldBeGreaterThan, 1) + So( + dbTypes.CompareSeverityString(dbTypes.SeverityUnknown.String(), vulnerabilities.MaxSeverity), + ShouldBeGreaterThan, + 0, + ) + // This really depends on the test data, but with the current test images it's CRITICAL + So(vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") + } + }) } func TestExpandedRepoInfo(t *testing.T) { @@ -965,12 +1062,12 @@ func TestUtilsMethod(t *testing.T) { routePrefix = common.GetRoutePrefix("a/b/test:latest") So(routePrefix, ShouldEqual, "/a") - allTags, infectedTags := getTags() + allTags, vulnerableTags := getTags() latestTag := common.GetLatestTag(allTags) So(latestTag.Name, ShouldEqual, "1.0.3") - fixedTags := common.GetFixedTags(allTags, infectedTags) + fixedTags := common.GetFixedTags(allTags, vulnerableTags) So(len(fixedTags), ShouldEqual, 2) log := log.NewLogger("debug", "") @@ -1937,7 +2034,7 @@ func TestGetRepositories(t *testing.T) { } func TestGlobalSearch(t *testing.T) { - Convey("Test utils", t, func() { + Convey("Test global search", t, func() { subpath := "/a" err := testSetup(t, subpath) @@ -2000,16 +2097,20 @@ func TestGlobalSearch(t *testing.T) { Os Arch } + Vulnerabilities { + Count + MaxSeverity + } } Repos { Name - LastUpdated - Size - Platforms { - Os - Arch - } - Vendors + LastUpdated + Size + Platforms { + Os + Arch + } + Vendors Score NewestImage { RepoName @@ -2023,6 +2124,10 @@ func TestGlobalSearch(t *testing.T) { Os Arch } + Vulnerabilities { + Count + MaxSeverity + } } } Layers { @@ -2098,6 +2203,235 @@ func TestGlobalSearch(t *testing.T) { So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor) So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os) So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch) + So(repo.NewestImage.Vulnerabilities.Count, ShouldEqual, 0) + So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "") + } + + // GetRepositories fail + + err = os.Chmod(rootDir, 0o333) + So(err, ShouldBeNil) + + resp, err = resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct = &GlobalSearchResultResp{} + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + So(responseStruct.Errors, ShouldNotBeEmpty) + err = os.Chmod(rootDir, 0o777) + So(err, ShouldBeNil) + }) + + Convey("Test global search with vulnerabitity scanning enabled", t, func() { + subpath := "/a" + + err := testSetup(t, subpath) + if err != nil { + panic(err) + } + + port := GetFreePort() + baseURL := GetBaseURL(port) + conf := config.New() + conf.HTTP.Port = port + conf.Storage.RootDirectory = rootDir + conf.Storage.SubPaths = make(map[string]config.StorageConfig) + conf.Storage.SubPaths[subpath] = config.StorageConfig{RootDirectory: subRootDir} + defaultVal := true + + updateDuration, _ := time.ParseDuration("1h") + cveConfig := &extconf.CVEConfig{ + UpdateInterval: updateDuration, + } + searchConfig := &extconf.SearchConfig{ + Enable: &defaultVal, + CVE: cveConfig, + } + conf.Extensions = &extconf.ExtensionConfig{ + Search: searchConfig, + } + + // we won't use the logging config feature as we want logs in both + // stdout and a file + logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") + So(err, ShouldBeNil) + logPath := logFile.Name() + defer os.Remove(logPath) + + writers := io.MultiWriter(os.Stdout, logFile) + + ctlr := api.NewController(conf) + ctlr.Log.Logger = ctlr.Log.Output(writers) + + go func() { + // this blocks + if err := ctlr.Run(context.Background()); err != nil { + return + } + }() + + // wait till ready + for { + _, err := resty.R().Get(baseURL) + if err == nil { + break + } + + time.Sleep(100 * time.Millisecond) + } + + // shut down server + + defer func() { + ctx := context.Background() + _ = ctlr.Server.Shutdown(ctx) + }() + + // Wait for trivy db to download + substring := "\"Extensions\":{\"Search\":{\"CVE\":{\"UpdateInterval\":3600000000000},\"Enable\":true},\"Sync\":null,\"Metrics\":null,\"Scrub\":null,\"Lint\":null}" //nolint:lll // gofumpt conflicts with lll + found, err := readFileAndSearchString(logPath, substring, 2*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + found, err = readFileAndSearchString(logPath, "updating the CVE database", 2*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + found, err = readFileAndSearchString(logPath, "DB update completed, next update scheduled", 4*time.Minute) + So(found, ShouldBeTrue) + So(err, ShouldBeNil) + + query := ` + { + GlobalSearch(query:""){ + Images { + RepoName + Tag + LastUpdated + Size + IsSigned + Vendor + Score + Platform { + Os + Arch + } + Vulnerabilities { + Count + MaxSeverity + } + } + Repos { + Name + LastUpdated + Size + Platforms { + Os + Arch + } + Vendors + Score + NewestImage { + RepoName + Tag + LastUpdated + Size + IsSigned + Vendor + Score + Platform { + Os + Arch + } + Vulnerabilities { + Count + MaxSeverity + } + } + } + Layers { + Digest + Size + } + } + }` + resp, err := resty.R().Get(baseURL + graphqlQueryPrefix + "?query=" + url.QueryEscape(query)) + So(resp, ShouldNotBeNil) + So(err, ShouldBeNil) + So(resp.StatusCode(), ShouldEqual, 200) + + responseStruct := &GlobalSearchResultResp{} + + err = json.Unmarshal(resp.Body(), responseStruct) + So(err, ShouldBeNil) + + // There are 2 repos: zot-cve-test and zot-test, each having an image with tag 0.0.1 + imageStore := ctlr.StoreController.DefaultStore + + repos, err := imageStore.GetRepositories() + So(err, ShouldBeNil) + expectedRepoCount := len(repos) + + allExpectedTagMap := make(map[string][]string, expectedRepoCount) + expectedImageCount := 0 + for _, repo := range repos { + tags, err := imageStore.GetImageTags(repo) + So(err, ShouldBeNil) + + allExpectedTagMap[repo] = tags + expectedImageCount += len(tags) + } + + // Make sure the repo/image counts match before comparing actual content + So(responseStruct.GlobalSearchResult.GlobalSearch.Images, ShouldNotBeNil) + t.Logf("returned images: %v", responseStruct.GlobalSearchResult.GlobalSearch.Images) + So(len(responseStruct.GlobalSearchResult.GlobalSearch.Images), ShouldEqual, expectedImageCount) + t.Logf("returned repos: %v", responseStruct.GlobalSearchResult.GlobalSearch.Repos) + So(len(responseStruct.GlobalSearchResult.GlobalSearch.Repos), ShouldEqual, expectedRepoCount) + t.Logf("returned layers: %v", responseStruct.GlobalSearchResult.GlobalSearch.Layers) + So(len(responseStruct.GlobalSearchResult.GlobalSearch.Layers), ShouldNotBeEmpty) + + newestImageMap := make(map[string]ImageSummary) + for _, image := range responseStruct.GlobalSearchResult.GlobalSearch.Images { + // Make sure all returned results are supposed to be in the repo + So(allExpectedTagMap[image.RepoName], ShouldContain, image.Tag) + // Identify the newest image in each repo + if newestImage, ok := newestImageMap[image.RepoName]; ok { + if newestImage.LastUpdated.Before(image.LastUpdated) { + newestImageMap[image.RepoName] = image + } + } else { + newestImageMap[image.RepoName] = image + } + } + t.Logf("expected results for newest images in repos: %v", newestImageMap) + + for _, repo := range responseStruct.GlobalSearchResult.GlobalSearch.Repos { + image := newestImageMap[repo.Name] + So(repo.Name, ShouldEqual, image.RepoName) + So(repo.LastUpdated, ShouldEqual, image.LastUpdated) + So(repo.Size, ShouldEqual, image.Size) + So(repo.Vendors[0], ShouldEqual, image.Vendor) + So(repo.Platforms[0].Os, ShouldEqual, image.Platform.Os) + So(repo.Platforms[0].Arch, ShouldEqual, image.Platform.Arch) + So(repo.NewestImage.RepoName, ShouldEqual, image.RepoName) + So(repo.NewestImage.Tag, ShouldEqual, image.Tag) + So(repo.NewestImage.LastUpdated, ShouldEqual, image.LastUpdated) + So(repo.NewestImage.Size, ShouldEqual, image.Size) + So(repo.NewestImage.IsSigned, ShouldEqual, image.IsSigned) + So(repo.NewestImage.Vendor, ShouldEqual, image.Vendor) + So(repo.NewestImage.Platform.Os, ShouldEqual, image.Platform.Os) + So(repo.NewestImage.Platform.Arch, ShouldEqual, image.Platform.Arch) + t.Logf("Found vulnerability summary %v", repo.NewestImage.Vulnerabilities) + So(repo.NewestImage.Vulnerabilities.Count, ShouldEqual, image.Vulnerabilities.Count) + So(repo.NewestImage.Vulnerabilities.Count, ShouldBeGreaterThan, 1) + So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, image.Vulnerabilities.MaxSeverity) + // This really depends on the test data, but with the current test images it's CRITICAL + So(repo.NewestImage.Vulnerabilities.MaxSeverity, ShouldEqual, "CRITICAL") } // GetRepositories fail diff --git a/pkg/extensions/search/common/oci_layout.go b/pkg/extensions/search/common/oci_layout.go index 82015c10..f1fbab25 100644 --- a/pkg/extensions/search/common/oci_layout.go +++ b/pkg/extensions/search/common/oci_layout.go @@ -11,7 +11,6 @@ import ( "time" v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/types" notreg "github.com/notaryproject/notation-go/registry" godigest "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -24,7 +23,6 @@ type OciLayoutUtils interface { GetImageManifests(image string) ([]ispec.Descriptor, error) GetImageBlobManifest(imageDir string, digest godigest.Digest) (v1.Manifest, error) GetImageInfo(imageDir string, hash v1.Hash) (ispec.Image, error) - IsValidImageFormat(image string) (bool, error) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) GetImageLastUpdated(imageInfo ispec.Image) time.Time GetImagePlatform(imageInfo ispec.Image) (string, string) @@ -115,6 +113,7 @@ func (olu BaseOciLayoutUtils) GetImageManifest(repo string, reference string) (i return manifest, nil } +// Provide a list of repositories from all the available image stores. func (olu BaseOciLayoutUtils) GetRepositories() ([]string, error) { defaultStore := olu.StoreController.DefaultStore substores := olu.StoreController.SubStore @@ -208,44 +207,6 @@ func (olu BaseOciLayoutUtils) GetImageInfo(imageDir string, hash v1.Hash) (ispec return imageInfo, err } -func (olu BaseOciLayoutUtils) IsValidImageFormat(image string) (bool, error) { - imageDir, inputTag := GetImageDirAndTag(image) - - manifests, err := olu.GetImageManifests(imageDir) - if err != nil { - return false, err - } - - for _, manifest := range manifests { - tag, ok := manifest.Annotations[ispec.AnnotationRefName] - - if ok && inputTag != "" && tag != inputTag { - continue - } - - blobManifest, err := olu.GetImageBlobManifest(imageDir, manifest.Digest) - if err != nil { - return false, err - } - - imageLayers := blobManifest.Layers - - for _, imageLayer := range imageLayers { - switch imageLayer.MediaType { - case types.OCILayer, types.DockerLayer: - return true, nil - - default: - olu.Log.Debug().Msg("image media type not supported for scanning") - - return false, errors.ErrScanNotSupported - } - } - } - - return false, nil -} - // GetImageTagsWithTimestamp returns a list of image tags with timestamp available in the specified repository. func (olu BaseOciLayoutUtils) GetImageTagsWithTimestamp(repo string) ([]TagInfo, error) { tagsInfo := make([]TagInfo, 0) diff --git a/pkg/extensions/search/cve/cve.go b/pkg/extensions/search/cve/cve.go index e331ddfb..c99126fa 100644 --- a/pkg/extensions/search/cve/cve.go +++ b/pkg/extensions/search/cve/cve.go @@ -1,155 +1,63 @@ package cveinfo import ( - "flag" "fmt" - "path" - "strings" - dbTypes "github.com/aquasecurity/trivy-db/pkg/types" - "github.com/aquasecurity/trivy/pkg/commands/artifact" - "github.com/aquasecurity/trivy/pkg/commands/operation" - "github.com/aquasecurity/trivy/pkg/report" - "github.com/aquasecurity/trivy/pkg/types" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/urfave/cli/v2" "zotregistry.io/zot/pkg/extensions/search/common" + cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" + "zotregistry.io/zot/pkg/extensions/search/cve/trivy" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/storage" ) -func getRoutePrefix(name string) string { - names := strings.SplitN(name, "/", 2) //nolint:gomnd - - if len(names) != 2 { // nolint: gomnd - // it means route is of global storage e.g "centos:latest" - if len(names) == 1 { - return "/" - } - } - - return fmt.Sprintf("/%s", names[0]) +type CveInfo interface { + GetImageListForCVE(repo, cveID string) ([]ImageInfoByCVE, error) + GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) + GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) + GetCVESummaryForImage(image string) (ImageCVESummary, error) + UpdateDB() error } -// UpdateCVEDb ... -func UpdateCVEDb(dbDir string, log log.Logger) error { - return operation.DownloadDB("dev", dbDir, false, false, false) +type Scanner interface { + ScanImage(image string) (map[string]cvemodel.CVE, error) + IsImageFormatScannable(image string) (bool, error) + CompareSeverities(severity1, severity2 string) int + UpdateDB() error } -// NewTrivyContext set some trivy configuration value and return a context. -func NewTrivyContext(dir string) *TrivyCtx { - trivyCtx := &TrivyCtx{} - - app := &cli.App{} - - flagSet := &flag.FlagSet{} - - var cacheDir string - - flagSet.StringVar(&cacheDir, "cache-dir", dir, "") - - var vuln string - - flagSet.StringVar(&vuln, "vuln-type", strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), "") - - var severity string - - flagSet.StringVar(&severity, "severity", strings.Join(dbTypes.SeverityNames, ","), "") - - flagSet.StringVar(&trivyCtx.Input, "input", "", "") - - var securityCheck string - - flagSet.StringVar(&securityCheck, "security-checks", types.SecurityCheckVulnerability, "") - - var reportFormat string - - flagSet.StringVar(&reportFormat, "format", "table", "") - - ctx := cli.NewContext(app, flagSet, nil) - - trivyCtx.Ctx = ctx - - return trivyCtx +type ImageInfoByCVE struct { + Tag string + Digest digest.Digest + Manifest v1.Manifest } -func ScanImage(ctx *cli.Context) (report.Report, error) { - return artifact.TrivyImageRun(ctx) +type ImageCVESummary struct { + Count int + MaxSeverity string } -func GetCVEInfo(storeController storage.StoreController, log log.Logger) (*CveInfo, error) { - cveController := CveTrivyController{} +type BaseCveInfo struct { + Log log.Logger + Scanner Scanner + LayoutUtils common.OciLayoutUtils +} + +func NewCVEInfo(storeController storage.StoreController, log log.Logger) *BaseCveInfo { layoutUtils := common.NewBaseOciLayoutUtils(storeController, log) + scanner := trivy.NewScanner(storeController, layoutUtils, log) - subCveConfig := make(map[string]*TrivyCtx) - - if storeController.DefaultStore != nil { - imageStore := storeController.DefaultStore - - rootDir := imageStore.RootDir() - - ctx := NewTrivyContext(rootDir) - - cveController.DefaultCveConfig = ctx - } - - if storeController.SubStore != nil { - for route, storage := range storeController.SubStore { - rootDir := storage.RootDir() - - ctx := NewTrivyContext(rootDir) - - subCveConfig[route] = ctx - } - } - - cveController.SubCveConfig = subCveConfig - - return &CveInfo{ - Log: log, CveTrivyController: cveController, StoreController: storeController, - LayoutUtils: layoutUtils, - }, nil + return &BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} } -func (cveinfo CveInfo) GetTrivyContext(image string) *TrivyCtx { - // Split image to get route prefix - prefixName := getRoutePrefix(image) - - var trivyCtx *TrivyCtx - - var ok bool - - var rootDir string - - // Get corresponding CVE trivy config, if no sub cve config present that means its default - trivyCtx, ok = cveinfo.CveTrivyController.SubCveConfig[prefixName] - if ok { - imgStore := cveinfo.StoreController.SubStore[prefixName] - - rootDir = imgStore.RootDir() - } else { - trivyCtx = cveinfo.CveTrivyController.DefaultCveConfig - - imgStore := cveinfo.StoreController.DefaultStore - - rootDir = imgStore.RootDir() - } - - trivyCtx.Input = path.Join(rootDir, image) - - return trivyCtx -} - -func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.ImageStore, - trivyCtx *TrivyCtx, -) ([]ImageInfoByCVE, error) { +func (cveinfo BaseCveInfo) GetImageListForCVE(repo, cveID string) ([]ImageInfoByCVE, error) { imgList := make([]ImageInfoByCVE, 0) - rootDir := imgStore.RootDir() - manifests, err := cveinfo.LayoutUtils.GetImageManifests(repo) if err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to get list of image tag") + cveinfo.Log.Error().Err(err).Str("repo", repo).Msg("unable to get list of tags from repo") return imgList, err } @@ -159,47 +67,141 @@ func (cveinfo CveInfo) GetImageListForCVE(repo, cvid string, imgStore storage.Im image := fmt.Sprintf("%s:%s", repo, tag) - trivyCtx.Input = path.Join(rootDir, image) - - isValidImage, _ := cveinfo.LayoutUtils.IsValidImageFormat(image) + isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image) if !isValidImage { - cveinfo.Log.Debug().Str("image", repo+":"+tag).Msg("image media type not supported for scanning") - continue } - cveinfo.Log.Info().Str("image", repo+":"+tag).Msg("scanning image") - - report, err := ScanImage(trivyCtx.Ctx) + cveMap, err := cveinfo.Scanner.ScanImage(image) if err != nil { - cveinfo.Log.Error().Err(err).Str("image", repo+":"+tag).Msg("unable to scan image") - continue } - for _, result := range report.Results { - for _, vulnerability := range result.Vulnerabilities { - if vulnerability.VulnerabilityID == cvid { - digest := manifest.Digest + for id := range cveMap { + if id == cveID { + digest := manifest.Digest - imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest) - if err != nil { - cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest") + imageBlobManifest, err := cveinfo.LayoutUtils.GetImageBlobManifest(repo, digest) + if err != nil { + cveinfo.Log.Error().Err(err).Msg("unable to read image blob manifest") - return []ImageInfoByCVE{}, err - } - - imgList = append(imgList, ImageInfoByCVE{ - Tag: tag, - Digest: digest, - Manifest: imageBlobManifest, - }) - - break + return []ImageInfoByCVE{}, err } + + imgList = append(imgList, ImageInfoByCVE{ + Tag: tag, + Digest: digest, + Manifest: imageBlobManifest, + }) + + break } } } return imgList, nil } + +func (cveinfo BaseCveInfo) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) { + tagsInfo, err := cveinfo.LayoutUtils.GetImageTagsWithTimestamp(repo) + if err != nil { + cveinfo.Log.Error().Err(err).Str("repo", repo).Msg("unable to get list of tags from repo") + + return []common.TagInfo{}, err + } + + vulnerableTags := make([]common.TagInfo, 0) + + var hasCVE bool + + for _, tag := range tagsInfo { + image := fmt.Sprintf("%s:%s", repo, tag.Name) + tagInfo := common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp, Digest: tag.Digest} + + isValidImage, _ := cveinfo.Scanner.IsImageFormatScannable(image) + if !isValidImage { + cveinfo.Log.Debug().Str("image", image). + Msg("image media type not supported for scanning, adding as a vulnerable image") + + vulnerableTags = append(vulnerableTags, tagInfo) + + continue + } + + cveMap, err := cveinfo.Scanner.ScanImage(image) + if err != nil { + cveinfo.Log.Debug().Str("image", image). + Msg("scanning failed, adding as a vulnerable image") + + vulnerableTags = append(vulnerableTags, tagInfo) + + continue + } + + hasCVE = false + + for id := range cveMap { + if id == cveID { + hasCVE = true + + break + } + } + + if hasCVE { + vulnerableTags = append(vulnerableTags, tagInfo) + } + } + + if len(vulnerableTags) != 0 { + cveinfo.Log.Info().Str("repo", repo).Msg("comparing fixed tags timestamp") + + tagsInfo = common.GetFixedTags(tagsInfo, vulnerableTags) + } else { + cveinfo.Log.Info().Str("repo", repo).Str("cve-id", cveID). + Msg("image does not contain any tag that have given cve") + } + + return tagsInfo, nil +} + +func (cveinfo BaseCveInfo) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) { + cveMap := make(map[string]cvemodel.CVE) + + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image) + if !isValidImage { + return cveMap, err + } + + return cveinfo.Scanner.ScanImage(image) +} + +func (cveinfo BaseCveInfo) GetCVESummaryForImage(image string) (ImageCVESummary, error) { + imageCVESummary := ImageCVESummary{ + Count: 0, + MaxSeverity: "UNKNOWN", + } + + isValidImage, err := cveinfo.Scanner.IsImageFormatScannable(image) + if !isValidImage { + return imageCVESummary, err + } + + cveMap, err := cveinfo.Scanner.ScanImage(image) + if err != nil { + return imageCVESummary, err + } + + for _, cve := range cveMap { + if cveinfo.Scanner.CompareSeverities(imageCVESummary.MaxSeverity, cve.Severity) > 0 { + imageCVESummary.MaxSeverity = cve.Severity + } + } + imageCVESummary.Count = len(cveMap) + + return imageCVESummary, nil +} + +func (cveinfo BaseCveInfo) UpdateDB() error { + return cveinfo.Scanner.UpdateDB() +} diff --git a/pkg/extensions/search/cve/cve_test.go b/pkg/extensions/search/cve/cve_test.go index cefdbddd..c8dfa494 100644 --- a/pkg/extensions/search/cve/cve_test.go +++ b/pkg/extensions/search/cve/cve_test.go @@ -14,9 +14,13 @@ import ( "testing" "time" + v1 "github.com/google/go-containerregistry/pkg/v1" + regTypes "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/opencontainers/go-digest" ispec "github.com/opencontainers/image-spec/specs-go/v1" . "github.com/smartystreets/goconvey/convey" "gopkg.in/resty.v1" + "zotregistry.io/zot/errors" "zotregistry.io/zot/pkg/api" "zotregistry.io/zot/pkg/api/config" "zotregistry.io/zot/pkg/api/constants" @@ -24,14 +28,17 @@ import ( "zotregistry.io/zot/pkg/extensions/monitoring" "zotregistry.io/zot/pkg/extensions/search/common" cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" + cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" + "zotregistry.io/zot/pkg/extensions/search/cve/trivy" "zotregistry.io/zot/pkg/log" "zotregistry.io/zot/pkg/storage" . "zotregistry.io/zot/pkg/test" + "zotregistry.io/zot/pkg/test/mocks" ) // nolint:gochecknoglobals var ( - cve *cveinfo.CveInfo + cve cveinfo.CveInfo dbDir string updateDuration time.Duration ) @@ -62,15 +69,8 @@ type ImgList struct { //nolint:tagliatelle // graphQL schema type CVEResultForImage struct { - Tag string `json:"Tag"` - CVEList []CVE `json:"CVEList"` -} - -//nolint:tagliatelle // graphQL schema -type CVE struct { - ID string `json:"Id"` - Description string `json:"Description"` - Severity string `json:"Severity"` + Tag string `json:"Tag"` + CVEList []cvemodel.CVE `json:"CVEList"` } func testSetup() error { @@ -89,8 +89,9 @@ func testSetup() error { storeController := storage.StoreController{DefaultStore: storage.NewImageStore(dir, false, storage.DefaultGCDelay, false, false, log, metrics, nil)} layoutUtils := common.NewBaseOciLayoutUtils(storeController, log) + scanner := trivy.NewScanner(storeController, layoutUtils, log) - cve = &cveinfo.CveInfo{Log: log, StoreController: storeController, LayoutUtils: layoutUtils} + cve = &cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} dbDir = dir @@ -318,43 +319,65 @@ func makeTestFile(fileName, content string) error { return nil } -func TestMultipleStoragePath(t *testing.T) { - Convey("Test multiple storage path", t, func() { - // Create temporary directory - firstRootDir := t.TempDir() - secondRootDir := t.TempDir() - thirdRootDir := t.TempDir() - +func TestImageFormat(t *testing.T) { + Convey("Test valid image", t, func() { log := log.NewLogger("debug", "") - metrics := monitoring.NewMetricsServer(false, log) + dbDir := "../../../../test/data" conf := config.New() conf.Extensions = &extconf.ExtensionConfig{} conf.Extensions.Lint = &extconf.LintConfig{} - // Create ImageStore - firstStore := storage.NewImageStore(firstRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) + metrics := monitoring.NewMetricsServer(false, log) + defaultStore := storage.NewImageStore(dbDir, false, storage.DefaultGCDelay, + false, false, log, metrics, nil) + storeController := storage.StoreController{DefaultStore: defaultStore} - secondStore := storage.NewImageStore(secondRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) - - thirdStore := storage.NewImageStore(thirdRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) - - storeController := storage.StoreController{} - - storeController.DefaultStore = firstStore - - subStore := make(map[string]storage.ImageStore) - - subStore["/a"] = secondStore - subStore["/b"] = thirdStore - - storeController.SubStore = subStore - - cveInfo, err := cveinfo.GetCVEInfo(storeController, log) + cveInfo := cveinfo.NewCVEInfo(storeController, log) + isValidImage, err := cveInfo.Scanner.IsImageFormatScannable("zot-test") So(err, ShouldBeNil) - So(cveInfo.StoreController.DefaultStore, ShouldNotBeNil) - So(cveInfo.StoreController.SubStore, ShouldNotBeNil) + So(isValidImage, ShouldEqual, true) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.1") + So(err, ShouldBeNil) + So(isValidImage, ShouldEqual, true) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-test:0.0.") + So(err, ShouldBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot--tet") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-noindex-test") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-noblobs") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-index") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-invalid-blob") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-squashfs-test:0.3.22-squashfs") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) + + isValidImage, err = cveInfo.Scanner.IsImageFormatScannable("zot-nonreadable-test") + So(err, ShouldNotBeNil) + So(isValidImage, ShouldEqual, false) }) } @@ -730,3 +753,457 @@ func TestHTTPOptionsResponse(t *testing.T) { }() }) } + +func TestCVEStruct(t *testing.T) { + Convey("Unit test the CVE struct", t, func() { + // Setup test image data in mock storage + layoutUtils := mocks.OciLayoutUtilsMock{ + GetImageManifestsFn: func(repo string) ([]ispec.Descriptor, error) { + // Valid image for scanning + if repo == "repo1" { //nolint: goconst + return []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: int64(0), + Annotations: map[string]string{ + ispec.AnnotationRefName: "0.1.0", + }, + Digest: "abcc", + }, + { + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: int64(0), + Annotations: map[string]string{ + ispec.AnnotationRefName: "1.0.0", + }, + Digest: "abcd", + }, + { + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: int64(0), + Annotations: map[string]string{ + ispec.AnnotationRefName: "1.1.0", + }, + Digest: "abce", + }, + { + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: int64(0), + Annotations: map[string]string{ + ispec.AnnotationRefName: "1.0.1", + }, + Digest: "abcf", + }, + }, nil + } + + // Image with non-scannable blob + if repo == "repo2" { //nolint: goconst + return []ispec.Descriptor{ + { + MediaType: "application/vnd.oci.image.manifest.v1+json", + Size: int64(0), + Annotations: map[string]string{ + ispec.AnnotationRefName: "1.0.0", + }, + Digest: "abcd", + }, + }, nil + } + + // By default the image is not found + return nil, errors.ErrRepoNotFound + }, + GetImageTagsWithTimestampFn: func(repo string) ([]common.TagInfo, error) { + // Valid image for scanning + if repo == "repo1" { //nolint: goconst + return []common.TagInfo{ + { + Name: "0.1.0", + Digest: "abcc", + Timestamp: time.Date(2008, 1, 1, 12, 0, 0, 0, time.UTC), + }, + { + Name: "1.0.0", + Digest: "abcd", + Timestamp: time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC), + }, + { + Name: "1.1.0", + Digest: "abce", + Timestamp: time.Date(2010, 1, 1, 12, 0, 0, 0, time.UTC), + }, + { + Name: "1.0.1", + Digest: "abcf", + Timestamp: time.Date(2011, 1, 1, 12, 0, 0, 0, time.UTC), + }, + }, nil + } + + // Image with non-scannable blob + if repo == "repo2" { //nolint: goconst + return []common.TagInfo{ + { + Name: "1.0.0", + Digest: "abcd", + Timestamp: time.Date(2009, 1, 1, 12, 0, 0, 0, time.UTC), + }, + }, nil + } + + // By default do not return any tags + return []common.TagInfo{}, errors.ErrRepoNotFound + }, + GetImageBlobManifestFn: func(imageDir string, digest digest.Digest) (v1.Manifest, error) { + // Valid image for scanning + if imageDir == "repo1" { //nolint: goconst + return v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: regTypes.OCILayer, + Size: 0, + Digest: v1.Hash{}, + }, + }, + }, nil + } + + // Image with non-scannable blob + if imageDir == "repo2" { //nolint: goconst + return v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: regTypes.OCIRestrictedLayer, + Size: 0, + Digest: v1.Hash{}, + }, + }, + }, nil + } + + return v1.Manifest{}, errors.ErrBlobNotFound + }, + } + + severities := map[string]int{ + "UNKNOWN": 0, + "LOW": 1, + "MEDIUM": 2, + "HIGH": 3, + "CRITICAL": 4, + } + + // Setup test CVE data in mock scanner + scanner := mocks.CveScannerMock{ + ScanImageFn: func(image string) (map[string]cvemodel.CVE, error) { + // Images in chronological order + if image == "repo1:0.1.0" { + return map[string]cvemodel.CVE{ + "CVE1": { + ID: "CVE1", + Severity: "MEDIUM", + Title: "Title CVE1", + Description: "Description CVE1", + }, + }, nil + } + + if image == "repo1:1.0.0" { + return map[string]cvemodel.CVE{ + "CVE1": { + ID: "CVE1", + Severity: "MEDIUM", + Title: "Title CVE1", + Description: "Description CVE1", + }, + "CVE2": { + ID: "CVE2", + Severity: "HIGH", + Title: "Title CVE2", + Description: "Description CVE2", + }, + "CVE3": { + ID: "CVE3", + Severity: "LOW", + Title: "Title CVE3", + Description: "Description CVE3", + }, + }, nil + } + + if image == "repo1:1.1.0" { + return map[string]cvemodel.CVE{ + "CVE3": { + ID: "CVE3", + Severity: "LOW", + Title: "Title CVE3", + Description: "Description CVE3", + }, + }, nil + } + + // As a minor release on 1.0.0 banch + // does not include all fixes published in 1.1.0 + if image == "repo1:1.0.1" { + return map[string]cvemodel.CVE{ + "CVE1": { + ID: "CVE1", + Severity: "MEDIUM", + Title: "Title CVE1", + Description: "Description CVE1", + }, + "CVE3": { + ID: "CVE3", + Severity: "LOW", + Title: "Title CVE3", + Description: "Description CVE3", + }, + }, nil + } + + // By default the image has no vulnerabilities + return map[string]cvemodel.CVE{}, nil + }, + CompareSeveritiesFn: func(severity1, severity2 string) int { + return severities[severity2] - severities[severity1] + }, + IsImageFormatScannableFn: func(image string) (bool, error) { + // Almost same logic compared to actual Trivy specific implementation + imageDir, inputTag := common.GetImageDirAndTag(image) + + manifests, err := layoutUtils.GetImageManifests(imageDir) + if err != nil { + return false, err + } + + for _, manifest := range manifests { + tag, ok := manifest.Annotations[ispec.AnnotationRefName] + + if ok && inputTag != "" && tag != inputTag { + continue + } + + blobManifest, err := layoutUtils.GetImageBlobManifest(imageDir, manifest.Digest) + if err != nil { + return false, err + } + + imageLayers := blobManifest.Layers + + for _, imageLayer := range imageLayers { + switch imageLayer.MediaType { + case regTypes.OCILayer, regTypes.DockerLayer: + return true, nil + + default: + return false, errors.ErrScanNotSupported + } + } + } + + return false, nil + }, + } + + log := log.NewLogger("debug", "") + + Convey("Test GetCVESummaryForImage", func() { + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} + + // Image is found + cveSummary, err := cveInfo.GetCVESummaryForImage("repo1:0.1.0") + So(err, ShouldBeNil) + So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") + + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.0") + So(err, ShouldBeNil) + So(cveSummary.Count, ShouldEqual, 3) + So(cveSummary.MaxSeverity, ShouldEqual, "HIGH") + + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.0.1") + So(err, ShouldBeNil) + So(cveSummary.Count, ShouldEqual, 2) + So(cveSummary.MaxSeverity, ShouldEqual, "MEDIUM") + + cveSummary, err = cveInfo.GetCVESummaryForImage("repo1:1.1.0") + So(err, ShouldBeNil) + So(cveSummary.Count, ShouldEqual, 1) + So(cveSummary.MaxSeverity, ShouldEqual, "LOW") + + // Image is not scannable + cveSummary, err = cveInfo.GetCVESummaryForImage("repo2:1.0.0") + So(err, ShouldEqual, errors.ErrScanNotSupported) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN") + + // Image is not found + cveSummary, err = cveInfo.GetCVESummaryForImage("repo3:1.0.0") + So(err, ShouldEqual, errors.ErrRepoNotFound) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN") + }) + + Convey("Test GetCVEListForImage", func() { + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} + + // Image is found + cveMap, err := cveInfo.GetCVEListForImage("repo1:0.1.0") + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 1) + So(cveMap, ShouldContainKey, "CVE1") + So(cveMap, ShouldNotContainKey, "CVE2") + So(cveMap, ShouldNotContainKey, "CVE3") + + cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.0") + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 3) + So(cveMap, ShouldContainKey, "CVE1") + So(cveMap, ShouldContainKey, "CVE2") + So(cveMap, ShouldContainKey, "CVE3") + + cveMap, err = cveInfo.GetCVEListForImage("repo1:1.0.1") + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 2) + So(cveMap, ShouldContainKey, "CVE1") + So(cveMap, ShouldNotContainKey, "CVE2") + So(cveMap, ShouldContainKey, "CVE3") + + cveMap, err = cveInfo.GetCVEListForImage("repo1:1.1.0") + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 1) + So(cveMap, ShouldNotContainKey, "CVE1") + So(cveMap, ShouldNotContainKey, "CVE2") + So(cveMap, ShouldContainKey, "CVE3") + + // Image is not scannable + cveMap, err = cveInfo.GetCVEListForImage("repo2:1.0.0") + So(err, ShouldEqual, errors.ErrScanNotSupported) + So(len(cveMap), ShouldEqual, 0) + + // Image is not found + cveMap, err = cveInfo.GetCVEListForImage("repo3:1.0.0") + So(err, ShouldEqual, errors.ErrRepoNotFound) + So(len(cveMap), ShouldEqual, 0) + }) + + Convey("Test GetImageListWithCVEFixed", func() { + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} + + // Image is found + tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1") + So(err, ShouldBeNil) + So(len(tagList), ShouldEqual, 1) + So(tagList[0].Name, ShouldEqual, "1.1.0") + + tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE2") + So(err, ShouldBeNil) + So(len(tagList), ShouldEqual, 2) + So(tagList[0].Name, ShouldEqual, "1.1.0") + So(tagList[1].Name, ShouldEqual, "1.0.1") + + tagList, err = cveInfo.GetImageListWithCVEFixed("repo1", "CVE3") + So(err, ShouldBeNil) + // CVE3 is not present in 0.1.0, but that is older than all other + // images where it is present. The rest of the images explicitly have it. + // This means we consider it not fixed in any image. + So(len(tagList), ShouldEqual, 0) + + // Image is not scannable + tagList, err = cveInfo.GetImageListWithCVEFixed("repo2", "CVE100") + // CVE is not considered fixed as scan is not possible + // but do not return an error + So(err, ShouldBeNil) + So(len(tagList), ShouldEqual, 0) + + // Image is not found + tagList, err = cveInfo.GetImageListWithCVEFixed("repo3", "CVE101") + So(err, ShouldEqual, errors.ErrRepoNotFound) + So(len(tagList), ShouldEqual, 0) + }) + + Convey("Test GetImageListForCVE", func() { + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: layoutUtils} + + // Image is found + imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1") + So(err, ShouldBeNil) + So(len(imageInfoByCveList), ShouldEqual, 3) + So(imageInfoByCveList[0].Tag, ShouldEqual, "0.1.0") + So(imageInfoByCveList[1].Tag, ShouldEqual, "1.0.0") + So(imageInfoByCveList[2].Tag, ShouldEqual, "1.0.1") + + imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo1", "CVE2") + So(err, ShouldBeNil) + So(len(imageInfoByCveList), ShouldEqual, 1) + So(imageInfoByCveList[0].Tag, ShouldEqual, "1.0.0") + + imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo1", "CVE3") + So(err, ShouldBeNil) + So(len(imageInfoByCveList), ShouldEqual, 3) + So(imageInfoByCveList[0].Tag, ShouldEqual, "1.0.0") + So(imageInfoByCveList[1].Tag, ShouldEqual, "1.1.0") + So(imageInfoByCveList[2].Tag, ShouldEqual, "1.0.1") + + // Image is not scannable + imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo2", "CVE100") + // Image is not considered affected with CVE as scan is not possible + // but do not return an error + So(err, ShouldBeNil) + So(len(imageInfoByCveList), ShouldEqual, 0) + + // Image is not found + imageInfoByCveList, err = cveInfo.GetImageListForCVE("repo3", "CVE101") + So(err, ShouldEqual, errors.ErrRepoNotFound) + So(len(imageInfoByCveList), ShouldEqual, 0) + }) + + Convey("Test errors while scanning", func() { + localScanner := scanner + + localScanner.ScanImageFn = func(image string) (map[string]cvemodel.CVE, error) { + // Could be any type of error, let's reuse this one + return nil, errors.ErrScanNotSupported + } + + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: localScanner, LayoutUtils: layoutUtils} + + cveSummary, err := cveInfo.GetCVESummaryForImage("repo1:0.1.0") + So(err, ShouldNotBeNil) + So(cveSummary.Count, ShouldEqual, 0) + So(cveSummary.MaxSeverity, ShouldEqual, "UNKNOWN") + + cveMap, err := cveInfo.GetCVEListForImage("repo1:0.1.0") + So(err, ShouldNotBeNil) + So(cveMap, ShouldBeNil) + + tagList, err := cveInfo.GetImageListWithCVEFixed("repo1", "CVE1") + // CVE is not considered fixed as scan is not possible + // but do not return an error + So(err, ShouldBeNil) + So(len(tagList), ShouldEqual, 0) + + imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1") + // Image is not considered affected with CVE as scan is not possible + // but do not return an error + So(err, ShouldBeNil) + So(len(imageInfoByCveList), ShouldEqual, 0) + }) + + Convey("Test error while reading blob manifest", func() { + localLayoutUtils := layoutUtils + localLayoutUtils.GetImageBlobManifestFn = func(imageDir string, + digest digest.Digest, + ) (v1.Manifest, error) { + return v1.Manifest{}, errors.ErrBlobNotFound + } + + cveInfo := cveinfo.BaseCveInfo{Log: log, Scanner: scanner, LayoutUtils: localLayoutUtils} + + imageInfoByCveList, err := cveInfo.GetImageListForCVE("repo1", "CVE1") + So(err, ShouldNotBeNil) + So(len(imageInfoByCveList), ShouldEqual, 0) + }) + }) +} diff --git a/pkg/extensions/search/cve/model/models.go b/pkg/extensions/search/cve/model/models.go new file mode 100644 index 00000000..a4e2cdc6 --- /dev/null +++ b/pkg/extensions/search/cve/model/models.go @@ -0,0 +1,17 @@ +package model + +//nolint:tagliatelle // graphQL schema +type CVE struct { + ID string `json:"Id"` + Description string `json:"Description"` + Severity string `json:"Severity"` + Title string `json:"Title"` + PackageList []Package `json:"PackageList"` +} + +//nolint:tagliatelle // graphQL schema +type Package struct { + Name string `json:"Name"` + InstalledVersion string `json:"InstalledVersion"` + FixedVersion string `json:"FixedVersion"` +} diff --git a/pkg/extensions/search/cve/models.go b/pkg/extensions/search/cve/models.go deleted file mode 100644 index 36eabe60..00000000 --- a/pkg/extensions/search/cve/models.go +++ /dev/null @@ -1,35 +0,0 @@ -// Package cveinfo ... -package cveinfo - -import ( - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/opencontainers/go-digest" - "github.com/urfave/cli/v2" - "zotregistry.io/zot/pkg/extensions/search/common" - "zotregistry.io/zot/pkg/log" - "zotregistry.io/zot/pkg/storage" -) - -// CveInfo ... -type CveInfo struct { - Log log.Logger - CveTrivyController CveTrivyController - StoreController storage.StoreController - LayoutUtils *common.BaseOciLayoutUtils -} - -type CveTrivyController struct { - DefaultCveConfig *TrivyCtx - SubCveConfig map[string]*TrivyCtx -} - -type TrivyCtx struct { - Input string - Ctx *cli.Context -} - -type ImageInfoByCVE struct { - Tag string - Digest digest.Digest - Manifest v1.Manifest -} diff --git a/pkg/extensions/search/cve/trivy/scanner.go b/pkg/extensions/search/cve/trivy/scanner.go new file mode 100644 index 00000000..ec3e540e --- /dev/null +++ b/pkg/extensions/search/cve/trivy/scanner.go @@ -0,0 +1,304 @@ +package trivy + +import ( + "flag" + "path" + "strings" + "sync" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/types" + regTypes "github.com/google/go-containerregistry/pkg/v1/types" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/urfave/cli/v2" + "zotregistry.io/zot/errors" + "zotregistry.io/zot/pkg/extensions/search/common" + cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" +) + +type trivyCtx struct { + Input string + Ctx *cli.Context +} + +// newTrivyContext set some trivy configuration value and return a context. +func newTrivyContext(dir string) *trivyCtx { + tCtx := &trivyCtx{} + + app := &cli.App{} + + flagSet := &flag.FlagSet{} + + var cacheDir string + + flagSet.StringVar(&cacheDir, "cache-dir", dir, "") + + var vuln string + + flagSet.StringVar(&vuln, "vuln-type", strings.Join([]string{types.VulnTypeOS, types.VulnTypeLibrary}, ","), "") + + var severity string + + flagSet.StringVar(&severity, "severity", strings.Join(dbTypes.SeverityNames, ","), "") + + flagSet.StringVar(&tCtx.Input, "input", "", "") + + var securityCheck string + + flagSet.StringVar(&securityCheck, "security-checks", types.SecurityCheckVulnerability, "") + + var reportFormat string + + flagSet.StringVar(&reportFormat, "format", "table", "") + + ctx := cli.NewContext(app, flagSet, nil) + + tCtx.Ctx = ctx + + return tCtx +} + +type cveTrivyController struct { + DefaultCveConfig *trivyCtx + SubCveConfig map[string]*trivyCtx +} + +type Scanner struct { + layoutUtils common.OciLayoutUtils + cveController cveTrivyController + storeController storage.StoreController + log log.Logger + dbLock *sync.Mutex +} + +func NewScanner(storeController storage.StoreController, + layoutUtils common.OciLayoutUtils, log log.Logger, +) *Scanner { + cveController := cveTrivyController{} + + subCveConfig := make(map[string]*trivyCtx) + + if storeController.DefaultStore != nil { + imageStore := storeController.DefaultStore + + rootDir := imageStore.RootDir() + + ctx := newTrivyContext(rootDir) + + cveController.DefaultCveConfig = ctx + } + + if storeController.SubStore != nil { + for route, storage := range storeController.SubStore { + rootDir := storage.RootDir() + + ctx := newTrivyContext(rootDir) + + subCveConfig[route] = ctx + } + } + + cveController.SubCveConfig = subCveConfig + + return &Scanner{ + log: log, + layoutUtils: layoutUtils, + cveController: cveController, + storeController: storeController, + dbLock: &sync.Mutex{}, + } +} + +func (scanner Scanner) getTrivyContext(image string) *trivyCtx { + // Split image to get route prefix + prefixName := common.GetRoutePrefix(image) + + var tCtx *trivyCtx + + var ok bool + + var rootDir string + + // Get corresponding CVE trivy config, if no sub cve config present that means its default + tCtx, ok = scanner.cveController.SubCveConfig[prefixName] + if ok { + imgStore := scanner.storeController.SubStore[prefixName] + + rootDir = imgStore.RootDir() + } else { + tCtx = scanner.cveController.DefaultCveConfig + + imgStore := scanner.storeController.DefaultStore + + rootDir = imgStore.RootDir() + } + + tCtx.Input = path.Join(rootDir, image) + + return tCtx +} + +func (scanner Scanner) IsImageFormatScannable(image string) (bool, error) { + imageDir, inputTag := common.GetImageDirAndTag(image) + + manifests, err := scanner.layoutUtils.GetImageManifests(imageDir) + if err != nil { + return false, err + } + + for _, manifest := range manifests { + tag, ok := manifest.Annotations[ispec.AnnotationRefName] + + if ok && inputTag != "" && tag != inputTag { + continue + } + + blobManifest, err := scanner.layoutUtils.GetImageBlobManifest(imageDir, manifest.Digest) + if err != nil { + return false, err + } + + imageLayers := blobManifest.Layers + + for _, imageLayer := range imageLayers { + switch imageLayer.MediaType { + case regTypes.OCILayer, regTypes.DockerLayer: + return true, nil + + default: + scanner.log.Debug().Str("image", + image).Msgf("image media type %s not supported for scanning", imageLayer.MediaType) + + return false, errors.ErrScanNotSupported + } + } + } + + return false, nil +} + +func (scanner Scanner) ScanImage(image string) (map[string]cvemodel.CVE, error) { + cveidMap := make(map[string]cvemodel.CVE) + + scanner.log.Info().Str("image", image).Msg("scanning image") + + tCtx := scanner.getTrivyContext(image) + + scanner.dbLock.Lock() + report, err := artifact.TrivyImageRun(tCtx.Ctx) + scanner.dbLock.Unlock() + + if err != nil { // nolint: wsl + scanner.log.Error().Err(err).Str("image", image).Msg("unable to scan image") + + return cveidMap, err + } + + for _, result := range report.Results { + for _, vulnerability := range result.Vulnerabilities { + pkgName := vulnerability.PkgName + + installedVersion := vulnerability.InstalledVersion + + var fixedVersion string + if vulnerability.FixedVersion != "" { + fixedVersion = vulnerability.FixedVersion + } else { + fixedVersion = "Not Specified" + } + + _, ok := cveidMap[vulnerability.VulnerabilityID] + if ok { + cveDetailStruct := cveidMap[vulnerability.VulnerabilityID] + + pkgList := cveDetailStruct.PackageList + + pkgList = append( + pkgList, + cvemodel.Package{ + Name: pkgName, + InstalledVersion: installedVersion, + FixedVersion: fixedVersion, + }, + ) + + cveDetailStruct.PackageList = pkgList + + cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct + } else { + newPkgList := make([]cvemodel.Package, 0) + + newPkgList = append( + newPkgList, + cvemodel.Package{ + Name: pkgName, + InstalledVersion: installedVersion, + FixedVersion: fixedVersion, + }, + ) + + cveidMap[vulnerability.VulnerabilityID] = cvemodel.CVE{ + ID: vulnerability.VulnerabilityID, + Title: vulnerability.Title, + Description: vulnerability.Description, + Severity: vulnerability.Severity, + PackageList: newPkgList, + } + } + } + } + + return cveidMap, nil +} + +// UpdateDb download the Trivy DB / Cache under the store root directory. +func (scanner Scanner) UpdateDB() error { + // We need a lock as using multiple substores each with it's own DB + // can result in a DATARACE because some varibles in trivy-db are global + // https://github.com/project-zot/trivy-db/blob/main/pkg/db/db.go#L23 + scanner.dbLock.Lock() + defer scanner.dbLock.Unlock() + + if scanner.storeController.DefaultStore != nil { + dbDir := scanner.storeController.DefaultStore.RootDir() + + err := scanner.updateDB(dbDir) + if err != nil { + return err + } + } + + if scanner.storeController.SubStore != nil { + for _, storage := range scanner.storeController.SubStore { + err := scanner.updateDB(storage.RootDir()) + if err != nil { + return err + } + } + } + + return nil +} + +func (scanner Scanner) updateDB(dbDir string) error { + scanner.log.Debug().Msgf("Download Trivy DB to destination dir: %s", dbDir) + + err := operation.DownloadDB("dev", dbDir, false, false, false) + if err != nil { + scanner.log.Error().Err(err).Msgf("Error downloading Trivy DB to destination dir: %s", dbDir) + + return err + } + + scanner.log.Debug().Msgf("Finished downloading Trivy DB to destination dir: %s", dbDir) + + return nil +} + +func (scanner Scanner) CompareSeverities(severity1, severity2 string) int { + return dbTypes.CompareSeverityString(severity1, severity2) +} diff --git a/pkg/extensions/search/cve/trivy/scanner_internal_test.go b/pkg/extensions/search/cve/trivy/scanner_internal_test.go new file mode 100644 index 00000000..9f383d67 --- /dev/null +++ b/pkg/extensions/search/cve/trivy/scanner_internal_test.go @@ -0,0 +1,144 @@ +package trivy + +import ( + "bytes" + "encoding/json" + "os" + "path" + "testing" + + godigest "github.com/opencontainers/go-digest" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + . "github.com/smartystreets/goconvey/convey" + "zotregistry.io/zot/pkg/api/config" + extconf "zotregistry.io/zot/pkg/extensions/config" + "zotregistry.io/zot/pkg/extensions/monitoring" + "zotregistry.io/zot/pkg/extensions/search/common" + "zotregistry.io/zot/pkg/log" + "zotregistry.io/zot/pkg/storage" + "zotregistry.io/zot/pkg/test" +) + +func generateTestImage(storeController storage.StoreController, image string) { + repoName, tag := common.GetImageDirAndTag(image) + + config, layers, manifest, err := test.GetImageComponents(10) + So(err, ShouldBeNil) + + store := storeController.GetImageStore(repoName) + err = store.InitRepo(repoName) + So(err, ShouldBeNil) + + for _, layerBlob := range layers { + layerReader := bytes.NewReader(layerBlob) + layerDigest := godigest.FromBytes(layerBlob) + _, _, err = store.FullBlobUpload(repoName, layerReader, layerDigest.String()) + So(err, ShouldBeNil) + } + + configBlob, err := json.Marshal(config) + So(err, ShouldBeNil) + configReader := bytes.NewReader(configBlob) + configDigest := godigest.FromBytes(configBlob) + _, _, err = store.FullBlobUpload(repoName, configReader, configDigest.String()) + So(err, ShouldBeNil) + + manifestBlob, err := json.Marshal(manifest) + So(err, ShouldBeNil) + _, err = store.PutImageManifest(repoName, tag, ispec.MediaTypeImageManifest, manifestBlob) + So(err, ShouldBeNil) +} + +func TestMultipleStoragePath(t *testing.T) { + Convey("Test multiple storage path", t, func() { + // Create temporary directory + firstRootDir := t.TempDir() + secondRootDir := t.TempDir() + thirdRootDir := t.TempDir() + + log := log.NewLogger("debug", "") + metrics := monitoring.NewMetricsServer(false, log) + + conf := config.New() + conf.Extensions = &extconf.ExtensionConfig{} + conf.Extensions.Lint = &extconf.LintConfig{} + + // Create ImageStore + firstStore := storage.NewImageStore(firstRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) + + secondStore := storage.NewImageStore(secondRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) + + thirdStore := storage.NewImageStore(thirdRootDir, false, storage.DefaultGCDelay, false, false, log, metrics, nil) + + storeController := storage.StoreController{} + + storeController.DefaultStore = firstStore + + subStore := make(map[string]storage.ImageStore) + + subStore["/a"] = secondStore + subStore["/b"] = thirdStore + + storeController.SubStore = subStore + + layoutUtils := common.NewBaseOciLayoutUtils(storeController, log) + + scanner := NewScanner(storeController, layoutUtils, log) + + So(scanner.storeController.DefaultStore, ShouldNotBeNil) + So(scanner.storeController.SubStore, ShouldNotBeNil) + + img0 := "test/image0:tag0" + img1 := "a/test/image1:tag1" + img2 := "b/test/image2:tag2" + + ctx := scanner.getTrivyContext(img0) + So(ctx.Input, ShouldEqual, path.Join(firstStore.RootDir(), img0)) + + ctx = scanner.getTrivyContext(img1) + So(ctx.Input, ShouldEqual, path.Join(secondStore.RootDir(), img1)) + + ctx = scanner.getTrivyContext(img2) + So(ctx.Input, ShouldEqual, path.Join(thirdStore.RootDir(), img2)) + + generateTestImage(storeController, img0) + generateTestImage(storeController, img1) + generateTestImage(storeController, img2) + + // Scanning image in default store + cveMap, err := scanner.ScanImage(img0) + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 0) + + // Scanning image in substore + cveMap, err = scanner.ScanImage(img1) + So(err, ShouldBeNil) + So(len(cveMap), ShouldEqual, 0) + + // Scanning image which does not exist + cveMap, err = scanner.ScanImage("a/test/image2:tag100") + So(err, ShouldNotBeNil) + So(len(cveMap), ShouldEqual, 0) + + // Download the DB to a default store location without permissions + err = os.Chmod(firstRootDir, 0o000) + So(err, ShouldBeNil) + err = scanner.UpdateDB() + So(err, ShouldNotBeNil) + + // Check the download works correctly when permissions allow + err = os.Chmod(firstRootDir, 0o777) + So(err, ShouldBeNil) + err = scanner.UpdateDB() + So(err, ShouldBeNil) + + // Download the DB to a substore location without permissions + err = os.Chmod(secondRootDir, 0o000) + So(err, ShouldBeNil) + err = scanner.UpdateDB() + So(err, ShouldNotBeNil) + + err = os.Chmod(secondRootDir, 0o777) + So(err, ShouldBeNil) + }) +} diff --git a/pkg/extensions/search/gql_generated/generated.go b/pkg/extensions/search/gql_generated/generated.go index 4363bcfb..da3b36c8 100644 --- a/pkg/extensions/search/gql_generated/generated.go +++ b/pkg/extensions/search/gql_generated/generated.go @@ -71,25 +71,31 @@ type ComplexityRoot struct { } ImageSummary struct { - ConfigDigest func(childComplexity int) int - Description func(childComplexity int) int - Digest func(childComplexity int) int - Documentation func(childComplexity int) int - DownloadCount func(childComplexity int) int - History func(childComplexity int) int - IsSigned func(childComplexity int) int - Labels func(childComplexity int) int - LastUpdated func(childComplexity int) int - Layers func(childComplexity int) int - Licenses func(childComplexity int) int - Platform func(childComplexity int) int - RepoName func(childComplexity int) int - Score func(childComplexity int) int - Size func(childComplexity int) int - Source func(childComplexity int) int - Tag func(childComplexity int) int - Title func(childComplexity int) int - Vendor func(childComplexity int) int + ConfigDigest func(childComplexity int) int + Description func(childComplexity int) int + Digest func(childComplexity int) int + Documentation func(childComplexity int) int + DownloadCount func(childComplexity int) int + History func(childComplexity int) int + IsSigned func(childComplexity int) int + Labels func(childComplexity int) int + LastUpdated func(childComplexity int) int + Layers func(childComplexity int) int + Licenses func(childComplexity int) int + Platform func(childComplexity int) int + RepoName func(childComplexity int) int + Score func(childComplexity int) int + Size func(childComplexity int) int + Source func(childComplexity int) int + Tag func(childComplexity int) int + Title func(childComplexity int) int + Vendor func(childComplexity int) int + Vulnerabilities func(childComplexity int) int + } + + ImageVulnerabilitySummary struct { + Count func(childComplexity int) int + MaxSeverity func(childComplexity int) int } LayerHistory struct { @@ -412,6 +418,27 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.ImageSummary.Vendor(childComplexity), true + case "ImageSummary.Vulnerabilities": + if e.complexity.ImageSummary.Vulnerabilities == nil { + break + } + + return e.complexity.ImageSummary.Vulnerabilities(childComplexity), true + + case "ImageVulnerabilitySummary.Count": + if e.complexity.ImageVulnerabilitySummary.Count == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.Count(childComplexity), true + + case "ImageVulnerabilitySummary.MaxSeverity": + if e.complexity.ImageVulnerabilitySummary.MaxSeverity == nil { + break + } + + return e.complexity.ImageVulnerabilitySummary.MaxSeverity(childComplexity), true + case "LayerHistory.HistoryDescription": if e.complexity.LayerHistory.HistoryDescription == nil { break @@ -789,6 +816,12 @@ type ImageSummary { Source: String Documentation: String History: [LayerHistory] + Vulnerabilities: ImageVulnerabilitySummary +} + +type ImageVulnerabilitySummary { + MaxSeverity: String + Count: Int } # Brief on a specific repo to be used in queries returning a list of repos @@ -1440,6 +1473,8 @@ func (ec *executionContext) fieldContext_GlobalSearchResult_Images(ctx context.C return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -2563,6 +2598,135 @@ func (ec *executionContext) fieldContext_ImageSummary_History(ctx context.Contex return fc, nil } +func (ec *executionContext) _ImageSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField, obj *ImageSummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Vulnerabilities, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*ImageVulnerabilitySummary) + fc.Result = res + return ec.marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageSummary_Vulnerabilities(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageSummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "MaxSeverity": + return ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) + case "Count": + return ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ImageVulnerabilitySummary", field.Name) + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.MaxSeverity, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*string) + fc.Result = res + return ec.marshalOString2ᚖstring(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_MaxSeverity(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + +func (ec *executionContext) _ImageVulnerabilitySummary_Count(ctx context.Context, field graphql.CollectedField, obj *ImageVulnerabilitySummary) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_ImageVulnerabilitySummary_Count(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Count, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*int) + fc.Result = res + return ec.marshalOInt2ᚖint(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_ImageVulnerabilitySummary_Count(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "ImageVulnerabilitySummary", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Int does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _LayerHistory_Layer(ctx context.Context, field graphql.CollectedField, obj *LayerHistory) (ret graphql.Marshaler) { fc, err := ec.fieldContext_LayerHistory_Layer(ctx, field) if err != nil { @@ -3128,6 +3292,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForCVE(ctx context.Conte return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3220,6 +3386,8 @@ func (ec *executionContext) fieldContext_Query_ImageListWithCVEFixed(ctx context return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3312,6 +3480,8 @@ func (ec *executionContext) fieldContext_Query_ImageListForDigest(ctx context.Co return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3470,6 +3640,8 @@ func (ec *executionContext) fieldContext_Query_ImageList(ctx context.Context, fi return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3686,6 +3858,8 @@ func (ec *executionContext) fieldContext_Query_DerivedImageList(ctx context.Cont return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3778,6 +3952,8 @@ func (ec *executionContext) fieldContext_Query_BaseImageList(ctx context.Context return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -3999,6 +4175,8 @@ func (ec *executionContext) fieldContext_RepoInfo_Images(ctx context.Context, fi return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -4395,6 +4573,8 @@ func (ec *executionContext) fieldContext_RepoSummary_NewestImage(ctx context.Con return ec.fieldContext_ImageSummary_Documentation(ctx, field) case "History": return ec.fieldContext_ImageSummary_History(ctx, field) + case "Vulnerabilities": + return ec.fieldContext_ImageSummary_Vulnerabilities(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type ImageSummary", field.Name) }, @@ -6536,6 +6716,39 @@ func (ec *executionContext) _ImageSummary(ctx context.Context, sel ast.Selection out.Values[i] = ec._ImageSummary_History(ctx, field, obj) + case "Vulnerabilities": + + out.Values[i] = ec._ImageSummary_Vulnerabilities(ctx, field, obj) + + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + +var imageVulnerabilitySummaryImplementors = []string{"ImageVulnerabilitySummary"} + +func (ec *executionContext) _ImageVulnerabilitySummary(ctx context.Context, sel ast.SelectionSet, obj *ImageVulnerabilitySummary) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, imageVulnerabilitySummaryImplementors) + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ImageVulnerabilitySummary") + case "MaxSeverity": + + out.Values[i] = ec._ImageVulnerabilitySummary_MaxSeverity(ctx, field, obj) + + case "Count": + + out.Values[i] = ec._ImageVulnerabilitySummary_Count(ctx, field, obj) + default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -7898,6 +8111,13 @@ func (ec *executionContext) marshalOImageSummary2ᚖzotregistryᚗioᚋzotᚋpkg return ec._ImageSummary(ctx, sel, v) } +func (ec *executionContext) marshalOImageVulnerabilitySummary2ᚖzotregistryᚗioᚋzotᚋpkgᚋextensionsᚋsearchᚋgql_generatedᚐImageVulnerabilitySummary(ctx context.Context, sel ast.SelectionSet, v *ImageVulnerabilitySummary) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ImageVulnerabilitySummary(ctx, sel, v) +} + func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) { if v == nil { return nil, nil diff --git a/pkg/extensions/search/gql_generated/models_gen.go b/pkg/extensions/search/gql_generated/models_gen.go index 7b017133..5428b57f 100644 --- a/pkg/extensions/search/gql_generated/models_gen.go +++ b/pkg/extensions/search/gql_generated/models_gen.go @@ -38,25 +38,31 @@ type HistoryDescription struct { } type ImageSummary struct { - RepoName *string `json:"RepoName"` - Tag *string `json:"Tag"` - Digest *string `json:"Digest"` - ConfigDigest *string `json:"ConfigDigest"` - LastUpdated *time.Time `json:"LastUpdated"` - IsSigned *bool `json:"IsSigned"` - Size *string `json:"Size"` - Platform *OsArch `json:"Platform"` - Vendor *string `json:"Vendor"` - Score *int `json:"Score"` - DownloadCount *int `json:"DownloadCount"` - Layers []*LayerSummary `json:"Layers"` - Description *string `json:"Description"` - Licenses *string `json:"Licenses"` - Labels *string `json:"Labels"` - Title *string `json:"Title"` - Source *string `json:"Source"` - Documentation *string `json:"Documentation"` - History []*LayerHistory `json:"History"` + RepoName *string `json:"RepoName"` + Tag *string `json:"Tag"` + Digest *string `json:"Digest"` + ConfigDigest *string `json:"ConfigDigest"` + LastUpdated *time.Time `json:"LastUpdated"` + IsSigned *bool `json:"IsSigned"` + Size *string `json:"Size"` + Platform *OsArch `json:"Platform"` + Vendor *string `json:"Vendor"` + Score *int `json:"Score"` + DownloadCount *int `json:"DownloadCount"` + Layers []*LayerSummary `json:"Layers"` + Description *string `json:"Description"` + Licenses *string `json:"Licenses"` + Labels *string `json:"Labels"` + Title *string `json:"Title"` + Source *string `json:"Source"` + Documentation *string `json:"Documentation"` + History []*LayerHistory `json:"History"` + Vulnerabilities *ImageVulnerabilitySummary `json:"Vulnerabilities"` +} + +type ImageVulnerabilitySummary struct { + MaxSeverity *string `json:"MaxSeverity"` + Count *int `json:"Count"` } type LayerHistory struct { diff --git a/pkg/extensions/search/resolver.go b/pkg/extensions/search/resolver.go index 850ae978..e8ee436b 100644 --- a/pkg/extensions/search/resolver.go +++ b/pkg/extensions/search/resolver.go @@ -29,37 +29,20 @@ import ( // Resolver ... type Resolver struct { - cveInfo *cveinfo.CveInfo + cveInfo cveinfo.CveInfo storeController storage.StoreController digestInfo *digestinfo.DigestInfo log log.Logger } -type cveDetail struct { - Title string - Description string - Severity string - PackageList []*gql_generated.PackageInfo -} - var ( ErrBadCtxFormat = errors.New("type assertion failed") ErrBadLayerCount = errors.New("manifest: layers count doesn't correspond to config history") ) // GetResolverConfig ... -func GetResolverConfig(log log.Logger, storeController storage.StoreController, enableCVE bool) gql_generated.Config { - var cveInfo *cveinfo.CveInfo - - var err error - - if enableCVE { - cveInfo, err = cveinfo.GetCVEInfo(storeController, log) - if err != nil { - panic(err) - } - } - +func GetResolverConfig(log log.Logger, storeController storage.StoreController, cveInfo cveinfo.CveInfo, +) gql_generated.Config { digestInfo := digestinfo.NewDigestInfo(storeController, log) resConfig := &Resolver{cveInfo: cveInfo, storeController: storeController, digestInfo: digestInfo, log: log} @@ -70,39 +53,6 @@ func GetResolverConfig(log log.Logger, storeController storage.StoreController, } } -func (r *queryResolver) getImageListForCVE(repoList []string, cvid string, imgStore storage.ImageStore, - trivyCtx *cveinfo.TrivyCtx, -) ([]*gql_generated.ImageSummary, error) { - cveResult := []*gql_generated.ImageSummary{} - olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) - - for _, repo := range repoList { - r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo") - - imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, cvid, imgStore, trivyCtx) - if err != nil { - r.log.Error().Err(err).Msg("error getting tag") - - return cveResult, err - } - - for _, imageByCVE := range imageListByCVE { - imageConfig, err := olu.GetImageConfigInfo(repo, imageByCVE.Digest) - if err != nil { - return []*gql_generated.ImageSummary{}, err - } - - imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig) - - cveResult = append( - cveResult, imageInfo, - ) - } - } - - return cveResult, nil -} - func (r *queryResolver) getImageListForDigest(repoList []string, digest string) ([]*gql_generated.ImageSummary, error) { imgResultForDigest := []*gql_generated.ImageSummary{} olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) @@ -138,6 +88,7 @@ func repoListWithNewestImage( ctx context.Context, repoList []string, olu common.OciLayoutUtils, + cveInfo cveinfo.CveInfo, log log.Logger, ) ([]*gql_generated.RepoSummary, error) { reposSummary := []*gql_generated.RepoSummary{} @@ -227,7 +178,8 @@ func repoListWithNewestImage( manifestTag, ok := manifest.Annotations[ispec.AnnotationRefName] if !ok { - msg := fmt.Sprintf("reference not found for manifest %s", manifest.Digest.String()) + msg := fmt.Sprintf("reference not found for manifest %s in repo %s", + manifest.Digest.String(), repoName) log.Error().Msg(msg) graphql.AddError(ctx, gqlerror.Errorf(msg)) @@ -237,6 +189,25 @@ func repoListWithNewestImage( break } + imageCveSummary := cveinfo.ImageCVESummary{} + // Check if vulnerability scanning is disabled + if cveInfo != nil { + imageName := fmt.Sprintf("%s:%s", repoName, manifestTag) + imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName) + + if err != nil { + // Log the error, but we should still include the manifest in results + msg := fmt.Sprintf( + "unable to run vulnerability scan on tag %s in repo %s", + manifestTag, + repoName, + ) + + log.Error().Msg(msg) + graphql.AddError(ctx, gqlerror.Errorf(msg)) + } + } + tag := manifestTag size := strconv.Itoa(int(imageSize)) manifestDigest := manifest.Digest.Hex() @@ -262,6 +233,10 @@ func repoListWithNewestImage( Licenses: &annotations.Licenses, Labels: &annotations.Labels, Source: &annotations.Source, + Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ + MaxSeverity: &imageCveSummary.MaxSeverity, + Count: &imageCveSummary.Count, + }, } if manifest.Digest.String() == lastUpdatedTag.Digest { @@ -301,7 +276,8 @@ func cleanQuerry(query string) string { return query } -func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils, log log.Logger) ( +func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils, + cveInfo cveinfo.CveInfo, log log.Logger) ( []*gql_generated.RepoSummary, []*gql_generated.ImageSummary, []*gql_generated.LayerSummary, ) { repos := []*gql_generated.RepoSummary{} @@ -413,6 +389,22 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils repoPlatforms = append(repoPlatforms, osArch) repoVendors = append(repoVendors, &annotations.Vendor) + imageCveSummary := cveinfo.ImageCVESummary{} + // Check if vulnerability scanning is disabled + if cveInfo != nil { + imageName := fmt.Sprintf("%s:%s", repo, manifestTag) + imageCveSummary, err = cveInfo.GetCVESummaryForImage(imageName) + + if err != nil { + // Log the error, but we should still include the manifest in results + log.Error().Err(err).Msgf( + "unable to run vulnerability scan on tag %s in repo %s", + manifestTag, + repo, + ) + } + } + imageSummary := gql_generated.ImageSummary{ RepoName: &repo, Tag: &manifestTag, @@ -430,6 +422,10 @@ func globalSearch(repoList []string, name, tag string, olu common.OciLayoutUtils Licenses: &annotations.Licenses, Labels: &annotations.Labels, Source: &annotations.Source, + Vulnerabilities: &gql_generated.ImageVulnerabilitySummary{ + MaxSeverity: &imageCveSummary.MaxSeverity, + Count: &imageCveSummary.Count, + }, } if manifest.Digest.String() == lastUpdatedTag.Digest { diff --git a/pkg/extensions/search/resolver_test.go b/pkg/extensions/search/resolver_test.go index 0bf170a4..1dce5584 100644 --- a/pkg/extensions/search/resolver_test.go +++ b/pkg/extensions/search/resolver_test.go @@ -31,8 +31,9 @@ func TestGlobalSearch(t *testing.T) { return common.TagInfo{}, ErrTestError }, } + mockCve := mocks.CveInfoMock{} - globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("GetImageTagsWithTimestamp fail", func() { @@ -41,8 +42,9 @@ func TestGlobalSearch(t *testing.T) { return []common.TagInfo{}, ErrTestError }, } + mockCve := mocks.CveInfoMock{} - globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("GetImageManifests fail", func() { @@ -51,8 +53,9 @@ func TestGlobalSearch(t *testing.T) { return []ispec.Descriptor{}, ErrTestError }, } + mockCve := mocks.CveInfoMock{} - globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("Manifests given, bad image blob manifest", func() { @@ -72,7 +75,9 @@ func TestGlobalSearch(t *testing.T) { return v1.Manifest{}, ErrTestError }, } - globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + + globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("Manifests given, no manifest tag", func() { @@ -86,8 +91,9 @@ func TestGlobalSearch(t *testing.T) { }, nil }, } + mockCve := mocks.CveInfoMock{} - globalSearch([]string{"repo1"}, "test", "tag", mockOlum, log.NewLogger("debug", "")) + globalSearch([]string{"repo1"}, "test", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("Global search success, no tag", func() { @@ -119,7 +125,8 @@ func TestGlobalSearch(t *testing.T) { }, nil }, } - globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("Manifests given, bad image config info", func() { @@ -139,7 +146,8 @@ func TestGlobalSearch(t *testing.T) { return ispec.Image{}, ErrTestError }, } - globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + globalSearch([]string{"repo1/name"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) Convey("Tag given, no layer match", func() { @@ -174,7 +182,8 @@ func TestGlobalSearch(t *testing.T) { }, nil }, } - globalSearch([]string{"repo1"}, "name", "tag", mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + globalSearch([]string{"repo1"}, "name", "tag", mockOlum, mockCve, log.NewLogger("debug", "")) }) }) } @@ -189,7 +198,8 @@ func TestRepoListWithNewestImage(t *testing.T) { } ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) - _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) errs := graphql.GetErrors(ctx) @@ -212,7 +222,8 @@ func TestRepoListWithNewestImage(t *testing.T) { } ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) - _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) errs := graphql.GetErrors(ctx) @@ -237,7 +248,8 @@ func TestRepoListWithNewestImage(t *testing.T) { } ctx := graphql.WithResponseContext(context.Background(), graphql.DefaultErrorPresenter, graphql.Recover) - _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, log.NewLogger("debug", "")) + mockCve := mocks.CveInfoMock{} + _, err := repoListWithNewestImage(ctx, []string{"repo1"}, mockOlum, mockCve, log.NewLogger("debug", "")) So(err, ShouldBeNil) errs := graphql.GetErrors(ctx) diff --git a/pkg/extensions/search/schema.graphql b/pkg/extensions/search/schema.graphql index c3e8bb64..7b21d725 100644 --- a/pkg/extensions/search/schema.graphql +++ b/pkg/extensions/search/schema.graphql @@ -54,6 +54,12 @@ type ImageSummary { Source: String Documentation: String History: [LayerHistory] + Vulnerabilities: ImageVulnerabilitySummary +} + +type ImageVulnerabilitySummary { + MaxSeverity: String + Count: Int } # Brief on a specific repo to be used in queries returning a list of repos diff --git a/pkg/extensions/search/schema.resolvers.go b/pkg/extensions/search/schema.resolvers.go index e37250d8..1e907c3e 100644 --- a/pkg/extensions/search/schema.resolvers.go +++ b/pkg/extensions/search/schema.resolvers.go @@ -6,96 +6,52 @@ package search import ( "context" "fmt" - "strings" godigest "github.com/opencontainers/go-digest" "zotregistry.io/zot/pkg/extensions/search/common" - cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" "zotregistry.io/zot/pkg/extensions/search/gql_generated" ) // CVEListForImage is the resolver for the CVEListForImage field. func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql_generated.CVEResultForImage, error) { - trivyCtx := r.cveInfo.GetTrivyContext(image) - - r.log.Info().Str("image", image).Msg("scanning image") - - isValidImage, err := r.cveInfo.LayoutUtils.IsValidImageFormat(image) - if !isValidImage { - r.log.Debug().Str("image", image).Msg("image media type not supported for scanning") - - return &gql_generated.CVEResultForImage{}, err - } - - report, err := cveinfo.ScanImage(trivyCtx.Ctx) + cveidMap, err := r.cveInfo.GetCVEListForImage(image) if err != nil { - r.log.Error().Err(err).Msg("unable to scan image repository") - return &gql_generated.CVEResultForImage{}, err } - var copyImgTag string - - if strings.Contains(image, ":") { - copyImgTag = strings.Split(image, ":")[1] - } - - cveidMap := make(map[string]cveDetail) - - for _, result := range report.Results { - for _, vulnerability := range result.Vulnerabilities { - pkgName := vulnerability.PkgName - - installedVersion := vulnerability.InstalledVersion - - var fixedVersion string - if vulnerability.FixedVersion != "" { - fixedVersion = vulnerability.FixedVersion - } else { - fixedVersion = "Not Specified" - } - - _, ok := cveidMap[vulnerability.VulnerabilityID] - if ok { - cveDetailStruct := cveidMap[vulnerability.VulnerabilityID] - - pkgList := cveDetailStruct.PackageList - - pkgList = append(pkgList, - &gql_generated.PackageInfo{Name: &pkgName, InstalledVersion: &installedVersion, FixedVersion: &fixedVersion}) - - cveDetailStruct.PackageList = pkgList - - cveidMap[vulnerability.VulnerabilityID] = cveDetailStruct - } else { - newPkgList := make([]*gql_generated.PackageInfo, 0) - - newPkgList = append(newPkgList, - &gql_generated.PackageInfo{Name: &pkgName, InstalledVersion: &installedVersion, FixedVersion: &fixedVersion}) - - cveidMap[vulnerability.VulnerabilityID] = cveDetail{ - Title: vulnerability.Title, - Description: vulnerability.Description, Severity: vulnerability.Severity, PackageList: newPkgList, - } - } - } - } + _, copyImgTag := common.GetImageDirAndTag(image) cveids := []*gql_generated.Cve{} for id, cveDetail := range cveidMap { vulID := id - desc := cveDetail.Description - title := cveDetail.Title - severity := cveDetail.Severity - pkgList := cveDetail.PackageList + 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, PackageList: pkgList}) + &gql_generated.Cve{ + ID: &vulID, + Title: &title, + Description: &desc, + Severity: &severity, + PackageList: pkgList, + }, + ) } return &gql_generated.CVEResultForImage{Tag: ©ImgTag, CVEList: cveids}, nil @@ -103,146 +59,82 @@ func (r *queryResolver) CVEListForImage(ctx context.Context, image string) (*gql // ImageListForCve is the resolver for the ImageListForCVE field. func (r *queryResolver) ImageListForCve(ctx context.Context, id string) ([]*gql_generated.ImageSummary, error) { - finalCveResult := []*gql_generated.ImageSummary{} + olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) + + affectedImages := []*gql_generated.ImageSummary{} r.log.Info().Msg("extracting repositories") - - defaultStore := r.storeController.DefaultStore - - defaultTrivyCtx := r.cveInfo.CveTrivyController.DefaultCveConfig - - repoList, err := defaultStore.GetRepositories() - if err != nil { + repoList, err := olu.GetRepositories() + if err != nil { // nolint: wsl r.log.Error().Err(err).Msg("unable to search repositories") - return finalCveResult, err + return affectedImages, err } - r.cveInfo.Log.Info().Msg("scanning each global repository") + r.log.Info().Msg("scanning each repository") - cveResult, err := r.getImageListForCVE(repoList, id, defaultStore, defaultTrivyCtx) - if err != nil { - r.log.Error().Err(err).Msg("error getting cve list for global repositories") + for _, repo := range repoList { + r.log.Info().Str("repo", repo).Msg("extracting list of tags available in image repo") - return finalCveResult, err - } - - finalCveResult = append(finalCveResult, cveResult...) - - subStore := r.storeController.SubStore - - for route, store := range subStore { - subRepoList, err := store.GetRepositories() + imageListByCVE, err := r.cveInfo.GetImageListForCVE(repo, id) if err != nil { - r.log.Error().Err(err).Msg("unable to search repositories") + r.log.Error().Str("repo", repo).Str("CVE", id).Err(err). + Msg("error getting image list for CVE from repo") - return cveResult, err + return affectedImages, err } - subTrivyCtx := r.cveInfo.CveTrivyController.SubCveConfig[route] + for _, imageByCVE := range imageListByCVE { + imageConfig, err := olu.GetImageConfigInfo(repo, imageByCVE.Digest) + if err != nil { + return affectedImages, err + } - subCveResult, err := r.getImageListForCVE(subRepoList, id, store, subTrivyCtx) - if err != nil { - r.log.Error().Err(err).Msg("unable to get cve result for sub repositories") + imageInfo := BuildImageInfo(repo, imageByCVE.Tag, imageByCVE.Digest, imageByCVE.Manifest, imageConfig) - return finalCveResult, err + affectedImages = append( + affectedImages, + imageInfo, + ) } - - finalCveResult = append(finalCveResult, subCveResult...) } - return finalCveResult, nil + return affectedImages, nil } // ImageListWithCVEFixed is the resolver for the ImageListWithCVEFixed field. func (r *queryResolver) ImageListWithCVEFixed(ctx context.Context, id string, image string) ([]*gql_generated.ImageSummary, error) { - tagListForCVE := []*gql_generated.ImageSummary{} + olu := common.NewBaseOciLayoutUtils(r.storeController, r.log) - r.log.Info().Str("image", image).Msg("extracting list of tags available in repo") + unaffectedImages := []*gql_generated.ImageSummary{} - tagsInfo, err := r.cveInfo.LayoutUtils.GetImageTagsWithTimestamp(image) + tagsInfo, err := r.cveInfo.GetImageListWithCVEFixed(image, id) if err != nil { - r.log.Error().Err(err).Msg("unable to read image tags") - - return tagListForCVE, err - } - - infectedTags := make([]common.TagInfo, 0) - - var hasCVE bool - - for _, tag := range tagsInfo { - image := fmt.Sprintf("%s:%s", image, tag.Name) - - isValidImage, _ := r.cveInfo.LayoutUtils.IsValidImageFormat(image) - if !isValidImage { - r.log.Debug().Str("image", - fmt.Sprintf("%s:%s", image, tag.Name)). - Msg("image media type not supported for scanning, adding as an infected image") - - infectedTags = append(infectedTags, common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp}) - - continue - } - - trivyCtx := r.cveInfo.GetTrivyContext(image) - - r.cveInfo.Log.Info().Str("image", fmt.Sprintf("%s:%s", image, tag.Name)).Msg("scanning image") - - report, err := cveinfo.ScanImage(trivyCtx.Ctx) - if err != nil { - r.log.Error().Err(err). - Str("image", fmt.Sprintf("%s:%s", image, tag.Name)).Msg("unable to scan image") - - continue - } - - hasCVE = false - - for _, result := range report.Results { - for _, vulnerability := range result.Vulnerabilities { - if vulnerability.VulnerabilityID == id { - hasCVE = true - - break - } - } - } - - if hasCVE { - infectedTags = append(infectedTags, common.TagInfo{Name: tag.Name, Timestamp: tag.Timestamp, Digest: tag.Digest}) - } - } - - if len(infectedTags) != 0 { - r.log.Info().Msg("comparing fixed tags timestamp") - - tagsInfo = common.GetFixedTags(tagsInfo, infectedTags) - } else { - r.log.Info().Str("image", image).Str("cve-id", id).Msg("image does not contain any tag that have given cve") + return unaffectedImages, err } for _, tag := range tagsInfo { digest := godigest.Digest(tag.Digest) - manifest, err := r.cveInfo.LayoutUtils.GetImageBlobManifest(image, digest) + manifest, err := olu.GetImageBlobManifest(image, digest) if err != nil { - r.log.Error().Err(err).Msg("extension api: error reading manifest") + r.log.Error().Err(err).Str("repo", image).Str("digest", tag.Digest). + Msg("extension api: error reading manifest") - return []*gql_generated.ImageSummary{}, err + return unaffectedImages, err } - imageConfig, err := r.cveInfo.LayoutUtils.GetImageConfigInfo(image, digest) + imageConfig, err := olu.GetImageConfigInfo(image, digest) if err != nil { return []*gql_generated.ImageSummary{}, err } imageInfo := BuildImageInfo(image, tag.Name, digest, manifest, imageConfig) - tagListForCVE = append(tagListForCVE, imageInfo) + unaffectedImages = append(unaffectedImages, imageInfo) } - return tagListForCVE, nil + return unaffectedImages, nil } // ImageListForDigest is the resolver for the ImageListForDigest field. @@ -326,7 +218,7 @@ func (r *queryResolver) RepoListWithNewestImage(ctx context.Context) ([]*gql_gen repoList = append(repoList, subRepoList...) } - reposSummary, err = repoListWithNewestImage(ctx, repoList, olu, r.log) + reposSummary, err = repoListWithNewestImage(ctx, repoList, olu, r.cveInfo, r.log) if err != nil { r.log.Error().Err(err).Msg("extension api: error extracting substore image list") @@ -492,7 +384,7 @@ func (r *queryResolver) GlobalSearch(ctx context.Context, query string) (*gql_ge return &gql_generated.GlobalSearchResult{}, err } - repos, images, layers := globalSearch(availableRepos, name, tag, olu, r.log) + repos, images, layers := globalSearch(availableRepos, name, tag, olu, r.cveInfo, r.log) return &gql_generated.GlobalSearchResult{ Images: images, diff --git a/pkg/test/mocks/cve_mock.go b/pkg/test/mocks/cve_mock.go new file mode 100644 index 00000000..f6b0dea1 --- /dev/null +++ b/pkg/test/mocks/cve_mock.go @@ -0,0 +1,94 @@ +package mocks + +import ( + "zotregistry.io/zot/pkg/extensions/search/common" + cveinfo "zotregistry.io/zot/pkg/extensions/search/cve" + cvemodel "zotregistry.io/zot/pkg/extensions/search/cve/model" +) + +type CveInfoMock struct { + GetImageListForCVEFn func(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error) + GetImageListWithCVEFixedFn func(repo, cveID string) ([]common.TagInfo, error) + GetCVEListForImageFn func(image string) (map[string]cvemodel.CVE, error) + GetCVESummaryForImageFn func(image string) (cveinfo.ImageCVESummary, error) + UpdateDBFn func() error +} + +func (cveInfo CveInfoMock) GetImageListForCVE(repo, cveID string) ([]cveinfo.ImageInfoByCVE, error) { + if cveInfo.GetImageListForCVEFn != nil { + return cveInfo.GetImageListForCVEFn(repo, cveID) + } + + return []cveinfo.ImageInfoByCVE{}, nil +} + +func (cveInfo CveInfoMock) GetImageListWithCVEFixed(repo, cveID string) ([]common.TagInfo, error) { + if cveInfo.GetImageListWithCVEFixedFn != nil { + return cveInfo.GetImageListWithCVEFixedFn(repo, cveID) + } + + return []common.TagInfo{}, nil +} + +func (cveInfo CveInfoMock) GetCVEListForImage(image string) (map[string]cvemodel.CVE, error) { + if cveInfo.GetCVEListForImageFn != nil { + return cveInfo.GetCVEListForImageFn(image) + } + + return map[string]cvemodel.CVE{}, nil +} + +func (cveInfo CveInfoMock) GetCVESummaryForImage(image string) (cveinfo.ImageCVESummary, error) { + if cveInfo.GetCVESummaryForImageFn != nil { + return cveInfo.GetCVESummaryForImageFn(image) + } + + return cveinfo.ImageCVESummary{}, nil +} + +func (cveInfo CveInfoMock) UpdateDB() error { + if cveInfo.UpdateDBFn != nil { + return cveInfo.UpdateDBFn() + } + + return nil +} + +type CveScannerMock struct { + IsImageFormatScannableFn func(image string) (bool, error) + ScanImageFn func(image string) (map[string]cvemodel.CVE, error) + CompareSeveritiesFn func(severity1, severity2 string) int + UpdateDBFn func() error +} + +func (scanner CveScannerMock) IsImageFormatScannable(image string) (bool, error) { + if scanner.IsImageFormatScannableFn != nil { + return scanner.IsImageFormatScannableFn(image) + } + + return true, nil +} + +func (scanner CveScannerMock) ScanImage(image string) (map[string]cvemodel.CVE, error) { + if scanner.ScanImageFn != nil { + return scanner.ScanImageFn(image) + } + + return map[string]cvemodel.CVE{}, nil +} + +func (scanner CveScannerMock) CompareSeverities(severity1, severity2 string) int { + if scanner.CompareSeveritiesFn != nil { + return scanner.CompareSeveritiesFn(severity1, severity2) + } + + return 0 +} + +func (scanner CveScannerMock) UpdateDB() error { + if scanner.UpdateDBFn != nil { + return scanner.UpdateDBFn() + } + + return nil +} diff --git a/pkg/test/mocks/oci_mock.go b/pkg/test/mocks/oci_mock.go index 8b09b135..9864bff9 100644 --- a/pkg/test/mocks/oci_mock.go +++ b/pkg/test/mocks/oci_mock.go @@ -13,7 +13,6 @@ type OciLayoutUtilsMock struct { GetImageManifestsFn func(image string) ([]ispec.Descriptor, error) GetImageBlobManifestFn func(imageDir string, digest godigest.Digest) (v1.Manifest, error) GetImageInfoFn func(imageDir string, hash v1.Hash) (ispec.Image, error) - IsValidImageFormatFn func(image string) (bool, error) GetImageTagsWithTimestampFn func(repo string) ([]common.TagInfo, error) GetImageLastUpdatedFn func(imageInfo ispec.Image) time.Time GetImagePlatformFn func(imageInfo ispec.Image) (string, string) @@ -28,7 +27,7 @@ type OciLayoutUtilsMock struct { } func (olum OciLayoutUtilsMock) GetRepositories() ([]string, error) { - if olum.GetImageManifestsFn != nil { + if olum.GetRepositoriesFn != nil { return olum.GetRepositoriesFn() } @@ -59,14 +58,6 @@ func (olum OciLayoutUtilsMock) GetImageInfo(imageDir string, hash v1.Hash) (ispe return ispec.Image{}, nil } -func (olum OciLayoutUtilsMock) IsValidImageFormat(image string) (bool, error) { - if olum.IsValidImageFormatFn != nil { - return olum.IsValidImageFormatFn(image) - } - - return true, nil -} - func (olum OciLayoutUtilsMock) GetImageTagsWithTimestamp(repo string) ([]common.TagInfo, error) { if olum.GetImageTagsWithTimestampFn != nil { return olum.GetImageTagsWithTimestampFn(repo)