diff --git a/go.mod b/go.mod index 7346e4220..e7e67ec84 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12 github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b - github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 + github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5 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 b1aa41dad..83fa17fa4 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,10 @@ 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/stereoscope v0.0.0-20210104203718-4c1d1bd9a255 h1:Ng7BDr9PQTCztANogjfEdEjjWUylhlPyZPhtarIGo00= github.com/anchore/stereoscope v0.0.0-20210104203718-4c1d1bd9a255/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw= +github.com/anchore/stereoscope v0.0.0-20210105000809-428eda0b2ec6 h1:JWpsV/8x1fuCYjJmNjT43cVFblLTpO/ISDnePukiTNw= +github.com/anchore/stereoscope v0.0.0-20210105000809-428eda0b2ec6/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw= +github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5 h1:NGRfS6BZKElgiMbqdoH9iQn+6oxT7CJdZYrqgwvGkWY= +github.com/anchore/stereoscope v0.0.0-20210105001222-7beea73cb7e5/go.mod h1:BMdPL0QEIYfpjQ3M7sHYZvuh6+vcomqF3TMHL8gr6Vw= 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= diff --git a/syft/cataloger/common/generic_cataloger_test.go b/syft/cataloger/common/generic_cataloger_test.go index e1c587962..92a5ace36 100644 --- a/syft/cataloger/common/generic_cataloger_test.go +++ b/syft/cataloger/common/generic_cataloger_test.go @@ -21,6 +21,10 @@ func newTestResolver() *testResolverMock { } } +func (r testResolverMock) HasPath(path string) bool { + panic("not implemented") +} + func (r *testResolverMock) FileContentsByLocation(_ source.Location) (io.ReadCloser, error) { return nil, fmt.Errorf("not implemented") } diff --git a/syft/cataloger/rpmdb/parse_rpmdb.go b/syft/cataloger/rpmdb/parse_rpmdb.go index 9f0b3ed80..f67a123e7 100644 --- a/syft/cataloger/rpmdb/parse_rpmdb.go +++ b/syft/cataloger/rpmdb/parse_rpmdb.go @@ -45,11 +45,6 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader allPkgs := make([]pkg.Package, 0) for _, entry := range pkgList { - records, err := extractRpmdbFileRecords(resolver, entry) - if err != nil { - return nil, err - } - p := pkg.Package{ Name: entry.Name, Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does, instead of fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch) @@ -67,7 +62,7 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader Vendor: entry.Vendor, License: entry.License, Size: entry.Size, - Files: records, + Files: extractRpmdbFileRecords(resolver, entry), }, } @@ -77,25 +72,19 @@ func parseRpmDB(resolver source.FileResolver, dbLocation source.Location, reader return allPkgs, nil } -func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) { +func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) []pkg.RpmdbFileRecord { var records = make([]pkg.RpmdbFileRecord, 0) for _, record := range entry.Files { - refs, err := resolver.FilesByPath(record.Path) - if err != nil { - return nil, fmt.Errorf("failed to resolve path=%+v: %w", record.Path, err) - } //only persist RPMDB file records which exist in the image/directory, otherwise ignore them - if len(refs) == 0 { - continue + if resolver.HasPath(record.Path) { + records = append(records, pkg.RpmdbFileRecord{ + Path: record.Path, + Mode: pkg.RpmdbFileMode(record.Mode), + Size: int(record.Size), + SHA256: record.SHA256, + }) } - - records = append(records, pkg.RpmdbFileRecord{ - Path: record.Path, - Mode: pkg.RpmdbFileMode(record.Mode), - Size: int(record.Size), - SHA256: record.SHA256, - }) } - return records, nil + return records } diff --git a/syft/cataloger/rpmdb/parse_rpmdb_test.go b/syft/cataloger/rpmdb/parse_rpmdb_test.go index a80fa2523..d615ac2cd 100644 --- a/syft/cataloger/rpmdb/parse_rpmdb_test.go +++ b/syft/cataloger/rpmdb/parse_rpmdb_test.go @@ -21,6 +21,10 @@ func newTestFileResolver(ignorePaths bool) *rpmdbTestFileResolverMock { } } +func (r rpmdbTestFileResolverMock) HasPath(path string) bool { + return !r.ignorePaths +} + func (r *rpmdbTestFileResolverMock) FilesByPath(paths ...string) ([]source.Location, error) { if r.ignorePaths { // act as if no paths exist diff --git a/syft/source/all_layers_resolver.go b/syft/source/all_layers_resolver.go index 757183127..f95472a43 100644 --- a/syft/source/all_layers_resolver.go +++ b/syft/source/all_layers_resolver.go @@ -5,12 +5,10 @@ import ( "fmt" "io" - "github.com/anchore/stereoscope/pkg/filetree" - - "github.com/anchore/syft/internal/log" - "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/internal/log" ) var _ Resolver = (*AllLayersResolver)(nil) @@ -37,6 +35,18 @@ func NewAllLayersResolver(img *image.Image) (*AllLayersResolver, error) { }, nil } +// HasPath indicates if the given path exists in the underlying source. +func (r *AllLayersResolver) HasPath(path string) bool { + p := file.Path(path) + for _, layerIdx := range r.layers { + tree := r.img.Layers[layerIdx].Tree + if tree.HasPath(p) { + return true + } + } + return false +} + func (r *AllLayersResolver) fileByRef(ref file.Reference, uniqueFileIDs file.ReferenceSet, layerIdx int) ([]file.Reference, error) { uniqueFiles := make([]file.Reference, 0) diff --git a/syft/source/all_layers_resolver_test.go b/syft/source/all_layers_resolver_test.go index a1aeebd5f..5aa3c8390 100644 --- a/syft/source/all_layers_resolver_test.go +++ b/syft/source/all_layers_resolver_test.go @@ -13,9 +13,10 @@ type resolution struct { func TestAllLayersResolver_FilesByPath(t *testing.T) { cases := []struct { - name string - linkPath string - resolutions []resolution + name string + linkPath string + resolutions []resolution + forcePositiveHasPath bool }{ { name: "link with previous data", @@ -66,14 +67,17 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) { }, }, { - name: "dead link", - linkPath: "/link-dead", - resolutions: []resolution{}, + name: "dead link", + linkPath: "/link-dead", + resolutions: []resolution{}, + forcePositiveHasPath: true, }, { name: "ignore directories", linkPath: "/bin", resolutions: []resolution{}, + // directories don't resolve BUT do exist + forcePositiveHasPath: true, }, } for _, c := range cases { @@ -86,6 +90,17 @@ func TestAllLayersResolver_FilesByPath(t *testing.T) { t.Fatalf("could not create resolver: %+v", err) } + hasPath := resolver.HasPath(c.linkPath) + if !c.forcePositiveHasPath { + if len(c.resolutions) > 0 && !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not") + } else if len(c.resolutions) == 0 && hasPath { + t.Errorf("expeced HasPath() to NOT indicate existance, but does") + } + } else if !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not (force path)") + } + refs, err := resolver.FilesByPath(c.linkPath) if err != nil { t.Fatalf("could not use resolver: %+v", err) diff --git a/syft/source/directory_resolver.go b/syft/source/directory_resolver.go index ccdd5ba8e..5aa415f37 100644 --- a/syft/source/directory_resolver.go +++ b/syft/source/directory_resolver.go @@ -19,27 +19,37 @@ type DirectoryResolver struct { Path string } +func (r DirectoryResolver) requestPath(userPath string) string { + fullPath := userPath + if filepath.IsAbs(fullPath) { + // a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is + fullPath = path.Join(r.Path, fullPath) + } + return fullPath +} + +// HasPath indicates if the given path exists in the underlying source. +func (r *DirectoryResolver) HasPath(userPath string) bool { + _, err := os.Stat(r.requestPath(userPath)) + return !os.IsNotExist(err) +} + // Stringer to represent a directory path data source -func (s DirectoryResolver) String() string { - return fmt.Sprintf("dir:%s", s.Path) +func (r DirectoryResolver) String() string { + return fmt.Sprintf("dir:%s", r.Path) } // FilesByPath returns all file.References that match the given paths from the directory. -func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) { +func (r DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) { var references = make([]Location, 0) for _, userPath := range userPaths { - userStrPath := userPath - - if filepath.IsAbs(userStrPath) { - // a path relative to root should be prefixed with the resolvers directory path, otherwise it should be left as is - userStrPath = path.Join(s.Path, userStrPath) - } + userStrPath := r.requestPath(userPath) fileMeta, err := os.Stat(userStrPath) if os.IsNotExist(err) { continue } else if err != nil { - log.Errorf("path (%s) is not valid: %v", userStrPath, err) + log.Errorf("path (%r) is not valid: %v", userStrPath, err) } // don't consider directories @@ -54,11 +64,11 @@ func (s DirectoryResolver) FilesByPath(userPaths ...string) ([]Location, error) } // FilesByGlob returns all file.References that match the given path glob pattern from any layer in the image. -func (s DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) { +func (r DirectoryResolver) FilesByGlob(patterns ...string) ([]Location, error) { result := make([]Location, 0) for _, pattern := range patterns { - pathPattern := path.Join(s.Path, pattern) + pathPattern := path.Join(r.Path, pattern) pathMatches, err := doublestar.Glob(pathPattern) if err != nil { return nil, err @@ -84,8 +94,8 @@ func (s DirectoryResolver) FilesByGlob(patterns ...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. For the // DirectoryResolver, this is a simple path lookup. -func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location { - paths, err := s.FilesByPath(path) +func (r *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Location { + paths, err := r.FilesByPath(path) if err != nil { return nil } @@ -97,7 +107,7 @@ func (s *DirectoryResolver) RelativeFileByPath(_ Location, path string) *Locatio } // MultipleFileContentsByLocation returns the file contents for all file.References relative a directory. -func (s DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) { +func (r DirectoryResolver) MultipleFileContentsByLocation(locations []Location) (map[Location]io.ReadCloser, error) { refContents := make(map[Location]io.ReadCloser) for _, location := range locations { refContents[location] = file.NewDeferredReadCloser(location.Path) @@ -107,6 +117,6 @@ func (s DirectoryResolver) MultipleFileContentsByLocation(locations []Location) // FileContentsByLocation fetches file contents for a single file reference relative to a directory. // If the path does not exist an error is returned. -func (s DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) { +func (r DirectoryResolver) FileContentsByLocation(location Location) (io.ReadCloser, error) { return file.NewDeferredReadCloser(location.Path), nil } diff --git a/syft/source/directory_resolver_test.go b/syft/source/directory_resolver_test.go index 8cec50f61..6a57929dd 100644 --- a/syft/source/directory_resolver_test.go +++ b/syft/source/directory_resolver_test.go @@ -6,11 +6,12 @@ import ( func TestDirectoryResolver_FilesByPath(t *testing.T) { cases := []struct { - name string - root string - input string - expected string - refCount int + name string + root string + input string + expected string + refCount int + forcePositiveHasPath bool }{ { name: "finds a file (relative)", @@ -47,15 +48,28 @@ func TestDirectoryResolver_FilesByPath(t *testing.T) { refCount: 1, }, { - name: "directories ignored", - root: "./test-fixtures/", - input: "/image-symlinks", - refCount: 0, + name: "directories ignored", + root: "./test-fixtures/", + input: "/image-symlinks", + refCount: 0, + forcePositiveHasPath: true, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { resolver := DirectoryResolver{c.root} + + hasPath := resolver.HasPath(c.input) + if !c.forcePositiveHasPath { + if c.refCount != 0 && !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not") + } else if c.refCount == 0 && hasPath { + t.Errorf("expeced HasPath() to NOT indicate existance, but does") + } + } else if !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not (force path)") + } + refs, err := resolver.FilesByPath(c.input) if err != nil { t.Fatalf("could not use resolver: %+v, %+v", err, refs) diff --git a/syft/source/image_squash_resolver.go b/syft/source/image_squash_resolver.go index 6ed0d7bb3..30a426deb 100644 --- a/syft/source/image_squash_resolver.go +++ b/syft/source/image_squash_resolver.go @@ -4,9 +4,8 @@ import ( "fmt" "io" - "github.com/anchore/stereoscope/pkg/filetree" - "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/stereoscope/pkg/filetree" "github.com/anchore/stereoscope/pkg/image" ) @@ -25,6 +24,11 @@ func NewImageSquashResolver(img *image.Image) (*ImageSquashResolver, error) { return &ImageSquashResolver{img: img}, nil } +// HasPath indicates if the given path exists in the underlying source. +func (r *ImageSquashResolver) HasPath(path string) bool { + return r.img.SquashedTree().HasPath(file.Path(path)) +} + // FilesByPath returns all file.References that match the given paths within the squashed representation of the image. func (r *ImageSquashResolver) FilesByPath(paths ...string) ([]Location, error) { uniqueFileIDs := file.NewFileReferenceSet() diff --git a/syft/source/image_squash_resolver_test.go b/syft/source/image_squash_resolver_test.go index 8b9d10a0f..a78150f16 100644 --- a/syft/source/image_squash_resolver_test.go +++ b/syft/source/image_squash_resolver_test.go @@ -8,10 +8,11 @@ import ( func TestImageSquashResolver_FilesByPath(t *testing.T) { cases := []struct { - name string - linkPath string - resolveLayer uint - resolvePath string + name string + linkPath string + resolveLayer uint + resolvePath string + forcePositiveHasPath bool }{ { name: "link with previous data", @@ -42,11 +43,15 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) { linkPath: "/link-dead", resolveLayer: 8, resolvePath: "", + // the path should exist, even if the link is dead + forcePositiveHasPath: true, }, { name: "ignore directories", linkPath: "/bin", resolvePath: "", + // the path should exist, even if we ignore it + forcePositiveHasPath: true, }, { name: "parent is a link (with overridden data)", @@ -65,6 +70,17 @@ func TestImageSquashResolver_FilesByPath(t *testing.T) { t.Fatalf("could not create resolver: %+v", err) } + hasPath := resolver.HasPath(c.linkPath) + if !c.forcePositiveHasPath { + if c.resolvePath != "" && !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not") + } else if c.resolvePath == "" && hasPath { + t.Errorf("expeced HasPath() to NOT indicate existance, but does") + } + } else if !hasPath { + t.Errorf("expected HasPath() to indicate existance, but did not (force path)") + } + refs, err := resolver.FilesByPath(c.linkPath) if err != nil { t.Fatalf("could not use resolver: %+v", err) diff --git a/syft/source/mock_resolver.go b/syft/source/mock_resolver.go index 3710937b7..53846ac95 100644 --- a/syft/source/mock_resolver.go +++ b/syft/source/mock_resolver.go @@ -28,6 +28,16 @@ func NewMockResolverForPaths(paths ...string) *MockResolver { return &MockResolver{Locations: locations} } +// HasPath indicates if the given path exists in the underlying source. +func (r MockResolver) HasPath(path string) bool { + for _, l := range r.Locations { + if l.Path == path { + return true + } + } + return false +} + // String returns the string representation of the MockResolver. func (r MockResolver) String() string { return fmt.Sprintf("mock:(%s,...)", r.Locations[0].Path) diff --git a/syft/source/resolver.go b/syft/source/resolver.go index 627f17b71..1d05fbb56 100644 --- a/syft/source/resolver.go +++ b/syft/source/resolver.go @@ -20,8 +20,10 @@ type ContentResolver interface { // TODO: we should consider refactoring to return a set of io.Readers or file.Openers instead of the full contents themselves (allow for optional buffering). } -// FileResolver knows how to get file.References for given string paths and globs +// FileResolver knows how to get a Location for given string paths and globs type FileResolver interface { + // HasPath indicates if the given path exists in the underlying source. + HasPath(path string) bool // FilesByPath fetches a set of file references which have the given path (for an image, there may be multiple matches) FilesByPath(paths ...string) ([]Location, error) // FilesByGlob fetches a set of file references which the given glob matches