Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Currently supported providers are: [GitHub](#github), [Bitbucket Server](#bitbuc
- [List Pull Request Labels](#list-pull-request-labels)
- [Unlabel Pull Request](#unlabel-pull-request)
- [Upload Code Scanning](#upload-code-scanning)
- [Check If Code Scanning Is Enabled](#check-if-code-scanning-is-enabled)
- [Download a File From a Repository](#download-a-file-from-a-repository)
- [Create a branch](#create-branch)
- [Allow workflows on organization](#allow-workflows)
Expand Down Expand Up @@ -836,6 +837,22 @@ scanResults := "results"
sarifID, err := client.UploadCodeScanning(ctx, owner, repo, branch, scanResults)
```

#### Check If Code Scanning Is Enabled

Notice - Code Scanning availability check is currently supported on GitHub only.

```go
// Go context
ctx := context.Background()
// The account owner of the git repository
owner := "user"
// The name of the repository
repo := "my_repo"

// Checks if code scanning is enabled for the repository
enabled, err := client.IsCodeScanningEnabled(ctx, owner, repo)
```

#### Download a File From a Repository

Note - This API is currently not supported for Bitbucket Cloud.
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-github/v53 v53.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down
4 changes: 4 additions & 0 deletions vcsclient/azurerepos.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,10 @@ func (client *AzureReposClient) UploadSnapshotToDependencyGraph(ctx context.Cont
return getUnsupportedInAzureError("uploading snapshot to dependency graph UI")
}

func (client *AzureReposClient) IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error) {
return false, getUnsupportedInAzureError("code scanning")
}

func parsePullRequestDetails(client *AzureReposClient, pullRequest git.GitPullRequest, owner, repository string, withBody bool) PullRequestInfo {
// Trim the branches prefix and get the actual branches name
shortSourceName := plumbing.ReferenceName(*pullRequest.SourceRefName).Short()
Expand Down
4 changes: 4 additions & 0 deletions vcsclient/bitbucketcloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,10 @@ func (client *BitbucketCloudClient) UploadSnapshotToDependencyGraph(ctx context.
return errBitbucketUploadSnapshotToDependencyGraphNotSupported
}

func (client *BitbucketCloudClient) IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error) {
return false, errBitbucketCodeScanningNotSupported
}

type pullRequestsResponse struct {
Values []pullRequestsDetails `json:"values"`
}
Expand Down
3 changes: 2 additions & 1 deletion vcsclient/bitbucketcommon.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package vcsclient

import (
"fmt"
"time"

"github.com/jfrog/froggit-go/vcsutils"
"github.com/mitchellh/mapstructure"
"time"
)

const (
Expand Down
4 changes: 4 additions & 0 deletions vcsclient/bitbucketserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,10 @@ func (client *BitbucketServerClient) UploadSnapshotToDependencyGraph(ctx context
return errBitbucketUploadSnapshotToDependencyGraphNotSupported
}

func (client *BitbucketServerClient) IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error) {
return false, errBitbucketCodeScanningNotSupported
}

func getBitbucketServerRepositoryVisibility(public bool) RepositoryVisibility {
if public {
return Public
Expand Down
61 changes: 61 additions & 0 deletions vcsclient/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -1715,3 +1715,64 @@ func convertToGitHubSnapshot(snapshot *SbomSnapshot) (*github.DependencyGraphSna
}
return ghSnapshot, nil
}

// IsCodeScanningEnabled checks if code scanning is enabled for a repository.
// Tests the SARIF upload endpoint with an empty body: 422=enabled, 403=disabled.
// TODO: Replace with proper GitHub API when available.
func (client *GitHubClient) IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error) {
err := validateParametersNotBlank(map[string]string{"owner": owner, "repository": repository})
if err != nil {
return false, err
}

requestUrl := fmt.Sprintf("%srepos/%s/%s/code-scanning/sarifs", client.ghClient.BaseURL.String(), owner, repository)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestUrl, strings.NewReader(""))
if err != nil {
return false, err
}

req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("Content-Type", "application/json")
if client.vcsInfo.Token != "" {
req.Header.Set("Authorization", "Bearer "+client.vcsInfo.Token)
}

resp, err := client.ghClient.Client().Do(req)
if err != nil {
return false, err
}
defer func() {
if closeErr := resp.Body.Close(); closeErr != nil {
client.logger.Warn("Failed to close response body:", closeErr)
}
}()

switch resp.StatusCode {
case http.StatusForbidden:
// Check if the error message mentions code scanning
body, readErr := io.ReadAll(resp.Body)
if readErr == nil {
bodyStr := strings.ToLower(string(body))
if strings.Contains(bodyStr, "code scanning") || strings.Contains(bodyStr, "advanced security") {
return false, nil
}
}
// If we can't read the body or it doesn't mention code scanning, log warning and return false
client.logger.Warn("Received 403 for code scanning check on", owner+"/"+repository, "but response doesn't clearly indicate code scanning status")
return false, nil
case http.StatusUnprocessableEntity:
// 422 means the endpoint exists and is processing our request, but the empty body is invalid
return true, nil
default:
// Any other status code is unexpected
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
// Success is unexpected with empty body
client.logger.Warn("Code scanning request unexpectedly succeeded for", owner+"/"+repository, "with status", resp.StatusCode, ". This should not happen.")
return true, nil
} else {
// Other error status codes
client.logger.Warn("Unexpected status code", resp.StatusCode, "when checking code scanning for", owner+"/"+repository)
return false, nil
}
}
}
94 changes: 94 additions & 0 deletions vcsclient/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1608,3 +1608,97 @@ func TestGithubClient_UploadSnapshotToDependencyGraph(t *testing.T) {
err = createBadGitHubClient(t).UploadSnapshotToDependencyGraph(ctx, owner, repo1, &snapshot)
assert.Error(t, err)
}

func TestGitHubClient_IsCodeScanningEnabled(t *testing.T) {
ctx := context.Background()

t.Run("code scanning enabled - 422 response", func(t *testing.T) {
unprocessableResponse := []byte(`{
"message": "Invalid request",
"errors": [
{
"code": "invalid",
"field": "sarif"
}
]
}`)

client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, unprocessableResponse,
fmt.Sprintf("/repos/%s/%s/code-scanning/sarifs", owner, repo1), http.StatusUnprocessableEntity,
createGitHubHandler)
defer cleanUp()

enabled, err := client.IsCodeScanningEnabled(ctx, owner, repo1)
assert.NoError(t, err)
assert.True(t, enabled)
})

t.Run("code scanning disabled - 403 with code scanning message", func(t *testing.T) {
forbiddenResponse := []byte(`{
"message": "Advanced Security must be enabled for this repository to use code scanning.",
"documentation_url": "https://docs.github.com/rest/reference/code-scanning"
}`)

client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, forbiddenResponse,
fmt.Sprintf("/repos/%s/%s/code-scanning/sarifs", owner, repo1), http.StatusForbidden,
createGitHubHandler)
defer cleanUp()

enabled, err := client.IsCodeScanningEnabled(ctx, owner, repo1)
assert.NoError(t, err)
assert.False(t, enabled)
})

t.Run("generic forbidden - 403 without code scanning message", func(t *testing.T) {
genericForbiddenResponse := []byte(`{
"message": "Forbidden"
}`)

client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, genericForbiddenResponse,
fmt.Sprintf("/repos/%s/%s/code-scanning/sarifs", owner, repo1), http.StatusForbidden,
createGitHubHandler)
defer cleanUp()

enabled, err := client.IsCodeScanningEnabled(ctx, owner, repo1)
assert.NoError(t, err)
assert.False(t, enabled)
})

t.Run("unexpected success - 200 response", func(t *testing.T) {
successResponse := []byte(`{"message": "Success"}`)

client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, successResponse,
fmt.Sprintf("/repos/%s/%s/code-scanning/sarifs", owner, repo1), http.StatusOK,
createGitHubHandler)
defer cleanUp()

enabled, err := client.IsCodeScanningEnabled(ctx, owner, repo1)
assert.NoError(t, err)
assert.True(t, enabled) // Should return true but log a warning
})

t.Run("network error", func(t *testing.T) {
enabled, err := createBadGitHubClient(t).IsCodeScanningEnabled(ctx, owner, repo1)
assert.Error(t, err)
assert.False(t, enabled)
})

t.Run("invalid parameters", func(t *testing.T) {
client, cleanUp := createServerAndClientReturningStatus(t, vcsutils.GitHub, false, nil,
fmt.Sprintf("/repos/%s/%s/code-scanning/sarifs", owner, repo1), http.StatusUnprocessableEntity,
createGitHubHandler)
defer cleanUp()

// Test empty owner
enabled, err := client.IsCodeScanningEnabled(ctx, "", repo1)
assert.Error(t, err)
assert.False(t, enabled)
assert.Contains(t, err.Error(), "owner")

// Test empty repository
enabled, err = client.IsCodeScanningEnabled(ctx, owner, "")
assert.Error(t, err)
assert.False(t, enabled)
assert.Contains(t, err.Error(), "repository")
})
}
4 changes: 4 additions & 0 deletions vcsclient/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,10 @@ func (client *GitLabClient) UploadSnapshotToDependencyGraph(ctx context.Context,
return errGitLabUploadSnapshotToDependencyGraphNotSupported
}

func (client *GitLabClient) IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error) {
return false, errGitLabCodeScanningNotSupported
}

func getProjectID(owner, project string) string {
return fmt.Sprintf("%s/%s", owner, project)
}
Expand Down
6 changes: 6 additions & 0 deletions vcsclient/vcsclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,12 @@ type VcsClient interface {

// UploadSnapshotToDependencyGraph uploads a snapshot to the GitHub dependency graph tab
UploadSnapshotToDependencyGraph(ctx context.Context, owner, repo string, snapshot *SbomSnapshot) error

// IsCodeScanningEnabled checks if code scanning is enabled by making a direct HTTP request
// to the code scanning route with an empty body and interpreting the response status codes
// owner - User or organization
// repository - VCS repository name
IsCodeScanningEnabled(ctx context.Context, owner, repository string) (bool, error)
}

// SbomSnapshot represents a snapshot for GitHub dependency submission API
Expand Down
Loading