diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dff06176a..dbe195a73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,9 +60,16 @@ jobs: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew --version - name: Install gon - run: brew install Bearer/tap/gon - - name: install docker - run: brew install docker + run: | + brew install Bearer/tap/gon + - name: Setup Docker on macOS + if: inputs.dev == false + uses: douglascamata/setup-docker-macos-action@v1-alpha + - name: Test docker + if: inputs.dev == false + run: | + docker version + docker info - name: Login to Docker Hub if: inputs.dev == false uses: docker/login-action@dd4fa0671be5250ee6f50aedf4cb05514abda2c7 #v1 diff --git a/.gitignore b/.gitignore index 628b8837c..ef37eb5d7 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,8 @@ override.tf.json # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan # example: *tfplan*dist/ -/dist \ No newline at end of file +/dist + +# Ignore CLI configuration files and installation log files +**/colima-Darwin-x86_64 +**/install.log \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml index ba0a3c767..db742e170 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,6 +12,7 @@ linters-settings: - github.com/spf13/cobra - github.com/pkg/errors - github.com/google + - github.com/MakeNowJust/heredoc dupl: threshold: 500 funlen: diff --git a/cmd/main.go b/cmd/main.go index 8ce4a8aa5..3383acc8e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -51,6 +51,7 @@ func main() { policyEvaluationPath := viper.GetString(params.PolicyEvaluationPathKey) sastMetadataPath := viper.GetString(params.SastMetadataPathKey) accessManagementPath := viper.GetString(params.AccessManagementPathKey) + byorPath := viper.GetString(params.ByorPathKey) scansWrapper := wrappers.NewHTTPScansWrapper(scans) resultsPdfReportsWrapper := wrappers.NewResultsPdfReportsHTTPWrapper(resultsPdfPath) @@ -81,6 +82,7 @@ func main() { policyWrapper := wrappers.NewHTTPPolicyWrapper(policyEvaluationPath) sastMetadataWrapper := wrappers.NewSastIncrementalHTTPWrapper(sastMetadataPath) accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) + byorWrapper := wrappers.NewByorHTTPWrapper(byorPath) astCli := commands.NewAstCLI( applicationsWrapper, @@ -112,6 +114,7 @@ func main() { policyWrapper, sastMetadataWrapper, accessManagementWrapper, + byorWrapper, ) exitListener() err = astCli.Execute() diff --git a/go.mod b/go.mod index d7e07d698..5f92424a8 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/checkmarx/ast-cli -go 1.22.2 +go 1.22.3 require ( github.com/MakeNowJust/heredoc v1.0.0 @@ -14,6 +14,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.9.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 golang.org/x/crypto v0.22.0 golang.org/x/text v0.14.0 @@ -21,6 +22,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -28,13 +30,13 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.9.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/atomic v1.9.0 // indirect @@ -43,5 +45,4 @@ require ( golang.org/x/sys v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - ) diff --git a/go.sum b/go.sum index 4d6ca44d0..71fbc614c 100644 --- a/go.sum +++ b/go.sum @@ -86,7 +86,6 @@ golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/internal/commands/.scripts/integration_up.sh b/internal/commands/.scripts/integration_up.sh index 7b16af05d..2168c7ff3 100755 --- a/internal/commands/.scripts/integration_up.sh +++ b/internal/commands/.scripts/integration_up.sh @@ -13,8 +13,8 @@ rm -rf ScaResolver-linux64.tar.gz go test \ -tags integration \ -v \ - -timeout 90m \ - -coverpkg github.com/checkmarx/ast-cli/internal/commands,github.com/checkmarx/ast-cli/internal/wrappers \ + -timeout 210m \ + -coverpkg github.com/checkmarx/ast-cli/internal/commands,github.com/checkmarx/ast-cli/internal/services,github.com/checkmarx/ast-cli/internal/wrappers \ -coverprofile cover.out \ github.com/checkmarx/ast-cli/test/integration diff --git a/internal/commands/.scripts/up.sh b/internal/commands/.scripts/up.sh index 85ac378ca..76a800b44 100755 --- a/internal/commands/.scripts/up.sh +++ b/internal/commands/.scripts/up.sh @@ -4,4 +4,4 @@ wget https://sca-downloads.s3.amazonaws.com/cli/latest/ScaResolver-linux64.tar.g tar -xzvf ScaResolver-linux64.tar.gz -C /tmp rm -rf ScaResolver-linux64.tar.gz # ignore mock and wrappers packages, as they checked by integration tests -go test $(go list ./... | grep -v "mock" | grep -v "wrappers" | grep -v "bitbucketserver" | grep -v "logger") -coverprofile cover.out +go test $(go list ./... | grep -v "mock" | grep -v "wrappers" | grep -v "bitbucketserver" | grep -v "logger") -timeout 940.000s -coverprofile cover.out diff --git a/internal/commands/groups.go b/internal/commands/groups.go index 74d82ccfb..e78f554cd 100644 --- a/internal/commands/groups.go +++ b/internal/commands/groups.go @@ -2,110 +2,25 @@ package commands import ( "encoding/json" - "strings" + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" commonParams "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" ) -const accessManagementEnabled = "ACCESS_MANAGEMENT_ENABLED" // feature flag - -func createGroupsMap(groupsStr string, groupsWrapper wrappers.GroupsWrapper) ([]*wrappers.Group, error) { - groups := strings.Split(groupsStr, ",") - var groupsMap []*wrappers.Group - var groupsNotFound []string - for _, group := range groups { - if len(group) > 0 { - groupsFromEnv, err := groupsWrapper.Get(group) - if err != nil { - groupsNotFound = append(groupsNotFound, group) - } else { - findGroup := findGroupByName(groupsFromEnv, group) - if findGroup != nil && findGroup.Name != "" { - groupsMap = append(groupsMap, findGroup) - } else { - groupsNotFound = append(groupsNotFound, group) - } - } - } - } - if len(groupsNotFound) > 0 { - return nil, errors.Errorf("%s: %v", failedFindingGroup, groupsNotFound) - } - return groupsMap, nil -} - -func findGroupByName(groups []wrappers.Group, name string) *wrappers.Group { - for i := 0; i < len(groups); i++ { - if groups[i].Name == name { - return &groups[i] - } - } - return nil -} - func updateGroupValues(input *[]byte, cmd *cobra.Command, groupsWrapper wrappers.GroupsWrapper) ([]*wrappers.Group, error) { groupListStr, _ := cmd.Flags().GetString(commonParams.GroupList) - groups, err := createGroupsMap(groupListStr, groupsWrapper) + groups, err := services.CreateGroupsMap(groupListStr, groupsWrapper) if err != nil { return groups, err } - if !wrappers.FeatureFlags[accessManagementEnabled] { + if !wrappers.FeatureFlags[featureFlagsConstants.AccessManagementEnabled] { var info map[string]interface{} _ = json.Unmarshal(*input, &info) - info["groups"] = getGroupIds(groups) + info["groups"] = services.GetGroupIds(groups) *input, _ = json.Marshal(info) } return groups, nil } -func getGroupsForRequest(groups []*wrappers.Group) []string { - if !wrappers.FeatureFlags[accessManagementEnabled] { - return getGroupIds(groups) - } - return nil -} -func getGroupIds(groups []*wrappers.Group) []string { - var groupIds []string - for _, group := range groups { - groupIds = append(groupIds, group.ID) - } - return groupIds -} - -func assignGroupsToProjectNewAccessManagement(projectID string, projectName string, groups []*wrappers.Group, - accessManagement wrappers.AccessManagementWrapper) error { - if !wrappers.FeatureFlags[accessManagementEnabled] { - return nil - } - groupsAssignedToTheProject, err := accessManagement.GetGroups(projectID) - if err != nil { - return err - } - groupsToAssign := getGroupsToAssign(groups, groupsAssignedToTheProject) - if len(groupsToAssign) == 0 { - return nil - } - - err = accessManagement.CreateGroupsAssignment(projectID, projectName, groupsToAssign) - if err != nil { - return err - } - return nil -} - -func getGroupsToAssign(receivedGroups, existingGroups []*wrappers.Group) []*wrappers.Group { - var groupsToAssign []*wrappers.Group - var groupsMap = make(map[string]bool) - for _, existingGroup := range existingGroups { - groupsMap[existingGroup.ID] = true - } - for _, receivedGroup := range receivedGroups { - find := groupsMap[receivedGroup.ID] - if !find { - groupsToAssign = append(groupsToAssign, receivedGroup) - } - } - return groupsToAssign -} diff --git a/internal/commands/project.go b/internal/commands/project.go index fba5ed9c2..7e69998a6 100644 --- a/internal/commands/project.go +++ b/internal/commands/project.go @@ -6,13 +6,13 @@ import ( "strings" "time" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" - "github.com/MakeNowJust/heredoc" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/logger" commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/services" "github.com/spf13/viper" "github.com/pkg/errors" @@ -22,18 +22,13 @@ import ( ) const ( - failedCreatingProj = "Failed creating a project" - failedUpdatingProj = "Failed updating a project" - failedProjectApplicationAssociation = "Failed association project to application" - failedGettingProj = "Failed getting a project" - failedDeletingProj = "Failed deleting a project" - failedGettingBranches = "Failed getting branches for project" - failedFindingGroup = "Failed finding groups" - projOriginLevel = "Project" - repoConfKey = "scan.handler.git.repository" - sshConfKey = "scan.handler.git.sshKey" - mandatoryRepoURLError = "flag --repo-url is mandatory when --ssh-key is provided" - invalidRepoURL = "provided repository url doesn't need a key. Make sure you are defining the right repository or remove the flag --ssh-key" + failedDeletingProj = "Failed deleting a project" + failedGettingBranches = "Failed getting branches for project" + projOriginLevel = "Project" + repoConfKey = "scan.handler.git.repository" + sshConfKey = "scan.handler.git.sshKey" + mandatoryRepoURLError = "flag --repo-url is mandatory when --ssh-key is provided" + invalidRepoURL = "provided repository url doesn't need a key. Make sure you are defining the right repository or remove the flag --ssh-key" ) var ( @@ -214,11 +209,12 @@ func updateProjectRequestValues(input *[]byte, cmd *cobra.Command) error { projectName, _ := cmd.Flags().GetString(commonParams.ProjectName) mainBranch, _ := cmd.Flags().GetString(commonParams.MainBranchFlag) _ = json.Unmarshal(*input, &info) - if projectName != "" { - info["name"] = projectName - } else { - return errors.Errorf("Project name is required") + if projectName == "" { + return errors.Errorf(errorConstants.ProjectNameIsRequired) } + + info["name"] = projectName + if mainBranch != "" { info["mainBranch"] = mainBranch } @@ -233,26 +229,21 @@ func runCreateProjectCommand( accessManagementWrapper wrappers.AccessManagementWrapper, ) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { - applicationName, err := cmd.Flags().GetString(commonParams.ApplicationName) - if err != nil { - return err - } - + applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName) var applicationID []string - if applicationName != "" { application, getAppErr := getApplication(applicationName, applicationsWrapper) if getAppErr != nil { return getAppErr } if application == nil { - return errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + return errors.Errorf(errorConstants.ApplicationDoesntExistOrNoPermission) } applicationID = []string{application.ID} } var input = []byte("{}") - err = updateProjectRequestValues(&input, cmd) + err := updateProjectRequestValues(&input, cmd) if err != nil { return err } @@ -272,26 +263,26 @@ func runCreateProjectCommand( // Try to parse to a project model in order to manipulate the request payload err = json.Unmarshal(input, &projModel) if err != nil { - return errors.Wrapf(err, "%s: Input in bad format", failedCreatingProj) + return errors.Wrapf(err, "%s: Input in bad format", services.FailedCreatingProj) } var payload []byte payload, _ = json.Marshal(projModel) logger.PrintIfVerbose(fmt.Sprintf("Payload to projects service: %s\n", string(payload))) projResponseModel, errorModel, err = projectsWrapper.Create(&projModel) if err != nil { - return errors.Wrapf(err, "%s", failedCreatingProj) + return errors.Wrapf(err, "%s", services.FailedCreatingProj) } // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedCreatingProj, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, services.FailedCreatingProj, errorModel.Code, errorModel.Message) } else if projResponseModel != nil { err = printByFormat(cmd, toProjectView(*projResponseModel)) if err != nil { - return errors.Wrapf(err, "%s", failedCreatingProj) + return errors.Wrapf(err, "%s", services.FailedCreatingProj) } } - err = assignGroupsToProjectNewAccessManagement(projResponseModel.ID, projResponseModel.Name, groups, accessManagementWrapper) + err = services.AssignGroupsToProjectNewAccessManagement(projResponseModel.ID, projResponseModel.Name, groups, accessManagementWrapper) if err != nil { return err } @@ -413,7 +404,7 @@ func runListProjectsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd * // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) } else if allProjectsModel != nil && allProjectsModel.Projects != nil { err = printByFormat(cmd, toProjectViews(allProjectsModel.Projects)) if err != nil { @@ -431,15 +422,15 @@ func runGetProjectByIDCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd var err error projectID, _ := cmd.Flags().GetString(commonParams.ProjectIDFlag) if projectID == "" { - return errors.Errorf("%s: Please provide a project ID", failedGettingProj) + return errors.Errorf("%s: Please provide a project ID", services.FailedGettingProj) } projectResponseModel, errorModel, err = projectsWrapper.GetByID(projectID) if err != nil { - return errors.Wrapf(err, "%s", failedGettingProj) + return errors.Wrapf(err, "%s", services.FailedGettingProj) } // Checking the response if errorModel != nil { - return errors.Errorf("%s: CODE: %d, %s", failedGettingProj, errorModel.Code, errorModel.Message) + return errors.Errorf("%s: CODE: %d, %s", services.FailedGettingProj, errorModel.Code, errorModel.Message) } else if projectResponseModel != nil { err = printByFormat(cmd, toProjectView(*projectResponseModel)) if err != nil { @@ -504,7 +495,7 @@ func runDeleteProjectCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd } // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedDeletingProj, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedDeletingProj, errorModel.Code, errorModel.Message) } return nil } diff --git a/internal/commands/project_test.go b/internal/commands/project_test.go index 347e5e937..ac3f13382 100644 --- a/internal/commands/project_test.go +++ b/internal/commands/project_test.go @@ -5,12 +5,11 @@ package commands import ( "testing" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "github.com/checkmarx/ast-cli/internal/wrappers/utils" "gotest.tools/assert" - - "github.com/checkmarx/ast-cli/internal/commands/util" ) func TestProjectHelp(t *testing.T) { @@ -31,22 +30,22 @@ func TestProjectCreate_ExistingApplication_CreateProjectUnderApplicationSuccessf func TestProjectCreate_ExistingApplicationWithNoPermission_FailToCreateProject(t *testing.T) { err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.NoPermissionApp) - assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) + assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission) } func TestProjectCreate_OnReceivingHttpBadRequestStatusCode_FailedToCreateScan(t *testing.T) { - err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeHTTPStatusBadRequest) - assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) + err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeBadRequest400) + assert.Assert(t, err.Error() == errorConstants.FailedToGetApplication) } func TestProjectCreate_OnReceivingHttpInternalServerErrorStatusCode_FailedToCreateScan(t *testing.T) { - err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeHTTPStatusInternalServerError) - assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) + err := execCmdNotNilAssertion(t, "project", "create", "--project-name", "test_project", "--application-name", mock.FakeInternalServerError500) + assert.Assert(t, err.Error() == errorConstants.FailedToGetApplication) } func TestRunCreateProjectCommandWithNoInput(t *testing.T) { err := execCmdNotNilAssertion(t, "project", "create") - assert.Assert(t, err.Error() == "Project name is required") + assert.Assert(t, err.Error() == errorConstants.ProjectNameIsRequired) } func TestRunCreateProjectCommandWithInvalidFormat(t *testing.T) { @@ -178,7 +177,7 @@ func TestCreateProjectWrongSSHKeyPath(t *testing.T) { "open dummy_key: no such file or directory", } - assert.Assert(t, util.Contains(expectedMessages, err.Error())) + assert.Assert(t, utils.Contains(expectedMessages, err.Error())) } func TestCreateProjectWithSSHKey(t *testing.T) { diff --git a/internal/commands/result.go b/internal/commands/result.go index b2b267825..fe0f9076c 100644 --- a/internal/commands/result.go +++ b/internal/commands/result.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "text/template" @@ -17,6 +18,7 @@ import ( "github.com/checkmarx/ast-cli/internal/commands/policymanagement" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/logger" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -165,12 +167,32 @@ func NewResultsCommand( showResultCmd := resultShowSubCommand(resultsWrapper, scanWrapper, resultsSbomWrapper, resultsPdfReportsWrapper, risksOverviewWrapper, policyWrapper) codeBashingCmd := resultCodeBashing(codeBashingWrapper) bflResultCmd := resultBflSubCommand(bflWrapper) + exitCodeSubcommand := exitCodeSubCommand(scanWrapper) resultCmd.AddCommand( - showResultCmd, bflResultCmd, codeBashingCmd, + showResultCmd, bflResultCmd, codeBashingCmd, exitCodeSubcommand, ) return resultCmd } +func exitCodeSubCommand(scanWrapper wrappers.ScansWrapper) *cobra.Command { + exitCodeCmd := &cobra.Command{ + Use: "exit-code", + Short: "Get exit code and details of a scan", + Long: "The exit-code command enables you to get the exit code and failure details of a requested scan in Checkmarx One.", + Example: heredoc.Doc( + ` + $ cx results exit-code --scan-id --scan-types + `, + ), + RunE: runGetExitCodeCommand(scanWrapper), + } + + exitCodeCmd.PersistentFlags().String(commonParams.ScanIDFlag, "", "Scan ID") + exitCodeCmd.PersistentFlags().String(commonParams.ScanTypes, "", "Scan types") + + return exitCodeCmd +} + func resultShowSubCommand( resultsWrapper wrappers.ResultsWrapper, scanWrapper wrappers.ScansWrapper, @@ -255,6 +277,110 @@ func resultBflSubCommand(bflWrapper wrappers.BflWrapper) *cobra.Command { return resultBflCmd } +func runGetExitCodeCommand(scanWrapper wrappers.ScansWrapper) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + scanID, _ := cmd.Flags().GetString(commonParams.ScanIDFlag) + if scanID == "" { + return errors.New(errorConstants.ScanIDRequired) + } + scanTypesFlagValue, _ := cmd.Flags().GetString(commonParams.ScanTypes) + results, err := GetScannerResults(scanWrapper, scanID, scanTypesFlagValue) + if err != nil { + return err + } + + if len(results) == 0 { + return nil + } + + return printer.Print(cmd.OutOrStdout(), results, printer.FormatIndentedJSON) + } +} + +func GetScannerResults(scanWrapper wrappers.ScansWrapper, scanID, scanTypesFlagValue string) ([]ScannerResponse, error) { + scanResponseModel, errorModel, err := scanWrapper.GetByID(scanID) + if err != nil { + return nil, errors.Wrapf(err, "%s", failedGetting) + } + if errorModel != nil { + return nil, errors.Errorf("%s: CODE: %d, %s", failedGettingScan, errorModel.Code, errorModel.Message) + } + results := getScannerResponse(scanTypesFlagValue, scanResponseModel) + return results, nil +} + +func getScannerResponse(scanTypesFlagValue string, scanResponseModel *wrappers.ScanResponseModel) []ScannerResponse { + var results []ScannerResponse + + if scanResponseModel.Status == wrappers.ScanCanceled || + scanResponseModel.Status == wrappers.ScanRunning || + scanResponseModel.Status == wrappers.ScanQueued || + scanResponseModel.Status == wrappers.ScanCompleted { + result := ScannerResponse{ + ScanID: scanResponseModel.ID, + Status: string(scanResponseModel.Status), + } + results = append(results, result) + return results + } + + if scanTypesFlagValue == "" { + results = createAllFailedScannersResponse(scanResponseModel) + } else { + scanTypes := sanitizeScannerNames(scanTypesFlagValue) + results = createRequestedScannersResponse(scanTypes, scanResponseModel) + } + + return results +} + +func createRequestedScannersResponse(scanTypes map[string]string, scanResponseModel *wrappers.ScanResponseModel) []ScannerResponse { + var results []ScannerResponse + for i := range scanResponseModel.StatusDetails { + if _, ok := scanTypes[scanResponseModel.StatusDetails[i].Name]; ok { + results = append(results, createScannerResponse(&scanResponseModel.StatusDetails[i])) + } + } + return results +} + +func createAllFailedScannersResponse(scanResponseModel *wrappers.ScanResponseModel) []ScannerResponse { + var results []ScannerResponse + for i := range scanResponseModel.StatusDetails { + if scanResponseModel.StatusDetails[i].Status == wrappers.ScanFailed { + results = append(results, createScannerResponse(&scanResponseModel.StatusDetails[i])) + } + } + return results +} + +func sanitizeScannerNames(scanTypes string) map[string]string { + scanTypeSlice := strings.Split(scanTypes, ",") + scanTypeMap := make(map[string]string) + for i := range scanTypeSlice { + lowered := strings.ToLower(scanTypeSlice[i]) + scanTypeMap[lowered] = lowered + } + + return scanTypeMap +} + +func createScannerResponse(statusDetails *wrappers.StatusInfo) ScannerResponse { + return ScannerResponse{ + Name: statusDetails.Name, + Status: statusDetails.Status, + Details: statusDetails.Details, + ErrorCode: stringifyErrorCode(statusDetails.ErrorCode), + } +} + +func stringifyErrorCode(errorCode int) string { + if errorCode == 0 { + return "" + } + return strconv.Itoa(errorCode) +} + func runGetBestFixLocationCommand(bflWrapper wrappers.BflWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var bflResponseModel *wrappers.BFLResponseModel @@ -783,7 +909,7 @@ func CreateScanReport( return err } if !scanPending { - results, err = ReadResults(resultsWrapper, scan, params) + results, err = ReadResults(resultsWrapper, scan, params, false) if err != nil { return err } @@ -904,6 +1030,9 @@ func createReport(format, resultsPdfReportsWrapper wrappers.ResultsPdfWrapper, useSCALocalFlow bool, retrySBOM int) error { + if printer.IsFormat(format, printer.FormatIndentedJSON) { + return nil + } if printer.IsFormat(format, printer.FormatSarif) && isValidScanStatus(summary.Status, printer.FormatSarif) { sarifRpt := createTargetName(targetFile, targetPath, printer.FormatSarif) return exportSarifResults(sarifRpt, results) @@ -988,6 +1117,7 @@ func ReadResults( resultsWrapper wrappers.ResultsWrapper, scan *wrappers.ScanResponseModel, params map[string]string, + isThresholdCheck bool, ) (results *wrappers.ScanResultsCollection, err error) { var resultsModel *wrappers.ScanResultsCollection var errorModel *wrappers.WebError @@ -1003,9 +1133,11 @@ func ReadResults( } if resultsModel != nil { - resultsModel, err = enrichScaResults(resultsWrapper, scan, params, resultsModel) - if err != nil { - return nil, err + if !isThresholdCheck { + resultsModel, err = enrichScaResults(resultsWrapper, scan, params, resultsModel) + if err != nil { + return nil, err + } } resultsModel.ScanID = scan.ID @@ -1020,7 +1152,7 @@ func enrichScaResults( params map[string]string, resultsModel *wrappers.ScanResultsCollection, ) (*wrappers.ScanResultsCollection, error) { - if util.Contains(scan.Engines, commonParams.ScaType) { + if slices.Contains(scan.Engines, commonParams.ScaType) { // Get additional information to enrich sca results scaPackageModel, errorModel, err := resultsWrapper.GetAllResultsPackageByScanID(params) if errorModel != nil { @@ -1044,7 +1176,7 @@ func enrichScaResults( } _, sastRedundancy := params[commonParams.SastRedundancyFlag] - if util.Contains(scan.Engines, commonParams.SastType) && sastRedundancy { + if slices.Contains(scan.Engines, commonParams.SastType) && sastRedundancy { // Compute SAST results redundancy resultsModel = ComputeRedundantSastResults(resultsModel) } @@ -1071,6 +1203,7 @@ func exportSarifResults(targetFile string, results *wrappers.ScanResultsCollecti func exportGlSastResults(targetFile string, results *wrappers.ScanResultsCollection, summary *wrappers.ResultSummary) error { log.Println("Creating gl-sast Report: ", targetFile) var glSast = new(wrappers.GlSastResultsCollection) + glSast.Vulnerabilities = []wrappers.GlVulnerabilities{} err := addScanToGlSastReport(summary, glSast) if err != nil { return errors.Wrapf(err, "%s: failed to add scan to gl sast report", failedListingResults) @@ -2027,3 +2160,11 @@ func filterViolatedRules(policyModel wrappers.PolicyResponseModel) *wrappers.Pol policyModel.Policies = policyModel.Policies[:i] return &policyModel } + +type ScannerResponse struct { + ScanID string `json:"ScanID,omitempty"` + Name string `json:"Name,omitempty"` + Status string `json:"Status,omitempty"` + Details string `json:"Details,omitempty"` + ErrorCode string `json:"ErrorCode,omitempty"` +} diff --git a/internal/commands/result_test.go b/internal/commands/result_test.go index d0ef34997..c687dc223 100644 --- a/internal/commands/result_test.go +++ b/internal/commands/result_test.go @@ -3,11 +3,13 @@ package commands import ( + "encoding/json" "fmt" "os" "testing" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/checkmarx/ast-cli/internal/wrappers/mock" @@ -35,6 +37,162 @@ func TestResultHelp(t *testing.T) { execCmdNilAssertion(t, "help", "results") } +func TestResultsExitCode_CompletedScan_PrintCorrectInfoToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ID: "MOCK", Status: wrappers.ScanCompleted, Engines: []string{params.ScaType, params.SastType, params.KicsType}} + results := getScannerResponse("", &model) + assert.Equal(t, len(results), 1, "") + assert.Equal(t, results[0].ScanID, "MOCK", "") + assert.Equal(t, results[0].Status, wrappers.ScanCompleted, "") +} + +func TestResultsExitCode_OnFailedKicsScanner_PrintCorrectFailedScannerInfoToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-scanner-fail", + Status: wrappers.ScanFailed, + StatusDetails: []wrappers.StatusInfo{ + { + Status: wrappers.ScanFailed, + Name: "kics", + Details: "error message from kics scanner", + ErrorCode: 1234, + }, + {Status: wrappers.ScanFailed, Name: "general", Details: "timeout", ErrorCode: 1234}, + }, + } + + results := getScannerResponse("", &model) + + assert.Equal(t, len(results), 2, "Scanner results should be empty") + assert.Equal(t, results[0].Name, "kics", "") + assert.Equal(t, results[0].ErrorCode, "1234", "") + assert.Equal(t, results[1].Name, "general", "") + assert.Equal(t, results[1].ErrorCode, "1234", "") + assert.Equal(t, results[1].Details, "timeout", "") +} + +func TestResultsExitCode_OnFailedKicsAndScaScanners_PrintCorrectFailedScannersInfoToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-multiple-scanner-fails", + Status: wrappers.ScanFailed, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 2344}, + {Status: wrappers.ScanFailed, Name: "sca", Details: "error message from sca scanner", ErrorCode: 4343}, + {Status: wrappers.ScanFailed, Name: "general", Details: "timeout", ErrorCode: 1234}, + }, + } + + results := getScannerResponse("", &model) + + assert.Equal(t, len(results), 3, "Scanner results should be empty") + assert.Equal(t, results[0].Name, "kics", "") + assert.Equal(t, results[0].ErrorCode, "2344", "") + assert.Equal(t, results[1].Name, "sca", "") + assert.Equal(t, results[1].ErrorCode, "4343", "") + assert.Equal(t, results[2].Name, "general", "") + assert.Equal(t, results[2].ErrorCode, "1234", "") + assert.Equal(t, results[2].Details, "timeout", "") +} + +func TestResultsExitCode_OnRequestedFailedScanner_PrintCorrectFailedScannerInfoToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-multiple-scanner-fails", + Status: wrappers.ScanFailed, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 2344}, + {Status: wrappers.ScanFailed, Name: "sca", Details: "error message from sca scanner", ErrorCode: 4343}, + {Status: wrappers.ScanFailed, Name: "general", Details: "timeout", ErrorCode: 1234}, + }, + } + + results := getScannerResponse("sca", &model) + + assert.Equal(t, len(results), 1, "Scanner results should be empty") + assert.Equal(t, results[0].Name, "sca", "") + assert.Equal(t, results[0].ErrorCode, "4343", "") +} + +func TestResultsExitCode_OnPartialScan_PrintOnlyFailedScannersInfoToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-sca-fail-partial-id", + Status: wrappers.ScanPartial, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "sca", Details: "error message from sca scanner", ErrorCode: 4343}, + {Status: wrappers.ScanCompleted, Name: "general"}, + }, + } + + results := getScannerResponse("", &model) + + assert.Equal(t, len(results), 1, "Scanner results should be empty") + assert.Equal(t, results[0].Name, "sca", "") + assert.Equal(t, results[0].ErrorCode, "4343", "") +} + +func TestResultsExitCode_OnCanceledScan_PrintOnlyScanIDAndStatusCanceledToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-fail-sast-canceled-id", + Status: wrappers.ScanCanceled, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "general"}, + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 6455}, + }, + } + + results := getScannerResponse("", &model) + + assert.Equal(t, len(results), 1, "Scanner results should be empty") + assert.Equal(t, results[0].ScanID, "fake-scan-id-kics-fail-sast-canceled-id", "") + assert.Equal(t, results[0].Status, wrappers.ScanCanceled, "") +} + +func TestResultsExitCode_OnCanceledScanWithRequestedSuccessfulScanner_PrintOnlyScanIDAndStatusCanceledToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-fail-sast-canceled-id", + Status: wrappers.ScanCanceled, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "general"}, + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 6455}, + }, + } + + results := getScannerResponse("sast", &model) + + assert.Equal(t, len(results), 1, "Scanner results should be empty") + assert.Equal(t, results[0].ScanID, "fake-scan-id-kics-fail-sast-canceled-id", "") + assert.Equal(t, results[0].Status, wrappers.ScanCanceled, "") +} + +func TestResultsExitCode_OnCanceledScanWithRequestedFailedScanner_PrintOnlyScanIDAndStatusCanceledToConsole(t *testing.T) { + model := wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-fail-sast-canceled-id", + Status: wrappers.ScanCanceled, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "general"}, + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 6455}, + }, + } + + results := getScannerResponse("kics", &model) + + assert.Equal(t, len(results), 1, "Scanner results should be empty") + assert.Equal(t, results[0].ScanID, "fake-scan-id-kics-fail-sast-canceled-id", "") + assert.Equal(t, results[0].Status, wrappers.ScanCanceled, "") +} + +func TestResultsExitCode_NoScanIdSent_FailCommandWithError(t *testing.T) { + err := execCmdNotNilAssertion(t, "results", "exit-code") + assert.Equal(t, err.Error(), errorConstants.ScanIDRequired, "Wrong expected error message") +} + +func TestResultsExitCode_OnErrorScan_FailCommandWithError(t *testing.T) { + err := execCmdNotNilAssertion(t, "results", "exit-code", "--scan-id", "fake-error-id") + assert.Equal(t, err.Error(), "Failed showing a scan: fake error message", "Wrong expected error message") +} + func TestRunGetResultsByScanIdSarifFormat(t *testing.T) { execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "sarif") // Remove generated sarif file @@ -371,11 +529,7 @@ func TestRunGetResultsByScanIdGLFormat(t *testing.T) { // Run test for gl-sast report type os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatGL)) } -func TestRunGetResultsByScanIdGLDependencyFormat(t *testing.T) { - execCmdNilAssertion(t, "results", "show", "--scan-id", "MOCK", "--report-format", "gl-dependency") - // Run test for gl-dependency report type - os.Remove(fmt.Sprintf("%s.%s", fileName, printer.FormatGLDependency)) -} + func Test_addPackageInformation(t *testing.T) { var dependencyPath = wrappers.DependencyPath{ID: "test-1"} var dependencyArray = [][]wrappers.DependencyPath{{dependencyPath}} diff --git a/internal/commands/root.go b/internal/commands/root.go index ed879366f..92d301907 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -19,8 +19,6 @@ import ( "github.com/spf13/viper" ) -const ErrorCodeFormat = "%s: CODE: %d, %s\n" - // NewAstCLI Return a Checkmarx One CLI root command to execute func NewAstCLI( applicationsWrapper wrappers.ApplicationsWrapper, @@ -52,6 +50,7 @@ func NewAstCLI( policyWrapper wrappers.PolicyWrapper, sastMetadataWrapper wrappers.SastMetadataWrapper, accessManagementWrapper wrappers.AccessManagementWrapper, + byorWrapper wrappers.ByorWrapper, ) *cobra.Command { // Create the root rootCmd := &cobra.Command{ @@ -74,6 +73,7 @@ func NewAstCLI( }, } + setUpFeatureFlags(featureFlagsWrapper) // Load default flags rootCmd.PersistentFlags().Bool(params.DebugFlag, false, params.DebugUsage) rootCmd.PersistentFlags().String(params.AccessKeyIDFlag, "", params.AccessKeyIDFlagUsage) @@ -106,15 +106,6 @@ func NewAstCLI( _ = cmd.Help() os.Exit(0) } - - if requiredFeatureFlagsCheck(cmd) { - err := wrappers.HandleFeatureFlags(featureFlagsWrapper) - - if err != nil { - fmt.Println(err) - os.Exit(1) - } - } } // Link the environment variable to the CLI argument(s). _ = viper.BindPFlag(params.AccessKeyIDConfigKey, rootCmd.PersistentFlags().Lookup(params.AccessKeyIDFlag)) @@ -162,6 +153,7 @@ func NewAstCLI( accessManagementWrapper, ) projectCmd := NewProjectCommand(applicationsWrapper, projectsWrapper, groupsWrapper, accessManagementWrapper) + resultsCmd := NewResultsCommand( resultsWrapper, scansWrapper, @@ -187,7 +179,14 @@ func NewAstCLI( chatWrapper, policyWrapper, scansWrapper, + projectsWrapper, + uploadsWrapper, + groupsWrapper, + accessManagementWrapper, + applicationsWrapper, + byorWrapper, ) + configCmd := util.NewConfigCommand() triageCmd := NewResultsPredicatesCommand(resultsPredicatesWrapper) @@ -210,16 +209,6 @@ func NewAstCLI( return rootCmd } -func requiredFeatureFlagsCheck(cmd *cobra.Command) bool { - for _, cmdFlag := range wrappers.FeatureFlagsBaseMap { - if cmdFlag.CommandName == cmd.CommandPath() { - return true - } - } - - return false -} - const configFormatString = "%30v: %s" func PrintConfiguration() { @@ -230,11 +219,17 @@ func PrintConfiguration() { } } -func getFilters(cmd *cobra.Command) (map[string]string, error) { - filters, err := cmd.Flags().GetStringSlice(params.FilterFlag) +func setUpFeatureFlags(featureFlagsWrapper wrappers.FeatureFlagsWrapper) { + err := wrappers.HandleFeatureFlags(featureFlagsWrapper) + if err != nil { - return nil, err + fmt.Println(err) + os.Exit(1) } +} + +func getFilters(cmd *cobra.Command) (map[string]string, error) { + filters, _ := cmd.Flags().GetStringSlice(params.FilterFlag) allFilters := make(map[string]string) for _, filter := range filters { filterKeyVal := strings.Split(filter, "=") diff --git a/internal/commands/root_test.go b/internal/commands/root_test.go index e301c4676..e0bbdd18d 100644 --- a/internal/commands/root_test.go +++ b/internal/commands/root_test.go @@ -59,6 +59,7 @@ func createASTTestCommand() *cobra.Command { policyWrapper := &mock.PolicyMockWrapper{} sastMetadataWrapper := &mock.SastMetadataMockWrapper{} accessManagementWrapper := &mock.AccessManagementMockWrapper{} + byorWrapper := &mock.ByorMockWrapper{} return NewAstCLI( applicationWrapper, @@ -90,6 +91,7 @@ func createASTTestCommand() *cobra.Command { policyWrapper, sastMetadataWrapper, accessManagementWrapper, + byorWrapper, ) } diff --git a/internal/commands/scan.go b/internal/commands/scan.go index c3faf073a..8ff68752e 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -14,17 +14,18 @@ import ( "path" "path/filepath" "reflect" - "slices" "strconv" "strings" "time" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" - "github.com/checkmarx/ast-cli/internal/commands/scarealtime" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + "github.com/checkmarx/ast-cli/internal/constants" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" "github.com/checkmarx/ast-cli/internal/logger" + "github.com/checkmarx/ast-cli/internal/services" "github.com/google/shlex" "github.com/google/uuid" "github.com/pkg/errors" @@ -597,214 +598,6 @@ func scanCreateSubCommand( return createScanCmd } -func findProject( - applicationID []string, - projectName string, - cmd *cobra.Command, - projectsWrapper wrappers.ProjectsWrapper, - groupsWrapper wrappers.GroupsWrapper, - accessManagementWrapper wrappers.AccessManagementWrapper, - applicationWrapper wrappers.ApplicationsWrapper, -) (string, error) { - params := make(map[string]string) - params["names"] = projectName - resp, _, err := projectsWrapper.Get(params) - if err != nil { - return "", err - } - - for i := 0; i < len(resp.Projects); i++ { - if resp.Projects[i].Name == projectName { - return updateProject(resp, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, applicationWrapper, projectName, applicationID) - } - } - projectID, err := createProject(projectName, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, applicationWrapper, applicationID) - if err != nil { - return "", err - } - return projectID, nil -} - -func createProject( - projectName string, - cmd *cobra.Command, - projectsWrapper wrappers.ProjectsWrapper, - groupsWrapper wrappers.GroupsWrapper, - accessManagementWrapper wrappers.AccessManagementWrapper, - applicationsWrapper wrappers.ApplicationsWrapper, - applicationID []string, -) (string, error) { - projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList) - projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList) - projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag) - - var projModel = wrappers.Project{} - projModel.Name = projectName - projModel.ApplicationIds = applicationID - - if projectPrivatePackage != "" { - projModel.PrivatePackage, _ = strconv.ParseBool(projectPrivatePackage) - } - projModel.Tags = createTagMap(projectTags) - resp, errorModel, err := projectsWrapper.Create(&projModel) - projectID := "" - if errorModel != nil { - err = errors.Errorf(ErrorCodeFormat, failedCreatingProj, errorModel.Code, errorModel.Message) - } - if err == nil { - projectID = resp.ID - - if len(applicationID) > 0 { - err = verifyApplicationAssociationDone(applicationID, projectID, applicationsWrapper) - if err != nil { - return projectID, err - } - } - - if projectGroups != "" { - err = UpsertProjectGroups(groupsWrapper, &projModel, projectsWrapper, accessManagementWrapper, nil, projectGroups, projectID, projectName) - if err != nil { - return projectID, err - } - } - } - return projectID, err -} - -func verifyApplicationAssociationDone(applicationID []string, projectID string, applicationsWrapper wrappers.ApplicationsWrapper) error { - var applicationRes *wrappers.ApplicationsResponseModel - var err error - params := make(map[string]string) - params["id"] = applicationID[0] - - logger.PrintIfVerbose("polling application until project association done or timeout of 2 min") - start := time.Now() - timeout := 2 * time.Minute - for applicationRes != nil && len(applicationRes.Applications) > 0 && - !slices.Contains(applicationRes.Applications[0].ProjectIds, projectID) { - applicationRes, err = applicationsWrapper.Get(params) - if err != nil { - return err - } else if time.Since(start) < timeout { - return errors.Errorf("%s: %v", failedProjectApplicationAssociation, "timeout of 2 min for association") - } - } - - logger.PrintIfVerbose("application association done successfully") - return nil -} - -func updateProject( - resp *wrappers.ProjectsCollectionResponseModel, - cmd *cobra.Command, - projectsWrapper wrappers.ProjectsWrapper, - groupsWrapper wrappers.GroupsWrapper, - accessManagementWrapper wrappers.AccessManagementWrapper, - applicationsWrapper wrappers.ApplicationsWrapper, - projectName string, - applicationID []string, - -) (string, error) { - var projectID string - var projModel = wrappers.Project{} - projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList) - projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList) - projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag) - for i := 0; i < len(resp.Projects); i++ { - if resp.Projects[i].Name == projectName { - projectID = resp.Projects[i].ID - } - if resp.Projects[i].MainBranch != "" { - projModel.MainBranch = resp.Projects[i].MainBranch - } - if resp.Projects[i].RepoURL != "" { - projModel.RepoURL = resp.Projects[i].RepoURL - } - } - if projectGroups == "" && projectTags == "" && projectPrivatePackage == "" && len(applicationID) == 0 { - logger.PrintIfVerbose("No groups, applicationId or tags to update. Skipping project update.") - return projectID, nil - } - if projectPrivatePackage != "" { - projModel.PrivatePackage, _ = strconv.ParseBool(projectPrivatePackage) - } - projModelResp, errModel, err := projectsWrapper.GetByID(projectID) - if errModel != nil { - err = errors.Errorf(ErrorCodeFormat, failedGettingProj, errModel.Code, errModel.Message) - } - if err != nil { - return "", err - } - projModel.Name = projModelResp.Name - projModel.Groups = projModelResp.Groups - projModel.Tags = projModelResp.Tags - projModel.ApplicationIds = projModelResp.ApplicationIds - - if projectTags != "" { - logger.PrintIfVerbose("Updating project tags") - projModel.Tags = createTagMap(projectTags) - } - if len(applicationID) > 0 { - logger.PrintIfVerbose("Updating project applicationIds") - projModel.ApplicationIds = createApplicationIds(applicationID, projModelResp.ApplicationIds) - } - err = projectsWrapper.Update(projectID, &projModel) - if err != nil { - return "", errors.Errorf("%s: %v", failedUpdatingProj, err) - } - - if len(applicationID) > 0 { - err = verifyApplicationAssociationDone(applicationID, projectID, applicationsWrapper) - if err != nil { - return projectID, err - } - } - - if projectGroups != "" { - err = UpsertProjectGroups(groupsWrapper, &projModel, projectsWrapper, accessManagementWrapper, projModelResp, projectGroups, projectID, projectName) - if err != nil { - return projectID, err - } - } - return projectID, nil -} - -func UpsertProjectGroups(groupsWrapper wrappers.GroupsWrapper, projModel *wrappers.Project, projectsWrapper wrappers.ProjectsWrapper, - accessManagementWrapper wrappers.AccessManagementWrapper, projModelResp *wrappers.ProjectResponseModel, - projectGroups string, projectID string, projectName string) error { - groupsMap, groupErr := createGroupsMap(projectGroups, groupsWrapper) - if groupErr != nil { - return errors.Errorf("%s: %v", failedUpdatingProj, groupErr) - } - - projModel.Groups = getGroupsForRequest(groupsMap) - if projModelResp != nil { - groups := append(getGroupsForRequest(groupsMap), projModelResp.Groups...) - projModel.Groups = groups - } - - err := assignGroupsToProjectNewAccessManagement(projectID, projectName, groupsMap, accessManagementWrapper) - if err != nil { - return err - } - - logger.PrintIfVerbose("Updating project groups") - err = projectsWrapper.Update(projectID, projModel) - if err != nil { - return errors.Errorf("%s: %v", failedUpdatingProj, err) - } - return nil -} - -func createApplicationIds(applicationID, existingApplicationIds []string) []string { - for _, id := range applicationID { - if !util.Contains(existingApplicationIds, id) { - existingApplicationIds = append(existingApplicationIds, id) - } - } - return existingApplicationIds -} - func setupScanTags(input *[]byte, cmd *cobra.Command) { tagListStr, _ := cmd.Flags().GetString(commonParams.TagList) tags := strings.Split(tagListStr, ",") @@ -828,22 +621,6 @@ func setupScanTags(input *[]byte, cmd *cobra.Command) { *input, _ = json.Marshal(info) } -func createTagMap(tagListStr string) map[string]string { - tagsList := strings.Split(tagListStr, ",") - tags := make(map[string]string) - for _, tag := range tagsList { - if len(tag) > 0 { - value := "" - keyValuePair := strings.Split(tag, ":") - if len(keyValuePair) > 1 { - value = keyValuePair[1] - } - tags[keyValuePair[0]] = value - } - } - return tags -} - func setupScanTypeProjectAndConfig( input *[]byte, cmd *cobra.Command, @@ -869,10 +646,7 @@ func setupScanTypeProjectAndConfig( return errors.Errorf("Project name is required") } - applicationName, err := cmd.Flags().GetString(commonParams.ApplicationName) - if err != nil { - return err - } + applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName) var applicationID []string if applicationName != "" { @@ -881,13 +655,13 @@ func setupScanTypeProjectAndConfig( return getAppErr } if application == nil { - return errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + return errors.Errorf(errorConstants.ApplicationDoesntExistOrNoPermission) } applicationID = []string{application.ID} } // We need to convert the project name into an ID - projectID, findProjectErr := findProject( + projectID, findProjectErr := services.FindProject( applicationID, info["project"].(map[string]interface{})["id"].(string), cmd, @@ -899,6 +673,7 @@ func setupScanTypeProjectAndConfig( if findProjectErr != nil { return findProjectErr } + info["project"].(map[string]interface{})["id"] = projectID // Handle the scan configuration var configArr []interface{} @@ -913,12 +688,9 @@ func setupScanTypeProjectAndConfig( ) userScanTypes, _ := cmd.Flags().GetString(commonParams.ScanTypes) // Get the latest scan configuration - resubmitConfig, err = getResubmitConfiguration(scansWrapper, projectID, userScanTypes) - if err != nil { - return err - } + resubmitConfig, _ = getResubmitConfiguration(scansWrapper, projectID, userScanTypes) } else if _, ok := info["config"]; !ok { - err = json.Unmarshal([]byte("[]"), &configArr) + err := json.Unmarshal([]byte("[]"), &configArr) if err != nil { return err } @@ -941,8 +713,13 @@ func setupScanTypeProjectAndConfig( configArr = append(configArr, apiSecConfig) } info["config"] = configArr - *input, err = json.Marshal(info) - return err + var err2 error + *input, err2 = json.Marshal(info) + if err2 != nil { + return err2 + } + + return nil } func getApplication(applicationName string, applicationsWrapper wrappers.ApplicationsWrapper) (*wrappers.Application, error) { @@ -951,7 +728,6 @@ func getApplication(applicationName string, applicationsWrapper wrappers.Applica params["name"] = applicationName resp, err := applicationsWrapper.Get(params) if err != nil { - return nil, err } if resp.Applications != nil && len(resp.Applications) > 0 { @@ -989,7 +765,7 @@ func getResubmitConfiguration(scansWrapper wrappers.ScansWrapper, projectID, use } // Checking the response for errors if errorModel != nil { - return nil, errors.Errorf(ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) + return nil, errors.Errorf(services.ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) } config := allScansModel.Scans[0].Metadata.Configs engines := allScansModel.Scans[0].Engines @@ -1158,7 +934,7 @@ func compressFolder(sourceDir, filter, userIncludeFilter, scaResolver string) (s return "", errors.Wrapf(err, "Cannot source code temp file.") } zipWriter := zip.NewWriter(outputFile) - err = addDirFiles(zipWriter, "", sourceDir, getUserFilters(filter), getIncludeFilters(userIncludeFilter)) + err = addDirFiles(zipWriter, "", sourceDir, getExcludeFilters(filter), getIncludeFilters(userIncludeFilter)) if err != nil { return "", err } @@ -1182,11 +958,11 @@ func compressFolder(sourceDir, filter, userIncludeFilter, scaResolver string) (s } func getIncludeFilters(userIncludeFilter string) []string { - return buildFilters(commonParams.BaseFilters, userIncludeFilter) + return buildFilters(commonParams.BaseIncludeFilters, userIncludeFilter) } -func getUserFilters(filterStr string) []string { - return buildFilters(nil, filterStr) +func getExcludeFilters(userExcludeFilter string) []string { + return buildFilters(commonParams.BaseExcludeFilters, userExcludeFilter) } func buildFilters(base []string, extra string) []string { @@ -1288,22 +1064,34 @@ func handleDir( newParent, newBase := GetNewParentAndBase(parentDir, file, baseDir) return addDirFilesIgnoreFilter(zipWriter, newBase, newParent) } - // Check if the folder is excluded + + isFiltered, err := isDirFiltered(file.Name(), filters) + if err != nil { + return err + } + if isFiltered { + logger.PrintIfVerbose("Excluded: " + parentDir + file.Name() + "/") + return nil + } + newParent, newBase := GetNewParentAndBase(parentDir, file, baseDir) + return addDirFiles(zipWriter, newBase, newParent, filters, includeFilters) +} + +func isDirFiltered(filename string, filters []string) (bool, error) { for _, filter := range filters { if filter[0] == '!' { filterStr := strings.TrimSuffix(filepath.ToSlash(filter[1:]), "/") - match, err := path.Match(filterStr, file.Name()) + match, err := path.Match(filterStr, filename) if err != nil { - return err + return false, err } if match { - logger.PrintIfVerbose("Excluded: " + parentDir + file.Name() + "/") - return nil + return true, nil } } } - newParent, newBase := GetNewParentAndBase(parentDir, file, baseDir) - return addDirFiles(zipWriter, newBase, newParent, filters, includeFilters) + + return false, nil } func GetNewParentAndBase(parentDir string, file fs.FileInfo, baseDir string) (newParent, newBase string) { @@ -1546,7 +1334,7 @@ func definePathForZipFileOrDirectory(cmd *cobra.Command) (zipFile, sourceDir str info, statErr := os.Stat(sourceTrimmed) if !os.IsNotExist(statErr) { - if filepath.Ext(sourceTrimmed) == ".zip" { + if filepath.Ext(sourceTrimmed) == constants.ZipExtension { zipFile = sourceTrimmed } else if info != nil && info.IsDir() { sourceDir = filepath.ToSlash(sourceTrimmed) @@ -1616,7 +1404,7 @@ func runCreateScanCommand( } // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedCreating, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedCreating, errorModel.Code, errorModel.Message) } else if scanResponseModel != nil { scanResponseModel = enrichScanResponseModel(cmd, scanResponseModel) err = printByScanInfoFormat(cmd, toScanView(scanResponseModel)) @@ -1661,7 +1449,7 @@ func runCreateScanCommand( return err } - err = applyThreshold(cmd, resultsWrapper, scanResponseModel, thresholdMap) + err = applyThreshold(resultsWrapper, scanResponseModel, thresholdMap) if err != nil { return err } @@ -1900,7 +1688,6 @@ func createReportsAfterScan( } func applyThreshold( - cmd *cobra.Command, resultsWrapper wrappers.ResultsWrapper, scanResponseModel *wrappers.ScanResponseModel, thresholdMap map[string]int, @@ -1959,14 +1746,44 @@ func parseThreshold(threshold string) map[string]int { } } } + return thresholdMap } +func validateThresholds(thresholdMap map[string]int) error { + var errMsgBuilder strings.Builder + + for engineName, limit := range thresholdMap { + if limit < 1 { + errMsgBuilder.WriteString(errors.Errorf("Invalid value for threshold limit %s. Threshold should be greater or equal to 1.\n", engineName).Error()) + } + } + + errMsg := errMsgBuilder.String() + if errMsg != "" { + return errors.New(errMsg) + } + return nil +} + +func parseThresholdLimit(limit string) (engineName string, intLimit int, err error) { + parts := strings.Split(limit, "=") + engineName = strings.Replace(parts[0], commonParams.KicsType, commonParams.IacType, 1) + if len(parts) <= 1 { + return engineName, 0, errors.Errorf("Error parsing threshold limit: missing values\n") + } + intLimit, err = strconv.Atoi(parts[1]) + if err != nil { + err = errors.Errorf("%s: Error parsing threshold limit: %v\n", engineName, err) + } + return engineName, intLimit, err +} + func getSummaryThresholdMap(resultsWrapper wrappers.ResultsWrapper, scan *wrappers.ScanResponseModel) ( map[string]int, error, ) { - results, err := ReadResults(resultsWrapper, scan, make(map[string]string)) + results, err := ReadResults(resultsWrapper, scan, make(map[string]string), true) if err != nil { return nil, err } @@ -2021,9 +1838,10 @@ func waitForScanCompletion( return errors.Wrapf(err, "%s\n", failedCanceling) } if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedCanceling, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedCanceling, errorModel.Code, errorModel.Message) } - return errors.Errorf("Timeout of %d minute(s) for scan reached", timeoutMinutes) + + return wrappers.NewAstError(exitCodes.MultipleEnginesFailedExitCode, errors.Errorf("Timeout of %d minute(s) for scan reached", timeoutMinutes)) } i++ } @@ -2068,13 +1886,49 @@ func isScanRunning( if reportErr != nil { return false, errors.New("unable to create report for partial scan") } - return false, errors.New("scan completed partially") + exitCode := getExitCode(scanResponseModel) + return false, wrappers.NewAstError(exitCode, errors.New("scan completed partially")) } else if scanResponseModel.Status != wrappers.ScanCompleted { - return false, errors.New("scan did not complete successfully") + exitCode := getExitCode(scanResponseModel) + return false, wrappers.NewAstError(exitCode, errors.New("scan did not complete successfully")) } return false, nil } +func getExitCode(scanResponseModel *wrappers.ScanResponseModel) int { + failedStatuses := make([]int, 0) + for _, scanner := range scanResponseModel.StatusDetails { + scannerNameLowerCase := strings.ToLower(scanner.Name) + scannerErrorExitCode, errorCodeByScannerExists := errorCodesByScanner[scannerNameLowerCase] + if scanner.Status == wrappers.ScanFailed && scanner.Name != General && errorCodeByScannerExists { + failedStatuses = append(failedStatuses, scannerErrorExitCode) + } + } + if len(failedStatuses) == 1 { + return failedStatuses[0] + } + + return exitCodes.MultipleEnginesFailedExitCode +} + +const ( + General = "general" + Sast = "sast" + Sca = "sca" + IacSecurity = "iac-security" // We get 'kics' from AST. Added for forward compatibility + Kics = "kics" + APISec = "apisec" +) + +var errorCodesByScanner = map[string]int{ + General: exitCodes.MultipleEnginesFailedExitCode, + Sast: exitCodes.SastEngineFailedExitCode, + Sca: exitCodes.ScaEngineFailedExitCode, + IacSecurity: exitCodes.IacSecurityEngineFailedExitCode, + Kics: exitCodes.KicsEngineFailedExitCode, + APISec: exitCodes.ApisecEngineFailedExitCode, +} + func runListScansCommand(scansWrapper wrappers.ScansWrapper, sastMetadataWrapper wrappers.SastMetadataWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var allScansModel *wrappers.ScansCollectionResponseModel @@ -2091,7 +1945,7 @@ func runListScansCommand(scansWrapper wrappers.ScansWrapper, sastMetadataWrapper // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedGettingAll, errorModel.Code, errorModel.Message) } else if allScansModel != nil && allScansModel.Scans != nil { views, err := toScanViews(allScansModel.Scans, sastMetadataWrapper) if err != nil { @@ -2172,7 +2026,7 @@ func runDeleteScanCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Co // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedDeleting, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedDeleting, errorModel.Code, errorModel.Message) } } @@ -2193,7 +2047,7 @@ func runCancelScanCommand(scansWrapper wrappers.ScansWrapper) func(cmd *cobra.Co } // Checking the response if errorModel != nil { - return errors.Errorf(ErrorCodeFormat, failedCanceling, errorModel.Code, errorModel.Message) + return errors.Errorf(services.ErrorCodeFormat, failedCanceling, errorModel.Code, errorModel.Message) } } @@ -2563,35 +2417,6 @@ func validateCreateScanFlags(cmd *cobra.Command) error { return nil } -func validateThresholds(thresholdMap map[string]int) error { - var errMsgBuilder strings.Builder - - for engineName, limit := range thresholdMap { - if limit < 1 { - errMsgBuilder.WriteString(errors.Errorf("Invalid value for threshold limit %s. Threshold should be greater or equal to 1.\n", engineName).Error()) - } - } - - errMsg := errMsgBuilder.String() - if errMsg != "" { - return errors.New(errMsg) - } - return nil -} - -func parseThresholdLimit(limit string) (engineName string, intLimit int, err error) { - parts := strings.Split(limit, "=") - engineName = strings.Replace(parts[0], commonParams.KicsType, commonParams.IacType, 1) - if len(parts) <= 1 { - return engineName, 0, errors.Errorf("Error parsing threshold limit: missing values\n") - } - intLimit, err = strconv.Atoi(parts[1]) - if err != nil { - err = errors.Errorf("%s: Error parsing threshold limit: %v\n", engineName, err) - } - return engineName, intLimit, err -} - func validateBooleanString(value string) error { if value == "" { return nil diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 6e0f569a2..1da86ccf8 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -3,17 +3,20 @@ package commands import ( + "fmt" "reflect" "strings" "testing" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" commonParams "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "github.com/checkmarx/ast-cli/internal/wrappers/utils" + "github.com/pkg/errors" "gotest.tools/assert" - "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -124,42 +127,42 @@ func TestCreateScan(t *testing.T) { execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") } +func TestCreateScanWithThreshold_ShouldSuccess(t *testing.T) { + execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch", "--scan-types", "sast", "--threshold", "sca-low=1 ; sast-medium=2") +} + func TestScanCreate_ExistingApplicationAndProject_CreateProjectUnderApplicationSuccessfully(t *testing.T) { execCmdNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") } func TestScanCreate_ApplicationNameIsNotExactMatch_FailedToCreateScan(t *testing.T) { err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", "MOC", "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) + assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission) } func TestScanCreate_ExistingProjectAndApplicationWithNoPermission_FailedToCreateScan(t *testing.T) { err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.ApplicationDoesntExist, "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) -} - -func TestScanCreate_ExistingApplication_CreateNewProjectUnderApplicationSuccessfully(t *testing.T) { - execCmdNilAssertion(t, "scan", "create", "--project-name", "NewProject", "--application-name", "MOCK", "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission) } func TestScanCreate_ExistingApplicationWithNoPermission_FailedToCreateScan(t *testing.T) { err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "NewProject", "--application-name", mock.NoPermissionApp, "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) + assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission) } func TestScanCreate_OnReceivingHttpBadRequestStatusCode_FailedToCreateScan(t *testing.T) { - err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeHTTPStatusBadRequest, "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeBadRequest400, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == errorConstants.FailedToGetApplication) } func TestScanCreate_OnReceivingHttpInternalServerErrorStatusCode_FailedToCreateScan(t *testing.T) { - err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeHTTPStatusInternalServerError, "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.FailedToGetApplication) + err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.FakeInternalServerError500, "-s", dummyRepo, "-b", "dummy_branch") + assert.Assert(t, err.Error() == errorConstants.FailedToGetApplication) } func TestCreateScanInsideApplicationProjectExistNoPermissions(t *testing.T) { err := execCmdNotNilAssertion(t, "scan", "create", "--project-name", "MOCK", "--application-name", mock.NoPermissionApp, "-s", dummyRepo, "-b", "dummy_branch") - assert.Assert(t, err.Error() == applicationErrors.ApplicationDoesntExistOrNoPermission) + assert.Assert(t, err.Error() == errorConstants.ApplicationDoesntExistOrNoPermission) } func TestCreateScanSourceDirectory(t *testing.T) { @@ -229,6 +232,43 @@ func TestCreateScanWithScanTypes(t *testing.T) { execCmdNilAssertion(t, append(baseArgs, "--scan-types", "sast,api-security")...) } +func TestScanCreate_KicsScannerFail_ReturnCorrectKicsExitCodeAndErrorMessage(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "fake-kics-scanner-fail", "-s", dummyRepo, "-b", "dummy_branch"} + err := execCmdNotNilAssertion(t, append(baseArgs, "--scan-types", Kics)...) + assertAstError(t, err, "scan did not complete successfully", exitCodes.KicsEngineFailedExitCode) +} + +func TestScanCreate_MultipleScannersFail_ReturnGeneralExitCodeAndErrorMessage(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "fake-multiple-scanner-fails", "-s", dummyRepo, "-b", "dummy_branch"} + baseArgs = append(baseArgs, "--scan-types", fmt.Sprintf("%s,%s", Kics, Sca)) + err := execCmdNotNilAssertion(t, baseArgs...) + assertAstError(t, err, "scan did not complete successfully", exitCodes.MultipleEnginesFailedExitCode) +} + +func TestScanCreate_ScaScannersFailPartialScan_ReturnScaExitCodeAndErrorMessage(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "fake-sca-fail-partial", "-s", dummyRepo, "-b", "dummy_branch"} + baseArgs = append(baseArgs, "--scan-types", Sca) + err := execCmdNotNilAssertion(t, baseArgs...) + assertAstError(t, err, "scan completed partially", exitCodes.ScaEngineFailedExitCode) +} + +func TestScanCreate_MultipleScannersDifferentStatusesOnlyKicsFail_ReturnKicsExitCodeAndErrorMessage(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "fake-kics-fail-sast-canceled", "-s", dummyRepo, "-b", "dummy_branch"} + baseArgs = append(baseArgs, "--scan-types", fmt.Sprintf("%s,%s,%s", Sca, Sast, Kics)) + err := execCmdNotNilAssertion(t, baseArgs...) + assertAstError(t, err, "scan did not complete successfully", exitCodes.KicsEngineFailedExitCode) +} + +func assertAstError(t *testing.T, err error, expectedErrorMessage string, expectedExitCode int) { + var e *wrappers.AstError + if errors.As(err, &e) { + assert.Equal(t, e.Error(), expectedErrorMessage) + assert.Equal(t, e.Code, expectedExitCode) + } else { + assert.Assert(t, false, "Error is not of type AstError") + } +} + func TestCreateScanWithNoFilteredProjects(t *testing.T) { baseArgs := []string{"scan", "create", "-s", dummyRepo, "-b", "dummy_branch"} // Cover "createProject" when no project is filtered when finding the provided project @@ -331,7 +371,7 @@ func TestCreateScanWrongSSHKeyPath(t *testing.T) { "open dummy_key: no such file or directory", } - assert.Assert(t, util.Contains(expectedMessages, err.Error())) + assert.Assert(t, utils.Contains(expectedMessages, err.Error())) } func TestCreateScanWithSSHKey(t *testing.T) { @@ -463,6 +503,17 @@ func Test_parseThresholdSuccess(t *testing.T) { } } +func Test_parseThresholdsSuccess(t *testing.T) { + want := make(map[string]int) + want["sast-high"] = 1 + want["sast-medium"] = 1 + want["sca-high"] = 1 + threshold := "sast-high=1; sast-medium=1; sca-high=1" + if got := parseThreshold(threshold); !reflect.DeepEqual(got, want) { + t.Errorf("parseThreshold() = %v, want %v", got, want) + } +} + func Test_parseThresholdParseError(t *testing.T) { want := make(map[string]int) threshold := " KICS - LoW=error" @@ -679,6 +730,69 @@ func TestCreateScanProjectTagsCheckResendToScan(t *testing.T) { assert.NilError(t, err) } +func Test_isDirFiltered(t *testing.T) { + type args struct { + filename string + filters []string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "WhenUserDefinedExcludedFolder_ReturnIsFilteredTrue", + args: args{ + filename: "user-folder-to-exclude", + filters: append(commonParams.BaseExcludeFilters, "!user-folder-to-exclude"), + }, + want: true, + wantErr: false, + }, + { + name: "WhenUserDefinedExcludedFolder_DoesNotAffectOtherFolders_ReturnIsFilteredFalse", + args: args{ + filename: "some-folder", + filters: append(commonParams.BaseExcludeFilters, "!exclude-other-folder"), + }, + want: false, + wantErr: false, + }, + { + name: "WhenFolderIsNotExcluded_ReturnIsFilteredFalse", + args: args{ + filename: "some-folder-name", + filters: commonParams.BaseExcludeFilters, + }, + want: false, + wantErr: false, + }, + { + name: "WhenDefaultFolderIsExcluded_ReturnIsFilteredTrue", + args: args{ + filename: ".vs", + filters: commonParams.BaseExcludeFilters, + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := isDirFiltered(ttt.args.filename, ttt.args.filters) + if (err != nil) != ttt.wantErr { + t.Errorf("isDirFiltered() error = %v, wantErr %v", err, ttt.wantErr) + return + } + if got != ttt.want { + t.Errorf("isDirFiltered() got = %v, want %v", got, ttt.want) + } + }) + } +} + func Test_parseThresholdLimit(t *testing.T) { type args struct { limit string diff --git a/internal/commands/util/completion.go b/internal/commands/util/completion.go index 1d738c5cc..ba264d310 100644 --- a/internal/commands/util/completion.go +++ b/internal/commands/util/completion.go @@ -6,6 +6,7 @@ import ( "os" "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/wrappers/utils" "github.com/spf13/cobra" ) @@ -62,7 +63,7 @@ func NewCompletionCommand() *cobra.Command { Args: func(cmd *cobra.Command, args []string) error { shellType, _ := cmd.Flags().GetString(shellFlag) - if shellType == "" || Contains(cmd.ValidArgs, shellType) { + if shellType == "" || utils.Contains(cmd.ValidArgs, shellType) { return nil } diff --git a/internal/commands/util/import.go b/internal/commands/util/import.go new file mode 100644 index 000000000..fe8ea2e00 --- /dev/null +++ b/internal/commands/util/import.go @@ -0,0 +1,112 @@ +package util + +import ( + "path/filepath" + "strings" + + "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/constants" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + commonParams "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 NewImportCommand( + projectsWrapper wrappers.ProjectsWrapper, + uploadsWrapper wrappers.UploadsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + byorWrapper wrappers.ByorWrapper, + applicationsWrapper wrappers.ApplicationsWrapper) *cobra.Command { + cmd := &cobra.Command{ + Use: "import", + Short: "Import SAST scan results", + Long: "The import command enables you to import SAST scan results from an external source into Checkmarx One. The results must be submitted in sarif format.", + Example: heredoc.Doc( + ` + $ cx utils import --project-name --import-file-path + `), + Annotations: map[string]string{ + "command:doc": heredoc.Doc( + ` + https://checkmarx.com/resource/documents/en/34965-68625-checkmarx-one-cli-commands.html + `, + ), + }, + RunE: runImportCommand(projectsWrapper, uploadsWrapper, groupsWrapper, accessManagementWrapper, applicationsWrapper, byorWrapper), + } + + cmd.PersistentFlags().String(commonParams.ImportFilePath, "", "Path to the import file (sarif file or zip archive containing sarif files)") + cmd.PersistentFlags().String(commonParams.ProjectName, "", "The project under which the file will be imported.") + + return cmd +} + +func runImportCommand( + projectsWrapper wrappers.ProjectsWrapper, + uploadsWrapper wrappers.UploadsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, + byorWrapper wrappers.ByorWrapper) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, args []string) error { + importFilePath, err := validateFilePath(cmd) + if err != nil { + return err + } + + projectName, _ := cmd.Flags().GetString(commonParams.ProjectName) + if projectName == "" { + return errors.Errorf(errorConstants.ProjectNameIsRequired) + } + + projectID, err := services.FindProject(nil, projectName, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, applicationsWrapper) + if err != nil { + return err + } + + err = importFile(projectID, importFilePath, uploadsWrapper, byorWrapper) + if err != nil { + return err + } + + return nil + } +} + +func validateFilePath(cmd *cobra.Command) (string, error) { + importFilePath, _ := cmd.Flags().GetString(commonParams.ImportFilePath) + if importFilePath == "" { + return "", errors.Errorf(errorConstants.ImportFilePathIsRequired) + } + + if validationError := validateFileExtension(importFilePath); validationError != nil { + return "", validationError + } + return importFilePath, nil +} + +func validateFileExtension(importFilePath string) error { + extension := filepath.Ext(importFilePath) + extension = strings.ToLower(extension) + if extension != constants.SarifExtension && extension != constants.ZipExtension { + return errors.Errorf(errorConstants.SarifInvalidFileExtension) + } + return nil +} + +func importFile(projectID string, path string, + uploadsWrapper wrappers.UploadsWrapper, byorWrapper wrappers.ByorWrapper) error { + uploadURL, err := uploadsWrapper.UploadFile(path) + if err != nil { + return err + } + _, err = byorWrapper.Import(projectID, *uploadURL) + if err != nil { + return err + } + return nil +} diff --git a/internal/commands/util/import_test.go b/internal/commands/util/import_test.go new file mode 100644 index 000000000..712875374 --- /dev/null +++ b/internal/commands/util/import_test.go @@ -0,0 +1,143 @@ +//go:build !integration + +package util + +import ( + "testing" + + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "gotest.tools/assert" +) + +func TestImport_ImportSarifFileWithCorrectFlags_CreateImportSuccessfully(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "my-project", "--import-file-path", "my-path.sarif"}) + err := cmd.Execute() + assert.Assert(t, err == nil) +} + +func TestImport_ImportSarifFileProjectDoesntExist_CreateImportWithProvidedNewNameSuccessfully(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "MOCK-PROJECT-NOT-EXIST", "--import-file-path", "my-path.sarif"}) + err := cmd.Execute() + assert.Assert(t, err == nil) +} + +func TestImport_ImportSarifFileMissingImportFilePath_CreateImportReturnsErrorWithCorrectMessage(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "my-project"}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.ImportFilePathIsRequired) +} + +func TestImport_ImportSarifFileEmptyImportFilePathValue_CreateImportReturnsErrorWithCorrectMessage(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "my-project", "--import-file-path", ""}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.ImportFilePathIsRequired) +} + +func TestImport_ImportSarifFileMissingImportProjectName_CreateImportReturnsErrorWithCorrectMessage(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--import-file-path", "my-path.zip"}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.ProjectNameIsRequired) +} + +func TestImport_ImportSarifFileProjectNameNotProvided_CreateImportWithProvidedNewNameSuccessfully(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "", "--import-file-path", "my-path.sarif"}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.ProjectNameIsRequired) +} + +func TestImport_ImportSarifFileUnacceptedFileExtension_CreateImportReturnsErrorWithCorrectMessage(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "MOCK-PROJECT-NOT-EXIST", "--import-file-path", "my-path.txt"}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.SarifInvalidFileExtension) +} + +func TestImport_ImportSarifFileMissingExtension_CreateImportReturnsErrorWithCorrectMessage(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + cmd := NewImportCommand( + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + &mock.ByorMockWrapper{}, + mock.ApplicationsMockWrapper{}) + cmd.SetArgs([]string{"utils", "import", "--project-name", "MOCK-PROJECT-NOT-EXIST", "--import-file-path", "some/path/no/extension/my-path"}) + err := cmd.Execute() + assert.Assert(t, err.Error() == errorConstants.SarifInvalidFileExtension) +} + +func TestImporFileFunction_FakeUnauthorizedHttpStatusCode_ReturnRelevantError(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + err := importFile(mock.FakeUnauthorized401, "importFilePath", &mock.UploadsMockWrapper{}, &mock.ByorMockWrapper{}) + assert.Assert(t, err.Error() == errorConstants.StatusUnauthorized) +} + +func TestImporFileFunction_FakeForbiddenHttpStatusCode_ReturnRelevantError(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + err := importFile(mock.FakeForbidden403, "importFilePath", &mock.UploadsMockWrapper{}, &mock.ByorMockWrapper{}) + assert.Assert(t, err.Error() == errorConstants.StatusForbidden) +} + +func TestImporFileFunction_FakeInternalServerErrorHttpStatusCode_ReturnRelevantError(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] = true + err := importFile(mock.FakeInternalServerError500, "importFilePath", &mock.UploadsMockWrapper{}, &mock.ByorMockWrapper{}) + assert.Assert(t, err.Error() == errorConstants.StatusInternalServerError) +} diff --git a/internal/commands/util/learnmore.go b/internal/commands/util/learnmore.go index b1d734b0e..123a6c04c 100644 --- a/internal/commands/util/learnmore.go +++ b/internal/commands/util/learnmore.go @@ -2,14 +2,15 @@ package util // nolint:goimports import ( + "html" + "log" + "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/wrappers" "github.com/pkg/errors" "github.com/spf13/cobra" - "html" - "log" ) const defaultFormat = "list" diff --git a/internal/commands/util/printer/printer.go b/internal/commands/util/printer/printer.go index 5d1efeb74..02a190bbf 100644 --- a/internal/commands/util/printer/printer.go +++ b/internal/commands/util/printer/printer.go @@ -14,6 +14,7 @@ import ( const ( FormatJSON = "json" + FormatIndentedJSON = "indented-json" FormatSarif = "sarif" FormatSonar = "sonar" FormatSummary = "summaryHTML" @@ -32,7 +33,13 @@ const ( ) func Print(w io.Writer, view interface{}, format string) error { - if IsFormat(format, FormatJSON) { + if IsFormat(format, FormatIndentedJSON) { + viewJSON, err := json.MarshalIndent(view, "", " ") + if err != nil { + return err + } + _, _ = fmt.Fprintln(w, string(viewJSON)) + } else if IsFormat(format, FormatJSON) { viewJSON, err := json.Marshal(view) if err != nil { return err diff --git a/internal/commands/util/utils.go b/internal/commands/util/utils.go index 52e98a8f0..d39533315 100644 --- a/internal/commands/util/utils.go +++ b/internal/commands/util/utils.go @@ -7,6 +7,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/checkmarx/ast-cli/internal/commands/util/usercount" + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/checkmarx/ast-cli/internal/wrappers/bitbucketserver" "github.com/spf13/cobra" @@ -30,6 +31,12 @@ func NewUtilsCommand( chatWrapper wrappers.ChatWrapper, policyWrapper wrappers.PolicyWrapper, scansWrapper wrappers.ScansWrapper, + projectsWrapper wrappers.ProjectsWrapper, + uploadsWrapper wrappers.UploadsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, + byorWrapper wrappers.ByorWrapper, ) *cobra.Command { utilsCmd := &cobra.Command{ Use: "utils", @@ -48,6 +55,9 @@ func NewUtilsCommand( ), }, } + + importCmd := NewImportCommand(projectsWrapper, uploadsWrapper, groupsWrapper, accessManagementWrapper, byorWrapper, applicationsWrapper) + envCheckCmd := NewEnvCheckCommand() completionCmd := NewCompletionCommand() @@ -62,6 +72,10 @@ func NewUtilsCommand( maskSecretsCmd := NewMaskSecretsCommand(chatWrapper) + if wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + utilsCmd.AddCommand(importCmd) + } + utilsCmd.AddCommand( completionCmd, envCheckCmd, @@ -83,14 +97,6 @@ func NewUtilsCommand( } // Contains Tests if a string exists in the provided array/** -func Contains(array []string, val string) bool { - for _, e := range array { - if e == val { - return true - } - } - return false -} func executeTestCommand(cmd *cobra.Command, args ...string) error { fmt.Println("Executing command with args ", args) diff --git a/internal/commands/util/utils_test.go b/internal/commands/util/utils_test.go index 2988d4399..4939a4764 100644 --- a/internal/commands/util/utils_test.go +++ b/internal/commands/util/utils_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "github.com/checkmarx/ast-cli/internal/wrappers/utils" "gotest.tools/assert" ) @@ -20,7 +21,14 @@ func TestNewUtilsCommand(t *testing.T) { mock.TenantConfigurationMockWrapper{}, mock.ChatMockWrapper{}, nil, - nil) + nil, + &mock.ProjectsMockWrapper{}, + &mock.UploadsMockWrapper{}, + &mock.GroupsMockWrapper{}, + mock.AccessManagementMockWrapper{}, + mock.ApplicationsMockWrapper{}, + &mock.ByorMockWrapper{}) + assert.Assert(t, cmd != nil, "Utils command must exist") } @@ -38,7 +46,7 @@ func TestContains(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - if got := Contains(tt.array, tt.val); got != tt.exists { + if got := utils.Contains(tt.array, tt.val); got != tt.exists { t.Errorf("Contains() = %v, want %v", got, tt.exists) } }) diff --git a/internal/constants/errors/errors.go b/internal/constants/errors/errors.go new file mode 100644 index 000000000..8576fb522 --- /dev/null +++ b/internal/constants/errors/errors.go @@ -0,0 +1,22 @@ +package errorconstants + +// HTTP Errors +const ( + StatusUnauthorized = "you are not authorized to make this request" + StatusForbidden = "you are not allowed to make this request" + RedirectURLNotFound = "redirect URL not found in response" + HTTPMethodNotFound = "HTTP method not found in request" + StatusInternalServerError = "an error occurred during this request" +) + +const ( + ApplicationDoesntExistOrNoPermission = "provided application does not exist or user has no permission to the application" + ImportFilePathIsRequired = "importFilePath is required" + ProjectNameIsRequired = "project name is required" + ProjectNotExists = "the project name you provided does not match any project" + ScanIDRequired = "scan ID is required" + FailedToGetApplication = "failed to get application" + SarifInvalidFileExtension = "Invalid file extension. Supported extensions are .sarif and .zip containing sarif files." + ImportSarifFileError = "There was a problem importing the SARIF file. Please contact support for further details." + ImportSarifFileErrorMessageWithMessage = "There was a problem importing the SARIF file. Please contact support for further details with the following error code: %d %s" +) diff --git a/internal/constants/exit-codes/exit-codes.go b/internal/constants/exit-codes/exit-codes.go new file mode 100644 index 000000000..0149def4a --- /dev/null +++ b/internal/constants/exit-codes/exit-codes.go @@ -0,0 +1,10 @@ +package exitcodes + +const ( + MultipleEnginesFailedExitCode = 1 + SastEngineFailedExitCode = 2 + ScaEngineFailedExitCode = 3 + IacSecurityEngineFailedExitCode = 4 // Same code as kics to support forward compatibility + KicsEngineFailedExitCode = 4 + ApisecEngineFailedExitCode = 5 +) diff --git a/internal/constants/feature-flags/feature-flags.go b/internal/constants/feature-flags/feature-flags.go new file mode 100644 index 000000000..678089baf --- /dev/null +++ b/internal/constants/feature-flags/feature-flags.go @@ -0,0 +1,6 @@ +package featureflags + +const ( + ByorEnabled = "BYOR_ENABLED" + AccessManagementEnabled = "ACCESS_MANAGEMENT_ENABLED" +) diff --git a/internal/constants/file-extensions.go b/internal/constants/file-extensions.go new file mode 100644 index 000000000..37853dd71 --- /dev/null +++ b/internal/constants/file-extensions.go @@ -0,0 +1,6 @@ +package constants + +const ( + ZipExtension = ".zip" + SarifExtension = ".sarif" +) diff --git a/internal/errors/application-errors.go b/internal/errors/application-errors.go deleted file mode 100644 index 21c43adce..000000000 --- a/internal/errors/application-errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package applicationerrors - -const ( - ApplicationDoesntExistOrNoPermission = "Provided application does not exist or user has no permission to the application" -) - -const ( - FailedToGetApplication = "Failed to get application" - RedirectURLNotFound = "redirect URL not found in response" - HTTPMethodNotFound = "HTTP method not found in request" -) diff --git a/internal/params/binds.go b/internal/params/binds.go index f522daf70..a15982f8e 100644 --- a/internal/params/binds.go +++ b/internal/params/binds.go @@ -62,4 +62,5 @@ var EnvVarsBinds = []struct { {FeatureFlagsKey, FeatureFlagsEnv, "api/flags"}, {PolicyEvaluationPathKey, PolicyEvaluationPathEnv, "api/policy_management_service_uri/evaluation"}, {AccessManagementPathKey, AccessManagementPathEnv, "api/access-management"}, + {ByorPathKey, ByorPathEnv, "api/byor"}, } diff --git a/internal/params/envs.go b/internal/params/envs.go index 0d1a426d7..c71d72653 100644 --- a/internal/params/envs.go +++ b/internal/params/envs.go @@ -60,5 +60,6 @@ const ( UploadURLEnv = "CX_UPLOAD_URL" PolicyEvaluationPathEnv = "CX_POLICY_EVALUATION_PATH" AccessManagementPathEnv = "CX_ACCESS_MANAGEMENT_PATH" + ByorPathEnv = "CX_BYOR_PATH" IgnoreProxyEnv = "CX_IGNORE_PROXY" ) diff --git a/internal/params/filters.go b/internal/params/filters.go index c84eebc00..da08e30f3 100644 --- a/internal/params/filters.go +++ b/internal/params/filters.go @@ -1,6 +1,6 @@ package params -var BaseFilters = []string{ +var BaseIncludeFilters = []string{ "*.javasln", "*.project", "*.java", @@ -135,6 +135,12 @@ var BaseFilters = []string{ "Directory.Packages.props", } +var BaseExcludeFilters = []string{ + "!.vs", + "!.vscode", + "!.idea", +} + var KicsBaseFilters = []string{ ".tf", ".yaml", diff --git a/internal/params/flags.go b/internal/params/flags.go index 4d3c6b707..a9394d818 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -26,6 +26,7 @@ const ( IgnorePolicyFlag = "ignore-policy" SourceDirFilterFlag = "file-filter" SourceDirFilterFlagSh = "f" + ImportFilePath = "import-file-path" IncludeFilterFlag = "file-include" IncludeFilterFlagSh = "i" ProjectIDFlag = "project-id" diff --git a/internal/params/keys.go b/internal/params/keys.go index 0ed4b1f98..369d5aba4 100644 --- a/internal/params/keys.go +++ b/internal/params/keys.go @@ -61,4 +61,5 @@ var ( FeatureFlagsKey = strings.ToLower(FeatureFlagsEnv) PolicyEvaluationPathKey = strings.ToLower(PolicyEvaluationPathEnv) AccessManagementPathKey = strings.ToLower(AccessManagementPathEnv) + ByorPathKey = strings.ToLower(ByorPathEnv) ) diff --git a/internal/services/applications.go b/internal/services/applications.go new file mode 100644 index 000000000..586fe9c60 --- /dev/null +++ b/internal/services/applications.go @@ -0,0 +1,12 @@ +package services + +import "github.com/checkmarx/ast-cli/internal/wrappers/utils" + +func createApplicationIds(applicationID, existingApplicationIds []string) []string { + for _, id := range applicationID { + if !utils.Contains(existingApplicationIds, id) { + existingApplicationIds = append(existingApplicationIds, id) + } + } + return existingApplicationIds +} diff --git a/internal/services/applications_test.go b/internal/services/applications_test.go new file mode 100644 index 000000000..06ecad984 --- /dev/null +++ b/internal/services/applications_test.go @@ -0,0 +1,38 @@ +package services + +import ( + "reflect" + "testing" +) + +func Test_createApplicationIds(t *testing.T) { + type args struct { + applicationID []string + existingApplicationIds []string + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "When adding new application IDs, add them to all applications", + args: args{ + applicationID: []string{"3", "4"}, + existingApplicationIds: []string{"1", "2"}}, + want: []string{"1", "2", "3", "4"}}, + {name: "When adding existing application IDs, do not re-add them", + args: args{ + applicationID: []string{"1"}, + existingApplicationIds: []string{"1", "2", "3"}}, + want: []string{"1", "2", "3"}}, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + if got := createApplicationIds(ttt.args.applicationID, ttt.args.existingApplicationIds); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("createApplicationIds() = %v, want %v", got, ttt.want) + } + }) + } +} diff --git a/internal/services/groups.go b/internal/services/groups.go new file mode 100644 index 000000000..1e742558b --- /dev/null +++ b/internal/services/groups.go @@ -0,0 +1,94 @@ +package services + +import ( + "strings" + + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" +) + +func CreateGroupsMap(groupsStr string, groupsWrapper wrappers.GroupsWrapper) ([]*wrappers.Group, error) { + groups := strings.Split(groupsStr, ",") + var groupsMap []*wrappers.Group + var groupsNotFound []string + for _, group := range groups { + if len(group) > 0 { + groupsFromEnv, err := groupsWrapper.Get(group) + if err != nil { + groupsNotFound = append(groupsNotFound, group) + } else { + findGroup := findGroupByName(groupsFromEnv, group) + if findGroup != nil && findGroup.Name != "" { + groupsMap = append(groupsMap, findGroup) + } else { + groupsNotFound = append(groupsNotFound, group) + } + } + } + } + if len(groupsNotFound) > 0 { + return nil, errors.Errorf("%s: %v", failedFindingGroup, groupsNotFound) + } + return groupsMap, nil +} + +func getGroupsForRequest(groups []*wrappers.Group) []string { + if !wrappers.FeatureFlags[featureFlagsConstants.AccessManagementEnabled] { + return GetGroupIds(groups) + } + return nil +} + +func AssignGroupsToProjectNewAccessManagement(projectID string, projectName string, groups []*wrappers.Group, + accessManagement wrappers.AccessManagementWrapper) error { + if !wrappers.FeatureFlags[featureFlagsConstants.AccessManagementEnabled] { + return nil + } + groupsAssignedToTheProject, err := accessManagement.GetGroups(projectID) + if err != nil { + return err + } + groupsToAssign := getGroupsToAssign(groups, groupsAssignedToTheProject) + if len(groupsToAssign) == 0 { + return nil + } + + err = accessManagement.CreateGroupsAssignment(projectID, projectName, groupsToAssign) + if err != nil { + return err + } + return nil +} + +func getGroupsToAssign(receivedGroups, existingGroups []*wrappers.Group) []*wrappers.Group { + var groupsToAssign []*wrappers.Group + var groupsMap = make(map[string]bool) + for _, existingGroup := range existingGroups { + groupsMap[existingGroup.ID] = true + } + for _, receivedGroup := range receivedGroups { + find := groupsMap[receivedGroup.ID] + if !find { + groupsToAssign = append(groupsToAssign, receivedGroup) + } + } + return groupsToAssign +} + +func GetGroupIds(groups []*wrappers.Group) []string { + var groupIds []string + for _, group := range groups { + groupIds = append(groupIds, group.ID) + } + return groupIds +} + +func findGroupByName(groups []wrappers.Group, name string) *wrappers.Group { + for i := 0; i < len(groups); i++ { + if groups[i].Name == name { + return &groups[i] + } + } + return nil +} diff --git a/internal/services/groups_test.go b/internal/services/groups_test.go new file mode 100644 index 000000000..9131118a3 --- /dev/null +++ b/internal/services/groups_test.go @@ -0,0 +1,220 @@ +package services + +import ( + "reflect" + "testing" + + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" +) + +func TestAssignGroupsToProject(t *testing.T) { + type args struct { + projectID string + projectName string + groups []*wrappers.Group + accessManagement wrappers.AccessManagementWrapper + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "When assigning group to project, no error should be returned", + args: args{ + projectID: "project-id", + projectName: "project-name", + groups: []*wrappers.Group{{ + ID: "group-id-to-assign", + Name: "group-name-to-assign", + }}, + accessManagement: &mock.AccessManagementMockWrapper{}, + }, + wantErr: false, + }, + } + for _, tt := range tests { + ttt := tt + wrappers.FeatureFlags[featureFlagsConstants.AccessManagementEnabled] = true + t.Run(tt.name, func(t *testing.T) { + if err := AssignGroupsToProjectNewAccessManagement(ttt.args.projectID, ttt.args.projectName, ttt.args.groups, ttt.args.accessManagement); (err != nil) != ttt.wantErr { + t.Errorf("AssignGroupsToProjectNewAccessManagement() error = %v, wantErr %v", err, ttt.wantErr) + } + }) + } +} + +func TestCreateGroupsMap(t *testing.T) { + type args struct { + groupsStr string + groupsWrapper wrappers.GroupsWrapper + } + tests := []struct { + name string + args args + want []*wrappers.Group + wantErr bool + }{ + { + name: "When creating a group map with existing group, no error should be returned", + args: args{ + groupsStr: "group", + groupsWrapper: &mock.GroupsMockWrapper{}, + }, + want: []*wrappers.Group{{ID: "1", Name: "group"}}, + wantErr: false, + }, + { + name: "When creating a group map with non-existing group, an error should be returned", + args: args{ + groupsStr: "not-existing-group", + groupsWrapper: &mock.GroupsMockWrapper{}, + }, + want: nil, + wantErr: true, + }, + { + name: "When faking an error upon calling groups wrapper, an error should be returned", + args: args{ + groupsStr: "fake-group-error", + groupsWrapper: &mock.GroupsMockWrapper{}, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := CreateGroupsMap(ttt.args.groupsStr, ttt.args.groupsWrapper) + if (err != nil) != ttt.wantErr { + t.Errorf("CreateGroupsMap() error = %v, wantErr %v", err, ttt.wantErr) + return + } + if !reflect.DeepEqual(got, ttt.want) { + t.Errorf("CreateGroupsMap() got = %v, want %v", got, ttt.want) + } + }) + } +} + +func TestGetGroupIds(t *testing.T) { + type args struct { + groups []*wrappers.Group + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "When passing a slice of groups, return a slice of group IDs", + args: args{groups: []*wrappers.Group{{ID: "group-id-1", Name: "group-name-1"}, {ID: "group-id-2", Name: "group-name-2"}}}, + want: []string{"group-id-1", "group-id-2"}, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + if got := GetGroupIds(ttt.args.groups); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("GetGroupIds() = %v, want %v", got, ttt.want) + } + }) + } +} + +func Test_findGroupByName(t *testing.T) { + type args struct { + groups []wrappers.Group + name string + } + tests := []struct { + name string + args args + want *wrappers.Group + }{ + { + name: "When calling with a group name, return the group with the same name", + args: args{ + groups: []wrappers.Group{{ + ID: "1", + Name: "group-one", + }, + { + ID: "2", + Name: "group-two", + }}, + name: "group-two", + }, + want: &wrappers.Group{ + ID: "2", + Name: "group-two", + }, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + if got := findGroupByName(ttt.args.groups, ttt.args.name); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("findGroupByName() = %v, want %v", got, ttt.want) + } + }) + } +} + +func Test_getGroupsForRequest(t *testing.T) { + type args struct { + groups []*wrappers.Group + } + tests := []struct { + name string + args args + want []string + }{ + { + name: "When access management is disabled, return group IDs of the groups", + args: args{groups: []*wrappers.Group{{ID: "group-id-1", Name: "group-name-1"}, {ID: "group-id-2", Name: "group-name-2"}}}, + want: []string{"group-id-1", "group-id-2"}, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + wrappers.FeatureFlags[featureFlagsConstants.AccessManagementEnabled] = false + if got := getGroupsForRequest(ttt.args.groups); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("getGroupsForRequest() = %v, want %v", got, ttt.want) + } + }) + } +} + +func Test_getGroupsToAssign(t *testing.T) { + type args struct { + receivedGroups []*wrappers.Group + existingGroups []*wrappers.Group + } + tests := []struct { + name string + args args + want []*wrappers.Group + }{ + { + name: "When calling with received groups, assign only the non-existing ones", + args: args{ + receivedGroups: []*wrappers.Group{{ID: "group-id-2", Name: "group-name-2"}, {ID: "group-id-3", Name: "group-name-3"}}, + existingGroups: []*wrappers.Group{{ID: "group-id-1", Name: "group-name-1"}, {ID: "group-id-2", Name: "group-name-2"}}, + }, + want: []*wrappers.Group{{ID: "group-id-3", Name: "group-name-3"}}, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + if got := getGroupsToAssign(ttt.args.receivedGroups, ttt.args.existingGroups); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("getGroupsToAssign() = %v, want %v", got, ttt.want) + } + }) + } +} diff --git a/internal/services/projects.go b/internal/services/projects.go new file mode 100644 index 000000000..a1c4f8f98 --- /dev/null +++ b/internal/services/projects.go @@ -0,0 +1,246 @@ +package services + +import ( + "slices" + "strconv" + "time" + + "github.com/checkmarx/ast-cli/internal/logger" + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +const ( + ErrorCodeFormat = "%s: CODE: %d, %s\n" + FailedCreatingProj = "Failed creating a project" + FailedGettingProj = "Failed getting a project" + failedUpdatingProj = "Failed updating a project" + failedFindingGroup = "Failed finding groups" + failedProjectApplicationAssociation = "Failed association project to application" +) + +func FindProject( + applicationID []string, + projectName string, + cmd *cobra.Command, + projectsWrapper wrappers.ProjectsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationWrapper wrappers.ApplicationsWrapper, +) (string, error) { + params := make(map[string]string) + params["names"] = projectName + resp, _, err := projectsWrapper.Get(params) + if err != nil { + return "", err + } + + for i := 0; i < len(resp.Projects); i++ { + if resp.Projects[i].Name == projectName { + projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList) + projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList) + projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag) + return updateProject( + resp, + cmd, + projectsWrapper, + groupsWrapper, + accessManagementWrapper, + applicationWrapper, + projectName, + applicationID, + projectGroups, + projectTags, + projectPrivatePackage) + } + } + + projectGroups, _ := cmd.Flags().GetString(commonParams.ProjectGroupList) + projectPrivatePackage, _ := cmd.Flags().GetString(commonParams.ProjecPrivatePackageFlag) + projectID, err := createProject(projectName, cmd, projectsWrapper, groupsWrapper, accessManagementWrapper, applicationWrapper, applicationID, projectGroups, projectPrivatePackage) + if err != nil { + logger.PrintIfVerbose("error in creating project!") + return "", err + } + return projectID, nil +} + +func createProject( + projectName string, + cmd *cobra.Command, + projectsWrapper wrappers.ProjectsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, + applicationID []string, + projectGroups string, + projectPrivatePackage string, +) (string, error) { + projectTags, _ := cmd.Flags().GetString(commonParams.ProjectTagList) + applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName) + var projModel = wrappers.Project{} + projModel.Name = projectName + projModel.ApplicationIds = applicationID + + if projectPrivatePackage != "" { + projModel.PrivatePackage, _ = strconv.ParseBool(projectPrivatePackage) + } + projModel.Tags = createTagMap(projectTags) + logger.PrintIfVerbose("Creating new project") + resp, errorModel, err := projectsWrapper.Create(&projModel) + projectID := "" + if errorModel != nil { + err = errors.Errorf(ErrorCodeFormat, FailedCreatingProj, errorModel.Code, errorModel.Message) + } + if err == nil { + projectID = resp.ID + if applicationName != "" || len(applicationID) > 0 { + err = verifyApplicationAssociationDone(applicationName, projectID, applicationsWrapper) + if err != nil { + return projectID, err + } + } + + if projectGroups != "" { + err = UpsertProjectGroups(groupsWrapper, &projModel, projectsWrapper, accessManagementWrapper, nil, projectGroups, projectID, projectName) + if err != nil { + return projectID, err + } + } + } + return projectID, err +} + +func verifyApplicationAssociationDone(applicationName, projectID string, applicationsWrapper wrappers.ApplicationsWrapper) error { + var applicationRes *wrappers.ApplicationsResponseModel + var err error + params := make(map[string]string) + params["name"] = applicationName + + logger.PrintIfVerbose("polling application until project association done or timeout of 2 min") + var timeoutDuration = 2 * time.Minute + timeout := time.Now().Add(timeoutDuration) + for time.Now().Before(timeout) { + applicationRes, err = applicationsWrapper.Get(params) + if err != nil { + return err + } else if applicationRes != nil && len(applicationRes.Applications) > 0 && + slices.Contains(applicationRes.Applications[0].ProjectIds, projectID) { + logger.PrintIfVerbose("application association done successfully") + return nil + } else if time.Now().After(timeout) { + return errors.Errorf("%s: %v", failedProjectApplicationAssociation, "timeout of 2 min for association") + } + time.Sleep(time.Second) + logger.PrintIfVerbose("application association polling - waiting for associating to complete") + } + + return errors.Errorf("%s: %v", failedProjectApplicationAssociation, "timeout of 2 min for association") +} + +//nolint:gocyclo +func updateProject( + resp *wrappers.ProjectsCollectionResponseModel, + cmd *cobra.Command, + projectsWrapper wrappers.ProjectsWrapper, + groupsWrapper wrappers.GroupsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, + applicationsWrapper wrappers.ApplicationsWrapper, + projectName string, + applicationID []string, + projectGroups string, + projectTags string, + projectPrivatePackage string, + +) (string, error) { + var projectID string + applicationName, _ := cmd.Flags().GetString(commonParams.ApplicationName) + var projModel = wrappers.Project{} + for i := 0; i < len(resp.Projects); i++ { + if resp.Projects[i].Name == projectName { + projectID = resp.Projects[i].ID + } + if resp.Projects[i].MainBranch != "" { + projModel.MainBranch = resp.Projects[i].MainBranch + } + if resp.Projects[i].RepoURL != "" { + projModel.RepoURL = resp.Projects[i].RepoURL + } + } + if projectGroups == "" && projectTags == "" && projectPrivatePackage == "" && len(applicationID) == 0 { + logger.PrintIfVerbose("No groups, applicationId or tags to update. Skipping project update.") + return projectID, nil + } + if projectPrivatePackage != "" { + projModel.PrivatePackage, _ = strconv.ParseBool(projectPrivatePackage) + } + + logger.PrintIfVerbose("Fetching existing Project for updating") + projModelResp, errModel, err := projectsWrapper.GetByID(projectID) + if errModel != nil { + err = errors.Errorf(ErrorCodeFormat, FailedGettingProj, errModel.Code, errModel.Message) + } + if err != nil { + return "", err + } + projModel.Name = projModelResp.Name + projModel.Groups = projModelResp.Groups + projModel.Tags = projModelResp.Tags + projModel.ApplicationIds = projModelResp.ApplicationIds + if projectTags != "" { + logger.PrintIfVerbose("Updating project tags") + projModel.Tags = createTagMap(projectTags) + } + if len(applicationID) > 0 { + logger.PrintIfVerbose("Updating project applicationIds") + projModel.ApplicationIds = createApplicationIds(applicationID, projModelResp.ApplicationIds) + } + err = projectsWrapper.Update(projectID, &projModel) + if err != nil { + return "", errors.Errorf("%s: %v", failedUpdatingProj, err) + } + + if applicationName != "" || len(applicationID) > 0 { + err = verifyApplicationAssociationDone(applicationName, projectID, applicationsWrapper) + if err != nil { + return projectID, err + } + } + + if projectGroups != "" { + err = UpsertProjectGroups(groupsWrapper, &projModel, projectsWrapper, accessManagementWrapper, projModelResp, projectGroups, projectID, projectName) + if err != nil { + return projectID, err + } + } + return projectID, nil +} + +func UpsertProjectGroups(groupsWrapper wrappers.GroupsWrapper, projModel *wrappers.Project, projectsWrapper wrappers.ProjectsWrapper, + accessManagementWrapper wrappers.AccessManagementWrapper, projModelResp *wrappers.ProjectResponseModel, + projectGroups string, projectID string, projectName string) error { + groupsMap, groupErr := CreateGroupsMap(projectGroups, groupsWrapper) + if groupErr != nil { + return errors.Errorf("%s: %v", failedUpdatingProj, groupErr) + } + + projModel.Groups = getGroupsForRequest(groupsMap) + if projModelResp != nil { + groups := append(getGroupsForRequest(groupsMap), projModelResp.Groups...) + projModel.Groups = groups + } + + err := AssignGroupsToProjectNewAccessManagement(projectID, projectName, groupsMap, accessManagementWrapper) + if err != nil { + return err + } + + logger.PrintIfVerbose("Updating project groups") + err = projectsWrapper.Update(projectID, projModel) + if err != nil { + return errors.Errorf("%s: %v", failedUpdatingProj, err) + } + return nil +} diff --git a/internal/services/projects_test.go b/internal/services/projects_test.go new file mode 100644 index 000000000..358a23a28 --- /dev/null +++ b/internal/services/projects_test.go @@ -0,0 +1,255 @@ +package services + +import ( + "testing" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/checkmarx/ast-cli/internal/wrappers/mock" + "github.com/spf13/cobra" +) + +func TestFindProject(t *testing.T) { + type args struct { + applicationID []string + projectName string + cmd *cobra.Command + projectsWrapper wrappers.ProjectsWrapper + groupsWrapper wrappers.GroupsWrapper + accessManagementWrapper wrappers.AccessManagementWrapper + applicationsWrapper wrappers.ApplicationsWrapper + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "Testing the update flow", + args: args{ + applicationID: []string{"1"}, + projectName: "MOCK", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + applicationsWrapper: &mock.ApplicationsMockWrapper{}, + }, + want: "MOCK", + wantErr: false, + }, + { + name: "Testing the create flow", + args: args{ + projectName: "new-MOCK", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + applicationsWrapper: &mock.ApplicationsMockWrapper{}, + }, + want: "ID-new-MOCK", + wantErr: false, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := FindProject( + ttt.args.applicationID, + ttt.args.projectName, + ttt.args.cmd, + ttt.args.projectsWrapper, + ttt.args.groupsWrapper, + ttt.args.accessManagementWrapper, + ttt.args.applicationsWrapper) + if (err != nil) != ttt.wantErr { + t.Errorf("FindProject() error = %v, wantErr %v", err, ttt.wantErr) + return + } + if got != ttt.want { + t.Errorf("FindProject() got = %v, want %v", got, ttt.want) + } + }) + } +} + +func Test_createProject(t *testing.T) { + type args struct { + projectName string + cmd *cobra.Command + projectsWrapper wrappers.ProjectsWrapper + groupsWrapper wrappers.GroupsWrapper + accessManagementWrapper wrappers.AccessManagementWrapper + applicationsWrapper wrappers.ApplicationsWrapper + applicationID []string + projectGroups string + projectPrivatePackage string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {name: "When called with a new project name return the Id of the newly created project", args: args{ + projectName: "new-project-name", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectGroups: "", + }, want: "ID-new-project-name", wantErr: false}, + {name: "When called with a new project name and non existing project groups return error", args: args{ + projectName: "new-project-name", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectGroups: "grp1,grp2", + }, want: "ID-new-project-name", wantErr: true}, + {name: "When called with mock fake error model return fake error from project create", args: args{ + projectName: "mock-some-error-model", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectGroups: "", + }, want: "", wantErr: true}, + {name: "When called with mock fake group error return fake error from project create", args: args{ + projectName: "new-project-name", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectGroups: "fake-group-error", + }, want: "ID-new-project-name", wantErr: true}, + {name: "When called with a new project name and projectPrivatePackage set to true return the Id of the newly created project", args: args{ + projectName: "new-project-name", + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectGroups: "", + projectPrivatePackage: "true", + }, want: "ID-new-project-name", wantErr: false}, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := createProject( + ttt.args.projectName, + ttt.args.cmd, + ttt.args.projectsWrapper, + ttt.args.groupsWrapper, + ttt.args.accessManagementWrapper, + ttt.args.applicationsWrapper, + ttt.args.applicationID, + ttt.args.projectGroups, + ttt.args.projectPrivatePackage) + if (err != nil) != ttt.wantErr { + t.Errorf("createProject() error = %v, wantErr %v", err, ttt.wantErr) + return + } + if got != ttt.want { + t.Errorf("createProject() got = %v, want %v", got, ttt.want) + } + }) + } +} + +func Test_updateProject(t *testing.T) { + type args struct { + resp *wrappers.ProjectsCollectionResponseModel + cmd *cobra.Command + projectsWrapper wrappers.ProjectsWrapper + groupsWrapper wrappers.GroupsWrapper + accessManagementWrapper wrappers.AccessManagementWrapper + applicationsWrapper wrappers.ApplicationsWrapper + projectName string + applicationID []string + projectGroups string + projectTags string + projectPrivatePackage string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + {name: "When called with existing project, update the project and return the project Id", args: args{ + resp: &wrappers.ProjectsCollectionResponseModel{ + Projects: []wrappers.ProjectResponseModel{ + {ID: "ID-project-name", Name: "project-name"}}, + }, + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectName: "project-name", + applicationID: nil, + }, want: "ID-project-name", wantErr: false}, + {name: "without app ID and with project tags", args: args{ + resp: &wrappers.ProjectsCollectionResponseModel{ + Projects: []wrappers.ProjectResponseModel{ + {ID: "ID-project-name", Name: "project-name"}}, + }, + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectName: "project-name", + projectTags: "tag1,tag2", + applicationID: nil, + }, want: "ID-project-name", wantErr: false}, + {name: "When called with application ID", args: args{ + resp: &wrappers.ProjectsCollectionResponseModel{ + Projects: []wrappers.ProjectResponseModel{ + {ID: "ID-project-name", Name: "project-name"}}, + }, + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + projectName: "project-name", + projectPrivatePackage: "true", + }, want: "ID-project-name", wantErr: false}, + {name: "When called with mock fake error model return fake error from project create", args: args{ + projectName: "mock-some-error-model", + resp: &wrappers.ProjectsCollectionResponseModel{ + Projects: []wrappers.ProjectResponseModel{ + {ID: "ID-mock-some-error-model", Name: "mock-some-error-model"}}, + }, + cmd: &cobra.Command{}, + projectsWrapper: &mock.ProjectsMockWrapper{}, + groupsWrapper: &mock.GroupsMockWrapper{}, + accessManagementWrapper: &mock.AccessManagementMockWrapper{}, + applicationID: []string{"1"}, + }, want: "", wantErr: true}, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := updateProject( + ttt.args.resp, + ttt.args.cmd, + ttt.args.projectsWrapper, + ttt.args.groupsWrapper, + ttt.args.accessManagementWrapper, + ttt.args.applicationsWrapper, + ttt.args.projectName, + ttt.args.applicationID, + ttt.args.projectGroups, + ttt.args.projectTags, + ttt.args.projectPrivatePackage) + if (err != nil) != ttt.wantErr { + t.Errorf("updateProject() error = %v, wantErr %v", err, ttt.wantErr) + return + } + if got != ttt.want { + t.Errorf("updateProject() got = %v, want %v", got, ttt.want) + } + }) + } +} diff --git a/internal/services/tags.go b/internal/services/tags.go new file mode 100644 index 000000000..77c9f1ed7 --- /dev/null +++ b/internal/services/tags.go @@ -0,0 +1,19 @@ +package services + +import "strings" + +func createTagMap(tagListStr string) map[string]string { + tagsList := strings.Split(tagListStr, ",") + tags := make(map[string]string) + for _, tag := range tagsList { + if len(tag) > 0 { + value := "" + keyValuePair := strings.Split(tag, ":") + if len(keyValuePair) > 1 { + value = keyValuePair[1] + } + tags[keyValuePair[0]] = value + } + } + return tags +} diff --git a/internal/services/tags_test.go b/internal/services/tags_test.go new file mode 100644 index 000000000..7a682979b --- /dev/null +++ b/internal/services/tags_test.go @@ -0,0 +1,31 @@ +package services + +import ( + "reflect" + "testing" +) + +func Test_createTagMap(t *testing.T) { + type args struct { + tagListStr string + } + tests := []struct { + name string + args args + want map[string]string + }{ + { + name: "Create tag map from tag string representing a map", + args: args{tagListStr: "tag1:val1,tag2:val2"}, + want: map[string]string{"tag1": "val1", "tag2": "val2"}, + }, + } + for _, tt := range tests { + ttt := tt + t.Run(tt.name, func(t *testing.T) { + if got := createTagMap(ttt.args.tagListStr); !reflect.DeepEqual(got, ttt.want) { + t.Errorf("createTagMap() = %v, want %v", got, ttt.want) + } + }) + } +} diff --git a/internal/wrappers/application-http.go b/internal/wrappers/application-http.go index c53ffdf17..157cb9258 100644 --- a/internal/wrappers/application-http.go +++ b/internal/wrappers/application-http.go @@ -4,7 +4,7 @@ import ( "encoding/json" "net/http" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" commonParams "github.com/checkmarx/ast-cli/internal/params" "github.com/pkg/errors" "github.com/spf13/viper" @@ -42,16 +42,16 @@ func (a *ApplicationsHTTPWrapper) Get(params map[string]string) (*ApplicationsRe switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: if err != nil { - return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + return nil, errors.Errorf(errorConstants.FailedToGetApplication) } return nil, nil case http.StatusForbidden: - return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + return nil, errors.Errorf(errorConstants.ApplicationDoesntExistOrNoPermission) case http.StatusOK: model := ApplicationsResponseModel{} err = decoder.Decode(&model) if err != nil { - return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + return nil, errors.Errorf(errorConstants.FailedToGetApplication) } return &model, nil default: diff --git a/internal/wrappers/byor-http.go b/internal/wrappers/byor-http.go new file mode 100644 index 000000000..749bcd809 --- /dev/null +++ b/internal/wrappers/byor-http.go @@ -0,0 +1,86 @@ +package wrappers + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + "github.com/checkmarx/ast-cli/internal/logger" + commonParams "github.com/checkmarx/ast-cli/internal/params" + "github.com/pkg/errors" + "github.com/spf13/viper" +) + +const ( + importsPath = "/imports" + successfulMessage = "The SARIF results were successfully imported into project %s importID: %s" +) + +type ByorHTTPWrapper struct { + path string + clientTimeout uint +} + +func NewByorHTTPWrapper(path string) ByorWrapper { + return &ByorHTTPWrapper{ + path: path, + clientTimeout: viper.GetUint(commonParams.ClientTimeoutKey), + } +} +func (b *ByorHTTPWrapper) Import(projectID, uploadURL string) (string, error) { + req := CreateImportsRequest{ + ProjectID: projectID, + UploadURL: uploadURL, + } + + jsonBytes, _ := json.Marshal(req) + resp, err := SendHTTPRequestWithJSONContentType(http.MethodPost, b.path+importsPath, bytes.NewBuffer(jsonBytes), true, b.clientTimeout) + if err != nil { + return "", err + } + defer func() { + _ = resp.Body.Close() + }() + decoder := json.NewDecoder(resp.Body) + switch resp.StatusCode { + case http.StatusForbidden: + return "", getError(decoder, errorConstants.StatusForbidden) + case http.StatusUnauthorized: + return "", getError(decoder, errorConstants.StatusUnauthorized) + case http.StatusInternalServerError: + byorErrorModel := ByorErrorModel{} + decodeErr := decoder.Decode(&byorErrorModel) + if decodeErr != nil { + return "", errors.Errorf(fmt.Sprintf(errorConstants.ImportSarifFileErrorMessageWithMessage, http.StatusInternalServerError, "Error decoding byor error model")) + } + return "", errors.Errorf(fmt.Sprintf(errorConstants.ImportSarifFileErrorMessageWithMessage, byorErrorModel.Code, byorErrorModel.Message)) + case http.StatusOK: + model := CreateImportsResponse{} + err = decoder.Decode(&model) + if err != nil { + return model.ImportID, errors.Errorf(errorConstants.ImportSarifFileError) + } + logger.Printf(successfulMessage, projectID, model.ImportID) + return model.ImportID, nil + default: + return "", errors.Errorf(fmt.Sprintf(errorConstants.ImportSarifFileErrorMessageWithMessage, resp.StatusCode, "")) + } +} + +func getError(decoder *json.Decoder, errorMessage string) error { + errorModel := ByorErrorModel{} + err := decoder.Decode(&errorModel) + if err != nil { + return errors.Errorf("Parsing error model failed - %s", err.Error()) + } + logger.PrintIfVerbose(errorModel.Message) + return errors.Errorf(errorMessage) +} + +type ByorErrorModel struct { + Message string `json:"message"` + Code int `json:"code"` + Details []string `json:"details"` +} diff --git a/internal/wrappers/byor.go b/internal/wrappers/byor.go new file mode 100644 index 000000000..9d1a90d10 --- /dev/null +++ b/internal/wrappers/byor.go @@ -0,0 +1,14 @@ +package wrappers + +type ByorWrapper interface { + Import(projectID, uploadURL string) (string, error) +} + +type CreateImportsRequest struct { + ProjectID string `json:"projectId"` + UploadURL string `json:"UploadUrl"` +} + +type CreateImportsResponse struct { + ImportID string `json:"importId"` +} diff --git a/internal/wrappers/client.go b/internal/wrappers/client.go index 724d3d4cf..7bd286be9 100644 --- a/internal/wrappers/client.go +++ b/internal/wrappers/client.go @@ -14,7 +14,7 @@ import ( "strings" "time" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + applicationErrors "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/golang-jwt/jwt" "github.com/checkmarx/ast-cli/internal/logger" @@ -447,6 +447,7 @@ func getClientCredentials(accessKeyID, accessKeySecret, astAPKey, authURI string func getClientCredentialsFromCache(tokenExpirySeconds int) string { logger.PrintIfVerbose("Checking cache for API access token.") + expired := time.Since(cachedAccessTime) > time.Duration(tokenExpirySeconds-expiryGraceSeconds)*time.Second if !expired { logger.PrintIfVerbose("Using cached API access token!") diff --git a/internal/wrappers/feature-flags-http.go b/internal/wrappers/feature-flags-http.go index 3806e79bf..c06e24c0d 100644 --- a/internal/wrappers/feature-flags-http.go +++ b/internal/wrappers/feature-flags-http.go @@ -5,6 +5,7 @@ import ( "net/http" "strings" + "github.com/pkg/errors" "github.com/spf13/viper" commonParams "github.com/checkmarx/ast-cli/internal/params" @@ -47,7 +48,6 @@ func (f FeatureFlagsHTTPWrapper) GetAll() (*FeatureFlagsResponseModel, error) { _ = resp.Body.Close() } }() - switch resp.StatusCode { case http.StatusBadRequest, http.StatusInternalServerError: return nil, err @@ -58,8 +58,9 @@ func (f FeatureFlagsHTTPWrapper) GetAll() (*FeatureFlagsResponseModel, error) { return nil, err } return &model, nil - + case http.StatusNotFound: + return nil, errors.New("feature flags not found") default: - return nil, nil + return nil, errors.New("failed to load feature flags for tenant") } } diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index 65f2b04d2..31a0f846d 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -1,6 +1,7 @@ package wrappers import ( + feature_flags "github.com/checkmarx/ast-cli/internal/constants/feature-flags" "github.com/checkmarx/ast-cli/internal/logger" ) @@ -27,6 +28,19 @@ var FeatureFlagsBaseMap = []CommandFlags{ { CommandName: "cx project create", }, + { + CommandName: "cx import", + FeatureFlags: []FlagBase{ + { + Name: MinioEnabled, + Default: true, + }, + { + Name: feature_flags.ByorEnabled, + Default: false, + }, + }, + }, { CommandName: "cx results show", FeatureFlags: []FlagBase{ diff --git a/internal/wrappers/mock/application-mock.go b/internal/wrappers/mock/application-mock.go index dce914bc7..d7d83df9b 100644 --- a/internal/wrappers/mock/application-mock.go +++ b/internal/wrappers/mock/application-mock.go @@ -3,7 +3,7 @@ package mock import ( "time" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/wrappers" "github.com/pkg/errors" ) @@ -12,23 +12,23 @@ type ApplicationsMockWrapper struct{} func (a ApplicationsMockWrapper) Get(params map[string]string) (*wrappers.ApplicationsResponseModel, error) { if params["name"] == NoPermissionApp { - return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + return nil, errors.Errorf(errorConstants.ApplicationDoesntExistOrNoPermission) } if params["name"] == ApplicationDoesntExist { - return nil, errors.Errorf(applicationErrors.ApplicationDoesntExistOrNoPermission) + return nil, errors.Errorf(errorConstants.ApplicationDoesntExistOrNoPermission) } - if params["name"] == FakeHTTPStatusBadRequest { - return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + if params["name"] == FakeBadRequest400 { + return nil, errors.Errorf(errorConstants.FailedToGetApplication) } - if params["name"] == FakeHTTPStatusInternalServerError { - return nil, errors.Errorf(applicationErrors.FailedToGetApplication) + if params["name"] == FakeInternalServerError500 { + return nil, errors.Errorf(errorConstants.FailedToGetApplication) } mockApplication := wrappers.Application{ ID: "mockID", Name: "MOCK", Description: "This is a mock application", Criticality: 2, - ProjectIds: []string{"ProjectID1", "ProjectID2"}, + ProjectIds: []string{"ProjectID1", "ProjectID2", "MOCK", "test_project", "ID-new-project-name"}, CreatedAt: time.Now(), } diff --git a/internal/wrappers/mock/byor-mock.go b/internal/wrappers/mock/byor-mock.go new file mode 100644 index 000000000..d92a959ef --- /dev/null +++ b/internal/wrappers/mock/byor-mock.go @@ -0,0 +1,22 @@ +package mock + +import ( + "fmt" + + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" +) + +type ByorMockWrapper struct{} + +func (b *ByorMockWrapper) Import(projectID, uploadURL string) (string, error) { + if projectID == FakeUnauthorized401 { + return "", fmt.Errorf(errorConstants.StatusUnauthorized) + } + if projectID == FakeForbidden403 { + return "", fmt.Errorf(errorConstants.StatusForbidden) + } + if projectID == FakeInternalServerError500 { + return "", fmt.Errorf(errorConstants.StatusInternalServerError) + } + return "", nil +} diff --git a/internal/wrappers/mock/constants.go b/internal/wrappers/mock/constants.go index 96d7d5a63..39b4b9984 100644 --- a/internal/wrappers/mock/constants.go +++ b/internal/wrappers/mock/constants.go @@ -1,8 +1,10 @@ package mock const ( - ApplicationDoesntExist = "application-doesnt-exist" - NoPermissionApp = "NoPermissionApp" - FakeHTTPStatusBadRequest = "fake-http-status-bad-request" - FakeHTTPStatusInternalServerError = "fake-http-status-internal-server-error" + ApplicationDoesntExist = "application-doesnt-exist" + NoPermissionApp = "NoPermissionApp" + FakeBadRequest400 = "fake-http-status-bad-request" + FakeUnauthorized401 = "fake-unauthorized-response" + FakeForbidden403 = "fake-forbidden-response" + FakeInternalServerError500 = "fake--internal-server-error" ) diff --git a/internal/wrappers/mock/groups-mock.go b/internal/wrappers/mock/groups-mock.go index 721b820e6..624282cbd 100644 --- a/internal/wrappers/mock/groups-mock.go +++ b/internal/wrappers/mock/groups-mock.go @@ -1,10 +1,16 @@ package mock -import "github.com/checkmarx/ast-cli/internal/wrappers" +import ( + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" +) type GroupsMockWrapper struct { } -func (g *GroupsMockWrapper) Get(_ string) ([]wrappers.Group, error) { +func (g *GroupsMockWrapper) Get(groupName string) ([]wrappers.Group, error) { + if groupName == "fake-group-error" { + return nil, errors.Errorf("fake grroup error") + } return []wrappers.Group{{ID: "1", Name: "group"}}, nil } diff --git a/internal/wrappers/mock/projects-mock.go b/internal/wrappers/mock/projects-mock.go index 78be2a59a..7bff9f76a 100644 --- a/internal/wrappers/mock/projects-mock.go +++ b/internal/wrappers/mock/projects-mock.go @@ -3,6 +3,7 @@ package mock import ( "fmt" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/wrappers" ) @@ -13,7 +14,15 @@ func (p *ProjectsMockWrapper) Create(model *wrappers.Project) ( *wrappers.ErrorModel, error) { fmt.Println("Called Create in ProjectsMockWrapper") + if model.Name == "mock-some-error-model" { + return nil, &wrappers.ErrorModel{ + Message: "some error message", + Type: "", + Code: 1, + }, fmt.Errorf("some error") + } return &wrappers.ProjectResponseModel{ + ID: fmt.Sprintf("ID-%s", model.Name), Name: model.Name, ApplicationIds: model.ApplicationIds, }, nil, nil @@ -40,21 +49,42 @@ func (p *ProjectsMockWrapper) Get(params map[string]string) ( filteredTotalCount = 0 } + var model *wrappers.ProjectsCollectionResponseModel + switch name := params["names"]; name { + case "fake-kics-scanner-fail": + model = getProjectResponseModel(fmt.Sprintf("%s-id", name), name, filteredTotalCount) + case "fake-multiple-scanner-fails": + model = getProjectResponseModel(fmt.Sprintf("%s-id", name), name, filteredTotalCount) + case "fake-sca-fail-partial": + model = getProjectResponseModel(fmt.Sprintf("%s-id", name), name, filteredTotalCount) + case "fake-kics-fail-sast-canceled": + model = getProjectResponseModel(fmt.Sprintf("%s-id", name), name, filteredTotalCount) + default: + model = getProjectResponseModel("MOCK", "MOCK", filteredTotalCount) + } + + return model, nil, nil +} + +func getProjectResponseModel(id, name string, filteredTotalCount int) *wrappers.ProjectsCollectionResponseModel { return &wrappers.ProjectsCollectionResponseModel{ FilteredTotalCount: uint(filteredTotalCount), Projects: []wrappers.ProjectResponseModel{ { - ID: "MOCK", - Name: "MOCK", + ID: id, + Name: name, }, }, - }, nil, nil + } } func (p *ProjectsMockWrapper) GetByID(projectID string) ( *wrappers.ProjectResponseModel, *wrappers.ErrorModel, error) { + if projectID == "ID-mock-some-error-model" { + return nil, &wrappers.ErrorModel{Code: 202, Message: "some-message"}, nil + } fmt.Println("Called GetByID in ProjectsMockWrapper") return &wrappers.ProjectResponseModel{ ID: projectID, @@ -69,6 +99,31 @@ func (p *ProjectsMockWrapper) GetByID(projectID string) ( }, nil, nil } +func (p *ProjectsMockWrapper) GetByName(name string) ( + *wrappers.ProjectResponseModel, + *wrappers.ErrorModel, + error) { + fmt.Println("Called GetByName in ProjectsMockWrapper") + if name == "mock-missing-file-path" { + return nil, nil, fmt.Errorf(errorConstants.ImportFilePathIsRequired) + } + if name == "" { + return nil, nil, fmt.Errorf(errorConstants.ProjectNameIsRequired) + } + return &wrappers.ProjectResponseModel{ + ID: "MOCK", + Name: name, + Tags: map[string]string{ + "a": "b", + "c": "d", + }, + Groups: []string{ + "a", + "b", + }, + }, nil, nil +} + func (p *ProjectsMockWrapper) GetBranchesByID(_ string, _ map[string]string) ( []string, *wrappers.ErrorModel, diff --git a/internal/wrappers/mock/results-mock.go b/internal/wrappers/mock/results-mock.go index 731b65d0b..558ff7154 100644 --- a/internal/wrappers/mock/results-mock.go +++ b/internal/wrappers/mock/results-mock.go @@ -42,11 +42,17 @@ func (r ResultsMockWrapper) GetAllResultsPackageByScanID(params map[string]strin return &scaPackages, nil, nil } -func (r ResultsMockWrapper) GetAllResultsByScanID(_ map[string]string) ( +func (r ResultsMockWrapper) GetAllResultsByScanID(params map[string]string) ( *wrappers.ScanResultsCollection, *wrappers.WebError, error, ) { + if params["scan-id"] == "MOCK_NO_VULNERABILITIES" { + return &wrappers.ScanResultsCollection{ + TotalCount: 0, + Results: nil, + }, nil, nil + } const mock = "mock" var dependencyPath = wrappers.DependencyPath{ID: mock, Name: mock, Version: mock, IsResolved: true, IsDevelopment: false, Locations: nil} var dependencyArray = [][]wrappers.DependencyPath{{dependencyPath}} @@ -215,88 +221,3 @@ func (r ResultsMockWrapper) GetAllResultsByScanID(_ map[string]string) ( func (r ResultsMockWrapper) GetResultsURL(projectID string) (string, error) { return fmt.Sprintf("projects/%s/overview", projectID), nil } - -func (r ResultsMockWrapper) GetScanSummariesByScanIDS(params map[string]string) (*wrappers.ScanSummariesModel, *wrappers.WebError, error) { - if params["scan-ids"] == "MOCKWEBERR" { - return nil, &wrappers.WebError{ - Message: "web error", - }, nil - } - if params["scan-ids"] == "MOCKERR" { - return nil, nil, fmt.Errorf("mock error") - } - return &wrappers.ScanSummariesModel{ - ScansSummaries: []wrappers.ScanSumaries{ - { - SastCounters: wrappers.SastCounters{ - SeverityCounters: []wrappers.SeverityCounters{ - { - Severity: "info", - Counter: 1, - }, - { - Severity: "low", - Counter: 1, - }, - { - Severity: "medium", - Counter: 1, - }, - { - Severity: "high", - Counter: 1, - }, - }, - TotalCounter: 4, - FilesScannedCounter: 1, - }, - KicsCounters: wrappers.KicsCounters{ - SeverityCounters: []wrappers.SeverityCounters{ - { - Severity: "info", - Counter: 1, - }, - { - Severity: "low", - Counter: 1, - }, - { - Severity: "medium", - Counter: 1, - }, - { - Severity: "high", - Counter: 1, - }, - }, - - TotalCounter: 4, - FilesScannedCounter: 1, - }, - ScaCounters: wrappers.ScaCounters{ - SeverityCounters: []wrappers.SeverityCounters{ - { - Severity: "info", - Counter: 1, - }, - { - Severity: "low", - Counter: 1, - }, - { - Severity: "medium", - Counter: 1, - }, - { - Severity: "high", - Counter: 1, - }, - }, - - TotalCounter: 4, - FilesScannedCounter: 1, - }, - }, - }, - }, nil, nil -} diff --git a/internal/wrappers/mock/sca-realtime-mock.go b/internal/wrappers/mock/sca-realtime-mock.go index c25da5727..a37985292 100644 --- a/internal/wrappers/mock/sca-realtime-mock.go +++ b/internal/wrappers/mock/sca-realtime-mock.go @@ -2,6 +2,7 @@ package mock import ( "fmt" + "github.com/checkmarx/ast-cli/internal/wrappers" ) diff --git a/internal/wrappers/mock/scans-mock.go b/internal/wrappers/mock/scans-mock.go index 088e4d9fe..9c4d44806 100644 --- a/internal/wrappers/mock/scans-mock.go +++ b/internal/wrappers/mock/scans-mock.go @@ -5,8 +5,8 @@ import ( "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/google/uuid" + "github.com/pkg/errors" ) type ScansMockWrapper struct { @@ -17,8 +17,33 @@ func (m *ScansMockWrapper) GetWorkflowByID(_ string) ([]*wrappers.ScanTaskRespon return nil, nil, nil } -func (m *ScansMockWrapper) Create(_ *wrappers.Scan) (*wrappers.ScanResponseModel, *wrappers.ErrorModel, error) { +func (m *ScansMockWrapper) Create(scanModel *wrappers.Scan) (*wrappers.ScanResponseModel, *wrappers.ErrorModel, error) { fmt.Println("Called Create in ScansMockWrapper") + if scanModel.Project.ID == "fake-kics-scanner-fail-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-scanner-fail", + Status: "MOCK", + }, nil, nil + } + if scanModel.Project.ID == "fake-multiple-scanner-fails-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-multiple-scanner-fails", + Status: "MOCK", + }, nil, nil + } + if scanModel.Project.ID == "fake-sca-fail-partial-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-sca-fail-partial-id", + Status: "MOCK", + }, nil, nil + } + if scanModel.Project.ID == "fake-kics-fail-sast-canceled-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-fail-sast-canceled-id", + Status: "MOCK", + }, nil, nil + } + return &wrappers.ScanResponseModel{ ID: uuid.New().String(), Status: "MOCK", @@ -78,6 +103,56 @@ func (m *ScansMockWrapper) Get(_ map[string]string) ( func (m *ScansMockWrapper) GetByID(scanID string) (*wrappers.ScanResponseModel, *wrappers.ErrorModel, error) { fmt.Println("Called GetByID in ScansMockWrapper") + if scanID == "fake-error-id" { + return nil, nil, errors.New("fake error message") + } + + if scanID == "fake-scan-id-kics-scanner-fail" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-scanner-fail", + Status: wrappers.ScanFailed, + StatusDetails: []wrappers.StatusInfo{ + { + Status: wrappers.ScanFailed, + Name: "kics", + Details: "error message from kics scanner", + ErrorCode: 1234, + }, + }, + }, nil, nil + } + if scanID == "fake-scan-id-multiple-scanner-fails" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-multiple-scanner-fails", + Status: wrappers.ScanFailed, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 2344}, + {Status: wrappers.ScanFailed, Name: "sca", Details: "error message from sca scanner", ErrorCode: 4343}, + }, + }, nil, nil + } + if scanID == "fake-scan-id-sca-fail-partial-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-sca-fail-partial-id", + Status: wrappers.ScanPartial, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "sca", Details: "error message from sca scanner", ErrorCode: 4343}, + }, + }, nil, nil + } + if scanID == "fake-scan-id-kics-fail-sast-canceled-id" { + return &wrappers.ScanResponseModel{ + ID: "fake-scan-id-kics-fail-sast-canceled-id", + Status: wrappers.ScanCanceled, + StatusDetails: []wrappers.StatusInfo{ + {Status: wrappers.ScanCompleted, Name: "general"}, + {Status: wrappers.ScanCompleted, Name: "sast"}, + {Status: wrappers.ScanFailed, Name: "kics", Details: "error message from kics scanner", ErrorCode: 6455}, + }, + }, nil, nil + } + var status wrappers.ScanStatus = "Completed" m.Running = !m.Running return &wrappers.ScanResponseModel{ diff --git a/internal/wrappers/projects-http.go b/internal/wrappers/projects-http.go index fe4814a32..8ed7bbd60 100644 --- a/internal/wrappers/projects-http.go +++ b/internal/wrappers/projects-http.go @@ -9,6 +9,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/viper" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" commonParams "github.com/checkmarx/ast-cli/internal/params" ) @@ -148,6 +149,31 @@ func (p *ProjectsHTTPWrapper) GetByID(projectID string) ( return handleProjectResponseWithBody(resp, err, http.StatusOK) } +func (p *ProjectsHTTPWrapper) GetByName(name string) ( + *ProjectResponseModel, + *ErrorModel, + error) { + params := make(map[string]string) + params["name"] = name + resp, _, err := p.Get(params) + if err != nil { + return nil, nil, err + } + + projectCount := len(resp.Projects) + if resp.Projects == nil || projectCount == 0 { + return nil, nil, errors.Errorf(errorConstants.ProjectNotExists) + } + + for i := range resp.Projects { + if resp.Projects[i].Name == name { + return &resp.Projects[i], nil, nil + } + } + + return nil, nil, errors.Errorf(errorConstants.ProjectNotExists) +} + func (p *ProjectsHTTPWrapper) GetBranchesByID(projectID string, params map[string]string) ([]string, *ErrorModel, error) { clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) diff --git a/internal/wrappers/projects.go b/internal/wrappers/projects.go index a1337a9ae..deef8ce18 100644 --- a/internal/wrappers/projects.go +++ b/internal/wrappers/projects.go @@ -51,6 +51,7 @@ type ProjectsWrapper interface { Update(projectID string, model *Project) error Get(params map[string]string) (*ProjectsCollectionResponseModel, *ErrorModel, error) GetByID(projectID string) (*ProjectResponseModel, *ErrorModel, error) + GetByName(name string) (*ProjectResponseModel, *ErrorModel, error) GetBranchesByID(projectID string, params map[string]string) ([]string, *ErrorModel, error) Delete(projectID string) (*ErrorModel, error) Tags() (map[string][]string, *ErrorModel, error) diff --git a/internal/wrappers/results-http.go b/internal/wrappers/results-http.go index 116beddb7..7205947ce 100644 --- a/internal/wrappers/results-http.go +++ b/internal/wrappers/results-http.go @@ -3,6 +3,7 @@ package wrappers import ( "encoding/json" "fmt" + "io" "net/http" "github.com/checkmarx/ast-cli/internal/logger" @@ -150,13 +151,6 @@ func (r *ResultsHTTPWrapper) GetAllResultsPackageByScanID(params map[string]stri decoder := json.NewDecoder(resp.Body) switch resp.StatusCode { - case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := WebError{} - err = decoder.Decode(&errorModel) - if err != nil { - return nil, nil, errors.Wrapf(err, failedToParseGetResults) - } - return nil, &errorModel, nil case http.StatusOK: var model []ScaPackageCollection err = decoder.Decode(&model) @@ -168,6 +162,9 @@ func (r *ResultsHTTPWrapper) GetAllResultsPackageByScanID(params map[string]stri logger.PrintIfVerbose("SCA packages for enrichment not found") return nil, nil, nil default: + responseData, _ := io.ReadAll(resp.Body) + responseString := string(responseData) + logger.PrintIfVerbose("Failed to get SCA packages " + responseString) return nil, nil, errors.Errorf(respStatusCode, resp.StatusCode) } } @@ -235,48 +232,6 @@ func (r *ResultsHTTPWrapper) GetResultsURL(projectID string) (string, error) { return baseURI, nil } -// GetScanSummariesByScanIDS will no longer be used because it does not support --filters flag -func (r *ResultsHTTPWrapper) GetScanSummariesByScanIDS(params map[string]string) ( - *ScanSummariesModel, - *WebError, - error, -) { - clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) - - resp, err := SendPrivateHTTPRequestWithQueryParams(http.MethodGet, r.scanSummaryPath, params, http.NoBody, clientTimeout) - if err != nil { - return nil, nil, err - } - - defer func() { - if err == nil { - _ = resp.Body.Close() - } - }() - - decoder := json.NewDecoder(resp.Body) - - switch resp.StatusCode { - case http.StatusBadRequest, http.StatusInternalServerError: - errorModel := WebError{} - err = decoder.Decode(&errorModel) - if err != nil { - return nil, nil, errors.Wrapf(err, failedTogetScanSummaries) - } - return nil, &errorModel, nil - case http.StatusOK: - model := ScanSummariesModel{} - err = decoder.Decode(&model) - if err != nil { - return nil, nil, errors.Wrapf(err, failedToParseScanSummaries) - } - - return &model, nil, nil - default: - return nil, nil, errors.Errorf(respStatusCode, resp.StatusCode) - } -} - func DefaultMapValue(params map[string]string, key, value string) { if _, ok := params[key]; !ok { params[key] = value diff --git a/internal/wrappers/results.go b/internal/wrappers/results.go index c90664fb4..5aae15c85 100644 --- a/internal/wrappers/results.go +++ b/internal/wrappers/results.go @@ -2,7 +2,6 @@ package wrappers type ResultsWrapper interface { GetAllResultsByScanID(params map[string]string) (*ScanResultsCollection, *WebError, error) - GetScanSummariesByScanIDS(params map[string]string) (*ScanSummariesModel, *WebError, error) GetAllResultsPackageByScanID(params map[string]string) (*[]ScaPackageCollection, *WebError, error) GetAllResultsTypeByScanID(params map[string]string) (*[]ScaTypeCollection, *WebError, error) GetResultsURL(projectID string) (string, error) diff --git a/internal/wrappers/uploads-http.go b/internal/wrappers/uploads-http.go index 10b959bbb..edccfdef1 100644 --- a/internal/wrappers/uploads-http.go +++ b/internal/wrappers/uploads-http.go @@ -5,6 +5,7 @@ import ( "net/http" "os" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" commonParams "github.com/checkmarx/ast-cli/internal/params" "github.com/pkg/errors" "github.com/spf13/viper" @@ -29,6 +30,7 @@ func (u *UploadsHTTPWrapper) UploadFile(sourcesFile string) (*string, error) { } *preSignedURL = string(preSignedURLBytes) viper.Set(commonParams.UploadURLEnv, *preSignedURL) + file, err := os.Open(sourcesFile) if err != nil { return nil, errors.Errorf("Failed to open file %s: %s", sourcesFile, err.Error()) @@ -62,6 +64,8 @@ func (u *UploadsHTTPWrapper) UploadFile(sourcesFile string) (*string, error) { }() switch resp.StatusCode { + case http.StatusUnauthorized: + return nil, errors.Errorf(errorConstants.StatusUnauthorized) case http.StatusOK: return preSignedURL, nil default: diff --git a/test/integration/data/malformed-sarif.sarif b/test/integration/data/malformed-sarif.sarif new file mode 100644 index 000000000..01db99d4c --- /dev/null +++ b/test/integration/data/malformed-sarif.sarif @@ -0,0 +1,52 @@ +{ + "version": "2.1.0", + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", + "runs": [ + { + "tool": { + "driver": { + "name": "ESLint", + "informationUri": "https://eslint.org", + "rules": [ + { + "id": "no-unused-vars", + "shortDescription": { + "text": "disallow unused variables" + }, + "helpUri": "https://eslint.org/docs/rules/no-unused-vars", + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" + } + } + ], + "results": [ + { + "level": "error", + "text": "'x' is assigned a value but never used." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 5 + } + } + } + ], + "ruleId": "no-unused-vars", + "ruleIndex": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/integration/data/sarif-missing-version.sarif b/test/integration/data/sarif-missing-version.sarif new file mode 100644 index 000000000..2c61fba21 --- /dev/null +++ b/test/integration/data/sarif-missing-version.sarif @@ -0,0 +1,56 @@ +{ + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", + "runs": [ + { + "tool": { + "driver": { + "name": "ESLint", + "informationUri": "https://eslint.org", + "rules": [ + { + "id": "no-unused-vars", + "shortDescription": { + "text": "disallow unused variables" + }, + "helpUri": "https://eslint.org/docs/rules/no-unused-vars", + "properties": { + "category": "Variables" + } + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" + } + } + ], + "results": [ + { + "level": "error", + "message": { + "text": "'x' is assigned a value but never used." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 5 + } + } + } + ], + "ruleId": "no-unused-vars", + "ruleIndex": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/integration/data/sarif.sarif b/test/integration/data/sarif.sarif new file mode 100644 index 000000000..2d2fa5cac --- /dev/null +++ b/test/integration/data/sarif.sarif @@ -0,0 +1,57 @@ +{ + "version": "2.1.0", + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", + "runs": [ + { + "tool": { + "driver": { + "name": "ESLint", + "informationUri": "https://eslint.org", + "rules": [ + { + "id": "no-unused-vars", + "shortDescription": { + "text": "disallow unused variables" + }, + "helpUri": "https://eslint.org/docs/rules/no-unused-vars", + "properties": { + "category": "Variables" + } + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" + } + } + ], + "results": [ + { + "level": "error", + "message": { + "text": "'x' is assigned a value but never used." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 5 + } + } + } + ], + "ruleId": "no-unused-vars", + "ruleIndex": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/test/integration/data/sarif.zip b/test/integration/data/sarif.zip new file mode 100644 index 000000000..809217924 Binary files /dev/null and b/test/integration/data/sarif.zip differ diff --git a/test/integration/data/ssh-key-file.txt b/test/integration/data/ssh-key-file.txt new file mode 100644 index 000000000..5b30163b3 --- /dev/null +++ b/test/integration/data/ssh-key-file.txt @@ -0,0 +1 @@ +-rsa-ssh mocksshkey \ No newline at end of file diff --git a/test/integration/import_test.go b/test/integration/import_test.go new file mode 100644 index 000000000..a8abb783d --- /dev/null +++ b/test/integration/import_test.go @@ -0,0 +1,158 @@ +//go:build integration + +package integration + +import ( + "fmt" + "testing" + + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + featureFlagsConstants "github.com/checkmarx/ast-cli/internal/constants/feature-flags" + "github.com/checkmarx/ast-cli/internal/params" + "github.com/checkmarx/ast-cli/internal/wrappers" + "gotest.tools/assert" +) + +func TestImport_ImportSarifFileWithCorrectFlags_CreateImportSuccessfully(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + projectId, projectName := createProject(t, nil, nil) + defer deleteProject(t, projectId) + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + flag(params.ImportFilePath), "data/sarif.sarif", + } + err, _ := executeCommand(t, args...) + + assert.NilError(t, err, "Import command failed with existing project") + } +} + +func TestImport_ImportSarifFileWithCorrectFlagsZipFileExtention_CreateImportSuccessfully(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + + projectId, projectName := createProject(t, nil, nil) + defer deleteProject(t, projectId) + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + flag(params.ImportFilePath), "data/sarif.zip", + } + err, _ := executeCommand(t, args...) + + assert.NilError(t, err, "Import command failed with existing project") + } +} + +func TestImport_ImportSarifFileProjectDoesntExist_CreateImportWithProvidedNewNameSuccessfully(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + projectName := projectNameRandom + defer deleteProjectByName(t, projectName) + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + flag(params.ImportFilePath), "data/sarif.sarif", + } + err, _ := executeCommand(t, args...) + + assert.NilError(t, err, "Import command failed with non-existing project") + } +} + +func TestImport_ImportSarifFileMissingVersion_ImportFailWithCorrectMessage(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + assert.Assert(t, true, "Byor is disabled") + } else { + projectName := projectNameRandom + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + flag(params.ImportFilePath), "data/sarif-missing-version.sarif", + } + err, _ := executeCommand(t, args...) + + deleteProjectByName(t, projectName) + + assertError(t, err, fmt.Sprintf(errorConstants.ImportSarifFileErrorMessageWithMessage, 400, "")) + } +} + +func TestImport_ImportMalformedSarifFile_ImportFailWithCorrectMessage(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + + projectName := projectNameRandom + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + flag(params.ImportFilePath), "data/malformed-sarif.sarif", + } + err, _ := executeCommand(t, args...) + defer deleteProjectByName(t, projectName) + assertError(t, err, fmt.Sprintf(errorConstants.ImportSarifFileErrorMessageWithMessage, 400, "")) + } +} + +func TestImport_MissingImportFlag_ImportFailWithCorrectMessage(t *testing.T) { + // createASTIntegrationTestCommand is called just to load the FF values + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + + projectName := projectNameRandom + + args := []string{ + "utils", "import", + flag(params.ProjectName), projectName, + } + err, _ := executeCommand(t, args...) + deleteProjectByName(t, projectName) + assertError(t, err, errorConstants.ImportFilePathIsRequired) + } +} + +func TestGetProjectNameFunction_ProjectNameValueIsEmpty_ReturnRelevantError(t *testing.T) { + + createASTIntegrationTestCommand(t) + if !wrappers.FeatureFlags[featureFlagsConstants.ByorEnabled] { + //t.Skip("BYOR flag is currently disabled") + assert.Assert(t, true, "Byor is disabled") + } else { + + args := []string{ + "utils", "import", + flag(params.ProjectName), "", + flag(params.ImportFilePath), "data/sarif.sarif", + } + err, _ := executeCommand(t, args...) + assertError(t, err, errorConstants.ProjectNameIsRequired) + } +} diff --git a/test/integration/learnmore_test.go b/test/integration/learnmore_test.go index ac16d56b7..284374a73 100644 --- a/test/integration/learnmore_test.go +++ b/test/integration/learnmore_test.go @@ -3,10 +3,11 @@ package integration import ( + "testing" + "github.com/checkmarx/ast-cli/internal/params" "github.com/spf13/viper" "gotest.tools/assert" - "testing" ) func TestGetLearnMoreInformationFailure(t *testing.T) { diff --git a/test/integration/project_test.go b/test/integration/project_test.go index c78c75995..6f16ba7dc 100644 --- a/test/integration/project_test.go +++ b/test/integration/project_test.go @@ -11,12 +11,11 @@ import ( "strings" "testing" - "github.com/google/uuid" - "github.com/checkmarx/ast-cli/internal/commands/util/printer" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/google/uuid" "github.com/spf13/viper" "gotest.tools/assert" @@ -101,7 +100,7 @@ func TestProjectCreate_ApplicationDoesntExist_FailAndReturnErrorMessage(t *testi flag(params.ApplicationName), "application-that-doesnt-exist", ) - assertError(t, err, applicationErrors.ApplicationDoesntExistOrNoPermission) + assertError(t, err, errorConstants.ApplicationDoesntExistOrNoPermission) } func TestProjectCreate_ApplicationExists_CreateProjectSuccessfully(t *testing.T) { @@ -127,6 +126,16 @@ func TestCreateWithInvalidGroup(t *testing.T) { assertError(t, err, "Failed finding groups: [invalidGroup]") } +func TestProjectCreate_WhenCreatingProjectWithExistingName_FailProjectCreation(t *testing.T) { + err, _ := executeCommand( + t, "project", "create", + flag(params.FormatFlag), + printer.FormatJSON, + flag(params.ProjectName), "project", + ) + assertError(t, err, "Failed creating a project: CODE: 208, Failed to create a project, project name 'project' already exists\n") +} + // Test list project's branches func TestProjectBranches(t *testing.T) { @@ -184,6 +193,14 @@ func deleteProject(t *testing.T, projectID string) { ) } +func deleteProjectByName(t *testing.T, projectName string) { + projectsWrapper := wrappers.NewHTTPProjectsWrapper(viper.GetString(params.ProjectsPathKey)) + projectModel, _, err := projectsWrapper.GetByName(projectName) + if err == nil && projectModel != nil { + deleteProject(t, projectModel.ID) + } +} + func listProjectByID(t *testing.T, projectID string) []wrappers.ProjectResponseModel { idFilter := fmt.Sprintf("ids=%s", projectID) fmt.Println("Listing project for id ", projectID) diff --git a/test/integration/result_test.go b/test/integration/result_test.go index 611bffc2c..44ffdee25 100644 --- a/test/integration/result_test.go +++ b/test/integration/result_test.go @@ -10,9 +10,13 @@ import ( "strings" "testing" + "github.com/checkmarx/ast-cli/internal/commands" "github.com/checkmarx/ast-cli/internal/commands/util/printer" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/spf13/viper" + testifyAssert "github.com/stretchr/testify/assert" "gotest.tools/assert" ) @@ -21,7 +25,52 @@ const ( resultsDirectory = "output-results-folder/" ) -// Create a scan and test getting its results +func TestResultsExitCode_OnSendingFakeScanId_ShouldReturnNotFoundError(t *testing.T) { + bindKeysToEnvAndDefault(t) + scansPath := viper.GetString(params.ScansPathKey) + scansWrapper := wrappers.NewHTTPScansWrapper(scansPath) + results, _ := commands.GetScannerResults(scansWrapper, "FakeScanId", "sast,sca") + + testifyAssert.Nil(t, results, "results should be nil") +} + +func TestResultsExitCode_OnSuccessfulScan_ShouldReturnStatusCompleted(t *testing.T) { + scanID, _ := getRootScan(t) + + scansPath := viper.GetString(params.ScansPathKey) + scansWrapper := wrappers.NewHTTPScansWrapper(scansPath) + results, _ := commands.GetScannerResults(scansWrapper, scanID, "sast,sca") + + assert.Equal(t, 1, len(results)) + assert.Equal(t, wrappers.ScanCompleted, (results[0]).Status) + assert.Equal(t, "", (results[0]).Details) + assert.Equal(t, "", (results[0]).ErrorCode) + assert.Equal(t, "", (results[0]).Name) +} + +func TestResultsExitCode_NoScanIdSent_FailCommandWithError(t *testing.T) { + bindKeysToEnvAndDefault(t) + args := []string{ + "results", "exit-code", + flag(params.ScanTypes), "sast", + } + + err, _ := executeCommand(t, args...) + assert.ErrorContains(t, err, errorConstants.ScanIDRequired) +} + +func TestResultsExitCode_FakeScanIdSent_FailCommandWithError(t *testing.T) { + bindKeysToEnvAndDefault(t) + args := []string{ + "results", "exit-code", + flag(params.ScanTypes), "sast", + flag(params.ScanIDFlag), "FakeScanId", + } + + err, _ := executeCommand(t, args...) + assert.ErrorContains(t, err, "Failed showing a scan") +} + func TestResultListJson(t *testing.T) { assertRequiredParameter(t, "Please provide a scan ID", "results", "show") @@ -36,6 +85,7 @@ func TestResultListJson(t *testing.T) { flag(params.TargetFormatFlag), strings.Join( []string{ printer.FormatJSON, + printer.FormatIndentedJSON, printer.FormatSarif, printer.FormatSummary, printer.FormatSummaryConsole, diff --git a/test/integration/root_test.go b/test/integration/root_test.go index 24d28ae16..035cf7bc9 100644 --- a/test/integration/root_test.go +++ b/test/integration/root_test.go @@ -51,6 +51,11 @@ func TestMain(m *testing.M) { os.Exit(exitVal) } +func TestRootVersion(t *testing.T) { + testInstance = t + executeCmdNilAssertion(t, "test root version", "version") +} + // Create or return a scan to be shared between tests func getRootScan(t *testing.T, scanTypes ...string) (string, string) { testInstance = t diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index ee5b76a6b..8656d1c9e 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -22,9 +22,11 @@ import ( realtime "github.com/checkmarx/ast-cli/internal/commands/scarealtime" "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" - applicationErrors "github.com/checkmarx/ast-cli/internal/errors" + errorConstants "github.com/checkmarx/ast-cli/internal/constants/errors" + exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" "github.com/checkmarx/ast-cli/internal/params" "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/pkg/errors" "github.com/spf13/viper" "gotest.tools/assert" ) @@ -109,7 +111,7 @@ func TestScanCreate_ApplicationDoesntExist_FailScanWithError(t *testing.T) { } err, _ := executeCommand(t, args...) - assertError(t, err, applicationErrors.ApplicationDoesntExistOrNoPermission) + assertError(t, err, errorConstants.ApplicationDoesntExistOrNoPermission) } // Create scans from current dir, zip and url and perform assertions in executeScanAssertions @@ -189,7 +191,9 @@ func TestScaResolverArg(t *testing.T) { "sast,iac-security", viper.GetString(resolverEnvVar), ) + defer deleteProject(t, projectID) + assert.Assert( t, pollScanUntilStatus(t, scanID, wrappers.ScanCompleted, FullScanWait, ScanPollSleep), @@ -684,7 +688,7 @@ func TestPartialScanWithWrongPreset(t *testing.T) { } err, _ := executeCommand(t, args...) - assertError(t, err, "scan completed partially") + assertAstError(t, err, "scan completed partially", exitCodes.SastEngineFailedExitCode) } func TestFailedScanWithWrongPreset(t *testing.T) { @@ -702,9 +706,19 @@ func TestFailedScanWithWrongPreset(t *testing.T) { flag(params.PolicyTimeoutFlag), "999999", } - err, _ := executeCommand(t, args...) - assertError(t, err, "scan did not complete successfully") + assertAstError(t, err, "scan did not complete successfully", exitCodes.SastEngineFailedExitCode) +} + +func assertAstError(t *testing.T, err error, expectedErrorMessage string, expectedExitCode int) { + var e *wrappers.AstError + if errors.As(err, &e) { + assert.Equal(t, e.Error(), expectedErrorMessage) + assert.Equal(t, e.Code, expectedExitCode) + } else { + assertError(t, err, "Error is not of type AstError") + assert.Assert(t, false, fmt.Sprintf("Error is not of type AstError. Error message: %s", err.Error())) + } } func retrieveResultsFromScanId(t *testing.T, scanId string) (wrappers.ScanResultsCollection, error) { diff --git a/test/integration/tenant_test.go b/test/integration/tenant_test.go index 8d29add10..f5f27a771 100644 --- a/test/integration/tenant_test.go +++ b/test/integration/tenant_test.go @@ -3,23 +3,23 @@ package integration import ( - "testing" + "testing" - "github.com/checkmarx/ast-cli/internal/params" - "gotest.tools/assert" + "github.com/checkmarx/ast-cli/internal/params" + "gotest.tools/assert" ) func TestGetTenantConfigurationSuccessCaseJson(t *testing.T) { - err, _ := executeCommand( - t, "utils", "tenant", - flag(params.FormatFlag), "json", - ) - assert.NilError(t, err, "Must not fail") + err, _ := executeCommand( + t, "utils", "tenant", + flag(params.FormatFlag), "json", + ) + assert.NilError(t, err, "Must not fail") } func TestGetTenantConfigurationSuccessCaseList(t *testing.T) { - err, _ := executeCommand(t, "utils", "tenant") - assert.NilError(t, err, "Must not fail") + err, _ := executeCommand(t, "utils", "tenant") + assert.NilError(t, err, "Must not fail") } //func TestGetTenantConfigurationFailCase(t *testing.T) { diff --git a/test/integration/util_command.go b/test/integration/util_command.go index 0ecec9c58..50030347f 100644 --- a/test/integration/util_command.go +++ b/test/integration/util_command.go @@ -82,6 +82,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { policyEvaluationPath := viper.GetString(params.PolicyEvaluationPathKey) sastIncrementalPath := viper.GetString(params.SastMetadataPathKey) accessManagementPath := viper.GetString(params.AccessManagementPathKey) + byorPath := viper.GetString(params.ByorPathKey) scansWrapper := wrappers.NewHTTPScansWrapper(scans) applicationsWrapper := wrappers.NewApplicationsHTTPWrapper(applications) @@ -112,6 +113,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { policyWrapper := wrappers.NewHTTPPolicyWrapper(policyEvaluationPath) sastMetadataWrapper := wrappers.NewSastIncrementalHTTPWrapper(sastIncrementalPath) accessManagementWrapper := wrappers.NewAccessManagementHTTPWrapper(accessManagementPath) + ByorWrapper := wrappers.NewByorHTTPWrapper(byorPath) astCli := commands.NewAstCLI( applicationsWrapper, @@ -143,6 +145,7 @@ func createASTIntegrationTestCommand(t *testing.T) *cobra.Command { policyWrapper, sastMetadataWrapper, accessManagementWrapper, + ByorWrapper, ) return astCli }