github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/fanal/test/integration/containerd_test.go (about)

     1  //go:build integration && linux
     2  
     3  package integration
     4  
     5  import (
     6  	"compress/gzip"
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/samber/lo"
    19  
    20  	"github.com/containerd/containerd"
    21  	"github.com/containerd/containerd/images"
    22  	"github.com/containerd/containerd/namespaces"
    23  	dockercontainer "github.com/docker/docker/api/types/container"
    24  	v1 "github.com/google/go-containerregistry/pkg/v1"
    25  	"github.com/stretchr/testify/assert"
    26  	"github.com/stretchr/testify/require"
    27  	"github.com/testcontainers/testcontainers-go"
    28  	"github.com/testcontainers/testcontainers-go/wait"
    29  
    30  	"github.com/devseccon/trivy/pkg/fanal/analyzer"
    31  	"github.com/devseccon/trivy/pkg/fanal/applier"
    32  	"github.com/devseccon/trivy/pkg/fanal/artifact"
    33  	aimage "github.com/devseccon/trivy/pkg/fanal/artifact/image"
    34  	"github.com/devseccon/trivy/pkg/fanal/cache"
    35  	"github.com/devseccon/trivy/pkg/fanal/image"
    36  	"github.com/devseccon/trivy/pkg/fanal/types"
    37  )
    38  
    39  func setupContainerd(t *testing.T, ctx context.Context, namespace string) *containerd.Client {
    40  	t.Helper()
    41  	tmpDir := t.TempDir()
    42  
    43  	containerdDir := filepath.Join(tmpDir, "containerd")
    44  	err := os.MkdirAll(containerdDir, os.ModePerm)
    45  	require.NoError(t, err)
    46  
    47  	socketPath := filepath.Join(containerdDir, "containerd.sock")
    48  
    49  	// Set a containerd socket
    50  	t.Setenv("CONTAINERD_ADDRESS", socketPath)
    51  	t.Setenv("CONTAINERD_NAMESPACE", namespace)
    52  
    53  	startContainerd(t, ctx, tmpDir)
    54  
    55  	// Retry up to 3 times until containerd is ready
    56  	var client *containerd.Client
    57  	iteration, _, err := lo.AttemptWhileWithDelay(3, 1*time.Second, func(int, time.Duration) (error, bool) {
    58  		client, err = containerd.New(socketPath)
    59  		if err != nil {
    60  			if !errors.Is(err, os.ErrPermission) {
    61  				return err, false // unexpected error
    62  			}
    63  			return err, true
    64  		}
    65  		t.Cleanup(func() {
    66  			assert.NoError(t, client.Close())
    67  		})
    68  		return nil, false
    69  	})
    70  	require.NoErrorf(t, err, "attempted %d times ", iteration)
    71  
    72  	return client
    73  }
    74  
    75  func startContainerd(t *testing.T, ctx context.Context, hostPath string) {
    76  	t.Helper()
    77  	t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
    78  	req := testcontainers.ContainerRequest{
    79  		Name:  "containerd",
    80  		Image: "ghcr.io/devseccon/trivy-test-images/containerd:latest",
    81  		Entrypoint: []string{
    82  			"/bin/sh",
    83  			"-c",
    84  			"/usr/local/bin/containerd",
    85  		},
    86  		Mounts: testcontainers.Mounts(
    87  			testcontainers.BindMount(hostPath, "/run"),
    88  		),
    89  		HostConfigModifier: func(hostConfig *dockercontainer.HostConfig) {
    90  			hostConfig.AutoRemove = true
    91  		},
    92  		WaitingFor: wait.ForLog("containerd successfully booted"),
    93  	}
    94  	containerdC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
    95  		ContainerRequest: req,
    96  		Started:          true,
    97  	})
    98  	require.NoError(t, err)
    99  
   100  	_, _, err = containerdC.Exec(ctx, []string{
   101  		"chmod",
   102  		"666",
   103  		"/run/containerd/containerd.sock",
   104  	})
   105  	require.NoError(t, err)
   106  
   107  	t.Cleanup(func() {
   108  		assert.NoError(t, containerdC.Terminate(ctx))
   109  	})
   110  }
   111  
   112  // Each of these tests imports an image and tags it with the name found in the
   113  // `imageName` field. Then, the containerd store is searched by the reference
   114  // provided in the `searchName` field.
   115  func TestContainerd_SearchLocalStoreByNameOrDigest(t *testing.T) {
   116  	// Each architecture needs different images and test cases.
   117  	// Currently only amd64 architecture is supported
   118  	if runtime.GOARCH != "amd64" {
   119  		t.Skip("'Containerd' test only supports amd64 architecture")
   120  	}
   121  
   122  	digest := "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a"
   123  	basename := "hello"
   124  	tag := "world"
   125  	importedImageOriginalName := "ghcr.io/devseccon/trivy-test-images:alpine-310"
   126  
   127  	tests := []struct {
   128  		name       string
   129  		imageName  string
   130  		searchName string
   131  		wantErr    bool
   132  	}{
   133  		{
   134  			name:       "familiarName:tag",
   135  			imageName:  fmt.Sprintf("%s:%s", basename, tag),
   136  			searchName: fmt.Sprintf("%s:%s", basename, tag),
   137  		},
   138  		{
   139  			name:       "compound/name:tag",
   140  			imageName:  fmt.Sprintf("say/%s:%s", basename, tag),
   141  			searchName: fmt.Sprintf("say/%s:%s", basename, tag),
   142  		},
   143  		{
   144  			name:       "docker.io/library/name:tag",
   145  			imageName:  fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
   146  			searchName: fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
   147  		},
   148  		{
   149  			name:       "other-registry.io/library/name:tag",
   150  			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
   151  			searchName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
   152  		},
   153  		{
   154  			name:       "other-registry.io/library/name:wrongTag should fail",
   155  			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
   156  			searchName: fmt.Sprintf("other-registry.io/library/%s:badtag", basename),
   157  			wantErr:    true,
   158  		},
   159  		{
   160  			name:       "other-registry.io/library/wrongName:tag should fail",
   161  			imageName:  fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag),
   162  			searchName: fmt.Sprintf("other-registry.io/library/badname:%s", tag),
   163  			wantErr:    true,
   164  		},
   165  		{
   166  			name:       "digest should succeed",
   167  			imageName:  "",
   168  			searchName: digest,
   169  		},
   170  		{
   171  			name:       "wrong digest should fail",
   172  			imageName:  "",
   173  			searchName: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
   174  			wantErr:    true,
   175  		},
   176  		{
   177  			name:       "name@digest",
   178  			imageName:  fmt.Sprintf("%s:%s", basename, tag),
   179  			searchName: fmt.Sprintf("hello@%s", digest),
   180  		},
   181  		{
   182  			name:       "compound/name@digest",
   183  			imageName:  fmt.Sprintf("compound/%s:%s", basename, tag),
   184  			searchName: fmt.Sprintf("compound/%s@%s", basename, digest),
   185  		},
   186  		{
   187  			name:       "docker.io/library/name@digest",
   188  			imageName:  fmt.Sprintf("docker.io/library/%s:%s", basename, tag),
   189  			searchName: fmt.Sprintf("docker.io/library/%s@%s", basename, digest),
   190  		},
   191  		{
   192  			name:       "otherdomain.io/name@digest",
   193  			imageName:  fmt.Sprintf("otherdomain.io/%s:%s", basename, tag),
   194  			searchName: fmt.Sprintf("otherdomain.io/%s@%s", basename, digest),
   195  		},
   196  		{
   197  			name:       "wrongName@digest should fail",
   198  			imageName:  fmt.Sprintf("%s:%s", basename, tag),
   199  			searchName: fmt.Sprintf("badname@%s", digest),
   200  			wantErr:    true,
   201  		},
   202  		{
   203  			name:       "compound/wrongName@digest should fail",
   204  			imageName:  fmt.Sprintf("compound/%s:%s", basename, tag),
   205  			searchName: fmt.Sprintf("compound/badname@%s", digest),
   206  			wantErr:    true,
   207  		},
   208  	}
   209  
   210  	namespace := "default"
   211  	ctx := namespaces.WithNamespace(context.Background(), namespace)
   212  	client := setupContainerd(t, ctx, namespace)
   213  
   214  	for _, tt := range tests {
   215  		t.Run(tt.name, func(t *testing.T) {
   216  			archive, err := os.Open("../../../../integration/testdata/fixtures/images/alpine-310.tar.gz")
   217  			require.NoError(t, err)
   218  
   219  			uncompressedArchive, err := gzip.NewReader(archive)
   220  			require.NoError(t, err)
   221  			defer archive.Close()
   222  
   223  			cacheDir := t.TempDir()
   224  			c, err := cache.NewFSCache(cacheDir)
   225  			require.NoError(t, err)
   226  
   227  			imgs, err := client.Import(ctx, uncompressedArchive)
   228  			require.NoError(t, err)
   229  			_ = imgs
   230  
   231  			digestTest := tt.imageName == ""
   232  
   233  			if !digestTest {
   234  				// Tag the image, taken from the code in `ctr image tag...`
   235  				imgs[0].Name = tt.imageName
   236  				_, err = client.ImageService().Create(ctx, imgs[0])
   237  				require.NoError(t, err)
   238  
   239  				// Remove the image by its original name, to ensure the image
   240  				// is known only by the tag we have given it.
   241  				err = client.ImageService().Delete(ctx, importedImageOriginalName, images.SynchronousDelete())
   242  				require.NoError(t, err)
   243  			}
   244  
   245  			t.Cleanup(func() {
   246  				for _, img := range imgs {
   247  					err = client.ImageService().Delete(ctx, img.Name, images.SynchronousDelete())
   248  					require.NoError(t, err)
   249  				}
   250  			})
   251  
   252  			// enable only containerd
   253  			img, cleanup, err := image.NewContainerImage(ctx, tt.searchName,
   254  				types.ImageOptions{ImageSources: types.ImageSources{types.ContainerdImageSource}})
   255  			defer cleanup()
   256  			if tt.wantErr {
   257  				require.Error(t, err)
   258  				return
   259  			}
   260  			require.NoError(t, err)
   261  
   262  			ar, err := aimage.NewArtifact(img, c, artifact.Option{})
   263  			require.NoError(t, err)
   264  
   265  			ref, err := ar.Inspect(ctx)
   266  			require.NoError(t, err)
   267  
   268  			if digestTest {
   269  				actualDigest := strings.Split(ref.ImageMetadata.RepoDigests[0], "@")[1]
   270  				require.Equal(t, tt.searchName, actualDigest)
   271  				return
   272  			}
   273  
   274  			require.Equal(t, tt.searchName, ref.Name)
   275  		})
   276  	}
   277  }
   278  
   279  func TestContainerd_LocalImage(t *testing.T) {
   280  	localImageTestWithNamespace(t, "default")
   281  }
   282  
   283  func TestContainerd_LocalImage_Alternative_Namespace(t *testing.T) {
   284  	localImageTestWithNamespace(t, "test")
   285  }
   286  
   287  func localImageTestWithNamespace(t *testing.T, namespace string) {
   288  	// Each architecture needs different images and test cases.
   289  	// Currently only amd64 architecture is supported
   290  	if runtime.GOARCH != "amd64" {
   291  		t.Skip("'Containerd' test only supports amd64 architecture")
   292  	}
   293  
   294  	tests := []struct {
   295  		name         string
   296  		imageName    string
   297  		tarArchive   string
   298  		wantMetadata types.ImageMetadata
   299  	}{
   300  		{
   301  			name:       "alpine 3.10",
   302  			imageName:  "ghcr.io/devseccon/trivy-test-images:alpine-310",
   303  			tarArchive: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz",
   304  			wantMetadata: types.ImageMetadata{
   305  				ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   306  				DiffIDs: []string{
   307  					"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   308  				},
   309  				RepoTags:    []string{"ghcr.io/devseccon/trivy-test-images:alpine-310"},
   310  				RepoDigests: []string{"ghcr.io/devseccon/trivy-test-images@sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a"},
   311  				ConfigFile: v1.ConfigFile{
   312  					Architecture: "amd64",
   313  					Created: v1.Time{
   314  						Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC),
   315  					},
   316  					OS: "linux",
   317  					RootFS: v1.RootFS{
   318  						Type: "layers",
   319  						DiffIDs: []v1.Hash{
   320  							{
   321  								Algorithm: "sha256",
   322  								Hex:       "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   323  							},
   324  						},
   325  					},
   326  					Config: v1.Config{
   327  						Cmd: []string{
   328  							"/bin/sh",
   329  						},
   330  						Env: []string{
   331  							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   332  						},
   333  					},
   334  					History: []v1.History{
   335  						{
   336  							Created:   v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)},
   337  							CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ",
   338  						},
   339  						{
   340  							Created:    v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)},
   341  							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
   342  							EmptyLayer: true,
   343  						},
   344  					},
   345  				},
   346  			},
   347  		},
   348  		{
   349  			name:       "vulnimage",
   350  			imageName:  "ghcr.io/devseccon/trivy-test-images:vulnimage",
   351  			tarArchive: "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz",
   352  			wantMetadata: types.ImageMetadata{
   353  				ID: "sha256:c17083664da903e13e9092fa3a3a1aeee2431aa2728298e3dbcec72f26369c41",
   354  				DiffIDs: []string{
   355  					"sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
   356  					"sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
   357  					"sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
   358  					"sha256:dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1",
   359  					"sha256:5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0",
   360  					"sha256:9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6",
   361  					"sha256:6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013",
   362  					"sha256:83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63",
   363  					"sha256:c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227",
   364  					"sha256:2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4",
   365  					"sha256:82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34",
   366  					"sha256:2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708",
   367  					"sha256:6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36",
   368  					"sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812",
   369  					"sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
   370  					"sha256:4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9",
   371  					"sha256:9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60",
   372  					"sha256:a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb",
   373  					"sha256:ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da",
   374  					"sha256:6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c",
   375  				},
   376  				RepoTags:    []string{"ghcr.io/devseccon/trivy-test-images:vulnimage"},
   377  				RepoDigests: []string{"ghcr.io/devseccon/trivy-test-images@sha256:e74abbfd81e00baaf464cf9e09f8b24926e5255171e3150a60aa341ce064688f"},
   378  				ConfigFile: v1.ConfigFile{
   379  					Architecture: "amd64",
   380  					Created: v1.Time{
   381  						Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC),
   382  					},
   383  					OS: "linux",
   384  					RootFS: v1.RootFS{
   385  						Type: "layers",
   386  						DiffIDs: []v1.Hash{
   387  							{
   388  								Algorithm: "sha256",
   389  								Hex:       "ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888",
   390  							},
   391  							{
   392  								Algorithm: "sha256",
   393  								Hex:       "0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33",
   394  							},
   395  							{
   396  								Algorithm: "sha256",
   397  								Hex:       "9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303",
   398  							},
   399  							{
   400  								Algorithm: "sha256",
   401  								Hex:       "dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1",
   402  							},
   403  							{
   404  								Algorithm: "sha256",
   405  								Hex:       "5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0",
   406  							},
   407  							{
   408  								Algorithm: "sha256",
   409  								Hex:       "9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6",
   410  							},
   411  							{
   412  								Algorithm: "sha256",
   413  								Hex:       "6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013",
   414  							},
   415  							{
   416  								Algorithm: "sha256",
   417  								Hex:       "83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63",
   418  							},
   419  							{
   420  								Algorithm: "sha256",
   421  								Hex:       "c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227",
   422  							},
   423  							{
   424  								Algorithm: "sha256",
   425  								Hex:       "2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4",
   426  							},
   427  							{
   428  								Algorithm: "sha256",
   429  								Hex:       "82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34",
   430  							},
   431  							{
   432  								Algorithm: "sha256",
   433  								Hex:       "2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708",
   434  							},
   435  							{
   436  								Algorithm: "sha256",
   437  								Hex:       "6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36",
   438  							},
   439  							{
   440  								Algorithm: "sha256",
   441  								Hex:       "154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812",
   442  							},
   443  							{
   444  								Algorithm: "sha256",
   445  								Hex:       "b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079",
   446  							},
   447  							{
   448  								Algorithm: "sha256",
   449  								Hex:       "4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9",
   450  							},
   451  							{
   452  								Algorithm: "sha256",
   453  								Hex:       "9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60",
   454  							},
   455  							{
   456  								Algorithm: "sha256",
   457  								Hex:       "a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb",
   458  							},
   459  							{
   460  								Algorithm: "sha256",
   461  								Hex:       "ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da",
   462  							},
   463  							{
   464  								Algorithm: "sha256",
   465  								Hex:       "6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c",
   466  							},
   467  						},
   468  					},
   469  					Config: v1.Config{
   470  						Cmd: []string{"composer"},
   471  						Env: []string{
   472  							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   473  							"PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c",
   474  							"PHP_INI_DIR=/usr/local/etc/php",
   475  							"PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2",
   476  							"PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2",
   477  							"PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie",
   478  							"GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F",
   479  							"PHP_VERSION=7.2.11",
   480  							"PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror",
   481  							"PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror",
   482  							"PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985",
   483  							"PHP_MD5=",
   484  							"COMPOSER_ALLOW_SUPERUSER=1",
   485  							"COMPOSER_HOME=/tmp",
   486  							"COMPOSER_VERSION=1.7.2",
   487  						},
   488  						WorkingDir: "/app",
   489  						Entrypoint: []string{
   490  							"/bin/sh",
   491  							"/docker-entrypoint.sh",
   492  						},
   493  					},
   494  					History: []v1.History{
   495  						{
   496  							Created:   v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 38, 885299940, time.UTC)},
   497  							CreatedBy: "/bin/sh -c #(nop) ADD file:49f9e47e678d868d5b023482aa8dded71276a241a665c4f8b55ca77269321b34 in / ",
   498  						},
   499  						{
   500  							Created:    v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 39, 58628442, time.UTC)},
   501  							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
   502  							EmptyLayer: true,
   503  						},
   504  						{
   505  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 26, 59, 951316015, time.UTC)},
   506  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c",
   507  							EmptyLayer: true,
   508  						},
   509  						{
   510  							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 1, 470388635, time.UTC)},
   511  							CreatedBy: "/bin/sh -c apk add --no-cache --virtual .persistent-deps \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\tlibressl",
   512  						},
   513  						{
   514  							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 432381785, time.UTC)},
   515  							CreatedBy: "/bin/sh -c set -x \t&& addgroup -g 82 -S www-data \t&& adduser -u 82 -D -S -G www-data www-data",
   516  						},
   517  						{
   518  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 715120309, time.UTC)},
   519  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_INI_DIR=/usr/local/etc/php",
   520  							EmptyLayer: true,
   521  						},
   522  						{
   523  							Created:   v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 655421341, time.UTC)},
   524  							CreatedBy: "/bin/sh -c mkdir -p $PHP_INI_DIR/conf.d",
   525  						},
   526  						{
   527  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 931799562, time.UTC)},
   528  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2",
   529  							EmptyLayer: true,
   530  						},
   531  						{
   532  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 210945499, time.UTC)},
   533  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2",
   534  							EmptyLayer: true,
   535  						},
   536  						{
   537  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 523116501, time.UTC)},
   538  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie",
   539  							EmptyLayer: true,
   540  						},
   541  						{
   542  							Created:    v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 795176159, time.UTC)},
   543  							CreatedBy:  "/bin/sh -c #(nop)  ENV GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F",
   544  							EmptyLayer: true,
   545  						},
   546  						{
   547  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 415761689, time.UTC)},
   548  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_VERSION=7.2.11",
   549  							EmptyLayer: true,
   550  						},
   551  						{
   552  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 599097853, time.UTC)},
   553  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror",
   554  							EmptyLayer: true,
   555  						},
   556  						{
   557  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 782890412, time.UTC)},
   558  							CreatedBy:  "/bin/sh -c #(nop)  ENV PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985 PHP_MD5=",
   559  							EmptyLayer: true,
   560  						},
   561  						{
   562  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 22, 795846753, time.UTC)},
   563  							CreatedBy: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del .fetch-deps",
   564  						},
   565  						{
   566  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 23, 71406376, time.UTC)},
   567  							CreatedBy: "/bin/sh -c #(nop) COPY file:207c686e3fed4f71f8a7b245d8dcae9c9048d276a326d82b553c12a90af0c0ca in /usr/local/bin/ ",
   568  						},
   569  						{
   570  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 93396680, time.UTC)},
   571  							CreatedBy: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibressl-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\tsqlite-dev \t\t&& export CFLAGS=\"$PHP_CFLAGS\" \t\tCPPFLAGS=\"$PHP_CPPFLAGS\" \t\tLDFLAGS=\"$PHP_LDFLAGS\" \t&& docker-php-source extract \t&& cd /usr/src/php \t&& gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\" \t&& ./configure \t\t--build=\"$gnuArch\" \t\t--with-config-file-path=\"$PHP_INI_DIR\" \t\t--with-config-file-scan-dir=\"$PHP_INI_DIR/conf.d\" \t\t\t\t--enable-option-checking=fatal \t\t\t\t--with-mhash \t\t\t\t--enable-ftp \t\t--enable-mbstring \t\t--enable-mysqlnd \t\t--with-sodium=shared \t\t\t\t--with-curl \t\t--with-libedit \t\t--with-openssl \t\t--with-zlib \t\t\t\t$(test \"$gnuArch\" = 's390x-linux-gnu' && echo '--without-pcre-jit') \t\t\t\t$PHP_EXTRA_CONFIGURE_ARGS \t&& make -j \"$(nproc)\" \t&& make install \t&& { find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; } \t&& make clean \t\t&& cp -v php.ini-* \"$PHP_INI_DIR/\" \t\t&& cd / \t&& docker-php-source delete \t\t&& runDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\" \t&& apk add --no-cache --virtual .php-rundeps $runDeps \t\t&& apk del .build-deps \t\t&& pecl update-channels \t&& rm -rf /tmp/pear ~/.pearrc",
   572  						},
   573  						{
   574  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 722586262, time.UTC)},
   575  							CreatedBy: "/bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9ae610fab7848e94bc2f64b4d85710d55fd6f79e44dacf73d8 in /usr/local/bin/ ",
   576  						},
   577  						{
   578  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 618087104, time.UTC)},
   579  							CreatedBy: "/bin/sh -c docker-php-ext-enable sodium",
   580  						},
   581  						{
   582  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 826981756, time.UTC)},
   583  							CreatedBy:  "/bin/sh -c #(nop)  ENTRYPOINT [\"docker-php-entrypoint\"]",
   584  							EmptyLayer: true,
   585  						},
   586  						{
   587  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 15, 10831572, time.UTC)},
   588  							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"php\" \"-a\"]",
   589  							EmptyLayer: true,
   590  						},
   591  						{
   592  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 21, 919735971, time.UTC)},
   593  							CreatedBy: "/bin/sh -c apk --no-cache add git subversion openssh mercurial tini bash patch",
   594  						},
   595  						{
   596  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 22, 611763893, time.UTC)},
   597  							CreatedBy: "/bin/sh -c echo \"memory_limit=-1\" > \"$PHP_INI_DIR/conf.d/memory-limit.ini\"  && echo \"date.timezone=${PHP_TIMEZONE:-UTC}\" > \"$PHP_INI_DIR/conf.d/date_timezone.ini\"",
   598  						},
   599  						{
   600  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 224278478, time.UTC)},
   601  							CreatedBy: "/bin/sh -c apk add --no-cache --virtual .build-deps zlib-dev  && docker-php-ext-install zip  && runDeps=\"$(     scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions     | tr ',' '\\n'     | sort -u     | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }'     )\"  && apk add --virtual .composer-phpext-rundeps $runDeps  && apk del .build-deps",
   602  						},
   603  						{
   604  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 503010161, time.UTC)},
   605  							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_ALLOW_SUPERUSER=1",
   606  							EmptyLayer: true,
   607  						},
   608  						{
   609  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 775378559, time.UTC)},
   610  							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_HOME=/tmp",
   611  							EmptyLayer: true,
   612  						},
   613  						{
   614  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 51, 35012363, time.UTC)},
   615  							CreatedBy:  "/bin/sh -c #(nop)  ENV COMPOSER_VERSION=1.7.2",
   616  							EmptyLayer: true,
   617  						},
   618  						{
   619  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 491402624, time.UTC)},
   620  							CreatedBy: "/bin/sh -c curl --silent --fail --location --retry 3 --output /tmp/installer.php --url https://raw.githubusercontent.com/composer/getcomposer.org/b107d959a5924af895807021fcef4ffec5a76aa9/web/installer  && php -r \"     \\$signature = '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061';     \\$hash = hash('SHA384', file_get_contents('/tmp/installer.php'));     if (!hash_equals(\\$signature, \\$hash)) {         unlink('/tmp/installer.php');         echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL;         exit(1);     }\"  && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer --version=${COMPOSER_VERSION}  && composer --ansi --version --no-interaction  && rm -rf /tmp/* /tmp/.htaccess",
   621  						},
   622  						{
   623  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 948859545, time.UTC)},
   624  							CreatedBy: "/bin/sh -c #(nop) COPY file:295943a303e8f27de4302b6aa3687bce4b1d1392335efaaab9ecd37bec5ab4c5 in /docker-entrypoint.sh ",
   625  						},
   626  						{
   627  							Created:   v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 295399872, time.UTC)},
   628  							CreatedBy: "/bin/sh -c #(nop) WORKDIR /app",
   629  						},
   630  						{
   631  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 582920705, time.UTC)},
   632  							CreatedBy:  "/bin/sh -c #(nop)  ENTRYPOINT [\"/bin/sh\" \"/docker-entrypoint.sh\"]",
   633  							EmptyLayer: true,
   634  						},
   635  						{
   636  							Created:    v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 798628678, time.UTC)},
   637  							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"composer\"]",
   638  							EmptyLayer: true,
   639  						},
   640  						{
   641  							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 211142800, time.UTC)},
   642  							CreatedBy: "/bin/sh -c #(nop) ADD file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ",
   643  						},
   644  						{
   645  							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 583779000, time.UTC)},
   646  							CreatedBy: "/bin/sh -c #(nop) ADD file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ",
   647  						},
   648  						{
   649  							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 921730100, time.UTC)},
   650  							CreatedBy: "/bin/sh -c #(nop) ADD file:54a1c52556a5ebe98fd124f51c25d071f9e29e2714c72c80d6d3d254b9e83386 in /node-app/package-lock.json ",
   651  						},
   652  						{
   653  							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 311593100, time.UTC)},
   654  							CreatedBy: "/bin/sh -c #(nop) ADD file:097d32f46acde76c4da9e55f17110d69d02cc6d16c86da907980da335fc0fc5f in /python-app/Pipfile.lock ",
   655  						},
   656  						{
   657  							Created:   v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC)},
   658  							CreatedBy: "/bin/sh -c #(nop) ADD file:7f147d85de19bfb905c260a0c175f227a433259022c163017b96d0efacdcd105 in /rust-app/Cargo.lock ",
   659  						},
   660  					},
   661  				},
   662  			},
   663  		},
   664  	}
   665  
   666  	t.Helper()
   667  	ctx := namespaces.WithNamespace(context.Background(), namespace)
   668  	client := setupContainerd(t, ctx, namespace)
   669  
   670  	for _, tt := range tests {
   671  		t.Run(tt.name, func(t *testing.T) {
   672  			cacheDir := t.TempDir()
   673  			c, err := cache.NewFSCache(cacheDir)
   674  			require.NoError(t, err)
   675  
   676  			defer func() {
   677  				c.Clear()
   678  				c.Close()
   679  			}()
   680  
   681  			archive, err := os.Open(tt.tarArchive)
   682  			require.NoError(t, err)
   683  
   684  			uncompressedArchive, err := gzip.NewReader(archive)
   685  			require.NoError(t, err)
   686  			defer archive.Close()
   687  
   688  			_, err = client.Import(ctx, uncompressedArchive)
   689  			require.NoError(t, err)
   690  
   691  			// Enable only containerd
   692  			img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{
   693  				ImageSources: types.ImageSources{types.ContainerdImageSource},
   694  			})
   695  			require.NoError(t, err)
   696  			defer cleanup()
   697  
   698  			ar, err := aimage.NewArtifact(img, c, artifact.Option{
   699  				DisabledAnalyzers: []analyzer.Type{
   700  					analyzer.TypeExecutable,
   701  					analyzer.TypeLicenseFile,
   702  				},
   703  			})
   704  			require.NoError(t, err)
   705  
   706  			ref, err := ar.Inspect(ctx)
   707  			require.NoError(t, err)
   708  			require.Equal(t, tt.wantMetadata, ref.ImageMetadata)
   709  
   710  			a := applier.NewApplier(c)
   711  			got, err := a.ApplyLayers(ref.ID, ref.BlobIDs)
   712  			require.NoError(t, err)
   713  
   714  			tag := strings.Split(tt.imageName, ":")[1]
   715  			goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag)
   716  
   717  			if *update {
   718  				b, err := json.MarshalIndent(got.Packages, "", "  ")
   719  				require.NoError(t, err)
   720  				err = os.WriteFile(goldenFile, b, 0666)
   721  				require.NoError(t, err)
   722  			}
   723  
   724  			// Parse a golden file
   725  			golden, err := os.Open(goldenFile)
   726  			require.NoError(t, err)
   727  			defer golden.Close()
   728  
   729  			var wantPkgs types.Packages
   730  			err = json.NewDecoder(golden).Decode(&wantPkgs)
   731  			require.NoError(t, err)
   732  
   733  			// Assert
   734  			assert.Equal(t, wantPkgs, got.Packages)
   735  		})
   736  	}
   737  }
   738  
   739  func TestContainerd_PullImage(t *testing.T) {
   740  	// Each architecture needs different images and test cases.
   741  	// Currently only amd64 architecture is supported
   742  	if runtime.GOARCH != "amd64" {
   743  		t.Skip("'Containerd' test only supports amd64 architecture")
   744  	}
   745  
   746  	tests := []struct {
   747  		name         string
   748  		imageName    string
   749  		wantMetadata types.ImageMetadata
   750  	}{
   751  		{
   752  			name:      "remote alpine 3.10",
   753  			imageName: "ghcr.io/devseccon/trivy-test-images:alpine-310",
   754  			wantMetadata: types.ImageMetadata{
   755  				ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4",
   756  				DiffIDs: []string{
   757  					"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   758  				},
   759  				RepoTags:    []string{"ghcr.io/devseccon/trivy-test-images:alpine-310"},
   760  				RepoDigests: []string{"ghcr.io/devseccon/trivy-test-images@sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb"},
   761  				ConfigFile: v1.ConfigFile{
   762  					Architecture: "amd64",
   763  					Created: v1.Time{
   764  						Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC),
   765  					},
   766  					OS: "linux",
   767  					RootFS: v1.RootFS{
   768  						Type: "layers",
   769  						DiffIDs: []v1.Hash{
   770  							{
   771  								Algorithm: "sha256",
   772  								Hex:       "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0",
   773  							},
   774  						},
   775  					},
   776  					Config: v1.Config{
   777  						Cmd: []string{
   778  							"/bin/sh",
   779  						},
   780  						Env: []string{
   781  							"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
   782  						},
   783  						ArgsEscaped: false,
   784  					},
   785  					History: []v1.History{
   786  						{
   787  							Created:   v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)},
   788  							CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ",
   789  						},
   790  						{
   791  							Created:    v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)},
   792  							CreatedBy:  "/bin/sh -c #(nop)  CMD [\"/bin/sh\"]",
   793  							EmptyLayer: true,
   794  						},
   795  					},
   796  				},
   797  			},
   798  		},
   799  	}
   800  
   801  	namespace := "default"
   802  	ctx := namespaces.WithNamespace(context.Background(), namespace)
   803  	client := setupContainerd(t, ctx, namespace)
   804  
   805  	for _, tt := range tests {
   806  		t.Run(tt.name, func(t *testing.T) {
   807  			cacheDir := t.TempDir()
   808  			c, err := cache.NewFSCache(cacheDir)
   809  			require.NoError(t, err)
   810  
   811  			defer func() {
   812  				c.Clear()
   813  				c.Close()
   814  			}()
   815  
   816  			_, err = client.Pull(ctx, tt.imageName)
   817  			require.NoError(t, err)
   818  
   819  			// Enable only containerd
   820  			img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{
   821  				ImageSources: types.ImageSources{types.ContainerdImageSource},
   822  			})
   823  			require.NoError(t, err)
   824  			defer cleanup()
   825  
   826  			art, err := aimage.NewArtifact(img, c, artifact.Option{
   827  				DisabledAnalyzers: []analyzer.Type{
   828  					analyzer.TypeExecutable,
   829  					analyzer.TypeLicenseFile,
   830  				},
   831  			})
   832  			require.NoError(t, err)
   833  			require.NotNil(t, art)
   834  
   835  			ref, err := art.Inspect(context.Background())
   836  			require.NoError(t, err)
   837  			require.Equal(t, tt.wantMetadata, ref.ImageMetadata)
   838  
   839  			a := applier.NewApplier(c)
   840  			got, err := a.ApplyLayers(ref.ID, ref.BlobIDs)
   841  			require.NoError(t, err)
   842  
   843  			// Parse a golden file
   844  			tag := strings.Split(tt.imageName, ":")[1]
   845  			golden, err := os.Open(fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag))
   846  			require.NoError(t, err)
   847  
   848  			var wantPkgs types.Packages
   849  			err = json.NewDecoder(golden).Decode(&wantPkgs)
   850  			require.NoError(t, err)
   851  
   852  			// Assert
   853  			assert.Equal(t, wantPkgs, got.Packages)
   854  		})
   855  	}
   856  }