Skip to content

Commit a991501

Browse files
Merge branch 'main' into feat/oss-123-azure-cosmosdb-detector
2 parents 30b7549 + 3d197de commit a991501

File tree

3 files changed

+106
-27
lines changed

3 files changed

+106
-27
lines changed

pkg/analyzer/analyzers/privatekey/privatekey.go

+27
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"os"
9+
"regexp"
910
"strings"
1011
"sync"
1112
"time"
@@ -130,6 +131,9 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
130131
return
131132
}
132133

134+
// key entered through command line may have spaces instead of newlines, replace them
135+
token = replaceSpacesWithNewlines(token)
136+
133137
info, err := AnalyzePermissions(context.Background(), cfg, token)
134138
if err != nil {
135139
color.Red("[x] Error: %s", err.Error())
@@ -301,3 +305,26 @@ func analyzeGithubUser(ctx context.Context, parsedKey any) (*string, error) {
301305
func analyzeGitlabUser(ctx context.Context, parsedKey any) (*string, error) {
302306
return privatekey.VerifyGitLabUser(ctx, parsedKey)
303307
}
308+
309+
// replaceSpacesWithNewlines extracts the base64 part, replaces spaces with newlines if needed, and reconstructs the key.
310+
func replaceSpacesWithNewlines(privateKey string) string {
311+
// Regex pattern to extract the key content
312+
re := regexp.MustCompile(`(?i)(-----\s*BEGIN[ A-Z0-9_-]*PRIVATE KEY\s*-----)\s*([\s\S]*?)\s*(-----\s*END[ A-Z0-9_-]*PRIVATE KEY\s*-----)`)
313+
314+
// Find matches
315+
matches := re.FindStringSubmatch(privateKey)
316+
if len(matches) != 4 {
317+
// no need to process
318+
return privateKey
319+
}
320+
321+
header := matches[1] // BEGIN line
322+
base64Part := matches[2] // Base64 content
323+
footer := matches[3] // END line
324+
325+
// Replace spaces with newlines
326+
formattedBase64 := strings.ReplaceAll(base64Part, " ", "\n")
327+
328+
// Reconstruct the private key
329+
return fmt.Sprintf("%s\n%s\n%s", header, formattedBase64, footer)
330+
}

pkg/detectors/ipquality/ipquality.go

+74-20
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ package ipquality
22

33
import (
44
"context"
5+
"encoding/json"
6+
"errors"
57
"fmt"
6-
regexp "github.com/wasilibs/go-re2"
78
"io"
89
"net/http"
910
"strings"
1011

12+
regexp "github.com/wasilibs/go-re2"
13+
1114
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1215
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1316
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -22,9 +25,20 @@ var (
2225
client = common.SaneHttpClient()
2326

2427
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
25-
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ipquality"}) + `\b([0-9a-z]{32})\b`)
28+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"ipquality"}) + `\b([0-9a-zA-Z]{32})\b`)
2629
)
2730

31+
const (
32+
// response messages
33+
invalidKeyMessage = "Invalid or unauthorized key"
34+
insufficientCreditMessage = "insufficient credits"
35+
)
36+
37+
type apiResponse struct {
38+
Success bool `json:"success"`
39+
Message string `json:"message"`
40+
}
41+
2842
// Keywords are used for efficiently pre-filtering chunks.
2943
// Use identifiers in the secret preferably, or the provider name.
3044
func (s Scanner) Keywords() []string {
@@ -46,24 +60,10 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
4660
}
4761

