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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package cmd
     5  
     6  import (
     7  	"os"
     8  
     9  	"github.com/cockroachdb/errors"
    10  	"github.com/hashicorp/go-plugin"
    11  	"github.com/rs/zerolog/log"
    12  	"github.com/spf13/cobra"
    13  	"go.mondoo.com/cnquery/cli/config"
    14  	"go.mondoo.com/cnquery/cli/printer"
    15  	"go.mondoo.com/cnquery/cli/reporter"
    16  	"go.mondoo.com/cnquery/cli/shell"
    17  	"go.mondoo.com/cnquery/llx"
    18  	"go.mondoo.com/cnquery/logger"
    19  	"go.mondoo.com/cnquery/mqlc"
    20  	"go.mondoo.com/cnquery/mqlc/parser"
    21  	"go.mondoo.com/cnquery/providers"
    22  	pp "go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    23  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    24  	"go.mondoo.com/cnquery/shared"
    25  	run "go.mondoo.com/cnquery/shared/proto"
    26  	"google.golang.org/protobuf/proto"
    27  )
    28  
    29  // pluginCmd represents the version command
    30  var pluginCmd = &cobra.Command{
    31  	Use:    "run_as_plugin",
    32  	Hidden: true,
    33  	Short:  "Run as a plugin.",
    34  	Run: func(cmd *cobra.Command, args []string) {
    35  		plugin.Serve(&plugin.ServeConfig{
    36  			HandshakeConfig: shared.Handshake,
    37  			Plugins: map[string]plugin.Plugin{
    38  				"counter": &shared.CNQueryPlugin{Impl: &cnqueryPlugin{}},
    39  			},
    40  
    41  			// A non-nil value here enables gRPC serving for this plugin...
    42  			GRPCServer: plugin.DefaultGRPCServer,
    43  		})
    44  	},
    45  }
    46  
    47  func init() {
    48  	rootCmd.AddCommand(pluginCmd)
    49  }
    50  
    51  type cnqueryPlugin struct{}
    52  
    53  func (c *cnqueryPlugin) RunQuery(conf *run.RunQueryConfig, runtime *providers.Runtime, out shared.OutputHelper) error {
    54  	if conf.Command == "" && conf.Input == "" {
    55  		return errors.New("No command provided, nothing to do.")
    56  	}
    57  
    58  	opts, optsErr := config.Read()
    59  	if optsErr != nil {
    60  		log.Fatal().Err(optsErr).Msg("could not load configuration")
    61  	}
    62  
    63  	config.DisplayUsedConfig()
    64  
    65  	if conf.DoParse {
    66  		ast, err := parser.Parse(conf.Command)
    67  		if err != nil {
    68  			return errors.Wrap(err, "failed to parse command")
    69  		}
    70  		out.WriteString(logger.PrettyJSON(ast))
    71  		return nil
    72  	}
    73  
    74  	if conf.DoAst {
    75  		b, err := mqlc.Compile(conf.Command, nil, mqlc.NewConfig(runtime.Schema(), conf.Features))
    76  		if err != nil {
    77  			return errors.Wrap(err, "failed to compile command")
    78  		}
    79  
    80  		out.WriteString(logger.PrettyJSON((b)) + "\n" + printer.DefaultPrinter.CodeBundle(b))
    81  		return nil
    82  	}
    83  
    84  	var upstreamConfig *upstream.UpstreamConfig
    85  	serviceAccount := opts.GetServiceCredential()
    86  	if serviceAccount != nil {
    87  		upstreamConfig = &upstream.UpstreamConfig{
    88  			SpaceMrn:    opts.GetParentMrn(),
    89  			ApiEndpoint: opts.UpstreamApiEndpoint(),
    90  			Incognito:   conf.Incognito,
    91  			Creds:       serviceAccount,
    92  		}
    93  	}
    94  
    95  	err := runtime.Connect(&pp.ConnectReq{
    96  		Features: config.Features,
    97  		Asset:    conf.Inventory.Spec.Assets[0],
    98  		Upstream: upstreamConfig,
    99  	})
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	if conf.Format == "json" {
   105  		out.WriteString("[")
   106  	}
   107  
   108  	// FIXME: workaround for gcp-snapshot
   109  	// For a gcp-snapshot asset, we start with a GCP connection.
   110  	// This get's overridden by a filesystem connection. The filesystem connection is what we need for the scan
   111  	// But later, we need the GCP runtime to cleanup the snapshot disk
   112  	if runtime.Provider.Instance.Name == "gcp" && runtime.Provider.Connection.Name == "filesystem" {
   113  		defer runtime.Close()
   114  	}
   115  
   116  	assets, err := providers.ProcessAssetCandidates(runtime, runtime.Provider.Connection, upstreamConfig, conf.PlatformId)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	for i := range assets {
   122  		connectAsset := assets[i]
   123  		connectAssetRuntime, err := providers.Coordinator.RuntimeFor(connectAsset, runtime)
   124  		if err != nil {
   125  			return err
   126  		}
   127  
   128  		err = connectAssetRuntime.Connect(&pp.ConnectReq{
   129  			Features: config.Features,
   130  			Asset:    connectAsset,
   131  			Upstream: upstreamConfig,
   132  		})
   133  		if err != nil {
   134  			return err
   135  		}
   136  
   137  		// when we close the shell, we need to close the backend and store the recording
   138  		onCloseHandler := func() {
   139  			// FIXME: store recording
   140  			// m.StoreRecording(viper.GetString("record-file"))
   141  		}
   142  
   143  		shellOptions := []shell.ShellOption{}
   144  		shellOptions = append(shellOptions, shell.WithOnCloseListener(onCloseHandler))
   145  		shellOptions = append(shellOptions, shell.WithFeatures(conf.Features))
   146  		shellOptions = append(shellOptions, shell.WithOutput(out))
   147  
   148  		if upstreamConfig != nil {
   149  			shellOptions = append(shellOptions, shell.WithUpstreamConfig(upstreamConfig))
   150  		}
   151  
   152  		sh, err := shell.New(connectAssetRuntime, shellOptions...)
   153  		if err != nil {
   154  			return errors.Wrap(err, "failed to initialize the shell")
   155  		}
   156  		defer func() {
   157  			// prevent the recording from being closed multiple times
   158  			connectAssetRuntime.Recording = providers.NullRecording{}
   159  			sh.Close()
   160  		}()
   161  
   162  		var code *llx.CodeBundle
   163  		var results map[string]*llx.RawResult
   164  		if conf.Input != "" {
   165  			var raw []byte
   166  			raw, err = os.ReadFile(conf.Input)
   167  			if err != nil {
   168  				return errors.Wrap(err, "failed to read code bundle from file")
   169  			}
   170  			var b llx.CodeBundle
   171  			if err = proto.Unmarshal(raw, &b); err != nil {
   172  				return errors.Wrap(err, "failed to unmarshal code bundle")
   173  			}
   174  			code = &b
   175  			results, err = sh.RunOnceBundle(code)
   176  		} else {
   177  			code, results, err = sh.RunOnce(conf.Command)
   178  		}
   179  		if err != nil {
   180  			return errors.Wrap(err, "failed to run")
   181  		}
   182  
   183  		if conf.Format == "llx" && conf.Output != "" {
   184  			out, err := proto.Marshal(code)
   185  			if err != nil {
   186  				return errors.Wrap(err, "failed to marshal code bundle")
   187  			}
   188  			err = os.WriteFile(conf.Output, out, 0o644)
   189  			if err != nil {
   190  				return errors.Wrap(err, "failed to save code bundle")
   191  			}
   192  			return nil
   193  		}
   194  
   195  		if conf.Format != "json" {
   196  			sh.PrintResults(code, results)
   197  		} else {
   198  			reporter.BundleResultsToJSON(code, results, out)
   199  			if len(assets) != i+1 {
   200  				out.WriteString(",")
   201  			}
   202  		}
   203  
   204  	}
   205  
   206  	if conf.Format == "json" {
   207  		out.WriteString("]")
   208  	}
   209  
   210  	return nil
   211  }