mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 07:04:24 +00:00
[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:
parent
899f59fbb4
commit
becc2c4010
5 changed files with 431 additions and 20 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
96
pkg/analyzer/analyzers/bitbucket/bitbucket_test.go
Normal file
96
pkg/analyzer/analyzers/bitbucket/bitbucket_test.go
Normal 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
|
||||
})
|
||||
}
|
92
pkg/analyzer/analyzers/bitbucket/expected_output.json
Normal file
92
pkg/analyzer/analyzers/bitbucket/expected_output.json
Normal 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
|
||||
}
|
131
pkg/analyzer/analyzers/bitbucket/permissions.go
Normal file
131
pkg/analyzer/analyzers/bitbucket/permissions.go
Normal 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")
|
||||
}
|
16
pkg/analyzer/analyzers/bitbucket/permissions.yaml
Normal file
16
pkg/analyzer/analyzers/bitbucket/permissions.yaml
Normal 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
|
Loading…
Reference in a new issue