diff --git a/internal/config/anchore.go b/internal/config/anchore.go new file mode 100644 index 000000000..7b363baa0 --- /dev/null +++ b/internal/config/anchore.go @@ -0,0 +1,13 @@ +package config + +type anchore struct { + // upload options + Host string `yaml:"host" json:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to (setting this value enables upload) + Path string `yaml:"path" json:"path" mapstructure:"path"` // override the engine/enterprise API upload path + // IMPORTANT: do not show the username in any YAML/JSON output (sensitive information) + Username string `yaml:"-" json:"-" mapstructure:"username"` // -u , username to authenticate upload + // IMPORTANT: do not show the password in any YAML/JSON output (sensitive information) + Password string `yaml:"-" json:"-" mapstructure:"password"` // -p , password to authenticate upload + Dockerfile string `yaml:"dockerfile" json:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload + OverwriteExistingImage bool `yaml:"overwrite-existing-image" json:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload +} diff --git a/internal/config/application.go b/internal/config/application.go new file mode 100644 index 000000000..7ec137331 --- /dev/null +++ b/internal/config/application.go @@ -0,0 +1,181 @@ +package config + +import ( + "fmt" + "path" + "strings" + + "github.com/anchore/syft/syft/source" + + "github.com/adrg/xdg" + "github.com/anchore/syft/internal" + "github.com/mitchellh/go-homedir" + "github.com/sirupsen/logrus" + "github.com/spf13/viper" + "gopkg.in/yaml.v2" +) + +// Application is the main syft application configuration. +type Application struct { + ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) + Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting + Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) + Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options + CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config) + Dev Development `yaml:"dev" json:"dev" mapstructure:"dev"` + 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 + Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise + Packages Packages `yaml:"packages" json:"packages" mapstructure:"packages"` + FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` +} + +// LoadApplicationConfig populates the given viper object with application configuration discovered on disk +func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application, error) { + // the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead + setNonCliDefaultAppConfigValues(v) + _ = readConfig(v, cliOpts.ConfigPath) + + config := &Application{ + CliOptions: cliOpts, + } + + if err := v.Unmarshal(config); err != nil { + return nil, fmt.Errorf("unable to parse config: %w", err) + } + config.ConfigPath = v.ConfigFileUsed() + + if err := config.build(); err != nil { + return nil, fmt.Errorf("invalid application config: %w", err) + } + + return config, nil +} + +// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in. +func (cfg *Application) build() error { + if cfg.Quiet { + // TODO: this is bad: quiet option trumps all other logging options + // we should be able to quiet the console logging and leave file logging alone... + // ... this will be an enhancement for later + cfg.Log.LevelOpt = logrus.PanicLevel + } else { + if cfg.Log.Level != "" { + if cfg.CliOptions.Verbosity > 0 { + return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together") + } + + lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level)) + if err != nil { + return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err) + } + // set the log level explicitly + cfg.Log.LevelOpt = lvl + } else { + // set the log level implicitly + switch v := cfg.CliOptions.Verbosity; { + case v == 1: + cfg.Log.LevelOpt = logrus.InfoLevel + case v >= 2: + cfg.Log.LevelOpt = logrus.DebugLevel + default: + cfg.Log.LevelOpt = logrus.WarnLevel + } + } + } + + if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" { + return fmt.Errorf("cannot provide dockerfile option without enabling upload") + } + + for _, builder := range []func() error{ + cfg.Packages.build, + cfg.FileMetadata.build, + } { + if err := builder(); err != nil { + return err + } + } + + return nil +} + +func (cfg Application) String() string { + // yaml is pretty human friendly (at least when compared to json) + appCfgStr, err := yaml.Marshal(&cfg) + + if err != nil { + return err.Error() + } + + return string(appCfgStr) +} + +// readConfig attempts to read the given config path from disk or discover an alternate store location +func readConfig(v *viper.Viper, configPath string) error { + v.AutomaticEnv() + v.SetEnvPrefix(internal.ApplicationName) + // allow for nested options to be specified via environment variables + // e.g. pod.context = APPNAME_POD_CONTEXT + v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) + + // use explicitly the given user config + if configPath != "" { + v.SetConfigFile(configPath) + if err := v.ReadInConfig(); err != nil { + return fmt.Errorf("unable to read application config=%q : %w", configPath, err) + } + // don't fall through to other options if the config path was explicitly provided + return nil + } + + // start searching for valid configs in order... + + // 1. look for ..yaml (in the current directory) + v.AddConfigPath(".") + v.SetConfigName(internal.ApplicationName) + if err := v.ReadInConfig(); err == nil { + return nil + } + + // 2. look for ./config.yaml (in the current directory) + v.AddConfigPath("." + internal.ApplicationName) + v.SetConfigName("config") + if err := v.ReadInConfig(); err == nil { + return nil + } + + // 3. look for ~/..yaml + home, err := homedir.Dir() + if err == nil { + v.AddConfigPath(home) + v.SetConfigName("." + internal.ApplicationName) + if err := v.ReadInConfig(); err == nil { + return nil + } + } + + // 4. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) + v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName)) + for _, dir := range xdg.ConfigDirs { + v.AddConfigPath(path.Join(dir, internal.ApplicationName)) + } + v.SetConfigName("config") + if err := v.ReadInConfig(); err == nil { + return nil + } + + return fmt.Errorf("application config not found") +} + +// setNonCliDefaultAppConfigValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value) +func setNonCliDefaultAppConfigValues(v *viper.Viper) { + v.SetDefault("anchore.path", "") + v.SetDefault("log.structured", false) + v.SetDefault("check-for-app-update", true) + v.SetDefault("dev.profile-cpu", false) + v.SetDefault("dev.profile-mem", false) + v.SetDefault("packages.cataloging-enabled", true) + v.SetDefault("file-metadata.cataloging-enabled", true) + v.SetDefault("file-metadata.scope", source.SquashedScope) + v.SetDefault("file-metadata.digests", []string{"sha256"}) +} diff --git a/internal/config/cli_only_options.go b/internal/config/cli_only_options.go new file mode 100644 index 000000000..5ece4d7eb --- /dev/null +++ b/internal/config/cli_only_options.go @@ -0,0 +1,7 @@ +package config + +// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file) +type CliOnlyOptions struct { + ConfigPath string // -c. where the read config is on disk + Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be +} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index b957ae588..000000000 --- a/internal/config/config.go +++ /dev/null @@ -1,228 +0,0 @@ -package config - -import ( - "fmt" - "path" - "strings" - - "github.com/adrg/xdg" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/syft/presenter" - "github.com/anchore/syft/syft/source" - "github.com/mitchellh/go-homedir" - "github.com/sirupsen/logrus" - "github.com/spf13/viper" - "gopkg.in/yaml.v2" -) - -// Application is the main syft application configuration. -type Application struct { - ConfigPath string `yaml:",omitempty"` // the location where the application config was read from (either from -c or discovered while loading) - PresenterOpt presenter.Option `yaml:"-"` // -o, the native Presenter.Option to use for report formatting - Output string `yaml:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting - ScopeOpt source.Scope `yaml:"-"` // -s, the native source.Scope option to use for how to catalog the container image - Scope string `yaml:"scope" mapstructure:"scope"` // -s, the source.Scope string hint for how to catalog the container image - Quiet bool `yaml:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI) - Log logging `yaml:"log" mapstructure:"log"` // all logging-related options - CliOptions CliOnlyOptions `yaml:"-"` // all options only available through the CLI (not via env vars or config) - Dev Development `mapstructure:"dev"` - CheckForAppUpdate bool `yaml:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not - Anchore anchore `yaml:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise -} - -// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file) -type CliOnlyOptions struct { - ConfigPath string // -c. where the read config is on disk - Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be -} - -// logging contains all logging-related configuration options available to the user via the application config. -type logging struct { - Structured bool `yaml:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings - LevelOpt logrus.Level `yaml:"level"` // the native log level object used by the logger - Level string `yaml:"-" mapstructure:"level"` // the log level string hint - FileLocation string `yaml:"file" mapstructure:"file"` // the file path to write logs to -} - -type anchore struct { - // upload options - UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option) - Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to - Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path - Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload - Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload - Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload - OverwriteExistingImage bool `yaml:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload -} - -type Development struct { - ProfileCPU bool `mapstructure:"profile-cpu"` - ProfileMem bool `mapstructure:"profile-mem"` -} - -// LoadApplicationConfig populates the given viper object with application configuration discovered on disk -func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions, wasHostnameSet bool) (*Application, error) { - // the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead - setNonCliDefaultValues(v) - _ = readConfig(v, cliOpts.ConfigPath) - - config := &Application{ - CliOptions: cliOpts, - } - - if err := v.Unmarshal(config); err != nil { - return nil, fmt.Errorf("unable to parse config: %w", err) - } - config.ConfigPath = v.ConfigFileUsed() - - if err := config.build(v, wasHostnameSet); err != nil { - return nil, fmt.Errorf("invalid config: %w", err) - } - - return config, nil -} - -// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in. -func (cfg *Application) build(v *viper.Viper, wasHostnameSet bool) error { - // set the presenter - presenterOption := presenter.ParseOption(cfg.Output) - if presenterOption == presenter.UnknownPresenter { - return fmt.Errorf("bad --output value '%s'", cfg.Output) - } - cfg.PresenterOpt = presenterOption - - // set the source - scopeOption := source.ParseScope(cfg.Scope) - if scopeOption == source.UnknownScope { - return fmt.Errorf("bad --scope value '%s'", cfg.Scope) - } - cfg.ScopeOpt = scopeOption - - if cfg.Quiet { - // TODO: this is bad: quiet option trumps all other logging options - // we should be able to quiet the console logging and leave file logging alone... - // ... this will be an enhancement for later - cfg.Log.LevelOpt = logrus.PanicLevel - } else { - if cfg.Log.Level != "" { - if cfg.CliOptions.Verbosity > 0 { - return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together") - } - - lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level)) - if err != nil { - return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err) - } - // set the log level explicitly - cfg.Log.LevelOpt = lvl - } else { - // set the log level implicitly - switch v := cfg.CliOptions.Verbosity; { - case v == 1: - cfg.Log.LevelOpt = logrus.InfoLevel - case v >= 2: - cfg.Log.LevelOpt = logrus.DebugLevel - default: - cfg.Log.LevelOpt = logrus.WarnLevel - } - } - } - // check if upload should be done relative to the CLI and config behavior - if !v.IsSet("anchore.upload-enabled") && wasHostnameSet { - // we know the user didn't specify to upload in the config file and a --hostname option was provided (so set upload) - cfg.Anchore.UploadEnabled = true - } - - if !cfg.Anchore.UploadEnabled && cfg.Anchore.Dockerfile != "" { - return fmt.Errorf("cannot provide dockerfile option without enabling upload") - } - - return nil -} - -func (cfg Application) String() string { - // redact sensitive information - if cfg.Anchore.Username != "" { - cfg.Anchore.Username = "********" - } - - if cfg.Anchore.Password != "" { - cfg.Anchore.Password = "********" - } - - // yaml is pretty human friendly (at least when compared to json) - appCfgStr, err := yaml.Marshal(&cfg) - - if err != nil { - return err.Error() - } - - return string(appCfgStr) -} - -// readConfig attempts to read the given config path from disk or discover an alternate store location -func readConfig(v *viper.Viper, configPath string) error { - v.AutomaticEnv() - v.SetEnvPrefix(internal.ApplicationName) - // allow for nested options to be specified via environment variables - // e.g. pod.context = APPNAME_POD_CONTEXT - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - - // use explicitly the given user config - if configPath != "" { - v.SetConfigFile(configPath) - if err := v.ReadInConfig(); err == nil { - return nil - } - // don't fall through to other options if this fails - return fmt.Errorf("unable to read config: %v", configPath) - } - - // start searching for valid configs in order... - - // 1. look for ..yaml (in the current directory) - v.AddConfigPath(".") - v.SetConfigName(internal.ApplicationName) - if err := v.ReadInConfig(); err == nil { - return nil - } - - // 2. look for ./config.yaml (in the current directory) - v.AddConfigPath("." + internal.ApplicationName) - v.SetConfigName("config") - if err := v.ReadInConfig(); err == nil { - return nil - } - - // 3. look for ~/..yaml - home, err := homedir.Dir() - if err == nil { - v.AddConfigPath(home) - v.SetConfigName("." + internal.ApplicationName) - if err := v.ReadInConfig(); err == nil { - return nil - } - } - - // 4. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) - v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName)) - for _, dir := range xdg.ConfigDirs { - v.AddConfigPath(path.Join(dir, internal.ApplicationName)) - } - v.SetConfigName("config") - if err := v.ReadInConfig(); err == nil { - return nil - } - - return fmt.Errorf("application config not found") -} - -// setNonCliDefaultValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value) -func setNonCliDefaultValues(v *viper.Viper) { - v.SetDefault("log.level", "") - v.SetDefault("log.file", "") - v.SetDefault("log.structured", false) - v.SetDefault("check-for-app-update", true) - v.SetDefault("dev.profile-cpu", false) - v.SetDefault("dev.profile-mem", false) -} diff --git a/internal/config/development.go b/internal/config/development.go new file mode 100644 index 000000000..dbb38ff08 --- /dev/null +++ b/internal/config/development.go @@ -0,0 +1,6 @@ +package config + +type Development struct { + ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"` + ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"` +} diff --git a/internal/config/file_metadata.go b/internal/config/file_metadata.go new file mode 100644 index 000000000..a58f6410c --- /dev/null +++ b/internal/config/file_metadata.go @@ -0,0 +1,24 @@ +package config + +import ( + "fmt" + + "github.com/anchore/syft/syft/source" +) + +type FileMetadata struct { + CatalogingEnabled bool `yaml:"cataloging-enabled" json:"cataloging-enabled" mapstructure:"cataloging-enabled"` + Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` + ScopeOpt source.Scope `yaml:"-" json:"-"` + Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"` +} + +func (cfg *FileMetadata) build() error { + scopeOption := source.ParseScope(cfg.Scope) + if scopeOption == source.UnknownScope { + return fmt.Errorf("bad scope value %q", cfg.Scope) + } + cfg.ScopeOpt = scopeOption + + return nil +} diff --git a/internal/config/logging.go b/internal/config/logging.go new file mode 100644 index 000000000..53139419a --- /dev/null +++ b/internal/config/logging.go @@ -0,0 +1,11 @@ +package config + +import "github.com/sirupsen/logrus" + +// logging contains all logging-related configuration options available to the user via the application config. +type logging struct { + Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings + LevelOpt logrus.Level `yaml:"-" json:"-"` // the native log level object used by the logger + Level string `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint + FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to +} diff --git a/internal/config/packages.go b/internal/config/packages.go new file mode 100644 index 000000000..63a1245f6 --- /dev/null +++ b/internal/config/packages.go @@ -0,0 +1,23 @@ +package config + +import ( + "fmt" + + "github.com/anchore/syft/syft/source" +) + +type Packages struct { + CatalogingEnabled bool `yaml:"cataloging-enabled" json:"cataloging-enabled" mapstructure:"cataloging-enabled"` + Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` + ScopeOpt source.Scope `yaml:"-" json:"-"` +} + +func (cfg *Packages) build() error { + scopeOption := source.ParseScope(cfg.Scope) + if scopeOption == source.UnknownScope { + return fmt.Errorf("bad scope value %q", cfg.Scope) + } + cfg.ScopeOpt = scopeOption + + return nil +}