From 61b622da3a4453a2629bb1a489cc80e71c8afefd Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Tue, 6 Jun 2023 10:19:35 +0300 Subject: [PATCH] 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 | 41 ++++++++++----------- syft/source/source_test.go | 6 +-- test/integration/catalog_packages_test.go | 2 +- test/integration/utils_test.go | 4 +- 10 files changed, 98 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 997f3307de2c..4ad77aa74e17 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 f6992a948c2b..00a6a7f10cba 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 12695e4f086c..1b65f6391f72 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 b4e524feadde..0b04c56323b9 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 9f3274265fa2..5407dc0b426a 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 000000000000..5c1ea732b520 --- /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 4ff747ae297f..5e5b922b79ca 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,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) } - s, err := NewFromDirectoryWithNameVersion(in.Location, in.Name, in.Version) + s, err := NewFromDirectoryRootWithNameVersion(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 bfa085d09a6c..55c61cf8dcfa 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 2c88c0615feb..b3d49059ea32 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 77f50045051b..0f6669e5e4d9 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)