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  }