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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package cmd
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"os"
    10  	"sort"
    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/cli/config"
    18  	"go.mondoo.com/cnquery/cli/execruntime"
    19  	"go.mondoo.com/cnquery/cli/inventoryloader"
    20  	"go.mondoo.com/cnquery/cli/reporter"
    21  	"go.mondoo.com/cnquery/cli/theme"
    22  	"go.mondoo.com/cnquery/explorer"
    23  	"go.mondoo.com/cnquery/explorer/scan"
    24  	"go.mondoo.com/cnquery/providers"
    25  	"go.mondoo.com/cnquery/providers-sdk/v1/inventory"
    26  	"go.mondoo.com/cnquery/providers-sdk/v1/plugin"
    27  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    28  )
    29  
    30  func init() {
    31  	rootCmd.AddCommand(scanCmd)
    32  
    33  	scanCmd.Flags().StringP("output", "o", "compact", "Set output format: "+reporter.AllFormats())
    34  	scanCmd.Flags().BoolP("json", "j", false, "Run the query and return the object in a JSON structure.")
    35  	scanCmd.Flags().String("platform-id", "", "Select a specific target asset by providing its platform ID.")
    36  
    37  	scanCmd.Flags().String("inventory-file", "", "Set the path to the inventory file.")
    38  	scanCmd.Flags().Bool("inventory-ansible", false, "Set the inventory format to Ansible.")
    39  	scanCmd.Flags().Bool("inventory-domainlist", false, "Set the inventory format to domain list.")
    40  
    41  	// bundles, packs & incognito mode
    42  	scanCmd.Flags().Bool("incognito", false, "Run in incognito mode. Do not report scan results to  Mondoo Platform.")
    43  	scanCmd.Flags().StringSlice("querypack", nil, "Set the query packs to execute. This requires `querypack-bundle`. You can specify multiple UIDs.")
    44  	scanCmd.Flags().StringSliceP("querypack-bundle", "f", nil, "Path to local query pack file")
    45  	// flag completion command
    46  	scanCmd.RegisterFlagCompletionFunc("querypack", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
    47  		return getQueryPacksForCompletion(), cobra.ShellCompDirectiveDefault
    48  	})
    49  	scanCmd.Flags().String("asset-name", "", "User-override for the asset name")
    50  	scanCmd.Flags().StringToString("annotation", nil, "Add an annotation to the asset.") // user-added, editable
    51  	scanCmd.Flags().StringToString("props", nil, "Custom values for properties")
    52  
    53  	// v6 should make detect-cicd and category flag public
    54  	scanCmd.Flags().Bool("detect-cicd", true, "Try to detect CI/CD environments. If detected, set the asset category to 'cicd'.")
    55  	scanCmd.Flags().String("category", "inventory", "Set the category for the assets to 'inventory|cicd'.")
    56  	scanCmd.Flags().MarkHidden("category")
    57  }
    58  
    59  var scanCmd = &cobra.Command{
    60  	Use:   "scan",
    61  	Short: "Scan assets with one or more query packs.",
    62  	Long: `
    63  This command scans an asset using a query pack. For example, you can scan
    64  the local system with its pre-configured query pack:
    65  
    66  		$ cnquery scan local
    67  
    68  To manually configure a query pack, use this:
    69  
    70  		$ cnquery scan local -f bundle.mql.yaml --incognito
    71  
    72  `,
    73  	PreRun: func(cmd *cobra.Command, args []string) {
    74  		// Special handling for users that want to see what output options are
    75  		// available. We have to do this before printing the help because we
    76  		// don't have a target connection or provider.
    77  		output, _ := cmd.Flags().GetString("output")
    78  		if output == "help" {
    79  			fmt.Println("Available output formats: " + reporter.AllFormats())
    80  			os.Exit(0)
    81  		}
    82  
    83  		viper.BindPFlag("platform-id", cmd.Flags().Lookup("platform-id"))
    84  
    85  		viper.BindPFlag("inventory-file", cmd.Flags().Lookup("inventory-file"))
    86  		viper.BindPFlag("inventory-ansible", cmd.Flags().Lookup("inventory-ansible"))
    87  		viper.BindPFlag("inventory-domainlist", cmd.Flags().Lookup("inventory-domainlist"))
    88  		viper.BindPFlag("querypack-bundle", cmd.Flags().Lookup("querypack-bundle"))
    89  		viper.BindPFlag("detect-cicd", cmd.Flags().Lookup("detect-cicd"))
    90  		viper.BindPFlag("category", cmd.Flags().Lookup("category"))
    91  
    92  		// for all assets
    93  		viper.BindPFlag("incognito", cmd.Flags().Lookup("incognito"))
    94  		viper.BindPFlag("insecure", cmd.Flags().Lookup("insecure"))
    95  		viper.BindPFlag("querypacks", cmd.Flags().Lookup("querypack"))
    96  		viper.BindPFlag("sudo.active", cmd.Flags().Lookup("sudo"))
    97  		viper.BindPFlag("record", cmd.Flags().Lookup("record"))
    98  
    99  		viper.BindPFlag("output", cmd.Flags().Lookup("output"))
   100  	},
   101  	ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   102  		if len(args) == 0 {
   103  			return []string{"yml", "yaml", "json"}, cobra.ShellCompDirectiveFilterFileExt
   104  		}
   105  		return []string{}, cobra.ShellCompDirectiveNoFileComp
   106  	},
   107  	// we have to initialize an empty run so it shows up as a runnable command in --help
   108  	Run: func(cmd *cobra.Command, args []string) {},
   109  }
   110  
   111  var scanCmdRun = func(cmd *cobra.Command, runtime *providers.Runtime, cliRes *plugin.ParseCLIRes) {
   112  	conf, err := getCobraScanConfig(cmd, runtime, cliRes)
   113  	if err != nil {
   114  		log.Fatal().Err(err).Msg("failed to prepare config")
   115  	}
   116  
   117  	err = conf.loadBundles()
   118  	if err != nil {
   119  		log.Fatal().Err(err).Msg("failed to resolve query packs")
   120  	}
   121  
   122  	report, err := RunScan(conf)
   123  	if err != nil {
   124  		log.Fatal().Err(err).Msg("failed to run scan")
   125  	}
   126  
   127  	printReports(report, conf, cmd)
   128  }
   129  
   130  // helper method to retrieve the list of query packs for autocomplete
   131  func getQueryPacksForCompletion() []string {
   132  	querypackList := []string{}
   133  
   134  	// TODO: autocompletion
   135  	sort.Strings(querypackList)
   136  
   137  	return querypackList
   138  }
   139  
   140  type scanConfig struct {
   141  	Features       cnquery.Features
   142  	Inventory      *inventory.Inventory
   143  	Output         string
   144  	QueryPackPaths []string
   145  	QueryPackNames []string
   146  	Props          map[string]string
   147  	Bundle         *explorer.Bundle
   148  	runtime        *providers.Runtime
   149  	// annotations that will be applied to all discovered assets
   150  	annotations map[string]string
   151  
   152  	IsIncognito bool
   153  }
   154  
   155  func getCobraScanConfig(cmd *cobra.Command, runtime *providers.Runtime, cliRes *plugin.ParseCLIRes) (*scanConfig, error) {
   156  	opts, err := config.Read()
   157  	if err != nil {
   158  		log.Fatal().Err(err).Msg("failed to load config")
   159  	}
   160  
   161  	config.DisplayUsedConfig()
   162  
   163  	props, err := cmd.Flags().GetStringToString("props")
   164  	if err != nil {
   165  		log.Fatal().Err(err).Msg("failed to parse props")
   166  	}
   167  
   168  	inv, err := inventoryloader.ParseOrUse(cliRes.Asset, viper.GetBool("insecure"))
   169  	if err != nil {
   170  		log.Fatal().Err(err).Msg("failed to parse inventory")
   171  	}
   172  
   173  	annotations, err := cmd.Flags().GetStringToString("annotation")
   174  	if err != nil {
   175  		log.Fatal().Err(err).Msg("failed to parse annotations")
   176  	}
   177  
   178  	// merge the config and the user-provided annotations with the latter having precedence
   179  	optAnnotations := opts.Annotations
   180  	if optAnnotations == nil {
   181  		optAnnotations = map[string]string{}
   182  	}
   183  	for k, v := range annotations {
   184  		optAnnotations[k] = v
   185  	}
   186  	conf := scanConfig{
   187  		Features:       opts.GetFeatures(),
   188  		IsIncognito:    viper.GetBool("incognito"),
   189  		Inventory:      inv,
   190  		QueryPackPaths: viper.GetStringSlice("querypack-bundle"),
   191  		QueryPackNames: viper.GetStringSlice("querypacks"),
   192  		Props:          props,
   193  		runtime:        runtime,
   194  		annotations:    optAnnotations,
   195  	}
   196  
   197  	// if users want to get more information on available output options,
   198  	// print them before executing the scan
   199  	output, _ := cmd.Flags().GetString("output")
   200  	if output == "help" {
   201  		fmt.Println("Available output formats: " + reporter.AllFormats())
   202  		os.Exit(0)
   203  	}
   204  
   205  	// --json takes precedence
   206  	if ok, _ := cmd.Flags().GetBool("json"); ok {
   207  		output = "json"
   208  	}
   209  	conf.Output = output
   210  
   211  	// detect CI/CD runs and read labels from runtime and apply them to all assets in the inventory
   212  	runtimeEnv := execruntime.Detect()
   213  	if opts.AutoDetectCICDCategory && runtimeEnv.IsAutomatedEnv() || opts.Category == "cicd" {
   214  		log.Info().Msg("detected ci-cd environment")
   215  		// NOTE: we only apply those runtime environment labels for CI/CD runs to ensure other assets from the
   216  		// inventory are not touched, we may consider to add the data to the flagAsset
   217  		if runtimeEnv != nil {
   218  			runtimeLabels := runtimeEnv.Labels()
   219  			conf.Inventory.ApplyLabels(runtimeLabels)
   220  		}
   221  		conf.Inventory.ApplyCategory(inventory.AssetCategory_CATEGORY_CICD)
   222  	}
   223  
   224  	var serviceAccount *upstream.ServiceAccountCredentials
   225  	if !conf.IsIncognito {
   226  		serviceAccount = opts.GetServiceCredential()
   227  		if serviceAccount != nil {
   228  			// TODO: determine if this needs migrating
   229  			// // determine information about the client
   230  			// sysInfo, err := sysinfo.GatherSystemInfo()
   231  			// if err != nil {
   232  			// 	log.Warn().Err(err).Msg("could not gather client information")
   233  			// }
   234  			// plugins = append(plugins, defaultRangerPlugins(sysInfo, opts.GetFeatures())...)
   235  
   236  			log.Info().Msg("using service account credentials")
   237  			conf.runtime.UpstreamConfig = &upstream.UpstreamConfig{
   238  				SpaceMrn:    opts.GetParentMrn(),
   239  				ApiEndpoint: opts.UpstreamApiEndpoint(),
   240  				Incognito:   conf.IsIncognito,
   241  				Creds:       serviceAccount,
   242  			}
   243  		} else {
   244  			log.Warn().Msg("No credentials provided. Switching to --incognito mode.")
   245  			conf.IsIncognito = true
   246  		}
   247  	}
   248  
   249  	if len(conf.QueryPackPaths) > 0 && !conf.IsIncognito {
   250  		log.Warn().Msg("Scanning with local bundles will switch into --incognito mode by default. Your results will not be sent upstream.")
   251  		conf.IsIncognito = true
   252  	}
   253  
   254  	// print headline when its not printed to yaml
   255  	if output == "" {
   256  		fmt.Fprintln(os.Stdout, theme.DefaultTheme.Welcome)
   257  	}
   258  
   259  	return &conf, nil
   260  }
   261  
   262  func (c *scanConfig) loadBundles() error {
   263  	if c.IsIncognito {
   264  		if len(c.QueryPackPaths) == 0 {
   265  			return nil
   266  		}
   267  
   268  		bundle, err := explorer.BundleFromPaths(c.QueryPackPaths...)
   269  		if err != nil {
   270  			return err
   271  		}
   272  
   273  		_, err = bundle.CompileExt(context.Background(), explorer.BundleCompileConf{
   274  			Schema: c.runtime.Schema(),
   275  			// We don't care about failing queries for local runs. We may only
   276  			// process a subset of all the queries in the bundle. When we receive
   277  			// things from the server, upstream can filter things for us. But running
   278  			// them locally requires us to do it in here.
   279  			RemoveFailing: true,
   280  		})
   281  		if err != nil {
   282  			return errors.Wrap(err, "failed to compile bundle")
   283  		}
   284  
   285  		c.Bundle = bundle
   286  		return nil
   287  	}
   288  
   289  	return nil
   290  }
   291  
   292  func RunScan(config *scanConfig) (*explorer.ReportCollection, error) {
   293  	opts := []scan.ScannerOption{}
   294  	if config.runtime.UpstreamConfig != nil {
   295  		opts = append(opts, scan.WithUpstream(config.runtime.UpstreamConfig))
   296  	}
   297  	if config.runtime.Recording != nil {
   298  		opts = append(opts, scan.WithRecording(config.runtime.Recording))
   299  	}
   300  
   301  	scanner := scan.NewLocalScanner(opts...)
   302  	ctx := cnquery.SetFeatures(context.Background(), config.Features)
   303  
   304  	if config.IsIncognito {
   305  		return scanner.RunIncognito(
   306  			ctx,
   307  			&scan.Job{
   308  				Inventory:        config.Inventory,
   309  				Bundle:           config.Bundle,
   310  				QueryPackFilters: config.QueryPackNames,
   311  				Props:            config.Props,
   312  				Annotations:      config.annotations,
   313  			})
   314  	}
   315  	return scanner.Run(
   316  		ctx,
   317  		&scan.Job{
   318  			Inventory:        config.Inventory,
   319  			Bundle:           config.Bundle,
   320  			QueryPackFilters: config.QueryPackNames,
   321  			Props:            config.Props,
   322  			Annotations:      config.annotations,
   323  		})
   324  }
   325  
   326  func printReports(report *explorer.ReportCollection, conf *scanConfig, cmd *cobra.Command) {
   327  	// print the output using the specified output format
   328  	r, err := reporter.New(conf.Output)
   329  	if err != nil {
   330  		log.Fatal().Msg(err.Error())
   331  	}
   332  
   333  	r.IsIncognito = conf.IsIncognito
   334  
   335  	if err = r.Print(report, os.Stdout); err != nil {
   336  		log.Fatal().Err(err).Msg("failed to print")
   337  	}
   338  }