Files
zot/pkg/extensions/search/cve/trivy/scanner_test.go
Andrei Aaron 451e7b8e47 feat: Add TrivyConfig.VulnSeveritySources (Trivy's --vuln-severity-source) (#3943)
And default it to ["auto"] when unset, with an info log from applyDefaultValues.

Refactor CVE NewScanner to take *CVEConfig instead of separate DB repository
strings so the full Trivy block is available to the scanner.

Extend CLI and search tests for the new field and logged config; document
CVE/Trivy in examples/README and add examples/config-cve-trivy.json.

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2026-04-08 09:39:26 +03:00

515 lines
16 KiB
Go

//go:build search
package trivy_test
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
"time"
xos "github.com/aquasecurity/trivy/pkg/x/os"
godigest "github.com/opencontainers/go-digest"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
. "github.com/smartystreets/goconvey/convey"
"zotregistry.dev/zot/v2/pkg/api"
"zotregistry.dev/zot/v2/pkg/api/config"
extconf "zotregistry.dev/zot/v2/pkg/extensions/config"
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
"zotregistry.dev/zot/v2/pkg/extensions/search/cve/trivy"
"zotregistry.dev/zot/v2/pkg/log"
"zotregistry.dev/zot/v2/pkg/meta"
"zotregistry.dev/zot/v2/pkg/meta/boltdb"
"zotregistry.dev/zot/v2/pkg/meta/types"
"zotregistry.dev/zot/v2/pkg/storage"
"zotregistry.dev/zot/v2/pkg/storage/local"
. "zotregistry.dev/zot/v2/pkg/test/common"
. "zotregistry.dev/zot/v2/pkg/test/image-utils"
"zotregistry.dev/zot/v2/pkg/test/mocks"
)
var ErrTestError = errors.New("test error")
func TestScanBigTestFile(t *testing.T) {
Convey("Scan zot-test", t, func() {
projRootDir, err := GetProjectRootDir()
So(err, ShouldBeNil)
testImage := filepath.Join(projRootDir, "test/data/zot-test")
tempDir := t.TempDir()
port := GetFreePort()
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
err = CopyFiles(testImage, filepath.Join(tempDir, "zot-test"))
So(err, ShouldBeNil)
cm := NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// scan
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.MetaDB, &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
}, ctlr.Log)
err = scanner.UpdateDB(context.Background())
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage(context.Background(), "zot-test:0.0.1")
So(err, ShouldBeNil)
So(cveMap, ShouldNotBeNil)
})
}
func TestScanningByDigest(t *testing.T) {
Convey("Scan the individual manifests inside an index", t, func() {
// start server
tempDir := t.TempDir()
port := GetFreePort()
baseURL := GetBaseURL(port)
conf := config.New()
conf.HTTP.Port = port
defaultVal := true
conf.Storage.RootDirectory = tempDir
conf.Extensions = &extconf.ExtensionConfig{
Search: &extconf.SearchConfig{
BaseConfig: extconf.BaseConfig{Enable: &defaultVal},
},
}
ctlr := api.NewController(conf)
So(ctlr, ShouldNotBeNil)
cm := NewControllerManager(ctlr)
cm.StartAndWait(port)
defer cm.StopServer()
// push index with 2 manifests: one with vulns and one without
vulnImage := CreateDefaultVulnerableImage()
simpleImage := CreateRandomImage()
multiArch := CreateMultiarchWith().Images([]Image{simpleImage, //nolint:staticcheck
vulnImage}).Build()
err := UploadMultiarchImage(multiArch, baseURL, "multi-arch", "multi-arch-tag")
So(err, ShouldBeNil)
// scan
scanner := trivy.NewScanner(ctlr.StoreController, ctlr.MetaDB, &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
}, ctlr.Log)
ctx := context.Background()
err = scanner.UpdateDB(ctx)
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage(ctx, "multi-arch@"+vulnImage.DigestStr())
So(err, ShouldBeNil)
t.Logf("cveMap=%v\n", cveMap)
So(cveMap, ShouldContainKey, Vulnerability1ID)
So(cveMap, ShouldContainKey, Vulnerability2ID)
So(cveMap, ShouldContainKey, Vulnerability3ID)
cveMap, err = scanner.ScanImage(ctx, "multi-arch@"+simpleImage.DigestStr())
So(err, ShouldBeNil)
So(cveMap, ShouldBeEmpty)
cveMap, err = scanner.ScanImage(ctx, "multi-arch@"+multiArch.DigestStr())
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, Vulnerability1ID)
So(cveMap, ShouldContainKey, Vulnerability2ID)
So(cveMap, ShouldContainKey, Vulnerability3ID)
cveMap, err = scanner.ScanImage(ctx, "multi-arch:multi-arch-tag")
So(err, ShouldBeNil)
So(cveMap, ShouldContainKey, Vulnerability1ID)
So(cveMap, ShouldContainKey, Vulnerability2ID)
So(cveMap, ShouldContainKey, Vulnerability3ID)
})
}
func TestVulnerableLayer(t *testing.T) {
Convey("Vulnerable layer", t, func() {
vulnerableLayer, err := GetLayerWithVulnerability()
So(err, ShouldBeNil)
created, err := time.Parse(time.RFC3339, "2023-03-29T18:19:24Z")
So(err, ShouldBeNil)
config := ispec.Image{
Created: &created,
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img := CreateImageWith().
LayerBlobs([][]byte{vulnerableLayer}).
ImageConfig(config).
Build()
tempDir := t.TempDir()
log := log.NewTestLogger()
imageStore := local.NewImageStore(tempDir, false, false,
log, monitoring.NewMetricsServer(false, log), nil, nil, nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
err = WriteImageToFileSystem(img, "repo", img.DigestStr(), storeController)
So(err, ShouldBeNil)
params := boltdb.DBParameters{
RootDir: tempDir,
}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
err = meta.ParseStorage(metaDB, storeController, log)
So(err, ShouldBeNil)
scanner := trivy.NewScanner(storeController, metaDB, &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
}, log)
err = scanner.UpdateDB(context.Background())
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage(context.Background(), "repo@"+img.DigestStr())
So(err, ShouldBeNil)
t.Logf("cveMap: %v", cveMap)
// As of September 17 2023 there are 5 CVEs:
// CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446
// There may be more discovered in the future
So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5)
So(cveMap, ShouldContainKey, "CVE-2023-1255")
So(cveMap, ShouldContainKey, "CVE-2023-2650")
So(cveMap, ShouldContainKey, "CVE-2023-2975")
So(cveMap, ShouldContainKey, "CVE-2023-3817")
So(cveMap, ShouldContainKey, "CVE-2023-3446")
})
Convey("Vulnerable layer with vulnerability in language-specific file", t, func() {
vulnerableLayer, err := GetLayerWithLanguageFileVulnerability()
So(err, ShouldBeNil)
created, err := time.Parse(time.RFC3339, "2024-02-15T09:56:01.500079786Z")
So(err, ShouldBeNil)
config := ispec.Image{
Created: &created,
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:d789b0723f3e6e5064d612eb3c84071cc84a7cf7921d549642252c3295e5f937"},
},
}
img := CreateImageWith().
LayerBlobs([][]byte{vulnerableLayer}).
ImageConfig(config).
Build()
tempDir := t.TempDir()
log := log.NewTestLogger()
imageStore := local.NewImageStore(tempDir, false, false,
log, monitoring.NewMetricsServer(false, log), nil, nil, nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
err = WriteImageToFileSystem(img, "repo", img.DigestStr(), storeController)
So(err, ShouldBeNil)
params := boltdb.DBParameters{
RootDir: tempDir,
}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
err = meta.ParseStorage(metaDB, storeController, log)
So(err, ShouldBeNil)
scanner := trivy.NewScanner(storeController, metaDB, &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
JavaDBRepository: "ghcr.io/project-zot/trivy-java-db",
},
}, log)
err = scanner.UpdateDB(context.Background())
So(err, ShouldBeNil)
cveMap, err := scanner.ScanImage(context.Background(), "repo@"+img.DigestStr())
So(err, ShouldBeNil)
t.Logf("cveMap: %v", cveMap)
// As of Feb 15 2024, there is 1 CVE in this layer:
So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 1)
So(cveMap, ShouldContainKey, "CVE-2016-1000027")
cveData := cveMap["CVE-2016-1000027"]
vulnerablePackages := cveData.PackageList
// There is only 1 vulnerable package in this layer
So(len(vulnerablePackages), ShouldEqual, 1)
vulnerableSpringWebPackage := vulnerablePackages[0]
So(vulnerableSpringWebPackage.Name, ShouldEqual, "org.springframework:spring-web")
So(vulnerableSpringWebPackage.InstalledVersion, ShouldEqual, "5.3.31")
So(vulnerableSpringWebPackage.FixedVersion, ShouldEqual, "6.0.0")
So(vulnerableSpringWebPackage.PackagePath, ShouldEqual, "usr/local/artifacts/spring-web-5.3.31.jar")
})
}
func TestWithTempDirErrorHandling(t *testing.T) {
Convey("Temp Dir error handling", t, func() {
vulnerableLayer, err := GetLayerWithVulnerability()
So(err, ShouldBeNil)
created, err := time.Parse(time.RFC3339, "2023-03-29T18:19:24Z")
So(err, ShouldBeNil)
config := ispec.Image{
Created: &created,
Platform: ispec.Platform{
Architecture: "amd64",
OS: "linux",
},
Config: ispec.ImageConfig{
Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"},
Cmd: []string{"/bin/sh"},
},
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
}
img := CreateImageWith().
LayerBlobs([][]byte{vulnerableLayer}).
ImageConfig(config).
Build()
tempDir := t.TempDir()
log := log.NewTestLogger()
imageStore := local.NewImageStore(tempDir, false, false,
log, monitoring.NewMetricsServer(false, log), nil, nil, nil, nil)
storeController := storage.StoreController{
DefaultStore: imageStore,
}
err = WriteImageToFileSystem(img, "repo", img.DigestStr(), storeController)
So(err, ShouldBeNil)
params := boltdb.DBParameters{
RootDir: tempDir,
}
boltDriver, err := boltdb.GetBoltDriver(params)
So(err, ShouldBeNil)
metaDB, err := boltdb.New(boltDriver, log)
So(err, ShouldBeNil)
err = meta.ParseStorage(metaDB, storeController, log)
So(err, ShouldBeNil)
scanner := trivy.NewScanner(storeController, metaDB, &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
}, log)
// Clean up any existing temp directory first
_ = xos.Cleanup()
// Get temp directory path (xos.TempDir() uses sync.OnceValues, so it won't recreate after cleanup)
tempDirPath := xos.TempDir()
// Manually create the directory since xos.Cleanup() removed it
err = os.MkdirAll(tempDirPath, 0o755)
So(err, ShouldBeNil)
// Change permissions to 0o000 to test error handling
err = os.Chmod(tempDirPath, 0o000)
So(err, ShouldBeNil)
defer func() {
// Clean up in case test fails
// Note: directory may have been cleaned up by withTempDir, so we just call xos.Cleanup()
_ = os.Chmod(tempDirPath, 0o755)
_ = xos.Cleanup()
}()
err = scanner.UpdateDB(context.Background())
So(err, ShouldNotBeNil)
// Verify temp directory no longer exists
_, err = os.Stat(tempDirPath)
So(err, ShouldNotBeNil)
err = scanner.UpdateDB(context.Background())
So(err, ShouldBeNil)
// Verify temp directory no longer exists
_, err = os.Stat(tempDirPath)
So(err, ShouldNotBeNil)
// Test scan with no permissions on temp directory
// Ensure directory exists again after cleanup (xos.Cleanup() removed it)
tempDirPath = xos.TempDir()
err = os.MkdirAll(tempDirPath, 0o755)
So(err, ShouldBeNil)
err = os.Chmod(tempDirPath, 0o000)
So(err, ShouldBeNil)
_, err = scanner.ScanImage(context.Background(), "repo@"+img.DigestStr())
So(err, ShouldNotBeNil)
// Verify temp directory no longer exists
_, err = os.Stat(tempDirPath)
So(err, ShouldNotBeNil)
cveMap, err := scanner.ScanImage(context.Background(), "repo@"+img.DigestStr())
So(err, ShouldBeNil)
t.Logf("cveMap: %v", cveMap)
// As of September 17 2023 there are 5 CVEs:
// CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446
// There may be more discovered in the future
So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5)
So(cveMap, ShouldContainKey, "CVE-2023-1255")
So(cveMap, ShouldContainKey, "CVE-2023-2650")
So(cveMap, ShouldContainKey, "CVE-2023-2975")
So(cveMap, ShouldContainKey, "CVE-2023-3817")
So(cveMap, ShouldContainKey, "CVE-2023-3446")
// Verify temp directory no longer exists
_, err = os.Stat(tempDirPath)
So(err, ShouldNotBeNil)
})
}
func TestScannerErrors(t *testing.T) {
Convey("Errors", t, func() {
storeController := storage.StoreController{}
metaDB := mocks.MetaDBMock{}
log := log.NewTestLogger()
testTrivyCVEConfig := func() *extconf.CVEConfig {
return &extconf.CVEConfig{
Trivy: &extconf.TrivyConfig{
DBRepository: "ghcr.io/project-zot/trivy-db",
},
}
}
Convey("IsImageFormatScannable", func() {
storeController.DefaultStore = mocks.MockedImageStore{}
metaDB.GetImageMetaFn = func(digest godigest.Digest) (types.ImageMeta, error) {
return types.ImageMeta{}, ErrTestError
}
scanner := trivy.NewScanner(storeController, metaDB, testTrivyCVEConfig(), log)
_, err := scanner.IsImageFormatScannable("repo", godigest.FromString("dig").String())
So(err, ShouldNotBeNil)
})
Convey("IsImageMediaScannable", func() {
storeController.DefaultStore = mocks.MockedImageStore{}
metaDB.GetImageMetaFn = func(digest godigest.Digest) (types.ImageMeta, error) {
return types.ImageMeta{}, ErrTestError
}
scanner := trivy.NewScanner(storeController, metaDB, testTrivyCVEConfig(), log)
Convey("Manifest", func() {
_, err := scanner.IsImageMediaScannable("repo", godigest.FromString("dig").String(), ispec.MediaTypeImageManifest)
So(err, ShouldNotBeNil)
})
Convey("Index", func() {
_, err := scanner.IsImageMediaScannable("repo", godigest.FromString("dig").String(), ispec.MediaTypeImageIndex)
So(err, ShouldNotBeNil)
})
Convey("Index with nil index", func() {
metaDB.GetImageMetaFn = func(digest godigest.Digest) (types.ImageMeta, error) {
return types.ImageMeta{}, nil
}
scanner := trivy.NewScanner(storeController, metaDB, testTrivyCVEConfig(), log)
_, err := scanner.IsImageMediaScannable("repo", godigest.FromString("dig").String(), ispec.MediaTypeImageIndex)
So(err, ShouldNotBeNil)
})
Convey("Index with good index", func() {
metaDB.GetImageMetaFn = func(digest godigest.Digest) (types.ImageMeta, error) {
return types.ImageMeta{
Index: &ispec.Index{
Manifests: []ispec.Descriptor{{MediaType: ispec.MediaTypeImageLayer}},
},
Manifests: []types.ManifestMeta{{Manifest: ispec.Manifest{
Layers: []ispec.Descriptor{{MediaType: ispec.MediaTypeImageLayer}},
}}},
}, nil
}
scanner := trivy.NewScanner(storeController, metaDB, testTrivyCVEConfig(), log)
_, err := scanner.IsImageMediaScannable("repo", godigest.FromString("dig").String(), ispec.MediaTypeImageIndex)
So(err, ShouldBeNil)
})
})
Convey("ScanImage", func() {
storeController.DefaultStore = mocks.MockedImageStore{}
metaDB.GetImageMetaFn = func(digest godigest.Digest) (types.ImageMeta, error) {
return types.ImageMeta{}, ErrTestError
}
scanner := trivy.NewScanner(storeController, metaDB, testTrivyCVEConfig(), log)
_, err := scanner.ScanImage(context.Background(), "image@"+godigest.FromString("digest").String())
So(err, ShouldNotBeNil)
})
})
}