diff --git a/.golangci.yml b/.golangci.yml index 1ac6a4e41..79693ccdb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,7 +2,7 @@ linters-settings: depguard: list-type: blacklist dupl: - threshold: 200 + threshold: 500 funlen: lines: 150 statements: 50 diff --git a/cmd/main.go b/cmd/main.go index fc251fb88..c8607f78f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -47,16 +47,19 @@ func main() { scans := viper.GetString(scansPath) uploads := viper.GetString(uploadsPath) projects := viper.GetString(projectsPath) + results := viper.GetString(resultsPath) scansURL := fmt.Sprintf("%s/%s", ast, scans) uploadsURL := fmt.Sprintf("%s/%s", ast, uploads) projectsURL := fmt.Sprintf("%s/%s", ast, projects) + resultsURL := fmt.Sprintf("%s/%s", ast, results) scansWrapper := wrappers.NewHTTPScansWrapper(scansURL) uploadsWrapper := wrappers.NewUploadsHTTPWrapper(uploadsURL) projectsWrapper := wrappers.NewHTTPProjectsWrapper(projectsURL) + resultsWrapper := wrappers.NewHTTPResultsWrapper(resultsURL) - astCli := commands.NewAstCLI(scansWrapper, uploadsWrapper, projectsWrapper) + astCli := commands.NewAstCLI(scansWrapper, uploadsWrapper, projectsWrapper, resultsWrapper) err := astCli.Execute() if err != nil { fmt.Println(err.Error()) diff --git a/go.mod b/go.mod index 2043fb2d3..6df4a8338 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/checkmarxDev/ast-cli go 1.13 require ( - github.com/checkmarxDev/scans v1.2.3 + github.com/checkmarxDev/scans v1.2.7 github.com/checkmarxDev/uploads v1.0.0 github.com/pkg/errors v0.8.1 github.com/spf13/cobra v0.0.6 diff --git a/go.sum b/go.sum index 3cbab326c..067d4771e 100644 --- a/go.sum +++ b/go.sum @@ -37,10 +37,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/checkmarxDev/scans v1.2.2 h1:C0LbOpG9cdDmB3U+v8Q2rsYuzh8/+liPdwGCP4sIceo= -github.com/checkmarxDev/scans v1.2.2/go.mod h1:76lXRiVKa8XL7ST9qEQZUSnma4vUQ0GomTCSRfV4TsY= -github.com/checkmarxDev/scans v1.2.3 h1:YOc+c6EthgwTqVD7DmLRJa+b4dWOHyOzHi7mOKAeQYo= -github.com/checkmarxDev/scans v1.2.3/go.mod h1:76lXRiVKa8XL7ST9qEQZUSnma4vUQ0GomTCSRfV4TsY= +github.com/checkmarxDev/scans v1.2.7 h1:yGOZaWttkY0Ll6B2rQi70PKoRnh9VjMDPmtJjI71H/E= +github.com/checkmarxDev/scans v1.2.7/go.mod h1:hcAM6h2mAq8iC+IgqaKUPDFsKoYJEnLJFFPt/wJdnhA= github.com/checkmarxDev/uploads v1.0.0 h1:Bypl38qpKyYM/P4WH+qnMC/cMrmXL+3gDgAZ8dc+wSY= github.com/checkmarxDev/uploads v1.0.0/go.mod h1:7WMSc96f012/4O3+3rYV2vhKI9GTEBGNVkp56b8F0ok= github.com/cheggaaa/pb v1.0.28/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= diff --git a/internal/commands/payloads/uploads.json b/internal/commands/payloads/uploads.json index 0d7509615..6b7c4e02d 100644 --- a/internal/commands/payloads/uploads.json +++ b/internal/commands/payloads/uploads.json @@ -3,10 +3,9 @@ "id": "test", "type": "upload", "handler": { - "url": "MOSHIKO" + "url": "" }, - "tags": { - } + "tags": [] }, "config": [ { @@ -17,6 +16,5 @@ } } ], - "tags": { - } + "tags": [] } \ No newline at end of file diff --git a/internal/commands/project.go b/internal/commands/project.go index 86ab177aa..5e544ec97 100644 --- a/internal/commands/project.go +++ b/internal/commands/project.go @@ -5,12 +5,12 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "github.com/pkg/errors" wrappers "github.com/checkmarxDev/ast-cli/internal/wrappers" - projApi "github.com/checkmarxDev/scans/api/v1/rest/projects" - projModels "github.com/checkmarxDev/scans/pkg/projects" + projectsRESTApi "github.com/checkmarxDev/scans/api/v1/rest/projects" "github.com/spf13/cobra" ) @@ -20,6 +20,52 @@ const ( failedDeletingProj = "Failed deleting a project" ) +func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper) *cobra.Command { + projCmd := &cobra.Command{ + Use: "project", + Short: "Manage AST projects", + } + + createProjCmd := &cobra.Command{ + Use: "create", + Short: "Creates a new project", + RunE: runCreateProjectCommand(projectsWrapper), + } + createProjCmd.PersistentFlags().StringP(inputFlag, inputFlagSh, "", + "The object representing the requested project, in JSON format") + createProjCmd.PersistentFlags().StringP(inputFileFlag, inputFileFlagSh, "", + "A file holding the requested project object in JSON format. Takes precedence over --input") + + getAllProjCmd := &cobra.Command{ + Use: "get-all", + Short: "Returns all projects in the system", + RunE: runGetAllProjectsCommand(projectsWrapper), + } + getAllProjCmd.PersistentFlags().Uint64P(limitFlag, limitFlagSh, 0, limitUsage) + getAllProjCmd.PersistentFlags().Uint64P(offsetFlag, offsetFlagSh, 0, offsetUsage) + + getProjCmd := &cobra.Command{ + Use: "get", + Short: "Returns information about a project", + RunE: runGetProjectByIDCommand(projectsWrapper), + } + + deleteProjCmd := &cobra.Command{ + Use: "delete", + Short: "Delete a project", + RunE: runDeleteProjectCommand(projectsWrapper), + } + + tagsCmd := &cobra.Command{ + Use: "tags", + Short: "Get a list of all available tags", + RunE: runGetProjectsTagsCommand(projectsWrapper), + } + + projCmd.AddCommand(createProjCmd, getProjCmd, getAllProjCmd, deleteProjCmd, tagsCmd) + return projCmd +} + func runCreateProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var input []byte @@ -51,9 +97,9 @@ func runCreateProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd // No input was given return errors.Errorf("%s: no input was given\n", failedCreatingProj) } - var projModel = projApi.Project{} - var projResponseModel *projModels.ProjectResponseModel - var errorModel *projModels.ErrorModel + var projModel = projectsRESTApi.Project{} + var projResponseModel *projectsRESTApi.ProjectResponseModel + var errorModel *projectsRESTApi.ErrorModel // Try to parse to a project model in order to manipulate the request payload err = json.Unmarshal(input, &projModel) if err != nil { @@ -73,63 +119,27 @@ func runCreateProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedCreatingProj, errorModel.Code, errorModel.Message) } else if projResponseModel != nil { - fmt.Printf("Project created successfully:\n") + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(projResponseModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize project response ", failedCreatingProj) + } + cmdOut := cmd.OutOrStdout() + fmt.Fprintln(os.Stdout, "Project created successfully") + fmt.Fprintln(cmdOut, string(responseModelJSON)) } return nil } } -func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper) *cobra.Command { - projCmd := &cobra.Command{ - Use: "project", - Short: "Manage AST projects", - } - - createProjCmd := &cobra.Command{ - Use: "create", - Short: "Creates a new project", - RunE: runCreateProjectCommand(projectsWrapper), - } - createProjCmd.PersistentFlags().StringP(inputFlag, inputFlagSh, "", - "The object representing the requested project, in JSON format") - createProjCmd.PersistentFlags().StringP(inputFileFlag, inputFileFlagSh, "", - "A file holding the requested project object in JSON format. Takes precedence over --input") - - getAllProjCmd := &cobra.Command{ - Use: "get-all", - Short: "Returns all projects in the system", - RunE: runGetAllProjectsCommand(projectsWrapper), - } - - getProjCmd := &cobra.Command{ - Use: "get", - Short: "Returns information about a project", - RunE: runGetProjectByIDCommand(projectsWrapper), - } - - deleteProjCmd := &cobra.Command{ - Use: "delete", - Short: "Delete a project", - RunE: runDeleteProjectCommand(projectsWrapper), - } - - tagsCmd := &cobra.Command{ - Use: "tags", - Short: "Get a list of all available tags", - RunE: runGetProjectsTagsCommand(projectsWrapper), - } - - projCmd.AddCommand(createProjCmd, getProjCmd, getAllProjCmd, deleteProjCmd, tagsCmd) - return projCmd -} - func runGetAllProjectsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var allProjectsModel *projModels.ResponseModel - var errorModel *projModels.ErrorModel + var allProjectsModel *projectsRESTApi.SlicedProjectsResponseModel + var errorModel *projectsRESTApi.ErrorModel var err error + limit, offset := getLimitAndOffset(cmd) - allProjectsModel, errorModel, err = projectsWrapper.Get() + allProjectsModel, errorModel, err = projectsWrapper.Get(limit, offset) if err != nil { return errors.Wrapf(err, "%s\n", failedGettingAll) } @@ -137,11 +147,25 @@ func runGetAllProjectsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedGettingAll, errorModel.Code, errorModel.Message) } else if allProjectsModel != nil && allProjectsModel.Projects != nil { - for _, proj := range allProjectsModel.Projects { - fmt.Println("----------------------------") - fmt.Printf("Project ID %s:\n", proj.ID) + cmdOut := cmd.OutOrStdout() + if cmdOut != os.Stdout { + var allProjectsJSON []byte + allProjectsJSON, err = json.Marshal(allProjectsModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize project response ", failedGettingAll) + } + fmt.Fprintln(cmdOut, string(allProjectsJSON)) } - fmt.Println("----------------------------") + for _, project := range allProjectsModel.Projects { + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(project) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize project response ", failedGettingAll) + } + fmt.Fprintln(os.Stdout, "----------------------------") + fmt.Fprintln(os.Stdout, string(responseModelJSON)) + } + fmt.Fprintln(os.Stdout, "----------------------------") } return nil } @@ -149,8 +173,8 @@ func runGetAllProjectsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd func runGetProjectByIDCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var projectResponseModel *projModels.ProjectResponseModel - var errorModel *projModels.ErrorModel + var projectResponseModel *projectsRESTApi.ProjectResponseModel + var errorModel *projectsRESTApi.ErrorModel var err error if len(args) == 0 { return errors.Errorf("%s: Please provide a project ID", failedGettingProj) @@ -164,7 +188,14 @@ func runGetProjectByIDCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s", failedGettingProj, errorModel.Code, errorModel.Message) } else if projectResponseModel != nil { - fmt.Printf("Project ID %s:\n", projectResponseModel.ID) + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(projectResponseModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize scan response ", failedGetting) + } + cmdOut := cmd.OutOrStdout() + fmt.Fprintf(os.Stdout, "-----Project ID %s-----\n", projectResponseModel.ID) + fmt.Fprintln(cmdOut, string(responseModelJSON)) } return nil } @@ -172,22 +203,19 @@ func runGetProjectByIDCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd func runDeleteProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var projectResponseModel *projModels.ProjectResponseModel - var errorModel *projModels.ErrorModel + var errorModel *projectsRESTApi.ErrorModel var err error if len(args) == 0 { return errors.Errorf("%s: Please provide a project ID", failedDeletingProj) } projectID := args[0] - projectResponseModel, errorModel, err = projectsWrapper.Delete(projectID) + errorModel, err = projectsWrapper.Delete(projectID) if err != nil { return errors.Wrapf(err, "%s\n", failedDeletingProj) } // Checking the response if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedDeletingProj, errorModel.Code, errorModel.Message) - } else if projectResponseModel != nil { - fmt.Printf("Project ID %s:\n", projectResponseModel.ID) } return nil } @@ -196,7 +224,7 @@ func runDeleteProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd func runGetProjectsTagsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var tags *[]string - var errorModel *projModels.ErrorModel + var errorModel *projectsRESTApi.ErrorModel var err error tags, errorModel, err = projectsWrapper.Tags() if err != nil { @@ -206,10 +234,14 @@ func runGetProjectsTagsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cm if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s", failedGettingTags, errorModel.Code, errorModel.Message) } else if tags != nil { - fmt.Println("Tags:") - for _, t := range *tags { - fmt.Println(t) + var tagsJSON []byte + tagsJSON, err = json.Marshal(tags) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize project tags response ", failedGettingTags) } + cmdOut := cmd.OutOrStdout() + fmt.Fprintln(os.Stdout, "-----Tags-----") + fmt.Fprintln(cmdOut, string(tagsJSON)) } return nil } diff --git a/internal/commands/project_test.go b/internal/commands/project_test.go index 4f3798f95..e0b028bd1 100644 --- a/internal/commands/project_test.go +++ b/internal/commands/project_test.go @@ -1,3 +1,5 @@ +// +build !integration + package commands import ( @@ -7,20 +9,20 @@ import ( ) func TestProjectHelp(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "help", "project") assert.NilError(t, err) } func TestProjectNoSub(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "project") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == subcommandRequired) } func TestRunCreateProjectCommandWithFile(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "create", "--inputFile", "./payloads/nonsense.json") assert.Assert(t, err != nil) err = executeTestCommand(cmd, "-v", "project", "create", "--inputFile", "./payloads/projects.json") @@ -28,78 +30,94 @@ func TestRunCreateProjectCommandWithFile(t *testing.T) { } func TestRunCreateProjectCommandWithNoInput(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "create") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed creating a project: no input was given\n") } func TestRunCreateProjectCommandWithInput(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "create", "--input", "{\"id\": \"test_project\"}") assert.NilError(t, err) } func TestRunCreateProjectCommandWithInputBadFormat(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "create", "--input", "[]") assert.Assert(t, err != nil) } func TestRunGetProjectByIdCommandNoScanID(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "get") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed getting a project: Please provide a project ID") } func TestRunGetProjectByIdCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "get", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } func TestRunGetProjectByIdCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "get", "MOCK") assert.NilError(t, err) } func TestRunDeleteProjectByIdCommandNoProjectID(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "delete") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed deleting a project: Please provide a project ID") } func TestRunDeleteProjectByIdCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "project", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } func TestRunDeleteProjectByIdCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "delete", "MOCK") assert.NilError(t, err) } func TestRunGetAllProjectsCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "get-all") assert.NilError(t, err) } func TestRunGetAllProjectsCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "get-all", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } +func TestRunGetAllProjectsCommandWithLimit(t *testing.T) { + cmd := createASTTestCommand() + err := executeTestCommand(cmd, "-v", "project", "get-all", "--limit", "40") + assert.NilError(t, err) + err = executeTestCommand(cmd, "-v", "project", "get-all", "-l", "40") + assert.NilError(t, err) +} + +func TestRunGetAllProjectsCommandWithOffset(t *testing.T) { + cmd := createASTTestCommand() + err := executeTestCommand(cmd, "-v", "project", "get-all", "--offset", "150") + assert.NilError(t, err) + err = executeTestCommand(cmd, "-v", "project", "get-all", "-o", "150") + assert.NilError(t, err) +} + func TestRunGetProjectTagsCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "project", "tags") assert.NilError(t, err) } diff --git a/internal/commands/result.go b/internal/commands/result.go new file mode 100644 index 000000000..600168cc1 --- /dev/null +++ b/internal/commands/result.go @@ -0,0 +1,62 @@ +package commands + +import ( + "encoding/json" + "fmt" + + "github.com/checkmarxDev/ast-cli/internal/wrappers" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + failedGettingResults = "Failed getting results" +) + +func NewResultCommand(resultsWrapper wrappers.ResultsWrapper) *cobra.Command { + resultCmd := &cobra.Command{ + Use: "result", + Short: "Retrieve AST results", + } + + getResultsCmd := &cobra.Command{ + Use: "get", + Short: "Returns results for a given scan", + RunE: runGetResultByScanIDCommand(resultsWrapper), + } + getResultsCmd.PersistentFlags().Uint64P(limitFlag, limitFlagSh, 0, limitUsage) + getResultsCmd.PersistentFlags().Uint64P(offsetFlag, offsetFlagSh, 0, offsetUsage) + + resultCmd.AddCommand(getResultsCmd) + return resultCmd +} + +func runGetResultByScanIDCommand(resultsWrapper wrappers.ResultsWrapper) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + var resultResponseModel []wrappers.ResultResponseModel + var errorModel *wrappers.ResultError + var err error + if len(args) == 0 { + return errors.Errorf("%s: Please provide a scan ID", failedGettingResults) + } + scanID := args[0] + limit, offset := getLimitAndOffset(cmd) + resultResponseModel, errorModel, err = resultsWrapper.GetByScanID(scanID, limit, offset) + if err != nil { + return errors.Wrapf(err, "%s", failedGettingResults) + } + // Checking the response + if errorModel != nil { + return errors.Errorf("%s: CODE: %d, %s", failedGettingResults, errorModel.Code, errorModel.Message) + } else if resultResponseModel != nil { + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(resultResponseModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize results response ", failedGettingResults) + } + cmdOut := cmd.OutOrStdout() + fmt.Fprintln(cmdOut, string(responseModelJSON)) + } + return nil + } +} diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go new file mode 100644 index 000000000..2d3599233 --- /dev/null +++ b/internal/commands/result_test.go @@ -0,0 +1,27 @@ +// +build !integration + +package commands + +import ( + "testing" + + "gotest.tools/assert" +) + +func TestResultHelp(t *testing.T) { + cmd := createASTTestCommand() + err := executeTestCommand(cmd, "help", "result") + assert.NilError(t, err) +} + +func TestRunGetResultsByScanIDCommandNoScanID(t *testing.T) { + cmd := createASTTestCommand() + err := executeTestCommand(cmd, "-v", "result", "get") + assert.Assert(t, err != nil) + assert.Assert(t, err.Error() == "Failed getting results: Please provide a scan ID") +} +func TestRunGetResultsByScanIDCommand(t *testing.T) { + cmd := createASTTestCommand() + err := executeTestCommand(cmd, "-v", "result", "get", "MOCK") + assert.NilError(t, err) +} diff --git a/internal/commands/root.go b/internal/commands/root.go index c568389b3..0f66314a3 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -16,12 +16,19 @@ const ( inputFlagSh = "i" inputFileFlag = "inputFile" inputFileFlagSh = "f" + limitFlag = "limit" + limitFlagSh = "l" + limitUsage = "The number of items to return" + offsetFlag = "offset" + offsetFlagSh = "o" + offsetUsage = "The number of items to skip before collecting the results" ) // Return an AST CLI root command to execute func NewAstCLI(scansWrapper wrappers.ScansWrapper, uploadsWrapper wrappers.UploadsWrapper, - projectsWrapper wrappers.ProjectsWrapper) *cobra.Command { + projectsWrapper wrappers.ProjectsWrapper, + resultsWrapper wrappers.ResultsWrapper) *cobra.Command { rootCmd := &cobra.Command{ Use: "ast", Short: "A CLI wrapping Checkmarx AST APIs", @@ -30,8 +37,9 @@ func NewAstCLI(scansWrapper wrappers.ScansWrapper, scanCmd := NewScanCommand(scansWrapper, uploadsWrapper) projectCmd := NewProjectCommand(projectsWrapper) + resultCmd := NewResultCommand(resultsWrapper) versionCmd := NewVersionCommand() - rootCmd.AddCommand(scanCmd, projectCmd, versionCmd) + rootCmd.AddCommand(scanCmd, projectCmd, resultCmd, versionCmd) rootCmd.SilenceUsage = true return rootCmd } @@ -42,9 +50,11 @@ func PrintIfVerbose(verbose bool, msg string) { } } -func createASTCommand() *cobra.Command { - scansMockWrapper := &wrappers.ScansMockWrapper{} - uploadsMockWrapper := &wrappers.UploadsMockWrapper{} - projectsMockWrapper := &wrappers.ProjectsMockWrapper{} - return NewAstCLI(scansMockWrapper, uploadsMockWrapper, projectsMockWrapper) +func getLimitAndOffset(cmd *cobra.Command) (limit, offset uint64) { + verbose, _ := cmd.Flags().GetBool(verboseFlag) + limit, _ = cmd.Flags().GetUint64(limitFlag) + offset, _ = cmd.Flags().GetUint64(offsetFlag) + PrintIfVerbose(verbose, fmt.Sprintf("%s: %d", limitFlag, limit)) + PrintIfVerbose(verbose, fmt.Sprintf("%s: %d", offsetFlag, offset)) + return } diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index 118061f32..806d00837 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -1,3 +1,5 @@ +// +build !integration + package commands import ( @@ -6,6 +8,8 @@ import ( "os" "testing" + "github.com/checkmarxDev/ast-cli/internal/wrappers" + "github.com/spf13/cobra" "gotest.tools/assert" ) @@ -18,15 +22,23 @@ func TestMain(m *testing.M) { os.Exit(exitVal) } +func createASTTestCommand() *cobra.Command { + scansMockWrapper := &wrappers.ScansMockWrapper{} + uploadsMockWrapper := &wrappers.UploadsMockWrapper{} + projectsMockWrapper := &wrappers.ProjectsMockWrapper{} + resultsMockWrapper := &wrappers.ResultsMockWrapper{} + return NewAstCLI(scansMockWrapper, uploadsMockWrapper, projectsMockWrapper, resultsMockWrapper) +} + func TestRootHelp(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() args := fmt.Sprintf("--help") err := executeTestCommand(cmd, args) assert.NilError(t, err) } func TestRootVersion(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "version") assert.NilError(t, err) } diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 4b540563c..bd999bc78 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -5,12 +5,13 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "github.com/pkg/errors" wrappers "github.com/checkmarxDev/ast-cli/internal/wrappers" - scansApi "github.com/checkmarxDev/scans/api/v1/rest/scans" - "github.com/checkmarxDev/scans/pkg/scans" + scansRESTApi "github.com/checkmarxDev/scans/api/v1/rest/scans" + "github.com/spf13/cobra" ) @@ -22,6 +23,54 @@ const ( failedGettingAll = "Failed getting all" ) +func NewScanCommand(scansWrapper wrappers.ScansWrapper, uploadsWrapper wrappers.UploadsWrapper) *cobra.Command { + scanCmd := &cobra.Command{ + Use: "scan", + Short: "Manage AST scans", + } + + createScanCmd := &cobra.Command{ + Use: "create", + Short: "Create and run a new scan", + RunE: runCreateScanCommand(scansWrapper, uploadsWrapper), + } + createScanCmd.PersistentFlags().StringP(sourcesFlag, sourcesFlagSh, "", + "A path to the sources file to scan") + createScanCmd.PersistentFlags().StringP(inputFlag, inputFlagSh, "", + "The object representing the requested scan, in JSON format") + createScanCmd.PersistentFlags().StringP(inputFileFlag, inputFileFlagSh, "", + "A file holding the requested scan object in JSON format. Takes precedence over --input") + + getAllScansCmd := &cobra.Command{ + Use: "get-all", + Short: "Returns all scans in the system", + RunE: runGetAllScansCommand(scansWrapper), + } + getAllScansCmd.PersistentFlags().Uint64P(limitFlag, limitFlagSh, 0, limitUsage) + getAllScansCmd.PersistentFlags().Uint64P(offsetFlag, offsetFlagSh, 0, offsetUsage) + + getScanCmd := &cobra.Command{ + Use: "get", + Short: "Returns information about a scan", + RunE: runGetScanByIDCommand(scansWrapper), + } + + deleteScanCmd := &cobra.Command{ + Use: "delete", + Short: "Stops a scan from running", + RunE: runDeleteScanCommand(scansWrapper), + } + + tagsCmd := &cobra.Command{ + Use: "tags", + Short: "Get a list of all available tags to filter by", + RunE: runGetTagsCommand(scansWrapper), + } + + scanCmd.AddCommand(createScanCmd, getScanCmd, getAllScansCmd, deleteScanCmd, tagsCmd) + return scanCmd +} + func runCreateScanCommand(scansWrapper wrappers.ScansWrapper, uploadsWrapper wrappers.UploadsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { @@ -57,9 +106,9 @@ func runCreateScanCommand(scansWrapper wrappers.ScansWrapper, // No input was given return errors.Errorf("%s: no input was given\n", failedCreating) } - var scanModel = scansApi.Scan{} - var scanResponseModel *scans.ScanResponseModel - var errorModel *scans.ErrorModel + var scanModel = scansRESTApi.Scan{} + var scanResponseModel *scansRESTApi.ScanResponseModel + var errorModel *scansRESTApi.ErrorModel // Try to parse to a scan model in order to manipulate the request payload err = json.Unmarshal(input, &scanModel) if err != nil { @@ -73,7 +122,7 @@ func runCreateScanCommand(scansWrapper wrappers.ScansWrapper, return errors.Wrapf(err, "%s: Failed to upload sources file\n", failedCreating) } // We are in upload mode - populate fields accordingly - projectHandlerModel := scansApi.UploadProjectHandler{ + projectHandlerModel := scansRESTApi.UploadProjectHandler{ URL: *preSignedURL, } var projectHandlerModelSerialized []byte @@ -82,7 +131,7 @@ func runCreateScanCommand(scansWrapper wrappers.ScansWrapper, return errors.Wrapf(err, "%s: Failed to upload sources file: Failed to serialize project handler", failedCreating) } - scanModel.Project.Type = scansApi.UploadProject + scanModel.Project.Type = scansRESTApi.UploadProject scanModel.Project.Handler = projectHandlerModelSerialized } var payload []byte @@ -98,65 +147,27 @@ func runCreateScanCommand(scansWrapper wrappers.ScansWrapper, if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedCreating, errorModel.Code, errorModel.Message) } else if scanResponseModel != nil { - fmt.Printf("Scan created successfully: Scan ID %s\n", scanResponseModel.ID) + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(scanResponseModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize scan response ", failedCreating) + } + cmdOut := cmd.OutOrStdout() + fmt.Fprintln(os.Stdout, "Scan created successfully") + fmt.Fprintln(cmdOut, string(responseModelJSON)) } return nil } } -func NewScanCommand(scansWrapper wrappers.ScansWrapper, uploadsWrapper wrappers.UploadsWrapper) *cobra.Command { - scanCmd := &cobra.Command{ - Use: "scan", - Short: "Manage AST scans", - } - - createScanCmd := &cobra.Command{ - Use: "create", - Short: "Creates and runs a new scan", - RunE: runCreateScanCommand(scansWrapper, uploadsWrapper), - } - createScanCmd.PersistentFlags().StringP(sourcesFlag, sourcesFlagSh, "", - "A path to the sources file to scan") - createScanCmd.PersistentFlags().StringP(inputFlag, inputFlagSh, "", - "The object representing the requested scan, in JSON format") - createScanCmd.PersistentFlags().StringP(inputFileFlag, inputFileFlagSh, "", - "A file holding the requested scan object in JSON format. Takes precedence over --input") - - getAllScanCmd := &cobra.Command{ - Use: "get-all", - Short: "Returns all scans in the system", - RunE: runGetAllScansCommand(scansWrapper), - } - - getScanCmd := &cobra.Command{ - Use: "get", - Short: "Returns information about a scan", - RunE: runGetScanByIDCommand(scansWrapper), - } - - deleteScanCmd := &cobra.Command{ - Use: "delete", - Short: "Stops a scan from running", - RunE: runDeleteScanCommand(scansWrapper), - } - - tagsCmd := &cobra.Command{ - Use: "tags", - Short: "Get a list of all available tags to filter by", - RunE: runGetTagsCommand(scansWrapper), - } - - scanCmd.AddCommand(createScanCmd, getScanCmd, getAllScanCmd, deleteScanCmd, tagsCmd) - return scanCmd -} - func runGetAllScansCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var allScansModel *scans.ResponseModel - var errorModel *scans.ErrorModel + var allScansModel *scansRESTApi.SlicedScansResponseModel + var errorModel *scansRESTApi.ErrorModel var err error + limit, offset := getLimitAndOffset(cmd) - allScansModel, errorModel, err = scansWrapper.Get() + allScansModel, errorModel, err = scansWrapper.Get(limit, offset) if err != nil { return errors.Wrapf(err, "%s\n", failedGettingAll) } @@ -164,12 +175,15 @@ func runGetAllScansCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.C if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedGettingAll, errorModel.Code, errorModel.Message) } else if allScansModel != nil && allScansModel.Scans != nil { - for _, scan := range allScansModel.Scans { - fmt.Println("----------------------------") - fmt.Printf("Scan ID %s:\n", scan.ID) - fmt.Printf("Status: %s\n", scan.Status) + cmdOut := cmd.OutOrStdout() + if cmdOut != os.Stdout { + var allScansJSON []byte + allScansJSON, err = json.Marshal(allScansModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize scan response ", failedGettingAll) + } + fmt.Fprintln(cmdOut, string(allScansJSON)) } - fmt.Println("----------------------------") } return nil } @@ -177,8 +191,8 @@ func runGetAllScansCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.C func runGetScanByIDCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var scanResponseModel *scans.ScanResponseModel - var errorModel *scans.ErrorModel + var scanResponseModel *scansRESTApi.ScanResponseModel + var errorModel *scansRESTApi.ErrorModel var err error if len(args) == 0 { return errors.Errorf("%s: Please provide a scan ID", failedGetting) @@ -192,8 +206,14 @@ func runGetScanByIDCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.C if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s", failedGetting, errorModel.Code, errorModel.Message) } else if scanResponseModel != nil { - fmt.Printf("Scan ID %s:\n", scanResponseModel.ID) - fmt.Printf("Status: %s\n", scanResponseModel.Status) + var responseModelJSON []byte + responseModelJSON, err = json.Marshal(scanResponseModel) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize scan response ", failedGetting) + } + cmdOut := cmd.OutOrStdout() + fmt.Fprintf(os.Stdout, "-----Scan ID %s-----\n", scanResponseModel.ID) + fmt.Fprintln(cmdOut, string(responseModelJSON)) } return nil } @@ -201,23 +221,19 @@ func runGetScanByIDCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.C func runDeleteScanCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - var scanResponseModel *scans.ScanResponseModel - var errorModel *scans.ErrorModel + var errorModel *scansRESTApi.ErrorModel var err error if len(args) == 0 { return errors.Errorf("%s: Please provide a scan ID", failedDeleting) } scanID := args[0] - scanResponseModel, errorModel, err = scansWrapper.Delete(scanID) + errorModel, err = scansWrapper.Delete(scanID) if err != nil { return errors.Wrapf(err, "%s\n", failedDeleting) } // Checking the response if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s\n", failedDeleting, errorModel.Code, errorModel.Message) - } else if scanResponseModel != nil { - fmt.Printf("Scan ID %s:\n", scanResponseModel.ID) - fmt.Printf("Status: %s\n", scanResponseModel.Status) } return nil } @@ -226,7 +242,7 @@ func runDeleteScanCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Co func runGetTagsCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var tags *[]string - var errorModel *scans.ErrorModel + var errorModel *scansRESTApi.ErrorModel var err error tags, errorModel, err = scansWrapper.Tags() if err != nil { @@ -236,10 +252,14 @@ func runGetTagsCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Comma if errorModel != nil { return errors.Errorf("%s: CODE: %d, %s", failedGettingTags, errorModel.Code, errorModel.Message) } else if tags != nil { - fmt.Println("Tags:") - for _, t := range *tags { - fmt.Println(t) + var tagsJSON []byte + tagsJSON, err = json.Marshal(tags) + if err != nil { + return errors.Wrapf(err, "%s: failed to serialize scan tags response ", failedGettingTags) } + cmdOut := cmd.OutOrStdout() + fmt.Fprintln(os.Stdout, "-----Tags-----") + fmt.Fprintln(cmdOut, string(tagsJSON)) } return nil } diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 3f1152434..cbe0f6963 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -1,3 +1,5 @@ +// +build !integration + package commands import ( @@ -12,20 +14,20 @@ const ( ) func TestScanHelp(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "help", "scan") assert.NilError(t, err) } func TestScanNoSub(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "scan") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == subcommandRequired) } func TestRunCreateScanCommandWithFile(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "create", "--inputFile", "./payloads/nonsense.json") assert.Assert(t, err != nil) err = executeTestCommand(cmd, "-v", "scan", "create", "--inputFile", "./payloads/uploads.json", "--sources", "./payloads/sources.zip") @@ -33,80 +35,80 @@ func TestRunCreateScanCommandWithFile(t *testing.T) { } func TestRunCreateScanCommandWithNoInput(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "create") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed creating a scan: no input was given\n") } func TestRunCreateScanCommandWithInput(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "create", "--input", "{\"project\":{\"id\":\"test\",\"type\":\"upload\",\"handler\":"+ - "{\"url\":\"MOSHIKO\"},\"tags\":{}},\"config\":"+ - "[{\"type\":\"sast\",\"value\":{\"presetName\":\"Default\"}}],\"tags\":{}}", "--sources", "./payloads/sources.zip") + "{\"url\":\"MOSHIKO\"},\"tags\":[]},\"config\":"+ + "[{\"type\":\"sast\",\"value\":{\"presetName\":\"Default\"}}],\"tags\":[]}", "--sources", "./payloads/sources.zip") assert.NilError(t, err) } func TestRunCreateScanCommandWithInputBadFormat(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "create", "--input", "[]", "--sources", "./payloads/sources.zip") assert.Assert(t, err != nil) } func TestRunGetScanByIdCommandNoScanID(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "get") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed getting a scan: Please provide a scan ID") } func TestRunGetScanByIdCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "get", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } func TestRunGetScanByIdCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "get", "MOCK") assert.NilError(t, err) } func TestRunDeleteScanByIdCommandNoScanID(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "delete") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == "Failed deleting a scan: Please provide a scan ID") } func TestRunDeleteByIdCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "delete", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } func TestRunDeleteScanByIdCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "delete", "MOCK") assert.NilError(t, err) } func TestRunGetAllCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "get-all") assert.NilError(t, err) } func TestRunGetAllCommandFlagNonExist(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "get-all", "--chibutero") assert.Assert(t, err != nil) assert.Assert(t, err.Error() == unknownFlag) } func TestRunTagsCommand(t *testing.T) { - cmd := createASTCommand() + cmd := createASTTestCommand() err := executeTestCommand(cmd, "-v", "scan", "tags") assert.NilError(t, err) } diff --git a/internal/wrappers/projects-http.go b/internal/wrappers/projects-http.go index 2e127b657..5325b6379 100644 --- a/internal/wrappers/projects-http.go +++ b/internal/wrappers/projects-http.go @@ -4,11 +4,16 @@ import ( "bytes" "encoding/json" "net/http" + "strconv" "github.com/pkg/errors" - projApi "github.com/checkmarxDev/scans/api/v1/rest/projects" - projModels "github.com/checkmarxDev/scans/pkg/projects" + projectsRESTApi "github.com/checkmarxDev/scans/api/v1/rest/projects" +) + +const ( + limitQueryParam = "limit" + offsetQueryParam = "offset" ) type ProjectsHTTPWrapper struct { @@ -23,18 +28,22 @@ func NewHTTPProjectsWrapper(url string) ProjectsWrapper { } } -func (p *ProjectsHTTPWrapper) Create(model *projApi.Project) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsHTTPWrapper) Create(model *projectsRESTApi.Project) ( + *projectsRESTApi.ProjectResponseModel, + *projectsRESTApi.ErrorModel, error) { jsonBytes, err := json.Marshal(model) if err != nil { return nil, nil, err } resp, err := http.Post(p.url, p.contentType, bytes.NewBuffer(jsonBytes)) - return handleProjectsResponse(resp, err, http.StatusCreated) + return handleProjectResponseWithBody(resp, err, http.StatusCreated) } -func (p *ProjectsHTTPWrapper) Get() (*projModels.ResponseModel, *projModels.ErrorModel, error) { - resp, err := http.Get(p.url) +func (p *ProjectsHTTPWrapper) Get(limit, offset uint64) ( + *projectsRESTApi.SlicedProjectsResponseModel, + *projectsRESTApi.ErrorModel, error) { + resp, err := getRequestWithLimitAndOffset(p.url, limit, offset) if err != nil { return nil, nil, err } @@ -43,14 +52,14 @@ func (p *ProjectsHTTPWrapper) Get() (*projModels.ResponseModel, *projModels.Erro defer resp.Body.Close() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := projModels.ErrorModel{} + errorModel := projectsRESTApi.ErrorModel{} err = decoder.Decode(&errorModel) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseGetAll) } return nil, &errorModel, nil case http.StatusOK: - model := projModels.ResponseModel{} + model := projectsRESTApi.SlicedProjectsResponseModel{} err = decoder.Decode(&model) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseGetAll) @@ -62,28 +71,33 @@ func (p *ProjectsHTTPWrapper) Get() (*projModels.ResponseModel, *projModels.Erro } } -func (p *ProjectsHTTPWrapper) GetByID(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsHTTPWrapper) GetByID(projectID string) ( + *projectsRESTApi.ProjectResponseModel, + *projectsRESTApi.ErrorModel, + error) { resp, err := http.Get(p.url + "/" + projectID) if err != nil { return nil, nil, err } - return handleProjectsResponse(resp, err, http.StatusOK) + return handleProjectResponseWithBody(resp, err, http.StatusOK) } -func (p *ProjectsHTTPWrapper) Delete(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsHTTPWrapper) Delete(projectID string) ( + *projectsRESTApi.ErrorModel, + error) { client := &http.Client{} req, err := http.NewRequest("DELETE", p.url+"/"+projectID, nil) if err != nil { - return nil, nil, err + return nil, err } resp, err := client.Do(req) - if err != nil { - return nil, nil, err - } - return handleProjectsResponse(resp, err, http.StatusOK) + return handleProjectResponseWithNoBody(resp, err, http.StatusNoContent) } -func (p *ProjectsHTTPWrapper) Tags() (*[]string, *projModels.ErrorModel, error) { +func (p *ProjectsHTTPWrapper) Tags() ( + *[]string, + *projectsRESTApi.ErrorModel, + error) { resp, err := http.Get(p.url + "/tags") if err != nil { return nil, nil, err @@ -93,7 +107,7 @@ func (p *ProjectsHTTPWrapper) Tags() (*[]string, *projModels.ErrorModel, error) defer resp.Body.Close() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := projModels.ErrorModel{} + errorModel := projectsRESTApi.ErrorModel{} err = decoder.Decode(&errorModel) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseTags) @@ -112,38 +126,20 @@ func (p *ProjectsHTTPWrapper) Tags() (*[]string, *projModels.ErrorModel, error) } } -func handleProjectsResponse( - resp *http.Response, - err error, - successStatusCode int) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func getRequestWithLimitAndOffset(url string, limit, offset uint64) (*http.Response, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) if err != nil { - return nil, nil, err + return nil, err } - decoder := json.NewDecoder(resp.Body) - - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := projModels.ErrorModel{} - err = decoder.Decode(&errorModel) - if err != nil { - return responseProjectsParsingFailed(err) - } - return nil, &errorModel, nil - case successStatusCode: - model := projModels.ProjectResponseModel{} - err = decoder.Decode(&model) - if err != nil { - return responseProjectsParsingFailed(err) - } - return &model, nil, nil - - default: - return nil, nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + q := req.URL.Query() + if limit > 0 { + q.Add(limitQueryParam, strconv.FormatUint(limit, 10)) } -} - -func responseProjectsParsingFailed(err error) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { - msg := "Failed to parse a project response" - return nil, nil, errors.Wrapf(err, msg) + if offset > 0 { + q.Add(offsetQueryParam, strconv.FormatUint(offset, 10)) + } + req.URL.RawQuery = q.Encode() + resp, err := client.Do(req) + return resp, err } diff --git a/internal/wrappers/projects-mock.go b/internal/wrappers/projects-mock.go index 918be7da5..60d3d91d0 100644 --- a/internal/wrappers/projects-mock.go +++ b/internal/wrappers/projects-mock.go @@ -3,23 +3,28 @@ package wrappers import ( "fmt" - projApi "github.com/checkmarxDev/scans/api/v1/rest/projects" - projModels "github.com/checkmarxDev/scans/pkg/projects" + projectsRESTApi "github.com/checkmarxDev/scans/api/v1/rest/projects" ) type ProjectsMockWrapper struct{} -func (p *ProjectsMockWrapper) Create(model *projApi.Project) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsMockWrapper) Create(model *projectsRESTApi.Project) ( + *projectsRESTApi.ProjectResponseModel, + *projectsRESTApi.ErrorModel, + error) { fmt.Println("Called Create in ProjectsMockWrapper") - return &projModels.ProjectResponseModel{ + return &projectsRESTApi.ProjectResponseModel{ ID: model.ID, }, nil, nil } -func (p *ProjectsMockWrapper) Get() (*projModels.ResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsMockWrapper) Get(limit, offset uint64) ( + *projectsRESTApi.SlicedProjectsResponseModel, + *projectsRESTApi.ErrorModel, + error) { fmt.Println("Called Get in ProjectsMockWrapper") - return &projModels.ResponseModel{ - Projects: []projModels.ProjectResponseModel{ + return &projectsRESTApi.SlicedProjectsResponseModel{ + Projects: []projectsRESTApi.ProjectResponseModel{ { ID: "MOCK", }, @@ -27,21 +32,27 @@ func (p *ProjectsMockWrapper) Get() (*projModels.ResponseModel, *projModels.Erro }, nil, nil } -func (p *ProjectsMockWrapper) GetByID(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsMockWrapper) GetByID(projectID string) ( + *projectsRESTApi.ProjectResponseModel, + *projectsRESTApi.ErrorModel, + error) { fmt.Println("Called GetByID in ProjectsMockWrapper") - return &projModels.ProjectResponseModel{ + return &projectsRESTApi.ProjectResponseModel{ ID: projectID, }, nil, nil } -func (p *ProjectsMockWrapper) Delete(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) { +func (p *ProjectsMockWrapper) Delete(projectID string) ( + *projectsRESTApi.ErrorModel, + error) { fmt.Println("Called Delete in ProjectsMockWrapper") - return &projModels.ProjectResponseModel{ - ID: projectID, - }, nil, nil + return nil, nil } -func (p *ProjectsMockWrapper) Tags() (*[]string, *projModels.ErrorModel, error) { +func (p *ProjectsMockWrapper) Tags() ( + *[]string, + *projectsRESTApi.ErrorModel, + error) { fmt.Println("Called Tags in ProjectsMockWrapper") return &[]string{"t1"}, nil, nil } diff --git a/internal/wrappers/projects.go b/internal/wrappers/projects.go index 70df5a69d..0ee2cc9d7 100644 --- a/internal/wrappers/projects.go +++ b/internal/wrappers/projects.go @@ -1,14 +1,13 @@ package wrappers import ( - projApi "github.com/checkmarxDev/scans/api/v1/rest/projects" - projModels "github.com/checkmarxDev/scans/pkg/projects" + projectsRESTApi "github.com/checkmarxDev/scans/api/v1/rest/projects" ) type ProjectsWrapper interface { - Create(model *projApi.Project) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) - Get() (*projModels.ResponseModel, *projModels.ErrorModel, error) - GetByID(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) - Delete(projectID string) (*projModels.ProjectResponseModel, *projModels.ErrorModel, error) - Tags() (*[]string, *projModels.ErrorModel, error) + Create(model *projectsRESTApi.Project) (*projectsRESTApi.ProjectResponseModel, *projectsRESTApi.ErrorModel, error) + Get(limit, offset uint64) (*projectsRESTApi.SlicedProjectsResponseModel, *projectsRESTApi.ErrorModel, error) + GetByID(projectID string) (*projectsRESTApi.ProjectResponseModel, *projectsRESTApi.ErrorModel, error) + Delete(projectID string) (*projectsRESTApi.ErrorModel, error) + Tags() (*[]string, *projectsRESTApi.ErrorModel, error) } diff --git a/internal/wrappers/response.go b/internal/wrappers/response.go new file mode 100644 index 000000000..6cab6f180 --- /dev/null +++ b/internal/wrappers/response.go @@ -0,0 +1,129 @@ +package wrappers + +import ( + "encoding/json" + "net/http" + + projectsApi "github.com/checkmarxDev/scans/api/v1/rest/projects" + scansApi "github.com/checkmarxDev/scans/api/v1/rest/scans" + "github.com/pkg/errors" +) + +const ( + failedToParseErr = "Failed to parse error response" +) + +func handleScanResponseWithNoBody(resp *http.Response, err error, + successStatusCode int) (*scansApi.ErrorModel, error) { + if err != nil { + return nil, err + } + decoder := json.NewDecoder(resp.Body) + + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := scansApi.ErrorModel{} + err = decoder.Decode(&errorModel) + if err != nil { + return nil, errors.Wrapf(err, failedToParseErr) + } + return &errorModel, nil + case successStatusCode: + return nil, nil + + default: + return nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + } +} + +func handleScanResponseWithBody(resp *http.Response, err error, + successStatusCode int) (*scansApi.ScanResponseModel, *scansApi.ErrorModel, error) { + if err != nil { + return nil, nil, err + } + decoder := json.NewDecoder(resp.Body) + + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := scansApi.ErrorModel{} + err = decoder.Decode(&errorModel) + if err != nil { + return responseScanParsingFailed(err) + } + return nil, &errorModel, nil + case successStatusCode: + model := scansApi.ScanResponseModel{} + err = decoder.Decode(&model) + if err != nil { + return responseScanParsingFailed(err) + } + return &model, nil, nil + + default: + return nil, nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + } +} + +func handleProjectResponseWithNoBody(resp *http.Response, err error, + successStatusCode int) (*projectsApi.ErrorModel, error) { + if err != nil { + return nil, err + } + decoder := json.NewDecoder(resp.Body) + + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := projectsApi.ErrorModel{} + err = decoder.Decode(&errorModel) + if err != nil { + return nil, errors.Wrapf(err, failedToParseErr) + } + return &errorModel, nil + case successStatusCode: + return nil, nil + + default: + return nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + } +} + +func handleProjectResponseWithBody(resp *http.Response, err error, + successStatusCode int) (*projectsApi.ProjectResponseModel, *projectsApi.ErrorModel, error) { + if err != nil { + return nil, nil, err + } + decoder := json.NewDecoder(resp.Body) + + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := projectsApi.ErrorModel{} + err = decoder.Decode(&errorModel) + if err != nil { + return responseProjectParsingFailed(err) + } + return nil, &errorModel, nil + case successStatusCode: + model := projectsApi.ProjectResponseModel{} + err = decoder.Decode(&model) + if err != nil { + return responseProjectParsingFailed(err) + } + return &model, nil, nil + + default: + return nil, nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + } +} + +func responseScanParsingFailed(err error) (*scansApi.ScanResponseModel, *scansApi.ErrorModel, error) { + msg := "Failed to parse scan response" + return nil, nil, errors.Wrapf(err, msg) +} +func responseProjectParsingFailed(err error) (*projectsApi.ProjectResponseModel, *projectsApi.ErrorModel, error) { + msg := "Failed to parse project response" + return nil, nil, errors.Wrapf(err, msg) +} diff --git a/internal/wrappers/results-http.go b/internal/wrappers/results-http.go new file mode 100644 index 000000000..d27ea29cd --- /dev/null +++ b/internal/wrappers/results-http.go @@ -0,0 +1,53 @@ +package wrappers + +import ( + "encoding/json" + "net/http" + + "github.com/pkg/errors" +) + +const ( + failedToParseGetResults = "Failed to parse get results" +) + +type ResultsHTTPWrapper struct { + url string + contentType string +} + +func NewHTTPResultsWrapper(url string) ResultsWrapper { + return &ResultsHTTPWrapper{ + url: url, + contentType: "application/json", + } +} + +func (r *ResultsHTTPWrapper) GetByScanID(scanID string, limit, offset uint64) ([]ResultResponseModel, *ResultError, error) { + resp, err := getRequestWithLimitAndOffset(r.url+"/scan"+scanID+"/items", limit, offset) + if err != nil { + return nil, nil, err + } + decoder := json.NewDecoder(resp.Body) + + defer resp.Body.Close() + switch resp.StatusCode { + case http.StatusBadRequest, http.StatusInternalServerError: + errorModel := ResultError{} + err = decoder.Decode(&errorModel) + if err != nil { + return nil, nil, errors.Wrapf(err, failedToParseGetResults) + } + return nil, &errorModel, nil + case http.StatusOK: + model := []ResultResponseModel{} + err = decoder.Decode(&model) + if err != nil { + return nil, nil, errors.Wrapf(err, failedToParseGetResults) + } + return model, nil, nil + + default: + return nil, nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) + } +} diff --git a/internal/wrappers/results-mock.go b/internal/wrappers/results-mock.go new file mode 100644 index 000000000..339c478bf --- /dev/null +++ b/internal/wrappers/results-mock.go @@ -0,0 +1,7 @@ +package wrappers + +type ResultsMockWrapper struct{} + +func (r ResultsMockWrapper) GetByScanID(scanID string, limit, offset uint64) ([]ResultResponseModel, *ResultError, error) { + return []ResultResponseModel{}, nil, nil +} diff --git a/internal/wrappers/results.go b/internal/wrappers/results.go new file mode 100644 index 000000000..c95a9a59d --- /dev/null +++ b/internal/wrappers/results.go @@ -0,0 +1,81 @@ +package wrappers + +import ( + "math/big" +) + +type ResultsWrapper interface { + GetByScanID(scanID string, limit, offset uint64) ([]ResultResponseModel, *ResultError, error) +} + +type ResultError struct { + Code int32 `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Data interface{} `json:"data,omitempty"` +} + +type ResultNode struct { + // Column position of the node + Column int32 `json:"column,omitempty"` + // Full file name of the containing source file + FileName string `json:"fileName,omitempty"` + // FQN of the node + FullName string `json:"fullName,omitempty"` + // Length of the node + Length int32 `json:"length,omitempty"` + // Line position of the node + Line int32 `json:"line,omitempty"` + // Line position of the containing method + MethodLine int32 `json:"methodLine,omitempty"` + // node name + Name string `json:"name,omitempty"` + // ID of node + NodeID int32 `json:"nodeID,omitempty"` + // node DomType + DomType string `json:"domType,omitempty"` + // ID of the customer tenant + NodeSystemID string `json:"nodeSystemID,omitempty"` +} + +type ResultResponseModel struct { + // Query ID + QueryID int32 `json:"queryID,omitempty"` + // Query name + QueryName string `json:"queryName,omitempty"` + // Query group; sperate by ':' + GroupName string `json:"groupName,omitempty"` + // Severity of result + Severity string `json:"severity,omitempty"` + // Common Weakness Enumeration ID + CweID int32 `json:"cweID,omitempty"` + // ID of the path. changes from scan to scan. + PathID int32 `json:"pathID,omitempty"` + // ID of the Similarity feature (Indicator to identify a result by its first and last nodes) + SimilarityID int32 `json:"similarityID,omitempty"` + // Same as similarityID but can change in the future (SAST feature) + UniqueID int32 `json:"uniqueID,omitempty"` + // Confidence Level of the exsitin of the result + ConfidenceLevel big.Float `json:"confidenceLevel,omitempty"` + + Nodes []ResultNode `json:"nodes,omitempty"` + // ID of the customer tenant + TenantID string `json:"tenantID,omitempty"` + // ID of the scan + ScanID string `json:"scanID,omitempty"` + // Creation date of the result + CreatedAt string `json:"createdAt,omitempty"` + + Classification string `json:"classification,omitempty"` + // Groups arrays + Groups []string `json:"groups,omitempty"` + // ID of the customer tenant + PathSystemID string `json:"pathSystemID,omitempty"` + // ID created from queryMetaInfo + similarityID + files name + PathSystemIDBySimiAndFilesPaths string `json:"pathSystemIDBySimiAndFilesPaths,omitempty"` + // enum of the current state(new,old,fixed) + Status string `json:"status,omitempty"` + // TBD + MetadataJSON string `json:"metadataJSON,omitempty"` + // TBD + ExtraJSON string `json:"extraJSON,omitempty"` +} diff --git a/internal/wrappers/scans-http.go b/internal/wrappers/scans-http.go index c5733e432..c74715bc1 100644 --- a/internal/wrappers/scans-http.go +++ b/internal/wrappers/scans-http.go @@ -6,7 +6,6 @@ import ( "net/http" scansApi "github.com/checkmarxDev/scans/api/v1/rest/scans" - scansModels "github.com/checkmarxDev/scans/pkg/scans" "github.com/pkg/errors" ) @@ -20,18 +19,18 @@ type ScansHTTPWrapper struct { contentType string } -func (s *ScansHTTPWrapper) Create(model *scansApi.Scan) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (s *ScansHTTPWrapper) Create(model *scansApi.Scan) (*scansApi.ScanResponseModel, *scansApi.ErrorModel, error) { jsonBytes, err := json.Marshal(model) if err != nil { return nil, nil, err } resp, err := http.Post(s.url, s.contentType, bytes.NewBuffer(jsonBytes)) - return handleResponse(resp, err, http.StatusCreated) + return handleScanResponseWithBody(resp, err, http.StatusCreated) } -func (s *ScansHTTPWrapper) Get() (*scansModels.ResponseModel, *scansModels.ErrorModel, error) { - resp, err := http.Get(s.url) +func (s *ScansHTTPWrapper) Get(limit, offset uint64) (*scansApi.SlicedScansResponseModel, *scansApi.ErrorModel, error) { + resp, err := getRequestWithLimitAndOffset(s.url, limit, offset) if err != nil { return nil, nil, err } @@ -40,14 +39,14 @@ func (s *ScansHTTPWrapper) Get() (*scansModels.ResponseModel, *scansModels.Error defer resp.Body.Close() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := scansModels.ErrorModel{} + errorModel := scansApi.ErrorModel{} err = decoder.Decode(&errorModel) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseGetAll) } return nil, &errorModel, nil case http.StatusOK: - model := scansModels.ResponseModel{} + model := scansApi.SlicedScansResponseModel{} err = decoder.Decode(&model) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseGetAll) @@ -59,28 +58,25 @@ func (s *ScansHTTPWrapper) Get() (*scansModels.ResponseModel, *scansModels.Error } } -func (s *ScansHTTPWrapper) GetByID(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (s *ScansHTTPWrapper) GetByID(scanID string) (*scansApi.ScanResponseModel, *scansApi.ErrorModel, error) { resp, err := http.Get(s.url + "/" + scanID) if err != nil { return nil, nil, err } - return handleResponse(resp, err, http.StatusOK) + return handleScanResponseWithBody(resp, err, http.StatusOK) } -func (s *ScansHTTPWrapper) Delete(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (s *ScansHTTPWrapper) Delete(scanID string) (*scansApi.ErrorModel, error) { client := &http.Client{} req, err := http.NewRequest("DELETE", s.url+"/"+scanID, nil) if err != nil { - return nil, nil, err + return nil, err } resp, err := client.Do(req) - if err != nil { - return nil, nil, err - } - return handleResponse(resp, err, http.StatusOK) + return handleScanResponseWithNoBody(resp, err, http.StatusOK) } -func (s *ScansHTTPWrapper) Tags() (*[]string, *scansModels.ErrorModel, error) { +func (s *ScansHTTPWrapper) Tags() (*[]string, *scansApi.ErrorModel, error) { resp, err := http.Get(s.url + "/tags") if err != nil { return nil, nil, err @@ -90,7 +86,7 @@ func (s *ScansHTTPWrapper) Tags() (*[]string, *scansModels.ErrorModel, error) { defer resp.Body.Close() switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := scansModels.ErrorModel{} + errorModel := scansApi.ErrorModel{} err = decoder.Decode(&errorModel) if err != nil { return nil, nil, errors.Wrapf(err, failedToParseTags) @@ -115,39 +111,3 @@ func NewHTTPScansWrapper(url string) ScansWrapper { contentType: "application/json", } } - -func responseParsingFailed(err error) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { - msg := "Failed to parse a scan response" - return nil, nil, errors.Wrapf(err, msg) -} - -func handleResponse( - resp *http.Response, - err error, - successStatusCode int) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { - if err != nil { - return nil, nil, err - } - decoder := json.NewDecoder(resp.Body) - - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := scansModels.ErrorModel{} - err = decoder.Decode(&errorModel) - if err != nil { - return responseParsingFailed(err) - } - return nil, &errorModel, nil - case successStatusCode: - model := scansModels.ScanResponseModel{} - err = decoder.Decode(&model) - if err != nil { - return responseParsingFailed(err) - } - return &model, nil, nil - - default: - return nil, nil, errors.Errorf("Unknown response status code %d", resp.StatusCode) - } -} diff --git a/internal/wrappers/scans-mock.go b/internal/wrappers/scans-mock.go index aa2e5a9c2..900495e77 100644 --- a/internal/wrappers/scans-mock.go +++ b/internal/wrappers/scans-mock.go @@ -3,25 +3,24 @@ package wrappers import ( "fmt" - scansApi "github.com/checkmarxDev/scans/api/v1/rest/scans" - scansModels "github.com/checkmarxDev/scans/pkg/scans" + scansRESTApi "github.com/checkmarxDev/scans/api/v1/rest/scans" ) type ScansMockWrapper struct { } -func (m *ScansMockWrapper) Create(model *scansApi.Scan) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (m *ScansMockWrapper) Create(model *scansRESTApi.Scan) (*scansRESTApi.ScanResponseModel, *scansRESTApi.ErrorModel, error) { fmt.Println("Called Create in ScansMockWrapper") - return &scansModels.ScanResponseModel{ + return &scansRESTApi.ScanResponseModel{ ID: model.ScanID, Status: "MOCK", }, nil, nil } -func (m *ScansMockWrapper) Get() (*scansModels.ResponseModel, *scansModels.ErrorModel, error) { +func (m *ScansMockWrapper) Get(limit, offset uint64) (*scansRESTApi.SlicedScansResponseModel, *scansRESTApi.ErrorModel, error) { fmt.Println("Called Get in ScansMockWrapper") - return &scansModels.ResponseModel{ - Scans: []scansModels.ScanResponseModel{ + return &scansRESTApi.SlicedScansResponseModel{ + Scans: []scansRESTApi.ScanResponseModel{ { ID: "MOCK", Status: "STATUS", @@ -30,22 +29,20 @@ func (m *ScansMockWrapper) Get() (*scansModels.ResponseModel, *scansModels.Error }, nil, nil } -func (m *ScansMockWrapper) GetByID(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (m *ScansMockWrapper) GetByID(scanID string) (*scansRESTApi.ScanResponseModel, *scansRESTApi.ErrorModel, error) { fmt.Println("Called GetByID in ScansMockWrapper") - return &scansModels.ScanResponseModel{ + return &scansRESTApi.ScanResponseModel{ ID: scanID, Status: "STATUS", }, nil, nil } -func (m *ScansMockWrapper) Delete(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) { +func (m *ScansMockWrapper) Delete(scanID string) (*scansRESTApi.ErrorModel, error) { fmt.Println("Called Delete in ScansMockWrapper") - return &scansModels.ScanResponseModel{ - ID: scanID, - Status: "STATUS", - }, nil, nil + return nil, nil } -func (m *ScansMockWrapper) Tags() (*[]string, *scansModels.ErrorModel, error) { + +func (m *ScansMockWrapper) Tags() (*[]string, *scansRESTApi.ErrorModel, error) { fmt.Println("Called Tags in ScansMockWrapper") return &[]string{"t1"}, nil, nil } diff --git a/internal/wrappers/scans.go b/internal/wrappers/scans.go index 779d00f09..bcf89fbea 100644 --- a/internal/wrappers/scans.go +++ b/internal/wrappers/scans.go @@ -1,14 +1,13 @@ package wrappers import ( - scansApi "github.com/checkmarxDev/scans/api/v1/rest/scans" - scansModels "github.com/checkmarxDev/scans/pkg/scans" + scansRESTApi "github.com/checkmarxDev/scans/api/v1/rest/scans" ) type ScansWrapper interface { - Create(model *scansApi.Scan) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) - Get() (*scansModels.ResponseModel, *scansModels.ErrorModel, error) - GetByID(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) - Delete(scanID string) (*scansModels.ScanResponseModel, *scansModels.ErrorModel, error) - Tags() (*[]string, *scansModels.ErrorModel, error) + Create(model *scansRESTApi.Scan) (*scansRESTApi.ScanResponseModel, *scansRESTApi.ErrorModel, error) + Get(limit, offset uint64) (*scansRESTApi.SlicedScansResponseModel, *scansRESTApi.ErrorModel, error) + GetByID(scanID string) (*scansRESTApi.ScanResponseModel, *scansRESTApi.ErrorModel, error) + Delete(scanID string) (*scansRESTApi.ErrorModel, error) + Tags() (*[]string, *scansRESTApi.ErrorModel, error) } diff --git a/internal/wrappers/uploads-http.go b/internal/wrappers/uploads-http.go index a80978d12..b597e5d35 100644 --- a/internal/wrappers/uploads-http.go +++ b/internal/wrappers/uploads-http.go @@ -8,6 +8,7 @@ import ( "mime/multipart" "net/http" "os" + "path/filepath" "time" uploads "github.com/checkmarxDev/uploads/api/rest/v1" @@ -37,7 +38,8 @@ func (u UploadsHTTPWrapper) Create(sourcesFile string) (*string, error) { // Initialize the file field var fileWriter io.Writer - fileWriter, err = multiPartWriter.CreateFormFile("sources", sourcesFile) + sourcesFileName := filepath.Base(sourcesFile) + fileWriter, err = multiPartWriter.CreateFormFile("sources", sourcesFileName) if err != nil { return nil, errors.Errorf("Failed creating FormFile - %s", err.Error()) } diff --git a/test/integration/project_payload.json b/test/integration/project_payload.json new file mode 100644 index 000000000..4a8fe9aeb --- /dev/null +++ b/test/integration/project_payload.json @@ -0,0 +1,6 @@ +{ + "id": "integration_test", + "tags": [ + "A", "B", "C" + ] +} \ No newline at end of file diff --git a/test/integration/project_test.go b/test/integration/project_test.go new file mode 100644 index 000000000..da8fc0d35 --- /dev/null +++ b/test/integration/project_test.go @@ -0,0 +1,124 @@ +// +build integration + +package integration + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "strconv" + "testing" + + projectsRESTApi "github.com/checkmarxDev/scans/api/v1/rest/projects" + "gotest.tools/assert" +) + +func TestProjectsE2E(t *testing.T) { + projectFromFile := createProjectFromInputFile(t) + projectID := createProjectFromInput(t, RandomizeString(5), []string{}) + deleteProject(t, projectID) + getAllProjects(t, projectFromFile) + getProjectByID(t, projectFromFile) + _ = createProjectFromInput(t, RandomizeString(5), []string{"A", "B", "D"}) + getProjectTags(t) +} + +func createProjectFromInputFile(t *testing.T) string { + b := bytes.NewBufferString("") + createProjCommand := createASTIntegrationTestCommand() + createProjCommand.SetOut(b) + err := execute(createProjCommand, "-v", "project", "create", "--inputFile", "project_payload.json") + return executeCreateProject(t, err, b) +} + +func createProjectFromInput(t *testing.T, projectID string, tags []string) string { + b := bytes.NewBufferString("") + createProjCommand := createASTIntegrationTestCommand() + createProjCommand.SetOut(b) + tagsJSON, err := json.Marshal(tags) + assert.NilError(t, err, "Marshaling tags should pass") + payload := fmt.Sprintf("{\"id\":\"integration_test_%s\", \"tags\":%s}", projectID, string(tagsJSON)) + err = execute(createProjCommand, "-v", "project", "create", "--input", payload) + return executeCreateProject(t, err, b) +} + +func executeCreateProject(t *testing.T, err error, b *bytes.Buffer) string { + assert.NilError(t, err, "Creating a project should pass") + // Read response from buffer + var createdProjectJSON []byte + createdProjectJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading project response JSON should pass") + createdProject := projectsRESTApi.ProjectResponseModel{} + err = json.Unmarshal(createdProjectJSON, &createdProject) + assert.NilError(t, err, "Parsing project response JSON should pass") + log.Printf("Project ID %s created in test", createdProject.ID) + return createdProject.ID +} + +func getProjectByID(t *testing.T, projectID string) { + b := bytes.NewBufferString("") + getProjectCommand := createASTIntegrationTestCommand() + getProjectCommand.SetOut(b) + err := execute(getProjectCommand, "-v", "project", "get", projectID) + assert.NilError(t, err, "Getting a project should pass") + // Read response from buffer + var projectJSON []byte + projectJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading project response JSON should pass") + project := projectsRESTApi.ProjectResponseModel{} + err = json.Unmarshal(projectJSON, &project) + assert.NilError(t, err, "Parsing project response JSON should pass") + assert.Assert(t, projectID == project.ID) + assert.Assert(t, project.Tags != nil) + assert.Assert(t, len(project.Tags) == 3) + assert.Assert(t, project.Tags[0] == "A") + assert.Assert(t, project.Tags[1] == "B") + assert.Assert(t, project.Tags[2] == "C") +} + +func getAllProjects(t *testing.T, projectID string) { + b := bytes.NewBufferString("") + getAllCommand := createASTIntegrationTestCommand() + getAllCommand.SetOut(b) + var limit uint64 = 40 + var offset uint64 = 0 + err := execute(getAllCommand, "-v", "project", "get-all", "--limit", strconv.FormatUint(limit, 10), "--offset", strconv.FormatUint(offset, 10)) + assert.NilError(t, err, "Getting all projects should pass") + // Read response from buffer + var getAllJSON []byte + getAllJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading all projects response JSON should pass") + allProjects := projectsRESTApi.SlicedProjectsResponseModel{} + err = json.Unmarshal(getAllJSON, &allProjects) + assert.NilError(t, err, "Parsing all projects response JSON should pass") + assert.Assert(t, uint64(allProjects.Limit) == limit, fmt.Sprintf("limit should be %d", limit)) + assert.Assert(t, uint64(allProjects.Offset) == offset, fmt.Sprintf("offset should be %d", offset)) + assert.Assert(t, allProjects.TotalCount == 1, "Total should be 1") + assert.Assert(t, len(allProjects.Projects) == 1, "Total should be 1") + assert.Assert(t, allProjects.Projects[0].ID == projectID) +} + +func deleteProject(t *testing.T, projectID string) { + deleteProjCommand := createASTIntegrationTestCommand() + err := execute(deleteProjCommand, "-v", "project", "delete", projectID) + assert.NilError(t, err, "Deleting a project should pass") +} + +func getProjectTags(t *testing.T) { + b := bytes.NewBufferString("") + tagsCommand := createASTIntegrationTestCommand() + tagsCommand.SetOut(b) + err := execute(tagsCommand, "-v", "project", "tags") + assert.NilError(t, err, "Getting tags should pass") + // Read response from buffer + var tagsJSON []byte + tagsJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading tags JSON should pass") + tags := []string{} + err = json.Unmarshal(tagsJSON, &tags) + assert.NilError(t, err, "Parsing tags JSON should pass") + assert.Assert(t, tags != nil) + assert.Assert(t, len(tags) == 4) +} diff --git a/test/integration/root_test.go b/test/integration/root_test.go new file mode 100644 index 000000000..20b2025f8 --- /dev/null +++ b/test/integration/root_test.go @@ -0,0 +1,88 @@ +// +build integration + +package integration + +import ( + "fmt" + "github.com/checkmarxDev/ast-cli/internal/commands" + "github.com/checkmarxDev/ast-cli/internal/wrappers" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "log" + "math/rand" + "os" + "testing" + "time" +) + +const ( + astSchema = "AST_SCHEMA" + astHost = "AST_HOST" + astPort = "80" + scansPath = "SCANS_PATH" + projectsPath = "PROJECTS_PATH" + resultsPath = "RESULTS_PATH" + uploadsPath = "UPLOADS_PATH" + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +func RandomizeString(length int) string { + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, length) + for i := range b { + b[i] = letterBytes[seededRand.Intn(len(letterBytes))] + } + return string(b) +} + +func TestMain(m *testing.M) { + log.Println("CLI integration tests started") + // Run all tests + exitVal := m.Run() + log.Println("CLI integration tests done") + os.Exit(exitVal) +} + +func createASTIntegrationTestCommand() *cobra.Command { + log.Println("Reading env variables") + viper.AutomaticEnv() + viper.AddConfigPath(".") + viper.SetConfigName("config") + viper.SetConfigType("env") + _ = viper.ReadInConfig() + + viper.SetDefault(astSchema, "http") + viper.SetDefault(astHost, "localhost") + viper.SetDefault(astPort, "80") + viper.SetDefault(scansPath, "scans") + viper.SetDefault(projectsPath, "projects") + viper.SetDefault(uploadsPath, "uploads") + viper.SetDefault(resultsPath, "results") + + schema := viper.GetString(astSchema) + host := viper.GetString(astHost) + port := viper.GetString(astPort) + ast := fmt.Sprintf("%s://%s:%s/api", schema, host, port) + + scans := viper.GetString(scansPath) + uploads := viper.GetString(uploadsPath) + projects := viper.GetString(projectsPath) + results := viper.GetString(resultsPath) + + scansURL := fmt.Sprintf("%s/%s", ast, scans) + uploadsURL := fmt.Sprintf("%s/%s", ast, uploads) + projectsURL := fmt.Sprintf("%s/%s", ast, projects) + resultsURL := fmt.Sprintf("%s/%s", ast, results) + + scansWrapper := wrappers.NewHTTPScansWrapper(scansURL) + uploadsWrapper := wrappers.NewUploadsHTTPWrapper(uploadsURL) + projectsWrapper := wrappers.NewHTTPProjectsWrapper(projectsURL) + resultsWrapper := wrappers.NewHTTPResultsWrapper(resultsURL) + + return commands.NewAstCLI(scansWrapper, uploadsWrapper, projectsWrapper, resultsWrapper) +} + +func execute(cmd *cobra.Command, args ...string) error { + cmd.SetArgs(args) + return cmd.Execute() +} diff --git a/test/integration/scan_inc_payload.json b/test/integration/scan_inc_payload.json new file mode 100644 index 000000000..8c50ac79f --- /dev/null +++ b/test/integration/scan_inc_payload.json @@ -0,0 +1,22 @@ +{ + "config": [ + { + "type": "sast", + "value": { + "engineConfiguration": " url to the engine configuration file", + "extensions": " url to the extensions file", + "incremental": "true", + "presetName": "Checkmarx Default" + } + } + ], + "tags": ["s1","s2","s4"], + "project": { + "handler": { + "url": "http://minio:9000/uploads/2020_03_22_18_30_51.595_Sources.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ast%2F20200322%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200322T183051Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=00dc54dede9eb8d476df2b43e599cae14f8853930d0c387f1f4164215e7dd787" + }, + "id": "inc_scan", + "type": "upload" + }, + "scanID": "" +} \ No newline at end of file diff --git a/test/integration/scan_payload.json b/test/integration/scan_payload.json new file mode 100644 index 000000000..26c23ecee --- /dev/null +++ b/test/integration/scan_payload.json @@ -0,0 +1,22 @@ +{ + "config": [ + { + "type": "sast", + "value": { + "engineConfiguration": " url to the engine configuration file", + "extensions": " url to the extensions file", + "incremental": "false", + "presetName": "Checkmarx Default" + } + } + ], + "tags": ["s1","s2","s3"], + "project": { + "handler": { + "url": "http://minio:9000/uploads/2020_03_22_18_30_51.595_Sources.zip?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ast%2F20200322%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200322T183051Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=00dc54dede9eb8d476df2b43e599cae14f8853930d0c387f1f4164215e7dd787" + }, + "id": "inc_scan", + "type": "upload" + }, + "scanID": "" +} \ No newline at end of file diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go new file mode 100644 index 000000000..5a4fd50c8 --- /dev/null +++ b/test/integration/scan_test.go @@ -0,0 +1,162 @@ +// +build integration + +package integration + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + scansRESTApi "github.com/checkmarxDev/scans/api/v1/rest/scans" + "gotest.tools/assert/cmp" + "io/ioutil" + "log" + "strconv" + "testing" + "time" + + "github.com/spf13/viper" + "gotest.tools/assert" +) + +func TestScansE2E(t *testing.T) { + viper.SetDefault("TEST_FULL_SCAN_WAIT_COMPLETED_SECONDS", "120") + fullScanWaitTime := viper.GetInt("TEST_FULL_SCAN_WAIT_COMPLETED_SECONDS") + viper.SetDefault("TEST_INC_SCAN_WAIT_COMPLETED_SECONDS", "60") + incScanWaitTime := viper.GetInt("TEST_INC_SCAN_WAIT_COMPLETED_SECONDS") + + scanID := createScanSourcesFile(t) + log.Printf("Waiting %d seconds for the full scan to complete...\n", fullScanWaitTime) + // Wait for the scan to finish. See it's completed successfully + scanCompletedCh := make(chan bool, 1) + pollScanUntilStatus(t, scanID, scanCompletedCh, scansRESTApi.ScanCompleted, fullScanWaitTime, 5) + scanCompleted := <-scanCompletedCh + assert.Assert(t, scanCompleted, "Full scan should be completed") + + incScanID := createIncScan(t) + log.Printf("Waiting %d seconds for the incremental scan to complete...\n", incScanWaitTime) + // Wait for the inc scan to finish. See it's completed successfully + incScanCompletedCh := make(chan bool, 1) + pollScanUntilStatus(t, incScanID, incScanCompletedCh, scansRESTApi.ScanCompleted, incScanWaitTime, 5) + incScanCompleted := <-incScanCompletedCh + assert.Assert(t, incScanCompleted, "Incremental scan should be completed") + + getAllScans(t) + getScansTags(t) +} + +func createScanSourcesFile(t *testing.T) string { + // Create a full scan + b := bytes.NewBufferString("") + createCommand := createASTIntegrationTestCommand() + createCommand.SetOut(b) + err := execute(createCommand, "-v", "scan", "create", "--inputFile", "scan_payload.json", "--sources", "sources.zip") + assert.NilError(t, err, "Creating a scan should pass") + // Read response from buffer + var createdScanJSON []byte + createdScanJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading scan response JSON should pass") + createdScan := scansRESTApi.ScanResponseModel{} + err = json.Unmarshal(createdScanJSON, &createdScan) + assert.NilError(t, err, "Parsing scan response JSON should pass") + assert.Assert(t, createdScan.Status == scansRESTApi.ScanCreated) + log.Printf("Scan ID %s created in test", createdScan.ID) + return createdScan.ID +} + +func deleteScan(t *testing.T) { + +} + +func getAllScans(t *testing.T) { + b := bytes.NewBufferString("") + getAllCommand := createASTIntegrationTestCommand() + getAllCommand.SetOut(b) + var limit uint64 = 40 + var offset uint64 = 0 + l := strconv.FormatUint(limit, 10) + o := strconv.FormatUint(offset, 10) + err := execute(getAllCommand, "-v", "scan", "get-all", "--limit", l, "--offset", o) + assert.NilError(t, err, "Getting all scans should pass") + // Read response from buffer + var getAllJSON []byte + getAllJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading all scans response JSON should pass") + allScans := scansRESTApi.SlicedScansResponseModel{} + err = json.Unmarshal(getAllJSON, &allScans) + assert.NilError(t, err, "Parsing all scans response JSON should pass") + assert.Assert(t, uint64(allScans.Limit) == limit, fmt.Sprintf("limit should be %d", limit)) + assert.Assert(t, uint64(allScans.Offset) == offset, fmt.Sprintf("offset should be %d", offset)) + assert.Assert(t, allScans.TotalCount == 2, "Total should be 2") + assert.Assert(t, len(allScans.Scans) == 2, "Total should be 2") +} + +func getScanByID(t *testing.T, scanID string) *scansRESTApi.ScanResponseModel { + getBuffer := bytes.NewBufferString("") + getCommand := createASTIntegrationTestCommand() + getCommand.SetOut(getBuffer) + err := execute(getCommand, "-v", "scan", "get", scanID) + assert.NilError(t, err) + // Read response from buffer + var getScanJSON []byte + getScanJSON, err = ioutil.ReadAll(getBuffer) + assert.NilError(t, err, "Reading scan response JSON should pass") + getScan := scansRESTApi.ScanResponseModel{} + err = json.Unmarshal(getScanJSON, &getScan) + assert.NilError(t, err, "Parsing scan response JSON should pass") + assert.Assert(t, cmp.Equal(getScan.ID, scanID)) + return &getScan +} + +func getScansTags(t *testing.T) { + b := bytes.NewBufferString("") + tagsCommand := createASTIntegrationTestCommand() + tagsCommand.SetOut(b) + err := execute(tagsCommand, "-v", "scan", "tags") + assert.NilError(t, err, "Getting tags should pass") + // Read response from buffer + var tagsJSON []byte + tagsJSON, err = ioutil.ReadAll(b) + assert.NilError(t, err, "Reading tags JSON should pass") + tags := []string{} + err = json.Unmarshal(tagsJSON, &tags) + assert.NilError(t, err, "Parsing tags JSON should pass") + assert.Assert(t, tags != nil) + assert.Assert(t, len(tags) == 4) +} + +func createIncScan(t *testing.T) string { + // Create an incremental scan + incBuff := bytes.NewBufferString("") + createIncCommand := createASTIntegrationTestCommand() + createIncCommand.SetOut(incBuff) + err := execute(createIncCommand, "-v", "scan", "create", "--inputFile", "scan_inc_payload.json", "--sources", "sources_inc.zip") + assert.NilError(t, err, "Creating an incremental scan should pass") + // Read response from buffer + var createdIncScanJSON []byte + createdIncScanJSON, err = ioutil.ReadAll(incBuff) + assert.NilError(t, err, "Reading incremental scan response JSON should pass") + createdIncScan := scansRESTApi.ScanResponseModel{} + err = json.Unmarshal(createdIncScanJSON, &createdIncScan) + assert.NilError(t, err, "Parsing incremental scan response JSON should pass") + assert.Assert(t, createdIncScan.Status == scansRESTApi.ScanCreated) + return createdIncScan.ID +} + +func pollScanUntilStatus(t *testing.T, scanID string, ch chan<- bool, requiredStatus scansRESTApi.ScanStatus, timeout, sleep int) { + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + for { + log.Printf("Polling scan %s\n", scanID) + scan := getScanByID(t, scanID) + if string(scan.Status) == string(requiredStatus) { + ch <- true + return + } else { + time.Sleep(time.Duration(sleep) * time.Second) + } + } + + <-ctx.Done() + ch <- false +} diff --git a/test/integration/sources.zip b/test/integration/sources.zip new file mode 100644 index 000000000..f1443c917 Binary files /dev/null and b/test/integration/sources.zip differ diff --git a/test/integration/sources_inc.zip b/test/integration/sources_inc.zip new file mode 100644 index 000000000..88ffdedbf Binary files /dev/null and b/test/integration/sources_inc.zip differ