From 78435c802fb3db738e3e4ea9f00d781c6ac828eb Mon Sep 17 00:00:00 2001 From: Nabeel Alam Date: Thu, 3 Jul 2025 16:18:51 +0500 Subject: [PATCH] updated verification method in the dwolla detector (#4282) Co-authored-by: Shahzad Haider <76992801+shahzadhaider1@users.noreply.github.com> --- pkg/detectors/dwolla/dwolla.go | 89 +++++++++++++------ .../dwolla/dwolla_integration_test.go | 32 +++++-- pkg/detectors/dwolla/dwolla_test.go | 2 - 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/pkg/detectors/dwolla/dwolla.go b/pkg/detectors/dwolla/dwolla.go index 8cbf58a59..d9223a7cb 100644 --- a/pkg/detectors/dwolla/dwolla.go +++ b/pkg/detectors/dwolla/dwolla.go @@ -4,6 +4,7 @@ import ( "context" b64 "encoding/base64" "fmt" + "io" "net/http" "strings" @@ -15,6 +16,7 @@ import ( ) type Scanner struct { + client *http.Client detectors.DefaultMultiPartCredentialProvider } @@ -22,7 +24,7 @@ type Scanner struct { var _ detectors.Detector = (*Scanner)(nil) var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"dwolla"}) + `\b([a-zA-Z-0-9]{50})\b`) @@ -35,46 +37,44 @@ func (s Scanner) Keywords() []string { return []string{"dwolla"} } +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + return defaultClient +} + // FromData will find and optionally verify Dwolla 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) - idMatches := idPat.FindAllStringSubmatch(dataStr, -1) - secretMatches := secretPat.FindAllStringSubmatch(dataStr, -1) + uniqueIDs := make(map[string]struct{}) + for _, matches := range idPat.FindAllStringSubmatch(dataStr, -1) { + uniqueIDs[matches[1]] = struct{}{} + } - for _, match := range idMatches { + uniqueSecrets := make(map[string]struct{}) + for _, matches := range secretPat.FindAllStringSubmatch(dataStr, -1) { + uniqueSecrets[matches[1]] = struct{}{} + } - idMatch := strings.TrimSpace(match[1]) - - for _, secret := range secretMatches { - - secretMatch := strings.TrimSpace(secret[1]) + for id := range uniqueIDs { + for secret := range uniqueSecrets { + if id == secret { + continue // Skip if ID and secret are the same. + } s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_Dwolla, - Raw: []byte(idMatch), - RawV2: []byte(idMatch + secretMatch), + Raw: []byte(id), + RawV2: []byte(id + secret), } if verify { - data := fmt.Sprintf("%s:%s", idMatch, secretMatch) - encoded := b64.StdEncoding.EncodeToString([]byte(data)) - payload := strings.NewReader("grant_type=client_credentials") - - req, err := http.NewRequestWithContext(ctx, "POST", "https://api-sandbox.dwolla.com/token", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encoded)) - - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + client := s.getClient() + isVerified, err := verifyMatch(ctx, client, id, secret) + s1.Verified = isVerified + s1.SetVerificationError(err, id, secret) } results = append(results, s1) @@ -84,6 +84,37 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, id, secret string) (bool, error) { + data := fmt.Sprintf("%s:%s", id, secret) + encoded := b64.StdEncoding.EncodeToString([]byte(data)) + payload := strings.NewReader("grant_type=client_credentials") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api-sandbox.dwolla.com/token", payload) + if err != nil { + return false, err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encoded)) + + res, err := client.Do(req) + if err != nil { + return false, err + } + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + switch res.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Dwolla } diff --git a/pkg/detectors/dwolla/dwolla_integration_test.go b/pkg/detectors/dwolla/dwolla_integration_test.go index 364b0fa85..dfedc6a99 100644 --- a/pkg/detectors/dwolla/dwolla_integration_test.go +++ b/pkg/detectors/dwolla/dwolla_integration_test.go @@ -9,7 +9,8 @@ import ( "testing" "time" - "github.com/kylelemons/godebug/pretty" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" @@ -19,13 +20,13 @@ import ( func TestDwolla_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } - id := testSecrets.MustGetField("DWOLLA") + id := testSecrets.MustGetField("DWOLLA_ID") secret := testSecrets.MustGetField("DWOLLA_SECRET") - inactiveSecret := testSecrets.MustGetField("DWOLLA_SECRET_INACTIVE") + inactiveSecret := testSecrets.MustGetField("DWOLLA_INACTIVE") type args struct { ctx context.Context @@ -48,6 +49,10 @@ func TestDwolla_FromChunk(t *testing.T) { verify: true, }, want: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Dwolla, + Verified: false, + }, { DetectorType: detectorspb.DetectorType_Dwolla, Verified: true, @@ -68,6 +73,10 @@ func TestDwolla_FromChunk(t *testing.T) { DetectorType: detectorspb.DetectorType_Dwolla, Verified: false, }, + { + DetectorType: detectorspb.DetectorType_Dwolla, + Verified: false, + }, }, wantErr: false, }, @@ -95,9 +104,20 @@ func TestDwolla_FromChunk(t *testing.T) { if len(got[i].Raw) == 0 { t.Fatalf("no raw secret present: \n %+v", got[i]) } - got[i].Raw = nil + 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()) + } } - if diff := pretty.Compare(got, tt.want); diff != "" { + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret") + if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { t.Errorf("Dwolla.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } }) diff --git a/pkg/detectors/dwolla/dwolla_test.go b/pkg/detectors/dwolla/dwolla_test.go index 726969d7c..cf154799c 100644 --- a/pkg/detectors/dwolla/dwolla_test.go +++ b/pkg/detectors/dwolla/dwolla_test.go @@ -33,10 +33,8 @@ var ( # - The above credentials should only be used in a secure environment. ` secrets = []string{ - "MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6rMvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6r", "MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6rq3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1", "q3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1MvkLktYDS7PSE0xRMHIYBKrAjXruEk5P1VrJUUGtgspa3KTi6r", - "q3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1q3DZbY7iviUpewfCHEpK1I51G8XW63GuLuJyAIEqOFtEB1qlg1", } )