From 24339cb52a8de800281c2a7c2724021e6290e53d Mon Sep 17 00:00:00 2001 From: Victoria Jeffrey Date: Mon, 2 Oct 2023 00:55:10 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=B9=20migrate=20vuln=20cmd=20from=20v8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/cnspec/cmd/root.go | 5 + apps/cnspec/cmd/vuln.go | 479 ++++++++++++---------------------------- 2 files changed, 142 insertions(+), 342 deletions(-) diff --git a/apps/cnspec/cmd/root.go b/apps/cnspec/cmd/root.go index d0d490f9..ba3ffc86 100644 --- a/apps/cnspec/cmd/root.go +++ b/apps/cnspec/cmd/root.go @@ -99,6 +99,11 @@ func Execute() { Run: scanCmdRun, Action: "Scan ", }, + &providers.Command{ + Command: vulnCmd, + Run: vulnCmdRun, + Action: "Check for vulnerabilities ", + }, ) if err != nil { log.Error().Msg(err.Error()) diff --git a/apps/cnspec/cmd/vuln.go b/apps/cnspec/cmd/vuln.go index c99111b8..4df6027d 100644 --- a/apps/cnspec/cmd/vuln.go +++ b/apps/cnspec/cmd/vuln.go @@ -4,359 +4,154 @@ package cmd import ( + "os" + + "github.com/mitchellh/mapstructure" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.mondoo.com/cnquery/cli/shell" + "go.mondoo.com/cnquery/explorer/executor" + "go.mondoo.com/cnquery/logger" + "go.mondoo.com/cnquery/providers" + "go.mondoo.com/cnquery/providers-sdk/v1/plugin" + "go.mondoo.com/cnquery/providers-sdk/v1/upstream/mvd" + "go.mondoo.com/cnspec/cli/reporter" + "go.mondoo.com/ranger-rpc/codes" + "go.mondoo.com/ranger-rpc/status" ) func init() { rootCmd.AddCommand(vulnCmd) + vulnCmd.Flags().StringP("output", "o", "compact", "Set output format: "+reporter.AllFormats()) + vulnCmd.Flags().BoolP("json", "j", false, "Run the query and return the object in a JSON structure.") + vulnCmd.Flags().String("platform-id", "", "Select a specific target asset by providing its platform ID.") + + vulnCmd.Flags().String("inventory-file", "", "Set the path to the inventory file.") + vulnCmd.Flags().Bool("inventory-ansible", false, "Set the inventory format to Ansible.") + vulnCmd.Flags().Bool("inventory-domainlist", false, "Set the inventory format to domain list.") + vulnCmd.Flags().StringToString("props", nil, "Custom values for properties") } var vulnCmd = &cobra.Command{ Use: "vuln", Short: "Scans a target for Vulnerabilities.", - Run: func(cmd *cobra.Command, args []string) { - panic("NOT YET MIGRATED") + PreRun: func(cmd *cobra.Command, args []string) { + // for all assets + viper.BindPFlag("platform-id", cmd.Flags().Lookup("platform-id")) + + viper.BindPFlag("inventory-file", cmd.Flags().Lookup("inventory-file")) + viper.BindPFlag("inventory-ansible", cmd.Flags().Lookup("inventory-ansible")) + viper.BindPFlag("inventory-domainlist", cmd.Flags().Lookup("inventory-domainlist")) }, } -// var vulnCmd = builder.NewProviderCommand(builder.CommandOpts{ -// CommonFlags: func(cmd *cobra.Command) { -// // inventories for multi-asset scan -// cmd.Flags().String("inventory-file", "", "Path to inventory file.") -// cmd.Flags().Bool("inventory-ansible", false, "Set inventory format to Ansible.") -// cmd.Flags().Bool("inventory-domainlist", false, "Set inventory format to domain list.") - -// // policies & incognito mode -// cmd.Flags().Bool("incognito", false, "Incognito mode. Do not report scan results to Mondoo Platform.") -// cmd.Flags().StringSlice("policy", nil, "List policies to execute. This requires incognito mode. To scan multiple policies, pass `--policy POLICY`") -// cmd.Flags().StringSliceP("policy-bundle", "f", nil, "Path to local policy bundle file.") -// // flag completion command -// cmd.RegisterFlagCompletionFunc("policy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { -// return getPoliciesForCompletion(), cobra.ShellCompDirectiveDefault -// }) - -// // individual asset flags -// cmd.Flags().StringP("password", "p", "", "Password, such as for SSH/WinRM.") -// cmd.Flags().Bool("ask-pass", false, "Ask for connection password.") -// cmd.Flags().StringP("identity-file", "i", "", "Select a file from which too read the identity (private key) for public key authentication.") -// cmd.Flags().String("id-detector", "", "User override for platform ID detection mechanism. Supported: "+strings.Join(providers.AvailablePlatformIdDetector(), ", ")) - -// cmd.Flags().String("path", "", "Path to a local file or directory for the connection to use") -// cmd.Flags().StringToString("option", nil, "Additional connection options. You can pass multiple options using `--option key=value`") -// cmd.Flags().String("discover", discovery_common.DiscoveryAuto, "Enable the discovery of nested assets. Supported: 'all|instances|host-instances|host-machines|container|container-images|pods|cronjobs|statefulsets|deployments|jobs|replicasets|daemonsets'") -// cmd.Flags().StringToString("discover-filter", nil, "Additional filter for asset discovery.") -// cmd.Flags().StringToString("annotation", nil, "Add an annotation to the asset.") // user-added, editable - -// // global asset flags -// cmd.Flags().Bool("insecure", false, "Disable TLS/SSL checks or SSH hostkey config.") -// cmd.Flags().Bool("sudo", false, "Elevate privileges with sudo.") -// cmd.Flags().Int("score-threshold", 0, "If any score falls below the threshold, exit 1.") -// cmd.Flags().Bool("record", false, "Record backend calls.") -// cmd.Flags().MarkHidden("record") - -// // v6 should make detect-cicd and category flag public, default for "detect-cicd" should switch to true -// cmd.Flags().Bool("detect-cicd", true, "Try to detect CI/CD environments. If successful, sets the asset category to 'cicd'.") -// cmd.Flags().String("category", "inventory", "Set the category for the assets to 'inventory|cicd'.") -// cmd.Flags().MarkHidden("category") - -// // output rendering -// cmd.Flags().StringP("output", "o", "compact", "Set output format: "+reporter.AllFormats()) -// cmd.Flags().BoolP("json", "j", false, "Set output to JSON (shorthand).") -// cmd.Flags().Bool("no-pager", false, "Disable interactive scan output pagination.") -// cmd.Flags().String("pager", "", "Enable scan output pagination with custom pagination command. The default is 'less -R'.") -// }, -// CommonPreRun: func(cmd *cobra.Command, args []string) { -// // for all assets -// viper.BindPFlag("incognito", cmd.Flags().Lookup("incognito")) -// viper.BindPFlag("insecure", cmd.Flags().Lookup("insecure")) -// viper.BindPFlag("policies", cmd.Flags().Lookup("policy")) -// viper.BindPFlag("sudo.active", cmd.Flags().Lookup("sudo")) - -// viper.BindPFlag("output", cmd.Flags().Lookup("output")) - -// viper.BindPFlag("vault.name", cmd.Flags().Lookup("vault")) -// viper.BindPFlag("platform-id", cmd.Flags().Lookup("platform-id")) -// }, -// Docs: common.CommandsDocs{ -// Entries: map[string]common.CommandDocsEntry{ -// "local": { -// Short: "Scan your local system.", -// }, -// "vagrant": { -// Short: "Scan a Vagrant host.", -// }, -// "ssh": { -// Short: "Scan a SSH target.", -// }, -// "winrm": { -// Short: "Scan a WinRM target.", -// }, -// "container": { -// Short: "Connect to a container, image, or registry.", -// Long: `Connect to a container, container image, or container registry. By default cnspec tries to auto-detect the container or image from the provided ID, even -// if it's not the full ID: - -// cnspec vuln container b62b276baab6 -// cnspec vuln container b62 -// cnspec vuln container ubuntu:latest - -// You can also explicitly connect to an image or a container registry: - -// cnspec vuln container image ubuntu:20.04 -// cnspec vuln container registry harbor.lunalectric.com/project/repository -// `, -// }, -// "container-image": { -// Short: "Connect to a container image.", -// }, -// "container-registry": { -// Short: "Connect to a container registry.", -// Long: `Connect to a container registry. Supports more parameters for different registries: - -// cnspec vuln container registry harbor.lunalectric.com/project/repository -// cnspec vuln container registry yourname.azurecr.io -// cnspec vuln container registry 123456789.dkr.ecr.us-east-1.amazonaws.com/repository -// `, -// }, -// "docker": { -// Short: "Connect to a Docker container or image.", -// Long: `Connect to a Docker container or image by automatically detecting the provided ID. -// You can also specify a subcommand to narrow the scan to containers or images. - -// cnspec vuln docker b62b276baab6 - -// cnspec vuln docker container b62b -// cnspec vuln docker image ubuntu:latest -// `, -// }, -// "docker-container": { -// Short: "Connect to a Docker container.", -// Long: `Connect to a Docker container. Can be specified as the container ID (such as b62b276baab6) -// or container name (such as elated_poincare).`, -// }, -// "docker-image": { -// Short: "Connect to a Docker image.", -// Long: `Connect to a Docker image. Can be specified as the image ID (such as b6f507652425) -// or the image name (such as ubuntu:latest).`, -// }, -// "kubernetes": { -// Short: "Connect to a Kubernetes cluster or local manifest files(s).", -// }, -// "aws": { -// Short: "Connect to an AWS account or instance.", -// Long: `Connect to an AWS account or EC2 instance. cnspec uses your local AWS configuration -// for the account scan. See the subcommands to scan EC2 instances.`, -// }, -// "aws-ec2": { -// Short: "Connect to an AWS instance using one of the available connectors.", -// }, -// "aws-ec2-connect": { -// Short: "Connect to an AWS instance using EC2 Instance Connect.", -// }, -// "aws-ec2-ebs-instance": { -// Short: "Connect to an AWS instance using an EBS volume scan. This requires an AWS host.", -// Long: `Connect to an AWS instance using an EBS volume scan. This requires that the -// scan execute on an instance that is running inside of AWS.`, -// }, -// "aws-ec2-ebs-volume": { -// Short: "Connect to a specific AWS volume using an EBS volume scan. This requires an AWS host.", -// Long: `Connect to a specific AWS volume using an EBS volume scan. This requires that the -// scan execute on an instance that is running inside of AWS.`, -// }, -// "aws-ec2-ebs-snapshot": { -// Short: "Connect to a specific AWS snapshot using an EBS volume scan. This requires an AWS host.", -// Long: `Connect to a specific AWS snapshot using an EBS volume scan. This requires that the -// scan execute on an instance that is running inside of AWS.`, -// }, -// "aws-ec2-ssm": { -// Short: "Connect to an AWS instance using the AWS Systems Manager to connect.", -// }, -// "azure": { -// Short: "Connect to a Microsoft Azure subscription or virtual machines.", -// Long: `Connect to a Microsoft Azure subscriptions or virtual machines. cnspec uses your local Azure -// configuration for the account scan. To scan your Azure compute, you must -// configure your Azure credentials and have SSH access to your virtual machines.`, -// }, -// "gcp": { -// Short: "Connect to a Google Cloud Platform (GCP) project.", -// }, -// "gcp-gcr": { -// Short: "Connect to a Google Container Registry (GCR).", -// }, -// "vsphere": { -// Short: "Connect to a VMware vSphere API endpoint.", -// }, -// "vsphere-vm": { -// Short: "Connect to a VMware vSphere VM.", -// }, -// "host": { -// Short: "Connect to a host endpoint.", -// }, -// }, -// }, -// Run: func(cmd *cobra.Command, args []string, provider providers.ProviderType, assetType builder.AssetType) { -// conf, err := getCobraScanConfig(cmd, args, provider, assetType) -// if err != nil { -// log.Fatal().Err(err).Msg("failed to prepare config") -// } - -// unauthedErrorMsg := "vulnerability scan requires authentication, login with `cnspec login --token`" -// if conf.runtime.UpstreamConfig == nil { -// log.Fatal().Msg(unauthedErrorMsg) -// } - -// ctx := discovery.InitCtx(context.Background()) - -// log.Info().Msgf("discover related assets for %d asset(s)", len(conf.Inventory.Spec.Assets)) -// im, err := inventory.New(inventory.WithInventory(conf.Inventory)) -// if err != nil { -// log.Fatal().Err(err).Msg("could not load asset information") -// } -// assetErrors := im.Resolve(ctx) -// if len(assetErrors) > 0 { -// for a := range assetErrors { -// log.Error().Err(assetErrors[a]).Str("asset", a.Name).Msg("could not connect to asset") -// } -// log.Fatal().Msg("could not resolve assets") -// } - -// assetList := im.GetAssets() -// log.Debug().Msgf("resolved %d assets", len(assetList)) - -// if len(assetList) == 0 { -// log.Fatal().Msg("could not find an asset that we can connect to") -// } - -// platformID := "" -// var connectAsset *inventory.Asset -// if len(assetList) == 1 { -// connectAsset = assetList[0] -// } else if len(assetList) > 1 && platformID != "" { -// connectAsset, err = filterAssetByPlatformID(assetList, platformID) -// if err != nil { -// log.Fatal().Err(err).Send() -// } -// } else if len(assetList) > 1 { -// isTTY := isatty.IsTerminal(os.Stdout.Fd()) -// if isTTY { -// connectAsset = components.AssetSelect(assetList) -// } else { -// fmt.Println(components.AssetList(theme.OperatingSystemTheme, assetList)) -// log.Fatal().Msg("cannot connect to more than one asset, use --platform-id to select a specific asset") -// } -// } - -// if connectAsset == nil { -// log.Fatal().Msg("no asset selected") -// } - -// backend, err := provider_resolver.OpenAssetConnection(ctx, connectAsset, im.GetCredsResolver(), false) -// if err != nil { -// log.Fatal().Err(err).Msg("could not connect to asset") -// } - -// // when we close the shell, we need to close the backend and store the recording -// onCloseHandler := func() { -// // close backend connection -// backend.Close() -// } - -// shellOptions := []shell.ShellOption{} -// shellOptions = append(shellOptions, shell.WithOnCloseListener(onCloseHandler)) -// shellOptions = append(shellOptions, shell.WithFeatures(conf.Features)) - -// if conf.UpstreamConfig != nil { -// shellOptions = append(shellOptions, shell.WithUpstreamConfig(conf.UpstreamConfig)) -// } - -// sh, err := shell.New(backend, shellOptions...) -// if err != nil { -// log.Error().Err(err).Msg("failed to initialize cnspec shell") -// } - -// vulnReportQuery := "platform.vulnerabilityReport" -// vulnReportDatapointChecksum := executor.MustGetOneDatapoint(executor.MustCompile(vulnReportQuery)) -// _, results, err := sh.RunOnce(vulnReportQuery) -// if err != nil { -// log.Error().Err(err).Msg("failed to run query") -// return -// } - -// // render vulnerability report -// var vulnReport mvd.VulnReport -// value, ok := results[vulnReportDatapointChecksum] -// if !ok { -// log.Error().Msg("could not find advisory report\n\n") -// return -// } - -// if value == nil || value.Data == nil { -// log.Error().Msg("could not load advisory report\n\n") -// return -// } - -// if value.Data.Error != nil { -// err := value.Data.Error -// if status, ok := status.FromError(err); ok { -// code := status.Code() -// switch code { -// case codes.Unauthenticated: -// log.Fatal().Msg(unauthedErrorMsg) -// default: -// log.Err(value.Data.Error).Msg("could not load advisory report") -// return -// } -// } -// } - -// rawData := value.Data.Value -// cfg := &mapstructure.DecoderConfig{ -// Metadata: nil, -// Result: &vulnReport, -// TagName: "json", -// } -// decoder, _ := mapstructure.NewDecoder(cfg) -// err = decoder.Decode(rawData) -// if err != nil { -// log.Error().Msg("could not decode advisory report\n\n") -// return -// } - -// target := connectAsset.Name -// if target == "" { -// target = connectAsset.Mrn -// } - -// printVulns(&vulnReport, conf, target) -// }, -// }) - -// func filterAssetByPlatformID(assetList []*inventory.Asset, selectionID string) (*inventory.Asset, error) { -// var foundAsset *inventory.Asset -// for i := range assetList { -// assetObj := assetList[i] -// for j := range assetObj.PlatformIds { -// if assetObj.PlatformIds[j] == selectionID { -// return assetObj, nil -// } -// } -// } - -// if foundAsset == nil { -// return nil, errors.New("could not find an asset with the provided identifier: " + selectionID) -// } -// return foundAsset, nil -// } - -// func printVulns(report *mvd.VulnReport, conf *scanConfig, target string) { -// // print the output using the specified output format -// r, err := reporter.New(conf.Output) -// if err != nil { -// log.Fatal().Msg(err.Error()) -// } +var vulnCmdRun = func(cmd *cobra.Command, runtime *providers.Runtime, cliRes *plugin.ParseCLIRes) { + conf, err := getCobraScanConfig(cmd, runtime, cliRes) + // conf := cnquery_app.ParseShellConfig(cmd, cliRes) + if err != nil { + log.Fatal().Err(err).Msg("failed to prepare config") + } + + unauthedErrorMsg := "vulnerability scan requires authentication, login with `cnspec login --token`" + if runtime.UpstreamConfig == nil { + log.Fatal().Msg(unauthedErrorMsg) + } + + res, err := runtime.Provider.Instance.Plugin.Connect(&plugin.ConnectReq{ + Features: conf.Features, + Asset: cliRes.Asset, + Upstream: runtime.UpstreamConfig, + }, nil) + if err != nil { + log.Fatal().Err(err).Msg("could not load asset information") + } + runtime.Provider.Connection = res + + // when we close the shell, we need to close the backend and store the recording + onCloseHandler := func() { + // close backend connection + runtime.Close() + } + + shellOptions := []shell.ShellOption{} + shellOptions = append(shellOptions, shell.WithOnCloseListener(onCloseHandler)) + shellOptions = append(shellOptions, shell.WithFeatures(conf.Features)) + + if conf.runtime.UpstreamConfig != nil { + shellOptions = append(shellOptions, shell.WithUpstreamConfig(conf.runtime.UpstreamConfig)) + } + + sh, err := shell.New(runtime, shellOptions...) + if err != nil { + log.Error().Err(err).Msg("failed to initialize cnspec shell") + } + + vulnReportQuery := "asset.vulnerabilityReport" + vulnReportDatapointChecksum := executor.MustGetOneDatapoint(executor.MustCompile(vulnReportQuery)) + _, results, err := sh.RunOnce(vulnReportQuery) + if err != nil { + log.Error().Err(err).Msg("failed to run query") + return + } + + // render vulnerability report + var vulnReport mvd.VulnReport + value, ok := results[vulnReportDatapointChecksum] + if !ok { + log.Error().Msg("could not find advisory report\n\n") + return + } + + if value == nil || value.Data == nil { + log.Error().Msg("could not load advisory report\n\n") + return + } + + if value.Data.Error != nil { + err := value.Data.Error + if status, ok := status.FromError(err); ok { + code := status.Code() + switch code { + case codes.Unauthenticated: + log.Fatal().Msg(unauthedErrorMsg) + default: + log.Err(value.Data.Error).Msg("could not load advisory report") + return + } + } + } + + rawData := value.Data.Value + cfg := &mapstructure.DecoderConfig{ + Metadata: nil, + Result: &vulnReport, + TagName: "json", + } + decoder, _ := mapstructure.NewDecoder(cfg) + err = decoder.Decode(rawData) + if err != nil { + log.Error().Msg("could not decode advisory report\n\n") + return + } + + target := runtime.Provider.Connection.Asset.Name + if target == "" { + target = runtime.Provider.Connection.Asset.Mrn + } + + printVulns(&vulnReport, conf, target) +} -// logger.DebugDumpJSON("vulnReport", report) -// if err = r.PrintVulns(report, os.Stdout, target); err != nil { -// log.Fatal().Err(err).Msg("failed to print") -// } -// } +func printVulns(report *mvd.VulnReport, conf *scanConfig, target string) { + // print the output using the specified output format + r, err := reporter.New("full") + if err != nil { + log.Fatal().Msg(err.Error()) + } + + logger.DebugDumpJSON("vulnReport", report) + if err = r.PrintVulns(report, os.Stdout, target); err != nil { + log.Fatal().Err(err).Msg("failed to print") + } +}