[analyze] Add analyze interface for Bitbucket (#3224)

* impelmented analyzer interface with data models for Bitbucket

* Added bitbucket permissions in yaml
add ids to keep resources distinguishable.

* [chore]
moved expected output of test in json file to neat the code.
corrected the fully qualified name for repository resources.

---------

Co-authored-by: Abdul Basit <abasit@folio3.com>
This commit is contained in:
Abdul Basit 2024-09-06 04:04:35 +05:00 committed by GitHub
parent 899f59fbb4
commit becc2c4010
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 431 additions and 20 deletions

View file

@ -1,8 +1,9 @@
//go:generate generate_permissions permissions.yaml permissions.go bitbucket
package bitbucket
import (
"encoding/json"
"fmt"
"errors"
"net/http"
"os"
"sort"
@ -18,37 +19,33 @@ import (
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Bitbucket }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
_, err := AnalyzePermissions(a.Cfg, credInfo["key"])
if err != nil {
return nil, err
}
return nil, fmt.Errorf("not implemented")
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 []analyzers.Permission
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
@ -58,7 +55,81 @@ type RepoJSON struct {
Values []Repo `json:"values"`
}
func getScopesAndType(cfg *config.Config, key string) (string, []analyzers.Permission, error) {
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Bitbucket }
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: analyzerpb.AnalyzerType_Bitbucket,
}
// 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)
@ -82,10 +153,7 @@ func getScopesAndType(cfg *config.Config, key string) (string, []analyzers.Permi
credentialType := resp.Header.Get("x-credential-type")
oauthScopes := resp.Header.Get("x-oauth-scopes")
var scopes []analyzers.Permission
for _, scope := range strings.Split(oauthScopes, ", ") {
scopes = append(scopes, analyzers.Permission{Value: scope})
}
scopes := strings.Split(oauthScopes, ", ")
return credentialType, scopes, nil
}
@ -183,13 +251,21 @@ func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
}, nil
}
func convertScopeToAnalyzerPermissions(scopes []string) []analyzers.Permission {
permissions := make([]analyzers.Permission, 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, info.OauthScopes)
printScopes(info.Type, convertScopeToAnalyzerPermissions(info.OauthScopes))
printAccessibleRepositories(info.Repos)
}

View file

