feat: Add config option to allow user to select the default image source location

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Christopher Angelo Phillips 2023-03-31 10:04:10 -04:00 committed by GitHub
parent 2fa238af7c
commit dfcc07e512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 99 additions and 61 deletions

View file

@ -110,9 +110,7 @@ The above output includes only software that is visible in the container (i.e.,
syft <image> --scope all-layers syft <image> --scope all-layers
``` ```
### Supported sources
## Supported sources
Syft can generate a SBOM from a variety of 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) 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: ##### Image Scanning:
- alpmdb - alpmdb
@ -179,7 +183,7 @@ registry:yourrepo/yourimage:tag pull image directly from a registry (no
- conan - conan
- hackage - hackage
#### Non Default: ##### Non Default:
- cargo-auditable-binary - cargo-auditable-binary
### Excluding file paths ### Excluding file paths
@ -393,7 +397,7 @@ Certificate subject: test.email@testdomain.com
Certificate issuer URL: https://accounts.google.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: 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 # same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true 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: # a list of globs to exclude from scanning. same as --exclude ; for example:
# exclude: # exclude:
# - "/etc/**" # - "/etc/**"

View file

@ -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 // could be an image or a directory, with or without a scheme
// TODO: validate that source is image // TODO: validate that source is image
userInput := args[0] 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 { if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err) return fmt.Errorf("could not generate source input for packages command: %w", err)
} }

View file

@ -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 // could be an image or a directory, with or without a scheme
userInput := args[0] 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 { if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err) return fmt.Errorf("could not generate source input for packages command: %w", err)
} }

View file

@ -47,7 +47,7 @@ func Run(_ context.Context, app *config.Application, args []string) error {
}() }()
userInput := args[0] 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 { if err != nil {
return fmt.Errorf("could not generate source input for packages command: %w", err) return fmt.Errorf("could not generate source input for packages command: %w", err)
} }

View file

@ -60,6 +60,7 @@ type Application struct {
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"` 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 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 { func (cfg Application) ToCatalogerConfig() cataloger.Config {
@ -130,6 +131,12 @@ func (cfg *Application) parseConfigValues() error {
return err return err
} }
} }
if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil {
return err
}
// check for valid default source options
// parse nested config options // parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface // 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) // 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("check-for-app-update", true)
v.SetDefault("catalogers", nil) v.SetDefault("catalogers", nil)
v.SetDefault("parallelism", 1) 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 // 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{}) value := reflect.ValueOf(Application{})
@ -291,3 +299,15 @@ func loadConfig(v *viper.Viper, configPath string) error {
} }
return nil 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
}

View file

