go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/apps/cnquery/cmd/root.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package cmd
     5  
     6  import (
     7  	"net/http"
     8  	"os"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/rs/zerolog"
    13  	"github.com/rs/zerolog/log"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/cobra/doc"
    16  	"github.com/spf13/viper"
    17  	"go.mondoo.com/cnquery"
    18  	"go.mondoo.com/cnquery/cli/config"
    19  	"go.mondoo.com/cnquery/cli/providers"
    20  	"go.mondoo.com/cnquery/cli/sysinfo"
    21  	"go.mondoo.com/cnquery/cli/theme"
    22  	"go.mondoo.com/cnquery/logger"
    23  	"go.mondoo.com/ranger-rpc"
    24  	"go.mondoo.com/ranger-rpc/plugins/scope"
    25  )
    26  
    27  const (
    28  	askForPasswordValue = ">passwordisnotset<"
    29  	rootCmdDesc         = "cnquery is a cloud-native tool for querying your entire fleet.\n"
    30  
    31  	// we send a 78 exit code to prevent systemd service from restart
    32  	ConfigurationErrorCode = 78
    33  )
    34  
    35  // rootCmd represents the base command when called without any subcommands
    36  var rootCmd = &cobra.Command{
    37  	Use:   "cnquery",
    38  	Short: "cnquery CLI",
    39  	Long:  theme.DefaultTheme.Landing + "\n\n" + rootCmdDesc,
    40  	PersistentPreRun: func(cmd *cobra.Command, args []string) {
    41  		initLogger(cmd)
    42  	},
    43  }
    44  
    45  // Execute adds all child commands to the root command and sets flags appropriately.
    46  // This is called by main.main(). It only needs to happen once to the rootCmd.
    47  func Execute() {
    48  	err := providers.AttachCLIs(
    49  		rootCmd,
    50  		&providers.Command{
    51  			Command: shellCmd,
    52  			Run:     shellRun,
    53  			Action:  "Interactive shell with ",
    54  		},
    55  		&providers.Command{
    56  			Command: RunCmd,
    57  			Run:     RunCmdRun,
    58  			Action:  "Run a query with ",
    59  		},
    60  		&providers.Command{
    61  			Command: scanCmd,
    62  			Run:     scanCmdRun,
    63  			Action:  "Scan ",
    64  		},
    65  	)
    66  	if err != nil {
    67  		log.Error().Msg(err.Error())
    68  		os.Exit(1)
    69  	}
    70  
    71  	if err := rootCmd.Execute(); err != nil {
    72  		log.Error().Msg(err.Error())
    73  		os.Exit(1)
    74  	}
    75  }
    76  
    77  func init() {
    78  	// NOTE: we need to call this super early, otherwise the CLI color output on Windows is broken for the first lines
    79  	// since the log instance is already initialized, replace default zerolog color output with our own
    80  	// use color logger by default
    81  	logger.CliCompactLogger(logger.LogOutputWriter)
    82  	zerolog.SetGlobalLevel(zerolog.InfoLevel)
    83  
    84  	// TODO harmonize with initLogger, which is called later and attached to the command
    85  	// here we set the log level only by environment variable
    86  	envLevel, ok := logger.GetEnvLogLevel()
    87  	if ok {
    88  		logger.Set(envLevel)
    89  	}
    90  
    91  	config.DefaultConfigFile = "mondoo.yml"
    92  
    93  	rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output")
    94  	rootCmd.PersistentFlags().String("log-level", "info", "Set log level: error, warn, info, debug, trace")
    95  	rootCmd.PersistentFlags().String("api-proxy", "", "Set proxy for communications with Mondoo API")
    96  	rootCmd.PersistentFlags().Bool("auto-update", true, "Enable automatic provider installation and update")
    97  	viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
    98  	viper.BindPFlag("log-level", rootCmd.PersistentFlags().Lookup("log-level"))
    99  	viper.BindPFlag("api_proxy", rootCmd.PersistentFlags().Lookup("api-proxy"))
   100  	viper.BindPFlag("auto_update", rootCmd.PersistentFlags().Lookup("auto-update"))
   101  	viper.BindEnv("features")
   102  
   103  	config.Init(rootCmd)
   104  }
   105  
   106  func initLogger(cmd *cobra.Command) {
   107  	// environment variables always over-write custom flags
   108  	envLevel, ok := logger.GetEnvLogLevel()
   109  	if ok {
   110  		logger.Set(envLevel)
   111  		return
   112  	}
   113  
   114  	// retrieve log-level from flags
   115  	level := viper.GetString("log-level")
   116  	if v := viper.GetBool("verbose"); v {
   117  		level = "debug"
   118  	}
   119  	logger.Set(level)
   120  }
   121  
   122  func defaultRangerPlugins(sysInfo *sysinfo.SystemInfo, features cnquery.Features) []ranger.ClientPlugin {
   123  	plugins := []ranger.ClientPlugin{}
   124  	plugins = append(plugins, scope.NewRequestIDRangerPlugin())
   125  	plugins = append(plugins, sysInfoHeader(sysInfo, features))
   126  	return plugins
   127  }
   128  
   129  func sysInfoHeader(sysInfo *sysinfo.SystemInfo, features cnquery.Features) ranger.ClientPlugin {
   130  	const (
   131  		HttpHeaderUserAgent      = "User-Agent"
   132  		HttpHeaderClientFeatures = "Mondoo-Features"
   133  		HttpHeaderPlatformID     = "Mondoo-PlatformID"
   134  	)
   135  
   136  	h := http.Header{}
   137  	info := map[string]string{
   138  		"cnquery": cnquery.Version,
   139  		"build":   cnquery.Build,
   140  	}
   141  	if sysInfo != nil {
   142  		info["PN"] = sysInfo.Platform.Name
   143  		info["PR"] = sysInfo.Platform.Version
   144  		info["PA"] = sysInfo.Platform.Arch
   145  		info["IP"] = sysInfo.IP
   146  		info["HN"] = sysInfo.Hostname
   147  		h.Set(HttpHeaderPlatformID, sysInfo.PlatformId)
   148  	}
   149  	h.Set(HttpHeaderUserAgent, scope.XInfoHeader(info))
   150  	h.Set(HttpHeaderClientFeatures, features.Encode())
   151  	return scope.NewCustomHeaderRangerPlugin(h)
   152  }
   153  
   154  var reMdName = regexp.MustCompile(`/([^/]+)\.md$`)
   155  
   156  func GenerateMarkdown(dir string) error {
   157  	rootCmd.DisableAutoGenTag = true
   158  
   159  	// We need to remove our fancy logo from the markdown output,
   160  	// since it messes with the formatting.
   161  	rootCmd.Long = rootCmdDesc
   162  
   163  	files := []string{}
   164  	err := doc.GenMarkdownTreeCustom(rootCmd, dir, func(s string) string {
   165  		files = append(files, s)
   166  
   167  		titles := reMdName.FindStringSubmatch(s)
   168  		if len(titles) == 0 {
   169  			return ""
   170  		}
   171  		title := strings.ReplaceAll(titles[1], "_", " ")
   172  
   173  		return "---\n" +
   174  			"id: " + titles[1] + "\n" +
   175  			"title: " + title + "\n" +
   176  			"---\n\n"
   177  	}, func(s string) string { return s })
   178  	if err != nil {
   179  		return err
   180  	}
   181  
   182  	// we need to remove the first headline, since it is doubled with the
   183  	// headline from the ID. Really annoying, all this needs a rewrite.
   184  	for i := range files {
   185  		file := files[i]
   186  		raw, err := os.ReadFile(file)
   187  		if err != nil {
   188  			return err
   189  		}
   190  
   191  		if !strings.HasPrefix(string(raw), "---\nid:") {
   192  			continue
   193  		}
   194  
   195  		start := strings.Index(string(raw), "\n## ")
   196  		if start < 0 {
   197  			continue
   198  		}
   199  
   200  		end := start
   201  		for i := start + 3; i < len(raw); i++ {
   202  			if raw[i] == '\n' {
   203  				end = i
   204  				break
   205  			}
   206  		}
   207  
   208  		res := append(raw[0:start], raw[end:]...)
   209  		err = os.WriteFile(file, res, 0o644)
   210  		if err != nil {
   211  			return err
   212  		}
   213  	}
   214  
   215  	return nil
   216  }