diff --git a/cmd/main.go b/cmd/main.go index 9f9be2fcd..0e1bf5318 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -91,6 +91,7 @@ func main() { accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) byorWrapper := wrappers.NewByorHTTPWrapper(byorPath) containerResolverWrapper := wrappers.NewContainerResolverWrapper() + enginesWrapper := wrappers.NewHTTPEnginesWrapper() astCli := commands.NewAstCLI( applicationsWrapper, @@ -127,6 +128,7 @@ func main() { accessManagementWrapper, byorWrapper, containerResolverWrapper, + enginesWrapper, ) exitListener() err = astCli.Execute() diff --git a/internal/commands/engines.go b/internal/commands/engines.go new file mode 100644 index 000000000..b39e1688e --- /dev/null +++ b/internal/commands/engines.go @@ -0,0 +1,184 @@ +package commands + +import ( + "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/commands/util/printer" + "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/services" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +func NewEnginesCommand( + enginesWrapper wrappers.EnginesWrapper, +) *cobra.Command { + enginesCmd := &cobra.Command{ + Use: "engines", + Short: "Fetch supported API of scanner engines", + Long: "The engines command enables the ability to fetch engines APIs list in Checkmarx One.", + Annotations: map[string]string{ + "command:doc": heredoc.Doc( + ` + https://checkmarx.com/resource/documents/en/34965-68643-scan.html + `, + ), + }, + } + listEngineAPIcmd := enginesListAPISubCommand(enginesWrapper) + enginesCmd.AddCommand(listEngineAPIcmd) + return enginesCmd +} + +func enginesListAPISubCommand( + enginesWrapper wrappers.EnginesWrapper, +) *cobra.Command { + enginesListAPIcmd := &cobra.Command{ + Use: "list-api", + Short: "fetch the API list of scanner engines", + Long: "The create list-api fetch the API list of scanner engines in Checkmarx One.", + Example: heredoc.Doc( + ` + $ cx engines list-api --engine-name + `, + ), + Annotations: map[string]string{ + "command:doc": heredoc.Doc( + ` + https://checkmarx.com/resource/documents/en/34965-68643-scan.html#UUID-a0bb20d5-5182-3fb4-3da0-0e263344ffe7 + `, + ), + }, + RunE: runEnginesListAPICommand(enginesWrapper), + } + enginesListAPIcmd.PersistentFlags().String("engine-name", "", "The name of the Checkmarx scanner engine to use.") + + addOutputFormatFlag( + enginesListAPIcmd, + printer.FormatTable, + printer.FormatJSON, + printer.FormatYAML, + ) + return enginesListAPIcmd +} + +func runEnginesListAPICommand(enginesWrapper wrappers.EnginesWrapper) func(cmd *cobra.Command, args []string) error { + //fmt.Println("Inside the command execution runEnginesListAPICommand function") + return func(cmd *cobra.Command, args []string) error { + var apiModels []wrappers.ApiModel + var errorModel *wrappers.ErrorModel + //fmt.Println("Before flag") + engineName, err := cmd.Flags().GetString("engine-name") + if err != nil { + return errors.Wrapf(err, "%s", "Invalid 'engine-name' flag") + } + apiModels, errorModel, err = enginesWrapper.GetAllAPIs(engineName) + if err != nil { + return errors.Wrapf(err, "%s\n", "Failed to fetch all engines APIs") + } + + //fmt.Println(apiModels) + // Checking the response + if errorModel != nil { + return errors.Errorf(services.ErrorCodeFormat, "Failed to Getting All apis in error model", errorModel.Code, errorModel.Message) + } else if apiModels != nil && len(apiModels) > 0 { + f1, _ := cmd.Flags().GetString(params.OutputFormatFlag) + if f1 == "table" { + views := toAPIsViews(apiModels) + if err != nil { + return err + } + err = printByOutputFormat(cmd, views) + if err != nil { + return err + } + } else { + views := toEnginesView(apiModels) + if err != nil { + return err + } + err = printByOutputFormat(cmd, views) + if err != nil { + return err + } + } + } + return nil + } + +} + +type Engine struct { + EngineID string `json:"engine_id"` + EngineName string `json:"engine_name"` + APIs []API `json:"apis"` +} + +type API struct { + ApiUrl string `json:"api_url"` + ApiName string `json:"api_name"` + Description string `json:"description"` +} + +type EnginesView struct { + Engines []Engine `json:"engines"` +} + +func toEnginesView(models []wrappers.ApiModel) EnginesView { + engineMap := make(map[string]Engine) + + // Group APIs by engine + for _, model := range models { + api := API{ + ApiUrl: model.ApiUrl, + ApiName: model.ApiName, + Description: model.Description, + } + + engine, exists := engineMap[model.EngineId] + if !exists { + engine = Engine{ + EngineID: model.EngineId, + EngineName: model.EngineName, + APIs: []API{}, + } + } + engine.APIs = append(engine.APIs, api) + engineMap[model.EngineId] = engine + } + + // Collect all engines + var engines []Engine + for _, engine := range engineMap { + engines = append(engines, engine) + } + + return EnginesView{ + Engines: engines, + } +} + +func toAPIsViews(models []wrappers.ApiModel) []apiView { + result := make([]apiView, len(models)) + for i := 0; i < len(models); i++ { + result[i] = toAPIView(models[i]) + } + return result +} +func toAPIView(model wrappers.ApiModel) apiView { + return apiView{ + ApiName: model.ApiName, + Description: model.Description, + ApiUrl: model.ApiUrl, + EngineName: model.EngineName, + EngineId: model.EngineId, + } +} + +type apiView struct { + ApiName string + Description string + ApiUrl string + EngineName string + EngineId string +} diff --git a/internal/commands/engines_test.go b/internal/commands/engines_test.go new file mode 100644 index 000000000..43001f978 --- /dev/null +++ b/internal/commands/engines_test.go @@ -0,0 +1,98 @@ +package commands + +import ( + "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "gotest.tools/assert" + "testing" +) + +func TestNewEnginesCommand(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "") + assert.NilError(t, err) +} + +func TestNewEnginesCommandHelp(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "help") + assert.NilError(t, err) +} + +func TestNewEnginesSubCommand(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api") + assert.NilError(t, err) +} + +func TestNewEnginesSubCommandHelp(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--help") + assert.NilError(t, err) +} + +func TestSubCommandEngineType1(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--engine-name", "SAST") + assert.NilError(t, err) +} + +func TestSubCommandEngineType2(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--engine-name", "SCA") + assert.NilError(t, err) +} + +func TestSubCommandEngineType3(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--engine-name", "Iac") + assert.NilError(t, err) +} + +func TestSubCommandOutPutFormat1(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--output-format", "json") + assert.NilError(t, err) +} + +func TestSubCommandOutPutFormat2(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--output-format", "yaml") + assert.NilError(t, err) +} + +func TestSubCommandOutPutFormat3(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--output-format", "table") + assert.NilError(t, err) +} + +func TestSubCommandEngineError1(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--chibute", "SAST") + assert.Assert(t, err != nil) +} + +func TestSubCommandEngineError2(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--engine-name", "SASTS") + assert.NilError(t, err) +} + +func TestSubCommandEngineError3(t *testing.T) { + + cmd := NewEnginesCommand(&mock.NewHTTPEnginesMockWrapper{}) + err := executeTestCommand(cmd, "list-api", "--output-format", "jsonsa") + assert.Assert(t, err != nil) +} diff --git a/internal/commands/root.go b/internal/commands/root.go index cf5bc0462..3a8a193a2 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -19,7 +19,6 @@ import ( "github.com/spf13/viper" ) -// NewAstCLI Return a Checkmarx One CLI root command to execute func NewAstCLI( applicationsWrapper wrappers.ApplicationsWrapper, scansWrapper wrappers.ScansWrapper, @@ -55,8 +54,9 @@ func NewAstCLI( accessManagementWrapper wrappers.AccessManagementWrapper, byorWrapper wrappers.ByorWrapper, containerResolverWrapper wrappers.ContainerResolverWrapper, + enginesWrapper wrappers.EnginesWrapper, ) *cobra.Command { - // Create the root + rootCmd := &cobra.Command{ Use: "cx [flags]", Short: "Checkmarx One CLI", @@ -77,7 +77,6 @@ func NewAstCLI( }, } - // Load default flags rootCmd.PersistentFlags().Bool(params.DebugFlag, false, params.DebugUsage) rootCmd.PersistentFlags().String(params.AccessKeyIDFlag, "", params.AccessKeyIDFlagUsage) rootCmd.PersistentFlags().String(params.AccessKeySecretFlag, "", params.AccessKeySecretFlagUsage) @@ -99,8 +98,6 @@ func NewAstCLI( _ = rootCmd.PersistentFlags().MarkHidden(params.ApikeyOverrideFlag) - // This monitors and traps situations where "extra/garbage" commands - // are passed to Cobra. rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { PrintConfiguration() // Need to check the __complete command to allow correct behavior of the autocomplete @@ -109,7 +106,7 @@ func NewAstCLI( os.Exit(0) } } - // Link the environment variable to the CLI argument(s). + _ = viper.BindPFlag(params.AccessKeyIDConfigKey, rootCmd.PersistentFlags().Lookup(params.AccessKeyIDFlag)) _ = viper.BindPFlag(params.AccessKeySecretConfigKey, rootCmd.PersistentFlags().Lookup(params.AccessKeySecretFlag)) _ = viper.BindPFlag(params.BaseURIKey, rootCmd.PersistentFlags().Lookup(params.BaseURIFlag)) @@ -203,6 +200,8 @@ func NewAstCLI( chatCmd := NewChatCommand(chatWrapper, tenantWrapper) hooksCmd := NewHooksCommand(jwtWrapper) + enginesCmd := NewEnginesCommand(enginesWrapper) + rootCmd.AddCommand( scanCmd, projectCmd, @@ -214,6 +213,7 @@ func NewAstCLI( configCmd, chatCmd, hooksCmd, + enginesCmd, ) rootCmd.SilenceUsage = true @@ -257,7 +257,7 @@ func getFilters(cmd *cobra.Command) (map[string]string, error) { } func validateExtraFilters(filterKeyVal []string) []string { - // Add support for state = exclude-not-exploitable, will replace all values of filter flag state to "TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" + if extraFilter[filterKeyVal[0]] != nil { for privateFilter, value := range extraFilter[filterKeyVal[0]] { if strings.Contains(filterKeyVal[1], privateFilter) { @@ -296,6 +296,13 @@ func addResultFormatFlag(cmd *cobra.Command, defaultFormat string, otherAvailabl ) } +func addOutputFormatFlag(cmd *cobra.Command, defaultFormat string, otherAvailableFormats ...string) { + cmd.PersistentFlags().String( + params.OutputFormatFlag, defaultFormat, + fmt.Sprintf(params.FormatFlagUsageFormat, append(otherAvailableFormats, defaultFormat)), + ) +} + func markFlagAsRequired(cmd *cobra.Command, flag string) { err := cmd.MarkPersistentFlagRequired(flag) if err != nil { @@ -319,6 +326,12 @@ func printByFormat(cmd *cobra.Command, view interface{}) error { f, _ := cmd.Flags().GetString(params.FormatFlag) return printer.Print(cmd.OutOrStdout(), view, f) } + +func printByOutputFormat(cmd *cobra.Command, view interface{}) error { + f, _ := cmd.Flags().GetString(params.OutputFormatFlag) + return printer.Print(cmd.OutOrStdout(), view, f) +} + func printByScanInfoFormat(cmd *cobra.Command, view interface{}) error { f, _ := cmd.Flags().GetString(params.ScanInfoFormatFlag) return printer.Print(cmd.OutOrStdout(), view, f) diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index 7006b0e06..4dbb45b88 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -68,6 +68,7 @@ func createASTTestCommand() *cobra.Command { byorWrapper := &mock.ByorMockWrapper{} containerResolverMockWrapper := &mock.ContainerResolverMockWrapper{} customStatesMockWrapper := &mock.CustomStatesMockWrapper{} + newHTTPEnginesMockWrapper := &mock.NewHTTPEnginesMockWrapper{} return NewAstCLI( applicationWrapper, scansMockWrapper, @@ -103,6 +104,7 @@ func createASTTestCommand() *cobra.Command { accessManagementWrapper, byorWrapper, containerResolverMockWrapper, + newHTTPEnginesMockWrapper, ) } diff --git a/internal/commands/util/printer/printer.go b/internal/commands/util/printer/printer.go index 0ead320b4..2a877714c 100644 --- a/internal/commands/util/printer/printer.go +++ b/internal/commands/util/printer/printer.go @@ -3,6 +3,7 @@ package printer import ( "encoding/json" "fmt" + "gopkg.in/yaml.v3" "io" "reflect" "strconv" @@ -30,6 +31,7 @@ const ( FormatXML = "xml" FormatGLSast = "gl-sast" FormatGLSca = "gl-sca" + FormatYAML = "yaml" ) func Print(w io.Writer, view interface{}, format string) error { @@ -51,6 +53,12 @@ func Print(w io.Writer, view interface{}, format string) error { } else if IsFormat(format, FormatTable) { entities := toEntities(view) printTable(w, entities) + } else if IsFormat(format, FormatYAML) { + viewYaml, err := yaml.Marshal(view) + if err != nil { + return err + } + _, _ = fmt.Fprintln(w, string(viewYaml)) } else { return errors.Errorf("Invalid format %s", format) } diff --git a/internal/params/flags.go b/internal/params/flags.go index a6b6a6c9e..72afc9152 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -296,3 +296,6 @@ const NotExploitable = "NOT_EXPLOITABLE" const ProposedNotExploitable = "PROPOSED_NOT_EXPLOITABLE" const CONFIRMED = "CONFIRMED" const URGENT = "URGENT" + +// Output format flag +const OutputFormatFlag = "output-format" diff --git a/internal/wrappers/engines-http.go b/internal/wrappers/engines-http.go new file mode 100644 index 000000000..04f9b5741 --- /dev/null +++ b/internal/wrappers/engines-http.go @@ -0,0 +1,21 @@ +package wrappers + +type EnginesHTTPWrapper struct{} + +func NewHTTPEnginesWrapper() EnginesWrapper { return &EnginesHTTPWrapper{} } +func (e *EnginesHTTPWrapper) GetAllAPIs(engineName string) ([]ApiModel, *ErrorModel, error) { + var filteredApiModels []ApiModel = FilterByEngineType(ApiModels, engineName) + return filteredApiModels, nil, nil +} +func FilterByEngineType(apiModel []ApiModel, engineName string) []ApiModel { + var filtered []ApiModel + if engineName == "" { + return apiModel + } + for _, apiM := range apiModel { + if apiM.EngineName == engineName { + filtered = append(filtered, apiM) + } + } + return filtered +} diff --git a/internal/wrappers/engines.go b/internal/wrappers/engines.go new file mode 100644 index 000000000..ee79a013a --- /dev/null +++ b/internal/wrappers/engines.go @@ -0,0 +1,30 @@ +package wrappers + +type ApiModel struct { + ApiName string `json:"api_name"` + Description string `json:"description"` + ApiUrl string `json:"api_url"` + EngineName string `json:"engine_name"` + EngineId string `json:"engine_id"` +} + +var ApiModels = []ApiModel{ + {"GetAllSASTProject", + " Gets all SAST projects", + "https://cx_sast/projects", + "SAST", + "eSAST01"}, + {"GetAllSCAProject", + "Gets all SCA projects", + "https://cx_sca/projects", + "SCA", + "eSCA02"}, + {"Get all Iac Project", + "This API gets all IaC projects", + "https://cx_iac/projects", + "Iac", + "eIAC03"}} + +type EnginesWrapper interface { + GetAllAPIs(engineName string) ([]ApiModel, *ErrorModel, error) +} diff --git a/internal/wrappers/mock/engine-mock.go b/internal/wrappers/mock/engine-mock.go new file mode 100644 index 000000000..76b07b381 --- /dev/null +++ b/internal/wrappers/mock/engine-mock.go @@ -0,0 +1,10 @@ +package mock + +import "github.com/checkmarx/ast-cli/internal/wrappers" + +type NewHTTPEnginesMockWrapper struct{} + +func (n *NewHTTPEnginesMockWrapper) GetAllAPIs(engineType string) ([]wrappers.ApiModel, *wrappers.ErrorModel, error) { + var filteredApiModels []wrappers.ApiModel = wrappers.FilterByEngineType(wrappers.ApiModels, engineType) + return filteredApiModels, nil, nil +} diff --git a/test/integration/engines_test.go b/test/integration/engines_test.go new file mode 100644 index 000000000..9aa0e6948 --- /dev/null +++ b/test/integration/engines_test.go @@ -0,0 +1,125 @@ +//go:build integration + +package integration + +import ( + "gotest.tools/assert" + "testing" +) + +func TestEnginesApiList_WithoutFlagSuccess(t *testing.T) { + args := []string{ + "engines", "api-list", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_HelpSuccess(t *testing.T) { + args := []string{ + "engines", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeSuccess1(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeSuccess2(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "SAST", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeSuccess3(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "SCA", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeSuccess4(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "Iac", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeError1(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "xyz", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_EngineTypeError2(t *testing.T) { + args := []string{ + "engines", "list-api", "--engine-name", "", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_OutputFormatSuccess1(t *testing.T) { + args := []string{ + "engines", "list-api", "--output-format", "table", "--engine-name", "Iac", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_OutputFormatSuccess2(t *testing.T) { + args := []string{ + "engines", "list-api", "--output-format", "json", "--engine-name", "Iac", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_OutputFormatSuccess3(t *testing.T) { + args := []string{ + "engines", "list-api", "--output-format", "yaml", "--engine-name", "Iac", + } + + err, _ := executeCommand(t, args...) + assert.NilError(t, err) +} + +func TestEnginesApiList_OutputFormatError1(t *testing.T) { + args := []string{ + "engines", "list-api", "--output-format", "xyz", "--engine-name", "Iac", + } + + err, _ := executeCommand(t, args...) + assert.Equal(t, err.Error(), "Invalid format xyz") +} + +func TestEnginesApiList_FlagError1(t *testing.T) { + args := []string{ + "engines", "list-api", "--engines-name", + } + + err, _ := executeCommand(t, args...) + assert.Equal(t, err.Error(), "unknown flag: --engines-name") +} diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 4370c053c..2167faf91 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -123,6 +123,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) ByorWrapper := wrappers.NewByorHTTPWrapper(byorPath) containerResolverWrapper := wrappers.NewContainerResolverWrapper() + enginesWrapper := wrappers.NewHTTPEnginesWrapper() astCli := commands.NewAstCLI( applicationsWrapper, @@ -159,6 +160,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { accessManagementWrapper, ByorWrapper, containerResolverWrapper, + enginesWrapper, ) return astCli }