zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/search/cve/scan_test.go (about) 1 //go:build search 2 // +build search 3 4 package cveinfo_test 5 6 import ( 7 "context" 8 "errors" 9 "io" 10 "os" 11 "testing" 12 "time" 13 14 regTypes "github.com/google/go-containerregistry/pkg/v1/types" 15 godigest "github.com/opencontainers/go-digest" 16 ispec "github.com/opencontainers/image-spec/specs-go/v1" 17 . "github.com/smartystreets/goconvey/convey" 18 19 zerr "zotregistry.dev/zot/errors" 20 "zotregistry.dev/zot/pkg/api/config" 21 zcommon "zotregistry.dev/zot/pkg/common" 22 "zotregistry.dev/zot/pkg/extensions/monitoring" 23 cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve" 24 cvecache "zotregistry.dev/zot/pkg/extensions/search/cve/cache" 25 cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model" 26 "zotregistry.dev/zot/pkg/log" 27 "zotregistry.dev/zot/pkg/meta" 28 "zotregistry.dev/zot/pkg/meta/boltdb" 29 mTypes "zotregistry.dev/zot/pkg/meta/types" 30 "zotregistry.dev/zot/pkg/scheduler" 31 "zotregistry.dev/zot/pkg/storage" 32 "zotregistry.dev/zot/pkg/storage/local" 33 test "zotregistry.dev/zot/pkg/test/common" 34 . "zotregistry.dev/zot/pkg/test/image-utils" 35 "zotregistry.dev/zot/pkg/test/mocks" 36 ) 37 38 var ( 39 ErrBadTest = errors.New("there is a bug in the test") 40 ErrFailedScan = errors.New("scan has failed intentionally") 41 ) 42 43 func TestScanGeneratorWithMockedData(t *testing.T) { //nolint: gocyclo 44 Convey("Test CVE scanning task scheduler with diverse mocked data", t, func() { 45 repo1 := "repo1" 46 repoIndex := "repoIndex" 47 48 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 49 logPath := logFile.Name() 50 So(err, ShouldBeNil) 51 52 defer os.Remove(logFile.Name()) // clean up 53 54 logger := log.NewLogger("debug", logPath) 55 writers := io.MultiWriter(os.Stdout, logFile) 56 logger.Logger = logger.Output(writers) 57 58 cfg := config.New() 59 cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3} 60 metrics := monitoring.NewMetricsServer(true, logger) 61 sch := scheduler.NewScheduler(cfg, metrics, logger) 62 63 params := boltdb.DBParameters{ 64 RootDir: t.TempDir(), 65 } 66 boltDriver, err := boltdb.GetBoltDriver(params) 67 So(err, ShouldBeNil) 68 69 metaDB, err := boltdb.New(boltDriver, log.NewLogger("debug", "")) 70 So(err, ShouldBeNil) 71 72 // Refactor Idea: We can use InitializeTestMetaDB 73 74 // Create metadb data for scannable image with vulnerabilities 75 image11 := CreateImageWith().DefaultLayers(). 76 ImageConfig(ispec.Image{Created: DateRef(2008, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 77 78 err = metaDB.SetRepoReference(context.Background(), "repo1", "0.1.0", image11.AsImageMeta()) 79 So(err, ShouldBeNil) 80 81 image12 := CreateImageWith().DefaultLayers(). 82 ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 83 84 err = metaDB.SetRepoReference(context.Background(), "repo1", "1.0.0", image12.AsImageMeta()) 85 So(err, ShouldBeNil) 86 87 image13 := CreateImageWith().DefaultLayers(). 88 ImageConfig(ispec.Image{Created: DateRef(2010, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 89 90 err = metaDB.SetRepoReference(context.Background(), "repo1", "1.1.0", image13.AsImageMeta()) 91 So(err, ShouldBeNil) 92 93 image14 := CreateImageWith().DefaultLayers(). 94 ImageConfig(ispec.Image{Created: DateRef(2011, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 95 96 err = metaDB.SetRepoReference(context.Background(), "repo1", "1.0.1", image14.AsImageMeta()) 97 So(err, ShouldBeNil) 98 99 // Create metadb data for scannable image with no vulnerabilities 100 image61 := CreateImageWith().DefaultLayers(). 101 ImageConfig(ispec.Image{Created: DateRef(2016, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 102 103 err = metaDB.SetRepoReference(context.Background(), "repo6", "1.0.0", image61.AsImageMeta()) 104 So(err, ShouldBeNil) 105 106 // Create metadb data for image not supporting scanning 107 image21 := CreateImageWith().Layers([]Layer{{ 108 MediaType: ispec.MediaTypeImageLayerNonDistributableGzip, //nolint:staticcheck 109 Blob: []byte{10, 10, 10}, 110 Digest: godigest.FromBytes([]byte{10, 10, 10}), 111 }}).ImageConfig(ispec.Image{Created: DateRef(2009, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 112 113 err = metaDB.SetRepoReference(context.Background(), "repo2", "1.0.0", image21.AsImageMeta()) 114 So(err, ShouldBeNil) 115 116 // Create metadb data for invalid images/negative tests 117 img := CreateRandomImage() 118 digest31 := img.Digest() 119 120 err = metaDB.SetRepoReference(context.Background(), "repo3", "invalid-manifest", img.AsImageMeta()) 121 So(err, ShouldBeNil) 122 123 image41 := CreateImageWith().DefaultLayers(). 124 CustomConfigBlob([]byte("invalid config blob"), ispec.MediaTypeImageConfig).Build() 125 126 err = metaDB.SetRepoReference(context.Background(), "repo4", "invalid-config", image41.AsImageMeta()) 127 So(err, ShouldBeNil) 128 129 image15 := CreateRandomMultiarch() 130 131 digest51 := image15.Digest() 132 err = metaDB.SetRepoReference(context.Background(), "repo5", "nonexitent-manifests-for-multiarch", 133 image15.AsImageMeta()) 134 So(err, ShouldBeNil) 135 136 // Create metadb data for scannable image which errors during scan 137 image71 := CreateImageWith().DefaultLayers(). 138 ImageConfig(ispec.Image{Created: DateRef(2000, 1, 1, 12, 0, 0, 0, time.UTC)}).Build() 139 140 err = metaDB.SetRepoReference(context.Background(), "repo7", "1.0.0", image71.AsImageMeta()) 141 So(err, ShouldBeNil) 142 143 // Create multiarch image with vulnerabilities 144 multiarchImage := CreateRandomMultiarch() 145 146 err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[0].DigestStr(), 147 multiarchImage.Images[0].AsImageMeta()) 148 So(err, ShouldBeNil) 149 err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[1].DigestStr(), 150 multiarchImage.Images[1].AsImageMeta()) 151 So(err, ShouldBeNil) 152 err = metaDB.SetRepoReference(context.Background(), repoIndex, multiarchImage.Images[2].DigestStr(), 153 multiarchImage.Images[2].AsImageMeta()) 154 So(err, ShouldBeNil) 155 156 err = metaDB.SetRepoReference(context.Background(), repoIndex, "tagIndex", multiarchImage.AsImageMeta()) 157 So(err, ShouldBeNil) 158 159 err = metaDB.SetRepoMeta("repo-with-bad-tag-digest", mTypes.RepoMeta{ 160 Name: "repo-with-bad-tag-digest", 161 Tags: map[mTypes.Tag]mTypes.Descriptor{ 162 "tag": {MediaType: ispec.MediaTypeImageManifest, Digest: godigest.FromString("1").String()}, 163 "tag-multi-arch": {MediaType: ispec.MediaTypeImageIndex, Digest: godigest.FromString("2").String()}, 164 }, 165 }) 166 So(err, ShouldBeNil) 167 168 // Keep a record of all the image references / digest pairings 169 // This is normally done in MetaDB, but we want to verify 170 // the whole flow, including MetaDB 171 imageMap := map[string]string{} 172 173 image11Digest := image11.ManifestDescriptor.Digest.String() 174 image11Name := "repo1:0.1.0" 175 imageMap[image11Name] = image11Digest 176 image12Digest := image12.ManifestDescriptor.Digest.String() 177 image12Name := "repo1:1.0.0" 178 imageMap[image12Name] = image12Digest 179 image13Digest := image13.ManifestDescriptor.Digest.String() 180 image13Name := "repo1:1.1.0" 181 imageMap[image13Name] = image13Digest 182 image14Digest := image14.ManifestDescriptor.Digest.String() 183 image14Name := "repo1:1.0.1" 184 imageMap[image14Name] = image14Digest 185 image21Digest := image21.ManifestDescriptor.Digest.String() 186 image21Name := "repo2:1.0.0" 187 imageMap[image21Name] = image21Digest 188 image31Name := "repo3:invalid-manifest" 189 imageMap[image31Name] = digest31.String() 190 image41Digest := image41.ManifestDescriptor.Digest.String() 191 image41Name := "repo4:invalid-config" 192 imageMap[image41Name] = image41Digest 193 image51Name := "repo5:nonexitent-manifest-for-multiarch" 194 imageMap[image51Name] = digest51.String() 195 image61Digest := image61.ManifestDescriptor.Digest.String() 196 image61Name := "repo6:1.0.0" 197 imageMap[image61Name] = image61Digest 198 image71Digest := image71.ManifestDescriptor.Digest.String() 199 image71Name := "repo7:1.0.0" 200 imageMap[image71Name] = image71Digest 201 indexDigest := multiarchImage.IndexDescriptor.Digest.String() 202 indexName := "repoIndex:tagIndex" 203 imageMap[indexName] = indexDigest 204 indexM1Digest := multiarchImage.Images[0].ManifestDescriptor.Digest.String() 205 indexM1Name := "repoIndex@" + indexM1Digest 206 imageMap[indexM1Name] = indexM1Digest 207 indexM2Digest := multiarchImage.Images[1].ManifestDescriptor.Digest.String() 208 indexM2Name := "repoIndex@" + indexM2Digest 209 imageMap[indexM2Name] = indexM2Digest 210 indexM3Digest := multiarchImage.Images[2].ManifestDescriptor.Digest.String() 211 indexM3Name := "repoIndex@" + indexM3Digest 212 imageMap[indexM3Name] = indexM3Digest 213 214 // Initialize a test CVE cache 215 cache := cvecache.NewCveCache(20, logger) 216 217 // MetaDB loaded with initial data, now mock the scanner 218 // Setup test CVE data in mock scanner 219 scanner := mocks.CveScannerMock{ 220 ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { 221 result := cache.Get(image) 222 // Will not match sending the repo:tag as a parameter, but we don't care 223 if result != nil { 224 return result, nil 225 } 226 227 repo, ref, isTag := zcommon.GetImageDirAndReference(image) 228 if isTag { 229 foundRef, ok := imageMap[image] 230 if !ok { 231 return nil, ErrBadTest 232 } 233 ref = foundRef 234 } 235 236 // Images in chronological order 237 if repo == repo1 && ref == image11Digest { 238 result := map[string]cvemodel.CVE{ 239 "CVE1": { 240 ID: "CVE1", 241 Severity: "MEDIUM", 242 Title: "Title CVE1", 243 Description: "Description CVE1", 244 }, 245 } 246 247 cache.Add(ref, result) 248 249 return result, nil 250 } 251 252 if repo == repo1 && zcommon.Contains([]string{image12Digest, image21Digest}, ref) { 253 result := map[string]cvemodel.CVE{ 254 "CVE1": { 255 ID: "CVE1", 256 Severity: "MEDIUM", 257 Title: "Title CVE1", 258 Description: "Description CVE1", 259 }, 260 "CVE2": { 261 ID: "CVE2", 262 Severity: "HIGH", 263 Title: "Title CVE2", 264 Description: "Description CVE2", 265 }, 266 "CVE3": { 267 ID: "CVE3", 268 Severity: "LOW", 269 Title: "Title CVE3", 270 Description: "Description CVE3", 271 }, 272 } 273 274 cache.Add(ref, result) 275 276 return result, nil 277 } 278 279 if repo == repo1 && ref == image13Digest { 280 result := map[string]cvemodel.CVE{ 281 "CVE3": { 282 ID: "CVE3", 283 Severity: "LOW", 284 Title: "Title CVE3", 285 Description: "Description CVE3", 286 }, 287 } 288 289 cache.Add(ref, result) 290 291 return result, nil 292 } 293 294 // As a minor release on 1.0.0 banch 295 // does not include all fixes published in 1.1.0 296 if repo == repo1 && ref == image14Digest { 297 result := map[string]cvemodel.CVE{ 298 "CVE1": { 299 ID: "CVE1", 300 Severity: "MEDIUM", 301 Title: "Title CVE1", 302 Description: "Description CVE1", 303 }, 304 "CVE3": { 305 ID: "CVE3", 306 Severity: "LOW", 307 Title: "Title CVE3", 308 Description: "Description CVE3", 309 }, 310 } 311 312 cache.Add(ref, result) 313 314 return result, nil 315 } 316 317 // Unexpected error while scanning 318 if repo == "repo7" { 319 return map[string]cvemodel.CVE{}, ErrFailedScan 320 } 321 322 if (repo == repoIndex && ref == indexDigest) || 323 (repo == repoIndex && ref == indexM1Digest) { 324 result := map[string]cvemodel.CVE{ 325 "CVE1": { 326 ID: "CVE1", 327 Severity: "MEDIUM", 328 Title: "Title CVE1", 329 Description: "Description CVE1", 330 }, 331 } 332 333 // Simulate scanning an index results in scanning its manifests 334 if ref == indexDigest { 335 cache.Add(indexM1Digest, result) 336 cache.Add(indexM2Digest, map[string]cvemodel.CVE{}) 337 cache.Add(indexM3Digest, map[string]cvemodel.CVE{}) 338 } 339 340 cache.Add(ref, result) 341 342 return result, nil 343 } 344 345 // By default the image has no vulnerabilities 346 result = map[string]cvemodel.CVE{} 347 cache.Add(ref, result) 348 349 return result, nil 350 }, 351 IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { 352 if repo == repoIndex { 353 return true, nil 354 } 355 356 // Almost same logic compared to actual Trivy specific implementation 357 imageDir, inputTag := repo, reference 358 359 repoMeta, err := metaDB.GetRepoMeta(context.Background(), imageDir) 360 if err != nil { 361 return false, err 362 } 363 364 manifestDigestStr := reference 365 366 if zcommon.IsTag(reference) { 367 var ok bool 368 369 descriptor, ok := repoMeta.Tags[inputTag] 370 if !ok { 371 return false, zerr.ErrTagMetaNotFound 372 } 373 374 manifestDigestStr = descriptor.Digest 375 } 376 377 manifestDigest, err := godigest.Parse(manifestDigestStr) 378 if err != nil { 379 return false, err 380 } 381 382 manifestData, err := metaDB.GetImageMeta(manifestDigest) 383 if err != nil { 384 return false, err 385 } 386 387 for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers { 388 switch imageLayer.MediaType { 389 case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): 390 391 return true, nil 392 default: 393 394 return false, zerr.ErrScanNotSupported 395 } 396 } 397 398 return false, nil 399 }, 400 IsImageMediaScannableFn: func(repo, digest, mediaType string) (bool, error) { 401 if repo == "repo2" { 402 if digest == image21Digest { 403 return false, nil 404 } 405 } 406 407 return true, nil 408 }, 409 IsResultCachedFn: func(digest string) bool { 410 return cache.Contains(digest) 411 }, 412 UpdateDBFn: func(ctx context.Context) error { 413 cache.Purge() 414 415 return nil 416 }, 417 } 418 419 // Purge scan, it should not be needed 420 So(scanner.UpdateDB(context.Background()), ShouldBeNil) 421 422 // Verify none of the entries are cached to begin with 423 t.Log("verify cache is initially empty") 424 425 for image, digestStr := range imageMap { 426 t.Log("expecting " + image + " " + digestStr + " to be absent from cache") 427 So(scanner.IsResultCached(digestStr), ShouldBeFalse) 428 } 429 430 // Start the generator 431 generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger) 432 433 sch.SubmitGenerator(generator, 10*time.Second, scheduler.MediumPriority) 434 435 sch.RunScheduler() 436 437 defer sch.Shutdown() 438 439 // Make sure the scanner generator has completed despite errors 440 found, err := test.ReadLogFileAndSearchString(logPath, 441 "finished scanning available images during scheduled cve scan", 40*time.Second) 442 So(err, ShouldBeNil) 443 So(found, ShouldBeTrue) 444 445 t.Log("verify cache is up to date after scanner generator ran") 446 447 // Verify all of the entries are cached 448 for image, digestStr := range imageMap { 449 repo, _, _ := zcommon.GetImageDirAndReference(image) 450 451 ok, err := scanner.IsImageFormatScannable(repo, digestStr) 452 if ok && err == nil && repo != "repo7" { 453 t.Log("expecting " + image + " " + digestStr + " to be present in cache") 454 So(scanner.IsResultCached(digestStr), ShouldBeTrue) 455 } else { 456 // We don't cache results for un-scannable manifests 457 t.Log("expecting " + image + " " + digestStr + " to be absent from cache") 458 So(scanner.IsResultCached(digestStr), ShouldBeFalse) 459 } 460 } 461 462 found, err = test.ReadLogFileAndSearchString(logPath, 463 "failed to obtain repo metadata during scheduled cve scan", 20*time.Second) 464 So(err, ShouldBeNil) 465 So(found, ShouldBeTrue) 466 467 // Make sure the scanner generator is catching the scanning error for repo7 468 found, err = test.ReadLogFileAndSearchString(logPath, 469 "failed to perform scheduled cve scan for image", 20*time.Second) 470 So(err, ShouldBeNil) 471 So(found, ShouldBeTrue) 472 473 // Make sure the scanner generator is triggered at least twice 474 found, err = test.ReadLogFileAndCountStringOccurence(logPath, 475 "finished scanning available images during scheduled cve scan", 30*time.Second, 2) 476 So(err, ShouldBeNil) 477 So(found, ShouldBeTrue) 478 }) 479 } 480 481 func TestScanGeneratorWithRealData(t *testing.T) { 482 Convey("Test CVE scanning task scheduler real data", t, func() { 483 rootDir := t.TempDir() 484 485 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 486 logPath := logFile.Name() 487 So(err, ShouldBeNil) 488 489 defer os.Remove(logFile.Name()) // clean up 490 491 logger := log.NewLogger("debug", logPath) 492 writers := io.MultiWriter(os.Stdout, logFile) 493 logger.Logger = logger.Output(writers) 494 495 cfg := config.New() 496 cfg.Scheduler = &config.SchedulerConfig{NumWorkers: 3} 497 498 boltDriver, err := boltdb.GetBoltDriver(boltdb.DBParameters{RootDir: rootDir}) 499 So(err, ShouldBeNil) 500 501 metaDB, err := boltdb.New(boltDriver, logger) 502 So(err, ShouldBeNil) 503 504 metrics := monitoring.NewMetricsServer(true, logger) 505 imageStore := local.NewImageStore(rootDir, false, false, 506 logger, metrics, nil, nil) 507 storeController := storage.StoreController{DefaultStore: imageStore} 508 509 image := CreateRandomVulnerableImage() 510 511 err = WriteImageToFileSystem(image, "zot-test", "0.0.1", storeController) 512 So(err, ShouldBeNil) 513 514 err = meta.ParseStorage(metaDB, storeController, logger) 515 So(err, ShouldBeNil) 516 517 scanner := cveinfo.NewScanner(storeController, metaDB, "ghcr.io/project-zot/trivy-db", "", logger) 518 err = scanner.UpdateDB(context.Background()) 519 So(err, ShouldBeNil) 520 521 So(scanner.IsResultCached(image.DigestStr()), ShouldBeFalse) 522 523 sch := scheduler.NewScheduler(cfg, metrics, logger) 524 525 generator := cveinfo.NewScanTaskGenerator(metaDB, scanner, logger) 526 527 // Start the generator 528 sch.SubmitGenerator(generator, 120*time.Second, scheduler.MediumPriority) 529 530 sch.RunScheduler() 531 532 defer sch.Shutdown() 533 534 // Make sure the scanner generator has completed 535 found, err := test.ReadLogFileAndSearchString(logPath, 536 "finished scanning available images during scheduled cve scan", 120*time.Second) 537 So(err, ShouldBeNil) 538 So(found, ShouldBeTrue) 539 540 found, err = test.ReadLogFileAndSearchString(logPath, 541 image.ManifestDescriptor.Digest.String(), 120*time.Second) 542 So(err, ShouldBeNil) 543 So(found, ShouldBeTrue) 544 545 found, err = test.ReadLogFileAndSearchString(logPath, 546 "scheduled cve scan completed successfully for image", 120*time.Second) 547 So(err, ShouldBeNil) 548 So(found, ShouldBeTrue) 549 550 So(scanner.IsResultCached(image.DigestStr()), ShouldBeTrue) 551 552 cveMap, err := scanner.ScanImage(context.Background(), "zot-test:0.0.1") 553 So(err, ShouldBeNil) 554 t.Logf("cveMap: %v", cveMap) 555 // As of September 22 2023 there are 5 CVEs: 556 // CVE-2023-1255, CVE-2023-2650, CVE-2023-2975, CVE-2023-3817, CVE-2023-3446 557 // There may be more discovered in the future 558 So(len(cveMap), ShouldBeGreaterThanOrEqualTo, 5) 559 So(cveMap, ShouldContainKey, "CVE-2023-1255") 560 So(cveMap, ShouldContainKey, "CVE-2023-2650") 561 So(cveMap, ShouldContainKey, "CVE-2023-2975") 562 So(cveMap, ShouldContainKey, "CVE-2023-3817") 563 So(cveMap, ShouldContainKey, "CVE-2023-3446") 564 565 cveInfo := cveinfo.NewCVEInfo(scanner, metaDB, logger) 566 567 // Based on cache population only, no extra scanning 568 cveSummary, err := cveInfo.GetCVESummaryForImageMedia(context.Background(), "zot-test", image.DigestStr(), 569 image.ManifestDescriptor.MediaType) 570 So(err, ShouldBeNil) 571 So(cveSummary.Count, ShouldBeGreaterThanOrEqualTo, 5) 572 // As of September 22 the max severity is MEDIUM, but new CVEs could appear in the future 573 So([]string{"MEDIUM", "HIGH", "CRITICAL"}, ShouldContain, cveSummary.MaxSeverity) 574 }) 575 }