diff --git a/pkg/detectors/browserstack/browserstack.go b/pkg/detectors/browserstack/browserstack.go index 28d1f5d1ce3a..d0f1f52a4f9c 100644 --- a/pkg/detectors/browserstack/browserstack.go +++ b/pkg/detectors/browserstack/browserstack.go @@ -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 { @@ -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. @@ -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) @@ -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 @@ -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) } diff --git a/pkg/detectors/browserstack/browserstack_test.go b/pkg/detectors/browserstack/browserstack_test.go index fbb3074ec311..f8185173e137 100644 --- a/pkg/detectors/browserstack/browserstack_test.go +++ b/pkg/detectors/browserstack/browserstack_test.go @@ -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" ) @@ -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", @@ -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", @@ -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", @@ -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")