diff --git a/pkg/detectors/bannerbear/bannerbear.go b/pkg/detectors/bannerbear/bannerbear.go index 2e5957a68..5ca72ce1c 100644 --- a/pkg/detectors/bannerbear/bannerbear.go +++ b/pkg/detectors/bannerbear/bannerbear.go @@ -3,6 +3,7 @@ package bannerbear 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.bannerbear.com/v2/auth", 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 := verifyBannerBear(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -73,3 +65,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bannerbear is an API for generating dynamic images, videos, and GIFs. Bannerbear API keys can be used to access and manipulate these resources." } + +// docs: https://developers.bannerbear.com/ +func verifyBannerBear(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bannerbear.com/v2/auth", http.NoBody) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, nil + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/baremetrics/baremetrics.go b/pkg/detectors/baremetrics/baremetrics.go index 57e47970f..5359dec8a 100644 --- a/pkg/detectors/baremetrics/baremetrics.go +++ b/pkg/detectors/baremetrics/baremetrics.go @@ -3,6 +3,7 @@ package baremetrics import ( "context" "fmt" + "io" "net/http" "strings" @@ -52,18 +53,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.baremetrics.com/v1/account", 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 := verifyBaremetrics(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -79,3 +71,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Baremetrics is a subscription analytics and insights tool. Baremetrics API keys can be used to access and analyze subscription data." } + +// docs: https://developers.baremetrics.com/reference/authentication +func verifyBaremetrics(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.baremetrics.com/v1/account", http.NoBody) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, nil + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/beamer/beamer.go b/pkg/detectors/beamer/beamer.go index c597fbdcc..2a73e5213 100644 --- a/pkg/detectors/beamer/beamer.go +++ b/pkg/detectors/beamer/beamer.go @@ -2,6 +2,8 @@ package beamer import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,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.getbeamer.com/v0/url", nil) - if err != nil { - continue - } - req.Header.Add("Beamer-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 := verifyBeamer(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -72,3 +65,31 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Beamer is a user engagement platform that helps you communicate product updates and other important information to your users. Beamer API keys can be used to access and manage this information." } + +func verifyBeamer(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.getbeamer.com/v0/url", http.NoBody) + if err != nil { + return false, err + } + + req.Header.Add("Beamer-Api-Key", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/beebole/beebole.go b/pkg/detectors/beebole/beebole.go index 78687d52a..65f416e48 100644 --- a/pkg/detectors/beebole/beebole.go +++ b/pkg/detectors/beebole/beebole.go @@ -2,8 +2,8 @@ package beebole import ( "context" - b64 "encoding/base64" "fmt" + "io" "net/http" "strings" @@ -47,22 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - data := fmt.Sprintf("%s:X", resMatch) - sEnc := b64.StdEncoding.EncodeToString([]byte(data)) - payload := strings.NewReader(`{"service": "custom_field.list"}`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://beebole-apps.com/api/v2", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - 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 := verifyBeebole(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -78,3 +65,35 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Beebole is a time tracking and business management tool. Beebole API keys can be used to access and manage time tracking data and other business-related information." } + +// docs: https://beebole.com/help/api/ +func verifyBeebole(ctx context.Context, client *http.Client, key string) (bool, error) { + payload := strings.NewReader(`{"service": "custom_field.list"}`) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://beebole-apps.com/api/v2", payload) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(key, "x") + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/besnappy/besnappy.go b/pkg/detectors/besnappy/besnappy.go index effb5248b..9634bf98f 100644 --- a/pkg/detectors/besnappy/besnappy.go +++ b/pkg/detectors/besnappy/besnappy.go @@ -2,8 +2,8 @@ package besnappy import ( "context" - b64 "encoding/base64" "fmt" + "io" "net/http" "strings" @@ -46,20 +46,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result Raw: []byte(resMatch), } if verify { - data := fmt.Sprintf("%s:x", resMatch) - sEnc := b64.StdEncoding.EncodeToString([]byte(data)) - req, err := http.NewRequestWithContext(ctx, "GET", "https://app.besnappy.com/api/v1/accounts", 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 := verifyBesnappy(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -75,3 +64,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Besnappy is a customer service platform. The detected key can be used to access Besnappy's API, potentially exposing sensitive customer service data." } + +// docs: https://github.com/BeSnappy/api-docs +func verifyBesnappy(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://app.besnappy.com/api/v1/accounts", http.NoBody) + if err != nil { + return false, err + } + + req.SetBasicAuth(key, "x") + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/besttime/besttime.go b/pkg/detectors/besttime/besttime.go index c88565d99..30a51e90f 100644 --- a/pkg/detectors/besttime/besttime.go +++ b/pkg/detectors/besttime/besttime.go @@ -2,6 +2,7 @@ package besttime import ( "context" + "fmt" "io" "net/http" "strings" @@ -46,23 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://besttime.app/api/v1/keys/"+resMatch, nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - continue - } - body := string(bodyBytes) - - if strings.Contains(body, `"status": "OK"`) { - s1.Verified = true - } - } + isVerified, verificationErr := verifyBesttime(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -78,3 +65,40 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Besttime is a service used to predict the best time to visit a place. Besttime API keys can be used to access and utilize this service." } + +// docs: https://documentation.besttime.app/#api-reference +func verifyBesttime(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://besttime.app/api/v1/keys/"+key, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + body := string(bodyBytes) + + if strings.Contains(body, `"status": "OK"`) { + return true, nil + } else if strings.Contains(body, `"message": "Invalid api_key_private`) { + return false, nil + } + + return false, fmt.Errorf("unexpected response body: %s", body) + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/betterstack/betterstack.go b/pkg/detectors/betterstack/betterstack.go index c822daa30..6eecff1d1 100644 --- a/pkg/detectors/betterstack/betterstack.go +++ b/pkg/detectors/betterstack/betterstack.go @@ -3,6 +3,7 @@ package betterstack import ( "context" "fmt" + "io" "net/http" "strings" @@ -51,25 +52,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result if client == nil { client = defaultClient } - req, err := http.NewRequestWithContext(ctx, "GET", "https://uptime.betterstack.com/api/v2/monitors", 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 - } else if res.StatusCode == 401 { - // The secret is determinately not verified (nothing to do) - } else { - err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) - s1.SetVerificationError(err, resMatch) - } - } else { - s1.SetVerificationError(err, resMatch) - } + + isVerified, verificationErr := verifyBetterStack(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -85,3 +71,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Betterstack is a monitoring service for uptime and performance of websites and APIs. Betterstack API keys can be used to access and manage these monitoring services." } + +// docs: https://betterstack.com/docs/uptime/api/list-all-existing-monitors/ +func verifyBetterStack(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://uptime.betterstack.com/api/v2/monitors", nil) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/billomat/billomat.go b/pkg/detectors/billomat/billomat.go index 256da3f6a..d746c31f6 100644 --- a/pkg/detectors/billomat/billomat.go +++ b/pkg/detectors/billomat/billomat.go @@ -3,6 +3,7 @@ package billomat import ( "context" "fmt" + "io" "net/http" "strings" @@ -53,19 +54,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s.billomat.net/api/v2/clients/myself", resId), nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("X-BillomatApiKey", 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 := verifyBillomat(ctx, client, resId, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -82,3 +73,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Billomat is an online invoicing software. Billomat API keys can be used to access and manage invoices, clients, and other related data." } + +// docs: https://www.billomat.com/en/api/basics/authentication/ +func verifyBillomat(ctx context.Context, client *http.Client, id, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s.billomat.net/api/v2/clients/myself", id), nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-BillomatApiKey", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bitbar/bitbar.go b/pkg/detectors/bitbar/bitbar.go index 3082383fe..55b80dcb3 100644 --- a/pkg/detectors/bitbar/bitbar.go +++ b/pkg/detectors/bitbar/bitbar.go @@ -2,8 +2,8 @@ package bitbar import ( "context" - b64 "encoding/base64" "fmt" + "io" "net/http" "strings" @@ -23,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{"bitbar"}) + `\b([0-9a-z]{32})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"bitbar"}) + `\b([0-9a-zA-Z]{32})\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -47,21 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - data := fmt.Sprintf("%s:", resMatch) - baseToken := b64.StdEncoding.EncodeToString([]byte(data)) - req, err := http.NewRequestWithContext(ctx, "GET", "https://cloud.bitbar.com/api/me", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", baseToken)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyBitBar(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -77,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bitbar provides a cloud-based mobile app testing platform. Bitbar API keys can be used to access and manage testing resources and data." } + +// docs: https://support.smartbear.com/bitbar/docs/en/use-rest-apis-with-bitbar.html +func verifyBitBar(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://cloud.bitbar.com/api/me", http.NoBody) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(key, "") + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bitcoinaverage/bitcoinaverage.go b/pkg/detectors/bitcoinaverage/bitcoinaverage.go index 8821f5fe3..56e531ea8 100644 --- a/pkg/detectors/bitcoinaverage/bitcoinaverage.go +++ b/pkg/detectors/bitcoinaverage/bitcoinaverage.go @@ -3,6 +3,8 @@ package bitcoinaverage import ( "context" "encoding/json" + "fmt" + "io" "net/http" "strings" @@ -49,26 +51,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result DetectorType: detectorspb.DetectorType_BitcoinAverage, Raw: []byte(resMatch), } + if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://apiv2.bitcoinaverage.com/websocket/v3/get_ticket", nil) - if err != nil { - continue - } - req.Header.Add("x-ba-key", resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - resp := &response{} - if err = json.NewDecoder(res.Body).Decode(resp); err != nil { - s1.SetVerificationError(err, resMatch) - continue - } - if resp.Success { - s1.Verified = true - } - } - } + isVerified, verificationErr := verifyBitcoinAverage(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, resMatch) } results = append(results, s1) @@ -84,3 +71,41 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BitcoinAverage is a service that provides cryptocurrency market data. BitcoinAverage API keys can be used to access and retrieve this market data." } + +// docs: https://apiv2.bitcoinaverage.com/#authentication +func verifyBitcoinAverage(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://apiv2.bitcoinaverage.com/websocket/v3/get_ticket", nil) + if err != nil { + return false, err + } + + req.Header.Add("x-ba-key", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + apiResponse := &response{} + if err = json.NewDecoder(resp.Body).Decode(apiResponse); err != nil { + return false, err + } + + if apiResponse.Success { + return true, nil + } + + return false, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bitfinex/bitfinex.go b/pkg/detectors/bitfinex/bitfinex.go index 353a66151..8d0f728d6 100644 --- a/pkg/detectors/bitfinex/bitfinex.go +++ b/pkg/detectors/bitfinex/bitfinex.go @@ -43,50 +43,34 @@ func (s Scanner) Keywords() []string { func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) - apiKeyMatches := apiKeyPat.FindAllStringSubmatch(dataStr, -1) - apiSecretMatches := apiSecretPat.FindAllStringSubmatch(dataStr, -1) + var uniqueAPIKeys, uniqueAPISecrets = make(map[string]struct{}), make(map[string]struct{}) - for _, apiKeyMatch := range apiKeyMatches { - apiKeyRes := strings.TrimSpace(apiKeyMatch[1]) + for _, apiKey := range apiKeyPat.FindAllStringSubmatch(dataStr, -1) { + uniqueAPIKeys[apiKey[1]] = struct{}{} + } - s1 := detectors.Result{ - DetectorType: detectorspb.DetectorType_Bitfinex, - Raw: []byte(apiKeyRes), - } + for _, apiSecret := range apiSecretPat.FindAllStringSubmatch(dataStr, -1) { + uniqueAPISecrets[apiSecret[1]] = struct{}{} + } - for _, apiSecretMatch := range apiSecretMatches { - apiSecretRes := strings.TrimSpace(apiSecretMatch[1]) - - if apiKeyRes == apiSecretRes { + for apiKey := range uniqueAPIKeys { + for apiSecret := range uniqueAPISecrets { + // as both patterns are same, avoid verifying same string for both + if apiKey == apiSecret { continue } + s1 := detectors.Result{ + DetectorType: detectorspb.DetectorType_Bitfinex, + Raw: []byte(apiKey), + } + if verify { - // thankfully official golang examples exist but you just need to dig their many repos https://github.com/bitfinexcom/bitfinex-api-go/blob/master/examples/v2/rest-orders/main.go - key := apiKeyRes - secret := apiSecretRes - http.DefaultClient = client // filed https://github.com/bitfinexcom/bitfinex-api-go/issues/238 to improve this - c := rest.NewClientWithURL(*api).Credentials(key, secret) - - isValid := true // assume valid - _, err = c.Orders.AllHistory() - if err != nil { - if strings.HasPrefix(err.Error(), "POST https://") { // eg POST https://api-pub.bitfinex.com/v2/auth/r/orders/hist: 500 apikey: digest invalid (10100) - isValid = false - } - } - - s1.Verified = isValid - // If there is a valid one, we need to stop iterating now and return the valid result - if isValid { - break - } + isVerified, verificationErr := verifyBitfinex(apiKey, apiSecret) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } - } - // By appending results in the outer loop we can reduce false positives if there are multiple - // combinations of secrets and IDs found. - if len(apiSecretMatches) > 0 { results = append(results, s1) } } @@ -101,3 +85,19 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bitfinex is a cryptocurrency exchange offering various trading options. Bitfinex API keys can be used to access and manage trading accounts." } + +// docs: https://docs.bitfinex.com/docs/introduction +func verifyBitfinex(key, secret string) (bool, error) { + // thankfully official golang examples exist but you just need to dig their many repos https://github.com/bitfinexcom/bitfinex-api-go/blob/master/examples/v2/rest-orders/main.go + http.DefaultClient = client + c := rest.NewClientWithURL(*api).Credentials(key, secret) + + _, err := c.Orders.AllHistory() + if err != nil { + if strings.HasPrefix(err.Error(), "POST https://") { // eg POST https://api-pub.bitfinex.com/v2/auth/r/orders/hist: 500 apikey: digest invalid (10100) + return false, nil + } + } + + return true, nil +} diff --git a/pkg/detectors/bitmex/bitmex.go b/pkg/detectors/bitmex/bitmex.go index aa136bfe9..363e5163b 100644 --- a/pkg/detectors/bitmex/bitmex.go +++ b/pkg/detectors/bitmex/bitmex.go @@ -5,6 +5,8 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/hex" + "fmt" + "io" "net/http" "net/url" "strconv" @@ -59,28 +61,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - - timestamp := strconv.FormatInt(time.Now().Unix()+5, 10) - action := "GET" - path := "/api/v1/user" - payload := url.Values{} - - signature := getBitmexSignature(timestamp, resSecretMatch, action, path, payload.Encode()) - - req, err := http.NewRequestWithContext(ctx, action, "https://www.bitmex.com"+path, strings.NewReader(payload.Encode())) - if err != nil { - continue - } - req.Header.Add("api-expires", timestamp) - req.Header.Add("api-key", resMatch) - req.Header.Add("api-signature", signature) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + isVerified, verificationErr := verifyBitmex(ctx, client, resMatch, resSecretMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -90,14 +73,6 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } -func getBitmexSignature(timeStamp string, secret string, action string, path string, payload string) string { - - mac := hmac.New(sha256.New, []byte(secret)) - mac.Write([]byte(action + path + timeStamp + payload)) - macsum := mac.Sum(nil) - return hex.EncodeToString(macsum) -} - func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Bitmex } @@ -105,3 +80,47 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bitmex is a cryptocurrency exchange and derivative trading platform. Bitmex API keys can be used to access and trade on the platform programmatically." } + +// docs: https://www.bitmex.com/app/apiKeysUsage +func verifyBitmex(ctx context.Context, client *http.Client, key, secret string) (bool, error) { + timestamp := strconv.FormatInt(time.Now().Unix()+5, 10) + action := "GET" + path := "/api/v1/user" + payload := url.Values{} + + signature := getBitmexSignature(timestamp, secret, action, path, payload.Encode()) + + req, err := http.NewRequestWithContext(ctx, action, "https://www.bitmex.com"+path, strings.NewReader(payload.Encode())) + if err != nil { + return false, err + } + + req.Header.Add("api-expires", timestamp) + req.Header.Add("api-key", key) + req.Header.Add("api-signature", signature) + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} + +func getBitmexSignature(timeStamp string, secret string, action string, path string, payload string) string { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(action + path + timeStamp + payload)) + macsum := mac.Sum(nil) + return hex.EncodeToString(macsum) +} diff --git a/pkg/detectors/blazemeter/blazemeter.go b/pkg/detectors/blazemeter/blazemeter.go index 1eac63827..dc6df12f4 100644 --- a/pkg/detectors/blazemeter/blazemeter.go +++ b/pkg/detectors/blazemeter/blazemeter.go @@ -3,6 +3,7 @@ package blazemeter import ( "context" "fmt" + "io" "net/http" "strings" @@ -46,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.runscope.com/account", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - 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 := verifyBlazeMeter(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -74,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Blazemeter is a continuous testing platform for DevOps. The keys can be used to access and manage performance tests and other resources." } + +// docs: https://help.blazemeter.com/apidocs/api-monitoring/account.htm?tocpath=API%20Monitoring%7C_____12 +func verifyBlazeMeter(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.runscope.com/account", nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/blitapp/blitapp.go b/pkg/detectors/blitapp/blitapp.go index 8959dc4c8..21f10e0ab 100644 --- a/pkg/detectors/blitapp/blitapp.go +++ b/pkg/detectors/blitapp/blitapp.go @@ -2,6 +2,8 @@ package blitapp import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,18 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://blitapp.com/api/apps/all", nil) - if err != nil { - continue - } - req.Header.Add("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 := verifyBlitApp(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -72,3 +65,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BlitApp is a service used for managing applications. BlitApp API keys can be used to access and modify application data." } + +// docs: https://blitapp.com/api/#/App/get_apps_all +func verifyBlitApp(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://blitapp.com/api/apps/all", nil) + if err != nil { + return false, nil + } + + req.Header.Add("API-Key", key) + + resp, err := client.Do(req) + if err != nil { + return false, nil + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/blocknative/blocknative.go b/pkg/detectors/blocknative/blocknative.go index 335107821..c9e5e626f 100644 --- a/pkg/detectors/blocknative/blocknative.go +++ b/pkg/detectors/blocknative/blocknative.go @@ -2,6 +2,7 @@ package blocknative import ( "context" + "fmt" "io" "net/http" "strings" @@ -46,23 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.blocknative.com/gasprices/blockprices", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - continue - } - body := string(bodyBytes) - if !strings.Contains(body, "valid") { - s1.Verified = true - } - } + isVerified, verificationErr := verifyBlocknative(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -78,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Blocknative is a platform that provides real-time blockchain transaction monitoring and notification services. Blocknative API keys can be used to access and interact with these services." } + +// docs: https://docs.blocknative.com/gas-prediction/gas-platform#api-endpoint +func verifyBlocknative(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.blocknative.com/gasprices/blockprices", nil) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + // Right now the blocknative API logic is broken and return 200 for invalid key as well + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized, http.StatusTooManyRequests: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/blogger/blogger.go b/pkg/detectors/blogger/blogger.go index 800d75c50..ef672bb0e 100644 --- a/pkg/detectors/blogger/blogger.go +++ b/pkg/detectors/blogger/blogger.go @@ -2,6 +2,8 @@ package blogger import ( "context" + "fmt" + "io" "net/http" "strings" @@ -44,17 +46,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/blogger/v3/blogs/2399953?key="+resMatch, 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 := verifyBlogger(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -70,3 +64,30 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Blogger API keys can be used to access and manage blogs on the Blogger platform." } + +// docs: https://developers.google.com/blogger/docs/3.0/using +func verifyBlogger(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/blogger/v3/blogs/2399953?key="+key, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusBadRequest, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bombbomb/bombbomb.go b/pkg/detectors/bombbomb/bombbomb.go index ca1e2a282..177520eb3 100644 --- a/pkg/detectors/bombbomb/bombbomb.go +++ b/pkg/detectors/bombbomb/bombbomb.go @@ -2,6 +2,8 @@ package bombbomb import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,19 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - // Reference : https://developer.bombbomb.com/api#operations-Users-UserInfo - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bombbomb.com/v2/user/", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", "Bearer "+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 := verifyBombBomb(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -73,3 +65,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BombBomb is a video messaging platform that allows users to create and send video emails. BombBomb API keys can be used to access and manage video email campaigns and contacts." } + +// docs: https://developer.bombbomb.com/api#operations-Users-UserInfo +func verifyBombBomb(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bombbomb.com/v2/user/", nil) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", "Bearer "+key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/boostnote/boostnote.go b/pkg/detectors/boostnote/boostnote.go index 4ef5ea55c..085265bd8 100644 --- a/pkg/detectors/boostnote/boostnote.go +++ b/pkg/detectors/boostnote/boostnote.go @@ -3,6 +3,7 @@ package boostnote 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://boostnote.io/api/docs", 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 := verifyBoostnote(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -73,3 +65,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BoostNote is a note-taking application. The secret detected here is likely an API key or token used to access BoostNote services." } + +// docs: https://boostnote.io/features/public-api +func verifyBoostnote(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://boostnote.io/api/docs", nil) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/borgbase/borgbase.go b/pkg/detectors/borgbase/borgbase.go index de8daf244..f13b947d3 100644 --- a/pkg/detectors/borgbase/borgbase.go +++ b/pkg/detectors/borgbase/borgbase.go @@ -48,31 +48,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - timeout := 10 * time.Second - client.Timeout = timeout - payload := strings.NewReader(`{"query":"{ sshList {id, name}}"}`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://api.borgbase.com/graphql", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - res, err := client.Do(req) - if err == nil { - bodyBytes, err := io.ReadAll(res.Body) - if err == nil { - bodyString := string(bodyBytes) - validResponse := strings.Contains(bodyString, `"sshList":[]`) - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - if validResponse { - s1.Verified = true - } else { - s1.Verified = false - } - } - } - } + isVerified, verificationErr := verifyBorgbase(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -88,3 +66,49 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Borgbase is a service for hosting Borg repositories. Borgbase API keys can be used to manage and access these repositories." } + +// docs: https://docs.borgbase.com/api +func verifyBorgbase(ctx context.Context, client *http.Client, key string) (bool, error) { + timeout := 10 * time.Second + client.Timeout = timeout + + payload := strings.NewReader(`{"query":"{ sshList {id, name}}"}`) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://api.borgbase.com/graphql", payload) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + bodyString := string(bodyBytes) + validResponse := strings.Contains(bodyString, `"sshList":[]`) + if validResponse { + return true, nil + } + + return false, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/braintreepayments/braintreepayments.go b/pkg/detectors/braintreepayments/braintreepayments.go index 0b01b5caf..fe1b26b0e 100644 --- a/pkg/detectors/braintreepayments/braintreepayments.go +++ b/pkg/detectors/braintreepayments/braintreepayments.go @@ -3,11 +3,12 @@ package braintreepayments import ( "context" "fmt" - regexp "github.com/wasilibs/go-re2" "io" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -105,7 +106,10 @@ func verifyBraintree(ctx context.Context, client *http.Client, url, pubKey, priv if err != nil { return false, err } - defer res.Body.Close() + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() bodyString := string(bodyBytes) if !(res.StatusCode == http.StatusOK) { diff --git a/pkg/detectors/brandfetch/brandfetch.go b/pkg/detectors/brandfetch/brandfetch.go index 6ab54b5e2..eabb97b16 100644 --- a/pkg/detectors/brandfetch/brandfetch.go +++ b/pkg/detectors/brandfetch/brandfetch.go @@ -2,6 +2,8 @@ package brandfetch import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,22 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - payload := strings.NewReader(`{ - "domain": "www.example.com" - }`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://api.brandfetch.io/v1/color", payload) - 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 := verifyBrandFetch(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -76,3 +65,37 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Brandfetch is a service that provides brand data, including logos, colors, fonts, and more. Brandfetch API keys can be used to access this data." } + +// docs: https://docs.brandfetch.com/docs/brand-api#overview +func verifyBrandFetch(ctx context.Context, client *http.Client, key string) (bool, error) { + payload := strings.NewReader(`{ + "domain": "www.example.com" + }`) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://api.brandfetch.io/v1/color", payload) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("x-api-key", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized, http.StatusForbidden: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/browshot/browshot.go b/pkg/detectors/browshot/browshot.go index 71e4f4f6a..c6a3e0b33 100644 --- a/pkg/detectors/browshot/browshot.go +++ b/pkg/detectors/browshot/browshot.go @@ -2,6 +2,8 @@ package browshot 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://api.browshot.com/api/v1/instance/list?key="+resMatch, 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 := verifyBrowshot(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -71,3 +65,30 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Browshot is a service that allows you to take screenshots of web pages from different browsers and devices. Browshot API keys can be used to automate and manage these screenshots." } + +// docs: https://browshot.com/api/documentation#instance_list +func verifyBrowshot(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.browshot.com/api/v1/instance/list?key="+key, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusBadRequest, http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bscscan/bscscan.go b/pkg/detectors/bscscan/bscscan.go index 6142279f4..f72b477a1 100644 --- a/pkg/detectors/bscscan/bscscan.go +++ b/pkg/detectors/bscscan/bscscan.go @@ -2,6 +2,7 @@ package bscscan import ( "context" + "fmt" "io" "net/http" "strings" @@ -46,23 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bscscan.com/api?module=account&action=balance&address=0x70F657164e5b75689b64B7fd1fA275F334f28e18&apikey="+resMatch, nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - continue - } - body := string(bodyBytes) - - if !strings.Contains(body, "NOTOK") { - s1.Verified = true - } - } + isVerified, verificationErr := verifyBscScan(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -78,3 +65,41 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BscScan is a block explorer and analytics platform for Binance Smart Chain. BscScan API keys can be used to access data from the Binance Smart Chain blockchain." } + +// docs: https://docs.bscscan.com/api-endpoints/accounts +func verifyBscScan(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bscscan.com/api?module=account&action=balance&address=0x70F657164e5b75689b64B7fd1fA275F334f28e18&apikey="+key, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + body := string(bodyBytes) + + if !strings.Contains(body, "NOTOK") { + return true, nil + } + + return false, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/buddyns/buddyns.go b/pkg/detectors/buddyns/buddyns.go index dad033366..e6e0d2b72 100644 --- a/pkg/detectors/buddyns/buddyns.go +++ b/pkg/detectors/buddyns/buddyns.go @@ -3,6 +3,7 @@ package buddyns import ( "context" "fmt" + "io" "net/http" "strings" @@ -46,19 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://www.buddyns.com/api/v2/zone/", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("Token %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 := verifyBuddyns(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -74,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BuddyNS is a DNS hosting service. BuddyNS API keys can be used to manage DNS zones and records." } + +// docs: https://www.buddyns.com/support/api/v2/ +func verifyBuddyns(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://www.buddyns.com/api/v2/zone/", nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("Token %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/budibase/budibase.go b/pkg/detectors/budibase/budibase.go index 62129b06e..b37f802b0 100644 --- a/pkg/detectors/budibase/budibase.go +++ b/pkg/detectors/budibase/budibase.go @@ -3,6 +3,7 @@ package budibase import ( "context" "fmt" + "io" "net/http" "strings" @@ -52,31 +53,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result client = defaultClient } - // URL: https://docs.budibase.com/reference/appsearch - // API searches for the app with given name, since we only need to check api key, sending any appname will work. - payload := strings.NewReader(`{"name":"qwerty"}`) - - req, err := http.NewRequestWithContext(ctx, "POST", "https://budibase.app/api/public/v1/applications/search", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("x-budibase-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 - } else if res.StatusCode == 401 { - // The secret is determinately not verified (nothing to do) - } else { - err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) - s1.SetVerificationError(err, resMatch) - } - } else { - s1.SetVerificationError(err, resMatch) - } + isVerified, verificationErr := verifyBudibase(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -92,3 +71,37 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Budibase is a low-code platform for creating internal tools. Budibase API keys can be used to access and modify applications and data within the platform." } + +// docs: https://docs.budibase.com/docs/rest +func verifyBudibase(ctx context.Context, client *http.Client, key string) (bool, error) { + // URL: https://docs.budibase.com/reference/appsearch + // API searches for the app with given name, since we only need to check api key, sending any appname will work. + payload := strings.NewReader(`{"name":"qwerty"}`) + + req, err := http.NewRequestWithContext(ctx, "POST", "https://budibase.app/api/public/v1/applications/search", payload) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("x-budibase-api-key", key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/budibase/budibase_integration_test.go b/pkg/detectors/budibase/budibase_integration_test.go index ebd9ce6c8..40eead4b0 100644 --- a/pkg/detectors/budibase/budibase_integration_test.go +++ b/pkg/detectors/budibase/budibase_integration_test.go @@ -69,7 +69,7 @@ func TestBudibase_FromChunk(t *testing.T) { want: func() []detectors.Result { r := detectors.Result{ DetectorType: detectorspb.DetectorType_Budibase, - Verified: true, + Verified: false, } r.SetVerificationError(fmt.Errorf("unexpected HTTP response status 403")) return []detectors.Result{r} diff --git a/pkg/detectors/bugherd/bugherd.go b/pkg/detectors/bugherd/bugherd.go index 7120be07b..3544e784b 100644 --- a/pkg/detectors/bugherd/bugherd.go +++ b/pkg/detectors/bugherd/bugherd.go @@ -2,12 +2,13 @@ package bugherd import ( "context" - b64 "encoding/base64" "fmt" - regexp "github.com/wasilibs/go-re2" + "io" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -46,21 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - data := fmt.Sprintf("%s:x", resMatch) - sEnc := b64.StdEncoding.EncodeToString([]byte(data)) - req, err := http.NewRequestWithContext(ctx, "GET", "https://www.bugherd.com/api_v2/projects.json", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - 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 := verifyBugherd(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -76,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bugherd is a visual feedback and bug tracking tool for websites. Bugherd API keys can be used to access and manage projects, tasks, and feedback data." } + +// docs: https://www.bugherd.com/api_v2 +func verifyBugherd(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://www.bugherd.com/api_v2/projects.json", nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.SetBasicAuth(key, "x") + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bugsnag/bugsnag.go b/pkg/detectors/bugsnag/bugsnag.go index 15252f04a..752d6fb73 100644 --- a/pkg/detectors/bugsnag/bugsnag.go +++ b/pkg/detectors/bugsnag/bugsnag.go @@ -3,6 +3,7 @@ package bugsnag import ( "context" "fmt" + "io" "net/http" "strings" @@ -46,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.bugsnag.com/user/organizations?admin", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf("token %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 := verifyBugsnag(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -74,3 +65,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bugsnag is an error monitoring service for web and mobile applications. Bugsnag API keys can be used to report and manage errors." } + +// docs: https://docs.bugsnag.com/api/ +func verifyBugsnag(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bugsnag.com/user/organizations?admin", nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf("token %s", key)) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bulbul/bulbul.go b/pkg/detectors/bulbul/bulbul.go index d3b1aad84..e744b7509 100644 --- a/pkg/detectors/bulbul/bulbul.go +++ b/pkg/detectors/bulbul/bulbul.go @@ -3,11 +3,12 @@ package bulbul import ( "context" "fmt" - regexp "github.com/wasilibs/go-re2" "io" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -46,30 +47,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://prod-api.bulbul.io/view_all_users?api_key=%s", resMatch), nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - bodyBytes, err := io.ReadAll(res.Body) - - if err != nil { - continue - } - - bodyString := string(bodyBytes) - validResponse := strings.Contains(bodyString, `"message":"Successful",`) - - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - if validResponse { - s1.Verified = true - } else { - s1.Verified = false - } - } - } + isVerified, verificationErr := verifyBulbul(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -85,3 +65,41 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Bulbul is an API service. Bulbul API keys can be used to access and modify data within the service." } + +// docs: https://docs.jungleworks.com/bulbul/bulbul-api-details +func verifyBulbul(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://prod-api.bulbul.io/view_all_users?api_key=%s", key), nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + bodyString := string(bodyBytes) + + if strings.Contains(bodyString, `"message":"Successful",`) { + return true, nil + } else { + return false, nil + } + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/bulksms/bulksms.go b/pkg/detectors/bulksms/bulksms.go index 0a80a97bb..8cdbe7b8d 100644 --- a/pkg/detectors/bulksms/bulksms.go +++ b/pkg/detectors/bulksms/bulksms.go @@ -2,6 +2,7 @@ package bulksms import ( "context" + "fmt" "io" "net/http" @@ -56,27 +57,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bulksms.com/v1/messages", nil) - if err != nil { - continue - } - req.SetBasicAuth(id, key) - res, err := client.Do(req) - if err == nil { - defer func() { - _, _ = io.Copy(io.Discard, res.Body) - _ = res.Body.Close() - }() - - if res.StatusCode == http.StatusOK { - s1.Verified = true - results = append(results, s1) - // move to next id, by skipping remaining key's - break - } - } else { - s1.SetVerificationError(err, key) - } + isVerified, verificationErr := verifyBulksms(ctx, client, id, key) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -93,3 +76,32 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "BulkSMS is a service used for sending SMS messages in bulk. BulkSMS credentials can be used to access and send messages through the BulkSMS API." } + +// docs: https://www.bulksms.com/developer/json/v1/ +func verifyBulksms(ctx context.Context, client *http.Client, id, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.bulksms.com/v1/messages", nil) + if err != nil { + return false, err + } + + req.SetBasicAuth(id, key) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/buttercms/buttercms.go b/pkg/detectors/buttercms/buttercms.go index 5944e62dc..36c7a12a8 100644 --- a/pkg/detectors/buttercms/buttercms.go +++ b/pkg/detectors/buttercms/buttercms.go @@ -2,10 +2,13 @@ package buttercms import ( "context" - regexp "github.com/wasilibs/go-re2" + "fmt" + "io" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -44,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.buttercms.com/v2/posts/?auth_token="+resMatch, 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 := verifyButterCMS(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -71,3 +65,30 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "ButterCMS is a headless CMS that enables developers to build websites and applications with a content management system. The API keys can be used to access and modify content stored in ButterCMS." } + +// docs: https://buttercms.com/docs/api/#introduction +func verifyButterCMS(ctx context.Context, client *http.Client, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", "https://api.buttercms.com/v2/posts/?auth_token="+key, nil) + if err != nil { + return false, err + } + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +}