github.com/khulnasoft/trivy@v0.48.1-0.20231207234930-27df843a75e0/integration/client_server_test.go (about) 1 //go:build integration 2 3 package integration 4 5 import ( 6 "context" 7 "fmt" 8 "os" 9 "path/filepath" 10 "strings" 11 "testing" 12 "time" 13 14 dockercontainer "github.com/docker/docker/api/types/container" 15 "github.com/docker/go-connections/nat" 16 "github.com/stretchr/testify/assert" 17 "github.com/stretchr/testify/require" 18 testcontainers "github.com/testcontainers/testcontainers-go" 19 20 "github.com/khulnasoft/trivy/pkg/clock" 21 "github.com/khulnasoft/trivy/pkg/report" 22 "github.com/khulnasoft/trivy/pkg/uuid" 23 ) 24 25 type csArgs struct { 26 Command string 27 RemoteAddrOption string 28 Format string 29 TemplatePath string 30 IgnoreUnfixed bool 31 Severity []string 32 IgnoreIDs []string 33 Input string 34 ClientToken string 35 ClientTokenHeader string 36 ListAllPackages bool 37 Target string 38 secretConfig string 39 } 40 41 func TestClientServer(t *testing.T) { 42 tests := []struct { 43 name string 44 args csArgs 45 golden string 46 wantErr string 47 }{ 48 { 49 name: "alpine 3.9", 50 args: csArgs{ 51 Input: "testdata/fixtures/images/alpine-39.tar.gz", 52 }, 53 golden: "testdata/alpine-39.json.golden", 54 }, 55 { 56 name: "alpine 3.9 with high and critical severity", 57 args: csArgs{ 58 IgnoreUnfixed: true, 59 Severity: []string{ 60 "HIGH", 61 "CRITICAL", 62 }, 63 Input: "testdata/fixtures/images/alpine-39.tar.gz", 64 }, 65 golden: "testdata/alpine-39-high-critical.json.golden", 66 }, 67 { 68 name: "alpine 3.9 with .trivyignore", 69 args: csArgs{ 70 IgnoreUnfixed: false, 71 IgnoreIDs: []string{ 72 "CVE-2019-1549", 73 "CVE-2019-14697", 74 }, 75 Input: "testdata/fixtures/images/alpine-39.tar.gz", 76 }, 77 golden: "testdata/alpine-39-ignore-cveids.json.golden", 78 }, 79 { 80 name: "alpine 3.10", 81 args: csArgs{ 82 Input: "testdata/fixtures/images/alpine-310.tar.gz", 83 }, 84 golden: "testdata/alpine-310.json.golden", 85 }, 86 { 87 name: "alpine distroless", 88 args: csArgs{ 89 Input: "testdata/fixtures/images/alpine-distroless.tar.gz", 90 }, 91 golden: "testdata/alpine-distroless.json.golden", 92 }, 93 { 94 name: "debian buster/10", 95 args: csArgs{ 96 Input: "testdata/fixtures/images/debian-buster.tar.gz", 97 }, 98 golden: "testdata/debian-buster.json.golden", 99 }, 100 { 101 name: "debian buster/10 with --ignore-unfixed option", 102 args: csArgs{ 103 IgnoreUnfixed: true, 104 Input: "testdata/fixtures/images/debian-buster.tar.gz", 105 }, 106 golden: "testdata/debian-buster-ignore-unfixed.json.golden", 107 }, 108 { 109 name: "debian stretch/9", 110 args: csArgs{ 111 Input: "testdata/fixtures/images/debian-stretch.tar.gz", 112 }, 113 golden: "testdata/debian-stretch.json.golden", 114 }, 115 { 116 name: "ubuntu 18.04", 117 args: csArgs{ 118 Input: "testdata/fixtures/images/ubuntu-1804.tar.gz", 119 }, 120 golden: "testdata/ubuntu-1804.json.golden", 121 }, 122 { 123 name: "centos 7", 124 args: csArgs{ 125 Input: "testdata/fixtures/images/centos-7.tar.gz", 126 }, 127 golden: "testdata/centos-7.json.golden", 128 }, 129 { 130 name: "centos 7 with --ignore-unfixed option", 131 args: csArgs{ 132 IgnoreUnfixed: true, 133 Input: "testdata/fixtures/images/centos-7.tar.gz", 134 }, 135 golden: "testdata/centos-7-ignore-unfixed.json.golden", 136 }, 137 { 138 name: "centos 7 with medium severity", 139 args: csArgs{ 140 IgnoreUnfixed: true, 141 Severity: []string{"MEDIUM"}, 142 Input: "testdata/fixtures/images/centos-7.tar.gz", 143 }, 144 golden: "testdata/centos-7-medium.json.golden", 145 }, 146 { 147 name: "centos 6", 148 args: csArgs{ 149 Input: "testdata/fixtures/images/centos-6.tar.gz", 150 }, 151 golden: "testdata/centos-6.json.golden", 152 }, 153 { 154 name: "ubi 7", 155 args: csArgs{ 156 Input: "testdata/fixtures/images/ubi-7.tar.gz", 157 }, 158 golden: "testdata/ubi-7.json.golden", 159 }, 160 { 161 name: "almalinux 8", 162 args: csArgs{ 163 Input: "testdata/fixtures/images/almalinux-8.tar.gz", 164 }, 165 golden: "testdata/almalinux-8.json.golden", 166 }, 167 { 168 name: "rocky linux 8", 169 args: csArgs{ 170 Input: "testdata/fixtures/images/rockylinux-8.tar.gz", 171 }, 172 golden: "testdata/rockylinux-8.json.golden", 173 }, 174 { 175 name: "distroless base", 176 args: csArgs{ 177 Input: "testdata/fixtures/images/distroless-base.tar.gz", 178 }, 179 golden: "testdata/distroless-base.json.golden", 180 }, 181 { 182 name: "distroless python27", 183 args: csArgs{ 184 Input: "testdata/fixtures/images/distroless-python27.tar.gz", 185 }, 186 golden: "testdata/distroless-python27.json.golden", 187 }, 188 { 189 name: "amazon 1", 190 args: csArgs{ 191 Input: "testdata/fixtures/images/amazon-1.tar.gz", 192 }, 193 golden: "testdata/amazon-1.json.golden", 194 }, 195 { 196 name: "amazon 2", 197 args: csArgs{ 198 Input: "testdata/fixtures/images/amazon-2.tar.gz", 199 }, 200 golden: "testdata/amazon-2.json.golden", 201 }, 202 { 203 name: "oracle 8", 204 args: csArgs{ 205 Input: "testdata/fixtures/images/oraclelinux-8.tar.gz", 206 }, 207 golden: "testdata/oraclelinux-8.json.golden", 208 }, 209 { 210 name: "opensuse leap 15.1", 211 args: csArgs{ 212 Input: "testdata/fixtures/images/opensuse-leap-151.tar.gz", 213 }, 214 golden: "testdata/opensuse-leap-151.json.golden", 215 }, 216 { 217 name: "photon 3.0", 218 args: csArgs{ 219 Input: "testdata/fixtures/images/photon-30.tar.gz", 220 }, 221 golden: "testdata/photon-30.json.golden", 222 }, 223 { 224 name: "CBL-Mariner 1.0", 225 args: csArgs{ 226 Input: "testdata/fixtures/images/mariner-1.0.tar.gz", 227 }, 228 golden: "testdata/mariner-1.0.json.golden", 229 }, 230 { 231 name: "busybox with Cargo.lock", 232 args: csArgs{ 233 Input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz", 234 }, 235 golden: "testdata/busybox-with-lockfile.json.golden", 236 }, 237 { 238 name: "scan pox.xml with repo command in client/server mode", 239 args: csArgs{ 240 Command: "repo", 241 RemoteAddrOption: "--server", 242 Target: "testdata/fixtures/repo/pom/", 243 }, 244 golden: "testdata/pom.json.golden", 245 }, 246 { 247 name: "scan sample.pem with repo command in client/server mode", 248 args: csArgs{ 249 Command: "repo", 250 RemoteAddrOption: "--server", 251 secretConfig: "testdata/fixtures/repo/secrets/trivy-secret.yaml", 252 Target: "testdata/fixtures/repo/secrets/", 253 }, 254 golden: "testdata/secrets.json.golden", 255 }, 256 { 257 name: "scan remote repository with repo command in client/server mode", 258 args: csArgs{ 259 Command: "repo", 260 RemoteAddrOption: "--server", 261 Target: "https://github.com/knqyf263/trivy-ci-test", 262 }, 263 golden: "testdata/test-repo.json.golden", 264 }, 265 } 266 267 addr, cacheDir := setup(t, setupOptions{}) 268 269 for _, c := range tests { 270 t.Run(c.name, func(t *testing.T) { 271 osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden) 272 273 if c.args.secretConfig != "" { 274 osArgs = append(osArgs, "--secret-config", c.args.secretConfig) 275 } 276 277 // 278 err := execute(osArgs) 279 require.NoError(t, err) 280 281 compareReports(t, c.golden, outputFile, nil) 282 }) 283 } 284 } 285 286 func TestClientServerWithFormat(t *testing.T) { 287 tests := []struct { 288 name string 289 args csArgs 290 golden string 291 }{ 292 { 293 name: "alpine 3.10 with gitlab template", 294 args: csArgs{ 295 Format: "template", 296 TemplatePath: "@../contrib/gitlab.tpl", 297 Input: "testdata/fixtures/images/alpine-310.tar.gz", 298 }, 299 golden: "testdata/alpine-310.gitlab.golden", 300 }, 301 { 302 name: "alpine 3.10 with gitlab-codequality template", 303 args: csArgs{ 304 Format: "template", 305 TemplatePath: "@../contrib/gitlab-codequality.tpl", 306 Input: "testdata/fixtures/images/alpine-310.tar.gz", 307 }, 308 golden: "testdata/alpine-310.gitlab-codequality.golden", 309 }, 310 { 311 name: "alpine 3.10 with sarif format", 312 args: csArgs{ 313 Format: "sarif", 314 Input: "testdata/fixtures/images/alpine-310.tar.gz", 315 }, 316 golden: "testdata/alpine-310.sarif.golden", 317 }, 318 { 319 name: "alpine 3.10 with ASFF template", 320 args: csArgs{ 321 Format: "template", 322 TemplatePath: "@../contrib/asff.tpl", 323 Input: "testdata/fixtures/images/alpine-310.tar.gz", 324 }, 325 golden: "testdata/alpine-310.asff.golden", 326 }, 327 { 328 name: "scan secrets with ASFF template", 329 args: csArgs{ 330 Command: "repo", 331 RemoteAddrOption: "--server", 332 Format: "template", 333 TemplatePath: "@../contrib/asff.tpl", 334 Target: "testdata/fixtures/repo/secrets/", 335 }, 336 golden: "testdata/secrets.asff.golden", 337 }, 338 { 339 name: "alpine 3.10 with html template", 340 args: csArgs{ 341 Format: "template", 342 TemplatePath: "@../contrib/html.tpl", 343 Input: "testdata/fixtures/images/alpine-310.tar.gz", 344 }, 345 golden: "testdata/alpine-310.html.golden", 346 }, 347 { 348 name: "alpine 3.10 with github dependency snapshots format", 349 args: csArgs{ 350 Format: "github", 351 Input: "testdata/fixtures/images/alpine-310.tar.gz", 352 }, 353 golden: "testdata/alpine-310.gsbom.golden", 354 }, 355 } 356 357 fakeTime := time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC) 358 clock.SetFakeTime(t, fakeTime) 359 360 report.CustomTemplateFuncMap = map[string]interface{}{ 361 "now": func() time.Time { 362 return fakeTime 363 }, 364 "date": func(format string, t time.Time) string { 365 return t.Format(format) 366 }, 367 } 368 369 // For GitHub Dependency Snapshots 370 t.Setenv("GITHUB_REF", "/ref/feature-1") 371 t.Setenv("GITHUB_SHA", "39da54a1ff04120a31df8cbc94ce9ede251d21a3") 372 t.Setenv("GITHUB_JOB", "integration") 373 t.Setenv("GITHUB_RUN_ID", "1910764383") 374 t.Setenv("GITHUB_WORKFLOW", "workflow-name") 375 376 t.Cleanup(func() { 377 report.CustomTemplateFuncMap = map[string]interface{}{} 378 }) 379 380 addr, cacheDir := setup(t, setupOptions{}) 381 382 for _, tt := range tests { 383 t.Run(tt.name, func(t *testing.T) { 384 t.Setenv("AWS_REGION", "test-region") 385 t.Setenv("AWS_ACCOUNT_ID", "123456789012") 386 osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden) 387 388 // Run Trivy client 389 err := execute(osArgs) 390 require.NoError(t, err) 391 392 want, err := os.ReadFile(tt.golden) 393 require.NoError(t, err) 394 395 got, err := os.ReadFile(outputFile) 396 require.NoError(t, err) 397 398 assert.EqualValues(t, string(want), string(got)) 399 }) 400 } 401 } 402 403 func TestClientServerWithCycloneDX(t *testing.T) { 404 tests := []struct { 405 name string 406 args csArgs 407 golden string 408 }{ 409 { 410 name: "fluentd with RubyGems with CycloneDX format", 411 args: csArgs{ 412 Format: "cyclonedx", 413 Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", 414 }, 415 golden: "testdata/fluentd-multiple-lockfiles.cdx.json.golden", 416 }, 417 } 418 419 addr, cacheDir := setup(t, setupOptions{}) 420 for _, tt := range tests { 421 t.Run(tt.name, func(t *testing.T) { 422 clock.SetFakeTime(t, time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) 423 uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") 424 425 osArgs, outputFile := setupClient(t, tt.args, addr, cacheDir, tt.golden) 426 427 // Run Trivy client 428 err := execute(osArgs) 429 require.NoError(t, err) 430 431 compareCycloneDX(t, tt.golden, outputFile) 432 }) 433 } 434 } 435 436 func TestClientServerWithToken(t *testing.T) { 437 cases := []struct { 438 name string 439 args csArgs 440 golden string 441 wantErr string 442 }{ 443 { 444 name: "alpine 3.9 with token", 445 args: csArgs{ 446 Input: "testdata/fixtures/images/alpine-39.tar.gz", 447 ClientToken: "token", 448 ClientTokenHeader: "Trivy-Token", 449 }, 450 golden: "testdata/alpine-39.json.golden", 451 }, 452 { 453 name: "invalid token", 454 args: csArgs{ 455 Input: "testdata/fixtures/images/distroless-base.tar.gz", 456 ClientToken: "invalidtoken", 457 ClientTokenHeader: "Trivy-Token", 458 }, 459 wantErr: "twirp error unauthenticated: invalid token", 460 }, 461 { 462 name: "invalid token header", 463 args: csArgs{ 464 Input: "testdata/fixtures/images/distroless-base.tar.gz", 465 ClientToken: "token", 466 ClientTokenHeader: "Unknown-Header", 467 }, 468 wantErr: "twirp error unauthenticated: invalid token", 469 }, 470 } 471 472 serverToken := "token" 473 serverTokenHeader := "Trivy-Token" 474 addr, cacheDir := setup(t, setupOptions{ 475 token: serverToken, 476 tokenHeader: serverTokenHeader, 477 }) 478 479 for _, c := range cases { 480 t.Run(c.name, func(t *testing.T) { 481 osArgs, outputFile := setupClient(t, c.args, addr, cacheDir, c.golden) 482 483 // Run Trivy client 484 err := execute(osArgs) 485 if c.wantErr != "" { 486 require.Error(t, err, c.name) 487 assert.Contains(t, err.Error(), c.wantErr, c.name) 488 return 489 } 490 491 require.NoError(t, err, c.name) 492 compareReports(t, c.golden, outputFile, nil) 493 }) 494 } 495 } 496 497 func TestClientServerWithRedis(t *testing.T) { 498 // Set up a Redis container 499 ctx := context.Background() 500 // This test includes 2 checks 501 // redisC container will terminate after first check 502 redisC, addr := setupRedis(t, ctx) 503 504 // Set up Trivy server 505 addr, cacheDir := setup(t, setupOptions{cacheBackend: addr}) 506 t.Cleanup(func() { os.RemoveAll(cacheDir) }) 507 508 // Test parameters 509 testArgs := csArgs{ 510 Input: "testdata/fixtures/images/alpine-39.tar.gz", 511 } 512 golden := "testdata/alpine-39.json.golden" 513 514 t.Run("alpine 3.9", func(t *testing.T) { 515 osArgs, outputFile := setupClient(t, testArgs, addr, cacheDir, golden) 516 517 // Run Trivy client 518 err := execute(osArgs) 519 require.NoError(t, err) 520 521 compareReports(t, golden, outputFile, nil) 522 }) 523 524 // Terminate the Redis container 525 require.NoError(t, redisC.Terminate(ctx)) 526 527 t.Run("sad path", func(t *testing.T) { 528 osArgs, _ := setupClient(t, testArgs, addr, cacheDir, golden) 529 530 // Run Trivy client 531 err := execute(osArgs) 532 require.Error(t, err) 533 assert.Contains(t, err.Error(), "connect: connection refused") 534 }) 535 } 536 537 type setupOptions struct { 538 token string 539 tokenHeader string 540 cacheBackend string 541 } 542 543 func setup(t *testing.T, options setupOptions) (string, string) { 544 t.Helper() 545 546 // Set up testing DB 547 cacheDir := initDB(t) 548 549 // Set a temp dir so that modules will not be loaded 550 t.Setenv("XDG_DATA_HOME", cacheDir) 551 552 port, err := getFreePort() 553 assert.NoError(t, err) 554 addr := fmt.Sprintf("localhost:%d", port) 555 556 go func() { 557 osArgs := setupServer(addr, options.token, options.tokenHeader, cacheDir, options.cacheBackend) 558 559 // Run Trivy server 560 require.NoError(t, execute(osArgs)) 561 }() 562 563 ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) 564 err = waitPort(ctx, addr) 565 assert.NoError(t, err) 566 567 return addr, cacheDir 568 } 569 570 func setupServer(addr, token, tokenHeader, cacheDir, cacheBackend string) []string { 571 osArgs := []string{ 572 "--cache-dir", 573 cacheDir, 574 "server", 575 "--skip-update", 576 "--listen", 577 addr, 578 } 579 if token != "" { 580 osArgs = append(osArgs, []string{ 581 "--token", 582 token, 583 "--token-header", 584 tokenHeader, 585 }...) 586 } 587 if cacheBackend != "" { 588 osArgs = append(osArgs, "--cache-backend", cacheBackend) 589 } 590 return osArgs 591 } 592 593 func setupClient(t *testing.T, c csArgs, addr string, cacheDir string, golden string) ([]string, string) { 594 if c.Command == "" { 595 c.Command = "image" 596 } 597 if c.RemoteAddrOption == "" { 598 c.RemoteAddrOption = "--server" 599 } 600 t.Helper() 601 osArgs := []string{ 602 "--cache-dir", 603 cacheDir, 604 c.Command, 605 c.RemoteAddrOption, 606 "http://" + addr, 607 } 608 609 if c.Format != "" { 610 osArgs = append(osArgs, "--format", c.Format) 611 if c.TemplatePath != "" { 612 osArgs = append(osArgs, "--template", c.TemplatePath) 613 } 614 } else { 615 osArgs = append(osArgs, "--format", "json") 616 } 617 618 if c.IgnoreUnfixed { 619 osArgs = append(osArgs, "--ignore-unfixed") 620 } 621 if len(c.Severity) != 0 { 622 osArgs = append(osArgs, 623 "--severity", strings.Join(c.Severity, ","), 624 ) 625 } 626 627 if len(c.IgnoreIDs) != 0 { 628 trivyIgnore := filepath.Join(t.TempDir(), ".trivyignore") 629 err := os.WriteFile(trivyIgnore, []byte(strings.Join(c.IgnoreIDs, "\n")), 0444) 630 require.NoError(t, err, "failed to write .trivyignore") 631 osArgs = append(osArgs, "--ignorefile", trivyIgnore) 632 } 633 if c.ClientToken != "" { 634 osArgs = append(osArgs, "--token", c.ClientToken, "--token-header", c.ClientTokenHeader) 635 } 636 if c.Input != "" { 637 osArgs = append(osArgs, "--input", c.Input) 638 } 639 640 // Set up the output file 641 outputFile := filepath.Join(t.TempDir(), "output.json") 642 if *update { 643 outputFile = golden 644 } 645 646 osArgs = append(osArgs, "--output", outputFile) 647 648 if c.Target != "" { 649 osArgs = append(osArgs, c.Target) 650 } 651 652 return osArgs, outputFile 653 } 654 655 func setupRedis(t *testing.T, ctx context.Context) (testcontainers.Container, string) { 656 t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") 657 t.Helper() 658 imageName := "redis:5.0" 659 port := "6379/tcp" 660 req := testcontainers.ContainerRequest{ 661 Name: "redis", 662 Image: imageName, 663 ExposedPorts: []string{port}, 664 HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) { 665 hostConfig.AutoRemove = true 666 }, 667 } 668 669 redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ 670 ContainerRequest: req, 671 Started: true, 672 }) 673 require.NoError(t, err) 674 675 ip, err := redis.Host(ctx) 676 require.NoError(t, err) 677 678 p, err := redis.MappedPort(ctx, nat.Port(port)) 679 require.NoError(t, err) 680 681 addr := fmt.Sprintf("redis://%s:%s", ip, p.Port()) 682 return redis, addr 683 }