Skip to content

Commit d3459e6

Browse files
jonathongardnershahzadhaider1kashifkhan0771
authored
feat: add webexbot support (#4322)
* feat: add webexbot support * build proto using make * remove generic words * added the secret scanner to the engine defaults * remove keywords from regex and PR suggestions * fixed pattern tests * return err to set on results and remove duplicate keyword --------- Co-authored-by: Shahzad Haider <[email protected]> Co-authored-by: Shahzad Haider <[email protected]> Co-authored-by: Kashif Khan <[email protected]>
1 parent 4b4b1e8 commit d3459e6

File tree

6 files changed

+388
-6
lines changed

6 files changed

+388
-6
lines changed

pkg/detectors/webexbot/webexbot.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package webexbot
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
9+
regexp "github.com/wasilibs/go-re2"
10+
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
14+
)
15+
16+
type Scanner struct {
17+
client *http.Client
18+
}
19+
20+
// Ensure the Scanner satisfies the interface at compile time.
21+
var _ detectors.Detector = (*Scanner)(nil)
22+
23+
var (
24+
defaultClient = common.SaneHttpClient()
25+
keyPat = regexp.MustCompile(`([a-zA-Z0-9]{64}_[a-zA-Z0-9]{4}_[a-zA-Z0-9]{8}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{12})`)
26+
)
27+
28+
// Keywords are used for efficiently pre-filtering chunks.
29+
// Use identifiers in the secret preferably, or the provider name.
30+
func (s Scanner) Keywords() []string {
31+
return []string{"spark", "webex"}
32+
}
33+
34+
// FromData will find and optionally verify Webexbot secrets in a given set of bytes.
35+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
36+
dataStr := string(data)
37+
38+
uniqueMatches := make(map[string]struct{})
39+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
40+
if len(match) > 1 {
41+
uniqueMatches[match[1]] = struct{}{}
42+
}
43+
}
44+
45+
for match := range uniqueMatches {
46+
s1 := detectors.Result{
47+
DetectorType: detectorspb.DetectorType_WebexBot,
48+
Raw: []byte(match),
49+
Redacted: match[:5] + "...",
50+
ExtraData: map[string]string{},
51+
}
52+
53+
if verify {
54+
client := s.client
55+
if client == nil {
56+
client = defaultClient
57+
}
58+
59+
isVerified, extraData, verificationErr := verifyMatch(ctx, client, match)
60+
s1.Verified = isVerified
61+
s1.ExtraData = extraData
62+
s1.SetVerificationError(verificationErr, match)
63+
}
64+
65+
results = append(results, s1)
66+
}
67+
68+
return
69+
}
70+
71+
type response struct {
72+
Type string `json:"type"`
73+
ID string `json:"id"`
74+
DisplayName string `json:"displayName"`
75+
NickName string `json:"nickName"`
76+
UserName string `json:"username"`
77+
Emails []string `json:"emails"`
78+
}
79+
80+
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) {
81+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://webexapis.com/v1/people/me", nil)
82+
if err != nil {
83+
return false, nil, nil
84+
}
85+
req.Header.Add("Content-Type", "application/json")
86+
req.Header.Add("Accept", "application/json")
87+
req.Header.Add("Authorization", "Bearer "+token)
88+
89+
res, err := client.Do(req)
90+
if err != nil {
91+
return false, nil, err
92+
}
93+
defer func() {
94+
_ = res.Body.Close()
95+
}()
96+
97+
switch res.StatusCode {
98+
case http.StatusOK:
99+
// parse the response body to json
100+
var resp response
101+
extraData := make(map[string]string)
102+
if err := json.NewDecoder(res.Body).Decode(&resp); err != nil {
103+
return true, nil, fmt.Errorf("failed to decode response: %w", err)
104+
}
105+
106+
extraData["type"] = resp.Type
107+
extraData["username"] = resp.UserName
108+
return true, extraData, nil
109+
case http.StatusUnauthorized:
110+
// The secret is determinately not verified (nothing to do)
111+
return false, nil, nil
112+
default:
113+
return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
114+
}
115+
}
116+
117+
func (s Scanner) Type() detectorspb.DetectorType {
118+
return detectorspb.DetectorType_WebexBot
119+
}
120+
121+
func (s Scanner) Description() string {
122+
return "Webex bot allows applications to interact with users in Webex spaces."
123+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//go:build detectors
2+
// +build detectors
3+
4+
package webexbot
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 TestWebexbot_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", "detectors6")
24+
if err != nil {
25+
t.Fatalf("could not get test secrets from GCP: %s", err)
26+
}
27+
secret := testSecrets.MustGetField("WEBEXBOT")
28+
inactiveSecret := testSecrets.MustGetField("WEBEXBOT_INACTIVE")
29+
30+
redacted := secret[:5] + "..."
31+
inactiveRedacted := inactiveSecret[:5] + "..."
32+
33+
type args struct {
34+
ctx context.Context
35+
data []byte
36+
verify bool
37+
}
38+
tests := []struct {
39+
name string
40+
s Scanner
41+
args args
42+
want []detectors.Result
43+
wantErr bool
44+
wantVerificationErr bool
45+
}{
46+
{
47+
name: "found, verified",
48+
s: Scanner{},
49+
args: args{
50+
ctx: context.Background(),
51+
data: []byte(fmt.Sprintf("You can find a webexbot secret %s within", secret)),
52+
verify: true,
53+
},
54+
want: []detectors.Result{
55+
{
56+
DetectorType: detectorspb.DetectorType_WebexBot,
57+
Verified: true,
58+
Redacted: redacted,
59+
},
60+
},
61+
wantErr: false,
62+
wantVerificationErr: false,
63+
},
64+
{
65+
name: "found, unverified",
66+
s: Scanner{},
67+
args: args{
68+
ctx: context.Background(),
69+
data: []byte(fmt.Sprintf("You can find a webexbot secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
70+
verify: true,
71+
},
72+
want: []detectors.Result{
73+
{
74+
DetectorType: detectorspb.DetectorType_WebexBot,
75+
Verified: false,
76+
Redacted: inactiveRedacted,
77+
},
78+
},
79+
wantErr: false,
80+
wantVerificationErr: false,
81+
},
82+
{
83+
name: "not found",
84+
s: Scanner{},
85+
args: args{
86+
ctx: context.Background(),
87+
data: []byte("You cannot find the secret within"),
88+
verify: true,
89+
},
90+
want: nil,
91+
wantErr: false,
92+
wantVerificationErr: false,
93+
},
94+
{
95+
name: "found, would be verified if not for timeout",
96+
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
97+
args: args{
98+
ctx: context.Background(),
99+
data: []byte(fmt.Sprintf("You can find a webexbot secret %s within", secret)),
100+
verify: true,
101+
},
102+
want: []detectors.Result{
103+
{
104+
DetectorType: detectorspb.DetectorType_WebexBot,
105+
Verified: false,
106+
Redacted: redacted,
107+
},
108+
},
109+
wantErr: false,
110+
wantVerificationErr: true,
111+
},
112+
{
113+
name: "found, verified but unexpected api surface",
114+
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
115+
args: args{
116+
ctx: context.Background(),
117+
data: []byte(fmt.Sprintf("You can find a webexbot secret %s within", secret)),
118+
verify: true,
119+
},
120+
want: []detectors.Result{
121+
{
122+
DetectorType: detectorspb.DetectorType_WebexBot,
123+
Verified: false,
124+
Redacted: redacted,
125+
},
126+
},
127+
wantErr: false,
128+
wantVerificationErr: true,
129+
},
130+
}
131+
for _, tt := range tests {
132+
t.Run(tt.name, func(t *testing.T) {
133+
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
134+
if (err != nil) != tt.wantErr {
135+
t.Errorf("Webexbot.FromData() error = %v, wantErr %v", err, tt.wantErr)
136+
return
137+
}
138+
for i := range got {
139+
if len(got[i].Raw) == 0 {
140+
t.Fatalf("no raw secret present: \n %+v", got[i])
141+
}
142+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
143+
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
144+
}
145+
}
146+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "primarySecret", "ExtraData")
147+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
148+
t.Errorf("Webexbot.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
149+
}
150+
})
151+
}
152+
}
153+
154+
func BenchmarkFromData(benchmark *testing.B) {
155+
ctx := context.Background()
156+
s := Scanner{}
157+
for name, data := range detectors.MustGetBenchmarkData() {
158+
benchmark.Run(name, func(b *testing.B) {
159+
b.ResetTimer()
160+
for n := 0; n < b.N; n++ {
161+
_, err := s.FromData(ctx, false, data)
162+
if err != nil {
163+
b.Fatal(err)
164+
}
165+
}
166+
})
167+
}
168+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package webexbot
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 TestWebexbot_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: "webexbot_token = 'ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_asdf_asdkqw34-qwer-vbnm-asdf-qwertyuiopkl'",
23+
want: []string{"ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_asdf_asdkqw34-qwer-vbnm-asdf-qwertyuiopkl"},
24+
},
25+
{
26+
name: "finds multiple Webex Bot tokens",
27+
input: `
28+
webex_bot_token = 'ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_qwer_zxcv1234-asdf-ghjk-tyui-mnbvcxzqwert'
29+
webexbot = "ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_asdf_qwer4567-qwer-vbnm-asdf-xcvbnmasdfgh"
30+
`,
31+
want: []string{
32+
"ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_qwer_zxcv1234-asdf-ghjk-tyui-mnbvcxzqwert",
33+
"ajlksdjasda9090sadsa9dsad0saasdkl0asd90asdcasc90asdajij2n2njkjn3_asdf_qwer4567-qwer-vbnm-asdf-xcvbnmasdfgh",
34+
},
35+
},
36+
{
37+
name: "does not match invalid token name",
38+
input: "webex = 'asdf1234qwer5678zxcv9012lkjh_asdf_qwer3456-qwer-vbnm-asdf-zxcvbnmasdfgh'",
39+
want: []string{},
40+
},
41+
}
42+
43+
for _, test := range tests {
44+
t.Run(test.name, func(t *testing.T) {
45+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
46+
if len(matchedDetectors) == 0 {
47+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
48+
return
49+
}
50+
51+
results, err := d.FromData(context.Background(), false, []byte(test.input))
52+
if err != nil {
53+
t.Errorf("error = %v", err)
54+
return
55+
}
56+
57+
if len(results) != len(test.want) {
58+
if len(results) == 0 {
59+
t.Errorf("did not receive result")
60+
} else {
61+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
62+
}
63+
return
64+
}
65+
66+
actual := make(map[string]struct{}, len(results))
67+
for _, r := range results {
68+
if len(r.RawV2) > 0 {
69+
actual[string(r.RawV2)] = struct{}{}
70+
} else {
71+
actual[string(r.Raw)] = struct{}{}
72+
}
73+
}
74+
expected := make(map[string]struct{}, len(test.want))
75+
for _, v := range test.want {
76+
expected[v] = struct{}{}
77+
}
78+
79+
if diff := cmp.Diff(expected, actual); diff != "" {
80+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
81+
}
82+
})
83+
}
84+
}

pkg/engine/defaults/defaults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,7 @@ import (
811811
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/weatherstack"
812812
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/web3storage"
813813
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/webex"
814+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/webexbot"
814815
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/webflow"
815816
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/webscraper"
816817
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/webscraping"
@@ -1685,6 +1686,7 @@ func buildDetectorList() []detectors.Detector {
16851686
&weatherstack.Scanner{},
16861687
&web3storage.Scanner{},
16871688
&webex.Scanner{},
1689+
&webexbot.Scanner{},
16881690
&webflow.Scanner{},
16891691
&webscraper.Scanner{},
16901692
&webscraping.Scanner{},

0 commit comments

Comments
 (0)