package config import ( "errors" "fmt" "path" "reflect" "sort" "strings" "github.com/adrg/xdg" "github.com/mitchellh/go-homedir" "github.com/sirupsen/logrus" "github.com/spf13/viper" "gopkg.in/yaml.v2" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/pkg/cataloger" ) var ( ErrApplicationConfigNotFound = fmt.Errorf("application config not found") catalogerEnabledDefault = false ) type defaultValueLoader interface { loadDefaultValues(*viper.Viper) } type parser interface { parseConfigValues() error } // Application is the main syft application configuration. type Application struct { // the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml 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 Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise 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"` 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"` Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` } func (cfg Application) ToCatalogerConfig() cataloger.Config { return cataloger.Config{ Search: cataloger.SearchConfig{ IncludeIndexedArchives: cfg.Package.SearchIndexedArchives, IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives, Scope: cfg.Package.Cataloger.ScopeOpt, }, Catalogers: cfg.Catalogers, } } func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error { // priority order: viper.Set, flag, env, config, kv, defaults // flags have already been loaded into viper by command construction // check if user specified config; otherwise read all possible paths if err := loadConfig(v, configPath); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { // Not Found; ignore this error log.Debug("no config file found; proceeding with defaults") } } // load default config values into viper loadDefaultValues(v) // load environment variables v.SetEnvPrefix(internal.ApplicationName) v.AllowEmptyEnv(true) v.AutomaticEnv() // unmarshal fully populated viper object onto config err := v.Unmarshal(cfg) if err != nil { return err } // Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope return cfg.parseConfigValues() } func (cfg *Application) parseConfigValues() error { // parse options on this struct var catalogers []string for _, c := range cfg.Catalogers { for _, f := range strings.Split(c, ",") { catalogers = append(catalogers, strings.TrimSpace(f)) } } sort.Strings(catalogers) cfg.Catalogers = catalogers // parse application config options for _, optionFn := range []func() error{ cfg.parseUploadOptions, cfg.parseLogLevelOption, cfg.parseFile, } { if err := optionFn(); err != nil { return err } } // 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) value := reflect.ValueOf(cfg).Elem() for i := 0; i < value.NumField(); i++ { // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer. if parsable, ok := value.Field(i).Addr().Interface().(parser); ok { // the field implements parser, call it if err := parsable.parseConfigValues(); err != nil { return err } } } return nil } func (cfg *Application) parseUploadOptions() error { if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" { return fmt.Errorf("cannot provide dockerfile option without enabling upload") } return nil } func (cfg *Application) parseLogLevelOption() error { switch { case cfg.Quiet: // TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk) // 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 case cfg.Verbosity > 0: switch v := cfg.Verbosity; { case v == 1: cfg.Log.LevelOpt = logrus.InfoLevel case v >= 2: cfg.Log.LevelOpt = logrus.DebugLevel default: cfg.Log.LevelOpt = logrus.ErrorLevel } case cfg.Log.Level != "": 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) } cfg.Log.LevelOpt = lvl if cfg.Log.LevelOpt >= logrus.InfoLevel { cfg.Verbosity = 1 } default: cfg.Log.LevelOpt = logrus.WarnLevel } if cfg.Log.Level == "" { cfg.Log.Level = cfg.Log.LevelOpt.String() } return nil } func (cfg *Application) parseFile() error { if cfg.File != "" { expandedPath, err := homedir.Expand(cfg.File) if err != nil { return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err) } cfg.File = expandedPath } return nil } // init loads the default configuration values into the viper instance (before the config values are read and parsed). func loadDefaultValues(v *viper.Viper) { // set the default values for primitive fields in this struct v.SetDefault("quiet", false) v.SetDefault("check-for-app-update", true) v.SetDefault("catalogers", nil) // 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{}) for i := 0; i < value.NumField(); i++ { // note: the defaultValueLoader method receiver is NOT a pointer receiver. if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok { // the field implements defaultValueLoader, call it loadable.loadDefaultValues(v) } } } func (cfg Application) String() string { // yaml is pretty human friendly (at least when compared to json) appaStr, err := yaml.Marshal(&cfg) if err != nil { return err.Error() } return string(appaStr) } func loadConfig(v *viper.Viper, configPath string) error { var err error // 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) } v.Set("config", v.ConfigFileUsed()) // 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 { v.Set("config", v.ConfigFileUsed()) return nil } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) } // 2. look for ./config.yaml (in the current directory) v.AddConfigPath("." + internal.ApplicationName) v.SetConfigName("config") if err = v.ReadInConfig(); err == nil { v.Set("config", v.ConfigFileUsed()) return nil } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) } // 3. look for ~/..yaml home, err := homedir.Dir() if err == nil { v.AddConfigPath(home) v.SetConfigName("." + internal.ApplicationName) if err = v.ReadInConfig(); err == nil { v.Set("config", v.ConfigFileUsed()) return nil } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) } } // 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 { v.Set("config", v.ConfigFileUsed()) return nil } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) } return nil }