go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/cli/config/config.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package config
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/base64"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/cockroachdb/errors"
    13  	"github.com/rs/zerolog/log"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/viper"
    16  	"go.mondoo.com/cnquery"
    17  	"go.mondoo.com/cnquery/logger"
    18  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    19  )
    20  
    21  /*
    22  	Configuration is loaded in this order:
    23  	ENV -> ~/.mondoo.conf -> defaults
    24  */
    25  
    26  // Path is the currently loaded config location
    27  // or default if no config exits
    28  var (
    29  	Features cnquery.Features
    30  )
    31  
    32  const (
    33  	configSourceBase64 = "$MONDOO_CONFIG_BASE64"
    34  	defaultAPIendpoint = "https://us.api.mondoo.com"
    35  )
    36  
    37  // Init initializes and loads the mondoo config
    38  func Init(rootCmd *cobra.Command) {
    39  	cobra.OnInitialize(InitViperConfig)
    40  	Features = getFeatures()
    41  	// persistent flags are global for the application
    42  	rootCmd.PersistentFlags().StringVar(&UserProvidedPath, "config", "", "Set config file path (default $HOME/.config/mondoo/mondoo.yml)")
    43  }
    44  
    45  func getFeatures() cnquery.Features {
    46  	bitSet := make([]bool, 256)
    47  	flags := []byte{}
    48  
    49  	for _, f := range cnquery.DefaultFeatures {
    50  		if !bitSet[f] {
    51  			bitSet[f] = true
    52  			flags = append(flags, f)
    53  		}
    54  	}
    55  
    56  	envFeatures := viper.GetStringSlice("features")
    57  	for _, name := range envFeatures {
    58  		flag, ok := cnquery.FeaturesValue[name]
    59  		if ok {
    60  			if !bitSet[byte(flag)] {
    61  				bitSet[byte(flag)] = true
    62  				flags = append(flags, byte(flag))
    63  			}
    64  		} else {
    65  			log.Warn().Str("feature", name).Msg("could not parse feature")
    66  		}
    67  	}
    68  
    69  	return cnquery.Features(flags)
    70  }
    71  
    72  func InitViperConfig() {
    73  	viper.SetConfigType("yaml")
    74  
    75  	Path = strings.TrimSpace(UserProvidedPath)
    76  	// base 64 config env setting has always precedence
    77  	if len(os.Getenv("MONDOO_CONFIG_BASE64")) > 0 {
    78  		Source = configSourceBase64
    79  		decodedData, err := base64.StdEncoding.DecodeString(os.Getenv("MONDOO_CONFIG_BASE64"))
    80  		if err != nil {
    81  			log.Fatal().Err(err).Msg("could not parse base64 ")
    82  		}
    83  		viper.ReadConfig(bytes.NewBuffer(decodedData))
    84  	} else if len(Path) == 0 && len(os.Getenv("MONDOO_CONFIG_PATH")) > 0 {
    85  		// fallback to env variable if provided, but only if --config is not used
    86  		Source = "$MONDOO_CONFIG_PATH"
    87  		Path = os.Getenv("MONDOO_CONFIG_PATH")
    88  	} else if len(Path) != 0 {
    89  		Source = "--config"
    90  	} else {
    91  		Source = "default"
    92  	}
    93  	if strings.HasPrefix(Path, AWS_SSM_PARAMETERSTORE_PREFIX) {
    94  		err := loadAwsSSMParameterStore(Path)
    95  		if err != nil {
    96  			LoadedConfig = false
    97  			log.Error().Err(err).Str("path", Path).Msg("could not load aws parameter store config")
    98  		} else {
    99  			LoadedConfig = true
   100  		}
   101  	}
   102  
   103  	// check if the default config file is available
   104  	if Path == "" && Source != configSourceBase64 {
   105  		Path = autodetectConfig()
   106  	}
   107  
   108  	if Source != configSourceBase64 {
   109  		// we set this here, so that sub commands that rely on writing config, can use the default config
   110  		viper.SetConfigFile(Path)
   111  
   112  		// if the file exists, load it
   113  		_, err := AppFs.Stat(Path)
   114  		if err == nil {
   115  			log.Debug().Str("configfile", viper.ConfigFileUsed()).Msg("try to load local config file")
   116  			if err := viper.ReadInConfig(); err == nil {
   117  				LoadedConfig = true
   118  			} else {
   119  				LoadedConfig = false
   120  				log.Error().Err(err).Str("path", Path).Msg("could not read config file")
   121  			}
   122  		}
   123  	}
   124  
   125  	// by default it uses console output, for production we may want to set it to json output
   126  	if viper.GetString("log.format") == "json" {
   127  		logger.UseJSONLogging(logger.LogOutputWriter)
   128  	}
   129  
   130  	if viper.GetBool("log.color") == true {
   131  		logger.CliCompactLogger(logger.LogOutputWriter)
   132  	}
   133  
   134  	// override values with env variables
   135  	viper.SetEnvPrefix("mondoo")
   136  	// to parse env variables properly we need to replace some chars
   137  	// all hyphens need to be underscores
   138  	// all dots neeto to be underscores
   139  	replacer := strings.NewReplacer("-", "_", ".", "_")
   140  	viper.SetEnvKeyReplacer(replacer)
   141  
   142  	// read in environment variables that match
   143  	viper.AutomaticEnv()
   144  }
   145  
   146  func DisplayUsedConfig() {
   147  	// print config file
   148  	if !LoadedConfig && len(UserProvidedPath) > 0 {
   149  		log.Warn().Msg("could not load configuration file " + UserProvidedPath)
   150  	} else if LoadedConfig {
   151  		log.Info().Msg("loaded configuration from " + viper.ConfigFileUsed() + " using source " + Source)
   152  	} else if Source == configSourceBase64 {
   153  		log.Info().Msg("loaded configuration from environment using source " + Source)
   154  	} else {
   155  		log.Info().Msg("no Mondoo configuration file provided, using defaults")
   156  	}
   157  }
   158  
   159  func Read() (*Config, error) {
   160  	// load viper config into a struct
   161  	var opts Config
   162  	err := viper.Unmarshal(&opts)
   163  	if err != nil {
   164  		return nil, errors.Wrap(err, "unable to decode into config struct")
   165  	}
   166  
   167  	return &opts, nil
   168  }
   169  
   170  type Config struct {
   171  	// inherit common config
   172  	CommonOpts `mapstructure:",squash"`
   173  
   174  	// Asset Category
   175  	Category               string `json:"category,omitempty" mapstructure:"category"`
   176  	AutoDetectCICDCategory bool   `json:"detect-cicd,omitempty" mapstructure:"detect-cicd"`
   177  }
   178  
   179  type CommonOpts struct {
   180  	// client identifier
   181  	AgentMrn string `json:"agent_mrn,omitempty" mapstructure:"agent_mrn"`
   182  
   183  	// service account credentials
   184  	ServiceAccountMrn string `json:"mrn,omitempty" mapstructure:"mrn"`
   185  	ParentMrn         string `json:"parent_mrn,omitempty" mapstructure:"parent_mrn"`
   186  	SpaceMrn          string `json:"space_mrn,omitempty" mapstructure:"space_mrn"`
   187  	PrivateKey        string `json:"private_key,omitempty" mapstructure:"private_key"`
   188  	Certificate       string `json:"certificate,omitempty" mapstructure:"certificate"`
   189  	APIEndpoint       string `json:"api_endpoint,omitempty" mapstructure:"api_endpoint"`
   190  
   191  	// authentication
   192  	Authentication *CliConfigAuthentication `json:"auth,omitempty" mapstructure:"auth"`
   193  
   194  	// client features
   195  	Features []string `json:"features,omitempty" mapstructure:"features"`
   196  
   197  	// API Proxy for communicating with Mondoo API
   198  	APIProxy string `json:"api_proxy,omitempty" mapstructure:"api_proxy"`
   199  
   200  	// labels that will be applied to all assets
   201  	Labels map[string]string `json:"labels,omitempty" mapstructure:"labels"`
   202  
   203  	// annotations that will be applied to all assets
   204  	Annotations map[string]string `json:"annotations,omitempty" mapstructure:"annotations"`
   205  }
   206  
   207  type CliConfigAuthentication struct {
   208  	Method string `json:"method,omitempty" mapstructure:"method"`
   209  }
   210  
   211  func (c *CommonOpts) GetFeatures() cnquery.Features {
   212  	bitSet := make([]bool, 256)
   213  	flags := []byte{}
   214  
   215  	for _, f := range cnquery.DefaultFeatures {
   216  		if !bitSet[f] {
   217  			bitSet[f] = true
   218  			flags = append(flags, f)
   219  		}
   220  	}
   221  
   222  	for _, name := range c.Features {
   223  		flag, ok := cnquery.FeaturesValue[name]
   224  		if ok {
   225  			if !bitSet[byte(flag)] {
   226  				bitSet[byte(flag)] = true
   227  				flags = append(flags, byte(flag))
   228  			}
   229  		} else {
   230  			log.Warn().Str("feature", name).Msg("could not parse feature")
   231  		}
   232  	}
   233  
   234  	return flags
   235  }
   236  
   237  // GetServiceCredential returns the service credential that is defined in the config.
   238  // If no service credential is defined, it will return nil.
   239  func (c *CommonOpts) GetServiceCredential() *upstream.ServiceAccountCredentials {
   240  	if c.Authentication != nil && c.Authentication.Method == "ssh" {
   241  		log.Info().Msg("using ssh authentication method, generate temporary credentials")
   242  		serviceAccount, err := upstream.ExchangeSSHKey(c.UpstreamApiEndpoint(), c.ServiceAccountMrn, c.GetParentMrn())
   243  		if err != nil {
   244  			log.Error().Err(err).Msg("could not exchange ssh key")
   245  			return nil
   246  		}
   247  		return serviceAccount
   248  	}
   249  
   250  	// return nil when no service account is defined
   251  	if c.ServiceAccountMrn == "" && c.PrivateKey == "" && c.Certificate == "" {
   252  		return nil
   253  	}
   254  
   255  	return &upstream.ServiceAccountCredentials{
   256  		Mrn:         c.ServiceAccountMrn,
   257  		ParentMrn:   c.GetParentMrn(),
   258  		PrivateKey:  c.PrivateKey,
   259  		Certificate: c.Certificate,
   260  		ApiEndpoint: c.APIEndpoint,
   261  	}
   262  }
   263  
   264  func (c *CommonOpts) GetParentMrn() string {
   265  	parent := c.ParentMrn
   266  
   267  	// fallback to old space_mrn config
   268  	if parent == "" {
   269  		parent = c.SpaceMrn
   270  	}
   271  
   272  	return parent
   273  }
   274  
   275  func (c *CommonOpts) UpstreamApiEndpoint() string {
   276  	apiEndpoint := c.APIEndpoint
   277  
   278  	// fallback to default api if nothing was set
   279  	if apiEndpoint == "" {
   280  		apiEndpoint = defaultAPIendpoint
   281  	}
   282  
   283  	return apiEndpoint
   284  }