//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/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, errors.New("key not found in credentialInfo") } info, err := AnalyzePermissions(a.Cfg, key) if err != nil { return nil, 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 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 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() }