Skip to content

Commit e7accb0

Browse files
committed
LambdaTest secret detector
1 parent 5fca163 commit e7accb0

File tree

6 files changed

+393
-7
lines changed

6 files changed

+393
-7
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package lambdatest
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
11+
regexp "github.com/wasilibs/go-re2"
12+
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
16+
)
17+
18+
type Scanner struct {
19+
client *http.Client
20+
}
21+
22+
// Ensure the Scanner satisfies the interface at compile time.
23+
var _ detectors.Detector = (*Scanner)(nil)
24+
25+
var (
26+
defaultClient = common.SaneHttpClient()
27+
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
28+
usernamePat = regexp.MustCompile(detectors.PrefixRegex([]string{
29+
"hub.lambdatest.com",
30+
"userName",
31+
"\"username\":",
32+
"USER_NAME",
33+
"user",
34+
"lambdatestUser",
35+
"LT_USERNAME",
36+
"LAMBDATEST_USERNAME",
37+
}) + `\b([a-zA-Z0-9]+)\b`)
38+
39+
accessKeyPat = regexp.MustCompile(detectors.PrefixRegex([]string{
40+
"hub.lambdatest.com",
41+
"accessKey",
42+
"\"access_Key\":",
43+
"ACCESS_KEY",
44+
"key",
45+
"lambdatestKey",
46+
"LT_AUTHKEY",
47+
"LT_ACCESS_KEY",
48+
}) + `\b(LT_[a-zA-Z0-9]{47})\b`)
49+
)
50+
51+
// Keywords are used for efficiently pre-filtering chunks.
52+
// Use identifiers in the secret preferably, or the provider name.
53+
func (s Scanner) Keywords() []string {
54+
return []string{"lambdatest", "LT_"}
55+
}
56+
57+
// FromData will find and optionally verify Lambdatest secrets in a given set of bytes.
58+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
59+
dataStr := string(data)
60+
uniqueUsernameMatches := make(map[string]struct{})
61+
for _, match := range usernamePat.FindAllStringSubmatch(dataStr, -1) {
62+
uniqueUsernameMatches[match[1]] = struct{}{}
63+
}
64+
65+
uniqueAccessKeyMatches := make(map[string]struct{})
66+
for _, match := range accessKeyPat.FindAllStringSubmatch(dataStr, -1) {
67+
uniqueAccessKeyMatches[match[1]] = struct{}{}
68+
}
69+
for usernameMatch := range uniqueUsernameMatches {
70+
for accessKeyMatch := range uniqueAccessKeyMatches {
71+
72+
s1 := detectors.Result{
73+
DetectorType: detectorspb.DetectorType_LambdaTest,
74+
Raw: []byte(fmt.Sprintf("%s:%s", usernameMatch, accessKeyMatch)),
75+
}
76+
77+
if verify {
78+
client := s.client
79+
if client == nil {
80+
client = defaultClient
81+
}
82+
83+
isVerified, extraData, verificationErr := verifyMatch(ctx, client, usernameMatch, accessKeyMatch)
84+
s1.Verified = isVerified
85+
s1.ExtraData = extraData
86+
s1.SetVerificationError(verificationErr, usernameMatch)
87+
}
88+
89+
results = append(results, s1)
90+
}
91+
}
92+
93+
return results, nil
94+
}
95+
96+
func verifyMatch(ctx context.Context, client *http.Client, usernameMatch string, accessKeyMatch string) (bool, map[string]string, error) {
97+
body := map[string]string{
98+
"username": usernameMatch,
99+
"token": accessKeyMatch,
100+
}
101+
102+
// encode the body as JSON
103+
jsonBody, err := json.Marshal(body)
104+
if err != nil {
105+
return false, nil, err
106+
}
107+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://auth.lambdatest.com/api/user/token/auth", bytes.NewBuffer(jsonBody))
108+
if err != nil {
109+
return false, nil, nil
110+
}
111+
req.Header.Set("Content-Type", "application/json")
112+
113+
res, err := client.Do(req)
114+
if err != nil {
115+
return false, nil, err
116+
}
117+
defer func() {
118+
_, _ = io.Copy(io.Discard, res.Body)
119+
_ = res.Body.Close()
120+
}()
121+
122+
switch res.StatusCode {
123+
case http.StatusOK:
124+
// If the endpoint returns useful information, we can return it as a map.
125+
return true, nil, nil
126+
case http.StatusUnauthorized:
127+
// The secret is determinately not verified (nothing to do)
128+
return false, nil, fmt.Errorf("access key not present in db or invalid")
129+
default:
130+
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
131+
}
132+
}
133+
134+
func (s Scanner) Type() detectorspb.DetectorType {
135+
return detectorspb.DetectorType_LambdaTest
136+
}
137+
138+
func (s Scanner) Description() string {
139+
return "LambdaTest is a cloud-based cross-browser testing platform that allows developers to test their web applications across various browsers and devices."
140+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package lambdatest
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
14+
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
16+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
17+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
18+
)
19+
20+
func TestLambdatest_FromChunk(t *testing.T) {
21+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
22+
defer cancel()
23+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
24+
if err != nil {
25+
t.Fatalf("could not get test secrets from GCP: %s", err)
26+
}
27+
username := testSecrets.MustGetField("LAMBDATEST_USERNAME")
28+
accessKey := testSecrets.MustGetField("LAMBDATEST_ACCESSKEY")
29+
invalidUsername := testSecrets.MustGetField("LAMBDATEST_INVALID_USERNAME")
30+
invalidAccessKey := testSecrets.MustGetField("LAMBDATEST_INVALID_ACCESSKEY")
31+
32+
type args struct {
33+
ctx context.Context
34+
data []byte
35+
verify bool
36+
}
37+
tests := []struct {
38+
name string
39+
s Scanner
40+
args args
41+
want []detectors.Result
42+
wantErr bool
43+
wantVerificationErr bool
44+
}{
45+
{
46+
name: "found, verified",
47+
s: Scanner{},
48+
args: args{
49+
ctx: context.Background(),
50+
data: []byte(fmt.Sprintf("You can find a lambdatest username %s and lambdatest accesskey %s within", username, accessKey)),
51+
verify: true,
52+
},
53+
want: []detectors.Result{
54+
{
55+
DetectorType: detectorspb.DetectorType_LambdaTest,
56+
Verified: true,
57+
},
58+
},
59+
wantErr: false,
60+
wantVerificationErr: false,
61+
},
62+
{
63+
name: "found, unverified",
64+
s: Scanner{},
65+
args: args{
66+
ctx: context.Background(),
67+
data: []byte(fmt.Sprintf("You can find a lambdatest username %s and lambdatest accesskey %s within but not valid", invalidUsername, invalidAccessKey)), // the secret would satisfy the regex but not pass validation
68+
verify: true,
69+
},
70+
want: []detectors.Result{
71+
{
72+
DetectorType: detectorspb.DetectorType_LambdaTest,
73+
Verified: false,
74+
},
75+
},
76+
wantErr: false,
77+
wantVerificationErr: true,
78+
},
79+
{
80+
name: "not found",
81+
s: Scanner{},
82+
args: args{
83+
ctx: context.Background(),
84+
data: []byte("You cannot find the secret within"),
85+
verify: true,
86+
},
87+
want: nil,
88+
wantErr: false,
89+
wantVerificationErr: false,
90+
},
91+
{
92+
name: "found, would be verified if not for timeout",
93+
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
94+
args: args{
95+
ctx: context.Background(),
96+
data: []byte(fmt.Sprintf("You can find a lambdatest username %s and lambdatest accesskey %s within", username, accessKey)),
97+
verify: true,
98+
},
99+
want: []detectors.Result{
100+
{
101+
DetectorType: detectorspb.DetectorType_LambdaTest,
102+
Verified: false,
103+
},
104+
},
105+
wantErr: false,
106+
wantVerificationErr: true,
107+
},
108+
{
109+
name: "found, verified but unexpected api surface",
110+
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
111+
args: args{
112+
ctx: context.Background(),
113+
data: []byte(fmt.Sprintf("You can find a lambdatest username %s and lambdatest accesskey %s within", username, accessKey)),
114+
verify: true,
115+
},
116+
want: []detectors.Result{
117+
{
118+
DetectorType: detectorspb.DetectorType_LambdaTest,
119+
Verified: false,
120+
},
121+
},
122+
wantErr: false,
123+
wantVerificationErr: true,
124+
},
125+
}
126+
for _, tt := range tests {
127+
t.Run(tt.name, func(t *testing.T) {
128+
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
129+
if (err != nil) != tt.wantErr {
130+
t.Errorf("Lambdatest.FromData() error = %v, wantErr %v", err, tt.wantErr)
131+
return
132+
}
133+
for i := range got {
134+
if len(got[i].Raw) == 0 {
135+
t.Fatalf("no raw secret present: \n %+v", got[i])
136+
}
137+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
138+
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
139+
}
140+
}
141+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "primarySecret")
142+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
143+
t.Errorf("Lambdatest.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
144+
}
145+
})
146+
}
147+
}
148+
149+
func BenchmarkFromData(benchmark *testing.B) {
150+
ctx := context.Background()
151+
s := Scanner{}
152+
for name, data := range detectors.MustGetBenchmarkData() {
153+
benchmark.Run(name, func(b *testing.B) {
154+
b.ResetTimer()
155+
for n := 0; n < b.N; n++ {
156+
_, err := s.FromData(ctx, false, data)
157+
if err != nil {
158+
b.Fatal(err)
159+
}
160+
}
161+
})
162+
}
163+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package lambdatest
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
10+
)
11+
12+
func TestLambdatest_Pattern(t *testing.T) {
13+
d := Scanner{}
14+
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
15+
tests := []struct {
16+
name string
17+
input string
18+
want []string
19+
}{
20+
{
21+
name: "typical pattern",
22+
input: `lambdatest_username = sampleUserName123
23+
lambdatest_accessKey = LT_depBE6n7BUl4itF6aWhB0KVPUirnGtp3wu15I2yec60DByD`,
24+
want: []string{"sampleUserName123" + ":" + "LT_depBE6n7BUl4itF6aWhB0KVPUirnGtp3wu15I2yec60DByD"},
25+
},
26+
{
27+
name: "invalid pattern",
28+
input: `lambdatest_username = sampleUserName123
29+
lambdatest_token = 1a2b3c4d`,
30+
want: []string{},
31+
},
32+
}
33+
34+
for _, test := range tests {
35+
t.Run(test.name, func(t *testing.T) {
36+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
37+
if len(matchedDetectors) == 0 {
38+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
39+
return
40+
}
41+
42+
results, err := d.FromData(context.Background(), false, []byte(test.input))
43+
44+
if err != nil {
45+
t.Errorf("error = %v", err)
46+
return
47+
}
48+
49+
if len(results) != len(test.want) {
50+
if len(results) == 0 {
51+
t.Errorf("did not receive result")
52+
} else {
53+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
54+
}
55+
return
56+
}
57+
58+
actual := make(map[string]struct{}, len(results))
59+
for _, r := range results {
60+
if len(r.RawV2) > 0 {
61+
actual[string(r.RawV2)] = struct{}{}
62+
} else {
63+
actual[string(r.Raw)] = struct{}{}
64+
}
65+
}
66+
expected := make(map[string]struct{}, len(test.want))
67+
for _, v := range test.want {
68+
expected[v] = struct{}{}
69+
}
70+
71+
if diff := cmp.Diff(expected, actual); diff != "" {
72+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
73+
}
74+
})
75+
}
76+
}

pkg/engine/defaults/defaults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ import (
404404
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/kraken"
405405
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/kucoin"
406406
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/kylas"
407+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/lambdatest"
407408
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/langfuse"
408409
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/languagelayer"
409410
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/larksuite"
@@ -1260,6 +1261,7 @@ func buildDetectorList() []detectors.Detector {
12601261
&kraken.Scanner{},
12611262
&kucoin.Scanner{},
12621263
&kylas.Scanner{},
1264+
&lambdatest.Scanner{},
12631265
&langfuse.Scanner{},
12641266
&languagelayer.Scanner{},
12651267
&larksuite.Scanner{},

0 commit comments

Comments
 (0)