diff --git a/README.md b/README.md index 342e2d0b3d8..66268f789f0 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,7 @@ The above output includes only software that is visible in the container (i.e., syft --scope all-layers ``` - - -## Supported sources +### Supported sources Syft can generate a SBOM from a variety of sources: @@ -141,7 +139,13 @@ file:path/to/yourproject/file read directly from a path on disk (any registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) ``` -#### Default Cataloger Configuration by scan type +If an image source is not provided and cannot be detected from the given reference it is assumed the image should be pulled from the Docker daemon. +If docker is not present, then the Podman daemon is attempted next, followed by reaching out directly to the image registry last. + + +This default behavior can be overridden with the `default-image-pull-source` configuration option (See [Configuration](https://github.com/anchore/syft#configuration) for more details). + +### Default Cataloger Configuration by scan type ##### Image Scanning: - alpmdb @@ -179,7 +183,7 @@ registry:yourrepo/yourimage:tag pull image directly from a registry (no - conan - hackage -#### Non Default: +##### Non Default: - cargo-auditable-binary ### Excluding file paths @@ -393,7 +397,7 @@ Certificate subject: test.email@testdomain.com Certificate issuer URL: https://accounts.google.com ``` -#### Local private key support +### Local private key support To generate an SBOM attestation for a container image using a local private key: ``` @@ -436,6 +440,10 @@ file: "" # same as SYFT_CHECK_FOR_APP_UPDATE env var check-for-app-update: true +# allows users to specify which image source should be used to generate the sbom +# valid values are: registry, docker, podman +default-image-pull-source: "" + # a list of globs to exclude from scanning. same as --exclude ; for example: # exclude: # - "/etc/**" diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 69a63b2c5d8..05e17867933 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -47,7 +47,7 @@ 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.ParseInputWithName(userInput, app.Platform, true, app.Name) + si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, app.DefaultImagePullSource) if err != nil { return fmt.Errorf("could not generate source input for packages command: %w", err) } diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index 88fd707a756..1f3acd13e56 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -42,7 +42,7 @@ 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.ParseInputWithName(userInput, app.Platform, true, app.Name) + si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, app.DefaultImagePullSource) 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 82bc6ff393a..b6fae72fef1 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -47,7 +47,7 @@ func Run(_ context.Context, app *config.Application, args []string) error { }() userInput := args[0] - si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name) + si, err := source.ParseInputWithName(userInput, app.Platform, app.Name, 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 2e0b6a3905e..3785b86426f 100644 --- a/internal/config/application.go +++ b/internal/config/application.go @@ -40,26 +40,27 @@ type Application struct { ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"` Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) - Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` - Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output - OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output - File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to - CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not - Dev development `yaml:"dev" json:"dev" mapstructure:"dev"` - Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options - Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"` - Package pkg `yaml:"package" json:"package" mapstructure:"package"` - Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"` - Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` - FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` - FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` - FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` - Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` - Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` - Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` - Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` - Name string `yaml:"name" json:"name" mapstructure:"name"` - Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel + Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` + Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output + OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output + File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to + CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not + Dev development `yaml:"dev" json:"dev" mapstructure:"dev"` + Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options + Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"` + Package pkg `yaml:"package" json:"package" mapstructure:"package"` + Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"` + Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` + FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` + FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` + FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` + Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` + Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` + Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` + Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` + Name string `yaml:"name" json:"name" mapstructure:"name"` + 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 } func (cfg Application) ToCatalogerConfig() cataloger.Config { @@ -130,6 +131,12 @@ func (cfg *Application) parseConfigValues() error { return err } } + + if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil { + return err + } + + // check for valid default source options // parse nested config options // for each field in the configuration struct, see if the field implements the parser interface // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address) @@ -192,6 +199,7 @@ func loadDefaultValues(v *viper.Viper) { v.SetDefault("check-for-app-update", true) v.SetDefault("catalogers", nil) v.SetDefault("parallelism", 1) + v.SetDefault("default-image-pull-source", "") // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does value := reflect.ValueOf(Application{}) @@ -291,3 +299,15 @@ func loadConfig(v *viper.Viper, configPath string) error { } return nil } + +var validDefaultSourceValues = []string{"registry", "docker", "podman", ""} + +func checkDefaultSourceValues(source string) error { + validValues := internal.NewStringSet(validDefaultSourceValues...) + if !validValues.Contains(source) { + validValuesString := strings.Join(validDefaultSourceValues, ", ") + return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString) + } + + return nil +} diff --git a/syft/source/source.go b/syft/source/source.go index 29cfa461082..8433ed31613 100644 --- a/syft/source/source.go +++ b/syft/source/source.go @@ -40,24 +40,23 @@ type Source struct { // Input is an object that captures the detected user input regarding source location, scheme, and provider type. // It acts as a struct input for some source constructors. type Input struct { - UserInput string - Scheme Scheme - ImageSource image.Source - Location string - Platform string - Name string - autoDetectAvailableImageSources bool + UserInput string + Scheme Scheme + ImageSource image.Source + Location string + Platform string + Name 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, detectAvailableImageSources bool) (*Input, error) { - return ParseInputWithName(userInput, platform, detectAvailableImageSources, "") +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 string, detectAvailableImageSources bool, name string) (*Input, error) { +func ParseInputWithName(userInput string, platform, name, defaultImageSource string) (*Input, error) { fs := afero.NewOsFs() scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput) if err != nil { @@ -69,12 +68,13 @@ func ParseInputWithName(userInput string, platform string, detectAvailableImageS // only check on packages command, attest we automatically try to pull from userInput switch scheme { case ImageScheme, UnknownScheme: - if detectAvailableImageSources { - if imagePullSource := image.DetermineDefaultImagePullSource(userInput); imagePullSource != image.UnknownSource { - scheme = ImageScheme - source = imagePullSource - location = userInput - } + scheme = ImageScheme + location = userInput + if defaultImageSource != "" { + source = parseDefaultImageSource(defaultImageSource) + } else { + imagePullSource := image.DetermineDefaultImagePullSource(userInput) + source = imagePullSource } if location == "" { location = userInput @@ -89,16 +89,28 @@ func ParseInputWithName(userInput string, platform string, detectAvailableImageS // collect user input for downstream consumption return &Input{ - UserInput: userInput, - Scheme: scheme, - ImageSource: source, - Location: location, - Platform: platform, - Name: name, - autoDetectAvailableImageSources: detectAvailableImageSources, + UserInput: userInput, + Scheme: scheme, + ImageSource: source, + Location: location, + Platform: platform, + Name: name, }, nil } +func parseDefaultImageSource(defaultImageSource string) image.Source { + switch defaultImageSource { + case "registry": + return image.OciRegistrySource + case "docker": + return image.DockerDaemonSource + case "podman": + return image.PodmanDaemonSource + default: + return image.UnknownSource + } +} + type sourceDetector func(string) (image.Source, string, error) func NewFromRegistry(in Input, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), error) { @@ -203,9 +215,7 @@ func getImageWithRetryStrategy(in Input, registryOptions *image.RegistryOptions) // We need to determine the image source again, such that this determination // doesn't take scheme parsing into account. - if in.autoDetectAvailableImageSources { - in.ImageSource = image.DetermineDefaultImagePullSource(in.UserInput) - } + in.ImageSource = image.DetermineDefaultImagePullSource(in.UserInput) img, err = stereoscope.GetImageFromSource(ctx, in.UserInput, in.ImageSource, opts...) cleanup = func() { if err := img.Cleanup(); err != nil { diff --git a/syft/source/source_test.go b/syft/source/source_test.go index 971dfde9854..cfc6b8f2923 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, true) + sourceInput, err := ParseInput(test.input, 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, "", false) + 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, "", false) + 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 2251e2fab46..128b585aa8b 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -25,7 +25,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, "", false) + 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 c3f6abf461c..693d057c010 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, "", false) + 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, "", false) + sourceInput, err := source.ParseInput(userInput, "") require.NoError(t, err) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) t.Cleanup(cleanupSource)