diff --git a/cmd/cmd.go b/cmd/cmd.go index f59b08689..0c4a36327 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/anchore/syft/syft/presenter" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/anchore/stereoscope" "github.com/anchore/syft/internal/config" @@ -49,8 +49,8 @@ func setGlobalCliOptions() { // scan options flag := "scope" rootCmd.Flags().StringP( - "scope", "s", scope.SquashedScope.String(), - fmt.Sprintf("selection of layers to catalog, options=%v", scope.Options)) + "scope", "s", source.SquashedScope.String(), + fmt.Sprintf("selection of layers to catalog, options=%v", source.Options)) if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil { fmt.Printf("unable to bind flag '%s': %+v", flag, err) os.Exit(1) diff --git a/internal/config/config.go b/internal/config/config.go index f0faa32f5..ded48e198 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,7 +8,7 @@ import ( "github.com/adrg/xdg" "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/presenter" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/mitchellh/go-homedir" "github.com/sirupsen/logrus" "github.com/spf13/viper" @@ -23,7 +23,7 @@ type Application struct { ConfigPath string PresenterOpt presenter.Option Output string `mapstructure:"output"` - ScopeOpt scope.Option + ScopeOpt source.Scope Scope string `mapstructure:"scope"` Quiet bool `mapstructure:"quiet"` Log Logging `mapstructure:"log"` @@ -79,9 +79,9 @@ func (cfg *Application) Build() error { } cfg.PresenterOpt = presenterOption - // set the scope - scopeOption := scope.ParseOption(cfg.Scope) - if scopeOption == scope.UnknownScope { + // set the source + scopeOption := source.ParseOption(cfg.Scope) + if scopeOption == source.UnknownScope { return fmt.Errorf("bad --scope value '%s'", cfg.Scope) } cfg.ScopeOpt = scopeOption diff --git a/syft/cataloger/catalog.go b/syft/cataloger/catalog.go index 022f81855..0724be8b6 100644 --- a/syft/cataloger/catalog.go +++ b/syft/cataloger/catalog.go @@ -5,7 +5,7 @@ import ( "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/event" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/hashicorp/go-multierror" "github.com/wagoodman/go-partybus" "github.com/wagoodman/go-progress" @@ -32,11 +32,11 @@ func newMonitor() (*progress.Manual, *progress.Manual) { return &filesProcessed, &packagesDiscovered } -// Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages. +// Catalog a given source (container image or filesystem) with the given catalogers, returning all discovered packages. // In order to efficiently retrieve contents from a underlying container image the content fetch requests are // done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single // request. -func Catalog(resolver scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) { +func Catalog(resolver source.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) { catalog := pkg.NewCatalog() filesProcessed, packagesDiscovered := newMonitor() @@ -54,6 +54,8 @@ func Catalog(resolver scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, er log.Debugf("cataloger '%s' discovered '%d' packages", theCataloger.Name(), catalogedPackages) packagesDiscovered.N += int64(catalogedPackages) + // helper function to add synthesized information... + for _, p := range packages { catalog.Add(p) } diff --git a/syft/cataloger/cataloger.go b/syft/cataloger/cataloger.go index 0ee9f5be2..aba71dfb9 100644 --- a/syft/cataloger/cataloger.go +++ b/syft/cataloger/cataloger.go @@ -15,7 +15,7 @@ import ( "github.com/anchore/syft/syft/cataloger/rpmdb" "github.com/anchore/syft/syft/cataloger/ruby" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) // Cataloger describes behavior for an object to participate in parsing container image or file system @@ -25,7 +25,7 @@ type Cataloger interface { // Name returns a string that uniquely describes a cataloger Name() string // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source. - Catalog(resolver scope.Resolver) ([]pkg.Package, error) + Catalog(resolver source.Resolver) ([]pkg.Package, error) } // ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages. diff --git a/syft/cataloger/common/generic_cataloger.go b/syft/cataloger/common/generic_cataloger.go index e21270093..bc17d65ec 100644 --- a/syft/cataloger/common/generic_cataloger.go +++ b/syft/cataloger/common/generic_cataloger.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) // GenericCataloger implements the Catalog interface and is responsible for dispatching the proper parser function for @@ -53,7 +53,7 @@ func (c *GenericCataloger) clear() { } // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source. -func (c *GenericCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) { +func (c *GenericCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) { fileSelection := c.selectFiles(resolver) contents, err := resolver.MultipleFileContentsByRef(fileSelection...) if err != nil { @@ -63,7 +63,7 @@ func (c *GenericCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, erro } // SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging -func (c *GenericCataloger) selectFiles(resolver scope.FileResolver) []file.Reference { +func (c *GenericCataloger) selectFiles(resolver source.FileResolver) []file.Reference { // select by exact path for path, parser := range c.pathParsers { files, err := resolver.FilesByPath(file.Path(path)) diff --git a/syft/cataloger/deb/cataloger.go b/syft/cataloger/deb/cataloger.go index c10f0dd71..e9fdecf54 100644 --- a/syft/cataloger/deb/cataloger.go +++ b/syft/cataloger/deb/cataloger.go @@ -11,7 +11,7 @@ import ( "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) const ( @@ -33,7 +33,7 @@ func (c *Cataloger) Name() string { } // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files. -func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) { +func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) { dbFileMatches, err := resolver.FilesByGlob(dpkgStatusGlob) if err != nil { return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err) @@ -93,7 +93,7 @@ func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) { return pkgs, nil } -func fetchMd5Contents(resolver scope.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) { +func fetchMd5Contents(resolver source.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) { // fetch all MD5 file contents. This approach is more efficient than fetching each MD5 file one at a time var md5FileMatches []file.Reference @@ -146,7 +146,7 @@ func fetchMd5Contents(resolver scope.Resolver, dbRef file.Reference, pkgs []pkg. return contentsByName, refsByName, nil } -func fetchCopyrightContents(resolver scope.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) { +func fetchCopyrightContents(resolver source.Resolver, dbRef file.Reference, pkgs []pkg.Package) (map[string]io.Reader, map[string]file.Reference, error) { // fetch all copyright file contents. This approach is more efficient than fetching each copyright file one at a time var copyrightFileMatches []file.Reference diff --git a/syft/cataloger/deb/cataloger_test.go b/syft/cataloger/deb/cataloger_test.go index fb36c59e5..34ada8e75 100644 --- a/syft/cataloger/deb/cataloger_test.go +++ b/syft/cataloger/deb/cataloger_test.go @@ -5,7 +5,7 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/go-test/deep" ) @@ -54,7 +54,7 @@ func TestDpkgCataloger(t *testing.T) { img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-dpkg") defer cleanup() - s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + s, err := source.NewFromImage(img, source.AllLayersScope) if err != nil { t.Fatal(err) } diff --git a/syft/cataloger/python/package_cataloger.go b/syft/cataloger/python/package_cataloger.go index 051933987..bef3a891f 100644 --- a/syft/cataloger/python/package_cataloger.go +++ b/syft/cataloger/python/package_cataloger.go @@ -12,7 +12,7 @@ import ( "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) const ( @@ -33,7 +33,7 @@ func (c *PackageCataloger) Name() string { } // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing python egg and wheel installations. -func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) { +func (c *PackageCataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) { // nolint:prealloc var fileMatches []file.Reference @@ -59,7 +59,7 @@ func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, erro } // catalogEggOrWheel takes the primary metadata file reference and returns the python package it represents. -func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRef file.Reference) (*pkg.Package, error) { +func (c *PackageCataloger) catalogEggOrWheel(resolver source.Resolver, metadataRef file.Reference) (*pkg.Package, error) { metadata, sources, err := c.assembleEggOrWheelMetadata(resolver, metadataRef) if err != nil { return nil, err @@ -84,7 +84,7 @@ func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRe } // fetchRecordFiles finds a corresponding RECORD file for the given python package metadata file and returns the set of file records contained. -func (c *PackageCataloger) fetchRecordFiles(resolver scope.Resolver, metadataRef file.Reference) (files []pkg.PythonFileRecord, sources []file.Reference, err error) { +func (c *PackageCataloger) fetchRecordFiles(resolver source.Resolver, metadataRef file.Reference) (files []pkg.PythonFileRecord, sources []file.Reference, err error) { // we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory // or for an image... for an image the METADATA file may be present within multiple layers, so it is important // to reconcile the RECORD path to the same layer (or the next adjacent lower layer). @@ -116,7 +116,7 @@ func (c *PackageCataloger) fetchRecordFiles(resolver scope.Resolver, metadataRef } // fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained. -func (c *PackageCataloger) fetchTopLevelPackages(resolver scope.Resolver, metadataRef file.Reference) (pkgs []string, sources []file.Reference, err error) { +func (c *PackageCataloger) fetchTopLevelPackages(resolver source.Resolver, metadataRef file.Reference) (pkgs []string, sources []file.Reference, err error) { // a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages parentDir := filepath.Dir(string(metadataRef.Path)) topLevelPath := filepath.Join(parentDir, "top_level.txt") @@ -149,7 +149,7 @@ func (c *PackageCataloger) fetchTopLevelPackages(resolver scope.Resolver, metada } // assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from. -func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver scope.Resolver, metadataRef file.Reference) (*pkg.PythonPackageMetadata, []file.Reference, error) { +func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver source.Resolver, metadataRef file.Reference) (*pkg.PythonPackageMetadata, []file.Reference, error) { var sources = []file.Reference{metadataRef} metadataContents, err := resolver.FileContentsByRef(metadataRef) diff --git a/syft/cataloger/rpmdb/cataloger.go b/syft/cataloger/rpmdb/cataloger.go index 6563f076e..c36ac099b 100644 --- a/syft/cataloger/rpmdb/cataloger.go +++ b/syft/cataloger/rpmdb/cataloger.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) const ( @@ -28,7 +28,7 @@ func (c *Cataloger) Name() string { } // Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing rpm db installation. -func (c *Cataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) { +func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) { fileMatches, err := resolver.FilesByGlob(packagesGlob) if err != nil { return nil, fmt.Errorf("failed to find rpmdb's by glob: %w", err) diff --git a/syft/cataloger/rpmdb/parse_rpmdb.go b/syft/cataloger/rpmdb/parse_rpmdb.go index 5a88b45cc..7ec9bc206 100644 --- a/syft/cataloger/rpmdb/parse_rpmdb.go +++ b/syft/cataloger/rpmdb/parse_rpmdb.go @@ -9,6 +9,7 @@ import ( "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" rpmdb "github.com/anchore/go-rpmdb/pkg" "github.com/anchore/syft/internal" @@ -80,7 +81,7 @@ func parseRpmDB(resolver scope.FileResolver, dbRef file.Reference, reader io.Rea return allPkgs, nil } -func extractRpmdbFileRecords(resolver scope.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) { +func extractRpmdbFileRecords(resolver source.FileResolver, entry *rpmdb.PackageInfo) ([]pkg.RpmdbFileRecord, error) { var records = make([]pkg.RpmdbFileRecord, 0) for _, record := range entry.Files { diff --git a/syft/distro/identify.go b/syft/distro/identify.go index ec7cbcb2a..8868c136b 100644 --- a/syft/distro/identify.go +++ b/syft/distro/identify.go @@ -6,7 +6,7 @@ import ( "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) // returns a distro or nil @@ -18,7 +18,7 @@ type parseEntry struct { } // Identify parses distro-specific files to determine distro metadata like version and release. -func Identify(resolver scope.Resolver) Distro { +func Identify(resolver source.Resolver) Distro { distro := NewUnknownDistro() identityFiles := []parseEntry{ diff --git a/syft/distro/identify_test.go b/syft/distro/identify_test.go index 4371e20dc..2ed9fbc46 100644 --- a/syft/distro/identify_test.go +++ b/syft/distro/identify_test.go @@ -8,7 +8,7 @@ import ( "github.com/anchore/syft/internal" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) func TestIdentifyDistro(t *testing.T) { @@ -84,9 +84,9 @@ func TestIdentifyDistro(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - s, err := scope.NewScopeFromDir(test.fixture) + s, err := source.NewFromDirectory(test.fixture) if err != nil { - t.Fatalf("unable to produce a new scope for testing: %s", test.fixture) + t.Fatalf("unable to produce a new source for testing: %s", test.fixture) } d := Identify(s.Resolver) diff --git a/syft/lib.go b/syft/lib.go index 87f69fe1e..d2ba2e04e 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -7,8 +7,8 @@ Here is what the main execution path for syft does: 2. Invoke all catalogers to catalog the image, adding discovered packages to a single catalog object 3. Invoke a single presenter to show the contents of the catalog -A Scope object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer), -providing a way to inspect paths and file content within the image. The Scope object, not the image object, is used +A Source object encapsulates the image object to be cataloged and the user options (catalog all layers vs. squashed layer), +providing a way to inspect paths and file content within the image. The Source object, not the image object, is used throughout the main execution path. This abstraction allows for decoupling of what is cataloged (a docker image, an OCI image, a filesystem, etc) and how it is cataloged (the individual catalogers). @@ -28,15 +28,15 @@ import ( "github.com/anchore/syft/syft/logger" "github.com/anchore/syft/syft/pkg" jsonPresenter "github.com/anchore/syft/syft/presenter/json" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/wagoodman/go-partybus" ) -// Catalog the given image from a particular perspective (e.g. squashed scope, all-layers scope). Returns the discovered -// set of packages, the identified Linux distribution, and the scope object used to wrap the data source. -func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scope, *distro.Distro, error) { +// Catalog the given image from a particular perspective (e.g. squashed source, all-layers source). Returns the discovered +// set of packages, the identified Linux distribution, and the source object used to wrap the data source. +func Catalog(userInput string, scoptOpt source.Scope) (*pkg.Catalog, *source.Source, *distro.Distro, error) { log.Info("cataloging image") - s, cleanup, err := scope.NewScope(userInput, scoptOpt) + s, cleanup, err := source.NewSource(userInput, scoptOpt) defer cleanup() if err != nil { return nil, nil, nil, err @@ -53,8 +53,8 @@ func Catalog(userInput string, scoptOpt scope.Option) (*pkg.Catalog, *scope.Scop } // IdentifyDistro attempts to discover what the underlying Linux distribution may be from the available flat files -// provided by the given scope object. If results are inconclusive a "UnknownDistro" Type is returned. -func IdentifyDistro(s scope.Scope) distro.Distro { +// provided by the given source object. If results are inconclusive a "UnknownDistro" Type is returned. +func IdentifyDistro(s source.Source) distro.Distro { d := distro.Identify(s.Resolver) if d.Type != distro.UnknownDistroType { log.Infof("identified distro: %s", d.String()) @@ -64,16 +64,16 @@ func IdentifyDistro(s scope.Scope) distro.Distro { return d } -// Catalog the given scope, which may represent a container image or filesystem. Returns the discovered set of packages. -func CatalogFromScope(s scope.Scope) (*pkg.Catalog, error) { +// Catalog the given source, which may represent a container image or filesystem. Returns the discovered set of packages. +func CatalogFromScope(s source.Source) (*pkg.Catalog, error) { log.Info("building the catalog") // conditionally have two sets of catalogers var catalogers []cataloger.Cataloger switch s.Scheme { - case scope.ImageScheme: + case source.ImageScheme: catalogers = cataloger.ImageCatalogers() - case scope.DirectoryScheme: + case source.DirectoryScheme: catalogers = cataloger.DirectoryCatalogers() default: return nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", s.Scheme) diff --git a/syft/pkg/package.go b/syft/pkg/package.go index 805c9fb6c..3b81f46a2 100644 --- a/syft/pkg/package.go +++ b/syft/pkg/package.go @@ -17,11 +17,12 @@ type ID int64 // Package represents an application or library that has been bundled into a distributable format. type Package struct { - id ID // uniquely identifies a package, set by the cataloger - Name string `json:"manifest"` // the package name - Version string `json:"version"` // the version of the package - FoundBy string `json:"foundBy"` // the specific cataloger that discovered this package - Source []file.Reference `json:"sources"` // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) + id ID // uniquely identifies a package, set by the cataloger + Name string `json:"manifest"` // the package name + Version string `json:"version"` // the version of the package + FoundBy string `json:"foundBy"` // the specific cataloger that discovered this package + Source []file.Reference `json:"-"` // the locations that lead to the discovery of this package (note: this is not necessarily the locations that make up this package) + Location interface{} `json:"locations"` // TODO: should we move licenses into metadata? Licenses []string `json:"licenses"` // licenses discovered with the package metadata Language Language `json:"language"` // the language ecosystem this package belongs to (e.g. JavaScript, Python, etc) diff --git a/syft/presenter/cyclonedx/component.go b/syft/presenter/cyclonedx/component.go index 2217bb5d1..e540ef9ea 100644 --- a/syft/presenter/cyclonedx/component.go +++ b/syft/presenter/cyclonedx/component.go @@ -15,7 +15,7 @@ type Component struct { Description string `xml:"description,omitempty"` // A description of the component Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec - // TODO: scope, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences + // TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences // TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.) } diff --git a/syft/presenter/cyclonedx/presenter.go b/syft/presenter/cyclonedx/presenter.go index 1b8accebe..22d9a8238 100644 --- a/syft/presenter/cyclonedx/presenter.go +++ b/syft/presenter/cyclonedx/presenter.go @@ -11,21 +11,21 @@ import ( "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) -// Presenter writes a CycloneDX report from the given Catalog and Scope contents +// Presenter writes a CycloneDX report from the given Catalog and Source contents type Presenter struct { catalog *pkg.Catalog - scope scope.Scope + source source.Source distro distro.Distro } -// NewPresenter creates a CycloneDX presenter from the given Catalog and Scope objects. -func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Presenter { +// NewPresenter creates a CycloneDX presenter from the given Catalog and Source objects. +func NewPresenter(catalog *pkg.Catalog, s source.Source, d distro.Distro) *Presenter { return &Presenter{ catalog: catalog, - scope: s, + source: s, distro: d, } } @@ -34,8 +34,8 @@ func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Present func (pres *Presenter) Present(output io.Writer) error { bom := NewDocumentFromCatalog(pres.catalog, pres.distro) - switch src := pres.scope.Source.(type) { - case scope.DirSource: + switch src := pres.source.Target.(type) { + case source.DirSource: bom.BomDescriptor.Component = &BdComponent{ Component: Component{ Type: "file", @@ -43,7 +43,7 @@ func (pres *Presenter) Present(output io.Writer) error { Version: "", }, } - case scope.ImageSource: + case source.ImageSource: var imageID string var versionStr string if len(src.Img.Metadata.Tags) > 0 { diff --git a/syft/presenter/cyclonedx/presenter_test.go b/syft/presenter/cyclonedx/presenter_test.go index 39778d3c1..df8b3745c 100644 --- a/syft/presenter/cyclonedx/presenter_test.go +++ b/syft/presenter/cyclonedx/presenter_test.go @@ -12,7 +12,7 @@ import ( "github.com/anchore/go-testutils" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -57,7 +57,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) { }, }) - s, err := scope.NewScopeFromDir("/some/path") + s, err := source.NewFromDirectory("/some/path") if err != nil { t.Fatal(err) } @@ -147,7 +147,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) { }, }) - s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + s, err := source.NewFromImage(img, source.AllLayersScope) if err != nil { t.Fatal(err) } diff --git a/syft/presenter/json/artifact.go b/syft/presenter/json/artifact.go index a031e33d2..3c15e5353 100644 --- a/syft/presenter/json/artifact.go +++ b/syft/presenter/json/artifact.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Artifact struct { @@ -33,7 +33,7 @@ type ArtifactMetadataUnpacker struct { Metadata json.RawMessage `json:"metadata"` } -func NewArtifact(p *pkg.Package, s scope.Scope) (Artifact, error) { +func NewArtifact(p *pkg.Package, s source.Source) (Artifact, error) { locations, err := NewLocations(p, s) if err != nil { return Artifact{}, err diff --git a/syft/presenter/json/document.go b/syft/presenter/json/document.go index e1ca6dc51..78cb55ce2 100644 --- a/syft/presenter/json/document.go +++ b/syft/presenter/json/document.go @@ -7,7 +7,7 @@ import ( "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Document struct { @@ -22,7 +22,7 @@ type Descriptor struct { Name string `json:"name"` Version string `json:"version"` ReportTimestamp string `json:"reportTimestamp"` - // TODO: we should include scope option here as well (or in source) + // TODO: we should include source option here as well (or in source) } // Distribution provides information about a detected Linux Distribution @@ -32,7 +32,7 @@ type Distribution struct { IDLike string `json:"idLike"` } -func NewDocument(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) (Document, error) { +func NewDocument(catalog *pkg.Catalog, s source.Source, d distro.Distro) (Document, error) { src, err := NewSource(s) if err != nil { return Document{}, nil diff --git a/syft/presenter/json/image.go b/syft/presenter/json/image.go index bffa999bf..ee7796421 100644 --- a/syft/presenter/json/image.go +++ b/syft/presenter/json/image.go @@ -1,7 +1,7 @@ package json import ( - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Image struct { @@ -18,7 +18,7 @@ type Layer struct { Size int64 `json:"size"` } -func NewImage(src scope.ImageSource) *Image { +func NewImage(src source.ImageSource) *Image { // populate artifacts... tags := make([]string, len(src.Img.Metadata.Tags)) for idx, tag := range src.Img.Metadata.Tags { diff --git a/syft/presenter/json/location.go b/syft/presenter/json/location.go index 8a0fe1209..2321afa49 100644 --- a/syft/presenter/json/location.go +++ b/syft/presenter/json/location.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Locations interface{} @@ -14,9 +14,9 @@ type ImageLocation struct { LayerIndex uint `json:"layerIndex"` } -func NewLocations(p *pkg.Package, s scope.Scope) (Locations, error) { - switch src := s.Source.(type) { - case scope.ImageSource: +func NewLocations(p *pkg.Package, s source.Source) (Locations, error) { + switch src := s.Target.(type) { + case source.ImageSource: locations := make([]ImageLocation, len(p.Source)) for idx := range p.Source { entry, err := src.Img.FileCatalog.Get(p.Source[idx]) @@ -33,7 +33,7 @@ func NewLocations(p *pkg.Package, s scope.Scope) (Locations, error) { } return locations, nil - case scope.DirSource: + case source.DirSource: locations := make([]string, len(p.Source)) for idx := range p.Source { locations[idx] = string(p.Source[idx].Path) diff --git a/syft/presenter/json/presenter.go b/syft/presenter/json/presenter.go index 7c5ee5407..beb2e689b 100644 --- a/syft/presenter/json/presenter.go +++ b/syft/presenter/json/presenter.go @@ -6,25 +6,25 @@ import ( "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Presenter struct { catalog *pkg.Catalog - scope scope.Scope + source source.Source distro distro.Distro } -func NewPresenter(catalog *pkg.Catalog, s scope.Scope, d distro.Distro) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s source.Source, d distro.Distro) *Presenter { return &Presenter{ catalog: catalog, - scope: s, + source: s, distro: d, } } func (pres *Presenter) Present(output io.Writer) error { - doc, err := NewDocument(pres.catalog, pres.scope, pres.distro) + doc, err := NewDocument(pres.catalog, pres.source, pres.distro) if err != nil { return err } diff --git a/syft/presenter/json/presenter_test.go b/syft/presenter/json/presenter_test.go index f60d53dba..c7f389df5 100644 --- a/syft/presenter/json/presenter_test.go +++ b/syft/presenter/json/presenter_test.go @@ -10,7 +10,7 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -53,7 +53,7 @@ func TestJsonDirsPresenter(t *testing.T) { }, }) d := distro.NewUnknownDistro() - s, err := scope.NewScopeFromDir("/some/path") + s, err := source.NewFromDirectory("/some/path") if err != nil { t.Fatal(err) } @@ -124,7 +124,7 @@ func TestJsonImgsPresenter(t *testing.T) { }, }) - s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + s, err := source.NewFromImage(img, source.AllLayersScope) d := distro.NewUnknownDistro() pres := NewPresenter(catalog, s, d) diff --git a/syft/presenter/json/source.go b/syft/presenter/json/source.go index d103bc567..6172f9bb4 100644 --- a/syft/presenter/json/source.go +++ b/syft/presenter/json/source.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Source struct { @@ -17,14 +17,14 @@ type SourceUnpacker struct { Target json.RawMessage `json:"target"` } -func NewSource(s scope.Scope) (Source, error) { - switch src := s.Source.(type) { - case scope.ImageSource: +func NewSource(s source.Source) (Source, error) { + switch src := s.Target.(type) { + case source.ImageSource: return Source{ Type: "image", Target: NewImage(src), }, nil - case scope.DirSource: + case source.DirSource: return Source{ Type: "directory", Target: src.Path, diff --git a/syft/presenter/presenter.go b/syft/presenter/presenter.go index 65184a74e..56f00f9e6 100644 --- a/syft/presenter/presenter.go +++ b/syft/presenter/presenter.go @@ -15,7 +15,7 @@ import ( "github.com/anchore/syft/syft/presenter/json" "github.com/anchore/syft/syft/presenter/table" "github.com/anchore/syft/syft/presenter/text" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) // Presenter defines the expected behavior for an object responsible for displaying arbitrary input and processed data @@ -25,7 +25,7 @@ type Presenter interface { } // GetPresenter returns a presenter for images or directories -func GetPresenter(option Option, s scope.Scope, catalog *pkg.Catalog, d *distro.Distro) Presenter { +func GetPresenter(option Option, s source.Source, catalog *pkg.Catalog, d *distro.Distro) Presenter { switch option { case JSONPresenter: return json.NewPresenter(catalog, s, *d) diff --git a/syft/presenter/table/presenter.go b/syft/presenter/table/presenter.go index 29f4631c8..6af222559 100644 --- a/syft/presenter/table/presenter.go +++ b/syft/presenter/table/presenter.go @@ -9,18 +9,18 @@ import ( "github.com/olekukonko/tablewriter" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Presenter struct { catalog *pkg.Catalog - scope scope.Scope + source source.Source } -func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s source.Source) *Presenter { return &Presenter{ catalog: catalog, - scope: s, + source: s, } } diff --git a/syft/presenter/table/presenter_test.go b/syft/presenter/table/presenter_test.go index 4958b1943..afd8ac465 100644 --- a/syft/presenter/table/presenter_test.go +++ b/syft/presenter/table/presenter_test.go @@ -3,14 +3,15 @@ package table import ( "bytes" "flag" - "github.com/go-test/deep" "testing" + "github.com/go-test/deep" + "github.com/anchore/go-testutils" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -43,7 +44,7 @@ func TestTablePresenter(t *testing.T) { Type: pkg.DebPkg, }) - s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + s, err := source.NewFromImage(img, source.AllLayersScope) pres := NewPresenter(catalog, s) // run presenter diff --git a/syft/presenter/text/presenter.go b/syft/presenter/text/presenter.go index 3291370d9..aeaf26d67 100644 --- a/syft/presenter/text/presenter.go +++ b/syft/presenter/text/presenter.go @@ -7,18 +7,18 @@ import ( "text/tabwriter" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) type Presenter struct { catalog *pkg.Catalog - scope scope.Scope + source source.Source } -func NewPresenter(catalog *pkg.Catalog, s scope.Scope) *Presenter { +func NewPresenter(catalog *pkg.Catalog, s source.Source) *Presenter { return &Presenter{ catalog: catalog, - scope: s, + source: s, } } @@ -28,10 +28,10 @@ func (pres *Presenter) Present(output io.Writer) error { w := new(tabwriter.Writer) w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight) - switch src := pres.scope.Source.(type) { - case scope.DirSource: + switch src := pres.source.Target.(type) { + case source.DirSource: fmt.Fprintln(w, fmt.Sprintf("[Path: %s]", src.Path)) - case scope.ImageSource: + case source.ImageSource: fmt.Fprintln(w, "[Image]") for idx, l := range src.Img.Layers { diff --git a/syft/presenter/text/presenter_test.go b/syft/presenter/text/presenter_test.go index 96dbd84c1..9fdcd860a 100644 --- a/syft/presenter/text/presenter_test.go +++ b/syft/presenter/text/presenter_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/sergi/go-diff/diffmatchpatch" ) @@ -32,9 +32,9 @@ func TestTextDirPresenter(t *testing.T) { Type: pkg.DebPkg, }) - s, err := scope.NewScopeFromDir("/some/path") + s, err := source.NewFromDirectory("/some/path") if err != nil { - t.Fatalf("unable to create scope: %+v", err) + t.Fatalf("unable to create source: %+v", err) } pres := NewPresenter(catalog, s) @@ -98,7 +98,7 @@ func TestTextImgPresenter(t *testing.T) { l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53" } - s, err := scope.NewScopeFromImage(img, scope.AllLayersScope) + s, err := source.NewFromImage(img, source.AllLayersScope) if err != nil { t.Fatal(err) } diff --git a/syft/scope/resolvers/docs.go b/syft/scope/resolvers/docs.go deleted file mode 100644 index fe8f17e76..000000000 --- a/syft/scope/resolvers/docs.go +++ /dev/null @@ -1,4 +0,0 @@ -/* -Package resolvers provides concrete implementations for the scope.Resolver interface for all supported data sources and scope options. -*/ -package resolvers diff --git a/syft/scope/scope.go b/syft/scope/scope.go deleted file mode 100644 index bf2ed74f3..000000000 --- a/syft/scope/scope.go +++ /dev/null @@ -1,166 +0,0 @@ -/* -Package scope provides an abstraction to allow a user to loosely define a data source to catalog and expose a common interface that -catalogers and use explore and analyze data from the data source. All valid (cataloggable) data sources are defined -within this package. -*/ -package scope - -import ( - "fmt" - "strings" - - "github.com/mitchellh/go-homedir" - - "github.com/spf13/afero" - - "github.com/anchore/stereoscope" - - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/syft/scope/resolvers" -) - -const ( - UnknownScheme Scheme = "unknown-scheme" - DirectoryScheme Scheme = "directory-scheme" - ImageScheme Scheme = "image-scheme" -) - -type Scheme string - -// ImageSource represents a data source that is a container image -type ImageSource struct { - Img *image.Image // the image object to be cataloged -} - -// DirSource represents a data source that is a filesystem directory tree -type DirSource struct { - Path string // the root path to be cataloged -} - -// Scope is an object that captures the data source to be cataloged, configuration, and a specific resolver used -// in cataloging (based on the data source and configuration) -type Scope struct { - Option Option // specific perspective to catalog - Resolver Resolver // a Resolver object to use in file path/glob resolution and file contents resolution - Source interface{} // the specific source object to be cataloged - Scheme Scheme // the source data scheme type (directory or image) -} - -// NewScope produces a Scope based on userInput like dir: or image:tag -func NewScope(userInput string, o Option) (Scope, func(), error) { - fs := afero.NewOsFs() - parsedScheme, location, err := detectScheme(fs, image.DetectSource, userInput) - if err != nil { - return Scope{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err) - } - - switch parsedScheme { - case DirectoryScheme: - fileMeta, err := fs.Stat(location) - if err != nil { - return Scope{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err) - } - - if !fileMeta.IsDir() { - return Scope{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err) - } - - s, err := NewScopeFromDir(location) - if err != nil { - return Scope{}, func() {}, fmt.Errorf("could not populate scope from path=%q: %w", location, err) - } - return s, func() {}, nil - - case ImageScheme: - img, err := stereoscope.GetImage(location) - cleanup := func() { - stereoscope.Cleanup() - } - - if err != nil || img == nil { - return Scope{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err) - } - - s, err := NewScopeFromImage(img, o) - if err != nil { - return Scope{}, cleanup, fmt.Errorf("could not populate scope with image: %w", err) - } - return s, cleanup, nil - } - - return Scope{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput) -} - -// NewScopeFromDir creates a new scope object tailored to catalog a given filesystem directory recursively. -func NewScopeFromDir(path string) (Scope, error) { - return Scope{ - Resolver: &resolvers.DirectoryResolver{ - Path: path, - }, - Source: DirSource{ - Path: path, - }, - Scheme: DirectoryScheme, - }, nil -} - -// NewScopeFromImage creates a new scope object tailored to catalog a given container image, relative to the -// option given (e.g. all-layers, squashed, etc) -func NewScopeFromImage(img *image.Image, option Option) (Scope, error) { - if img == nil { - return Scope{}, fmt.Errorf("no image given") - } - - resolver, err := getImageResolver(img, option) - if err != nil { - return Scope{}, fmt.Errorf("could not determine file resolver: %w", err) - } - - return Scope{ - Option: option, - Resolver: resolver, - Source: ImageSource{ - Img: img, - }, - Scheme: ImageScheme, - }, nil -} - -type sourceDetector func(string) (image.Source, string, error) - -func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, string, error) { - if strings.HasPrefix(userInput, "dir:") { - // blindly trust the user's scheme - dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:")) - if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to expand directory path: %w", err) - } - return DirectoryScheme, dirLocation, nil - } - - // we should attempt to let stereoscope determine what the source is first --just because the source is a valid directory - // doesn't mean we yet know if it is an OCI layout directory (to be treated as an image) or if it is a generic filesystem directory. - source, imageSpec, err := imageDetector(userInput) - if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err) - } - - if source == image.UnknownSource { - dirLocation, err := homedir.Expand(userInput) - if err != nil { - return UnknownScheme, "", fmt.Errorf("unable to expand potential directory path: %w", err) - } - - fileMeta, err := fs.Stat(dirLocation) - if err != nil { - return UnknownScheme, "", nil - } - - if fileMeta.IsDir() { - return DirectoryScheme, dirLocation, nil - } - return UnknownScheme, "", nil - } - - return ImageScheme, imageSpec, nil -} diff --git a/syft/scope/resolvers/all_layers_resolver.go b/syft/source/all_layers_resolver.go similarity index 98% rename from syft/scope/resolvers/all_layers_resolver.go rename to syft/source/all_layers_resolver.go index a1c57ccda..a050ad616 100644 --- a/syft/scope/resolvers/all_layers_resolver.go +++ b/syft/source/all_layers_resolver.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "archive/tar" @@ -8,7 +8,7 @@ import ( "github.com/anchore/stereoscope/pkg/image" ) -// AllLayersResolver implements path and content access for the AllLayers scope option for container image data sources. +// AllLayersResolver implements path and content access for the AllLayers source option for container image data sources. type AllLayersResolver struct { img *image.Image layers []int @@ -41,7 +41,7 @@ func (r *AllLayersResolver) fileByRef(ref file.Reference, uniqueFileIDs file.Ref if entry.Metadata.TypeFlag == tar.TypeLink || entry.Metadata.TypeFlag == tar.TypeSymlink { // a link may resolve in this layer or higher, assuming a squashed tree is used to search - // we should search all possible resolutions within the valid scope + // we should search all possible resolutions within the valid source for _, subLayerIdx := range r.layers[layerIdx:] { resolvedRef, err := r.img.ResolveLinkByLayerSquash(ref, subLayerIdx) if err != nil { diff --git a/syft/scope/resolvers/all_layers_resolver_test.go b/syft/source/all_layers_resolver_test.go similarity index 99% rename from syft/scope/resolvers/all_layers_resolver_test.go rename to syft/source/all_layers_resolver_test.go index e99ebd8a6..e8076f4cb 100644 --- a/syft/scope/resolvers/all_layers_resolver_test.go +++ b/syft/source/all_layers_resolver_test.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "testing" diff --git a/syft/scope/resolvers/directory_resolver.go b/syft/source/directory_resolver.go similarity index 99% rename from syft/scope/resolvers/directory_resolver.go rename to syft/source/directory_resolver.go index 5677c6dde..89b717db0 100644 --- a/syft/scope/resolvers/directory_resolver.go +++ b/syft/source/directory_resolver.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "fmt" diff --git a/syft/scope/resolvers/directory_resolver_test.go b/syft/source/directory_resolver_test.go similarity index 99% rename from syft/scope/resolvers/directory_resolver_test.go rename to syft/source/directory_resolver_test.go index 809148da8..fa234c10e 100644 --- a/syft/scope/resolvers/directory_resolver_test.go +++ b/syft/source/directory_resolver_test.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "testing" diff --git a/syft/scope/resolvers/image_squash_resolver.go b/syft/source/image_squash_resolver.go similarity index 98% rename from syft/scope/resolvers/image_squash_resolver.go rename to syft/source/image_squash_resolver.go index be1b7fe80..07a9a54d9 100644 --- a/syft/scope/resolvers/image_squash_resolver.go +++ b/syft/source/image_squash_resolver.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/anchore/stereoscope/pkg/image" ) -// ImageSquashResolver implements path and content access for the Squashed scope option for container image data sources. +// ImageSquashResolver implements path and content access for the Squashed source option for container image data sources. type ImageSquashResolver struct { img *image.Image } diff --git a/syft/scope/resolvers/image_squash_resolver_test.go b/syft/source/image_squash_resolver_test.go similarity index 99% rename from syft/scope/resolvers/image_squash_resolver_test.go rename to syft/source/image_squash_resolver_test.go index dee432125..88f8074a2 100644 --- a/syft/scope/resolvers/image_squash_resolver_test.go +++ b/syft/source/image_squash_resolver_test.go @@ -1,4 +1,4 @@ -package resolvers +package source import ( "testing" diff --git a/syft/scope/resolver.go b/syft/source/resolver.go similarity index 86% rename from syft/scope/resolver.go rename to syft/source/resolver.go index d32740f3b..6af9feaa2 100644 --- a/syft/scope/resolver.go +++ b/syft/source/resolver.go @@ -1,11 +1,10 @@ -package scope +package source import ( "fmt" "github.com/anchore/stereoscope/pkg/file" "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/syft/scope/resolvers" ) // Resolver is an interface that encompasses how to get specific file references and file contents for a generic data source. @@ -32,13 +31,13 @@ type FileResolver interface { RelativeFileByPath(reference file.Reference, path string) (*file.Reference, error) } -// getImageResolver returns the appropriate resolve for a container image given the scope option -func getImageResolver(img *image.Image, option Option) (Resolver, error) { +// getImageResolver returns the appropriate resolve for a container image given the source option +func getImageResolver(img *image.Image, option Scope) (Resolver, error) { switch option { case SquashedScope: - return resolvers.NewImageSquashResolver(img) + return NewImageSquashResolver(img) case AllLayersScope: - return resolvers.NewAllLayersResolver(img) + return NewAllLayersResolver(img) default: return nil, fmt.Errorf("bad option provided: %+v", option) } diff --git a/syft/source/scheme.go b/syft/source/scheme.go new file mode 100644 index 000000000..c12a1f69f --- /dev/null +++ b/syft/source/scheme.go @@ -0,0 +1,55 @@ +package source + +import ( + "fmt" + "strings" + + "github.com/anchore/stereoscope/pkg/image" + "github.com/mitchellh/go-homedir" + "github.com/spf13/afero" +) + +type Scheme string + +const ( + UnknownScheme Scheme = "unknown-scheme" + DirectoryScheme Scheme = "directory-scheme" + ImageScheme Scheme = "image-scheme" +) + +func detectScheme(fs afero.Fs, imageDetector sourceDetector, userInput string) (Scheme, string, error) { + if strings.HasPrefix(userInput, "dir:") { + // blindly trust the user's scheme + dirLocation, err := homedir.Expand(strings.TrimPrefix(userInput, "dir:")) + if err != nil { + return UnknownScheme, "", fmt.Errorf("unable to expand directory path: %w", err) + } + return DirectoryScheme, dirLocation, nil + } + + // we should attempt to let stereoscope determine what the source is first --just because the source is a valid directory + // doesn't mean we yet know if it is an OCI layout directory (to be treated as an image) or if it is a generic filesystem directory. + source, imageSpec, err := imageDetector(userInput) + if err != nil { + return UnknownScheme, "", fmt.Errorf("unable to detect the scheme from %q: %w", userInput, err) + } + + if source == image.UnknownSource { + dirLocation, err := homedir.Expand(userInput) + if err != nil { + return UnknownScheme, "", fmt.Errorf("unable to expand potential directory path: %w", err) + } + + fileMeta, err := fs.Stat(dirLocation) + if err != nil { + return UnknownScheme, "", nil + } + + if fileMeta.IsDir() { + return DirectoryScheme, dirLocation, nil + } + return UnknownScheme, "", nil + } + + return ImageScheme, imageSpec, nil +} diff --git a/syft/scope/option.go b/syft/source/scope.go similarity index 75% rename from syft/scope/option.go rename to syft/source/scope.go index fcfd49302..d870da2da 100644 --- a/syft/scope/option.go +++ b/syft/source/scope.go @@ -1,14 +1,14 @@ -package scope +package source import "strings" const ( - UnknownScope Option = iota + UnknownScope Scope = iota SquashedScope AllLayersScope ) -type Option int +type Scope int var optionStr = []string{ "UnknownScope", @@ -16,12 +16,12 @@ var optionStr = []string{ "AllLayers", } -var Options = []Option{ +var Options = []Scope{ SquashedScope, AllLayersScope, } -func ParseOption(userStr string) Option { +func ParseOption(userStr string) Scope { switch strings.ToLower(userStr) { case strings.ToLower(SquashedScope.String()): return SquashedScope @@ -31,7 +31,7 @@ func ParseOption(userStr string) Option { return UnknownScope } -func (o Option) String() string { +func (o Scope) String() string { if int(o) >= len(optionStr) || o < 0 { return optionStr[0] } diff --git a/syft/scope/option_test.go b/syft/source/scope_test.go similarity index 55% rename from syft/scope/option_test.go rename to syft/source/scope_test.go index a684c2260..0c210e013 100644 --- a/syft/scope/option_test.go +++ b/syft/source/scope_test.go @@ -1,4 +1,4 @@ -package scope +package source import ( "fmt" @@ -6,12 +6,12 @@ import ( ) func TestOptionStringerBoundary(t *testing.T) { - var _ fmt.Stringer = Option(0) + var _ fmt.Stringer = Scope(0) for _, c := range []int{-1, 0, 3} { - option := Option(c) + option := Scope(c) if option.String() != UnknownScope.String() { - t.Errorf("expected Option(%d) to be unknown, found '%+v'", c, option) + t.Errorf("expected Scope(%d) to be unknown, found '%+v'", c, option) } } } diff --git a/syft/source/source.go b/syft/source/source.go new file mode 100644 index 000000000..b3e49d22d --- /dev/null +++ b/syft/source/source.go @@ -0,0 +1,117 @@ +/* +Package source provides an abstraction to allow a user to loosely define a data source to catalog and expose a common interface that +catalogers and use explore and analyze data from the data source. All valid (cataloggable) data sources are defined +within this package. +*/ +package source + +import ( + "fmt" + + "github.com/spf13/afero" + + "github.com/anchore/stereoscope" + + "github.com/anchore/stereoscope/pkg/image" +) + +// ImageSource represents a data source that is a container image +type ImageSource struct { + Img *image.Image // the image object to be cataloged +} + +// DirSource represents a data source that is a filesystem directory tree +type DirSource struct { + Path string // the root path to be cataloged +} + +// Source is an object that captures the data source to be cataloged, configuration, and a specific resolver used +// in cataloging (based on the data source and configuration) +type Source struct { + Scope Scope // specific perspective to catalog + Resolver Resolver // a Resolver object to use in file path/glob resolution and file contents resolution + Target interface{} // the specific source object to be cataloged + Scheme Scheme // the source data scheme type (directory or image) +} + +type sourceDetector func(string) (image.Source, string, error) + +// NewSource produces a Source based on userInput like dir: or image:tag +func NewSource(userInput string, o Scope) (Source, func(), error) { + fs := afero.NewOsFs() + parsedScheme, location, err := detectScheme(fs, image.DetectSource, userInput) + if err != nil { + return Source{}, func() {}, fmt.Errorf("unable to parse input=%q: %w", userInput, err) + } + + switch parsedScheme { + case DirectoryScheme: + fileMeta, err := fs.Stat(location) + if err != nil { + return Source{}, func() {}, fmt.Errorf("unable to stat dir=%q: %w", location, err) + } + + if !fileMeta.IsDir() { + return Source{}, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", location, err) + } + + s, err := NewFromDirectory(location) + if err != nil { + return Source{}, func() {}, fmt.Errorf("could not populate source from path=%q: %w", location, err) + } + return s, func() {}, nil + + case ImageScheme: + img, err := stereoscope.GetImage(location) + cleanup := func() { + stereoscope.Cleanup() + } + + if err != nil || img == nil { + return Source{}, cleanup, fmt.Errorf("could not fetch image '%s': %w", location, err) + } + + s, err := NewFromImage(img, o) + if err != nil { + return Source{}, cleanup, fmt.Errorf("could not populate source with image: %w", err) + } + return s, cleanup, nil + } + + return Source{}, func() {}, fmt.Errorf("unable to process input for scanning: '%s'", userInput) +} + +// NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively. +func NewFromDirectory(path string) (Source, error) { + return Source{ + Resolver: &DirectoryResolver{ + Path: path, + }, + Target: DirSource{ + Path: path, + }, + Scheme: DirectoryScheme, + }, nil +} + +// NewFromImage creates a new source object tailored to catalog a given container image, relative to the +// option given (e.g. all-layers, squashed, etc) +func NewFromImage(img *image.Image, option Scope) (Source, error) { + if img == nil { + return Source{}, fmt.Errorf("no image given") + } + + resolver, err := getImageResolver(img, option) + if err != nil { + return Source{}, fmt.Errorf("could not determine file resolver: %w", err) + } + + return Source{ + Scope: option, + Resolver: resolver, + Target: ImageSource{ + Img: img, + }, + Scheme: ImageScheme, + }, nil +} diff --git a/syft/scope/scope_test.go b/syft/source/source_test.go similarity index 95% rename from syft/scope/scope_test.go rename to syft/source/source_test.go index b6656fc07..27668b1cf 100644 --- a/syft/scope/scope_test.go +++ b/syft/source/source_test.go @@ -1,4 +1,4 @@ -package scope +package source import ( "os" @@ -12,7 +12,7 @@ import ( func TestNewScopeFromImageFails(t *testing.T) { t.Run("no image given", func(t *testing.T) { - _, err := NewScopeFromImage(nil, AllLayersScope) + _, err := NewFromImage(nil, AllLayersScope) if err == nil { t.Errorf("expected an error condition but none was given") } @@ -23,7 +23,7 @@ func TestNewScopeFromImageUnknownOption(t *testing.T) { img := image.Image{} t.Run("unknown option is an error", func(t *testing.T) { - _, err := NewScopeFromImage(&img, UnknownScope) + _, err := NewFromImage(&img, UnknownScope) if err == nil { t.Errorf("expected an error condition but none was given") } @@ -36,10 +36,10 @@ func TestNewScopeFromImage(t *testing.T) { Layers: []*image.Layer{layer}, } - t.Run("create a new Scope object from image", func(t *testing.T) { - _, err := NewScopeFromImage(&img, AllLayersScope) + t.Run("create a new Source object from image", func(t *testing.T) { + _, err := NewFromImage(&img, AllLayersScope) if err != nil { - t.Errorf("unexpected error when creating a new Scope from img: %w", err) + t.Errorf("unexpected error when creating a new Source from img: %w", err) } }) } @@ -79,13 +79,13 @@ func TestDirectoryScope(t *testing.T) { } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewScopeFromDir(test.input) + p, err := NewFromDirectory(test.input) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } - if p.Source.(DirSource).Path != test.input { - t.Errorf("mismatched stringer: '%s' != '%s'", p.Source.(DirSource).Path, test.input) + if p.Target.(DirSource).Path != test.input { + t.Errorf("mismatched stringer: '%s' != '%s'", p.Target.(DirSource).Path, test.input) } refs, err := p.Resolver.FilesByPath(test.inputPaths...) @@ -123,7 +123,7 @@ func TestMultipleFileContentsByRefContents(t *testing.T) { } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewScopeFromDir(test.input) + p, err := NewFromDirectory(test.input) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } @@ -163,7 +163,7 @@ func TestMultipleFileContentsByRefNoContents(t *testing.T) { } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewScopeFromDir(test.input) + p, err := NewFromDirectory(test.input) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } @@ -208,7 +208,7 @@ func TestFilesByGlob(t *testing.T) { } for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - p, err := NewScopeFromDir(test.input) + p, err := NewFromDirectory(test.input) if err != nil { t.Errorf("could not create NewDirScope: %w", err) } diff --git a/syft/scope/resolvers/test-fixtures/image-symlinks/Dockerfile b/syft/source/test-fixtures/image-symlinks/Dockerfile similarity index 100% rename from syft/scope/resolvers/test-fixtures/image-symlinks/Dockerfile rename to syft/source/test-fixtures/image-symlinks/Dockerfile diff --git a/syft/scope/resolvers/test-fixtures/image-symlinks/file-1.txt b/syft/source/test-fixtures/image-symlinks/file-1.txt similarity index 100% rename from syft/scope/resolvers/test-fixtures/image-symlinks/file-1.txt rename to syft/source/test-fixtures/image-symlinks/file-1.txt diff --git a/syft/scope/resolvers/test-fixtures/image-symlinks/file-2.txt b/syft/source/test-fixtures/image-symlinks/file-2.txt similarity index 100% rename from syft/scope/resolvers/test-fixtures/image-symlinks/file-2.txt rename to syft/source/test-fixtures/image-symlinks/file-2.txt diff --git a/syft/scope/resolvers/test-fixtures/image-symlinks/nested/nested/file-3.txt b/syft/source/test-fixtures/image-symlinks/nested/nested/file-3.txt similarity index 100% rename from syft/scope/resolvers/test-fixtures/image-symlinks/nested/nested/file-3.txt rename to syft/source/test-fixtures/image-symlinks/nested/nested/file-3.txt diff --git a/syft/scope/resolvers/test-fixtures/image-symlinks/new-file-2.txt b/syft/source/test-fixtures/image-symlinks/new-file-2.txt similarity index 100% rename from syft/scope/resolvers/test-fixtures/image-symlinks/new-file-2.txt rename to syft/source/test-fixtures/image-symlinks/new-file-2.txt diff --git a/syft/scope/test-fixtures/path-detected/.vimrc b/syft/source/test-fixtures/path-detected/.vimrc similarity index 100% rename from syft/scope/test-fixtures/path-detected/.vimrc rename to syft/source/test-fixtures/path-detected/.vimrc diff --git a/syft/scope/test-fixtures/path-detected/empty b/syft/source/test-fixtures/path-detected/empty similarity index 100% rename from syft/scope/test-fixtures/path-detected/empty rename to syft/source/test-fixtures/path-detected/empty diff --git a/test/integration/distro_test.go b/test/integration/distro_test.go index fdbb5a824..a0ad3fd64 100644 --- a/test/integration/distro_test.go +++ b/test/integration/distro_test.go @@ -6,7 +6,7 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/distro" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/go-test/deep" ) @@ -16,7 +16,7 @@ func TestDistroImage(t *testing.T) { tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) defer cleanup() - _, _, actualDistro, err := syft.Catalog("docker-archive:"+tarPath, scope.AllLayersScope) + _, _, actualDistro, err := syft.Catalog("docker-archive:"+tarPath, source.AllLayersScope) if err != nil { t.Fatalf("failed to catalog image: %+v", err) } diff --git a/test/integration/document_import_test.go b/test/integration/document_import_test.go index f42492a21..946421f18 100644 --- a/test/integration/document_import_test.go +++ b/test/integration/document_import_test.go @@ -8,7 +8,7 @@ import ( "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/presenter/json" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/go-test/deep" ) @@ -31,7 +31,7 @@ func TestCatalogFromJSON(t *testing.T) { tarPath := imagetest.GetFixtureImageTarPath(t, test.fixture) defer cleanup() - expectedCatalog, s, expectedDistro, err := syft.Catalog("docker-archive:"+tarPath, scope.AllLayersScope) + expectedCatalog, s, expectedDistro, err := syft.Catalog("docker-archive:"+tarPath, source.AllLayersScope) if err != nil { t.Fatalf("failed to catalog image: %+v", err) } diff --git a/test/integration/json_schema_test.go b/test/integration/json_schema_test.go index 279b74337..15970ee0e 100644 --- a/test/integration/json_schema_test.go +++ b/test/integration/json_schema_test.go @@ -15,7 +15,7 @@ import ( "github.com/anchore/syft/syft/distro" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/presenter" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" "github.com/xeipuuv/gojsonschema" ) @@ -53,7 +53,7 @@ func validateAgainstV1Schema(t *testing.T, json string) { } } -func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope *scope.Scope, prefix string) { +func testJsonSchema(t *testing.T, catalog *pkg.Catalog, theScope *source.Source, prefix string) { // make the json output example dir if it does not exist absJsonSchemaExamplesPath := path.Join(repoRoot(t), jsonSchemaExamplesPath) if _, err := os.Stat(absJsonSchemaExamplesPath); os.IsNotExist(err) { @@ -101,7 +101,7 @@ func TestJsonSchemaImg(t *testing.T) { tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) defer cleanup() - catalog, theScope, _, err := syft.Catalog("docker-archive:"+tarPath, scope.AllLayersScope) + catalog, theScope, _, err := syft.Catalog("docker-archive:"+tarPath, source.AllLayersScope) if err != nil { t.Fatalf("failed to catalog image: %+v", err) } @@ -118,9 +118,9 @@ func TestJsonSchemaImg(t *testing.T) { } func TestJsonSchemaDirs(t *testing.T) { - catalog, theScope, _, err := syft.Catalog("dir:test-fixtures/image-pkg-coverage", scope.AllLayersScope) + catalog, theScope, _, err := syft.Catalog("dir:test-fixtures/image-pkg-coverage", source.AllLayersScope) if err != nil { - t.Errorf("unable to create scope from dir: %+v", err) + t.Errorf("unable to create source from dir: %+v", err) } var cases []testCase diff --git a/test/integration/pkg_coverage_test.go b/test/integration/pkg_coverage_test.go index 13a5af513..78e821829 100644 --- a/test/integration/pkg_coverage_test.go +++ b/test/integration/pkg_coverage_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) func TestPkgCoverageImage(t *testing.T) { @@ -18,7 +18,7 @@ func TestPkgCoverageImage(t *testing.T) { tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) defer cleanup() - catalog, _, _, err := syft.Catalog("docker-archive:"+tarPath, scope.AllLayersScope) + catalog, _, _, err := syft.Catalog("docker-archive:"+tarPath, source.AllLayersScope) if err != nil { t.Fatalf("failed to catalog image: %+v", err) } @@ -100,10 +100,10 @@ func TestPkgCoverageImage(t *testing.T) { } func TestPkgCoverageDirectory(t *testing.T) { - catalog, _, _, err := syft.Catalog("dir:test-fixtures/image-pkg-coverage", scope.AllLayersScope) + catalog, _, _, err := syft.Catalog("dir:test-fixtures/image-pkg-coverage", source.AllLayersScope) if err != nil { - t.Errorf("unable to create scope from dir: %+v", err) + t.Errorf("unable to create source from dir: %+v", err) } observedLanguages := internal.NewStringSet() diff --git a/test/integration/regression_test.go b/test/integration/regression_test.go index 572ddae8d..d018e85b0 100644 --- a/test/integration/regression_test.go +++ b/test/integration/regression_test.go @@ -9,7 +9,7 @@ import ( "github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/syft/syft" - "github.com/anchore/syft/syft/scope" + "github.com/anchore/syft/syft/source" ) func TestRegression212ApkBufferSize(t *testing.T) { @@ -21,7 +21,7 @@ func TestRegression212ApkBufferSize(t *testing.T) { tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) defer cleanup() - catalog, _, _, err := syft.Catalog("docker-archive:"+tarPath, scope.SquashedScope) + catalog, _, _, err := syft.Catalog("docker-archive:"+tarPath, source.SquashedScope) if err != nil { t.Fatalf("failed to catalog image: %+v", err) }