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, &notFound) {
   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  }