From b419bbc26a0396b263a8a75f47546a25b5c8eca6 Mon Sep 17 00:00:00 2001 From: Mikita Iwanowski Date: Mon, 8 Jul 2024 10:03:46 +0200 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=20Framework=20cmd=20(#1355)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: list frameworks base * feat: set framework state commands * feat: handle download framework * feat: handle framework upload and better errors handling * feat: handle list from local bundle * fix: flags -> env handling * copyright * feat: retrive active only and handle --all flag * tidy * fix test: update test data - help message * Update apps/cnspec/cmd/framework.go Co-authored-by: Tim Smith * Update test/cli/testdata/cnspec.ct Co-authored-by: Tim Smith * format list error message --------- Co-authored-by: Tim Smith --- apps/cnspec/cmd/framework.go | 300 +++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +- test/cli/testdata/cnspec.ct | 1 + upstream/framework.gql.go | 99 ++++++++++++ 5 files changed, 403 insertions(+), 3 deletions(-) create mode 100644 apps/cnspec/cmd/framework.go create mode 100644 upstream/framework.gql.go diff --git a/apps/cnspec/cmd/framework.go b/apps/cnspec/cmd/framework.go new file mode 100644 index 00000000..23af2b1b --- /dev/null +++ b/apps/cnspec/cmd/framework.go @@ -0,0 +1,300 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cmd + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/muesli/termenv" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "go.mondoo.com/cnquery/v11/cli/config" + "go.mondoo.com/cnquery/v11/cli/theme" + "go.mondoo.com/cnspec/v11/policy" + cnspec_upstream "go.mondoo.com/cnspec/v11/upstream" + mondoogql "go.mondoo.com/mondoo-go" + "k8s.io/utils/ptr" +) + +const ( + FrameworkMrnPrefix = "//policy.api.mondoo.app/frameworks" +) + +func init() { + rootCmd.AddCommand(frameworkCmd) + + // list + frameworkListCmd.Flags().StringP("file", "f", "", "a local bundle file") + frameworkListCmd.Flags().BoolP("all", "a", false, "list all frameworks, not only the active ones (applicable only for upstream)") + frameworkCmd.AddCommand(frameworkListCmd) + + // preview + frameworkCmd.AddCommand(frameworkPreviewCmd) + // active + frameworkCmd.AddCommand(frameworkActiveCmd) + // download + frameworkDownloadCmd.Flags().StringP("file", "f", "", "output file") + frameworkCmd.AddCommand(frameworkDownloadCmd) + // upload + frameworkUploadCmd.Flags().StringP("file", "f", "", "input file") + frameworkCmd.AddCommand(frameworkUploadCmd) +} + +var frameworkCmd = &cobra.Command{ + Use: "framework", + Short: "Manage local and Mondoo Platform hosted compliance frameworks", +} + +var frameworkListCmd = &cobra.Command{ + Use: "list", + Short: "List available compliance frameworks", + Args: cobra.MaximumNArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlag("file", cmd.Flags().Lookup("file")); err != nil { + return err + } + if err := viper.BindPFlag("all", cmd.Flags().Lookup("all")); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + bundleFile := viper.GetString("file") + var frameworks []*cnspec_upstream.UpstreamFramework + + if bundleFile != "" { + policyBundle, err := policy.DefaultBundleLoader().BundleFromPaths(bundleFile) + if err != nil { + return err + } + for _, f := range policyBundle.Frameworks { + frameworks = append(frameworks, &cnspec_upstream.UpstreamFramework{Framework: *f}) + } + } else { + opts, err := config.Read() + if err != nil { + return err + } + config.DisplayUsedConfig() + + mondooClient, err := getGqlClient(opts) + if err != nil { + return err + } + + state := ptr.To(mondoogql.ComplianceFrameworkStateActive) + if viper.GetBool("all") { + state = nil + } + + frameworks, err = cnspec_upstream.ListFrameworks(context.Background(), mondooClient, opts.GetParentMrn(), state) + if err != nil { + log.Error().Msgf("failed to list compliance frameworks: %s", err) + os.Exit(1) + return err + } + } + + for _, framework := range frameworks { + extraInfo := []string{} + if framework.State == mondoogql.ComplianceFrameworkStateActive { + extraInfo = append(extraInfo, theme.DefaultTheme.Success("active")) + } else if framework.State == mondoogql.ComplianceFrameworkState("") { + extraInfo = append(extraInfo, theme.DefaultTheme.Disabled("local")) + } + + extraInfoStr := "" + if len(extraInfo) > 0 { + extraInfoStr = " (" + strings.Join(extraInfo, ", ") + ")" + } + fmt.Println(framework.Name + " " + framework.Version + extraInfoStr) + id := framework.Uid + if framework.Mrn != "" { + id = framework.Mrn + } + fmt.Println(termenv.String(" " + id).Foreground(theme.DefaultTheme.Colors.Disabled)) + } + return nil + }, +} + +var frameworkDownloadCmd = &cobra.Command{ + Use: "download [mrn]", + Short: "Download a compliance framework", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlag("file", cmd.Flags().Lookup("file")); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + outputFile := viper.GetString("file") + if outputFile == "" { + log.Error().Msgf("output file is required") + os.Exit(1) + } + + opts, err := config.Read() + if err != nil { + log.Error().Msgf("failed to get config: %s", err) + os.Exit(1) + } + config.DisplayUsedConfig() + + mondooClient, err := getGqlClient(opts) + if err != nil { + return err + } + + frameworkMrn := args[0] + if !strings.HasPrefix(frameworkMrn, PolicyMrnPrefix) { + frameworkMrn = FrameworkMrnPrefix + "/" + frameworkMrn + } + + data, err := cnspec_upstream.DownloadFramework(context.Background(), mondooClient, frameworkMrn, opts.GetParentMrn()) + if err != nil { + log.Error().Msgf("failed to download compliance framework: %s", err) + os.Exit(1) + } + + if err := os.WriteFile(outputFile, []byte(data), 0o644); err != nil { + log.Error().Msgf("failed to store framework: %s", err) + os.Exit(1) + } + log.Info().Msg(theme.DefaultTheme.Success("successfully downloaded to ", outputFile)) + + return nil + }, +} + +var frameworkUploadCmd = &cobra.Command{ + Use: "upload [file]", + Short: "Upload a compliance framework", + Args: cobra.ExactArgs(0), + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := viper.BindPFlag("file", cmd.Flags().Lookup("file")); err != nil { + return err + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + inputFile := viper.GetString("file") + if inputFile == "" { + log.Error().Msgf("output file is required") + os.Exit(1) + } + + opts, err := config.Read() + if err != nil { + log.Error().Msgf("failed to get config: %s", err) + os.Exit(1) + } + config.DisplayUsedConfig() + + mondooClient, err := getGqlClient(opts) + if err != nil { + return err + } + + data, err := os.ReadFile(inputFile) + if err != nil { + log.Error().Msgf("failed to read file: %s", err) + os.Exit(1) + } + + ok, err := cnspec_upstream.UploadFramework(context.Background(), mondooClient, data, opts.GetParentMrn()) + if err != nil { + log.Error().Msgf("failed to upload compliance framework: %s", err) + os.Exit(1) + } + if !ok { + log.Error().Msgf("failed to upload compliance framework") + os.Exit(1) + } + log.Info().Msg(theme.DefaultTheme.Success("successfully uploaded compliance framework")) + return nil + }, +} + +var frameworkPreviewCmd = &cobra.Command{ + Use: "preview [mrn]", + Short: "Change a framework status to preview", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts, err := config.Read() + if err != nil { + return err + } + config.DisplayUsedConfig() + + mondooClient, err := getGqlClient(opts) + if err != nil { + return err + } + + frameworkMrn := args[0] + if !strings.HasPrefix(frameworkMrn, PolicyMrnPrefix) { + frameworkMrn = FrameworkMrnPrefix + "/" + frameworkMrn + } + ok, err := cnspec_upstream.MutateFrameworkState( + context.Background(), mondooClient, frameworkMrn, + opts.GetParentMrn(), mondoogql.ComplianceFrameworkMutationActionPreview, + ) + if err != nil { + log.Error().Msgf("failed to set compliance framework to preview state in space: %s", err) + os.Exit(1) + } + if !ok { + log.Error().Msgf("failed to set compliance framework to preview state in space") + os.Exit(1) + } + log.Info().Msg(theme.DefaultTheme.Success("successfully set compliance framework to preview state in space")) + + return nil + }, +} + +var frameworkActiveCmd = &cobra.Command{ + Use: "active [mrn]", + Short: "Change a framework status to active", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts, err := config.Read() + if err != nil { + return err + } + config.DisplayUsedConfig() + + mondooClient, err := getGqlClient(opts) + if err != nil { + return err + } + + frameworkMrn := args[0] + if !strings.HasPrefix(frameworkMrn, PolicyMrnPrefix) { + frameworkMrn = FrameworkMrnPrefix + "/" + frameworkMrn + } + + ok, err := cnspec_upstream.MutateFrameworkState( + context.Background(), mondooClient, frameworkMrn, + opts.GetParentMrn(), mondoogql.ComplianceFrameworkMutationActionPreview, + ) + if err != nil { + log.Error().Msgf("failed to set compliance framework to active state in space: %s", err) + os.Exit(1) + } + if !ok { + log.Error().Msgf("failed to set compliance framework to preview state in space") + os.Exit(1) + } + log.Info().Msg(theme.DefaultTheme.Success("successfully set compliance framework to active state in space")) + + return nil + }, +} diff --git a/go.mod b/go.mod index 3e240847..43a4029b 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 go.mondoo.com/cnquery/v11 v11.11.1-0.20240703150909-053a52bebd81 - go.mondoo.com/mondoo-go v0.0.0-20240611114249-2c3b9b20e67a + go.mondoo.com/mondoo-go v0.0.0-20240704105318-097765f8523d go.mondoo.com/ranger-rpc v0.6.1 go.opentelemetry.io/otel v1.28.0 gocloud.dev v0.37.0 diff --git a/go.sum b/go.sum index 1ccc9481..1480f5aa 100644 --- a/go.sum +++ b/go.sum @@ -1233,8 +1233,8 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3 go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= go.mondoo.com/cnquery/v11 v11.11.1-0.20240703150909-053a52bebd81 h1:ksvbrg63acRH7UOcZEf+8x+PSYGpAQO3ptDdxzJE3CA= go.mondoo.com/cnquery/v11 v11.11.1-0.20240703150909-053a52bebd81/go.mod h1:fwsl8ivZwHW/GDEevxir1cQF864/gJ0rmjVtAigQuS4= -go.mondoo.com/mondoo-go v0.0.0-20240611114249-2c3b9b20e67a h1:+EQW5uXRyUyeiyZnTy2Jc371PTynJm5OruUWt3SqiT4= -go.mondoo.com/mondoo-go v0.0.0-20240611114249-2c3b9b20e67a/go.mod h1:4032UBD0ph9LyhXq5OQmmxkJv37HdAGi34YLWbhnMDA= +go.mondoo.com/mondoo-go v0.0.0-20240704105318-097765f8523d h1:Jr55zA89Yf70egaA1wZXUUJGnUc+O5HkTGBBKjU9poI= +go.mondoo.com/mondoo-go v0.0.0-20240704105318-097765f8523d/go.mod h1:4032UBD0ph9LyhXq5OQmmxkJv37HdAGi34YLWbhnMDA= go.mondoo.com/ranger-rpc v0.6.1 h1:aOMsKD7zwQBGmt998fdAkk/G+XWk5+sjsi/XPVUSCJw= go.mondoo.com/ranger-rpc v0.6.1/go.mod h1:sbv789sxgfu1vpJzmD7j4/FgjFB41GDWsM0d6fNsu68= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/test/cli/testdata/cnspec.ct b/test/cli/testdata/cnspec.ct index d759237d..291f89a0 100644 --- a/test/cli/testdata/cnspec.ct +++ b/test/cli/testdata/cnspec.ct @@ -12,6 +12,7 @@ Usage: Available Commands: completion Generate the autocompletion script for the specified shell + framework Manage local and Mondoo Platform hosted compliance frameworks help Help about any command login Register with Mondoo Platform logout Log out from Mondoo Platform diff --git a/upstream/framework.gql.go b/upstream/framework.gql.go new file mode 100644 index 00000000..f5b1ac6d --- /dev/null +++ b/upstream/framework.gql.go @@ -0,0 +1,99 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package upstream + +import ( + "context" + "encoding/base64" + "fmt" + + "go.mondoo.com/cnspec/v11/policy" + mondoogql "go.mondoo.com/mondoo-go" + + "go.mondoo.com/cnquery/v11/providers-sdk/v1/upstream/gql" +) + +type UpstreamFramework struct { + policy.Framework + + State mondoogql.ComplianceFrameworkState +} + +func ListFrameworks(ctx context.Context, c *gql.MondooClient, scopeMrn string, state *mondoogql.ComplianceFrameworkState) ([]*UpstreamFramework, error) { + var q struct { + Frameworks []struct { + Mrn string + Name string + Version string + State mondoogql.ComplianceFrameworkState + } `graphql:"complianceFrameworks(input: $input)"` + } + err := c.Query(ctx, &q, map[string]any{ + "input": mondoogql.ComplianceFrameworksInput{ + ScopeMrn: mondoogql.String(scopeMrn), + State: state, + }, + }) + if err != nil { + return nil, err + } + + frameworks := make([]*UpstreamFramework, len(q.Frameworks)) + for i, f := range q.Frameworks { + frameworks[i] = &UpstreamFramework{ + Framework: policy.Framework{ + Mrn: f.Mrn, + Name: f.Name, + Version: f.Version, + }, + State: f.State, + } + } + + return frameworks, nil +} + +func MutateFrameworkState(ctx context.Context, c *gql.MondooClient, mrn, scopeMrn string, action mondoogql.ComplianceFrameworkMutationAction) (bool, error) { + var q struct { + Mutation bool `graphql:"applyFrameworkMutation(input: $input)"` + } + err := c.Mutate(ctx, &q, mondoogql.ComplianceFrameworkMutationInput{ + FrameworkMrn: mondoogql.String(mrn), + ScopeMrn: mondoogql.String(scopeMrn), + Action: action, + }, nil) + return q.Mutation, err +} + +func DownloadFramework(ctx context.Context, c *gql.MondooClient, mrn, scopeMrn string) (string, error) { + var q struct { + Download struct { + Yaml string + } `graphql:"downloadFramework(input: $input)"` + } + err := c.Query(ctx, &q, map[string]any{ + "input": mondoogql.DownloadFrameworkInput{ + Mrn: mondoogql.String(mrn), + ScopeMrn: mondoogql.String(scopeMrn), + }, + }) + if err != nil { + return "", err + } + + return q.Download.Yaml, nil +} + +func UploadFramework(ctx context.Context, c *gql.MondooClient, yaml []byte, spaceMrn string) (bool, error) { + var q struct { + Result bool `graphql:"uploadFramework(input: $input)"` + } + + data := base64.StdEncoding.EncodeToString(yaml) + err := c.Mutate(ctx, &q, mondoogql.UploadFrameworkInput{ + SpaceMrn: mondoogql.String(spaceMrn), + Dataurl: mondoogql.String(fmt.Sprintf("data:application/octet-stream;base64,%s", data)), + }, nil) + return q.Result, err +}