Files
trufflehog/pkg/analyzer/analyzers/bitbucket/bitbucket.go
T
Amaan Ullah 0fa069c12f Enable errcheck and staticcheck for golangci-lint v2 and resolve all issues (#4924)
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues

* skip lint on intentional reference of deprecated DetectorType values
2026-05-15 17:07:14 +05:00

322 lines
8.3 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go bitbucket
package bitbucket
import (
"encoding/json"
"errors"
"net/http"
"os"
"sort"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
var resource_name_map = map[string]string{
"repo_access_token": "Repository",
"project_access_token": "Project",
"workspace_access_token": "Workspace",
}
type SecretInfo struct {
Type string
OauthScopes []string
Repos []Repo
}
type Repo struct {
ID string `json:"uuid"`
FullName string `json:"full_name"`
RepoName string `json:"name"`
Project struct {
ID string `json:"uuid"`
Name string `json:"name"`
} `json:"project"`
Workspace struct {
ID string `json:"uuid"`
Name string `json:"name"`
} `json:"workspace"`
IsPrivate bool `json:"is_private"`
Owner struct {
ID string `json:"uuid"`
Username string `json:"username"`
} `json:"owner"`
Role string
}
type RepoJSON struct {
Values []Repo `json:"values"`
}
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeBitbucket }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("key not found in credentialInfo"))
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err)
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeBitbucket,
}
// add unbounded resources
result.UnboundedResources = make([]analyzers.Resource, len(info.Repos))
for i, repo := range info.Repos {
result.UnboundedResources[i] = analyzers.Resource{
Type: "repository",
Name: repo.FullName,
FullyQualifiedName: "bitbucket.com/repository/" + repo.ID,
Parent: &analyzers.Resource{
Type: "project",
Name: repo.Project.Name,
FullyQualifiedName: "bitbucket.com/project/" + repo.Project.ID,
Parent: &analyzers.Resource{
Type: "workspace",
Name: repo.Workspace.Name,
FullyQualifiedName: "bitbucket.com/workspace/" + repo.Workspace.ID,
},
},
Metadata: map[string]any{
"owner_id": repo.Owner.ID,
"owner": repo.Owner.Username,
"is_private": repo.IsPrivate,
"role": repo.Role,
},
}
}
credentialResource := &analyzers.Resource{
Type: info.Type,
Name: resource_name_map[info.Type],
FullyQualifiedName: "bitbucket.com/credential/" + info.Type,
Metadata: map[string]any{
"type": credential_type_map[info.Type],
},
}
for _, scope := range info.OauthScopes {
result.Bindings = append(result.Bindings, analyzers.Binding{
Resource: *credentialResource,
Permission: analyzers.Permission{
Value: scope,
},
})
}
return &result
}
func getScopesAndType(cfg *config.Config, key string) (string, []string, error) {
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return "", nil, err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// response
resp, err := client.Do(req)
if err != nil {
return "", nil, err
}
defer func() { _ = resp.Body.Close() }()
// parse response headers
credentialType := resp.Header.Get("x-credential-type")
oauthScopes := resp.Header.Get("x-oauth-scopes")
scopes := strings.Split(oauthScopes, ", ")
return credentialType, scopes, nil
}
func scopesToBitbucketScopes(scopes ...analyzers.Permission) []BitbucketScope {
scopesSlice := []BitbucketScope{}
for _, scope := range scopes {
scope := scope.Value
mapping := oauth_scope_map[scope]
for _, impliedScope := range mapping.ImpliedScopes {
scopesSlice = append(scopesSlice, oauth_scope_map[impliedScope])
}
scopesSlice = append(scopesSlice, oauth_scope_map[scope])
}
// sort scopes by category
sort.Sort(ByCategoryAndName(scopesSlice))
return scopesSlice
}
func getRepositories(cfg *config.Config, key string, role string) (RepoJSON, error) {
var repos RepoJSON
// client
client := analyzers.NewAnalyzeClient(cfg)
// request
req, err := http.NewRequest("GET", "https://api.bitbucket.org/2.0/repositories", nil)
if err != nil {
return repos, err
}
// headers
req.Header.Set("Authorization", "Bearer "+key)
// add query params
q := req.URL.Query()
q.Add("role", role)
q.Add("pagelen", "100")
req.URL.RawQuery = q.Encode()
// response
resp, err := client.Do(req)
if err != nil {
return repos, err
}
defer func() { _ = resp.Body.Close() }()
// parse response body
err = json.NewDecoder(resp.Body).Decode(&repos)
if err != nil {
return repos, err
}
return repos, nil
}
func getAllRepos(cfg *config.Config, key string) ([]Repo, error) {
roles := []string{"member", "contributor", "admin", "owner"}
var allRepos = make(map[string]Repo, 0)
for _, role := range roles {
repos, err := getRepositories(cfg, key, role)
if err != nil {
return nil, err
}
// purposefully overwriting, so that get the most permissive role
for _, repo := range repos.Values {
repo.Role = role
allRepos[repo.FullName] = repo
}
}
repoSlice := make([]Repo, 0, len(allRepos))
for _, repo := range allRepos {
repoSlice = append(repoSlice, repo)
}
return repoSlice, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
credentialType, oauthScopes, err := getScopesAndType(cfg, key)
if err != nil {
return nil, err
}
// get all repos available to user
// ToDo: pagination
repos, err := getAllRepos(cfg, key)
if err != nil {
return nil, err
}
return &SecretInfo{
Type: credentialType,
OauthScopes: oauthScopes,
Repos: repos,
}, nil
}
func convertScopeToAnalyzerPermissions(scopes []string) []analyzers.Permission {
permissions := make([]analyzers.Permission, 0, len(scopes))
for _, scope := range scopes {
permissions = append(permissions, analyzers.Permission{Value: scope})
}
return permissions
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
printScopes(info.Type, convertScopeToAnalyzerPermissions(info.OauthScopes))
printAccessibleRepositories(info.Repos)
}
func printScopes(credentialType string, scopes []analyzers.Permission) {
if credentialType == "" {
color.Red("[x] Invalid Bitbucket access token.")
return
}
color.Green("[!] Valid Bitbucket access token.\n\n")
color.Green("[i] Credential Type: %s\n\n", credential_type_map[credentialType])
color.Yellow("[i] Access Token Scopes:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Category", "Permission"})
currentCategory := ""
for _, scope := range scopesToBitbucketScopes(scopes...) {
if currentCategory != scope.Category {
currentCategory = scope.Category
t.AppendRow([]any{scope.Category, ""})
}
t.AppendRow([]any{"", color.GreenString(scope.Name)})
}
t.Render()
}
func printAccessibleRepositories(repos []Repo) {
color.Yellow("\n[i] Accessible Repositories:")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Repository", "Project", "Workspace", "Owner", "Is Private", "This User's Role"})
for _, repo := range repos {
private := ""
if repo.IsPrivate {
private = color.GreenString("Yes")
} else {
private = color.RedString("No")
}
t.AppendRow([]any{
color.GreenString(repo.RepoName),
color.GreenString(repo.Project.Name),
color.GreenString(repo.Workspace.Name),
color.GreenString(repo.Owner.Username),
private,
color.GreenString(repo.Role),
})
}
t.Render()
}