mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2026-05-16 13:20:35 +00:00
0fa069c12f
* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues * skip lint on intentional reference of deprecated DetectorType values
322 lines
8.3 KiB
Go
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()
|
|
}
|