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  }