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

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package cmd
     5  
     6  import (
     7  	"context"
     8  	_ "embed"
     9  	"fmt"
    10  	"os"
    11  	"strconv"
    12  
    13  	"github.com/rs/zerolog/log"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/viper"
    16  	"go.mondoo.com/cnquery/cli/config"
    17  	"go.mondoo.com/cnquery/explorer"
    18  	"go.mondoo.com/cnquery/providers"
    19  	"go.mondoo.com/cnquery/providers-sdk/v1/upstream"
    20  	"go.mondoo.com/cnquery/utils/stringx"
    21  )
    22  
    23  func init() {
    24  	// bundle init
    25  	packBundlesCmd.AddCommand(queryPackInitCmd)
    26  
    27  	// bundle lint
    28  	packBundlesCmd.AddCommand(queryPackLintCmd)
    29  
    30  	// publish
    31  	queryPackPublishCmd.Flags().String("pack-version", "", "Override the version of each pack in the bundle")
    32  	packBundlesCmd.AddCommand(queryPackPublishCmd)
    33  
    34  	rootCmd.AddCommand(packBundlesCmd)
    35  }
    36  
    37  var packBundlesCmd = &cobra.Command{
    38  	Use:     "bundle",
    39  	Aliases: []string{"pack"},
    40  	Short:   "Create, upload, and validate query packs.",
    41  }
    42  
    43  //go:embed bundle_querypack-example.mql.yaml
    44  var embedQueryPackTemplate []byte
    45  
    46  var queryPackInitCmd = &cobra.Command{
    47  	Use:   "init [path]",
    48  	Short: "Create an example query pack that you can use as a starting point. If you don't provide a filename, cnquery uses `example-pack.mql.yaml`.",
    49  	Args:  cobra.MaximumNArgs(1),
    50  	Run: func(cmd *cobra.Command, args []string) {
    51  		name := "example-pack.mql.yaml"
    52  		if len(args) == 1 {
    53  			name = args[0]
    54  		}
    55  
    56  		_, err := os.Stat(name)
    57  		if err == nil {
    58  			log.Fatal().Msgf("Query Pack '%s' already exists", name)
    59  		}
    60  
    61  		err = os.WriteFile(name, embedQueryPackTemplate, 0o640)
    62  		if err != nil {
    63  			log.Fatal().Err(err).Msgf("Could not write '%s'", name)
    64  		}
    65  		log.Info().Msgf("Example query pack file written to %s", name)
    66  	},
    67  }
    68  
    69  func validate(queryPackBundle *explorer.Bundle) []string {
    70  	errors := []string{}
    71  
    72  	// check that we have uids for packs and queries
    73  	for i := range queryPackBundle.Packs {
    74  		pack := queryPackBundle.Packs[i]
    75  		packId := strconv.Itoa(i)
    76  
    77  		if pack.Uid == "" {
    78  			errors = append(errors, fmt.Sprintf("pack %s does not define a uid", packId))
    79  		} else {
    80  			packId = pack.Uid
    81  		}
    82  
    83  		if pack.Name == "" {
    84  			errors = append(errors, fmt.Sprintf("pack %s does not define a name", packId))
    85  		}
    86  
    87  		for j := range pack.Queries {
    88  			query := pack.Queries[j]
    89  			queryId := strconv.Itoa(j)
    90  			if query.Uid == "" {
    91  				errors = append(errors, fmt.Sprintf("query %s/%s does not define a uid", packId, queryId))
    92  			} else {
    93  				queryId = query.Uid
    94  			}
    95  
    96  			if query.Title == "" {
    97  				errors = append(errors, fmt.Sprintf("query %s/%s does not define a name", packId, queryId))
    98  			}
    99  		}
   100  	}
   101  
   102  	// we compile after the checks because it removes the uids and replaces it with mrns
   103  	schema := providers.DefaultRuntime().Schema()
   104  	_, err := queryPackBundle.Compile(context.Background(), schema)
   105  	if err != nil {
   106  		errors = append(errors, "could not compile the query pack bundle", err.Error())
   107  	}
   108  
   109  	return errors
   110  }
   111  
   112  var queryPackLintCmd = &cobra.Command{
   113  	Use:     "lint [path]",
   114  	Aliases: []string{"validate"},
   115  	Short:   "Apply style formatting to a query pack.",
   116  	Args:    cobra.ExactArgs(1),
   117  	Run: func(cmd *cobra.Command, args []string) {
   118  		log.Info().Str("file", args[0]).Msg("validate query pack")
   119  		queryPackBundle, err := explorer.BundleFromPaths(args[0])
   120  		if err != nil {
   121  			log.Fatal().Err(err).Msg("could not load query pack")
   122  		}
   123  
   124  		errors := validate(queryPackBundle)
   125  		if len(errors) > 0 {
   126  			log.Error().Msg("could not validate query pack")
   127  			for i := range errors {
   128  				fmt.Fprintf(os.Stderr, stringx.Indent(2, errors[i]))
   129  			}
   130  			os.Exit(1)
   131  		}
   132  
   133  		log.Info().Msg("valid query pack")
   134  	},
   135  }
   136  
   137  var queryPackPublishCmd = &cobra.Command{
   138  	Use:     "publish [path]",
   139  	Aliases: []string{"upload"},
   140  	Short:   "Add a user-owned query pack to the Mondoo Security Registry.",
   141  	Args:    cobra.ExactArgs(1),
   142  	PreRun: func(cmd *cobra.Command, args []string) {
   143  		viper.BindPFlag("pack-version", cmd.Flags().Lookup("pack-version"))
   144  	},
   145  	Run: func(cmd *cobra.Command, args []string) {
   146  		opts, optsErr := config.Read()
   147  		if optsErr != nil {
   148  			log.Fatal().Err(optsErr).Msg("could not load configuration")
   149  		}
   150  		config.DisplayUsedConfig()
   151  
   152  		filename := args[0]
   153  		log.Info().Str("file", filename).Msg("load query pack bundle")
   154  		queryPackBundle, err := explorer.BundleFromPaths(filename)
   155  		if err != nil {
   156  			log.Fatal().Err(err).Msg("could not load query pack bundle")
   157  		}
   158  
   159  		errors := validate(queryPackBundle)
   160  		if len(errors) > 0 {
   161  			log.Error().Msg("could not validate query pack")
   162  			for i := range errors {
   163  				fmt.Fprintf(os.Stderr, stringx.Indent(2, errors[i]))
   164  			}
   165  			os.Exit(1)
   166  		}
   167  		log.Info().Msg("valid query pack")
   168  
   169  		// compile manipulates the bundle, therefore we read it again
   170  		queryPackBundle, err = explorer.BundleFromPaths(filename)
   171  		if err != nil {
   172  			log.Fatal().Err(err).Msg("could not load query pack bundle")
   173  		}
   174  
   175  		log.Info().Str("space", opts.SpaceMrn).Msg("add query pack bundle to space")
   176  		overrideVersionFlag := false
   177  		overrideVersion := viper.GetString("pack-version")
   178  		if len(overrideVersion) > 0 {
   179  			overrideVersionFlag = true
   180  		}
   181  
   182  		serviceAccount := opts.GetServiceCredential()
   183  		if serviceAccount == nil {
   184  			log.Fatal().Msg("cnquery has no credentials. Log in with `cnquery login`")
   185  		}
   186  
   187  		certAuth, err := upstream.NewServiceAccountRangerPlugin(serviceAccount)
   188  		if err != nil {
   189  			log.Error().Err(err).Msg("could not initialize client authentication")
   190  			os.Exit(ConfigurationErrorCode)
   191  		}
   192  		httpClient, err := opts.GetHttpClient()
   193  		if err != nil {
   194  			log.Fatal().Err(err).Msg("error while creating Mondoo API client")
   195  		}
   196  		queryHubServices, err := explorer.NewQueryHubClient(opts.UpstreamApiEndpoint(), httpClient, certAuth)
   197  		if err != nil {
   198  			log.Fatal().Err(err).Msg("could not connect to the Mondoo Security Registry")
   199  		}
   200  
   201  		// set the owner mrn for spaces
   202  		queryPackBundle.OwnerMrn = opts.SpaceMrn
   203  		ctx := context.Background()
   204  
   205  		// override version and/or labels
   206  		for i := range queryPackBundle.Packs {
   207  			p := queryPackBundle.Packs[i]
   208  
   209  			// override query pack version
   210  			if overrideVersionFlag {
   211  				p.Version = overrideVersion
   212  			}
   213  		}
   214  
   215  		// send data upstream
   216  		_, err = queryHubServices.SetBundle(ctx, queryPackBundle)
   217  		if err != nil {
   218  			log.Fatal().Err(err).Msg("could not add query packs")
   219  		}
   220  
   221  		log.Info().Msg("successfully added query packs")
   222  	},
   223  }