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