diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dba7dc9fd..37ef322e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,7 @@ jobs: CX_AST_PASSWORD: ${{ secrets.CX_AST_PASSWORD }} CX_APIKEY: ${{ secrets.CX_APIKEY }} CX_TENANT: ${{ secrets.CX_TENANT }} + CX_SCAN_SSH_KEY: ${{ secrets.CX_SCAN_SSH_KEY }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PROXY_HOST: localhost PROXY_PORT: 3128 diff --git a/internal/commands/project.go b/internal/commands/project.go index af79de6dd..31b78cfeb 100644 --- a/internal/commands/project.go +++ b/internal/commands/project.go @@ -7,6 +7,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" + "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" commonParams "github.com/checkmarx/ast-cli/internal/params" @@ -23,6 +24,11 @@ const ( 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" ) var ( @@ -88,6 +94,8 @@ func NewProjectCommand(projectsWrapper wrappers.ProjectsWrapper, groupsWrapper w createProjCmd.PersistentFlags().String(commonParams.GroupList, "", "List of groups, ex: (PowerUsers,etc)") createProjCmd.PersistentFlags().StringP(commonParams.ProjectName, "", "", "Name of project") createProjCmd.PersistentFlags().StringP(commonParams.MainBranchFlag, "", "", "Main branch") + createProjCmd.PersistentFlags().String(commonParams.SSHKeyFlag, "", "Path to ssh private key") + createProjCmd.PersistentFlags().String(commonParams.RepoURLFlag, "", "Repository URL") listProjectsCmd := &cobra.Command{ Use: "list", @@ -283,7 +291,11 @@ func runCreateProjectCommand( if err != nil { return err } - updateTagValues(&input, cmd) + setupScanTags(&input, cmd) + err = validateConfiguration(cmd) + if err != nil { + return err + } var projModel = wrappers.Project{} var projResponseModel *wrappers.ProjectResponseModel var errorModel *wrappers.ErrorModel @@ -299,6 +311,7 @@ func runCreateProjectCommand( if err != nil { return errors.Wrapf(err, "%s", failedCreatingProj) } + // Checking the response if errorModel != nil { return errors.Errorf(ErrorCodeFormat, failedCreatingProj, errorModel.Code, errorModel.Message) @@ -308,10 +321,106 @@ func runCreateProjectCommand( return errors.Wrapf(err, "%s", failedCreatingProj) } } + + err = updateProjectConfigurationIfNeeded(cmd, projectsWrapper, projResponseModel.ID) + if err != nil { + return err + } + return nil } } +func updateProjectConfigurationIfNeeded(cmd *cobra.Command, projectsWrapper wrappers.ProjectsWrapper, projectID string) error { + // Just update project configuration id a repository url is defined + if cmd.Flags().Changed(commonParams.RepoURLFlag) { + var projectConfigurations []wrappers.ProjectConfiguration + + repoURL, _ := cmd.Flags().GetString(commonParams.RepoURLFlag) + + urlConf := getProjectConfiguration(repoConfKey, "repository", git, projOriginLevel, repoURL, "String", true) + + projectConfigurations = append(projectConfigurations, urlConf) + + if cmd.Flags().Changed(commonParams.SSHKeyFlag) { + sshKeyPath, _ := cmd.Flags().GetString(commonParams.SSHKeyFlag) + + sshKey, sshErr := util.ReadFileAsString(sshKeyPath) + if sshErr != nil { + return sshErr + } + + sshKeyConf := getProjectConfiguration(sshConfKey, "sshKey", git, projOriginLevel, sshKey, "Secret", true) + + projectConfigurations = append(projectConfigurations, sshKeyConf) + } + + _, configErr := projectsWrapper.UpdateConfiguration(projectID, projectConfigurations) + if configErr != nil { + return configErr + } + } + + return nil +} + +func getProjectConfiguration(key, name, category, level, value, valueType string, allowOverride bool) wrappers.ProjectConfiguration { + config := wrappers.ProjectConfiguration{} + config.Key = key + config.Name = name + config.Category = category + config.OriginLevel = level + config.Value = value + config.ValueType = valueType + config.AllowOverride = allowOverride + + return config +} + +func validateConfiguration(cmd *cobra.Command) error { + var sshKeyDefined bool + var repoURLDefined bool + + // Validate if ssh key is empty when provided + if cmd.Flags().Changed(commonParams.SSHKeyFlag) { + sshKey, _ := cmd.Flags().GetString(commonParams.SSHKeyFlag) + + if strings.TrimSpace(sshKey) == "" { + return errors.New("flag needs an argument: --ssh-key") + } + + sshKeyDefined = true + } + + // Validate if repo url is empty when provided + if cmd.Flags().Changed(commonParams.RepoURLFlag) { + repoURL, _ := cmd.Flags().GetString(commonParams.RepoURLFlag) + + if strings.TrimSpace(repoURL) == "" { + return errors.New("flag needs an argument: --repo-url") + } + + repoURLDefined = true + } + + // If ssh key is defined we have two checks to validate: + // 1. repo url needs to be provided + // 2. provided repo url needs to be a ssh url + if sshKeyDefined { + if !repoURLDefined { + return errors.New(mandatoryRepoURLError) + } + + repoURL, _ := cmd.Flags().GetString(commonParams.RepoURLFlag) + + if !util.IsSSHURL(repoURL) { + return errors.New(invalidRepoURL) + } + } + + return nil +} + func runListProjectsCommand(projectsWrapper wrappers.ProjectsWrapper) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { var allProjectsModel *wrappers.ProjectsCollectionResponseModel diff --git a/internal/commands/project_test.go b/internal/commands/project_test.go index 3115cc8e8..b40af41a9 100644 --- a/internal/commands/project_test.go +++ b/internal/commands/project_test.go @@ -6,6 +6,8 @@ import ( "testing" "gotest.tools/assert" + + "github.com/checkmarx/ast-cli/internal/commands/util" ) func TestProjectHelp(t *testing.T) { @@ -101,3 +103,64 @@ func TestRunProjectCreateInvalidGroup(t *testing.T) { "project", "create", "--project-name", "invalidprj", "--groups", "invalidgroup") assert.Assert(t, err.Error() == "Failed finding groups: [invalidgroup]") } + +func TestCreateProjectMissingSSHValue(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", " ")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) +} + +func TestCreateProjectMissingRepoURLWithSSHValue(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key", "--repo-url")...) + assert.Error(t, err, "flag needs an argument: --repo-url", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key", "--repo-url", "")...) + assert.Error(t, err, "flag needs an argument: --repo-url", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key", "--repo-url", " ")...) + assert.Error(t, err, "flag needs an argument: --repo-url", err.Error()) +} + +func TestCreateProjectMandatoryRepoURLWhenSSHKeyProvided(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key")...) + + assert.Error(t, err, mandatoryRepoURLError) +} + +func TestCreateProjectInvalidRepoURLWithSSHKey(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key", "--repo-url", "https://github.com/dummyuser/dummy_project.git")...) + + assert.Error(t, err, invalidRepoURL) +} + +func TestCreateProjectWrongSSHKeyPath(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "dummy_key", "--repo-url", "git@github.com:dummyRepo/dummyProject.git")...) + + expectedMessages := []string{ + "open dummy_key: The system cannot find the file specified.", + "open dummy_key: no such file or directory", + } + + assert.Assert(t, util.Contains(expectedMessages, err.Error())) +} + +func TestCreateProjectWithSSHKey(t *testing.T) { + baseArgs := []string{"project", "create", "--project-name", "MOCK"} + + execCmdNilAssertion(t, append(baseArgs, "--ssh-key", "data/sources.zip", "--repo-url", "git@github.com:dummyRepo/dummyProject.git")...) +} diff --git a/internal/commands/scan.go b/internal/commands/scan.go index bc9a47a37..235b1f6da 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/checkmarx/ast-cli/internal/commands/util/printer" "github.com/google/shlex" "github.com/pkg/errors" @@ -39,6 +40,8 @@ const ( mbBytes = 1024.0 * 1024.0 scaType = "sca" notExploitable = "NOT_EXPLOITABLE" + git = "git" + invalidSSHSource = "provided source does not need a key. Make sure you are defining the right source or remove the flag --ssh-key" ) var ( @@ -386,6 +389,9 @@ func scanCreateSubCommand( if err != nil { log.Fatal(err) } + + createScanCmd.PersistentFlags().String(commonParams.SSHKeyFlag, "", "Path to ssh private key") + return createScanCmd } @@ -440,7 +446,7 @@ func createProject( return projectID, err } -func updateTagValues(input *[]byte, cmd *cobra.Command) { +func setupScanTags(input *[]byte, cmd *cobra.Command) { tagListStr, _ := cmd.Flags().GetString(commonParams.TagList) tags := strings.Split(tagListStr, ",") var info map[string]interface{} @@ -479,17 +485,16 @@ func createTagMap(tagListStr string) map[string]string { return tags } -func updateScanRequestValues( +func setupScanTypeProjectAndConfig( input *[]byte, cmd *cobra.Command, - sourceType string, projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, ) error { var info map[string]interface{} newProjectName, _ := cmd.Flags().GetString(commonParams.ProjectName) _ = json.Unmarshal(*input, &info) - info["type"] = sourceType + info["type"] = getUploadType(cmd) // Handle the project settings if _, ok := info["project"]; !ok { var projectMap map[string]interface{} @@ -576,14 +581,12 @@ func addScaScan(cmd *cobra.Command) map[string]interface{} { return nil } -func determineScanTypes(cmd *cobra.Command) { +func validateScanTypes(cmd *cobra.Command) { userScanTypes, _ := cmd.Flags().GetString(commonParams.ScanTypes) if len(userScanTypes) > 0 { actualScanTypes = userScanTypes } -} -func validateScanTypes() { scanTypes := strings.Split(actualScanTypes, ",") for _, scanType := range scanTypes { isValid := false @@ -846,64 +849,82 @@ func addScaResults(zipWriter *zip.Writer) error { return nil } -func determineSourceFile( +func getUploadURLFromSource( + cmd *cobra.Command, uploadsWrapper wrappers.UploadsWrapper, - sourcesFile, sourceDir, sourceDirFilter, userIncludeFilter, scaResolver, scaResolverParams string, ) (string, error) { - var err error var preSignedURL string - if sourceDir != "" { + + sourceDirFilter, _ := cmd.Flags().GetString(commonParams.SourceDirFilterFlag) + userIncludeFilter, _ := cmd.Flags().GetString(commonParams.IncludeFilterFlag) + + zipFilePath, directoryPath, err := definePathForZipFileOrDirectory(cmd) + if err != nil { + return "", errors.Wrapf(err, "%s: Input in bad format", failedCreating) + } + + if directoryPath != "" { + var dirPathErr error + // Get sca resolver flags + scaResolverParams, dirPathErr := cmd.Flags().GetString(commonParams.ScaResolverParamsFlag) + if dirPathErr != nil { + scaResolverParams = "" + } + scaResolver, dirPathErr := cmd.Flags().GetString(commonParams.ScaResolverFlag) + if dirPathErr != nil { + scaResolver = "" + scaResolverParams = "" + } + // Make sure scaResolver only runs in sca type of scans if strings.Contains(actualScanTypes, scaType) { - err = runScaResolver(sourceDir, scaResolver, scaResolverParams) - if err != nil { - return "", errors.Wrapf(err, "ScaResolver error") + dirPathErr = runScaResolver(directoryPath, scaResolver, scaResolverParams) + if dirPathErr != nil { + return "", errors.Wrapf(dirPathErr, "ScaResolver error") } } - sourcesFile, err = compressFolder(sourceDir, sourceDirFilter, userIncludeFilter, scaResolver) - if err != nil { - return "", err + zipFilePath, dirPathErr = compressFolder(directoryPath, sourceDirFilter, userIncludeFilter, scaResolver) + if dirPathErr != nil { + return "", dirPathErr } } - if sourcesFile != "" { + if zipFilePath != "" { + var zipFilePathErr error // Send a request to uploads service var preSignedURL *string - preSignedURL, err = uploadsWrapper.UploadFile(sourcesFile) - if err != nil { - return "", errors.Wrapf(err, "%s: Failed to upload sources file\n", failedCreating) + preSignedURL, zipFilePathErr = uploadsWrapper.UploadFile(zipFilePath) + if zipFilePathErr != nil { + return "", errors.Wrapf(zipFilePathErr, "%s: Failed to upload sources file\n", failedCreating) } PrintIfVerbose(fmt.Sprintf("Uploading file to %s\n", *preSignedURL)) - return *preSignedURL, err + return *preSignedURL, zipFilePathErr } - return preSignedURL, err + return preSignedURL, nil } -func determineSourceType(sourcesFile string) (zipFile, sourceDir, scanRepoURL string, err error) { - if strings.HasPrefix(sourcesFile, "https://") || - strings.HasPrefix(sourcesFile, "http://") { - scanRepoURL = sourcesFile - - log.Printf("\n\nScanning branch %s...\n", viper.GetString(commonParams.BranchKey)) - } else { - info, statErr := os.Stat(sourcesFile) - if !os.IsNotExist(statErr) { - if filepath.Ext(sourcesFile) == ".zip" { - zipFile = sourcesFile - } else if info != nil && info.IsDir() { - sourceDir = filepath.ToSlash(sourcesFile) - if !strings.HasSuffix(sourceDir, "/") { - sourceDir += "/" - } - } else { - msg := fmt.Sprintf("Sources input has bad format: %v", sourcesFile) - err = errors.New(msg) +func definePathForZipFileOrDirectory(cmd *cobra.Command) (zipFile, sourceDir string, err error) { + source, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + sourceTrimmed := strings.TrimSpace(source) + + info, statErr := os.Stat(sourceTrimmed) + if !os.IsNotExist(statErr) { + if filepath.Ext(sourceTrimmed) == ".zip" { + zipFile = sourceTrimmed + } else if info != nil && info.IsDir() { + sourceDir = filepath.ToSlash(sourceTrimmed) + if !strings.HasSuffix(sourceDir, "/") { + sourceDir += "/" } } else { - msg := fmt.Sprintf("Sources input has bad format: %v", sourcesFile) + msg := fmt.Sprintf("Sources input has bad format: %v", sourceTrimmed) err = errors.New(msg) } + } else { + msg := fmt.Sprintf("Sources input has bad format: %v", sourceTrimmed) + err = errors.New(msg) } - return zipFile, sourceDir, scanRepoURL, err + + return zipFile, sourceDir, err } func runCreateScanCommand( @@ -964,81 +985,110 @@ func createScanModel( projectsWrapper wrappers.ProjectsWrapper, groupsWrapper wrappers.GroupsWrapper, ) (*wrappers.Scan, error) { - determineScanTypes(cmd) - validateScanTypes() - sourceDirFilter, _ := cmd.Flags().GetString(commonParams.SourceDirFilterFlag) - userIncludeFilter, _ := cmd.Flags().GetString(commonParams.IncludeFilterFlag) - sourcesFile, _ := cmd.Flags().GetString(commonParams.SourcesFlag) - sourcesFile, sourceDir, scanRepoURL, err := determineSourceType(strings.TrimSpace(sourcesFile)) - if err != nil { - return nil, errors.Wrapf(err, "%s: Input in bad format", failedCreating) - } - uploadType := getUploadType(sourceDir, sourcesFile) + validateScanTypes(cmd) + var input = []byte("{}") - err = updateScanRequestValues(&input, cmd, uploadType, projectsWrapper, groupsWrapper) + + // Define type, project and config in scan model + err := setupScanTypeProjectAndConfig(&input, cmd, projectsWrapper, groupsWrapper) if err != nil { return nil, err } - updateTagValues(&input, cmd) + + // set tags in scan model + setupScanTags(&input, cmd) + scanModel := wrappers.Scan{} // Try to parse to a scan model in order to manipulate the request payload err = json.Unmarshal(input, &scanModel) if err != nil { return nil, errors.Wrapf(err, "%s: Input in bad format", failedCreating) } - // Get sca resolver flags - scaResolverParams, err := cmd.Flags().GetString(commonParams.ScaResolverParamsFlag) - if err != nil { - scaResolverParams = "" - } - scaResolver, err := cmd.Flags().GetString(commonParams.ScaResolverFlag) - if err != nil { - scaResolver = "" - scaResolverParams = "" - } - // Set up the project handler (either git or upload) - pHandler, err := setupProjectHandler( - uploadsWrapper, - sourcesFile, - sourceDir, - sourceDirFilter, - userIncludeFilter, - scanRepoURL, - scaResolver, - scaResolverParams, - ) - scanModel.Handler, _ = json.Marshal(pHandler) + + // Set up the scan handler (either git or upload) + scanHandler, err := setupScanHandler(cmd, uploadsWrapper) if err != nil { return nil, err } + + scanModel.Handler, _ = json.Marshal(scanHandler) + + uploadType := getUploadType(cmd) + + if uploadType == "git" { + log.Printf("\n\nScanning branch %s...\n", viper.GetString(commonParams.BranchKey)) + } + return &scanModel, nil } -func getUploadType(sourceDir, sourcesFile string) string { - if sourceDir != "" || sourcesFile != "" { - return "upload" +func getUploadType(cmd *cobra.Command) string { + source, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + sourceTrimmed := strings.TrimSpace(source) + + if util.IsGitURL(sourceTrimmed) { + return git } - return "git" + + return "upload" } -func setupProjectHandler( +func setupScanHandler( + cmd *cobra.Command, uploadsWrapper wrappers.UploadsWrapper, - sourcesFile, sourceDir, sourceDirFilter, userIncludeFilter, scanRepoURL, scaResolver, scaResolverParams string, -) (wrappers.UploadProjectHandler, error) { - pHandler := wrappers.UploadProjectHandler{} - pHandler.Branch = viper.GetString(commonParams.BranchKey) +) (wrappers.ScanHandler, error) { + scanHandler := wrappers.ScanHandler{} + scanHandler.Branch = viper.GetString(commonParams.BranchKey) + + uploadType := getUploadType(cmd) + + if uploadType == git { + source, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + + scanHandler.RepoURL = strings.TrimSpace(source) + } else { + uploadURL, err := getUploadURLFromSource(cmd, uploadsWrapper) + if err != nil { + return scanHandler, err + } + + scanHandler.UploadURL = uploadURL + } + var err error - pHandler.UploadURL, err = determineSourceFile( - uploadsWrapper, - sourcesFile, - sourceDir, - sourceDirFilter, - userIncludeFilter, - scaResolver, - scaResolverParams, - ) - pHandler.RepoURL = scanRepoURL - return pHandler, err + + // Define SSH credentials if flag --ssh-key is provided + if cmd.Flags().Changed(commonParams.SSHKeyFlag) { + sshKeyPath, _ := cmd.Flags().GetString(commonParams.SSHKeyFlag) + + if strings.TrimSpace(sshKeyPath) == "" { + return scanHandler, errors.New("flag needs an argument: --ssh-key") + } + + source, _ := cmd.Flags().GetString(commonParams.SourcesFlag) + sourceTrimmed := strings.TrimSpace(source) + + if !util.IsSSHURL(sourceTrimmed) { + return scanHandler, errors.New(invalidSSHSource) + } + + err = defineSSHCredentials(strings.TrimSpace(sshKeyPath), &scanHandler) + } + + return scanHandler, err +} + +func defineSSHCredentials(sshKeyPath string, handler *wrappers.ScanHandler) error { + sshKey, err := util.ReadFileAsString(sshKeyPath) + + credentials := wrappers.GitCredentials{} + + credentials.Type = "ssh" + credentials.Value = sshKey + + handler.Credentials = credentials + + return err } func handleWait( diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 8d12507ae..d2f0ac7d4 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -8,6 +8,7 @@ import ( "gotest.tools/assert" + "github.com/checkmarx/ast-cli/internal/commands/util" "github.com/spf13/viper" ) @@ -15,7 +16,8 @@ const ( unknownFlag = "unknown flag: --chibutero" blankSpace = " " errorMissingBranch = "Failed creating a scan: Please provide a branch" - dummyRepo = "https://www.dummy-repo.com" + dummyRepo = "https://github.com/dummyuser/dummy_project.git" + dummySSHRepo = "git@github.com:dummyRepo/dummyProject.git" errorSourceBadFormat = "Failed creating a scan: Input in bad format: Sources input has bad format: " scaPathError = "ScaResolver error: exec: \"resolver\": executable file not found in " ) @@ -249,3 +251,51 @@ func TestScanWorkFlowWithScaFilter(t *testing.T) { err := executeTestCommand(cmd, baseArgs...) assert.NilError(t, err) } + +func TestCreateScanMissingSSHValue(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-s", "../..", "-b", "dummy_branch"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", "")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) + + err = execCmdNotNilAssertion(t, append(baseArgs, "--ssh-key", " ")...) + assert.Error(t, err, "flag needs an argument: --ssh-key", err.Error()) +} + +func TestCreateScanInvalidSSHSource(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-b", "dummy_branch"} + + // zip file with ssh + err := execCmdNotNilAssertion(t, append(baseArgs, "-s", "data/sources.zip", "--ssh-key", "dummy_key")...) + assert.Error(t, err, invalidSSHSource, err.Error()) + + // directory with ssh + err = execCmdNotNilAssertion(t, append(baseArgs, "-s", "../..", "--ssh-key", "dummy_key")...) + assert.Error(t, err, invalidSSHSource, err.Error()) + + // http url with ssh + err = execCmdNotNilAssertion(t, append(baseArgs, "-s", dummyRepo, "--ssh-key", "dummy_key")...) + assert.Error(t, err, invalidSSHSource, err.Error()) +} + +func TestCreateScanWrongSSHKeyPath(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-b", "dummy_branch"} + + err := execCmdNotNilAssertion(t, append(baseArgs, "-s", dummySSHRepo, "--ssh-key", "dummy_key")...) + + expectedMessages := []string{ + "open dummy_key: The system cannot find the file specified.", + "open dummy_key: no such file or directory", + } + + assert.Assert(t, util.Contains(expectedMessages, err.Error())) +} + +func TestCreateScanWithSSHKey(t *testing.T) { + baseArgs := []string{"scan", "create", "--project-name", "MOCK", "-b", "dummy_branch"} + + execCmdNilAssertion(t, append(baseArgs, "-s", dummySSHRepo, "--ssh-key", "data/sources.zip")...) +} diff --git a/internal/commands/util/completion.go b/internal/commands/util/completion.go index ce969b48a..13baf95bf 100644 --- a/internal/commands/util/completion.go +++ b/internal/commands/util/completion.go @@ -62,7 +62,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 == "" || Contains(cmd.ValidArgs, shellType) { return nil } diff --git a/internal/commands/util/utils.go b/internal/commands/util/utils.go index bb62ab2f0..8aed19a75 100644 --- a/internal/commands/util/utils.go +++ b/internal/commands/util/utils.go @@ -2,6 +2,8 @@ package util import ( "fmt" + "os" + "regexp" "github.com/MakeNowJust/heredoc" "github.com/checkmarx/ast-cli/internal/commands/util/usercount" @@ -9,6 +11,9 @@ import ( "github.com/spf13/cobra" ) +const gitURLRegex = "(?P:git|ssh|https?|git@[-\\w.]+):(\\/\\/)?(?P.*?)(\\.git)?$" +const sshURLRegex = "^(?P.*?)@(?P.*?):(?:(?P.*?)/)?(?P.*?/.*?)$" + func NewUtilsCommand(gitHubWrapper wrappers.GitHubWrapper, azureWrapper wrappers.AzureWrapper, bitBucketWrapper wrappers.BitBucketWrapper, @@ -39,10 +44,8 @@ func NewUtilsCommand(gitHubWrapper wrappers.GitHubWrapper, return utilsCmd } -/** -Tests if a string exists in the provided array -*/ -func contains(array []string, val string) bool { +// 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 @@ -57,3 +60,33 @@ func executeTestCommand(cmd *cobra.Command, args ...string) error { cmd.SilenceUsage = false return cmd.Execute() } + +// IsGitURL Check if provided URL is a valid git URL (http or ssh) +func IsGitURL(url string) bool { + compiledRegex := regexp.MustCompile(gitURLRegex) + urlParts := compiledRegex.FindStringSubmatch(url) + + if urlParts == nil || len(urlParts) < 4 { + return false + } + + return len(urlParts[1]) > 0 && len(urlParts[3]) > 0 +} + +// IsSSHURL Check if provided URL is a valid ssh URL +func IsSSHURL(url string) bool { + isGitURL, _ := regexp.MatchString(sshURLRegex, url) + + return isGitURL +} + +// ReadFileAsString Read a file and return its content as string +func ReadFileAsString(path string) (string, error) { + content, err := os.ReadFile(path) + + if err != nil { + return "", err + } + + return string(content), nil +} diff --git a/internal/params/flags.go b/internal/params/flags.go index 77e3efc7d..2f6f79a49 100644 --- a/internal/params/flags.go +++ b/internal/params/flags.go @@ -98,6 +98,8 @@ const ( GitLabURLFlag = "url-gitlab" URLFlagUsage = "API base URL" QueryIDFlag = "query-id" + SSHKeyFlag = "ssh-key" + RepoURLFlag = "repo-url" // INDIVIDUAL FILTER FLAGS SastFilterFlag = "sast-filter" diff --git a/internal/wrappers/mock/projects-mock.go b/internal/wrappers/mock/projects-mock.go index 6a453b5de..411ad0156 100644 --- a/internal/wrappers/mock/projects-mock.go +++ b/internal/wrappers/mock/projects-mock.go @@ -18,6 +18,11 @@ func (p *ProjectsMockWrapper) Create(model *wrappers.Project) ( }, nil, nil } +func (p *ProjectsMockWrapper) UpdateConfiguration(projectID string, configuration []wrappers.ProjectConfiguration) (*wrappers.ErrorModel, error) { + fmt.Println("Called Update Configuration for project", projectID, " in ProjectsMockWrapper with the configuration ", configuration) + return nil, nil +} + func (p *ProjectsMockWrapper) Get(params map[string]string) ( *wrappers.ProjectsCollectionResponseModel, *wrappers.ErrorModel, diff --git a/internal/wrappers/projects-http.go b/internal/wrappers/projects-http.go index 490ed8252..dc1cc0457 100644 --- a/internal/wrappers/projects-http.go +++ b/internal/wrappers/projects-http.go @@ -37,6 +37,25 @@ func (p *ProjectsHTTPWrapper) Create(model *Project) ( return handleProjectResponseWithBody(resp, err, http.StatusCreated) } +func (p *ProjectsHTTPWrapper) UpdateConfiguration(projectID string, configuration []ProjectConfiguration) (*ErrorModel, error) { + clientTimeout := viper.GetUint(commonParams.ClientTimeoutKey) + jsonBytes, err := json.Marshal(configuration) + if err != nil { + return nil, err + } + + params := map[string]string{ + commonParams.ProjectIDFlag: projectID, + } + + resp, err := SendHTTPRequestWithQueryParams(http.MethodPatch, "api/configuration/project", params, bytes.NewBuffer(jsonBytes), clientTimeout) + if err != nil { + return nil, err + } + + return handleProjectResponseWithNoBody(resp, err, http.StatusNoContent) +} + func (p *ProjectsHTTPWrapper) Get(params map[string]string) ( *ProjectsCollectionResponseModel, *ErrorModel, error) { diff --git a/internal/wrappers/projects.go b/internal/wrappers/projects.go index 223ad3c3a..43b09689f 100644 --- a/internal/wrappers/projects.go +++ b/internal/wrappers/projects.go @@ -33,6 +33,16 @@ type ProjectResponseModel struct { ScmRepoID string `json:"scmRepoId,omitempty"` } +type ProjectConfiguration struct { + Key string `json:"key"` + Name string `json:"name"` + Category string `json:"category"` + OriginLevel string `json:"originLevel"` + Value string `json:"value"` + ValueType string `json:"valuetype"` + AllowOverride bool `json:"allowOverride"` +} + type ProjectsWrapper interface { Create(model *Project) (*ProjectResponseModel, *ErrorModel, error) Get(params map[string]string) (*ProjectsCollectionResponseModel, *ErrorModel, error) @@ -40,4 +50,5 @@ type ProjectsWrapper interface { GetBranchesByID(projectID string, params map[string]string) ([]string, *ErrorModel, error) Delete(projectID string) (*ErrorModel, error) Tags() (map[string][]string, *ErrorModel, error) + UpdateConfiguration(projectID string, configuration []ProjectConfiguration) (*ErrorModel, error) } diff --git a/internal/wrappers/scans.go b/internal/wrappers/scans.go index 68d455d71..5adfdb4ab 100644 --- a/internal/wrappers/scans.go +++ b/internal/wrappers/scans.go @@ -31,12 +31,14 @@ type Config struct { Value map[string]string `protobuf:"bytes,2,rep,name=value,proto3" json:"value,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` } -type UploadProjectHandler struct { +type ScanHandler struct { // representative branch Branch string `json:"branch,omitempty"` // representative repository url RepoURL string `json:"repoUrl"` UploadURL string `json:"uploadUrl"` + // Credentials + Credentials GitCredentials `json:"credentials"` } type GitProjectHandler struct { @@ -96,7 +98,7 @@ type ScanProject struct { type Scan struct { Type string `json:"type"` // [git|upload] - Handler json.RawMessage `json:"handler"` // One of [GitProjectHandler|UploadProjectHandler] + Handler json.RawMessage `json:"handler"` // One of [GitProjectHandler|ScanHandler] Project ScanProject `json:"project,omitempty"` Config []Config `json:"config,omitempty"` Tags map[string]string `json:"tags,omitempty"` diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index e18c2c63f..d0c4c9dd6 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -116,40 +116,6 @@ func TestAuthRegister(t *testing.T) { flag(params.ClientRolesFlag), strings.Join(commands.RoleSlice, ","), ) assert.Error(t, err, "User does not have permission for roles [ast-admin ast-scanner]") - //assert.NilError(t, err, "Register should pass") - // - //result, err := io.ReadAll(buffer) - //assert.NilError(t, err, "Reading result should pass") - // - //lines := strings.Split(string(result), "\n") - // - //assert.Assert(t, strings.Contains(lines[0], "CX_CLIENT_ID="+clientIDPrefix)) - //assert.Assert(t, strings.Contains(lines[1], "CX_CLIENT_SECRET=")) - // - //clientID := strings.Split(lines[0], "=")[1] - //secret := strings.Split(lines[1], "=")[1] - //uuidLen := len(uuid.New().String()) - // - //assert.Assert(t, strings.Contains(clientID, clientIDPrefix)) - //assert.Assert(t, len(clientID) == len(clientIDPrefix)+uuidLen) - //assert.Assert(t, len(secret) == uuidLen) - // - //_, err = uuid.Parse(secret) - //assert.NilError(t, err, "Parsing UUID should pass") - // - //validateCommand, buffer := createRedirectedTestCommand(t) - // - //err = execute( - // validateCommand, - // "auth", - // "validate", - // flag(params.AccessKeyIDFlag), - // clientID, - // flag(params.AccessKeySecretFlag), - // secret, - //) - // - //assertSuccessAuthentication(t, err, buffer, defaultSuccessValidationMessage) } func TestFailProxyAuth(t *testing.T) { diff --git a/test/integration/project_test.go b/test/integration/project_test.go index aa959efe0..de282e231 100644 --- a/test/integration/project_test.go +++ b/test/integration/project_test.go @@ -4,18 +4,24 @@ package integration import ( "fmt" + "github.com/google/uuid" "io" + "io/ioutil" "log" + "os" "strings" "testing" "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/spf13/viper" "gotest.tools/assert" ) +const SSHKeyFilePath = "ssh-key-file.txt" + // End-to-end test of project handling. // - Create a project // - Get and assert the project exists @@ -72,7 +78,6 @@ func TestCreateEmptyProjectName(t *testing.T) { // Create the same project twice and assert that it fails func TestCreateAlreadyExistingProject(t *testing.T) { - assertRequiredParameter(t, "Project name is required", "project", "create") _, projectName := getRootProject(t) @@ -102,9 +107,9 @@ func TestProjectBranches(t *testing.T) { "branches", ) - projectId, _ := getRootProject(t) + projectID, _ := getRootProject(t) - buffer := executeCmdNilAssertion(t, "Branches should be listed", "project", "branches", "--project-id", projectId) + buffer := executeCmdNilAssertion(t, "Branches should be listed", "project", "branches", "--project-id", projectID) result, readingError := io.ReadAll(buffer) assert.NilError(t, readingError, "Reading result should pass") @@ -115,6 +120,7 @@ func createProject(t *testing.T, tags map[string]string) (string, string) { projectName := getProjectNameForTest() + "_for_project" tagsStr := formatTags(tags) + fmt.Printf("Creating project : %s \n", projectName) outBuffer := executeCmdNilAssertion( t, "Creating a project should pass", "project", "create", @@ -135,6 +141,7 @@ func createProject(t *testing.T, tags map[string]string) (string, string) { func deleteProject(t *testing.T, projectID string) { log.Println("Deleting the project with id ", projectID) + fmt.Println("Deleting the project with id ", projectID) executeCmdNilAssertion( t, "Deleting a project should pass", @@ -147,16 +154,17 @@ func deleteProject(t *testing.T, projectID string) { func listProjectByID(t *testing.T, projectID string) []wrappers.ProjectResponseModel { idFilter := fmt.Sprintf("ids=%s", projectID) - + fmt.Println("Listing project for id ", projectID) outputBuffer := executeCmdNilAssertion( t, "Getting the project should pass", "project", "list", flag(params.FormatFlag), printer.FormatJSON, flag(params.FilterFlag), idFilter, ) - + fmt.Println("Listing project for id output buffer", outputBuffer) var projects []wrappers.ProjectResponseModel _ = unmarshall(t, outputBuffer, &projects, "Reading all projects response JSON should pass") + fmt.Println("Listing project for id projects length: ", len(projects)) return projects } @@ -175,3 +183,34 @@ func showProject(t *testing.T, projectID string) wrappers.ProjectResponseModel { return project } + +func TestCreateProjectWithSSHKey(t *testing.T) { + projectName := fmt.Sprintf("ast-cli-tests_%s", uuid.New().String()) + "_for_project" + tagsStr := formatTags(Tags) + + _ = viper.BindEnv("CX_SCAN_SSH_KEY") + sshKey := viper.GetString("CX_SCAN_SSH_KEY") + + _ = ioutil.WriteFile(SSHKeyFilePath, []byte(sshKey), 0644) + defer func() { _ = os.Remove(SSHKeyFilePath) }() + + fmt.Printf("Creating project : %s \n", projectName) + outBuffer := executeCmdNilAssertion( + t, "Creating a project with ssh key should pass", + "project", "create", + flag(params.FormatFlag), printer.FormatJSON, + flag(params.ProjectName), projectName, + flag(params.BranchFlag), "master", + flag(params.TagList), tagsStr, + flag(params.RepoURLFlag), SSHRepo, + flag(params.SSHKeyFlag), SSHKeyFilePath, + ) + + createdProject := wrappers.ProjectResponseModel{} + createdProjectJSON := unmarshall(t, outBuffer, &createdProject, "Reading project create response JSON should pass") + + fmt.Println("Response after project is created : ", string(createdProjectJSON)) + fmt.Printf("New project created with id: %s \n", createdProject.ID) + + deleteProject(t, createdProject.ID) +} diff --git a/test/integration/root_test.go b/test/integration/root_test.go index 8fadcb495..8fc39e4c3 100644 --- a/test/integration/root_test.go +++ b/test/integration/root_test.go @@ -3,6 +3,7 @@ package integration import ( + "fmt" "log" "os" "testing" @@ -16,6 +17,7 @@ const ( Dir = "./data" Zip = "data/sources.zip" SlowRepo = "https://github.com/WebGoat/WebGoat" + SSHRepo = "git@github.com:hmmachadocx/hmmachado_dummy_project.git" SlowRepoBranch = "develop" resolverEnvVar = "SCA_RESOLVER" resolverEnvVarDefault = "./ScaResolver" @@ -77,6 +79,7 @@ func getRootProject(t *testing.T) (string, string) { testInstance = t if len(rootProjectId) > 0 { + fmt.Printf("Using the projectID: " + rootProjectId) log.Println("Using the projectID: ", rootProjectId) log.Println("Using the projectName: ", rootProjectName) return rootProjectId, rootProjectName diff --git a/test/integration/scan_test.go b/test/integration/scan_test.go index 223911504..d47014311 100644 --- a/test/integration/scan_test.go +++ b/test/integration/scan_test.go @@ -33,7 +33,6 @@ type ScanWorkflowResponse struct { // Create a scan with an empty project name // Assert the scan fails with correct message func TestScanCreateEmptyProjectName(t *testing.T) { - args := []string{ "scan", "create", flag(params.ProjectName), "", @@ -48,7 +47,6 @@ func TestScanCreateEmptyProjectName(t *testing.T) { // Create scans from current dir, zip and url and perform assertions in executeScanAssertions func TestScansE2E(t *testing.T) { - scanID, projectID := createScan(t, Zip, Tags) defer deleteProject(t, projectID) @@ -136,7 +134,7 @@ func TestCancelScan(t *testing.T) { defer deleteProject(t, projectID) defer deleteScan(t, scanID) - // cancelling too quickly after creating fails the scan... + // canceling too quickly after creating fails the scan... time.Sleep(30 * time.Second) executeCmdNilAssertion(t, "Cancel should pass", "scan", "cancel", flag(params.ScanIDFlag), scanID) @@ -275,7 +273,7 @@ func TestBrokenLinkScan(t *testing.T) { // - Get scan with 'scan show' and assert the ID // - Assert all tags exist and are assigned to the scan // - Delete the scan and assert it is deleted -func executeScanAssertions(t *testing.T, projectID string, scanID string, tags map[string]string) { +func executeScanAssertions(t *testing.T, projectID, scanID string, tags map[string]string) { response := listScanByID(t, scanID) assert.Equal(t, len(response), 1, "Total scans should be 1") @@ -349,7 +347,7 @@ func getCreateArgs(source string, tags map[string]string, scanTypes string) []st return getCreateArgsWithName(source, tags, projectName, scanTypes) } -func getCreateArgsWithName(source string, tags map[string]string, projectName string, scanTypes string) []string { +func getCreateArgsWithName(source string, tags map[string]string, projectName, scanTypes string) []string { args := []string{ "scan", "create", flag(params.ProjectName), projectName, @@ -363,7 +361,6 @@ func getCreateArgsWithName(source string, tags map[string]string, projectName st } func executeCreateScan(t *testing.T, args []string) (string, string) { - buffer := executeScanGetBuffer(t, args) createdScan := wrappers.ScanResponseModel{} @@ -547,3 +544,24 @@ func TestScanWorkFlowWithSastEngineFilter(t *testing.T) { } } } + +func TestScanCreateWithSSHKey(t *testing.T) { + _ = viper.BindEnv("CX_SCAN_SSH_KEY") + sshKey := viper.GetString("CX_SCAN_SSH_KEY") + + _ = ioutil.WriteFile(SSHKeyFilePath, []byte(sshKey), 0644) + + _, projectName := getRootProject(t) + + args := []string{ + "scan", "create", + flag(params.ProjectName), projectName, + flag(params.SourcesFlag), SSHRepo, + flag(params.BranchFlag), "main", + flag(params.SSHKeyFlag), SSHKeyFilePath, + } + + executeCmdWithTimeOutNilAssertion(t, "Create a scan with ssh-key should pass", 4*time.Minute, args...) + + _ = os.Remove(SSHKeyFilePath) +}