go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/apps/cnquery/cmd/shell.go (about) 1 // Copyright (c) Mondoo, Inc. 2 // SPDX-License-Identifier: BUSL-1.1 3 4 package cmd 5 6 import ( 7 "errors" 8 "fmt" 9 "os" 10 11 "github.com/mattn/go-isatty" 12 "github.com/rs/zerolog/log" 13 "github.com/spf13/cobra" 14 "github.com/spf13/viper" 15 "go.mondoo.com/cnquery" 16 "go.mondoo.com/cnquery/cli/components" 17 "go.mondoo.com/cnquery/cli/config" 18 "go.mondoo.com/cnquery/cli/shell" 19 "go.mondoo.com/cnquery/cli/theme" 20 "go.mondoo.com/cnquery/providers" 21 "go.mondoo.com/cnquery/providers-sdk/v1/inventory" 22 "go.mondoo.com/cnquery/providers-sdk/v1/inventory/manager" 23 "go.mondoo.com/cnquery/providers-sdk/v1/plugin" 24 "go.mondoo.com/cnquery/providers-sdk/v1/upstream" 25 ) 26 27 func init() { 28 rootCmd.AddCommand(shellCmd) 29 30 shellCmd.Flags().StringP("command", "c", "", "MQL query to executed in the shell.") 31 shellCmd.Flags().String("platform-id", "", "Select a specific target asset by providing its platform ID.") 32 } 33 34 var shellCmd = &cobra.Command{ 35 Use: "shell", 36 Short: "Interactive query shell for MQL.", 37 Long: `Allows the interactive exploration of MQL queries.`, 38 PreRun: func(cmd *cobra.Command, args []string) { 39 viper.BindPFlag("platform-id", cmd.Flags().Lookup("platform-id")) 40 }, 41 // we have to initialize an empty run so it shows up as a runnable command in --help 42 Run: func(cmd *cobra.Command, args []string) {}, 43 } 44 45 var shellRun = func(cmd *cobra.Command, runtime *providers.Runtime, cliRes *plugin.ParseCLIRes) { 46 shellConf := ParseShellConfig(cmd, cliRes) 47 if err := StartShell(runtime, shellConf); err != nil { 48 log.Fatal().Err(err).Msg("failed to run query") 49 } 50 } 51 52 // ShellConfig is the shared configuration for running a shell given all 53 // commandline and config inputs. 54 // TODO: the config is a shared structure, which should be moved to proto 55 type ShellConfig struct { 56 Command string 57 Asset *inventory.Asset 58 Features cnquery.Features 59 PlatformID string 60 WelcomeMessage string 61 UpstreamConfig *upstream.UpstreamConfig 62 } 63 64 func ParseShellConfig(cmd *cobra.Command, cliRes *plugin.ParseCLIRes) *ShellConfig { 65 conf, err := config.Read() 66 if err != nil { 67 log.Fatal().Err(err).Msg("failed to load config") 68 } 69 70 config.DisplayUsedConfig() 71 72 var upstreamConfig *upstream.UpstreamConfig 73 serviceAccount := conf.GetServiceCredential() 74 if serviceAccount != nil { 75 upstreamConfig = &upstream.UpstreamConfig{ 76 // AssetMrn: not necessary right now, especially since incognito 77 SpaceMrn: conf.GetParentMrn(), 78 ApiEndpoint: conf.UpstreamApiEndpoint(), 79 Incognito: viper.GetBool("incognito"), 80 Creds: conf.GetServiceCredential(), 81 } 82 } 83 84 shellConf := ShellConfig{ 85 Features: config.Features, 86 PlatformID: viper.GetString("platform-id"), 87 Asset: cliRes.Asset, 88 UpstreamConfig: upstreamConfig, 89 } 90 91 shellConf.Command, _ = cmd.Flags().GetString("command") 92 return &shellConf 93 } 94 95 // StartShell will start an interactive CLI shell 96 func StartShell(runtime *providers.Runtime, conf *ShellConfig) error { 97 // we go through inventory resolution to resolve credentials properly for the passed-in asset 98 im, err := manager.NewManager(manager.WithInventory(inventory.New(inventory.WithAssets(conf.Asset)), runtime)) 99 if err != nil { 100 return errors.New("failed to resolve inventory for connection") 101 } 102 resolvedAsset, err := im.ResolveAsset(conf.Asset) 103 if err != nil { 104 return err 105 } 106 res, err := runtime.Provider.Instance.Plugin.Connect(&plugin.ConnectReq{ 107 Features: conf.Features, 108 Asset: resolvedAsset, 109 Upstream: nil, 110 }, nil) 111 if err != nil { 112 log.Fatal().Err(err).Msg("could not load asset information") 113 } 114 115 assets, err := providers.ProcessAssetCandidates(runtime, res, conf.UpstreamConfig, conf.PlatformID) 116 if err != nil { 117 log.Fatal().Err(err).Msg("could not process assets") 118 } 119 if len(assets) == 0 { 120 log.Fatal().Msg("could not find an asset that we can connect to") 121 } 122 123 connectAsset := assets[0] 124 if len(assets) > 1 { 125 isTTY := isatty.IsTerminal(os.Stdout.Fd()) 126 if isTTY { 127 connectAsset = components.AssetSelect(assets) 128 } else { 129 fmt.Println(components.AssetList(theme.OperatingSystemTheme, assets)) 130 log.Fatal().Msg("cannot connect to more than one asset, use --platform-id to select a specific asset") 131 } 132 } 133 134 if connectAsset == nil { 135 log.Fatal().Msg("no asset selected") 136 } 137 138 err = runtime.Connect(&plugin.ConnectReq{ 139 Features: conf.Features, 140 Asset: connectAsset, 141 Upstream: conf.UpstreamConfig, 142 }) 143 if err != nil { 144 log.Fatal().Err(err).Msg("failed to connect to asset") 145 } 146 log.Info().Msgf("connected to %s", runtime.Provider.Connection.Asset.Platform.Title) 147 148 // when we close the shell, we need to close the backend and store the recording 149 onCloseHandler := func() { 150 runtime.Close() 151 } 152 153 shellOptions := []shell.ShellOption{} 154 shellOptions = append(shellOptions, shell.WithOnCloseListener(onCloseHandler)) 155 shellOptions = append(shellOptions, shell.WithFeatures(conf.Features)) 156 157 sh, err := shell.New(runtime, shellOptions...) 158 if err != nil { 159 log.Error().Err(err).Msg("failed to initialize interactive shell") 160 } 161 if conf.WelcomeMessage != "" { 162 sh.Theme.Welcome = conf.WelcomeMessage 163 } 164 sh.RunInteractive(conf.Command) 165 166 return nil 167 }