github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/image/image_test.go (about) 1 package image 2 3 import ( 4 "context" 5 "fmt" 6 "net/http/httptest" 7 "os" 8 "testing" 9 "time" 10 11 "github.com/golang-jwt/jwt" 12 v1 "github.com/google/go-containerregistry/pkg/v1" 13 "github.com/stretchr/testify/assert" 14 "github.com/stretchr/testify/require" 15 16 "github.com/aquasecurity/testdocker/auth" 17 "github.com/aquasecurity/testdocker/engine" 18 "github.com/aquasecurity/testdocker/registry" 19 "github.com/devseccon/trivy/pkg/fanal/types" 20 ) 21 22 func setupEngineAndRegistry() (*httptest.Server, *httptest.Server) { 23 imagePaths := map[string]string{ 24 "alpine:3.10": "../test/testdata/alpine-310.tar.gz", 25 "alpine:3.11": "../test/testdata/alpine-311.tar.gz", 26 "a187dde48cd2": "../test/testdata/alpine-311.tar.gz", 27 } 28 opt := engine.Option{ 29 APIVersion: "1.38", 30 ImagePaths: imagePaths, 31 } 32 te := engine.NewDockerEngine(opt) 33 34 imagePaths = map[string]string{ 35 "v2/library/alpine:3.10": "../test/testdata/alpine-310.tar.gz", 36 } 37 tr := registry.NewDockerRegistry(registry.Option{ 38 Images: imagePaths, 39 Auth: auth.Auth{}, 40 }) 41 42 os.Setenv("DOCKER_HOST", fmt.Sprintf("tcp://%s", te.Listener.Addr().String())) 43 44 return te, tr 45 } 46 47 func TestNewDockerImage(t *testing.T) { 48 te, tr := setupEngineAndRegistry() 49 defer func() { 50 te.Close() 51 tr.Close() 52 }() 53 serverAddr := tr.Listener.Addr().String() 54 55 type args struct { 56 imageName string 57 option types.ImageOptions 58 } 59 tests := []struct { 60 name string 61 args args 62 wantID string 63 wantConfigFile *v1.ConfigFile 64 wantRepoTags []string 65 wantRepoDigests []string 66 wantErr bool 67 }{ 68 { 69 name: "happy path with Docker Engine (use pattern <imageName>:<tag> for image name)", 70 args: args{ 71 imageName: "alpine:3.11", 72 }, 73 wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", 74 wantRepoTags: []string{"alpine:3.11"}, 75 wantConfigFile: &v1.ConfigFile{ 76 Architecture: "amd64", 77 Container: "fb71ddde5f6411a82eb056a9190f0cc1c80d7f77a8509ee90a2054428edb0024", 78 OS: "linux", 79 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, 80 DockerVersion: "18.09.7", 81 History: []v1.History{ 82 { 83 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, 84 CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", 85 Comment: "", 86 EmptyLayer: true, 87 }, 88 { 89 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, 90 CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", 91 EmptyLayer: false, 92 }, 93 }, 94 RootFS: v1.RootFS{ 95 Type: "layers", 96 DiffIDs: []v1.Hash{ 97 { 98 Algorithm: "sha256", 99 Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", 100 }, 101 }, 102 }, 103 Config: v1.Config{ 104 Cmd: []string{"/bin/sh"}, 105 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 106 Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c", 107 ArgsEscaped: true, 108 }, 109 OSVersion: "", 110 }, 111 }, 112 { 113 name: "happy path with Docker Engine (use pattern <ImageID> for image name)", 114 args: args{ 115 imageName: "a187dde48cd2", 116 }, 117 wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", 118 wantRepoTags: []string{"alpine:3.11"}, 119 wantConfigFile: &v1.ConfigFile{ 120 Architecture: "amd64", 121 Container: "fb71ddde5f6411a82eb056a9190f0cc1c80d7f77a8509ee90a2054428edb0024", 122 OS: "linux", 123 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, 124 DockerVersion: "18.09.7", 125 History: []v1.History{ 126 { 127 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, 128 CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", 129 Comment: "", 130 EmptyLayer: true, 131 }, 132 { 133 Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, 134 CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", 135 EmptyLayer: false, 136 }, 137 }, 138 RootFS: v1.RootFS{ 139 Type: "layers", 140 DiffIDs: []v1.Hash{ 141 { 142 Algorithm: "sha256", 143 Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", 144 }, 145 }, 146 }, 147 Config: v1.Config{ 148 Cmd: []string{"/bin/sh"}, 149 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 150 Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c", 151 ArgsEscaped: true, 152 }, 153 OSVersion: "", 154 }, 155 }, 156 { 157 name: "happy path with Docker Registry", 158 args: args{ 159 imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), 160 }, 161 wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e", 162 wantRepoTags: []string{serverAddr + "/library/alpine:3.10"}, 163 wantRepoDigests: []string{ 164 serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91", 165 }, 166 wantConfigFile: &v1.ConfigFile{ 167 Architecture: "amd64", 168 Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28", 169 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 686519038, time.UTC)}, 170 DockerVersion: "18.06.1-ce", 171 History: []v1.History{ 172 { 173 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 551172402, time.UTC)}, 174 CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ", 175 EmptyLayer: false, 176 }, 177 { 178 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 686519038, time.UTC)}, 179 CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", 180 Comment: "", 181 EmptyLayer: true, 182 }, 183 }, 184 OS: "linux", 185 186 RootFS: v1.RootFS{ 187 Type: "layers", 188 DiffIDs: []v1.Hash{ 189 { 190 Algorithm: "sha256", 191 Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", 192 }, 193 }, 194 }, 195 Config: v1.Config{ 196 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 197 Cmd: []string{"/bin/sh"}, 198 Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", 199 ArgsEscaped: true, 200 }, 201 OSVersion: "", 202 }, 203 }, 204 { 205 name: "happy path with insecure Docker Registry", 206 args: args{ 207 imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), 208 option: types.ImageOptions{ 209 RegistryOptions: types.RegistryOptions{ 210 Credentials: []types.Credential{ 211 { 212 Username: "test", 213 Password: "test", 214 }, 215 }, 216 Insecure: true, 217 }, 218 }, 219 }, 220 wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e", 221 wantRepoTags: []string{serverAddr + "/library/alpine:3.10"}, 222 wantRepoDigests: []string{ 223 serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91", 224 }, 225 wantConfigFile: &v1.ConfigFile{ 226 Architecture: "amd64", 227 Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28", 228 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 686519038, time.UTC)}, 229 DockerVersion: "18.06.1-ce", 230 History: []v1.History{ 231 { 232 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 551172402, time.UTC)}, 233 CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ", 234 EmptyLayer: false, 235 }, 236 { 237 Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 06, 686519038, time.UTC)}, 238 CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", 239 Comment: "", 240 EmptyLayer: true, 241 }, 242 }, 243 OS: "linux", 244 245 RootFS: v1.RootFS{ 246 Type: "layers", 247 DiffIDs: []v1.Hash{ 248 { 249 Algorithm: "sha256", 250 Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", 251 }, 252 }, 253 }, 254 Config: v1.Config{ 255 Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, 256 Cmd: []string{"/bin/sh"}, 257 Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", 258 ArgsEscaped: true, 259 }, 260 }, 261 }, 262 { 263 name: "sad path with invalid tag", 264 args: args{ 265 imageName: fmt.Sprintf("%s/library/alpine:3.11!!!", serverAddr), 266 }, 267 wantErr: true, 268 }, 269 { 270 name: "sad path with non-exist image", 271 args: args{ 272 imageName: fmt.Sprintf("%s/library/alpine:100", serverAddr), 273 }, 274 wantErr: true, 275 }, 276 } 277 for _, tt := range tests { 278 t.Run(tt.name, func(t *testing.T) { 279 tt.args.option.ImageSources = types.AllImageSources 280 img, cleanup, err := NewContainerImage(context.Background(), tt.args.imageName, tt.args.option) 281 defer cleanup() 282 283 if tt.wantErr { 284 assert.NotNil(t, err) 285 return 286 } 287 assert.NoError(t, err) 288 289 gotID, err := img.ID() 290 require.NoError(t, err) 291 assert.Equal(t, tt.wantID, gotID) 292 293 gotConfigFile, err := img.ConfigFile() 294 require.NoError(t, err) 295 assert.Equal(t, tt.wantConfigFile, gotConfigFile) 296 297 gotRepoTags := img.RepoTags() 298 assert.Equal(t, tt.wantRepoTags, gotRepoTags) 299 300 gotRepoDigests := img.RepoDigests() 301 assert.Equal(t, tt.wantRepoDigests, gotRepoDigests) 302 }) 303 } 304 } 305 306 func setupPrivateRegistry() *httptest.Server { 307 imagePaths := map[string]string{ 308 "v2/library/alpine:3.10": "../test/testdata/alpine-310.tar.gz", 309 } 310 tr := registry.NewDockerRegistry(registry.Option{ 311 Images: imagePaths, 312 Auth: auth.Auth{ 313 User: "test", 314 Password: "testpass", 315 Secret: "secret", 316 }, 317 }) 318 319 return tr 320 } 321 322 func TestNewDockerImageWithPrivateRegistry(t *testing.T) { 323 tr := setupPrivateRegistry() 324 defer tr.Close() 325 326 serverAddr := tr.Listener.Addr().String() 327 328 token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 329 "iss": "testdocker", 330 }) 331 332 registryToken, err := token.SignedString([]byte("secret")) 333 require.NoError(t, err) 334 335 type args struct { 336 imageName string 337 option types.ImageOptions 338 } 339 tests := []struct { 340 name string 341 args args 342 want v1.Image 343 wantErr string 344 }{ 345 { 346 name: "happy path with private Docker Registry", 347 args: args{ 348 imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), 349 option: types.ImageOptions{ 350 RegistryOptions: types.RegistryOptions{ 351 Credentials: []types.Credential{ 352 { 353 Username: "test", 354 Password: "testpass", 355 }, 356 }, 357 Insecure: true, 358 }, 359 }, 360 }, 361 }, 362 { 363 name: "happy path with registry token", 364 args: args{ 365 imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), 366 option: types.ImageOptions{ 367 RegistryOptions: types.RegistryOptions{ 368 RegistryToken: registryToken, 369 Insecure: true, 370 }, 371 }, 372 }, 373 }, 374 { 375 name: "sad path without a credential", 376 args: args{ 377 imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr), 378 }, 379 wantErr: "unexpected status code 401", 380 }, 381 { 382 name: "sad path with invalid registry token", 383 args: args{ 384 imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr), 385 option: types.ImageOptions{ 386 RegistryOptions: types.RegistryOptions{ 387 RegistryToken: registryToken + "invalid", 388 Insecure: true, 389 }, 390 }, 391 }, 392 wantErr: "signature is invalid", 393 }, 394 } 395 for _, tt := range tests { 396 t.Run(tt.name, func(t *testing.T) { 397 tt.args.option.ImageSources = types.AllImageSources 398 _, cleanup, err := NewContainerImage(context.Background(), tt.args.imageName, tt.args.option) 399 defer cleanup() 400 401 if tt.wantErr != "" { 402 assert.NotNil(t, err) 403 assert.Contains(t, err.Error(), tt.wantErr, err) 404 } else { 405 assert.NoError(t, err) 406 } 407 }) 408 } 409 } 410 411 func TestNewArchiveImage(t *testing.T) { 412 type args struct { 413 fileName string 414 } 415 tests := []struct { 416 name string 417 args args 418 want v1.Image 419 wantErr string 420 }{ 421 { 422 name: "happy path", 423 args: args{ 424 fileName: "../test/testdata/alpine-310.tar.gz", 425 }, 426 }, 427 { 428 name: "happy path with OCI Image Format", 429 args: args{ 430 fileName: "../test/testdata/test.oci", 431 }, 432 }, 433 { 434 name: "happy path with OCI Image and tag Format", 435 args: args{ 436 fileName: "../test/testdata/test_image_tag.oci:0.0.1", 437 }, 438 }, 439 { 440 name: "happy path with OCI Image only", 441 args: args{ 442 fileName: "../test/testdata/test_image_tag.oci", 443 }, 444 }, 445 { 446 name: "sad path with OCI Image and invalid tagFormat", 447 args: args{ 448 fileName: "../test/testdata/test_image_tag.oci:0.0.0", 449 }, 450 wantErr: "invalid OCI image ref", 451 }, 452 { 453 name: "sad path, oci image not found", 454 args: args{ 455 fileName: "../test/testdata/invalid.tar.gz", 456 }, 457 wantErr: "unable to open", 458 }, 459 { 460 name: "sad path with OCI Image Format index.json directory", 461 args: args{ 462 fileName: "../test/testdata/test_index_json_dir.oci", 463 }, 464 wantErr: "unable to retrieve index.json", 465 }, 466 { 467 name: "sad path with OCI Image Format invalid index.json", 468 args: args{ 469 fileName: "../test/testdata/test_bad_index_json.oci", 470 }, 471 wantErr: "invalid index.json", 472 }, 473 { 474 name: "sad path with OCI Image Format no valid manifests", 475 args: args{ 476 fileName: "../test/testdata/test_no_valid_manifests.oci", 477 }, 478 wantErr: "no valid manifest", 479 }, 480 { 481 name: "sad path with OCI Image Format with invalid oci image digest", 482 args: args{ 483 fileName: "../test/testdata/test_invalid_oci_image.oci", 484 }, 485 wantErr: "invalid OCI image", 486 }, 487 } 488 for _, tt := range tests { 489 t.Run(tt.name, func(t *testing.T) { 490 img, err := NewArchiveImage(tt.args.fileName) 491 switch { 492 case tt.wantErr != "": 493 require.NotNil(t, err) 494 assert.Contains(t, err.Error(), tt.wantErr, tt.name) 495 return 496 default: 497 assert.NoError(t, err, tt.name) 498 } 499 500 // archive doesn't support RepoTags and RepoDigests 501 assert.Empty(t, img.RepoTags()) 502 assert.Empty(t, img.RepoDigests()) 503 }) 504 } 505 } 506 507 func TestDockerPlatformArguments(t *testing.T) { 508 tr := setupPrivateRegistry() 509 defer tr.Close() 510 511 serverAddr := tr.Listener.Addr().String() 512 513 type args struct { 514 option types.ImageOptions 515 } 516 tests := []struct { 517 name string 518 args args 519 want v1.Image 520 wantErr string 521 }{ 522 { 523 name: "happy path with valid platform", 524 args: args{ 525 option: types.ImageOptions{ 526 RegistryOptions: types.RegistryOptions{ 527 Credentials: []types.Credential{ 528 { 529 Username: "test", 530 Password: "testpass", 531 }, 532 }, 533 Insecure: true, 534 Platform: types.Platform{ 535 Platform: &v1.Platform{ 536 Architecture: "arm", 537 OS: "linux", 538 }, 539 }, 540 }, 541 }, 542 }, 543 }, 544 } 545 for _, tt := range tests { 546 t.Run(tt.name, func(t *testing.T) { 547 imageName := fmt.Sprintf("%s/library/alpine:3.10", serverAddr) 548 tt.args.option.ImageSources = types.AllImageSources 549 _, cleanup, err := NewContainerImage(context.Background(), imageName, tt.args.option) 550 defer cleanup() 551 552 if tt.wantErr != "" { 553 assert.ErrorContains(t, err, tt.wantErr, err) 554 } else { 555 assert.NoError(t, err) 556 } 557 }) 558 } 559 }