diff --git a/pkg/detectors/caflou/caflou.go b/pkg/detectors/caflou/caflou.go index 3abbe3859942..189fb9530fd4 100644 --- a/pkg/detectors/caflou/caflou.go +++ b/pkg/detectors/caflou/caflou.go @@ -3,6 +3,7 @@ package caflou import ( "context" "fmt" + "io" "net/http" "strings" "time" @@ -54,18 +55,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result client = defaultClient } - req, err := http.NewRequestWithContext(ctx, "GET", "https://app.caflou.com/api/v1/accounts", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -74,6 +66,33 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://app.caflou.com/api/v1/accounts", nil) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + 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 HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Caflou } diff --git a/pkg/detectors/caflou/caflou_integration_test.go b/pkg/detectors/caflou/caflou_integration_test.go index b8eb1bec3024..aa54fd57d090 100644 --- a/pkg/detectors/caflou/caflou_integration_test.go +++ b/pkg/detectors/caflou/caflou_integration_test.go @@ -19,12 +19,12 @@ import ( func TestCaflou_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } secret := testSecrets.MustGetField("CAFLOU") - inactiveSecret := testSecrets.MustGetField("CAFLOU_INACTIVE") + inactiveSecret := testSecrets.MustGetField("CAFLOU_INVALID") type args struct { ctx context.Context diff --git a/pkg/detectors/calendarific/calendarific.go b/pkg/detectors/calendarific/calendarific.go index ff4cd385a33d..a56684e7c9f0 100644 --- a/pkg/detectors/calendarific/calendarific.go +++ b/pkg/detectors/calendarific/calendarific.go @@ -2,6 +2,8 @@ package calendarific import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,17 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://calendarific.com/api/v2/holidays?&api_key="+resMatch+"&country=US&year=2019", nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -64,6 +58,32 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://calendarific.com/api/v2/holidays?&api_key="+token+"&country=US&year=2019", http.NoBody) + if err != nil { + return false, err + } + 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 HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Calendarific } diff --git a/pkg/detectors/calendlyapikey/calendlyapikey.go b/pkg/detectors/calendlyapikey/calendlyapikey.go index 17ec832e1979..453cb9e84a05 100644 --- a/pkg/detectors/calendlyapikey/calendlyapikey.go +++ b/pkg/detectors/calendlyapikey/calendlyapikey.go @@ -3,6 +3,7 @@ package calendlyapikey import ( "context" "fmt" + "io" "net/http" "strings" @@ -22,7 +23,7 @@ var ( client = 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{"calendly"}) + `\b(eyJ[A-Za-z0-9-_]{100,300}\.eyJ[A-Za-z0-9-_]{100,300}\.[A-Za-z0-9-_]+)\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"calendly"}) + `\b(eyJ[A-Za-z0-9-_]{10,300}\.eyJ[A-Za-z0-9-_]{10,300}\.[A-Za-z0-9-_]+)\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -46,18 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.calendly.com/users/me", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -66,6 +58,33 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.calendly.com/users/me", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + 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 HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_CalendlyApiKey } diff --git a/pkg/detectors/calorieninja/calorieninja.go b/pkg/detectors/calorieninja/calorieninja.go index 0ca01216d2f0..5aac6e30b8df 100644 --- a/pkg/detectors/calorieninja/calorieninja.go +++ b/pkg/detectors/calorieninja/calorieninja.go @@ -2,6 +2,8 @@ package calorieninja import ( "context" + "fmt" + "io" "net/http" "strings" @@ -21,7 +23,7 @@ var ( client = 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{"calorieninja"}) + `\b([0-9A-Za-z]{40})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"calorieninja"}) + `\b([0-9A-Za-z=+/]{40})\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -45,19 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.calorieninjas.com/v1/nutrition?query", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("X-Api-Key", resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -66,6 +58,35 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.calorieninjas.com/v1/nutrition?query", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-Api-Key", token) + 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 + // Invalid API key returns 400 bad request + case http.StatusBadRequest: + return false, nil + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_CalorieNinja } diff --git a/pkg/detectors/calorieninja/calorieninja_integration_test.go b/pkg/detectors/calorieninja/calorieninja_integration_test.go index 4b680cb75011..d2ccd997bb9a 100644 --- a/pkg/detectors/calorieninja/calorieninja_integration_test.go +++ b/pkg/detectors/calorieninja/calorieninja_integration_test.go @@ -19,7 +19,7 @@ import ( func TestCalorieninja_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } diff --git a/pkg/detectors/calorieninja/calorieninja_test.go b/pkg/detectors/calorieninja/calorieninja_test.go index 145c9a069444..b25e95f6e5ef 100644 --- a/pkg/detectors/calorieninja/calorieninja_test.go +++ b/pkg/detectors/calorieninja/calorieninja_test.go @@ -28,7 +28,12 @@ var ( # - Remember to rotate the secret every 90 days. # - The above credentials should only be used in a secure environment. ` + validPatternWithBase64 = ` + # Configuration with base64-like API key + calorieninja_api_key: "e2xRY0yQmqhSetiQx0ZKWg==QMP1HstAoHdzP8qg" + ` secret = "ix1aaifujilTcGEjB67e1EBBRXcr7r9cdChAR5hb" + secretBase64 = "e2xRY0yQmqhSetiQx0ZKWg==QMP1HstAoHdzP8qg" ) func TestCalorieNinja_Pattern(t *testing.T) { @@ -45,6 +50,11 @@ func TestCalorieNinja_Pattern(t *testing.T) { input: validPattern, want: []string{secret}, }, + { + name: "valid pattern with base64 characters", + input: validPatternWithBase64, + want: []string{secretBase64}, + }, } for _, test := range tests { diff --git a/pkg/detectors/campayn/campayn.go b/pkg/detectors/campayn/campayn.go index 018e715b0eb1..6e3aa6408e16 100644 --- a/pkg/detectors/campayn/campayn.go +++ b/pkg/detectors/campayn/campayn.go @@ -2,8 +2,11 @@ package campayn import ( "context" + "fmt" + "io" "net/http" "strings" + "time" regexp "github.com/wasilibs/go-re2" @@ -18,7 +21,7 @@ type Scanner struct{} var _ detectors.Detector = (*Scanner)(nil) var ( - client = common.SaneHttpClient() + client = common.SaneHttpClientTimeOut(10 * time.Second) // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"campayn"}) + `\b([a-z0-9]{64})\b`) @@ -45,18 +48,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://campayn.com/api/v1/lists", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", "TRUEREST apikey="+resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -65,6 +60,34 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://campayn.com/api/v1/lists", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Authorization", "TRUEREST apikey="+token) + 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 + // Timeout issues lead to flaky integration test results + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Campayn } diff --git a/pkg/detectors/campayn/campayn_integration_test.go b/pkg/detectors/campayn/campayn_integration_test.go index 7cd07d212c00..9fab356b509b 100644 --- a/pkg/detectors/campayn/campayn_integration_test.go +++ b/pkg/detectors/campayn/campayn_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" @@ -94,9 +95,10 @@ func TestCampayn_FromChunk(t *testing.T) { 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 != "" { + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo") + ignoreUnexported := cmpopts.IgnoreUnexported(detectors.Result{}) + if diff := cmp.Diff(got, tt.want, ignoreOpts, ignoreUnexported); diff != "" { t.Errorf("Campayn.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } }) diff --git a/pkg/detectors/cannyio/cannyio.go b/pkg/detectors/cannyio/cannyio.go index c85d2edde1ee..12f98e2d48ac 100644 --- a/pkg/detectors/cannyio/cannyio.go +++ b/pkg/detectors/cannyio/cannyio.go @@ -2,6 +2,8 @@ package cannyio import ( "context" + "fmt" + "io" "net/http" "strings" @@ -44,19 +46,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - payload := strings.NewReader("apiKey=" + resMatch) - req, err := http.NewRequestWithContext(ctx, "POST", "https://canny.io/api/v1/boards/list", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -65,6 +57,43 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + payload := strings.NewReader("apiKey=" + token) + req, err := http.NewRequestWithContext(ctx, "POST", "https://canny.io/api/v1/boards/list", payload) + if err != nil { + return false, err + } + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + 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.StatusBadRequest: + body, err := io.ReadAll(res.Body) + if err != nil { + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } + + if strings.Contains(strings.ToLower(string(body)), "invalid api key") { + return false, nil + } + + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_CannyIo } diff --git a/pkg/detectors/capsulecrm/capsulecrm.go b/pkg/detectors/capsulecrm/capsulecrm.go index 3af7c28d044f..85a437fb38dc 100644 --- a/pkg/detectors/capsulecrm/capsulecrm.go +++ b/pkg/detectors/capsulecrm/capsulecrm.go @@ -3,6 +3,7 @@ package capsulecrm import ( "context" "fmt" + "io" "net/http" "strings" @@ -46,18 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.capsulecrm.com/api/v2/users", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -66,6 +58,33 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.capsulecrm.com/api/v2/users", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + 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 HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_CapsuleCRM } diff --git a/pkg/detectors/carboninterface/carboninterface.go b/pkg/detectors/carboninterface/carboninterface.go index 7c768cae466f..68f6e1d24b50 100644 --- a/pkg/detectors/carboninterface/carboninterface.go +++ b/pkg/detectors/carboninterface/carboninterface.go @@ -3,6 +3,7 @@ package carboninterface import ( "context" "fmt" + "io" "net/http" "strings" @@ -46,20 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - payload := strings.NewReader(`{"type":"flight","passengers":2,"legs":[{"departure_airport":"sfo","destination_airport":"yyz"},{"departure_airport":"yyz","destination_airport":"sfo"}]}`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://www.carboninterface.com/api/v1/estimates", payload) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - req.Header.Add("Content-type", "application/json") - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -68,6 +58,34 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.carboninterface.com/api/v1/auth", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Add("Content-type", "application/json") + 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, http.StatusCreated: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_CarbonInterface } diff --git a/pkg/detectors/cashboard/cashboard.go b/pkg/detectors/cashboard/cashboard.go index 7b38ffc117e0..7b10fc447047 100644 --- a/pkg/detectors/cashboard/cashboard.go +++ b/pkg/detectors/cashboard/cashboard.go @@ -4,6 +4,7 @@ import ( "context" b64 "encoding/base64" "fmt" + "io" "net/http" "strings" @@ -26,7 +27,7 @@ var ( // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cashboard"}) + `\b([0-9A-Z]{3}-[0-9A-Z]{3}-[0-9A-Z]{3}-[0-9A-Z]{3})\b`) - userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"cashboard"}) + `\b([0-9a-z]{1,})\b`) + userPat = regexp.MustCompile(detectors.PrefixRegex([]string{"username"}) + `\b([0-9a-z]{1,})\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -55,21 +56,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - data := fmt.Sprintf("%s:%s", resUser, resMatch) - sEnc := b64.StdEncoding.EncodeToString([]byte(data)) - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.cashboardapp.com/account.xml", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc)) - - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyMatch(ctx, client, resUser, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -78,6 +67,38 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, user, token string) (bool, error) { + data := fmt.Sprintf("%s:%s", user, token) + sEnc := b64.StdEncoding.EncodeToString([]byte(data)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.cashboardapp.com/account", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", sEnc)) + req.Header.Add("Content-Type", "application/xml") + req.Header.Add("Accept", "application/xml") + + 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 HTTP response status %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Cashboard } diff --git a/pkg/detectors/cashboard/cashboard_integration_test.go b/pkg/detectors/cashboard/cashboard_integration_test.go index 4b4fd5db01db..f6f27426fcb2 100644 --- a/pkg/detectors/cashboard/cashboard_integration_test.go +++ b/pkg/detectors/cashboard/cashboard_integration_test.go @@ -19,12 +19,12 @@ import ( func TestCashboard_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } secret := testSecrets.MustGetField("CASHBOARD") - user := testSecrets.MustGetField("SCANNER_USERNAME") + user := testSecrets.MustGetField("CASHBOARD_USERNAME") inactiveSecret := testSecrets.MustGetField("CASHBOARD_INACTIVE") type args struct { @@ -44,7 +44,7 @@ func TestCashboard_FromChunk(t *testing.T) { s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a cashboard secret %s within %s", secret, user)), + data: []byte(fmt.Sprintf("You can find a cashboard secret %s and username %s", secret, user)), verify: true, }, want: []detectors.Result{ @@ -60,7 +60,7 @@ func TestCashboard_FromChunk(t *testing.T) { s: Scanner{}, args: args{ ctx: context.Background(), - data: []byte(fmt.Sprintf("You can find a cashboard secret %s within %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation + data: []byte(fmt.Sprintf("You can find a cashboard secret %s username %s but not valid", inactiveSecret, user)), // the secret would satisfy the regex but not pass validation verify: true, }, want: []detectors.Result{ @@ -96,6 +96,7 @@ func TestCashboard_FromChunk(t *testing.T) { t.Fatalf("no raw secret present: \n %+v", got[i]) } got[i].Raw = nil + got[i].RawV2 = nil } if diff := pretty.Compare(got, tt.want); diff != "" { t.Errorf("Cashboard.FromData() %s diff: (-got +want)\n%s", tt.name, diff) diff --git a/pkg/detectors/cashboard/cashboard_test.go b/pkg/detectors/cashboard/cashboard_test.go index e6c20e60721e..431bcecf3d17 100644 --- a/pkg/detectors/cashboard/cashboard_test.go +++ b/pkg/detectors/cashboard/cashboard_test.go @@ -12,23 +12,12 @@ import ( var ( validPattern = ` - # Configuration File: config.yaml - database: - host: $DB_HOST - port: $DB_PORT - username: $DB_USERNAME - password: $DB_PASS # IMPORTANT: Do not share this password publicly - api: cashboard_key: "F1A-NEI-HY4-PZK" - cashboard_user: "ts7z" + cashboard_username: "ts7z" auth_type: Basic base_url: "https://api.example.com/v1/user" auth_token: "" - - # Notes: - # - Remember to rotate the secret every 90 days. - # - The above credentials should only be used in a secure environment. ` secret = "F1A-NEI-HY4-PZKts7z" )