@ -0,0 +1,96 @@
package bitbucket
import (
_ "embed"
"encoding/json"
"sort"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
//go:embed expected_output.json
var expectedOutput []byte
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
sid string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Bitbucket key",
key: testSecrets.MustGetField("BITBUCKET_ANALYZE_TOKEN"),
want: string(expectedOutput),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
got, err := a.Analyze(ctx, map[string]string{"key": tt.key, "sid": tt.sid})
if (err != nil) != tt.wantErr {
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
return
}
// bindings need to be in the same order to be comparable
sortBindings(got.Bindings)
// Marshal the actual result to JSON
gotJSON, err := json.Marshal(got)
if err != nil {
t.Fatalf("could not marshal got to JSON: %s", err)
}
// Parse the expected JSON string
var wantObj analyzers.AnalyzerResult
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
t.Fatalf("could not unmarshal want JSON string: %s", err)
}
// bindings need to be in the same order to be comparable
sortBindings(wantObj.Bindings)
// Marshal the expected result to JSON (to normalize)
wantJSON, err := json.Marshal(wantObj)
if err != nil {
t.Fatalf("could not marshal want to JSON: %s", err)
}
// Compare the JSON strings
if string(gotJSON) != string(wantJSON) {
// Pretty-print both JSON strings for easier comparison
var gotIndented []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
}
})
}
}
// Helper function to sort bindings
func sortBindings(bindings []analyzers.Binding) {
sort.SliceStable(bindings, func(i, j int) bool {
if bindings[i].Resource.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}

View file

@ -0,0 +1,92 @@
{
"AnalyzerType": 3,
"Bindings": [
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "pipeline",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "pullrequest",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "runner",
"Parent": null
}
},
{
"Resource": {
"Name": "Repository",
"FullyQualifiedName": "bitbucket.com/credential/repo_access_token",
"Type": "repo_access_token",
"Metadata": {
"type": "Repository Access Token (Can access 1 repository)"
},
"Parent": null
},
"Permission": {
"Value": "webhook",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "basit-trufflesec/repo1",
"FullyQualifiedName": "bitbucket.com/repository/{8961ef70-000c-47ca-9348-5f9ecee875d6}",
"Type": "repository",
"Metadata": {
"is_private": true,
"owner": "basit-trufflesec",
"owner_id": "{521b49b6-7709-484a-8aa8-ecc3a6da08eb}",
"role": "admin"
},
"Parent": {
"Name": "repo-analyzer",
"FullyQualifiedName": "bitbucket.com/project/{8a693e10-087f-41fc-ba67-2d1414ab1c86}",
"Type": "project",
"Metadata": null,
"Parent": {
"Name": "basit-trufflesec",
"FullyQualifiedName": "bitbucket.com/workspace/{521b49b6-7709-484a-8aa8-ecc3a6da08eb}",
"Type": "workspace",
"Metadata": null,
"Parent": null
}
}
}
],
"Metadata": null
}

View file

@ -0,0 +1,131 @@
// Code generated by go generate; DO NOT EDIT.
package bitbucket
import "errors"
type Permission int
const (
Invalid Permission = iota
Project Permission = iota
ProjectAdmin Permission = iota
Repository Permission = iota
RepositoryWrite Permission = iota
RepositoryAdmin Permission = iota
RepositoryDelete Permission = iota
Pullrequest Permission = iota
PullrequestWrite Permission = iota
Webhook Permission = iota
Account Permission = iota
Pipeline Permission = iota
PipelineWrite Permission = iota
PipelineVariable Permission = iota
Runner Permission = iota
RunnerWrite Permission = iota
)
var (
PermissionStrings = map[Permission]string{
Project: "project",
ProjectAdmin: "project:admin",
Repository: "repository",
RepositoryWrite: "repository:write",
RepositoryAdmin: "repository:admin",
RepositoryDelete: "repository:delete",
Pullrequest: "pullrequest",
PullrequestWrite: "pullrequest:write",
Webhook: "webhook",
Account: "account",
Pipeline: "pipeline",
PipelineWrite: "pipeline:write",
PipelineVariable: "pipeline:variable",
Runner: "runner",
RunnerWrite: "runner:write",
}
StringToPermission = map[string]Permission{
"project": Project,
"project:admin": ProjectAdmin,
"repository": Repository,
"repository:write": RepositoryWrite,
"repository:admin": RepositoryAdmin,
"repository:delete": RepositoryDelete,
"pullrequest": Pullrequest,
"pullrequest:write": PullrequestWrite,
"webhook": Webhook,
"account": Account,
"pipeline": Pipeline,
"pipeline:write": PipelineWrite,
"pipeline:variable": PipelineVariable,
"runner": Runner,
"runner:write": RunnerWrite,
}
PermissionIDs = map[Permission]int{
Project: 1,
ProjectAdmin: 2,
Repository: 3,
RepositoryWrite: 4,
RepositoryAdmin: 5,
RepositoryDelete: 6,
Pullrequest: 7,
PullrequestWrite: 8,
Webhook: 9,
Account: 10,
Pipeline: 11,
PipelineWrite: 12,
PipelineVariable: 13,
Runner: 14,
RunnerWrite: 15,
}
IdToPermission = map[int]Permission{
1: Project,
2: ProjectAdmin,
3: Repository,
4: RepositoryWrite,
5: RepositoryAdmin,
6: RepositoryDelete,
7: Pullrequest,
8: PullrequestWrite,
9: Webhook,
10: Account,
11: Pipeline,
12: PipelineWrite,
13: PipelineVariable,
14: Runner,
15: RunnerWrite,
}
)
// ToString converts a Permission enum to its string representation
func (p Permission) ToString() (string, error) {
if str, ok := PermissionStrings[p]; ok {
return str, nil
}
return "", errors.New("invalid permission")
}
// ToID converts a Permission enum to its ID
func (p Permission) ToID() (int, error) {
if id, ok := PermissionIDs[p]; ok {
return id, nil
}
return 0, errors.New("invalid permission")
}
// PermissionFromString converts a string representation to its Permission enum
func PermissionFromString(s string) (Permission, error) {
if p, ok := StringToPermission[s]; ok {
return p, nil
}
return 0, errors.New("invalid permission string")
}
// PermissionFromID converts an ID to its Permission enum
func PermissionFromID(id int) (Permission, error) {
if p, ok := IdToPermission[id]; ok {
return p, nil
}
return 0, errors.New("invalid permission ID")
}

View file

@ -0,0 +1,16 @@
permissions:
- project
- project:admin
- repository
- repository:write
- repository:admin
- repository:delete
- pullrequest
- pullrequest:write
- webhook
- account
- pipeline
- pipeline:write
- pipeline:variable
- runner
- runner:write