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 }