From e25965a68cd8f9ae98cfcf62753adc80b7c42955 Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Tue, 6 Jun 2023 10:19:35 +0300 Subject: [PATCH 1/3] CLI flag for directory base Signed-off-by: Avi Deitcher --- cmd/syft/cli/attest/attest.go | 7 +++- cmd/syft/cli/options/packages.go | 8 ++++ cmd/syft/cli/packages/packages.go | 8 +++- cmd/syft/cli/poweruser/poweruser.go | 7 +++- internal/config/application.go | 1 + syft/source/options.go | 45 ++++++++++++++++++++++ syft/source/source.go | 46 ++++++++++++----------- syft/source/source_test.go | 6 +-- test/integration/catalog_packages_test.go | 2 +- test/integration/utils_test.go | 4 +- 10 files changed, 103 insertions(+), 31 deletions(-) create mode 100644 syft/source/options.go diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 997f3307de2..4ad77aa74e1 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -47,7 +47,12 @@ func Run(_ context.Context, app *config.Application, args []string) error { // could be an image or a directory, with or without a scheme // TODO: validate that source is image userInput := args[0] - si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) + si, err := source.ParseInput(userInput, + source.WithPlatform(app.Platform), + source.WithName(app.SourceName), + source.WithVersion(app.SourceVersion), + source.WithDefaultImageSource(app.DefaultImagePullSource), + ) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) } diff --git a/cmd/syft/cli/options/packages.go b/cmd/syft/cli/options/packages.go index f6992a948c2..00a6a7f10cb 100644 --- a/cmd/syft/cli/options/packages.go +++ b/cmd/syft/cli/options/packages.go @@ -23,6 +23,7 @@ type PackagesOptions struct { Catalogers []string SourceName string SourceVersion string + BasePath string } var _ Interface = (*PackagesOptions)(nil) @@ -59,6 +60,9 @@ func (o *PackagesOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error { cmd.Flags().StringVarP(&o.SourceVersion, "source-version", "", "", "set the name of the target being analyzed") + cmd.Flags().StringVarP(&o.BasePath, "base-path", "", "", + "base directory for scanning, no links will be followed above this directory, and all paths will be reported relative to this directory") + return bindPackageConfigOptions(cmd.Flags(), v) } @@ -106,5 +110,9 @@ func bindPackageConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error { return err } + if err := v.BindPFlag("base-path", flags.Lookup("base-path")); err != nil { + return err + } + return nil } diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index 12695e4f086..1b65f6391f7 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -42,7 +42,13 @@ func Run(_ context.Context, app *config.Application, args []string) error { // could be an image or a directory, with or without a scheme userInput := args[0] - si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) + si, err := source.ParseInput(userInput, + source.WithPlatform(app.Platform), + source.WithName(app.SourceName), + source.WithVersion(app.SourceVersion), + source.WithDefaultImageSource(app.DefaultImagePullSource), + source.WithBasePath(app.BasePath), + ) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) } diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go index b4e524feadd..0b04c56323b 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -47,7 +47,12 @@ func Run(_ context.Context, app *config.Application, args []string) error { }() userInput := args[0] - si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) + si, err := source.ParseInput(userInput, + source.WithPlatform(app.Platform), + source.WithName(app.SourceName), + source.WithVersion(app.SourceVersion), + source.WithDefaultImageSource(app.DefaultImagePullSource), + ) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) } diff --git a/internal/config/application.go b/internal/config/application.go index 9f3274265fa..5407dc0b426 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -65,6 +65,7 @@ type Application struct { SourceVersion string `yaml:"source-version" json:"source-version" mapstructure:"source-version"` Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source + BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths } func (cfg Application) ToCatalogerConfig() cataloger.Config { diff --git a/syft/source/options.go b/syft/source/options.go new file mode 100644 index 00000000000..5c1ea732b52 --- /dev/null +++ b/syft/source/options.go @@ -0,0 +1,45 @@ +package source + +type sourceOpt struct { + name string + version string + platform string + defaultImageSource string + base string +} +type Option func(*sourceOpt) error + +func WithName(name string) Option { + return func(s *sourceOpt) error { + s.name = name + return nil + } +} + +func WithPlatform(platform string) Option { + return func(s *sourceOpt) error { + s.platform = platform + return nil + } +} + +func WithVersion(version string) Option { + return func(s *sourceOpt) error { + s.version = version + return nil + } +} + +func WithDefaultImageSource(defaultImageSource string) Option { + return func(s *sourceOpt) error { + s.defaultImageSource = defaultImageSource + return nil + } +} + +func WithBasePath(base string) Option { + return func(s *sourceOpt) error { + s.base = base + return nil + } +} diff --git a/syft/source/source.go b/syft/source/source.go index 4ff747ae297..20c8ddf92de 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -49,23 +49,18 @@ type Input struct { Platform string Name string Version string + BasePath string } // ParseInput generates a source Input that can be used as an argument to generate a new source // from specific providers including a registry. -func ParseInput(userInput string, platform string) (*Input, error) { - return ParseInputWithName(userInput, platform, "", "") -} - -// ParseInputWithName generates a source Input that can be used as an argument to generate a new source -// from specific providers including a registry, with an explicit name. -func ParseInputWithName(userInput string, platform, name, defaultImageSource string) (*Input, error) { - return ParseInputWithNameVersion(userInput, platform, name, "", defaultImageSource) -} - -// ParseInputWithNameVersion generates a source Input that can be used as an argument to generate a new source -// from specific providers including a registry, with an explicit name and version. -func ParseInputWithNameVersion(userInput, platform, name, version, defaultImageSource string) (*Input, error) { +func ParseInput(userInput string, opts ...Option) (*Input, error) { + opt := &sourceOpt{} + for _, o := range opts { + if err := o(opt); err != nil { + return nil, err + } + } fs := afero.NewOsFs() scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput) if err != nil { @@ -79,8 +74,8 @@ func ParseInputWithNameVersion(userInput, platform, name, version, defaultImageS case ImageScheme, UnknownScheme: scheme = ImageScheme location = userInput - if defaultImageSource != "" { - source = parseDefaultImageSource(defaultImageSource) + if opt.defaultImageSource != "" { + source = parseDefaultImageSource(opt.defaultImageSource) } else { imagePullSource := image.DetermineDefaultImagePullSource(userInput) source = imagePullSource @@ -92,20 +87,22 @@ func ParseInputWithNameVersion(userInput, platform, name, version, defaultImageS } } - if scheme != ImageScheme && platform != "" { + if scheme != ImageScheme && opt.platform != "" { return nil, fmt.Errorf("cannot specify a platform for a non-image source") } // collect user input for downstream consumption - return &Input{ + in := &Input{ UserInput: userInput, Scheme: scheme, ImageSource: source, Location: location, - Platform: platform, - Name: name, - Version: version, - }, nil + Platform: opt.platform, + Name: opt.name, + Version: opt.version, + BasePath: opt.base, + } + return in, nil } func parseDefaultImageSource(defaultImageSource string) image.Source { @@ -259,7 +256,12 @@ func generateDirectorySource(fs afero.Fs, in Input) (*Source, func(), error) { return nil, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", in.Location, err) } - s, err := NewFromDirectoryWithNameVersion(in.Location, in.Name, in.Version) + var s Source + if in.BasePath != "" { + s, err = NewFromDirectoryRootWithNameVersion(in.Location, in.Name, in.Version) + } else { + s, err = NewFromDirectoryWithNameVersion(in.Location, in.Name, in.Version) + } if err != nil { return nil, func() {}, fmt.Errorf("could not populate source from path=%q: %w", in.Location, err) } diff --git a/syft/source/source_test.go b/syft/source/source_test.go index bfa085d09a6..55c61cf8dcf 100644 --- a/syft/source/source_test.go +++ b/syft/source/source_test.go @@ -52,7 +52,7 @@ func TestParseInput(t *testing.T) { if test.errFn == nil { test.errFn = require.NoError } - sourceInput, err := ParseInput(test.input, test.platform) + sourceInput, err := ParseInput(test.input, WithPlatform(test.platform)) test.errFn(t, err) if test.expected != "" { require.NotNil(t, sourceInput) @@ -596,7 +596,7 @@ func TestDirectoryExclusions(t *testing.T) { registryOpts := &image.RegistryOptions{} for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - sourceInput, err := ParseInput("dir:"+test.input, "") + sourceInput, err := ParseInput("dir:" + test.input) require.NoError(t, err) src, fn, err := New(*sourceInput, registryOpts, test.exclusions) defer fn() @@ -696,7 +696,7 @@ func TestImageExclusions(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input) - sourceInput, err := ParseInput(archiveLocation, "") + sourceInput, err := ParseInput(archiveLocation) require.NoError(t, err) src, fn, err := New(*sourceInput, registryOpts, test.exclusions) defer fn() diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 2c88c0615fe..b3d49059ea3 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -26,7 +26,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) { // in case of future alteration where state is persisted, assume no dependency is safe to reuse userInput := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userInput, "") + sourceInput, err := source.ParseInput(userInput) require.NoError(b, err) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) b.Cleanup(cleanupSource) diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index 77f50045051..0f6669e5e4d 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -16,7 +16,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) userInput := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userInput, "") + sourceInput, err := source.ParseInput(userInput) require.NoError(t, err) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) t.Cleanup(cleanupSource) @@ -52,7 +52,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) { userInput := "dir:" + dir - sourceInput, err := source.ParseInput(userInput, "") + sourceInput, err := source.ParseInput(userInput) require.NoError(t, err) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) t.Cleanup(cleanupSource) From 39dd9d3abd10eee3d6c007c993c2cc44e990a04b Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 8 Jun 2023 11:41:50 -0400 Subject: [PATCH 2/3] chore: keep API compatibility, move to functional options, deprecate redundant functions Signed-off-by: Keith Zantow --- cmd/syft/cli/attest/attest.go | 6 +- cmd/syft/cli/packages/packages.go | 7 +- cmd/syft/cli/poweruser/poweruser.go | 6 +- internal/config/application.go | 10 + syft/source/options.go | 53 +++-- syft/source/source.go | 230 +++++++++++++--------- syft/source/source_test.go | 107 ++++++---- test/integration/catalog_packages_test.go | 4 +- test/integration/utils_test.go | 8 +- 9 files changed, 250 insertions(+), 181 deletions(-) diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 4ad77aa74e1..230bc231b80 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -47,10 +47,8 @@ func Run(_ context.Context, app *config.Application, args []string) error { // could be an image or a directory, with or without a scheme // TODO: validate that source is image userInput := args[0] - si, err := source.ParseInput(userInput, + si, err := source.ParseUserInput(userInput, source.WithPlatform(app.Platform), - source.WithName(app.SourceName), - source.WithVersion(app.SourceVersion), source.WithDefaultImageSource(app.DefaultImagePullSource), ) if err != nil { @@ -76,7 +74,7 @@ func Run(_ context.Context, app *config.Application, args []string) error { } func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) { - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) + src, cleanup, err := source.NewSource(si, app.Registry.ToOptions(), app.ToSourceOptions()...) if cleanup != nil { defer cleanup() } diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index 1b65f6391f7..ccd3b6b13ab 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -42,12 +42,9 @@ func Run(_ context.Context, app *config.Application, args []string) error { // could be an image or a directory, with or without a scheme userInput := args[0] - si, err := source.ParseInput(userInput, + si, err := source.ParseUserInput(userInput, source.WithPlatform(app.Platform), - source.WithName(app.SourceName), - source.WithVersion(app.SourceVersion), source.WithDefaultImageSource(app.DefaultImagePullSource), - source.WithBasePath(app.BasePath), ) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) @@ -72,7 +69,7 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <- go func() { defer close(errs) - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) + src, cleanup, err := source.NewSource(si, app.Registry.ToOptions(), app.ToSourceOptions()...) if cleanup != nil { defer cleanup() } diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go index 0b04c56323b..8bfdacedaf9 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -47,10 +47,8 @@ func Run(_ context.Context, app *config.Application, args []string) error { }() userInput := args[0] - si, err := source.ParseInput(userInput, + si, err := source.ParseUserInput(userInput, source.WithPlatform(app.Platform), - source.WithName(app.SourceName), - source.WithVersion(app.SourceVersion), source.WithDefaultImageSource(app.DefaultImagePullSource), ) if err != nil { @@ -86,7 +84,7 @@ func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <- return } - src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) + src, cleanup, err := source.NewSource(si, app.Registry.ToOptions(), app.ToSourceOptions()...) if err != nil { errs <- err return diff --git a/internal/config/application.go b/internal/config/application.go index 5407dc0b426..5c6e2fcb5fb 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -20,6 +20,7 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger" golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/kernel" + "github.com/anchore/syft/syft/source" ) var ( @@ -240,6 +241,15 @@ func (cfg Application) String() string { return string(appaStr) } +func (cfg Application) ToSourceOptions() []source.Option { + return []source.Option{ + source.WithName(cfg.SourceName), + source.WithVersion(cfg.SourceVersion), + source.WithBasePath(cfg.BasePath), + source.WithExclusions(cfg.Exclusions), + } +} + // nolint:funlen func loadConfig(v *viper.Viper, configPath string) error { var err error diff --git a/syft/source/options.go b/syft/source/options.go index 5c1ea732b52..e24301df22f 100644 --- a/syft/source/options.go +++ b/syft/source/options.go @@ -1,45 +1,44 @@ package source -type sourceOpt struct { - name string - version string - platform string - defaultImageSource string - base string -} -type Option func(*sourceOpt) error +// Option is used when constructing new Source objects +type Option func(*Source) func WithName(name string) Option { - return func(s *sourceOpt) error { - s.name = name - return nil - } -} - -func WithPlatform(platform string) Option { - return func(s *sourceOpt) error { - s.platform = platform - return nil + return func(s *Source) { + s.Metadata.Name = name } } func WithVersion(version string) Option { - return func(s *sourceOpt) error { - s.version = version - return nil + return func(s *Source) { + s.Metadata.Version = version } } -func WithDefaultImageSource(defaultImageSource string) Option { - return func(s *sourceOpt) error { - s.defaultImageSource = defaultImageSource - return nil +func WithExclusions(exclusions []string) Option { + return func(s *Source) { + s.Exclusions = exclusions } } func WithBasePath(base string) Option { - return func(s *sourceOpt) error { + return func(s *Source) { s.base = base - return nil + s.Metadata.Base = base + } +} + +// InputOption is used during ParseUserInput +type InputOption func(*Input) + +func WithPlatform(platform string) InputOption { + return func(input *Input) { + input.Platform = platform + } +} + +func WithDefaultImageSource(defaultImageSource string) InputOption { + return func(input *Input) { + input.ImageSource = parseDefaultImageSource(defaultImageSource) } } diff --git a/syft/source/source.go b/syft/source/source.go index 20c8ddf92de..114bda69d89 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -47,64 +47,83 @@ type Input struct { ImageSource image.Source Location string Platform string - Name string - Version string - BasePath string + // Deprecated: this will be removed in favor of Source options + Name string + // Deprecated: this will be removed in favor of Source options + Version string } // ParseInput generates a source Input that can be used as an argument to generate a new source // from specific providers including a registry. -func ParseInput(userInput string, opts ...Option) (*Input, error) { - opt := &sourceOpt{} +// +// Deprecated: this function will remove the platform argument in favor of using the WithPlatform option +func ParseInput(userInput string, platform string, opts ...InputOption) (*Input, error) { + return ParseUserInput(userInput, append(opts, WithPlatform(platform))...) +} + +// ParseUserInput generates a source Input that can be used as an argument to generate a new source +// from specific providers including a registry. +func ParseUserInput(userInput string, opts ...InputOption) (*Input, error) { + // collect user input for downstream consumption + in := &Input{ + UserInput: userInput, + } + for _, o := range opts { - if err := o(opt); err != nil { - return nil, err - } + o(in) } + fs := afero.NewOsFs() scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput) if err != nil { return nil, err } + in.Scheme = scheme + in.Location = location + if source == image.UnknownSource { // only run for these two scheme // only check on packages command, attest we automatically try to pull from userInput switch scheme { case ImageScheme, UnknownScheme: - scheme = ImageScheme - location = userInput - if opt.defaultImageSource != "" { - source = parseDefaultImageSource(opt.defaultImageSource) - } else { - imagePullSource := image.DetermineDefaultImagePullSource(userInput) - source = imagePullSource - } - if location == "" { - location = userInput + in.Scheme = ImageScheme + in.Location = userInput + if in.ImageSource == image.UnknownSource { + in.ImageSource = image.DetermineDefaultImagePullSource(userInput) } default: } + } else { + in.ImageSource = source } - if scheme != ImageScheme && opt.platform != "" { + if in.Scheme != ImageScheme && in.Platform != "" { return nil, fmt.Errorf("cannot specify a platform for a non-image source") } - // collect user input for downstream consumption - in := &Input{ - UserInput: userInput, - Scheme: scheme, - ImageSource: source, - Location: location, - Platform: opt.platform, - Name: opt.name, - Version: opt.version, - BasePath: opt.base, - } return in, nil } +// ParseInputWithName generates a source Input that can be used as an argument to generate a new source +// from specific providers including a registry, with an explicit name. +// +// Deprecated: this function will be removed in favor of using ParseUserInput with options +func ParseInputWithName(userInput string, platform, name, defaultImageSource string) (*Input, error) { + return ParseInputWithNameVersion(userInput, platform, name, "", defaultImageSource) +} + +// ParseInputWithNameVersion generates a source Input that can be used as an argument to generate a new source +// from specific providers including a registry, with an explicit name and version. +// +// Deprecated: this function will be removed in favor of using ParseUserInput with options +func ParseInputWithNameVersion(userInput, platform, name, version, defaultImageSource string) (*Input, error) { + i, err := ParseUserInput(userInput, WithPlatform(platform), WithDefaultImageSource(defaultImageSource)) + i.Name = name + i.Version = version + return i, err +} + func parseDefaultImageSource(defaultImageSource string) image.Source { switch defaultImageSource { case "registry": @@ -129,7 +148,14 @@ func NewFromRegistry(in Input, registryOptions *image.RegistryOptions, exclusion } // New produces a Source based on userInput like dir: or image:tag +// +// Deprecated: this function will be removed in favor of NewSource with options func New(in Input, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), error) { + return NewSource(in, registryOptions, WithExclusions(exclusions)) +} + +// NewSource produces a Source based on userInput like dir: or image:tag +func NewSource(in Input, registryOptions *image.RegistryOptions, opts ...Option) (*Source, func(), error) { var err error fs := afero.NewOsFs() var source *Source @@ -146,10 +172,16 @@ func New(in Input, registryOptions *image.RegistryOptions, exclusions []string) err = fmt.Errorf("unable to process input for scanning: %q", in.UserInput) } - if err == nil { - source.Exclusions = exclusions + if source.Metadata.Name == "" { + source.Metadata.Name = in.Name } + if source.Metadata.Version == "" { + source.Metadata.Version = in.Version + } + + applyOpts(source, opts) + return source, cleanupFn, err } @@ -159,7 +191,7 @@ func generateImageSource(in Input, registryOptions *image.RegistryOptions) (*Sou return nil, cleanup, fmt.Errorf("could not fetch image %q: %w", in.Location, err) } - s, err := NewFromImageWithNameVersion(img, in.Location, in.Name, in.Version) + s, err := NewFromImage(img, in.Location) if err != nil { return nil, cleanup, fmt.Errorf("could not populate source with image: %w", err) } @@ -256,12 +288,7 @@ func generateDirectorySource(fs afero.Fs, in Input) (*Source, func(), error) { return nil, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", in.Location, err) } - var s Source - if in.BasePath != "" { - s, err = NewFromDirectoryRootWithNameVersion(in.Location, in.Name, in.Version) - } else { - s, err = NewFromDirectoryWithNameVersion(in.Location, in.Name, in.Version) - } + s, err := NewFromDirectory(in.Location) if err != nil { return nil, func() {}, fmt.Errorf("could not populate source from path=%q: %w", in.Location, err) } @@ -279,92 +306,90 @@ func generateFileSource(fs afero.Fs, in Input) (*Source, func(), error) { return nil, func() {}, fmt.Errorf("given path is not a directory (path=%q): %w", in.Location, err) } - s, cleanupFn := NewFromFileWithNameVersion(in.Location, in.Name, in.Version) + s, cleanupFn := NewFromFile(in.Location) return &s, cleanupFn, nil } // NewFromDirectory creates a new source object tailored to catalog a given filesystem directory recursively. -func NewFromDirectory(path string) (Source, error) { - return NewFromDirectoryWithName(path, "") +func NewFromDirectory(path string, opts ...Option) (Source, error) { + s := Source{ + mutex: &sync.Mutex{}, + Metadata: Metadata{ + Scheme: DirectoryScheme, + Path: path, + }, + path: path, + } + applyOpts(&s, opts) + s.SetID() + return s, nil } // NewFromDirectoryWithName creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name. +// +// Deprecated: this function will be removed in favor of using NewFromDirectory with options func NewFromDirectoryWithName(path string, name string) (Source, error) { return NewFromDirectoryWithNameVersion(path, name, "") } // NewFromDirectoryWithNameVersion creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name. +// +// Deprecated: this function will be removed in favor of using NewFromDirectory with options func NewFromDirectoryWithNameVersion(path string, name string, version string) (Source, error) { - s := Source{ - mutex: &sync.Mutex{}, - Metadata: Metadata{ - Name: name, - Version: version, - Scheme: DirectoryScheme, - Path: path, - }, - path: path, - } - s.SetID() - return s, nil + return NewFromDirectory(path, WithName(name), WithVersion(version)) } // NewFromDirectoryRoot creates a new source object tailored to catalog a given filesystem directory recursively. +// +// Deprecated: this function will be removed in favor of using NewFromDirectory with options func NewFromDirectoryRoot(path string) (Source, error) { return NewFromDirectoryRootWithName(path, "") } // NewFromDirectoryRootWithName creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name. +// +// Deprecated: this function will be removed in favor of using NewFromDirectory with options func NewFromDirectoryRootWithName(path string, name string) (Source, error) { return NewFromDirectoryRootWithNameVersion(path, name, "") } // NewFromDirectoryRootWithNameVersion creates a new source object tailored to catalog a given filesystem directory recursively, with an explicitly provided name. +// +// Deprecated: this function will be removed in favor of using NewFromDirectory with options func NewFromDirectoryRootWithNameVersion(path string, name string, version string) (Source, error) { + return NewFromDirectory(path, WithName(name), WithVersion(version), WithBasePath(path)) +} + +// NewFromFile creates a new source object tailored to catalog a file. +func NewFromFile(path string, opts ...Option) (Source, func()) { + analysisPath, cleanupFn := fileAnalysisPath(path) + s := Source{ mutex: &sync.Mutex{}, Metadata: Metadata{ - Name: name, - Version: version, - Scheme: DirectoryScheme, - Path: path, - Base: path, + Scheme: FileScheme, + Path: path, }, - path: path, - base: path, + path: analysisPath, } + applyOpts(&s, opts) s.SetID() - return s, nil -} - -// NewFromFile creates a new source object tailored to catalog a file. -func NewFromFile(path string) (Source, func()) { - return NewFromFileWithName(path, "") + return s, cleanupFn } // NewFromFileWithName creates a new source object tailored to catalog a file, with an explicitly provided name. +// +// Deprecated: this function will be removed in favor of using NewFromFile with options func NewFromFileWithName(path string, name string) (Source, func()) { return NewFromFileWithNameVersion(path, name, "") } // NewFromFileWithNameVersion creates a new source object tailored to catalog a file, with an explicitly provided name and version. +// +// Deprecated: this function will be removed in favor of using NewFromFile with options func NewFromFileWithNameVersion(path string, name string, version string) (Source, func()) { - analysisPath, cleanupFn := fileAnalysisPath(path) - - s := Source{ - mutex: &sync.Mutex{}, - Metadata: Metadata{ - Name: name, - Version: version, - Scheme: FileScheme, - Path: path, - }, - path: analysisPath, - } - - s.SetID() - return s, cleanupFn + return NewFromFile(path, WithName(name), WithVersion(version)) } // fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive @@ -401,19 +426,7 @@ func fileAnalysisPath(path string) (string, func()) { // 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, userImageStr string) (Source, error) { - return NewFromImageWithName(img, userImageStr, "") -} - -// NewFromImageWithName creates a new source object tailored to catalog a given container image, relative to the -// option given (e.g. all-layers, squashed, etc), with an explicit name. -func NewFromImageWithName(img *image.Image, userImageStr string, name string) (Source, error) { - return NewFromImageWithNameVersion(img, userImageStr, name, "") -} - -// NewFromImageWithNameVersion creates a new source object tailored to catalog a given container image, relative to the -// option given (e.g. all-layers, squashed, etc), with an explicit name and version. -func NewFromImageWithNameVersion(img *image.Image, userImageStr string, name string, version string) (Source, error) { +func NewFromImage(img *image.Image, userImageStr string, opts ...Option) (Source, error) { if img == nil { return Source{}, fmt.Errorf("no image given") } @@ -421,16 +434,39 @@ func NewFromImageWithNameVersion(img *image.Image, userImageStr string, name str s := Source{ Image: img, Metadata: Metadata{ - Name: name, - Version: version, Scheme: ImageScheme, ImageMetadata: NewImageMetadata(img, userImageStr), }, } + + applyOpts(&s, opts) + s.SetID() return s, nil } +// NewFromImageWithName creates a new source object tailored to catalog a given container image, relative to the +// option given (e.g. all-layers, squashed, etc), with an explicit name. +// +// Deprecated: this function will be removed in favor of using NewFromImage with options +func NewFromImageWithName(img *image.Image, userImageStr string, name string) (Source, error) { + return NewFromImageWithNameVersion(img, userImageStr, name, "") +} + +// NewFromImageWithNameVersion creates a new source object tailored to catalog a given container image, relative to the +// option given (e.g. all-layers, squashed, etc), with an explicit name and version. +// +// Deprecated: this function will be removed in favor of using NewFromImage with options +func NewFromImageWithNameVersion(img *image.Image, userImageStr string, name string, version string) (Source, error) { + return NewFromImage(img, userImageStr, WithName(name), WithVersion(version)) +} + +func applyOpts(s *Source, opts []Option) { + for _, o := range opts { + o(s) + } +} + func (s *Source) ID() artifact.ID { if s.id == "" { s.SetID() diff --git a/syft/source/source_test.go b/syft/source/source_test.go index 55c61cf8dcf..6bf37e3bea3 100644 --- a/syft/source/source_test.go +++ b/syft/source/source_test.go @@ -26,38 +26,80 @@ import ( "github.com/anchore/syft/syft/internal/fileresolver" ) -func TestParseInput(t *testing.T) { +func Test_ParseInput(t *testing.T) { tests := []struct { name string input string - platform string - expected Scheme + opts []InputOption + expected *Input errFn require.ErrorAssertionFunc }{ { - name: "ParseInput parses a file input", - input: "test-fixtures/image-simple/file-1.txt", - expected: FileScheme, + name: "file input", + input: "test-fixtures/image-simple/file-1.txt", + expected: &Input{ + UserInput: "test-fixtures/image-simple/file-1.txt", + Location: "test-fixtures/image-simple/file-1.txt", + Scheme: FileScheme, + }, + }, + { + name: "dir input", + input: "test-fixtures/image-simple", + expected: &Input{ + UserInput: "test-fixtures/image-simple", + Location: "test-fixtures/image-simple", + Scheme: DirectoryScheme, + }, + }, + { + name: "explicit dir input", + input: "dir:test-fixtures/image-simple", + expected: &Input{ + UserInput: "dir:test-fixtures/image-simple", + Location: "test-fixtures/image-simple", + Scheme: DirectoryScheme, + }, + }, + { + name: "image input with default source", + input: "alpine:latest", + opts: []InputOption{WithDefaultImageSource("podman")}, + expected: &Input{ + UserInput: "alpine:latest", + Location: "alpine:latest", + ImageSource: image.PodmanDaemonSource, + Scheme: ImageScheme, + }, }, { - name: "errors out when using platform for non-image scheme", - input: "test-fixtures/image-simple/file-1.txt", - platform: "arm64", - errFn: require.Error, + name: "image input with overridden source", + input: "docker:alpine:latest", + opts: []InputOption{WithDefaultImageSource("podman")}, + expected: &Input{ + UserInput: "docker:alpine:latest", + Location: "alpine:latest", + ImageSource: image.DockerDaemonSource, + Scheme: ImageScheme, + }, + }, + { + name: "error when using platform for non-image scheme", + input: "test-fixtures/image-simple/file-1.txt", + opts: []InputOption{WithPlatform("arm64")}, + errFn: require.Error, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - if test.errFn == nil { - test.errFn = require.NoError - } - sourceInput, err := ParseInput(test.input, WithPlatform(test.platform)) - test.errFn(t, err) - if test.expected != "" { - require.NotNil(t, sourceInput) - assert.Equal(t, sourceInput.Scheme, test.expected) + got, err := ParseUserInput(test.input, test.opts...) + + if test.errFn != nil { + test.errFn(t, err) } + + assert.Equal(t, test.expected, got) }) } } @@ -596,9 +638,9 @@ func TestDirectoryExclusions(t *testing.T) { registryOpts := &image.RegistryOptions{} for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { - sourceInput, err := ParseInput("dir:" + test.input) + sourceInput, err := ParseUserInput("dir:" + test.input) require.NoError(t, err) - src, fn, err := New(*sourceInput, registryOpts, test.exclusions) + src, fn, err := NewSource(*sourceInput, registryOpts, WithExclusions(test.exclusions)) defer fn() if test.err { @@ -696,9 +738,9 @@ func TestImageExclusions(t *testing.T) { for _, test := range testCases { t.Run(test.desc, func(t *testing.T) { archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input) - sourceInput, err := ParseInput(archiveLocation) + sourceInput, err := ParseUserInput(archiveLocation) require.NoError(t, err) - src, fn, err := New(*sourceInput, registryOpts, test.exclusions) + src, fn, err := NewSource(*sourceInput, registryOpts, WithExclusions(test.exclusions)) defer fn() if err != nil { @@ -724,23 +766,19 @@ type dummyInfo struct { } func (d dummyInfo) Name() string { - //TODO implement me - panic("implement me") + panic("not implemented") } func (d dummyInfo) Size() int64 { - //TODO implement me - panic("implement me") + panic("not implemented") } func (d dummyInfo) Mode() fs.FileMode { - //TODO implement me - panic("implement me") + panic("not implemented") } func (d dummyInfo) ModTime() time.Time { - //TODO implement me - panic("implement me") + panic("not implemented") } func (d dummyInfo) IsDir() bool { @@ -748,8 +786,7 @@ func (d dummyInfo) IsDir() bool { } func (d dummyInfo) Sys() any { - //TODO implement me - panic("implement me") + panic("not implemented") } func Test_crossPlatformExclusions(t *testing.T) { @@ -912,9 +949,3 @@ func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string { return destinationArchiveFilePath } - -func assertNoError(t testing.TB, fn func() error) func() { - return func() { - assert.NoError(t, fn()) - } -} diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index b3d49059ea3..967a08ef241 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -26,9 +26,9 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) { // in case of future alteration where state is persisted, assume no dependency is safe to reuse userInput := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userInput) + sourceInput, err := source.ParseUserInput(userInput) require.NoError(b, err) - theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) + theSource, cleanupSource, err := source.NewSource(*sourceInput, nil) b.Cleanup(cleanupSource) if err != nil { b.Fatalf("unable to get source: %+v", err) diff --git a/test/integration/utils_test.go b/test/integration/utils_test.go index 0f6669e5e4d..948ac5b1b39 100644 --- a/test/integration/utils_test.go +++ b/test/integration/utils_test.go @@ -16,9 +16,9 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) userInput := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userInput) + sourceInput, err := source.ParseUserInput(userInput) require.NoError(t, err) - theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) + theSource, cleanupSource, err := source.NewSource(*sourceInput, nil) t.Cleanup(cleanupSource) require.NoError(t, err) @@ -52,9 +52,9 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) { userInput := "dir:" + dir - sourceInput, err := source.ParseInput(userInput) + sourceInput, err := source.ParseUserInput(userInput) require.NoError(t, err) - theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) + theSource, cleanupSource, err := source.NewSource(*sourceInput, nil) t.Cleanup(cleanupSource) require.NoError(t, err) From f363ae78fc6979011aa41d81cc4975134b033e19 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Mon, 10 Jul 2023 13:20:57 -0400 Subject: [PATCH 3/3] chore: update base-path option to new source structure Signed-off-by: Keith Zantow --- cmd/syft/cli/attest/attest.go | 9 +----- cmd/syft/cli/packages/packages.go | 1 + cmd/syft/cli/poweruser/poweruser.go | 1 + internal/config/application.go | 10 ------- syft/source/detection.go | 7 ++++- syft/source/options.go | 44 ----------------------------- 6 files changed, 9 insertions(+), 63 deletions(-) delete mode 100644 syft/source/options.go diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 26dcc29b2f5..758af670635 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -38,14 +38,6 @@ func Run(_ context.Context, app *config.Application, args []string) error { // note: must be a container image userInput := args[0] - si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) - if err != nil { - return fmt.Errorf("could not generate source input for packages command: %w", err) - } - - if si.Scheme != source.ImageScheme { - return fmt.Errorf("attestations are only supported for oci images at this time") - } _, err = exec.LookPath("cosign") if err != nil { @@ -107,6 +99,7 @@ func buildSBOM(app *config.Application, userInput string, errs chan error) (*sbo Paths: app.Exclusions, }, DigestAlgorithms: hashers, + BasePath: app.BasePath, }, ) diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index a84b3c09af1..a7c1d5521dc 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -97,6 +97,7 @@ func execWorker(app *config.Application, userInput string, writer sbom.Writer) < Paths: app.Exclusions, }, DigestAlgorithms: hashers, + BasePath: app.BasePath, }, ) diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go index e9a251f3e26..cfc10e1bcc1 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -103,6 +103,7 @@ func execWorker(app *config.Application, userInput string, writer sbom.Writer) < Paths: app.Exclusions, }, DigestAlgorithms: nil, + BasePath: app.BasePath, }, ) diff --git a/internal/config/application.go b/internal/config/application.go index 0ace3663d88..e7c726134d8 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -20,7 +20,6 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger" golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/kernel" - "github.com/anchore/syft/syft/source" ) var ( @@ -240,15 +239,6 @@ func (cfg Application) String() string { return string(appaStr) } -func (cfg Application) ToSourceOptions() []source.Option { - return []source.Option{ - source.WithName(cfg.SourceName), - source.WithVersion(cfg.SourceVersion), - source.WithBasePath(cfg.BasePath), - source.WithExclusions(cfg.Exclusions), - } -} - // nolint:funlen func loadConfig(v *viper.Viper, configPath string) error { var err error diff --git a/syft/source/detection.go b/syft/source/detection.go index 3d301f14da5..f96dc0023f5 100644 --- a/syft/source/detection.go +++ b/syft/source/detection.go @@ -87,6 +87,7 @@ type DetectionSourceConfig struct { Platform *image.Platform Exclude ExcludeConfig DigestAlgorithms []crypto.Hash + BasePath string } func DefaultDetectionSourceConfig() DetectionSourceConfig { @@ -117,10 +118,14 @@ func (d Detection) NewSource(cfg DetectionSourceConfig) (Source, error) { }, ) case directoryType: + base := cfg.BasePath + if base == "" { + base = d.location + } src, err = NewFromDirectory( DirectoryConfig{ Path: d.location, - Base: d.location, + Base: base, Exclude: cfg.Exclude, Alias: cfg.Alias, }, diff --git a/syft/source/options.go b/syft/source/options.go deleted file mode 100644 index e24301df22f..00000000000 --- a/syft/source/options.go +++ /dev/null @@ -1,44 +0,0 @@ -package source - -// Option is used when constructing new Source objects -type Option func(*Source) - -func WithName(name string) Option { - return func(s *Source) { - s.Metadata.Name = name - } -} - -func WithVersion(version string) Option { - return func(s *Source) { - s.Metadata.Version = version - } -} - -func WithExclusions(exclusions []string) Option { - return func(s *Source) { - s.Exclusions = exclusions - } -} - -func WithBasePath(base string) Option { - return func(s *Source) { - s.base = base - s.Metadata.Base = base - } -} - -// InputOption is used during ParseUserInput -type InputOption func(*Input) - -func WithPlatform(platform string) InputOption { - return func(input *Input) { - input.Platform = platform - } -} - -func WithDefaultImageSource(defaultImageSource string) InputOption { - return func(input *Input) { - input.ImageSource = parseDefaultImageSource(defaultImageSource) - } -}