github.com/lineaje-labs/syft@v0.98.1-0.20231227153149-9e393f60ff1b/syft/internal/fileresolver/container_image_all_layers_test.go (about)

     1  package fileresolver
     2  
     3  import (
     4  	"io"
     5  	"sort"
     6  	"testing"
     7  
     8  	"github.com/google/go-cmp/cmp"
     9  	"github.com/scylladb/go-set/strset"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/anchore/stereoscope/pkg/imagetest"
    14  	"github.com/anchore/syft/syft/file"
    15  )
    16  
    17  type resolution struct {
    18  	layer uint
    19  	path  string
    20  }
    21  
    22  func TestAllLayersResolver_FilesByPath(t *testing.T) {
    23  	cases := []struct {
    24  		name                 string
    25  		linkPath             string
    26  		resolutions          []resolution
    27  		forcePositiveHasPath bool
    28  	}{
    29  		{
    30  			name:     "link with previous data",
    31  			linkPath: "/link-1",
    32  			resolutions: []resolution{
    33  				{
    34  					layer: 1,
    35  					path:  "/file-1.txt",
    36  				},
    37  			},
    38  		},
    39  		{
    40  			name:     "link with in layer data",
    41  			linkPath: "/link-within",
    42  			resolutions: []resolution{
    43  				{
    44  					layer: 5,
    45  					path:  "/file-3.txt",
    46  				},
    47  			},
    48  		},
    49  		{
    50  			name:     "link with overridden data",
    51  			linkPath: "/link-2",
    52  			resolutions: []resolution{
    53  				{
    54  					layer: 4,
    55  					path:  "/file-2.txt",
    56  				},
    57  				{
    58  					layer: 7,
    59  					path:  "/file-2.txt",
    60  				},
    61  			},
    62  		},
    63  		{
    64  			name:     "indirect link (with overridden data)",
    65  			linkPath: "/link-indirect",
    66  			resolutions: []resolution{
    67  				{
    68  					layer: 4,
    69  					path:  "/file-2.txt",
    70  				},
    71  				{
    72  					layer: 7,
    73  					path:  "/file-2.txt",
    74  				},
    75  			},
    76  		},
    77  		{
    78  			name:                 "dead link",
    79  			linkPath:             "/link-dead",
    80  			resolutions:          []resolution{},
    81  			forcePositiveHasPath: true,
    82  		},
    83  		{
    84  			name:        "ignore directories",
    85  			linkPath:    "/bin",
    86  			resolutions: []resolution{},
    87  			// directories don't resolve BUT do exist
    88  			forcePositiveHasPath: true,
    89  		},
    90  	}
    91  	for _, c := range cases {
    92  		t.Run(c.name, func(t *testing.T) {
    93  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
    94  
    95  			resolver, err := NewFromContainerImageAllLayers(img)
    96  			require.NoError(t, err)
    97  
    98  			hasPath := resolver.HasPath(c.linkPath)
    99  			if !c.forcePositiveHasPath {
   100  				if len(c.resolutions) > 0 && !hasPath {
   101  					t.Errorf("expected HasPath() to indicate existance, but did not")
   102  				} else if len(c.resolutions) == 0 && hasPath {
   103  					t.Errorf("expeced HasPath() to NOT indicate existance, but does")
   104  				}
   105  			} else if !hasPath {
   106  				t.Errorf("expected HasPath() to indicate existance, but did not (force path)")
   107  			}
   108  
   109  			refs, err := resolver.FilesByPath(c.linkPath)
   110  			require.NoError(t, err)
   111  
   112  			if len(refs) != len(c.resolutions) {
   113  				t.Fatalf("unexpected number of resolutions: %d", len(refs))
   114  			}
   115  
   116  			for idx, actual := range refs {
   117  				expected := c.resolutions[idx]
   118  
   119  				if string(actual.Reference().RealPath) != expected.path {
   120  					t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path)
   121  				}
   122  
   123  				if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath {
   124  					t.Errorf("we should always prefer real paths over ones with links")
   125  				}
   126  
   127  				layer := img.FileCatalog.Layer(actual.Reference())
   128  				if layer.Metadata.Index != expected.layer {
   129  					t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer)
   130  				}
   131  			}
   132  		})
   133  	}
   134  }
   135  
   136  func TestAllLayersResolver_FilesByGlob(t *testing.T) {
   137  	cases := []struct {
   138  		name        string
   139  		glob        string
   140  		resolutions []resolution
   141  	}{
   142  		{
   143  			name: "link with previous data",
   144  			glob: "**/*ink-1",
   145  			resolutions: []resolution{
   146  				{
   147  					layer: 1,
   148  					path:  "/file-1.txt",
   149  				},
   150  			},
   151  		},
   152  		{
   153  			name: "link with in layer data",
   154  			glob: "**/*nk-within",
   155  			resolutions: []resolution{
   156  				{
   157  					layer: 5,
   158  					path:  "/file-3.txt",
   159  				},
   160  			},
   161  		},
   162  		{
   163  			name: "link with overridden data",
   164  			glob: "**/*ink-2",
   165  			resolutions: []resolution{
   166  				{
   167  					layer: 4,
   168  					path:  "/file-2.txt",
   169  				},
   170  				{
   171  					layer: 7,
   172  					path:  "/file-2.txt",
   173  				},
   174  			},
   175  		},
   176  		{
   177  			name: "indirect link (with overridden data)",
   178  			glob: "**/*nk-indirect",
   179  			resolutions: []resolution{
   180  				{
   181  					layer: 4,
   182  					path:  "/file-2.txt",
   183  				},
   184  				{
   185  					layer: 7,
   186  					path:  "/file-2.txt",
   187  				},
   188  			},
   189  		},
   190  		{
   191  			name:        "dead link",
   192  			glob:        "**/*k-dead",
   193  			resolutions: []resolution{},
   194  		},
   195  		{
   196  			name:        "ignore directories",
   197  			glob:        "**/bin",
   198  			resolutions: []resolution{},
   199  		},
   200  	}
   201  	for _, c := range cases {
   202  		t.Run(c.name, func(t *testing.T) {
   203  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   204  
   205  			resolver, err := NewFromContainerImageAllLayers(img)
   206  			require.NoError(t, err)
   207  
   208  			refs, err := resolver.FilesByGlob(c.glob)
   209  			require.NoError(t, err)
   210  
   211  			if len(refs) != len(c.resolutions) {
   212  				t.Fatalf("unexpected number of resolutions: %d", len(refs))
   213  			}
   214  
   215  			for idx, actual := range refs {
   216  				expected := c.resolutions[idx]
   217  
   218  				if string(actual.Reference().RealPath) != expected.path {
   219  					t.Errorf("bad resolve path: '%s'!='%s'", string(actual.Reference().RealPath), expected.path)
   220  				}
   221  
   222  				if expected.path != "" && string(actual.Reference().RealPath) != actual.RealPath {
   223  					t.Errorf("we should always prefer real paths over ones with links")
   224  				}
   225  
   226  				layer := img.FileCatalog.Layer(actual.Reference())
   227  
   228  				if layer.Metadata.Index != expected.layer {
   229  					t.Errorf("bad resolve layer: '%d'!='%d'", layer.Metadata.Index, expected.layer)
   230  				}
   231  			}
   232  		})
   233  	}
   234  }
   235  
   236  func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) {
   237  
   238  	tests := []struct {
   239  		fixtureName   string
   240  		mimeType      string
   241  		expectedPaths []string
   242  	}{
   243  		{
   244  			fixtureName:   "image-duplicate-path",
   245  			mimeType:      "text/plain",
   246  			expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"},
   247  		},
   248  	}
   249  	for _, test := range tests {
   250  		t.Run(test.fixtureName, func(t *testing.T) {
   251  			img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName)
   252  
   253  			resolver, err := NewFromContainerImageAllLayers(img)
   254  			assert.NoError(t, err)
   255  
   256  			locations, err := resolver.FilesByMIMEType(test.mimeType)
   257  			assert.NoError(t, err)
   258  
   259  			assert.Len(t, test.expectedPaths, len(locations))
   260  			for idx, l := range locations {
   261  				assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath)
   262  			}
   263  		})
   264  	}
   265  }
   266  
   267  func Test_imageAllLayersResolver_hasFilesystemIDInLocation(t *testing.T) {
   268  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-duplicate-path")
   269  
   270  	resolver, err := NewFromContainerImageAllLayers(img)
   271  	assert.NoError(t, err)
   272  
   273  	locations, err := resolver.FilesByMIMEType("text/plain")
   274  	assert.NoError(t, err)
   275  	assert.NotEmpty(t, locations)
   276  	for _, location := range locations {
   277  		assert.NotEmpty(t, location.FileSystemID)
   278  	}
   279  
   280  	locations, err = resolver.FilesByGlob("*.txt")
   281  	assert.NoError(t, err)
   282  	assert.NotEmpty(t, locations)
   283  	for _, location := range locations {
   284  		assert.NotEmpty(t, location.FileSystemID)
   285  	}
   286  
   287  	locations, err = resolver.FilesByPath("/somefile-1.txt")
   288  	assert.NoError(t, err)
   289  	assert.NotEmpty(t, locations)
   290  	for _, location := range locations {
   291  		assert.NotEmpty(t, location.FileSystemID)
   292  	}
   293  
   294  }
   295  
   296  func TestAllLayersImageResolver_FilesContents(t *testing.T) {
   297  
   298  	tests := []struct {
   299  		name     string
   300  		fixture  string
   301  		contents []string
   302  	}{
   303  		{
   304  			name:    "one degree",
   305  			fixture: "link-2",
   306  			contents: []string{
   307  				"file 2!",            // from the first resolved layer's perspective
   308  				"NEW file override!", // from the second resolved layers perspective
   309  			},
   310  		},
   311  		{
   312  			name:    "two degrees",
   313  			fixture: "link-indirect",
   314  			contents: []string{
   315  				"file 2!",
   316  				"NEW file override!",
   317  			},
   318  		},
   319  		{
   320  			name:     "dead link",
   321  			fixture:  "link-dead",
   322  			contents: []string{},
   323  		},
   324  	}
   325  
   326  	for _, test := range tests {
   327  		t.Run(test.name, func(t *testing.T) {
   328  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   329  
   330  			resolver, err := NewFromContainerImageAllLayers(img)
   331  			assert.NoError(t, err)
   332  
   333  			refs, err := resolver.FilesByPath(test.fixture)
   334  			require.NoError(t, err)
   335  
   336  			// the given path should have an overridden file
   337  			require.Len(t, refs, len(test.contents))
   338  
   339  			for idx, loc := range refs {
   340  				reader, err := resolver.FileContentsByLocation(loc)
   341  				require.NoError(t, err)
   342  
   343  				actual, err := io.ReadAll(reader)
   344  				require.NoError(t, err)
   345  
   346  				assert.Equal(t, test.contents[idx], string(actual))
   347  			}
   348  
   349  		})
   350  	}
   351  }
   352  
   353  func TestAllLayersImageResolver_FilesContents_errorOnDirRequest(t *testing.T) {
   354  
   355  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   356  
   357  	resolver, err := NewFromContainerImageAllLayers(img)
   358  	assert.NoError(t, err)
   359  
   360  	var dirLoc *file.Location
   361  	for loc := range resolver.AllLocations() {
   362  		entry, err := resolver.img.FileCatalog.Get(loc.Reference())
   363  		require.NoError(t, err)
   364  		if entry.Metadata.IsDir() {
   365  			dirLoc = &loc
   366  			break
   367  		}
   368  	}
   369  
   370  	require.NotNil(t, dirLoc)
   371  
   372  	reader, err := resolver.FileContentsByLocation(*dirLoc)
   373  	require.Error(t, err)
   374  	require.Nil(t, reader)
   375  }
   376  
   377  func Test_imageAllLayersResolver_resolvesLinks(t *testing.T) {
   378  	tests := []struct {
   379  		name     string
   380  		runner   func(file.Resolver) []file.Location
   381  		expected []file.Location
   382  	}{
   383  		{
   384  			name: "by mimetype",
   385  			runner: func(resolver file.Resolver) []file.Location {
   386  				// links should not show up when searching mimetype
   387  				actualLocations, err := resolver.FilesByMIMEType("text/plain")
   388  				assert.NoError(t, err)
   389  				return actualLocations
   390  			},
   391  			expected: []file.Location{
   392  				file.NewVirtualLocation("/etc/group", "/etc/group"),
   393  				file.NewVirtualLocation("/etc/passwd", "/etc/passwd"),
   394  				file.NewVirtualLocation("/etc/shadow", "/etc/shadow"),
   395  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   396  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   397  				// note: we're de-duping the redundant access to file-3.txt
   398  				// ... (there would usually be two copies)
   399  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   400  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"),               // copy 2
   401  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 1
   402  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // copy 2
   403  			},
   404  		},
   405  		{
   406  			name: "by glob to links",
   407  			runner: func(resolver file.Resolver) []file.Location {
   408  				// links are searched, but resolve to the real files
   409  				actualLocations, err := resolver.FilesByGlob("*ink-*")
   410  				assert.NoError(t, err)
   411  				return actualLocations
   412  			},
   413  			expected: []file.Location{
   414  				file.NewVirtualLocation("/file-1.txt", "/link-1"),
   415  				file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 1
   416  				file.NewVirtualLocation("/file-2.txt", "/link-2"), // copy 2
   417  				file.NewVirtualLocation("/file-3.txt", "/link-within"),
   418  			},
   419  		},
   420  		{
   421  			name: "by basename",
   422  			runner: func(resolver file.Resolver) []file.Location {
   423  				// links are searched, but resolve to the real files
   424  				actualLocations, err := resolver.FilesByGlob("**/file-2.txt")
   425  				assert.NoError(t, err)
   426  				return actualLocations
   427  			},
   428  			expected: []file.Location{
   429  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   430  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   431  			},
   432  		},
   433  		{
   434  			name: "by basename glob",
   435  			runner: func(resolver file.Resolver) []file.Location {
   436  				// links are searched, but resolve to the real files
   437  				actualLocations, err := resolver.FilesByGlob("**/file-?.txt")
   438  				assert.NoError(t, err)
   439  				return actualLocations
   440  			},
   441  			expected: []file.Location{
   442  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   443  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   444  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   445  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   446  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"),
   447  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied
   448  			},
   449  		},
   450  		{
   451  			name: "by extension",
   452  			runner: func(resolver file.Resolver) []file.Location {
   453  				// links are searched, but resolve to the real files
   454  				actualLocations, err := resolver.FilesByGlob("**/*.txt")
   455  				assert.NoError(t, err)
   456  				return actualLocations
   457  			},
   458  			expected: []file.Location{
   459  				file.NewVirtualLocation("/file-1.txt", "/file-1.txt"),
   460  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 1
   461  				file.NewVirtualLocation("/file-2.txt", "/file-2.txt"), // copy 2
   462  				file.NewVirtualLocation("/file-3.txt", "/file-3.txt"),
   463  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"),
   464  				file.NewVirtualLocation("/parent/file-4.txt", "/parent/file-4.txt"), // when we copy into the link path, the same file-4.txt is copied
   465  			},
   466  		},
   467  		{
   468  			name: "by path to degree 1 link",
   469  			runner: func(resolver file.Resolver) []file.Location {
   470  				// links resolve to the final file
   471  				actualLocations, err := resolver.FilesByPath("/link-2")
   472  				assert.NoError(t, err)
   473  				return actualLocations
   474  			},
   475  			expected: []file.Location{
   476  				// we have multiple copies across layers
   477  				file.NewVirtualLocation("/file-2.txt", "/link-2"),
   478  				file.NewVirtualLocation("/file-2.txt", "/link-2"),
   479  			},
   480  		},
   481  		{
   482  			name: "by path to degree 2 link",
   483  			runner: func(resolver file.Resolver) []file.Location {
   484  				// multiple links resolves to the final file
   485  				actualLocations, err := resolver.FilesByPath("/link-indirect")
   486  				assert.NoError(t, err)
   487  				return actualLocations
   488  			},
   489  			expected: []file.Location{
   490  				// we have multiple copies across layers
   491  				file.NewVirtualLocation("/file-2.txt", "/link-indirect"),
   492  				file.NewVirtualLocation("/file-2.txt", "/link-indirect"),
   493  			},
   494  		},
   495  	}
   496  
   497  	for _, test := range tests {
   498  		t.Run(test.name, func(t *testing.T) {
   499  
   500  			img := imagetest.GetFixtureImage(t, "docker-archive", "image-symlinks")
   501  
   502  			resolver, err := NewFromContainerImageAllLayers(img)
   503  			assert.NoError(t, err)
   504  
   505  			actual := test.runner(resolver)
   506  
   507  			compareLocations(t, test.expected, actual)
   508  		})
   509  	}
   510  
   511  }
   512  
   513  func TestAllLayersResolver_AllLocations(t *testing.T) {
   514  	img := imagetest.GetFixtureImage(t, "docker-archive", "image-files-deleted")
   515  
   516  	resolver, err := NewFromContainerImageAllLayers(img)
   517  	assert.NoError(t, err)
   518  
   519  	paths := strset.New()
   520  	for loc := range resolver.AllLocations() {
   521  		paths.Add(loc.RealPath)
   522  	}
   523  	expected := []string{
   524  		"/Dockerfile",
   525  		"/file-1.txt",
   526  		"/file-3.txt",
   527  		"/target",
   528  		"/target/file-2.txt",
   529  
   530  		"/.wh.bin",
   531  		"/.wh.file-1.txt",
   532  		"/.wh.lib",
   533  		"/bin",
   534  		"/bin/arch",
   535  		"/bin/ash",
   536  		"/bin/base64",
   537  		"/bin/bbconfig",
   538  		"/bin/busybox",
   539  		"/bin/cat",
   540  		"/bin/chattr",
   541  		"/bin/chgrp",
   542  		"/bin/chmod",
   543  		"/bin/chown",
   544  		"/bin/cp",
   545  		"/bin/date",
   546  		"/bin/dd",
   547  		"/bin/df",
   548  		"/bin/dmesg",
   549  		"/bin/dnsdomainname",
   550  		"/bin/dumpkmap",
   551  		"/bin/echo",
   552  		"/bin/ed",
   553  		"/bin/egrep",
   554  		"/bin/false",
   555  		"/bin/fatattr",
   556  		"/bin/fdflush",
   557  		"/bin/fgrep",
   558  		"/bin/fsync",
   559  		"/bin/getopt",
   560  		"/bin/grep",
   561  		"/bin/gunzip",
   562  		"/bin/gzip",
   563  		"/bin/hostname",
   564  		"/bin/ionice",
   565  		"/bin/iostat",
   566  		"/bin/ipcalc",
   567  		"/bin/kbd_mode",
   568  		"/bin/kill",
   569  		"/bin/link",
   570  		"/bin/linux32",
   571  		"/bin/linux64",
   572  		"/bin/ln",
   573  		"/bin/login",
   574  		"/bin/ls",
   575  		"/bin/lsattr",
   576  		"/bin/lzop",
   577  		"/bin/makemime",
   578  		"/bin/mkdir",
   579  		"/bin/mknod",
   580  		"/bin/mktemp",
   581  		"/bin/more",
   582  		"/bin/mount",
   583  		"/bin/mountpoint",
   584  		"/bin/mpstat",
   585  		"/bin/mv",
   586  		"/bin/netstat",
   587  		"/bin/nice",
   588  		"/bin/pidof",
   589  		"/bin/ping",
   590  		"/bin/ping6",
   591  		"/bin/pipe_progress",
   592  		"/bin/printenv",
   593  		"/bin/ps",
   594  		"/bin/pwd",
   595  		"/bin/reformime",
   596  		"/bin/rev",
   597  		"/bin/rm",
   598  		"/bin/rmdir",
   599  		"/bin/run-parts",
   600  		"/bin/sed",
   601  		"/bin/setpriv",
   602  		"/bin/setserial",
   603  		"/bin/sh",
   604  		"/bin/sleep",
   605  		"/bin/stat",
   606  		"/bin/stty",
   607  		"/bin/su",
   608  		"/bin/sync",
   609  		"/bin/tar",
   610  		"/bin/touch",
   611  		"/bin/true",
   612  		"/bin/umount",
   613  		"/bin/uname",
   614  		"/bin/usleep",
   615  		"/bin/watch",
   616  		"/bin/zcat",
   617  		"/lib",
   618  		"/lib/apk",
   619  		"/lib/apk/db",
   620  		"/lib/apk/db/installed",
   621  		"/lib/apk/db/lock",
   622  		"/lib/apk/db/scripts.tar",
   623  		"/lib/apk/db/triggers",
   624  		"/lib/apk/exec",
   625  		"/lib/firmware",
   626  		"/lib/ld-musl-x86_64.so.1",
   627  		"/lib/libapk.so.3.12.0",
   628  		"/lib/libc.musl-x86_64.so.1",
   629  		"/lib/libcrypto.so.3",
   630  		"/lib/libssl.so.3",
   631  		"/lib/libz.so.1",
   632  		"/lib/libz.so.1.2.13",
   633  		"/lib/mdev",
   634  		"/lib/modules-load.d",
   635  		"/lib/sysctl.d",
   636  		"/lib/sysctl.d/00-alpine.conf",
   637  	}
   638  
   639  	// depending on how the image is built (either from linux or mac), sys and proc might accidentally be added to the image.
   640  	// this isn't important for the test, so we remove them.
   641  	paths.Remove("/proc", "/sys", "/dev", "/etc")
   642  
   643  	pathsList := paths.List()
   644  	sort.Strings(pathsList)
   645  
   646  	assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
   647  }