diff --git a/pkg/detectors/ipquality/ipquality.go b/pkg/detectors/ipquality/ipquality.go index 2fdf8fe1e..25c9e3b31 100644 --- a/pkg/detectors/ipquality/ipquality.go +++ b/pkg/detectors/ipquality/ipquality.go @@ -2,12 +2,15 @@ package ipquality import ( "context" + "encoding/json" + "errors" "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" @@ -22,9 +25,20 @@ 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{"ipquality"}) + `\b([0-9a-z]{32})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ipquality"}) + `\b([0-9a-zA-Z]{32})\b`) ) +const ( + // response messages + invalidKeyMessage = "Invalid or unauthorized key" + insufficientCreditMessage = "insufficient credits" +) + +type apiResponse struct { + Success bool `json:"success"` + Message string `json:"message"` +} + // Keywords are used for efficiently pre-filtering chunks. // Use identifiers in the secret preferably, or the provider name. func (s Scanner) Keywords() []string { @@ -46,24 +60,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://www.ipqualityscore.com/api/json/account/%s", resMatch), nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - 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) - validResponse := strings.Contains(body, "insufficient credits") || strings.Contains(body, `"success":true`) - if (res.StatusCode >= 200 && res.StatusCode < 300) && validResponse { - s1.Verified = true - } - } + isVerified, verificationErr := verifyIPQualityAPIKey(ctx, client, resMatch) + + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -79,3 +79,57 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "IPQualityScore provides tools to detect and prevent fraudulent activity. IPQualityScore API keys can be used to access their fraud prevention services." } + +func verifyIPQualityAPIKey(ctx context.Context, client *http.Client, apiKey string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://www.ipqualityscore.com/api/json/account/%s", apiKey), nil) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + + 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: + var response apiResponse + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + if err = json.Unmarshal(bodyBytes, &response); err != nil { + return false, err + } + + switch response.Success { + case true: + return true, nil + case false: + /* + for invalid api key and for a key which has insufficient credit the API returns the same response. + The scenario where we have correct API key but it has insufficient credit is rare than a scenario that we capture + an invalid api key as the pattern is too common. Hence in case we get insufficient credit error message we mark the + API Key as inactive and send back a verification error as well. + */ + if strings.Contains(response.Message, insufficientCreditMessage) { + return false, errors.New("couldn't verify; API Key has " + insufficientCreditMessage) + } else if strings.Contains(response.Message, invalidKeyMessage) { + return false, nil + } + } + + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/ipquality/ipquality_integration_test.go b/pkg/detectors/ipquality/ipquality_integration_test.go index 237e024ef..61eb39659 100644 --- a/pkg/detectors/ipquality/ipquality_integration_test.go +++ b/pkg/detectors/ipquality/ipquality_integration_test.go @@ -1,10 +1,8 @@ -//go:build detectors -// +build detectors - package ipquality import ( "context" + "errors" "fmt" "testing" "time" @@ -26,6 +24,9 @@ func TestIpquality_FromChunk(t *testing.T) { secret := testSecrets.MustGetField("IPQUALITY") inactiveSecret := testSecrets.MustGetField("IPQUALITY_INACTIVE") + invalidResult := detectors.Result{DetectorType: detectorspb.DetectorType_IPQuality, Verified: false} + invalidResult.SetVerificationError(errors.New("couldn't verify; API Key has " + insufficientCreditMessage)) + type args struct { ctx context.Context data []byte @@ -63,10 +64,7 @@ func TestIpquality_FromChunk(t *testing.T) { verify: true, }, want: []detectors.Result{ - { - DetectorType: detectorspb.DetectorType_IPQuality, - Verified: false, - }, + invalidResult, }, wantErr: false, },