4862
if verify {
49-
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://www.ipqualityscore.com/api/json/account/%s", resMatch), nil)
50-
if err != nil {
51-
continue
52-
}
53-
req.Header.Add("Content-Type", "application/json")
54-
res, err := client.Do(req)
55-
if err == nil {
56-
defer res.Body.Close()
57-
bodyBytes, err := io.ReadAll(res.Body)
58-
if err != nil {
59-
continue
60-
}
61-
body := string(bodyBytes)
62-
validResponse := strings.Contains(body, "insufficient credits") || strings.Contains(body, `"success":true`)
63-
if (res.StatusCode >= 200 && res.StatusCode < 300) && validResponse {
64-
s1.Verified = true
65-
}
66-
}
63+
isVerified, verificationErr := verifyIPQualityAPIKey(ctx, client, resMatch)
64+
65+
s1.Verified = isVerified
66+
s1.SetVerificationError(verificationErr)
6767
}
6868

6969
results = append(results, s1)
@@ -79,3 +79,57 @@ func (s Scanner) Type() detectorspb.DetectorType {
7979
func (s Scanner) Description() string {
8080
return "IPQualityScore provides tools to detect and prevent fraudulent activity. IPQualityScore API keys can be used to access their fraud prevention services."
8181
}
82+
83+
func verifyIPQualityAPIKey(ctx context.Context, client *http.Client, apiKey string) (bool, error) {
84+
req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://www.ipqualityscore.com/api/json/account/%s", apiKey), nil)
85+
if err != nil {
86+
return false, err
87+
}
88+
89+
req.Header.Add("Content-Type", "application/json")
90+
91+
resp, err := client.Do(req)
92+
if err != nil {
93+
return false, err
94+
}
95+
96+
defer func() {
97+
_, _ = io.Copy(io.Discard, resp.Body)
98+
_ = resp.Body.Close()
99+
}()
100+
101+
switch resp.StatusCode {
102+
case http.StatusOK:
103+
var response apiResponse
104+
105+
bodyBytes, err := io.ReadAll(resp.Body)
106+
if err != nil {
107+
return false, err
108+
}
109+
110+
if err = json.Unmarshal(bodyBytes, &response); err != nil {
111+
return false, err
112+
}
113+
114+
switch response.Success {
115+
case true:
116+
return true, nil
117+
case false:
118+
/*
119+
for invalid api key and for a key which has insufficient credit the API returns the same response.
120+
The scenario where we have correct API key but it has insufficient credit is rare than a scenario that we capture
121+
an invalid api key as the pattern is too common. Hence in case we get insufficient credit error message we mark the
122+
API Key as inactive and send back a verification error as well.
123+
*/
124+
if strings.Contains(response.Message, insufficientCreditMessage) {
125+
return false, errors.New("couldn't verify; API Key has " + insufficientCreditMessage)
126+
} else if strings.Contains(response.Message, invalidKeyMessage) {
127+
return false, nil
128+
}
129+
}
130+
131+
return false, nil
132+
default:
133+
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
134+
}
135+
}

pkg/detectors/ipquality/ipquality_integration_test.go

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
//go:build detectors
2-
// +build detectors
3-
41
package ipquality
52

63
import (
74
"context"
5+
"errors"
86
"fmt"
97
"testing"
108
"time"
@@ -26,6 +24,9 @@ func TestIpquality_FromChunk(t *testing.T) {
2624
secret := testSecrets.MustGetField("IPQUALITY")
2725
inactiveSecret := testSecrets.MustGetField("IPQUALITY_INACTIVE")
2826

27+
invalidResult := detectors.Result{DetectorType: detectorspb.DetectorType_IPQuality, Verified: false}
28+
invalidResult.SetVerificationError(errors.New("couldn't verify; API Key has " + insufficientCreditMessage))
29+
2930
type args struct {
3031
ctx context.Context
3132
data []byte
@@ -63,10 +64,7 @@ func TestIpquality_FromChunk(t *testing.T) {
6364
verify: true,
6465
},
6566
want: []detectors.Result{
66-
{
67-
DetectorType: detectorspb.DetectorType_IPQuality,
68-
Verified: false,
69-
},
67+
invalidResult,
7068
},
7169
wantErr: false,
7270
},

0 commit comments

Comments
 (0)