zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/cli/client/cve_cmd_test.go (about) 1 //go:build search 2 // +build search 3 4 package client_test 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "os" 14 "path" 15 "regexp" 16 "strings" 17 "testing" 18 "time" 19 20 regTypes "github.com/google/go-containerregistry/pkg/v1/types" 21 godigest "github.com/opencontainers/go-digest" 22 ispec "github.com/opencontainers/image-spec/specs-go/v1" 23 . "github.com/smartystreets/goconvey/convey" 24 25 zerr "zotregistry.dev/zot/errors" 26 "zotregistry.dev/zot/pkg/api" 27 "zotregistry.dev/zot/pkg/api/config" 28 "zotregistry.dev/zot/pkg/cli/client" 29 zcommon "zotregistry.dev/zot/pkg/common" 30 extconf "zotregistry.dev/zot/pkg/extensions/config" 31 "zotregistry.dev/zot/pkg/extensions/monitoring" 32 cveinfo "zotregistry.dev/zot/pkg/extensions/search/cve" 33 cvemodel "zotregistry.dev/zot/pkg/extensions/search/cve/model" 34 "zotregistry.dev/zot/pkg/log" 35 mTypes "zotregistry.dev/zot/pkg/meta/types" 36 "zotregistry.dev/zot/pkg/storage" 37 "zotregistry.dev/zot/pkg/storage/local" 38 test "zotregistry.dev/zot/pkg/test/common" 39 . "zotregistry.dev/zot/pkg/test/image-utils" 40 "zotregistry.dev/zot/pkg/test/mocks" 41 ociutils "zotregistry.dev/zot/pkg/test/oci-utils" 42 ) 43 44 func TestNegativeServerResponse(t *testing.T) { 45 Convey("Test from real server without search endpoint", t, func() { 46 port := test.GetFreePort() 47 url := test.GetBaseURL(port) 48 conf := config.New() 49 conf.HTTP.Port = port 50 51 dir := t.TempDir() 52 53 srcStorageCtlr := ociutils.GetDefaultStoreController(dir, log.NewLogger("debug", "")) 54 err := WriteImageToFileSystem(CreateDefaultVulnerableImage(), "zot-cve-test", "0.0.1", srcStorageCtlr) 55 So(err, ShouldBeNil) 56 57 conf.Storage.RootDirectory = dir 58 trivyConfig := &extconf.TrivyConfig{ 59 DBRepository: "ghcr.io/project-zot/trivy-db", 60 } 61 cveConfig := &extconf.CVEConfig{ 62 UpdateInterval: 2, 63 Trivy: trivyConfig, 64 } 65 defaultVal := false 66 searchConfig := &extconf.SearchConfig{ 67 BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, 68 CVE: cveConfig, 69 } 70 conf.Extensions = &extconf.ExtensionConfig{ 71 Search: searchConfig, 72 } 73 74 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 75 if err != nil { 76 panic(err) 77 } 78 79 logPath := logFile.Name() 80 defer os.Remove(logPath) 81 82 writers := io.MultiWriter(os.Stdout, logFile) 83 84 ctlr := api.NewController(conf) 85 ctlr.Log.Logger = ctlr.Log.Output(writers) 86 87 cm := test.NewControllerManager(ctlr) 88 cm.StartAndWait(conf.HTTP.Port) 89 defer cm.StopServer() 90 91 _, err = test.ReadLogFileAndSearchString(logPath, "CVE config not provided, skipping CVE update", 90*time.Second) 92 if err != nil { 93 panic(err) 94 } 95 96 Convey("Status Code Not Found", func() { 97 args := []string{"list", "zot-cve-test:0.0.1", "--config", "cvetest"} 98 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 99 defer os.Remove(configPath) 100 cveCmd := client.NewCVECommand(client.NewSearchService()) 101 buff := bytes.NewBufferString("") 102 cveCmd.SetOut(buff) 103 cveCmd.SetErr(buff) 104 cveCmd.SetArgs(args) 105 err = cveCmd.Execute() 106 So(err, ShouldNotBeNil) 107 So(err.Error(), ShouldContainSubstring, zerr.ErrExtensionNotEnabled.Error()) 108 }) 109 }) 110 111 Convey("Test non-existing manifest blob", t, func() { 112 port := test.GetFreePort() 113 url := test.GetBaseURL(port) 114 conf := config.New() 115 conf.HTTP.Port = port 116 117 dir := t.TempDir() 118 119 imageStore := local.NewImageStore(dir, false, false, 120 log.NewLogger("debug", ""), monitoring.NewMetricsServer(false, log.NewLogger("debug", "")), nil, nil) 121 122 storeController := storage.StoreController{ 123 DefaultStore: imageStore, 124 } 125 126 image := CreateRandomImage() 127 128 err := WriteImageToFileSystem(image, "zot-cve-test", "0.0.1", storeController) 129 So(err, ShouldBeNil) 130 131 err = os.RemoveAll(path.Join(dir, "zot-cve-test/blobs")) 132 if err != nil { 133 panic(err) 134 } 135 136 conf.Storage.RootDirectory = dir 137 trivyConfig := &extconf.TrivyConfig{ 138 DBRepository: "ghcr.io/project-zot/trivy-db", 139 } 140 cveConfig := &extconf.CVEConfig{ 141 UpdateInterval: 2, 142 Trivy: trivyConfig, 143 } 144 defaultVal := true 145 searchConfig := &extconf.SearchConfig{ 146 BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, 147 CVE: cveConfig, 148 } 149 conf.Extensions = &extconf.ExtensionConfig{ 150 Search: searchConfig, 151 } 152 153 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 154 if err != nil { 155 panic(err) 156 } 157 158 logPath := logFile.Name() 159 defer os.Remove(logPath) 160 161 writers := io.MultiWriter(os.Stdout, logFile) 162 163 ctlr := api.NewController(conf) 164 ctlr.Log.Logger = ctlr.Log.Output(writers) 165 166 if err := ctlr.Init(); err != nil { 167 panic(err) 168 } 169 170 ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB) 171 172 go func() { 173 if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { 174 panic(err) 175 } 176 }() 177 178 defer ctlr.Shutdown() 179 180 test.WaitTillServerReady(url) 181 182 _, err = test.ReadLogFileAndSearchString(logPath, "cve-db update completed, next update scheduled after interval", 183 90*time.Second) 184 if err != nil { 185 panic(err) 186 } 187 188 args := []string{"fixed", "zot-cve-test", "CVE-2019-9923", "--config", "cvetest"} 189 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 190 defer os.Remove(configPath) 191 cveCmd := client.NewCVECommand(client.NewSearchService()) 192 buff := bytes.NewBufferString("") 193 cveCmd.SetOut(buff) 194 cveCmd.SetErr(buff) 195 cveCmd.SetArgs(args) 196 err = cveCmd.Execute() 197 So(err, ShouldNotBeNil) 198 }) 199 } 200 201 func TestCVEDiffList(t *testing.T) { 202 port := test.GetFreePort() 203 url := test.GetBaseURL(port) 204 conf := config.New() 205 conf.HTTP.Port = port 206 207 dir := t.TempDir() 208 209 conf.Storage.RootDirectory = dir 210 trivyConfig := &extconf.TrivyConfig{ 211 DBRepository: "ghcr.io/project-zot/trivy-db", 212 } 213 cveConfig := &extconf.CVEConfig{ 214 UpdateInterval: 2, 215 Trivy: trivyConfig, 216 } 217 defaultVal := true 218 searchConfig := &extconf.SearchConfig{ 219 BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, 220 CVE: cveConfig, 221 } 222 conf.Extensions = &extconf.ExtensionConfig{ 223 Search: searchConfig, 224 } 225 226 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 227 if err != nil { 228 panic(err) 229 } 230 231 logPath := logFile.Name() 232 defer os.Remove(logPath) 233 234 writers := io.MultiWriter(os.Stdout, logFile) 235 236 ctlr := api.NewController(conf) 237 ctlr.Log.Logger = ctlr.Log.Output(writers) 238 239 if err := ctlr.Init(); err != nil { 240 panic(err) 241 } 242 243 layer1 := []byte{10, 20, 30} 244 layer2 := []byte{11, 21, 31} 245 layer3 := []byte{12, 22, 23} 246 247 otherImage := CreateImageWith().LayerBlobs([][]byte{ 248 layer1, 249 }).DefaultConfig().Build() 250 251 baseImage := CreateImageWith().LayerBlobs([][]byte{ 252 layer1, 253 layer2, 254 }).PlatformConfig("testArch", "testOs").Build() 255 256 image := CreateImageWith().LayerBlobs([][]byte{ 257 layer1, 258 layer2, 259 layer3, 260 }).PlatformConfig("testArch", "testOs").Build() 261 262 multiArchBase := CreateMultiarchWith().Images([]Image{baseImage, CreateRandomImage(), CreateRandomImage()}). 263 Build() 264 multiArchImage := CreateMultiarchWith().Images([]Image{image, CreateRandomImage(), CreateRandomImage()}). 265 Build() 266 267 getCveResults := func(digestStr string) map[string]cvemodel.CVE { 268 switch digestStr { 269 case image.DigestStr(): 270 return map[string]cvemodel.CVE{ 271 "CVE1": { 272 ID: "CVE1", 273 Severity: "HIGH", 274 Title: "Title CVE1", 275 Description: "Description CVE1", 276 PackageList: []cvemodel.Package{{}}, 277 }, 278 "CVE2": { 279 ID: "CVE2", 280 Severity: "MEDIUM", 281 Title: "Title CVE2", 282 Description: "Description CVE2", 283 PackageList: []cvemodel.Package{{}}, 284 }, 285 "CVE3": { 286 ID: "CVE3", 287 Severity: "LOW", 288 Title: "Title CVE3", 289 Description: "Description CVE3", 290 PackageList: []cvemodel.Package{{}}, 291 }, 292 } 293 case baseImage.DigestStr(): 294 return map[string]cvemodel.CVE{ 295 "CVE1": { 296 ID: "CVE1", 297 Severity: "HIGH", 298 Title: "Title CVE1", 299 Description: "Description CVE1", 300 PackageList: []cvemodel.Package{{}}, 301 }, 302 "CVE2": { 303 ID: "CVE2", 304 Severity: "MEDIUM", 305 Title: "Title CVE2", 306 Description: "Description CVE2", 307 PackageList: []cvemodel.Package{{}}, 308 }, 309 } 310 case otherImage.DigestStr(): 311 return map[string]cvemodel.CVE{ 312 "CVE1": { 313 ID: "CVE1", 314 Severity: "HIGH", 315 Title: "Title CVE1", 316 Description: "Description CVE1", 317 PackageList: []cvemodel.Package{{}}, 318 }, 319 } 320 } 321 322 // By default the image has no vulnerabilities 323 return map[string]cvemodel.CVE{} 324 } 325 326 // MetaDB loaded with initial data, now mock the scanner 327 // Setup test CVE data in mock scanner 328 scanner := mocks.CveScannerMock{ 329 ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { 330 repo, ref, _, _ := zcommon.GetRepoReference(image) 331 332 if zcommon.IsDigest(ref) { 333 return getCveResults(ref), nil 334 } 335 336 repoMeta, _ := ctlr.MetaDB.GetRepoMeta(ctx, repo) 337 338 if _, ok := repoMeta.Tags[ref]; !ok { 339 panic("unexpected tag '" + ref + "', test might be wrong") 340 } 341 342 return getCveResults(repoMeta.Tags[ref].Digest), nil 343 }, 344 GetCachedResultFn: func(digestStr string) map[string]cvemodel.CVE { 345 return getCveResults(digestStr) 346 }, 347 IsResultCachedFn: func(digestStr string) bool { 348 return true 349 }, 350 } 351 352 ctlr.CveScanner = scanner 353 354 go func() { 355 if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { 356 panic(err) 357 } 358 }() 359 360 defer ctlr.Shutdown() 361 362 test.WaitTillServerReady(url) 363 364 ctx := context.Background() 365 _, err = ociutils.InitializeTestMetaDB(ctx, ctlr.MetaDB, 366 ociutils.Repo{ 367 Name: "repo", 368 Images: []ociutils.RepoImage{ 369 {Image: otherImage, Reference: "other-image"}, 370 {Image: baseImage, Reference: "base-image"}, 371 {Image: image, Reference: "image"}, 372 }, 373 }, 374 ociutils.Repo{ 375 Name: "repo-multi", 376 MultiArchImages: []ociutils.RepoMultiArchImage{ 377 {MultiarchImage: CreateRandomMultiarch(), Reference: "multi-rand"}, 378 {MultiarchImage: multiArchBase, Reference: "multi-base"}, 379 {MultiarchImage: multiArchImage, Reference: "multi-img"}, 380 }, 381 }, 382 ) 383 384 Convey("Test CVE by image name - GQL - positive", t, func() { 385 args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"} 386 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 387 defer os.Remove(configPath) 388 cveCmd := client.NewCVECommand(client.NewSearchService()) 389 buff := bytes.NewBufferString("") 390 cveCmd.SetOut(buff) 391 cveCmd.SetErr(buff) 392 cveCmd.SetArgs(args) 393 err = cveCmd.Execute() 394 fmt.Println(buff.String()) 395 space := regexp.MustCompile(`\s+`) 396 str := space.ReplaceAllString(buff.String(), " ") 397 str = strings.TrimSpace(str) 398 So(str, ShouldContainSubstring, "CVE3") 399 So(str, ShouldNotContainSubstring, "CVE1") 400 So(str, ShouldNotContainSubstring, "CVE2") 401 }) 402 403 Convey("Errors", t, func() { 404 // args := []string{"diff", "repo:image", "repo:base-image", "--config", "cvetest"} 405 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 406 defer os.Remove(configPath) 407 cveCmd := client.NewCVECommand(client.NewSearchService()) 408 409 Convey("Set wrong number of params", func() { 410 args := []string{"diff", "repo:image", "--config", "cvetest"} 411 cveCmd.SetArgs(args) 412 So(cveCmd.Execute(), ShouldNotBeNil) 413 }) 414 Convey("First input is not a repo:tag", func() { 415 args := []string{"diff", "bad-input", "repo:base-image", "--config", "cvetest"} 416 cveCmd.SetArgs(args) 417 So(cveCmd.Execute(), ShouldNotBeNil) 418 }) 419 Convey("Second input is arch but not enough args", func() { 420 args := []string{"diff", "repo:base-image", "linux/amd64", "--config", "cvetest"} 421 cveCmd.SetArgs(args) 422 So(cveCmd.Execute(), ShouldNotBeNil) 423 }) 424 Convey("Second input is arch 3rd is repo:tag", func() { 425 args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "--config", "cvetest"} 426 cveCmd.SetArgs(args) 427 So(cveCmd.Execute(), ShouldBeNil) 428 }) 429 Convey("Second input is repo:tag 3rd is repo:tag", func() { 430 args := []string{"diff", "repo:base-image", "repo:base-image", "repo:base-image", "--config", "cvetest"} 431 cveCmd.SetArgs(args) 432 So(cveCmd.Execute(), ShouldNotBeNil) 433 }) 434 Convey("Second input is arch 3rd is arch as well", func() { 435 args := []string{"diff", "repo:base-image", "linux/amd64", "linux/amd64", "--config", "cvetest"} 436 cveCmd.SetArgs(args) 437 So(cveCmd.Execute(), ShouldNotBeNil) 438 }) 439 Convey("Second input is repo:tag 3rd is arch", func() { 440 args := []string{"diff", "repo:base-image", "repo:base-image", "linux/amd64", "--config", "cvetest"} 441 cveCmd.SetArgs(args) 442 So(cveCmd.Execute(), ShouldBeNil) 443 }) 444 Convey("Second input is repo:tag 3rd is arch, 4th is repo:tag", func() { 445 args := []string{ 446 "diff", "repo:base-image", "repo:base-image", "linux/amd64", "repo:base-image", 447 "--config", "cvetest", 448 } 449 450 cveCmd.SetArgs(args) 451 So(cveCmd.Execute(), ShouldNotBeNil) 452 }) 453 Convey("Second input is arch 3rd is repo:tag, 4th is arch", func() { 454 args := []string{"diff", "repo:base-image", "linux/amd64", "repo:base-image", "linux/amd64", "--config", "cvetest"} 455 cveCmd.SetArgs(args) 456 So(cveCmd.Execute(), ShouldBeNil) 457 }) 458 Convey("input is with digest ref", func() { 459 args := []string{"diff", "repo@sha256:123123", "--config", "cvetest"} 460 cveCmd.SetArgs(args) 461 So(cveCmd.Execute(), ShouldNotBeNil) 462 }) 463 Convey("input is with just repo no ref", func() { 464 args := []string{"diff", "repo", "--config", "cvetest"} 465 cveCmd.SetArgs(args) 466 So(cveCmd.Execute(), ShouldNotBeNil) 467 }) 468 }) 469 } 470 471 //nolint:dupl 472 func TestServerCVEResponse(t *testing.T) { 473 port := test.GetFreePort() 474 url := test.GetBaseURL(port) 475 conf := config.New() 476 conf.HTTP.Port = port 477 478 dir := t.TempDir() 479 480 conf.Storage.RootDirectory = dir 481 trivyConfig := &extconf.TrivyConfig{ 482 DBRepository: "ghcr.io/project-zot/trivy-db", 483 } 484 cveConfig := &extconf.CVEConfig{ 485 UpdateInterval: 2, 486 Trivy: trivyConfig, 487 } 488 defaultVal := true 489 searchConfig := &extconf.SearchConfig{ 490 BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, 491 CVE: cveConfig, 492 } 493 conf.Extensions = &extconf.ExtensionConfig{ 494 Search: searchConfig, 495 } 496 497 logFile, err := os.CreateTemp(t.TempDir(), "zot-log*.txt") 498 if err != nil { 499 panic(err) 500 } 501 502 logPath := logFile.Name() 503 defer os.Remove(logPath) 504 505 writers := io.MultiWriter(os.Stdout, logFile) 506 507 ctlr := api.NewController(conf) 508 ctlr.Log.Logger = ctlr.Log.Output(writers) 509 510 if err := ctlr.Init(); err != nil { 511 panic(err) 512 } 513 514 ctlr.CveScanner = getMockCveScanner(ctlr.MetaDB) 515 516 go func() { 517 if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { 518 panic(err) 519 } 520 }() 521 522 defer ctlr.Shutdown() 523 524 test.WaitTillServerReady(url) 525 526 image := CreateDefaultImage() 527 528 err = UploadImage(image, url, "zot-cve-test", "0.0.1") 529 if err != nil { 530 panic(err) 531 } 532 533 _, err = test.ReadLogFileAndSearchString(logPath, "cve-db update completed, next update scheduled after interval", 534 90*time.Second) 535 if err != nil { 536 panic(err) 537 } 538 539 Convey("Test CVE by image name - GQL - positive", t, func() { 540 args := []string{"list", "zot-cve-test:0.0.1", "--config", "cvetest"} 541 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 542 defer os.Remove(configPath) 543 cveCmd := client.NewCVECommand(client.NewSearchService()) 544 buff := bytes.NewBufferString("") 545 cveCmd.SetOut(buff) 546 cveCmd.SetErr(buff) 547 cveCmd.SetArgs(args) 548 err = cveCmd.Execute() 549 space := regexp.MustCompile(`\s+`) 550 str := space.ReplaceAllString(buff.String(), " ") 551 str = strings.TrimSpace(str) 552 So(err, ShouldBeNil) 553 So(str, ShouldContainSubstring, "ID SEVERITY TITLE") 554 So(str, ShouldContainSubstring, "CVE") 555 }) 556 557 Convey("Test CVE by image name - GQL - search CVE by title in results", t, func() { 558 args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-C1", "--config", "cvetest"} 559 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 560 defer os.Remove(configPath) 561 cveCmd := client.NewCVECommand(client.NewSearchService()) 562 buff := bytes.NewBufferString("") 563 cveCmd.SetOut(buff) 564 cveCmd.SetErr(buff) 565 cveCmd.SetArgs(args) 566 err = cveCmd.Execute() 567 space := regexp.MustCompile(`\s+`) 568 str := space.ReplaceAllString(buff.String(), " ") 569 str = strings.TrimSpace(str) 570 So(err, ShouldBeNil) 571 So(str, ShouldContainSubstring, "ID SEVERITY TITLE") 572 So(str, ShouldContainSubstring, "CVE-C1") 573 So(str, ShouldNotContainSubstring, "CVE-2") 574 }) 575 576 Convey("Test CVE by image name - GQL - search CVE by id in results", t, func() { 577 args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-2", "--config", "cvetest"} 578 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 579 defer os.Remove(configPath) 580 cveCmd := client.NewCVECommand(client.NewSearchService()) 581 buff := bytes.NewBufferString("") 582 cveCmd.SetOut(buff) 583 cveCmd.SetErr(buff) 584 cveCmd.SetArgs(args) 585 err = cveCmd.Execute() 586 space := regexp.MustCompile(`\s+`) 587 str := space.ReplaceAllString(buff.String(), " ") 588 str = strings.TrimSpace(str) 589 So(err, ShouldBeNil) 590 So(str, ShouldContainSubstring, "ID SEVERITY TITLE") 591 So(str, ShouldContainSubstring, "CVE-2") 592 So(str, ShouldNotContainSubstring, "CVE-1") 593 }) 594 595 Convey("Test CVE by image name - GQL - search nonexistent CVE", t, func() { 596 args := []string{"list", "zot-cve-test:0.0.1", "--cve-id", "CVE-100", "--config", "cvetest"} 597 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 598 defer os.Remove(configPath) 599 cveCmd := client.NewCVECommand(client.NewSearchService()) 600 buff := bytes.NewBufferString("") 601 cveCmd.SetOut(buff) 602 cveCmd.SetErr(buff) 603 cveCmd.SetArgs(args) 604 err = cveCmd.Execute() 605 space := regexp.MustCompile(`\s+`) 606 str := space.ReplaceAllString(buff.String(), " ") 607 str = strings.TrimSpace(str) 608 So(err, ShouldBeNil) 609 So(str, ShouldContainSubstring, "No CVEs found for image") 610 }) 611 612 Convey("Test CVE by image name - GQL - invalid image", t, func() { 613 args := []string{"list", "invalid:0.0.1", "--config", "cvetest"} 614 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 615 defer os.Remove(configPath) 616 cveCmd := client.NewCVECommand(client.NewSearchService()) 617 buff := bytes.NewBufferString("") 618 cveCmd.SetOut(buff) 619 cveCmd.SetErr(buff) 620 cveCmd.SetArgs(args) 621 err = cveCmd.Execute() 622 So(err, ShouldNotBeNil) 623 }) 624 625 Convey("Test CVE by image name - GQL - invalid image name and tag", t, func() { 626 args := []string{"list", "invalid:", "--config", "cvetest"} 627 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 628 defer os.Remove(configPath) 629 cveCmd := client.NewCVECommand(client.NewSearchService()) 630 buff := bytes.NewBufferString("") 631 cveCmd.SetOut(buff) 632 cveCmd.SetErr(buff) 633 cveCmd.SetArgs(args) 634 err = cveCmd.Execute() 635 So(err, ShouldNotBeNil) 636 }) 637 638 Convey("Test CVE by image name - GQL - invalid cli output format", t, func() { 639 args := []string{"list", "zot-cve-test:0.0.1", "-f", "random", "--config", "cvetest"} 640 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 641 defer os.Remove(configPath) 642 cveCmd := client.NewCVECommand(client.NewSearchService()) 643 buff := bytes.NewBufferString("") 644 cveCmd.SetOut(buff) 645 cveCmd.SetErr(buff) 646 cveCmd.SetArgs(args) 647 err = cveCmd.Execute() 648 So(err, ShouldNotBeNil) 649 So(buff.String(), ShouldContainSubstring, "invalid cli output format") 650 }) 651 652 Convey("Test images by CVE ID - GQL - positive", t, func() { 653 args := []string{"affected", "CVE-2019-9923", "--config", "cvetest"} 654 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 655 defer os.Remove(configPath) 656 cveCmd := client.NewCVECommand(client.NewSearchService()) 657 buff := bytes.NewBufferString("") 658 cveCmd.SetOut(buff) 659 cveCmd.SetErr(buff) 660 cveCmd.SetArgs(args) 661 err := cveCmd.Execute() 662 space := regexp.MustCompile(`\s+`) 663 str := space.ReplaceAllString(buff.String(), " ") 664 str = strings.TrimSpace(str) 665 So(err, ShouldBeNil) 666 So(str, ShouldEqual, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 db573b01 false 854B") 667 }) 668 669 Convey("Test images by CVE ID - GQL - invalid CVE ID", t, func() { 670 args := []string{"affected", "CVE-invalid", "--config", "cvetest"} 671 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 672 defer os.Remove(configPath) 673 cveCmd := client.NewCVECommand(client.NewSearchService()) 674 buff := bytes.NewBufferString("") 675 cveCmd.SetOut(buff) 676 cveCmd.SetErr(buff) 677 cveCmd.SetArgs(args) 678 err := cveCmd.Execute() 679 space := regexp.MustCompile(`\s+`) 680 str := space.ReplaceAllString(buff.String(), " ") 681 str = strings.TrimSpace(str) 682 So(err, ShouldBeNil) 683 So(str, ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") 684 }) 685 686 Convey("Test images by CVE ID - GQL - invalid cli output format", t, func() { 687 args := []string{"affected", "CVE-2019-9923", "-f", "random", "--config", "cvetest"} 688 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 689 defer os.Remove(configPath) 690 cveCmd := client.NewCVECommand(client.NewSearchService()) 691 buff := bytes.NewBufferString("") 692 cveCmd.SetOut(buff) 693 cveCmd.SetErr(buff) 694 cveCmd.SetArgs(args) 695 err = cveCmd.Execute() 696 So(err, ShouldNotBeNil) 697 So(buff.String(), ShouldContainSubstring, "invalid cli output format") 698 }) 699 700 Convey("Test fixed tags by image name and CVE ID - GQL - positive", t, func() { 701 args := []string{"fixed", "zot-cve-test", "CVE-2019-9923", "--config", "cvetest"} 702 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 703 defer os.Remove(configPath) 704 cveCmd := client.NewCVECommand(client.NewSearchService()) 705 buff := bytes.NewBufferString("") 706 cveCmd.SetOut(buff) 707 cveCmd.SetErr(buff) 708 cveCmd.SetArgs(args) 709 err := cveCmd.Execute() 710 space := regexp.MustCompile(`\s+`) 711 str := space.ReplaceAllString(buff.String(), " ") 712 str = strings.TrimSpace(str) 713 So(err, ShouldBeNil) 714 So(str, ShouldEqual, "") 715 }) 716 717 Convey("Test fixed tags by image name and CVE ID - GQL - random cve", t, func() { 718 args := []string{"fixed", "zot-cve-test", "random", "--config", "cvetest"} 719 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 720 defer os.Remove(configPath) 721 cveCmd := client.NewCVECommand(client.NewSearchService()) 722 buff := bytes.NewBufferString("") 723 cveCmd.SetOut(buff) 724 cveCmd.SetErr(buff) 725 cveCmd.SetArgs(args) 726 err := cveCmd.Execute() 727 space := regexp.MustCompile(`\s+`) 728 str := space.ReplaceAllString(buff.String(), " ") 729 str = strings.TrimSpace(str) 730 So(err, ShouldBeNil) 731 So(strings.TrimSpace(str), ShouldContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") 732 }) 733 734 Convey("Test fixed tags by image name and CVE ID - GQL - random image", t, func() { 735 args := []string{"fixed", "zot-cv-test", "CVE-2019-20807", "--config", "cvetest"} 736 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 737 defer os.Remove(configPath) 738 cveCmd := client.NewCVECommand(client.NewSearchService()) 739 buff := bytes.NewBufferString("") 740 cveCmd.SetOut(buff) 741 cveCmd.SetErr(buff) 742 cveCmd.SetArgs(args) 743 err := cveCmd.Execute() 744 space := regexp.MustCompile(`\s+`) 745 str := space.ReplaceAllString(buff.String(), " ") 746 str = strings.TrimSpace(str) 747 So(err, ShouldNotBeNil) 748 So(strings.TrimSpace(str), ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") 749 }) 750 751 Convey("Test fixed tags by image name and CVE ID - GQL - invalid image", t, func() { 752 args := []string{"fixed", "zot-cv-test:tag", "CVE-2019-20807", "--config", "cvetest"} 753 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 754 defer os.Remove(configPath) 755 cveCmd := client.NewCVECommand(client.NewSearchService()) 756 buff := bytes.NewBufferString("") 757 cveCmd.SetOut(buff) 758 cveCmd.SetErr(buff) 759 cveCmd.SetArgs(args) 760 err := cveCmd.Execute() 761 space := regexp.MustCompile(`\s+`) 762 str := space.ReplaceAllString(buff.String(), " ") 763 str = strings.TrimSpace(str) 764 So(err, ShouldNotBeNil) 765 So(strings.TrimSpace(str), ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE") 766 }) 767 768 Convey("Test CVE by name and CVE ID - GQL - positive", t, func() { 769 args := []string{"affected", "CVE-2019-9923", "--repo", "zot-cve-test", "--config", "cvetest"} 770 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 771 defer os.Remove(configPath) 772 cveCmd := client.NewCVECommand(client.NewSearchService()) 773 buff := bytes.NewBufferString("") 774 cveCmd.SetOut(buff) 775 cveCmd.SetErr(buff) 776 cveCmd.SetArgs(args) 777 err := cveCmd.Execute() 778 space := regexp.MustCompile(`\s+`) 779 str := space.ReplaceAllString(buff.String(), " ") 780 So(err, ShouldBeNil) 781 So(strings.TrimSpace(str), ShouldEqual, 782 "REPOSITORY TAG OS/ARCH DIGEST SIGNED SIZE zot-cve-test 0.0.1 linux/amd64 db573b01 false 854B") 783 }) 784 785 Convey("Test CVE by name and CVE ID - GQL - invalid name and CVE ID", t, func() { 786 args := []string{"affected", "CVE-20807", "--repo", "test", "--config", "cvetest"} 787 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 788 defer os.Remove(configPath) 789 cveCmd := client.NewCVECommand(client.NewSearchService()) 790 buff := bytes.NewBufferString("") 791 cveCmd.SetOut(buff) 792 cveCmd.SetErr(buff) 793 cveCmd.SetArgs(args) 794 err := cveCmd.Execute() 795 space := regexp.MustCompile(`\s+`) 796 str := space.ReplaceAllString(buff.String(), " ") 797 So(err, ShouldBeNil) 798 So(strings.TrimSpace(str), ShouldNotContainSubstring, "REPOSITORY TAG OS/ARCH SIGNED SIZE") 799 }) 800 801 Convey("Test CVE by name and CVE ID - GQL - invalid cli output format", t, func() { 802 args := []string{"affected", "CVE-2019-9923", "--repo", "zot-cve-test", "-f", "random", "--config", "cvetest"} 803 configPath := makeConfigFile(fmt.Sprintf(`{"configs":[{"_name":"cvetest","url":"%s","showspinner":false}]}`, url)) 804 defer os.Remove(configPath) 805 cveCmd := client.NewCVECommand(client.NewSearchService()) 806 buff := bytes.NewBufferString("") 807 cveCmd.SetOut(buff) 808 cveCmd.SetErr(buff) 809 cveCmd.SetArgs(args) 810 err = cveCmd.Execute() 811 So(err, ShouldNotBeNil) 812 So(buff.String(), ShouldContainSubstring, "invalid cli output format") 813 }) 814 } 815 816 func TestCVESort(t *testing.T) { 817 rootDir := t.TempDir() 818 port := test.GetFreePort() 819 baseURL := test.GetBaseURL(port) 820 conf := config.New() 821 conf.HTTP.Port = port 822 823 defaultVal := true 824 conf.Extensions = &extconf.ExtensionConfig{ 825 Search: &extconf.SearchConfig{ 826 BaseConfig: extconf.BaseConfig{Enable: &defaultVal}, 827 CVE: &extconf.CVEConfig{ 828 UpdateInterval: 2, 829 Trivy: &extconf.TrivyConfig{ 830 DBRepository: "ghcr.io/project-zot/trivy-db", 831 }, 832 }, 833 }, 834 } 835 ctlr := api.NewController(conf) 836 ctlr.Config.Storage.RootDirectory = rootDir 837 838 image1 := CreateRandomImage() 839 840 storeController := ociutils.GetDefaultStoreController(rootDir, ctlr.Log) 841 842 err := WriteImageToFileSystem(image1, "repo", "tag", storeController) 843 if err != nil { 844 t.FailNow() 845 } 846 847 if err := ctlr.Init(); err != nil { 848 panic(err) 849 } 850 851 ctlr.CveScanner = mocks.CveScannerMock{ 852 ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { 853 return map[string]cvemodel.CVE{ 854 "CVE-2023-1255": { 855 ID: "CVE-2023-1255", 856 Severity: "LOW", 857 Title: "Input buffer over-read in AES-XTS implementation and testing", 858 }, 859 "CVE-2023-2650": { 860 ID: "CVE-2023-2650", 861 Severity: "MEDIUM", 862 Title: "Possible DoS translating ASN.1 object identifier and executer", 863 }, 864 "CVE-2023-2975": { 865 ID: "CVE-2023-2975", 866 Severity: "HIGH", 867 Title: "AES-SIV cipher implementation contains a bug that can break", 868 }, 869 "CVE-2023-3446": { 870 ID: "CVE-2023-3446", 871 Severity: "CRITICAL", 872 Title: "Excessive time spent checking DH keys and parenthesis", 873 }, 874 "CVE-2023-3817": { 875 ID: "CVE-2023-3817", 876 Severity: "MEDIUM", 877 Title: "Excessive time spent checking DH q parameter and arguments", 878 }, 879 }, nil 880 }, 881 } 882 883 go func() { 884 if err := ctlr.Run(); !errors.Is(err, http.ErrServerClosed) { 885 panic(err) 886 } 887 }() 888 889 defer ctlr.Shutdown() 890 891 test.WaitTillServerReady(baseURL) 892 893 space := regexp.MustCompile(`\s+`) 894 895 Convey("test sorting", t, func() { 896 args := []string{"list", "repo:tag", "--sort-by", "severity", "--url", baseURL} 897 cmd := client.NewCVECommand(client.NewSearchService()) 898 buff := bytes.NewBufferString("") 899 cmd.SetOut(buff) 900 cmd.SetErr(buff) 901 cmd.SetArgs(args) 902 err := cmd.Execute() 903 So(err, ShouldBeNil) 904 str := space.ReplaceAllString(buff.String(), " ") 905 actual := strings.TrimSpace(str) 906 So(actual, ShouldResemble, 907 "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ 908 "CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+ 909 "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ 910 "CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+ 911 "CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ... "+ 912 "CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat...") 913 914 args = []string{"list", "repo:tag", "--sort-by", "alpha-asc", "--url", baseURL} 915 cmd = client.NewCVECommand(client.NewSearchService()) 916 buff = bytes.NewBufferString("") 917 cmd.SetOut(buff) 918 cmd.SetErr(buff) 919 cmd.SetArgs(args) 920 err = cmd.Execute() 921 So(err, ShouldBeNil) 922 str = space.ReplaceAllString(buff.String(), " ") 923 actual = strings.TrimSpace(str) 924 So(actual, ShouldResemble, 925 "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ 926 "CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat... "+ 927 "CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+ 928 "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ 929 "CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+ 930 "CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ...") 931 932 args = []string{"list", "repo:tag", "--sort-by", "alpha-dsc", "--url", baseURL} 933 cmd = client.NewCVECommand(client.NewSearchService()) 934 buff = bytes.NewBufferString("") 935 cmd.SetOut(buff) 936 cmd.SetErr(buff) 937 cmd.SetArgs(args) 938 err = cmd.Execute() 939 So(err, ShouldBeNil) 940 str = space.ReplaceAllString(buff.String(), " ") 941 actual = strings.TrimSpace(str) 942 So(actual, ShouldResemble, 943 "CRITICAL 1, HIGH 1, MEDIUM 2, LOW 1, UNKNOWN 0, TOTAL 5 ID SEVERITY TITLE "+ 944 "CVE-2023-3817 MEDIUM Excessive time spent checking DH q parameter ... "+ 945 "CVE-2023-3446 CRITICAL Excessive time spent checking DH keys and par... "+ 946 "CVE-2023-2975 HIGH AES-SIV cipher implementation contains a bug ... "+ 947 "CVE-2023-2650 MEDIUM Possible DoS translating ASN.1 object identif... "+ 948 "CVE-2023-1255 LOW Input buffer over-read in AES-XTS implementat...") 949 }) 950 } 951 952 func getMockCveScanner(metaDB mTypes.MetaDB) cveinfo.Scanner { 953 // MetaDB loaded with initial data now mock the scanner 954 // Setup test CVE data in mock scanner 955 scanner := mocks.CveScannerMock{ 956 ScanImageFn: func(ctx context.Context, image string) (map[string]cvemodel.CVE, error) { 957 if strings.Contains(image, "zot-cve-test@sha256:db573b01") || 958 image == "zot-cve-test:0.0.1" { 959 return map[string]cvemodel.CVE{ 960 "CVE-1": { 961 ID: "CVE-1", 962 Severity: "CRITICAL", 963 Title: "Title for CVE-C1", 964 Description: "Description of CVE-1", 965 }, 966 "CVE-2019-9923": { 967 ID: "CVE-2019-9923", 968 Severity: "HIGH", 969 Title: "Title for CVE-2", 970 Description: "Description of CVE-2", 971 }, 972 "CVE-3": { 973 ID: "CVE-3", 974 Severity: "MEDIUM", 975 Title: "Title for CVE-3", 976 Description: "Description of CVE-3", 977 }, 978 "CVE-4": { 979 ID: "CVE-4", 980 Severity: "LOW", 981 Title: "Title for CVE-4", 982 Description: "Description of CVE-4", 983 }, 984 "CVE-5": { 985 ID: "CVE-5", 986 Severity: "UNKNOWN", 987 Title: "Title for CVE-5", 988 Description: "Description of CVE-5", 989 }, 990 }, nil 991 } 992 993 // By default the image has no vulnerabilities 994 return map[string]cvemodel.CVE{}, nil 995 }, 996 IsImageFormatScannableFn: func(repo string, reference string) (bool, error) { 997 // Almost same logic compared to actual Trivy specific implementation 998 imageDir := repo 999 inputTag := reference 1000 1001 repoMeta, err := metaDB.GetRepoMeta(context.Background(), imageDir) 1002 if err != nil { 1003 return false, err 1004 } 1005 1006 manifestDigestStr := reference 1007 1008 if zcommon.IsTag(reference) { 1009 var ok bool 1010 1011 descriptor, ok := repoMeta.Tags[inputTag] 1012 if !ok { 1013 return false, zerr.ErrTagMetaNotFound 1014 } 1015 1016 manifestDigestStr = descriptor.Digest 1017 } 1018 1019 manifestDigest, err := godigest.Parse(manifestDigestStr) 1020 if err != nil { 1021 return false, err 1022 } 1023 1024 manifestData, err := metaDB.GetImageMeta(manifestDigest) 1025 if err != nil { 1026 return false, err 1027 } 1028 1029 for _, imageLayer := range manifestData.Manifests[0].Manifest.Layers { 1030 switch imageLayer.MediaType { 1031 case ispec.MediaTypeImageLayerGzip, ispec.MediaTypeImageLayer, string(regTypes.DockerLayer): 1032 1033 return true, nil 1034 default: 1035 1036 return false, zerr.ErrScanNotSupported 1037 } 1038 } 1039 1040 return false, nil 1041 }, 1042 } 1043 1044 return &scanner 1045 }