github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/internal/config/application.go (about) 1 package config 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path" 8 "reflect" 9 "sort" 10 "strings" 11 12 "github.com/adrg/xdg" 13 "github.com/mitchellh/go-homedir" 14 "github.com/spf13/viper" 15 "gopkg.in/yaml.v2" 16 17 "github.com/anchore/go-logger" 18 "github.com/kastenhq/syft/internal" 19 "github.com/kastenhq/syft/internal/log" 20 "github.com/kastenhq/syft/syft/pkg/cataloger" 21 golangCataloger "github.com/kastenhq/syft/syft/pkg/cataloger/golang" 22 "github.com/kastenhq/syft/syft/pkg/cataloger/kernel" 23 pythonCataloger "github.com/kastenhq/syft/syft/pkg/cataloger/python" 24 ) 25 26 var ( 27 ErrApplicationConfigNotFound = fmt.Errorf("application config not found") 28 catalogerEnabledDefault = false 29 ) 30 31 type defaultValueLoader interface { 32 loadDefaultValues(*viper.Viper) 33 } 34 35 type parser interface { 36 parseConfigValues() error 37 } 38 39 // Application is the main syft application configuration. 40 type Application struct { 41 // the location where the application config was read from (either from -c or discovered while loading); default .syft.yaml 42 ConfigPath string `yaml:"configPath,omitempty" json:"configPath" mapstructure:"config"` 43 Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` 44 // -q, indicates to not show any status output to stderr (ETUI or logging UI) 45 Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` 46 Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, the format to use for output 47 OutputTemplatePath string `yaml:"output-template-path" json:"output-template-path" mapstructure:"output-template-path"` // -t template file to use for output 48 File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to 49 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 50 Dev development `yaml:"dev" json:"dev" mapstructure:"dev"` 51 Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options 52 Catalogers []string `yaml:"catalogers" json:"catalogers" mapstructure:"catalogers"` 53 Package pkg `yaml:"package" json:"package" mapstructure:"package"` 54 Golang golang `yaml:"golang" json:"golang" mapstructure:"golang"` 55 LinuxKernel linuxKernel `yaml:"linux-kernel" json:"linux-kernel" mapstructure:"linux-kernel"` 56 Python python `yaml:"python" json:"python" mapstructure:"python"` 57 Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"` 58 FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"` 59 FileClassification fileClassification `yaml:"file-classification" json:"file-classification" mapstructure:"file-classification"` 60 FileContents fileContents `yaml:"file-contents" json:"file-contents" mapstructure:"file-contents"` 61 Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"` 62 Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` 63 Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"` 64 Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` 65 Name string `yaml:"name" json:"name" mapstructure:"name"` 66 Source sourceCfg `yaml:"source" json:"source" mapstructure:"source"` 67 Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel 68 DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` // specify default image pull source 69 BasePath string `yaml:"base-path" json:"base-path" mapstructure:"base-path"` // specify base path for all file paths 70 ExcludeBinaryOverlapByOwnership bool `yaml:"exclude-binary-overlap-by-ownership" json:"exclude-binary-overlap-by-ownership" mapstructure:"exclude-binary-overlap-by-ownership"` // exclude synthetic binary packages owned by os package files 71 } 72 73 func (cfg Application) ToCatalogerConfig() cataloger.Config { 74 return cataloger.Config{ 75 Search: cataloger.SearchConfig{ 76 IncludeIndexedArchives: cfg.Package.SearchIndexedArchives, 77 IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives, 78 Scope: cfg.Package.Cataloger.ScopeOpt, 79 }, 80 Catalogers: cfg.Catalogers, 81 Parallelism: cfg.Parallelism, 82 ExcludeBinaryOverlapByOwnership: cfg.ExcludeBinaryOverlapByOwnership, 83 Golang: golangCataloger.NewGoCatalogerOpts(). 84 WithSearchLocalModCacheLicenses(cfg.Golang.SearchLocalModCacheLicenses). 85 WithLocalModCacheDir(cfg.Golang.LocalModCacheDir). 86 WithSearchRemoteLicenses(cfg.Golang.SearchRemoteLicenses). 87 WithProxy(cfg.Golang.Proxy). 88 WithNoProxy(cfg.Golang.NoProxy), 89 LinuxKernel: kernel.LinuxCatalogerConfig{ 90 CatalogModules: cfg.LinuxKernel.CatalogModules, 91 }, 92 Python: pythonCataloger.CatalogerConfig{ 93 GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements, 94 }, 95 } 96 } 97 98 func (cfg *Application) LoadAllValues(v *viper.Viper, configPath string) error { 99 // priority order: viper.Set, flag, env, config, kv, defaults 100 // flags have already been loaded into viper by command construction 101 102 // check if user specified config; otherwise read all possible paths 103 if err := loadConfig(v, configPath); err != nil { 104 var notFound *viper.ConfigFileNotFoundError 105 if errors.As(err, ¬Found) { 106 log.Debugf("no config file found, using defaults") 107 } else { 108 return fmt.Errorf("unable to load config: %w", err) 109 } 110 } 111 112 // load default config values into viper 113 loadDefaultValues(v) 114 115 // load environment variables 116 v.SetEnvPrefix(internal.ApplicationName) 117 v.AllowEmptyEnv(true) 118 v.AutomaticEnv() 119 120 // unmarshal fully populated viper object onto config 121 err := v.Unmarshal(cfg) 122 if err != nil { 123 return err 124 } 125 126 // Convert all populated config options to their internal application values ex: scope string => scopeOpt source.Scope 127 return cfg.parseConfigValues() 128 } 129 130 func (cfg *Application) parseConfigValues() error { 131 // parse options on this struct 132 var catalogers []string 133 for _, c := range cfg.Catalogers { 134 for _, f := range strings.Split(c, ",") { 135 catalogers = append(catalogers, strings.TrimSpace(f)) 136 } 137 } 138 sort.Strings(catalogers) 139 cfg.Catalogers = catalogers 140 141 // parse application config options 142 for _, optionFn := range []func() error{ 143 cfg.parseLogLevelOption, 144 cfg.parseFile, 145 } { 146 if err := optionFn(); err != nil { 147 return err 148 } 149 } 150 151 if err := checkDefaultSourceValues(cfg.DefaultImagePullSource); err != nil { 152 return err 153 } 154 155 if cfg.Name != "" { 156 log.Warnf("name parameter is deprecated. please use: source-name. name will be removed in a future version") 157 if cfg.Source.Name == "" { 158 cfg.Source.Name = cfg.Name 159 } 160 } 161 162 // check for valid default source options 163 // parse nested config options 164 // for each field in the configuration struct, see if the field implements the parser interface 165 // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address) 166 value := reflect.ValueOf(cfg).Elem() 167 for i := 0; i < value.NumField(); i++ { 168 // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer. 169 if parsable, ok := value.Field(i).Addr().Interface().(parser); ok { 170 // the field implements parser, call it 171 if err := parsable.parseConfigValues(); err != nil { 172 return err 173 } 174 } 175 } 176 return nil 177 } 178 179 func (cfg *Application) parseLogLevelOption() error { 180 switch { 181 case cfg.Quiet: 182 // TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk) 183 // we should be able to quiet the console logging and leave file logging alone... 184 // ... this will be an enhancement for later 185 cfg.Log.Level = logger.DisabledLevel 186 187 case cfg.Verbosity > 0: 188 cfg.Log.Level = logger.LevelFromVerbosity(int(cfg.Verbosity), logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel) 189 190 case cfg.Log.Level != "": 191 var err error 192 cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level)) 193 if err != nil { 194 return err 195 } 196 197 if logger.IsVerbose(cfg.Log.Level) { 198 cfg.Verbosity = 1 199 } 200 default: 201 cfg.Log.Level = logger.WarnLevel 202 } 203 204 return nil 205 } 206 207 func (cfg *Application) parseFile() error { 208 if cfg.File != "" { 209 expandedPath, err := homedir.Expand(cfg.File) 210 if err != nil { 211 return fmt.Errorf("unable to expand file path=%q: %w", cfg.File, err) 212 } 213 cfg.File = expandedPath 214 } 215 return nil 216 } 217 218 // init loads the default configuration values into the viper instance (before the config values are read and parsed). 219 func loadDefaultValues(v *viper.Viper) { 220 // set the default values for primitive fields in this struct 221 v.SetDefault("quiet", false) 222 v.SetDefault("check-for-app-update", true) 223 v.SetDefault("catalogers", nil) 224 v.SetDefault("parallelism", 1) 225 v.SetDefault("default-image-pull-source", "") 226 v.SetDefault("exclude-binary-overlap-by-ownership", true) 227 228 // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does 229 value := reflect.ValueOf(Application{}) 230 for i := 0; i < value.NumField(); i++ { 231 // note: the defaultValueLoader method receiver is NOT a pointer receiver. 232 if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok { 233 // the field implements defaultValueLoader, call it 234 loadable.loadDefaultValues(v) 235 } 236 } 237 } 238 239 func (cfg Application) String() string { 240 // yaml is pretty human friendly (at least when compared to json) 241 appaStr, err := yaml.Marshal(&cfg) 242 243 if err != nil { 244 return err.Error() 245 } 246 247 return string(appaStr) 248 } 249 250 // nolint:funlen 251 func loadConfig(v *viper.Viper, configPath string) error { 252 var err error 253 // use explicitly the given user config 254 if configPath != "" { 255 v.SetConfigFile(configPath) 256 if err := v.ReadInConfig(); err != nil { 257 return fmt.Errorf("unable to read application config=%q : %w", configPath, err) 258 } 259 v.Set("config", v.ConfigFileUsed()) 260 // don't fall through to other options if the config path was explicitly provided 261 return nil 262 } 263 264 // start searching for valid configs in order... 265 // 1. look for .<appname>.yaml (in the current directory) 266 confFilePath := "." + internal.ApplicationName 267 268 // TODO: Remove this before v1.0.0 269 // See syft #1634 270 v.AddConfigPath(".") 271 v.SetConfigName(confFilePath) 272 273 // check if config.yaml exists in the current directory 274 // DEPRECATED: this will be removed in v1.0.0 275 if _, err := os.Stat("config.yaml"); err == nil { 276 log.Warn("DEPRECATED: ./config.yaml as a configuration file is deprecated and will be removed as an option in v1.0.0, please rename to .syft.yaml") 277 } 278 279 if _, err := os.Stat(confFilePath + ".yaml"); err == nil { 280 if err = v.ReadInConfig(); err == nil { 281 v.Set("config", v.ConfigFileUsed()) 282 return nil 283 } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 284 return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) 285 } 286 } 287 288 // 2. look for .<appname>/config.yaml (in the current directory) 289 v.AddConfigPath("." + internal.ApplicationName) 290 v.SetConfigName("config") 291 if err = v.ReadInConfig(); err == nil { 292 v.Set("config", v.ConfigFileUsed()) 293 return nil 294 } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 295 return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) 296 } 297 298 // 3. look for ~/.<appname>.yaml 299 home, err := homedir.Dir() 300 if err == nil { 301 v.AddConfigPath(home) 302 v.SetConfigName("." + internal.ApplicationName) 303 if err = v.ReadInConfig(); err == nil { 304 v.Set("config", v.ConfigFileUsed()) 305 return nil 306 } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 307 return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) 308 } 309 } 310 311 // 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) 312 v.SetConfigName("config") 313 configPath = path.Join(xdg.ConfigHome, internal.ApplicationName) 314 v.AddConfigPath(configPath) 315 for _, dir := range xdg.ConfigDirs { 316 v.AddConfigPath(path.Join(dir, internal.ApplicationName)) 317 } 318 if err = v.ReadInConfig(); err == nil { 319 v.Set("config", v.ConfigFileUsed()) 320 return nil 321 } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { 322 return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) 323 } 324 return nil 325 } 326 327 var validDefaultSourceValues = []string{"registry", "docker", "podman", ""} 328 329 func checkDefaultSourceValues(source string) error { 330 validValues := internal.NewStringSet(validDefaultSourceValues...) 331 if !validValues.Contains(source) { 332 validValuesString := strings.Join(validDefaultSourceValues, ", ") 333 return fmt.Errorf("%s is not a valid default source; please use one of the following: %s''", source, validValuesString) 334 } 335 336 return nil 337 }