Skip to content

Commit

Permalink
feat: Add config option to allow user to select the default image sou…
Browse files Browse the repository at this point in the history
…rce location

Signed-off-by: Christopher Phillips <christopher.phillips@anchore.com>
  • Loading branch information
spiffcs authored Mar 31, 2023
1 parent 2fa238a commit dfcc07e
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 61 deletions.
20 changes: 14 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,7 @@ The above output includes only software that is visible in the container (i.e.,
syft <image> --scope all-layers
```



## Supported sources
### Supported sources

Syft can generate a SBOM from a variety of sources:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
```
Expand Down Expand Up @@ -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/**"
Expand Down
2 changes: 1 addition & 1 deletion cmd/syft/cli/attest/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/syft/cli/packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/syft/cli/poweruser/poweruser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
60 changes: 40 additions & 20 deletions internal/config/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{})
Expand Down Expand Up @@ -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
}
62 changes: 36 additions & 26 deletions syft/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions syft/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion test/integration/catalog_packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions test/integration/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit dfcc07e

Please sign in to comment.