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