@ -46,18 +46,17 @@ type Input struct {
Location string Location string
Platform string Platform string
Name string Name string
autoDetectAvailableImageSources bool
} }
// ParseInput generates a source Input that can be used as an argument to generate a new source // ParseInput generates a source Input that can be used as an argument to generate a new source
// from specific providers including a registry. // from specific providers including a registry.
func ParseInput(userInput string, platform string, detectAvailableImageSources bool) (*Input, error) { func ParseInput(userInput string, platform string) (*Input, error) {
return ParseInputWithName(userInput, platform, detectAvailableImageSources, "") return ParseInputWithName(userInput, platform, "", "")
} }
// ParseInputWithName generates a source Input that can be used as an argument to generate a new source // 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. // 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() fs := afero.NewOsFs()
scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput) scheme, source, location, err := DetectScheme(fs, image.DetectSource, userInput)
if err != nil { 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 // only check on packages command, attest we automatically try to pull from userInput
switch scheme { switch scheme {
case ImageScheme, UnknownScheme: case ImageScheme, UnknownScheme:
if detectAvailableImageSources {
if imagePullSource := image.DetermineDefaultImagePullSource(userInput); imagePullSource != image.UnknownSource {
scheme = ImageScheme scheme = ImageScheme
source = imagePullSource
location = userInput location = userInput
} if defaultImageSource != "" {
source = parseDefaultImageSource(defaultImageSource)
} else {
imagePullSource := image.DetermineDefaultImagePullSource(userInput)
source = imagePullSource
} }
if location == "" { if location == "" {
location = userInput location = userInput
@ -95,10 +95,22 @@ func ParseInputWithName(userInput string, platform string, detectAvailableImageS
Location: location, Location: location,
Platform: platform, Platform: platform,
Name: name, Name: name,
autoDetectAvailableImageSources: detectAvailableImageSources,
}, nil }, 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) type sourceDetector func(string) (image.Source, string, error)
func NewFromRegistry(in Input, registryOptions *image.RegistryOptions, exclusions []string) (*Source, func(), 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 // We need to determine the image source again, such that this determination
// doesn't take scheme parsing into account. // 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...) img, err = stereoscope.GetImageFromSource(ctx, in.UserInput, in.ImageSource, opts...)
cleanup = func() { cleanup = func() {
if err := img.Cleanup(); err != nil { if err := img.Cleanup(); err != nil {

View file

@ -52,7 +52,7 @@ func TestParseInput(t *testing.T) {
if test.errFn == nil { if test.errFn == nil {
test.errFn = require.NoError test.errFn = require.NoError
} }
sourceInput, err := ParseInput(test.input, test.platform, true) sourceInput, err := ParseInput(test.input, test.platform)
test.errFn(t, err) test.errFn(t, err)
if test.expected != "" { if test.expected != "" {
require.NotNil(t, sourceInput) require.NotNil(t, sourceInput)
@ -596,7 +596,7 @@ func TestDirectoryExclusions(t *testing.T) {
registryOpts := &image.RegistryOptions{} registryOpts := &image.RegistryOptions{}
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { 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) require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions) src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn() defer fn()
@ -696,7 +696,7 @@ func TestImageExclusions(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input) archiveLocation := imagetest.PrepareFixtureImage(t, "docker-archive", test.input)
sourceInput, err := ParseInput(archiveLocation, "", false) sourceInput, err := ParseInput(archiveLocation, "")
require.NoError(t, err) require.NoError(t, err)
src, fn, err := New(*sourceInput, registryOpts, test.exclusions) src, fn, err := New(*sourceInput, registryOpts, test.exclusions)
defer fn() defer fn()

View file

@ -25,7 +25,7 @@ func BenchmarkImagePackageCatalogers(b *testing.B) {
for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) { for _, c := range cataloger.ImageCatalogers(cataloger.DefaultConfig()) {
// in case of future alteration where state is persisted, assume no dependency is safe to reuse // in case of future alteration where state is persisted, assume no dependency is safe to reuse
userInput := "docker-archive:" + tarPath userInput := "docker-archive:" + tarPath
sourceInput, err := source.ParseInput(userInput, "", false) sourceInput, err := source.ParseInput(userInput, "")
require.NoError(b, err) require.NoError(b, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
b.Cleanup(cleanupSource) b.Cleanup(cleanupSource)

View file

@ -16,7 +16,7 @@ func catalogFixtureImage(t *testing.T, fixtureImageName string, scope source.Sco
imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName)
tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName)
userInput := "docker-archive:" + tarPath userInput := "docker-archive:" + tarPath
sourceInput, err := source.ParseInput(userInput, "", false) sourceInput, err := source.ParseInput(userInput, "")
require.NoError(t, err) require.NoError(t, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
t.Cleanup(cleanupSource) 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) { func catalogDirectory(t *testing.T, dir string) (sbom.SBOM, *source.Source) {
userInput := "dir:" + dir userInput := "dir:" + dir
sourceInput, err := source.ParseInput(userInput, "", false) sourceInput, err := source.ParseInput(userInput, "")
require.NoError(t, err) require.NoError(t, err)
theSource, cleanupSource, err := source.New(*sourceInput, nil, nil) theSource, cleanupSource, err := source.New(*sourceInput, nil, nil)
t.Cleanup(cleanupSource) t.Cleanup(cleanupSource)