diff --git a/go.mod b/go.mod index 0c359d5f4..38d0e023f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/go-github/v45 v45.2.0 github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 - github.com/jfrog/froggit-go v1.21.1 + github.com/jfrog/froggit-go v1.22.0 github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260504054219-ba16d20c7b0f github.com/jfrog/jfrog-cli-security v1.28.0 diff --git a/go.sum b/go.sum index c5224019a..6a52763d7 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,8 @@ github.com/jfrog/archiver/v3 v3.6.3 h1:hkAmPjBw393tPmQ07JknLNWFNZjXdy2xFEnOW9wwO github.com/jfrog/archiver/v3 v3.6.3/go.mod h1:5V9l+Fte30Y4qe9dUOAd3yNTf8lmtVNuhKNrvI8PMhg= github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295 h1:EH0h86KwGvNHWyEBQoHoU9WfMMKy1GJ6jJQNmfy6E0U= github.com/jfrog/build-info-go v1.13.1-0.20260429070557-93b98034d295/go.mod h1:+OCtMb22/D+u7Wne5lzkjJjaWr0LRZcHlDwTH86Mpwo= -github.com/jfrog/froggit-go v1.21.1 h1:I/XUOO6GQ1d/rmBlM361F8T654C3ohIWrpw23xNL9JY= -github.com/jfrog/froggit-go v1.21.1/go.mod h1:umBiakJB0CSPFfe0AHVaC3n9xsmUT7NGkDCny3bRchI= +github.com/jfrog/froggit-go v1.22.0 h1:eeN5F8sOUo+h2cXkzArAu4nvSdjkDTAZtgqwrct70qg= +github.com/jfrog/froggit-go v1.22.0/go.mod h1:wRDryqyp3oe+eHgME2mpnEQmO8XBECIPagFwj0nHmdI= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYLipdsOFMY= diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index 72cba09d6..f74dc99e4 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -295,7 +295,8 @@ func (sr *ScanRepositoryCmd) fixSinglePackageAndCreatePR(repository *utils.Repos return } if existsInRemote { - log.Info(fmt.Sprintf("A pull request updating the dependency '%s' to version '%s' already exists. Skipping...", vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) + log.Info(fmt.Sprintf("Skipping fix pull request for dependency '%s' to version '%s': a fix branch already exists. If the pull request was previously closed, delete the fix branch to allow a new one to be created.", + vulnDetails.ImpactedDependencyName, vulnDetails.SuggestedFixedVersion)) return } diff --git a/utils/comment.go b/utils/comment.go index 5b1643b23..b3381cd01 100644 --- a/utils/comment.go +++ b/utils/comment.go @@ -5,10 +5,12 @@ import ( "errors" "fmt" "regexp" + "slices" "sort" "strconv" "github.com/jfrog/froggit-go/vcsclient" + "github.com/jfrog/froggit-go/vcsutils" "github.com/jfrog/gofrog/datastructures" "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/results" @@ -51,7 +53,12 @@ func HandlePullRequestCommentsAfterScan(issues *issues.ScansIssuesCollection, re // Add summary scan comment if issues.IssuesExists(repo.FrogbotConfig.ShowSecretsAsPrComment) || !repo.FrogbotConfig.HideSuccessBannerForNoIssues { - for _, comment := range generatePullRequestSummaryComment(*issues, resultContext, repo.FrogbotConfig.ShowSecretsAsPrComment, repo.OutputWriter) { + comments := generatePullRequestSummaryComment(*issues, resultContext, repo.FrogbotConfig.ShowSecretsAsPrComment, repo.OutputWriter) + if repo.OutputWriter.VcsProvider() == vcsutils.BitbucketCloud { + // Bitbucket Cloud Activity feed displays comments newest-first, so post in reverse to keep summary on top. + slices.Reverse(comments) + } + for _, comment := range comments { if err = client.AddPullRequestComment(context.Background(), repo.RepoOwner, repo.RepoName, comment, pullRequestID); err != nil { err = errors.New("couldn't add pull request comment: " + err.Error()) return diff --git a/utils/consts.go b/utils/consts.go index f124f9e1a..ba90f472b 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -15,6 +15,7 @@ const ( GitHub vcsProvider = "github" GitLab vcsProvider = "gitlab" BitbucketServer vcsProvider = "bitbucketServer" + BitbucketCloud vcsProvider = "bitbucketCloud" AzureRepos vcsProvider = "azureRepos" // CI providers params diff --git a/utils/getconfiguration.go b/utils/getconfiguration.go index 97429c60f..1177b3e19 100644 --- a/utils/getconfiguration.go +++ b/utils/getconfiguration.go @@ -410,7 +410,9 @@ func extractGitParamsFromEnvs() (*Git, error) { return nil, err } - // Mandatory only for Bitbucket Server, this authentication detail is required for performing git operations. + // Mandatory for Bitbucket Server. For Bitbucket Cloud, username is required only when using + // Basic Auth (Atlassian API token or app password). Bearer token users (Repository/Workspace + // Access Tokens) leave this unset and authenticate via JF_GIT_TOKEN alone. if err = readParamFromEnv(GitBitBucketUsernameEnv, &gitEnvParams.Username); err != nil && gitEnvParams.GitProvider == vcsutils.BitbucketServer { return nil, err } @@ -463,13 +465,15 @@ func extractVcsProviderFromEnv() (vcsutils.VcsProvider, error) { return vcsutils.GitHub, nil case string(GitLab): return vcsutils.GitLab, nil - // For backward compatibility, we are accepting also "bitbucket server" + // For backward compatibility, we are accepting also "bitbucket server" and "bitbucket cloud" case string(BitbucketServer), "bitbucket server": return vcsutils.BitbucketServer, nil + case string(BitbucketCloud): + return vcsutils.BitbucketCloud, nil case string(AzureRepos): return vcsutils.AzureRepos, nil } - return 0, fmt.Errorf("%s should be one of: '%s', '%s', '%s' or '%s'", GitProvider, GitHub, GitLab, BitbucketServer, AzureRepos) + return 0, fmt.Errorf("%s should be one of: '%s', '%s', '%s', '%s' or '%s'", GitProvider, GitHub, GitLab, BitbucketServer, BitbucketCloud, AzureRepos) } func SanitizeEnv() error { diff --git a/utils/getconfiguration_test.go b/utils/getconfiguration_test.go index b82a0d38c..4d4979e2d 100644 --- a/utils/getconfiguration_test.go +++ b/utils/getconfiguration_test.go @@ -122,12 +122,33 @@ func TestExtractVcsProviderFromEnv(t *testing.T) { assert.NoError(t, err) assert.Equal(t, vcsutils.BitbucketServer, vcsProvider) + SetEnvAndAssert(t, map[string]string{GitProvider: string(BitbucketCloud)}) + vcsProvider, err = extractVcsProviderFromEnv() + assert.NoError(t, err) + assert.Equal(t, vcsutils.BitbucketCloud, vcsProvider) + SetEnvAndAssert(t, map[string]string{GitProvider: string(AzureRepos)}) vcsProvider, err = extractVcsProviderFromEnv() assert.NoError(t, err) assert.Equal(t, vcsutils.AzureRepos, vcsProvider) } +func TestExtractGitParamsFromEnvs_BitbucketCloudNoUsernameRequired(t *testing.T) { + defer func() { + assert.NoError(t, SanitizeEnv()) + }() + SetEnvAndAssert(t, map[string]string{ + GitRepoEnv: "frogbot", + GitProvider: string(BitbucketCloud), + GitRepoOwnerEnv: "jfrog", + GitTokenEnv: "token123", + }) + // Bitbucket Cloud supports Bearer token auth (Repository/Workspace Access Tokens) without a username. + params, err := extractGitParamsFromEnvs() + assert.NoError(t, err) + assert.Empty(t, params.Username) +} + func TestExtractGitParamsFromEnvs(t *testing.T) { defer func() { assert.NoError(t, SanitizeEnv()) @@ -138,7 +159,7 @@ func TestExtractGitParamsFromEnvs(t *testing.T) { SetEnvAndAssert(t, map[string]string{GitRepoEnv: "frogbot"}) _, err = extractGitParamsFromEnvs() - assert.EqualError(t, err, "JF_GIT_PROVIDER should be one of: 'github', 'gitlab', 'bitbucketServer' or 'azureRepos'") + assert.EqualError(t, err, "JF_GIT_PROVIDER should be one of: 'github', 'gitlab', 'bitbucketServer', 'bitbucketCloud' or 'azureRepos'") SetEnvAndAssert(t, map[string]string{GitProvider: "github"}) _, err = extractGitParamsFromEnvs() diff --git a/utils/git.go b/utils/git.go index 508a126d5..9e68be2ef 100644 --- a/utils/git.go +++ b/utils/git.go @@ -3,6 +3,7 @@ package utils import ( "errors" "fmt" + "github.com/jfrog/jfrog-client-go/utils/errorutils" "net/http" "regexp" "strings" @@ -22,7 +23,6 @@ import ( "github.com/go-git/go-git/v5/plumbing/format/gitignore" "github.com/go-git/go-git/v5/plumbing/object" githttp "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" ) @@ -377,7 +377,6 @@ func (gm *GitManager) Push(force bool, branchName string) error { // On dry run do not push to any remote return nil } - // Pushing to remote if err := gm.localGitRepository.Push(&git.PushOptions{ RemoteName: gm.remoteName, Auth: gm.auth, @@ -525,9 +524,10 @@ func (gm *GitManager) GetRemoteName() string { } func toBasicAuth(username, token string) *githttp.BasicAuth { - // The username can be anything except for an empty string + // Bitbucket Cloud Repository/Workspace Access Tokens require "x-token-auth" as the git username. + // This is also safe for all other providers where the username is irrelevant for token auth. if username == "" { - username = "username" + username = "x-token-auth" } // Bitbucket server username starts with ~ prefix as the project key. We need to trim it for the authentication username = strings.TrimPrefix(username, "~") diff --git a/utils/outputwriter/outputwriter.go b/utils/outputwriter/outputwriter.go index e8c83ae90..9388dee94 100644 --- a/utils/outputwriter/outputwriter.go +++ b/utils/outputwriter/outputwriter.go @@ -101,7 +101,7 @@ func (mo *MarkdownOutput) SetSizeLimit(client vcsclient.VcsClient) { } func GetCompatibleOutputWriter(provider vcsutils.VcsProvider, hasInternetConnection bool) OutputWriter { - if provider == vcsutils.BitbucketServer { + if provider == vcsutils.BitbucketServer || provider == vcsutils.BitbucketCloud { return &SimplifiedOutput{MarkdownOutput{vcsProvider: provider, hasInternetConnection: hasInternetConnection}} } return &StandardOutput{MarkdownOutput{vcsProvider: provider, hasInternetConnection: hasInternetConnection}}