diff --git a/go.mod b/go.mod index 195b8bc0d..101e64916 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 - github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a + github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 github.com/antihax/optional v1.0.0 github.com/bmatcuk/doublestar/v2 v2.0.4 github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible diff --git a/go.sum b/go.sum index 0faf44c75..6a192fdda 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29 h1:K9LfnxwhqvihqU0+MF325FNy7fsKV9EGaUxdfR4gnWk= github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29/go.mod h1:Oc1UkGaJwY6ND6vtAqPSlYrptKRJngHwkwB6W7l1uP0= -github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a h1:RQb+Gft1MKxjDfJCnHP/f1mwfy0Jz50Kp9QGgSWKQiY= -github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a/go.mod h1:165DfE5jApgEkHTWwu7Bijeml9fofudrgcpuWaD9+tk= +github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578 h1:gSpftl0RWfdTwlPmsOLgEawHIw16xwwG1mFD/wrVDRE= +github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578/go.mod h1:kL7jfbXblrDcBhu5ja/s+VTYL3Mzof4eQNMJiSqcwXQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -242,6 +242,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gabriel-vasile/mimetype v1.3.0 h1:4YOHITFLyYwF+iqG0ybSLGArRItynpfwdlWRmJnd75E= +github.com/gabriel-vasile/mimetype v1.3.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-critic/go-critic v0.4.1/go.mod h1:7/14rZGnZbY6E38VEGk2kVhoq6itzc1E68facVDK23g= @@ -872,7 +874,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -939,7 +941,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/presenter/poweruser/json_file_metadata.go b/internal/presenter/poweruser/json_file_metadata.go index 35d2209d6..2f840ba57 100644 --- a/internal/presenter/poweruser/json_file_metadata.go +++ b/internal/presenter/poweruser/json_file_metadata.go @@ -22,6 +22,7 @@ type JSONFileMetadataEntry struct { UserID int `json:"userID"` GroupID int `json:"groupID"` Digests []file.Digest `json:"digests,omitempty"` + MIMEType string `json:"mimeType"` } func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) { @@ -46,6 +47,7 @@ func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests m UserID: metadata.UserID, GroupID: metadata.GroupID, Digests: digestResults, + MIMEType: metadata.MIMEType, }, }) } diff --git a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden index 2f2002886..be30db290 100644 --- a/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden +++ b/internal/presenter/poweruser/test-fixtures/snapshot/TestJSONPresenter.golden @@ -16,7 +16,8 @@ "mode": 775, "type": "directory", "userID": 0, - "groupID": 0 + "groupID": 0, + "mimeType": "" } }, { @@ -33,7 +34,8 @@ "algorithm": "sha256", "value": "366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703" } - ] + ], + "mimeType": "" } }, { @@ -45,7 +47,8 @@ "type": "symbolicLink", "linkDestination": "/c", "userID": 0, - "groupID": 0 + "groupID": 0, + "mimeType": "" } }, { @@ -62,7 +65,8 @@ "algorithm": "sha256", "value": "1b3722da2a7d90d033b87581a2a3f12021647445653e34666ef041e3b4f3707c" } - ] + ], + "mimeType": "" } } ], diff --git a/syft/file/metadata_cataloger_test.go b/syft/file/metadata_cataloger_test.go index c168533a3..aff3eb934 100644 --- a/syft/file/metadata_cataloger_test.go +++ b/syft/file/metadata_cataloger_test.go @@ -49,11 +49,12 @@ func TestFileMetadataCataloger(t *testing.T) { path: "/file-1.txt", exists: true, expected: source.FileMetadata{ - Mode: 0644, - Type: "RegularFile", - UserID: 1, - GroupID: 2, - Size: 7, + Mode: 0644, + Type: "RegularFile", + UserID: 1, + GroupID: 2, + Size: 7, + MIMEType: "text/plain", }, }, { @@ -65,6 +66,7 @@ func TestFileMetadataCataloger(t *testing.T) { LinkDestination: "file-1.txt", UserID: 1, GroupID: 2, + MIMEType: "", }, }, { @@ -76,46 +78,51 @@ func TestFileMetadataCataloger(t *testing.T) { LinkDestination: "file-1.txt", UserID: 0, GroupID: 0, + MIMEType: "", }, }, { path: "/char-device-1", exists: true, expected: source.FileMetadata{ - Mode: 0644 | os.ModeDevice | os.ModeCharDevice, - Type: "CharacterDevice", - UserID: 0, - GroupID: 0, + Mode: 0644 | os.ModeDevice | os.ModeCharDevice, + Type: "CharacterDevice", + UserID: 0, + GroupID: 0, + MIMEType: "", }, }, { path: "/block-device-1", exists: true, expected: source.FileMetadata{ - Mode: 0644 | os.ModeDevice, - Type: "BlockDevice", - UserID: 0, - GroupID: 0, + Mode: 0644 | os.ModeDevice, + Type: "BlockDevice", + UserID: 0, + GroupID: 0, + MIMEType: "", }, }, { path: "/fifo-1", exists: true, expected: source.FileMetadata{ - Mode: 0644 | os.ModeNamedPipe, - Type: "FIFONode", - UserID: 0, - GroupID: 0, + Mode: 0644 | os.ModeNamedPipe, + Type: "FIFONode", + UserID: 0, + GroupID: 0, + MIMEType: "", }, }, { path: "/bin", exists: true, expected: source.FileMetadata{ - Mode: 0755 | os.ModeDir, - Type: "Directory", - UserID: 0, - GroupID: 0, + Mode: 0755 | os.ModeDir, + Type: "Directory", + UserID: 0, + GroupID: 0, + MIMEType: "", }, }, } diff --git a/syft/pkg/cataloger/rpmdb/parse_rpmdb_test.go b/syft/pkg/cataloger/rpmdb/parse_rpmdb_test.go index 2502118d8..7e9720b51 100644 --- a/syft/pkg/cataloger/rpmdb/parse_rpmdb_test.go +++ b/syft/pkg/cataloger/rpmdb/parse_rpmdb_test.go @@ -45,11 +45,16 @@ func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Locat func (r *rpmdbTestFileResolverMock) FilesByGlob(...string) ([]source.Location, error) { return nil, fmt.Errorf("not implemented") } + func (r *rpmdbTestFileResolverMock) RelativeFileByPath(source.Location, string) *source.Location { panic(fmt.Errorf("not implemented")) return nil } +func (r rpmdbTestFileResolverMock) FilesByMIMEType(types ...string) ([]source.Location, error) { + panic("implement me") +} + func TestParseRpmDB(t *testing.T) { dbLocation := source.NewLocation("test-path") diff --git a/syft/source/all_layers_resolver.go b/syft/source/all_layers_resolver.go index b446eccbf..084eca7c2 100644 --- a/syft/source/all_layers_resolver.go +++ b/syft/source/all_layers_resolver.go @@ -188,6 +188,24 @@ func (r *allLayersResolver) FileContentsByLocation(location Location) (io.ReadCl return r.img.FileContentsByRef(location.ref) } +func (r *allLayersResolver) FilesByMIMEType(types ...string) ([]Location, error) { + var locations []Location + for _, layerIdx := range r.layers { + layer := r.img.Layers[layerIdx] + + refs, err := layer.FilesByMIMEType(types...) + if err != nil { + return nil, err + } + + for _, ref := range refs { + locations = append(locations, NewLocationFromReference(ref)) + } + } + + return locations, nil +} + func (r *allLayersResolver) AllLocations() <-chan Location { results := make(chan Location) go func() { diff --git a/syft/source/all_layers_resolver_test.go b/syft/source/all_layers_resolver_test.go index 421a6663d..fc50c5840 100644 --- a/syft/source/all_layers_resolver_test.go +++ b/syft/source/all_layers_resolver_test.go @@ -3,6 +3,8 @@ package source import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/anchore/stereoscope/pkg/imagetest" ) @@ -239,3 +241,34 @@ func TestAllLayersResolver_FilesByGlob(t *testing.T) { }) } } + +func Test_imageAllLayersResolver_FilesByMIMEType(t *testing.T) { + + tests := []struct { + fixtureName string + mimeType string + expectedPaths []string + }{ + { + fixtureName: "image-duplicate-path", + mimeType: "text/plain", + expectedPaths: []string{"/somefile-1.txt", "/somefile-1.txt"}, + }, + } + for _, test := range tests { + t.Run(test.fixtureName, func(t *testing.T) { + img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName) + + resolver, err := newAllLayersResolver(img) + assert.NoError(t, err) + + locations, err := resolver.FilesByMIMEType(test.mimeType) + assert.NoError(t, err) + + assert.Len(t, test.expectedPaths, len(locations)) + for idx, l := range locations { + assert.Equal(t, test.expectedPaths[idx], l.RealPath, "does not have path %q", l.RealPath) + } + }) + } +} diff --git a/syft/source/directory_resolver.go b/syft/source/directory_resolver.go index b34f2a964..b2d8366e7 100644 --- a/syft/source/directory_resolver.go +++ b/syft/source/directory_resolver.go @@ -8,7 +8,6 @@ import ( "path" "path/filepath" "strings" - "syscall" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/filetree" @@ -35,10 +34,11 @@ type directoryResolver struct { path string cwd string fileTree *filetree.FileTree - infos map[file.ID]os.FileInfo + metadata map[file.ID]FileMetadata // TODO: wire up to report these paths in the json report - pathFilterFns []pathFilterFn - errPaths map[string]error + pathFilterFns []pathFilterFn + refsByMIMEType map[string][]file.Reference + errPaths map[string]error } func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryResolver, error) { @@ -52,12 +52,13 @@ func newDirectoryResolver(root string, pathFilters ...pathFilterFn) (*directoryR } resolver := directoryResolver{ - path: root, - cwd: cwd, - fileTree: filetree.NewFileTree(), - infos: make(map[file.ID]os.FileInfo), - pathFilterFns: pathFilters, - errPaths: make(map[string]error), + path: root, + cwd: cwd, + fileTree: filetree.NewFileTree(), + metadata: make(map[file.ID]FileMetadata), + pathFilterFns: pathFilters, + refsByMIMEType: make(map[string][]file.Reference), + errPaths: make(map[string]error), } return &resolver, indexAllRoots(root, resolver.indexTree) @@ -158,7 +159,11 @@ func (r directoryResolver) addPathToIndex(p string, info os.FileInfo) (string, e } } - r.infos[ref.ID()] = info + metadata := fileMetadataFromPath(p, info) + if ref != nil { + r.refsByMIMEType[metadata.MIMEType] = append(r.refsByMIMEType[metadata.MIMEType], *ref) + } + r.metadata[ref.ID()] = metadata return newRoot, nil } @@ -207,6 +212,7 @@ func (r directoryResolver) FilesByPath(userPaths ...string) ([]Location, error) log.Warnf("unable to get file by path=%q : %+v", userPath, err) continue } + // TODO: why not use stored metadata? fileMeta, err := os.Stat(userStrPath) if os.IsNotExist(err) { continue @@ -280,25 +286,24 @@ func (r *directoryResolver) AllLocations() <-chan Location { } func (r *directoryResolver) FileMetadataByLocation(location Location) (FileMetadata, error) { - info, exists := r.infos[location.ref.ID()] + metadata, exists := r.metadata[location.ref.ID()] if !exists { return FileMetadata{}, fmt.Errorf("location: %+v : %w", location, os.ErrNotExist) } - uid := -1 - gid := -1 - if stat, ok := info.Sys().(*syscall.Stat_t); ok { - uid = int(stat.Uid) - gid = int(stat.Gid) - } + return metadata, nil +} - return FileMetadata{ - Mode: info.Mode(), - Type: newFileTypeFromMode(info.Mode()), - // unsupported across platforms - UserID: uid, - GroupID: gid, - }, nil +func (r *directoryResolver) FilesByMIMEType(types ...string) ([]Location, error) { + var locations []Location + for _, ty := range types { + if refs, ok := r.refsByMIMEType[ty]; ok { + for _, ref := range refs { + locations = append(locations, NewLocationFromDirectory(r.responsePath(string(ref.RealPath)), ref)) + } + } + } + return locations, nil } func isUnixSystemRuntimePath(path string) bool { diff --git a/syft/source/directory_resolver_test.go b/syft/source/directory_resolver_test.go index 9d85f21f5..df6a237b9 100644 --- a/syft/source/directory_resolver_test.go +++ b/syft/source/directory_resolver_test.go @@ -7,6 +7,8 @@ import ( "strings" "testing" + "github.com/scylladb/go-set/strset" + "github.com/anchore/stereoscope/pkg/file" "github.com/stretchr/testify/assert" "github.com/wagoodman/go-progress" @@ -303,7 +305,7 @@ func Test_directoryResolver_index(t *testing.T) { if assert.NoError(t, err) { return } - assert.Equal(t, info, r.infos[ref.ID()]) + assert.Equal(t, info, r.metadata[ref.ID()]) }) } } @@ -429,3 +431,31 @@ func Test_indexAllRoots(t *testing.T) { }) } } + +func Test_directoryResolver_FilesByMIMEType(t *testing.T) { + + tests := []struct { + fixturePath string + mimeType string + expectedPaths *strset.Set + }{ + { + fixturePath: "./test-fixtures/image-simple", + mimeType: "text/plain", + expectedPaths: strset.New("test-fixtures/image-simple/file-1.txt", "test-fixtures/image-simple/file-2.txt", "test-fixtures/image-simple/target/really/nested/file-3.txt", "test-fixtures/image-simple/Dockerfile"), + }, + } + for _, test := range tests { + t.Run(test.fixturePath, func(t *testing.T) { + + resolver, err := newDirectoryResolver(test.fixturePath) + assert.NoError(t, err) + locations, err := resolver.FilesByMIMEType(test.mimeType) + assert.NoError(t, err) + assert.Equal(t, test.expectedPaths.Size(), len(locations)) + for _, l := range locations { + assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath) + } + }) + } +} diff --git a/syft/source/file_metadata.go b/syft/source/file_metadata.go index f08be9421..ac9d18de8 100644 --- a/syft/source/file_metadata.go +++ b/syft/source/file_metadata.go @@ -2,6 +2,11 @@ package source import ( "os" + "syscall" + + "github.com/anchore/syft/internal/log" + + "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/image" ) @@ -13,6 +18,7 @@ type FileMetadata struct { GroupID int LinkDestination string Size int64 + MIMEType string } func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata, error) { @@ -28,5 +34,36 @@ func fileMetadataByLocation(img *image.Image, location Location) (FileMetadata, GroupID: entry.Metadata.GroupID, LinkDestination: entry.Metadata.Linkname, Size: entry.Metadata.Size, + MIMEType: entry.Metadata.MIMEType, }, nil } + +func fileMetadataFromPath(path string, info os.FileInfo) FileMetadata { + uid := -1 + gid := -1 + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + uid = int(stat.Uid) + gid = int(stat.Gid) + } + + f, err := os.Open(path) + if err != nil { + // TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns + f = nil + } else { + defer func() { + if err := f.Close(); err != nil { + log.Warnf("unable to close file while obtaining metadata: %s", path) + } + }() + } + + return FileMetadata{ + Mode: info.Mode(), + Type: newFileTypeFromMode(info.Mode()), + // unsupported across platforms + UserID: uid, + GroupID: gid, + MIMEType: file.MIMEType(f), + } +} diff --git a/syft/source/file_resolver.go b/syft/source/file_resolver.go index 331820af2..fe47d464d 100644 --- a/syft/source/file_resolver.go +++ b/syft/source/file_resolver.go @@ -29,6 +29,8 @@ type FilePathResolver interface { FilesByPath(paths ...string) ([]Location, error) // FilesByGlob fetches a set of file references which the given glob matches FilesByGlob(patterns ...string) ([]Location, error) + // FilesByGlob fetches a set of file references which the contents have been classified as one of the given MIME Types + FilesByMIMEType(types ...string) ([]Location, error) // RelativeFileByPath fetches a single file at the given path relative to the layer squash of the given reference. // This is helpful when attempting to find a file that is in the same layer or lower as another file. RelativeFileByPath(_ Location, path string) *Location diff --git a/syft/source/image_squash_resolver.go b/syft/source/image_squash_resolver.go index de51edf11..e336d6122 100644 --- a/syft/source/image_squash_resolver.go +++ b/syft/source/image_squash_resolver.go @@ -151,6 +151,20 @@ func (r *imageSquashResolver) AllLocations() <-chan Location { return results } +func (r *imageSquashResolver) FilesByMIMEType(types ...string) ([]Location, error) { + refs, err := r.img.FilesByMIMETypeFromSquash(types...) + if err != nil { + return nil, err + } + + var locations []Location + for _, ref := range refs { + locations = append(locations, NewLocationFromReference(ref)) + } + + return locations, nil +} + func (r *imageSquashResolver) FileMetadataByLocation(location Location) (FileMetadata, error) { return fileMetadataByLocation(r.img, location) } diff --git a/syft/source/image_squash_resolver_test.go b/syft/source/image_squash_resolver_test.go index 4d8c1556f..ff6c4ce7c 100644 --- a/syft/source/image_squash_resolver_test.go +++ b/syft/source/image_squash_resolver_test.go @@ -3,6 +3,9 @@ package source import ( "testing" + "github.com/scylladb/go-set/strset" + "github.com/stretchr/testify/assert" + "github.com/anchore/stereoscope/pkg/imagetest" ) @@ -225,3 +228,34 @@ func TestImageSquashResolver_FilesByGlob(t *testing.T) { }) } } + +func Test_imageSquashResolver_FilesByMIMEType(t *testing.T) { + + tests := []struct { + fixtureName string + mimeType string + expectedPaths *strset.Set + }{ + { + fixtureName: "image-simple", + mimeType: "text/plain", + expectedPaths: strset.New("/somefile-1.txt", "/somefile-2.txt", "/really/nested/file-3.txt"), + }, + } + for _, test := range tests { + t.Run(test.fixtureName, func(t *testing.T) { + img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureName) + + resolver, err := newImageSquashResolver(img) + assert.NoError(t, err) + + locations, err := resolver.FilesByMIMEType(test.mimeType) + assert.NoError(t, err) + + assert.Equal(t, test.expectedPaths.Size(), len(locations)) + for _, l := range locations { + assert.True(t, test.expectedPaths.Has(l.RealPath), "does not have path %q", l.RealPath) + } + }) + } +} diff --git a/syft/source/mock_resolver.go b/syft/source/mock_resolver.go index 9bd569475..114233806 100644 --- a/syft/source/mock_resolver.go +++ b/syft/source/mock_resolver.go @@ -14,8 +14,9 @@ var _ FileResolver = (*MockResolver)(nil) // It provides an implementation that can resolve local filesystem paths using only a provided discrete list of file // paths, which are typically paths to test fixtures. type MockResolver struct { - Locations []Location - Metadata map[Location]FileMetadata + locations []Location + metadata map[Location]FileMetadata + mimeTypeIndex map[string][]Location } // NewMockResolverForPaths creates a new MockResolver, where the only resolvable @@ -26,21 +27,30 @@ func NewMockResolverForPaths(paths ...string) *MockResolver { locations = append(locations, NewLocation(p)) } - return &MockResolver{Locations: locations} + return &MockResolver{ + locations: locations, + metadata: make(map[Location]FileMetadata), + } } func NewMockResolverForPathsWithMetadata(metadata map[Location]FileMetadata) *MockResolver { var locations []Location - for p := range metadata { - locations = append(locations, p) + var mimeTypeIndex = make(map[string][]Location) + for l, m := range metadata { + locations = append(locations, l) + mimeTypeIndex[m.MIMEType] = append(mimeTypeIndex[m.MIMEType], l) } - return &MockResolver{Locations: locations, Metadata: metadata} + return &MockResolver{ + locations: locations, + metadata: metadata, + mimeTypeIndex: mimeTypeIndex, + } } // HasPath indicates if the given path exists in the underlying source. func (r MockResolver) HasPath(path string) bool { - for _, l := range r.Locations { + for _, l := range r.locations { if l.RealPath == path { return true } @@ -50,13 +60,13 @@ func (r MockResolver) HasPath(path string) bool { // String returns the string representation of the MockResolver. func (r MockResolver) String() string { - return fmt.Sprintf("mock:(%s,...)", r.Locations[0].RealPath) + return fmt.Sprintf("mock:(%s,...)", r.locations[0].RealPath) } // FileContentsByLocation fetches file contents for a single location. If the // path does not exist, an error is returned. func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) { - for _, l := range r.Locations { + for _, l := range r.locations { if l == location { return os.Open(location.RealPath) } @@ -69,7 +79,7 @@ func (r MockResolver) FileContentsByLocation(location Location) (io.ReadCloser, func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) { var results []Location for _, p := range paths { - for _, location := range r.Locations { + for _, location := range r.locations { if p == location.RealPath { results = append(results, NewLocation(p)) } @@ -83,7 +93,7 @@ func (r MockResolver) FilesByPath(paths ...string) ([]Location, error) { func (r MockResolver) FilesByGlob(patterns ...string) ([]Location, error) { var results []Location for _, pattern := range patterns { - for _, location := range r.Locations { + for _, location := range r.locations { matches, err := doublestar.Match(pattern, location.RealPath) if err != nil { return nil, err @@ -115,7 +125,7 @@ func (r MockResolver) AllLocations() <-chan Location { results := make(chan Location) go func() { defer close(results) - for _, l := range r.Locations { + for _, l := range r.locations { results <- l } }() @@ -142,3 +152,11 @@ func (r MockResolver) FileMetadataByLocation(l Location) (FileMetadata, error) { Size: info.Size(), }, nil } + +func (r MockResolver) FilesByMIMEType(types ...string) ([]Location, error) { + var locations []Location + for _, ty := range types { + locations = append(r.mimeTypeIndex[ty], locations...) + } + return locations, nil +} diff --git a/syft/source/test-fixtures/image-duplicate-path/Dockerfile b/syft/source/test-fixtures/image-duplicate-path/Dockerfile new file mode 100644 index 000000000..be7f1eacf --- /dev/null +++ b/syft/source/test-fixtures/image-duplicate-path/Dockerfile @@ -0,0 +1,4 @@ +# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one. +FROM scratch +ADD file-1.txt /somefile-1.txt +ADD file-2.txt /somefile-1.txt diff --git a/syft/source/test-fixtures/image-duplicate-path/file-1.txt b/syft/source/test-fixtures/image-duplicate-path/file-1.txt new file mode 100644 index 000000000..985d3408e --- /dev/null +++ b/syft/source/test-fixtures/image-duplicate-path/file-1.txt @@ -0,0 +1 @@ +this file has contents \ No newline at end of file diff --git a/syft/source/test-fixtures/image-duplicate-path/file-2.txt b/syft/source/test-fixtures/image-duplicate-path/file-2.txt new file mode 100644 index 000000000..396d08bbc --- /dev/null +++ b/syft/source/test-fixtures/image-duplicate-path/file-2.txt @@ -0,0 +1 @@ +file-2 contents! \ No newline at end of file