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 }