Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions pkg/detectors/bitbucketapppassword/bitbucketapppassword.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package bitbucketapppassword

import (
"context"
"encoding/base64"
"fmt"
"net/http"
"regexp"
"strings"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

type Scanner struct{}

// Ensure the Scanner satisfies the interface at compile time.
var _ detectors.Detector = (*Scanner)(nil)

var (
client = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
// The following patterns cover the methods of authentication found here:
// https://support.atlassian.com/bitbucket-cloud/docs/using-app-passwords/, as well as for other general cases.

// Covers 'username:appPassword' pattern
usernamePat1 = regexp.MustCompile(`\b([A-Za-z0-9-_]{1,30}):ATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}\b`)
// Covers assignment of username to variable
usernamePat2 = regexp.MustCompile(`(?im)(?:user|usr)\S{0,40}?[:=\s]{1,3}[ '"=]?([a-zA-Z0-9-_]{1,30})\b`)
// Covers 'https://[email protected]' pattern
usernamePat3 = regexp.MustCompile(`https://([a-zA-Z0-9-_]{1,30})@bitbucket.org`)
// Covers '("username", "password")' pattern, used for HTTP Basic Auth
usernamePat4 = regexp.MustCompile(`"([a-zA-Z0-9-_]{1,30})",(?: )?"ATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}"`)

usernamePatterns = []*regexp.Regexp{usernamePat1, usernamePat2, usernamePat3, usernamePat4}

appPasswordPat = regexp.MustCompile(`\bATBB[A-Za-z0-9_=.-]+[A-Z0-9]{8}\b`)
)

// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
return []string{"bitbucketapppassword", "ATBB"}
}

// FromData will find and optionally verify Bitbucket App Password secrets in a given set of bytes.
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

var usernameMatches [][]string
for _, pattern := range usernamePatterns {
usernameMatches = append(usernameMatches, pattern.FindAllStringSubmatch(dataStr, -1)...)
}
appPasswordMatches := appPasswordPat.FindAllString(dataStr, -1)

for _, usernameMatch := range usernameMatches {
if len(usernameMatch) != 2 {
continue
}
resUsernameMatch := strings.TrimSpace(usernameMatch[1])

for _, resAppPasswordMatch := range appPasswordMatches {

s1 := detectors.Result{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Raw: []byte(fmt.Sprintf(`%s: %s`, resUsernameMatch, resAppPasswordMatch)),
}

if verify {
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bitbucket.org/2.0/user", nil)
if err != nil {
continue
}
req.Header.Add("Accept", "application/json")
data := fmt.Sprintf("%s:%s", resUsernameMatch, resAppPasswordMatch)
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(data))))
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
// Status 403 FORBIDDEN indicates a valid secret without valid scope
if res.StatusCode >= 200 && res.StatusCode < 300 || res.StatusCode == 403 {
s1.Verified = true
}
}
}

results = append(results, s1)
}
}

return results, nil
}

func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_BitbucketAppPassword
}

func (s Scanner) Description() string {
return "Bitbucket is a Git repository hosting service by Atlassian. Bitbucket App Passwords are used to authenticate to the Bitbucket API."
}
121 changes: 121 additions & 0 deletions pkg/detectors/bitbucketapppassword/bitbucketapppassword_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//go:build detectors
// +build detectors

package bitbucketapppassword

import (
"context"
"fmt"
"testing"
"time"

"github.com/kylelemons/godebug/pretty"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
)

func TestBitbucketapppassword_FromChunk(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}

username := testSecrets.MustGetField("USERNAME")
appPassword := testSecrets.MustGetField("BITBUCKETAPPPASSWORD")
inactiveAppPassword := testSecrets.MustGetField("BITBUCKETAPPPASSWORD_INACTIVE")

type args struct {
ctx context.Context
data []byte
verify bool
}
tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
}{
{
name: "found, verified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`You can find a secret within: https://%s:%[email protected]`, username, appPassword)),
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Verified: true,
},
},
wantErr: false,
},
{
name: "found, unverified",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf(`You can find a secret within but not valid: https://%s:%[email protected]`, username, inactiveAppPassword)), // the secret would satisfy the regex but not pass validation
verify: true,
},
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_BitbucketAppPassword,
Verified: false,
},
},
wantErr: false,
},
{
name: "not found",
s: Scanner{},
args: args{
ctx: context.Background(),
data: []byte("You cannot find the secret within"),
verify: true,
},
want: nil,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
if (err != nil) != tt.wantErr {
t.Errorf("BitbucketAppPassword.FromData() error = %v, wantErr %v", err, tt.wantErr)
return
}
for i := range got {
if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("BitbucketAppPassword.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}
}

func BenchmarkFromData(benchmark *testing.B) {
ctx := context.Background()
s := Scanner{}
for name, data := range detectors.MustGetBenchmarkData() {
benchmark.Run(name, func(b *testing.B) {
for n := 0; n < b.N; n++ {
_, err := s.FromData(ctx, false, data)
if err != nil {
b.Fatal(err)
}
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/billomat"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bingsubscriptionkey"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitbar"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitbucketapppassword"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitcoinaverage"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitfinex"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bitlyaccesstoken"
Expand Down Expand Up @@ -1710,6 +1711,7 @@ func buildDetectorList() []detectors.Detector {
&zonkafeedback.Scanner{},
&zulipchat.Scanner{},
&stripepaymentintent.Scanner{},
&bitbucketapppassword.Scanner{},
}
}

Expand Down
3 changes: 3 additions & 0 deletions pkg/pb/detectorspb/detectors.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions proto/detectors.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ enum DetectorType {
AzureAppConfigConnectionString = 1025;
DeepSeek = 1026;
StripePaymentIntent = 1027;
BitbucketAppPassword = 1028;
}

message Result {
Expand Down