Skip to content

Commit

Permalink
fix and refactor browserstack detector (#2208)
Browse files Browse the repository at this point in the history
* fix and refactor browserstack detector
  • Loading branch information
0x1 committed Dec 12, 2023
1 parent 5e3ea1a commit 6987507
Show file tree
Hide file tree
Showing 2 changed files with 82 additions and 29 deletions.
40 changes: 31 additions & 9 deletions pkg/detectors/browserstack/browserstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package browserstack
import (
"context"
"fmt"
"io"
"net/http"
"net/http/cookiejar"
"regexp"
"strings"

"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"golang.org/x/net/publicsuffix"
)

type Scanner struct {
Expand All @@ -22,11 +24,9 @@ var _ detectors.Detector = (*Scanner)(nil)
const browserStackAPIURL = "https://www.browserstack.com/automate/plan.json"

var (
defaultClient = common.SaneHttpClient()

// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hub-cloud.browserstack.com", "accessKey", "\"access_Key\":", "ACCESS_KEY", "key", "browserstackKey", "BS_AUTHKEY", "BROWSERSTACK_ACCESS_KEY"}) + `\b([0-9a-zA-Z]{20})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hub-cloud.browserstack.com", "userName", "\"username\":", "USER_NAME", "user", "browserstackUser", "BS_USERNAME", "BROWSERSTACK_USERNAME"}) + `\b([a-zA-Z\d]{3,18}[._-]?[a-zA-Z\d]{6,11})\b`)
userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hub-cloud.browserstack.com", "userName", "\"username\":", "USER_NAME", "user", "browserstackUser", "BS_USERNAME", "BROWSERSTACK_USERNAME"}) + `\b([a-zA-Z\d]{3,18}[._-]*[a-zA-Z\d]{6,11})\b`)
)

// Keywords are used for efficiently pre-filtering chunks.
Expand All @@ -35,6 +35,17 @@ func (s Scanner) Keywords() []string {
return []string{"browserstack"}
}

func (s Scanner) getClient(cookieJar *cookiejar.Jar) *http.Client {
if s.client != nil {
s.client.Jar = cookieJar
return s.client
}
// Using custom HTTP client instead of common.SaneHttpClient() here because, for unknown reasons, browserstack blocks those requests even with cookie jar attached
return &http.Client{
Jar: cookieJar,
}
}

// FromData will find and optionally verify BrowserStack 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)
Expand All @@ -61,10 +72,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
}

if verify {
client := s.client
if client == nil {
client = defaultClient
// browserstack (via cloudflare) requires cookies to be enabled
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
if err != nil {
return nil, err
}
client := s.getClient(jar)

isVerified, verificationErr := verifyBrowserStackCredentials(ctx, client, resUserMatch, resMatch)
s1.Verified = isVerified
Expand Down Expand Up @@ -96,9 +109,18 @@ func verifyBrowserStackCredentials(ctx context.Context, client *http.Client, use
}
defer res.Body.Close()

if res.StatusCode >= 200 && res.StatusCode < 300 {
if res.StatusCode == http.StatusOK {
return true, nil
} else if res.StatusCode != 401 {
} else if res.StatusCode == http.StatusForbidden {
// Sometimes browserstack (via Cloudflare) will block requests for security
body, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
if strings.Contains(string(body), "blocked") {
return false, fmt.Errorf("blocked by browserstack")
}
} else if res.StatusCode != http.StatusUnauthorized {
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
}

Expand Down
71 changes: 51 additions & 20 deletions pkg/detectors/browserstack/browserstack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"

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

Expand All @@ -34,12 +34,11 @@ func TestBrowserStack_FromChunk(t *testing.T) {
verify bool
}
tests := []struct {
name string
s Scanner
args args
want []detectors.Result
wantErr bool
wantVerificationErr bool
name string
s Scanner
args args
want []detectors.Result
wantErr bool
}{
{
name: "found, verified",
Expand Down Expand Up @@ -83,15 +82,17 @@ func TestBrowserStack_FromChunk(t *testing.T) {
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: []detectors.Result{
{
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
Verified: false,
},
},
wantErr: false,
wantVerificationErr: true,
}
r.SetVerificationError(fmt.Errorf("context deadline exceeded"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: false,
},
{
name: "found, verified but unexpected api surface",
Expand All @@ -101,15 +102,37 @@ func TestBrowserStack_FromChunk(t *testing.T) {
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
want: []detectors.Result{
{
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
},
}
r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 404"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: false,
},
{
name: "found, verified but blocked by browserstack",
s: Scanner{client: common.SaneHttpClient()},
args: args{
ctx: context.Background(),
data: []byte(fmt.Sprintf("You can find a BROWSERSTACK_ACCESS_KEY %s within BROWSERSTACK_USERNAME %s", secret, secretUser)),
verify: true,
},
wantErr: false,
wantVerificationErr: true,
want: func() []detectors.Result {
r := detectors.Result{
DetectorType: detectorspb.DetectorType_BrowserStack,
Verified: false,
RawV2: []byte(fmt.Sprintf("%s%s", secret, secretUser)),
}
r.SetVerificationError(fmt.Errorf("blocked by browserstack"), secret)
results := []detectors.Result{r}
return results
}(),
wantErr: false,
},
{
name: "not found",
Expand All @@ -134,8 +157,16 @@ func TestBrowserStack_FromChunk(t *testing.T) {
if len(got[i].Raw) == 0 {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
gotErr := ""
if got[i].VerificationError() != nil {
gotErr = got[i].VerificationError().Error()
}
wantErr := ""
if tt.want[i].VerificationError() != nil {
wantErr = tt.want[i].VerificationError().Error()
}
if gotErr != wantErr {
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.want[i].VerificationError(), got[i].VerificationError())
}
}
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
Expand Down

0 comments on commit 6987507

Please sign in to comment.