Skip to content

Commit 9d99de8

Browse files
rgmzankushgoel27
andauthored
feat(hubspot): update v1 detector (#2845)
feat(hubspot): unify v1 and v2 implementation Co-authored-by: ankushgoel27 <[email protected]>
1 parent ce9bac9 commit 9d99de8

File tree

8 files changed

+465
-100
lines changed

8 files changed

+465
-100
lines changed
+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
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 _ interface {
22+
detectors.Detector
23+
detectors.Versioner
24+
} = (*Scanner)(nil)
25+
26+
func (s Scanner) Version() int { return 1 }
27+
28+
var (
29+
defaultClient = common.SaneHttpClient()
30+
31+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"hubapi", "hapi_?key", "hubspot"}) + `\b([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})\b`)
32+
)
33+
34+
// Keywords are used for efficiently pre-filtering chunks.
35+
// Use identifiers in the secret preferably, or the provider name.
36+
func (s Scanner) Keywords() []string {
37+
return []string{"hubapi", "hapikey", "hapi_key", "hubspot"}
38+
}
39+
40+
// FromData will find and optionally verify HubSpotApiKey secrets in a given set of bytes.
41+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
42+
dataStr := string(data)
43+
44+
uniqueMatches := make(map[string]struct{})
45+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
46+
uniqueMatches[match[1]] = struct{}{}
47+
}
48+
49+
for token := range uniqueMatches {
50+
s1 := detectors.Result{
51+
DetectorType: detectorspb.DetectorType_HubSpotApiKey,
52+
Raw: []byte(token),
53+
}
54+
55+
if verify {
56+
client := s.client
57+
if client == nil {
58+
client = defaultClient
59+
}
60+
61+
verified, verificationErr := verifyToken(ctx, client, token)
62+
s1.Verified = verified
63+
s1.SetVerificationError(verificationErr)
64+
}
65+
66+
results = append(results, s1)
67+
}
68+
69+
return results, nil
70+
}
71+
72+
// See https://legacydocs.hubspot.com/docs/methods/auth/oauth-overview
73+
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
74+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.hubapi.com/contacts/v1/lists?hapikey="+token, nil)
75+
if err != nil {
76+
return false, err
77+
}
78+
79+
res, err := client.Do(req)
80+
if err != nil {
81+
return false, err
82+
}
83+
defer func() {
84+
_, _ = io.Copy(io.Discard, res.Body)
85+
_ = res.Body.Close()
86+
}()
87+
88+
switch res.StatusCode {
89+
case http.StatusOK:
90+
return true, nil
91+
case http.StatusUnauthorized:
92+
return false, nil
93+
case http.StatusForbidden:
94+
// The token is valid but lacks permission for the endpoint.
95+
return true, nil
96+
default:
97+
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
98+
}
99+
}
100+
101+
func (s Scanner) Type() detectorspb.DetectorType {
102+
return detectorspb.DetectorType_HubSpotApiKey
103+
}
104+
105+
func (s Scanner) Description() string {
106+
return "HubSpot is a CRM platform that provides tools for marketing, sales, and customer service. HubSpot API keys can be used to access and modify data within the HubSpot platform."
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package v1
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
9+
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
11+
)
12+
13+
func TestHubspotV1_Pattern(t *testing.T) {
14+
d := Scanner{}
15+
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})
16+
tests := []struct {
17+
name string
18+
input string
19+
want []string
20+
}{
21+
{
22+
name: "hapikey",
23+
input: `// const hapikey = 'b714cac4-a45c-42af-9905-da4de8838d75';
24+
const { HAPI_KEY } = process.env;
25+
const hs = new HubSpotAPI({ hapikey: HAPI_KEY });`,
26+
want: []string{"b714cac4-a45c-42af-9905-da4de8838d75"},
27+
},
28+
// TODO: Doesn't work because it's more than 40 characters.
29+
// {
30+
// name: "hubapi",
31+
// input: `curl https://api.hubapi.com/contacts/v1/lists/all/contacts/all \
32+
//--header "Authorization: Bearer b71aa2ed-9c76-417d-bd8e-c5f4980d21ef"`,
33+
// want: []string{"b71aa2ed-9c76-417d-bd8e-c5f4980d21ef"},
34+
// },
35+
{
36+
name: "hubspot_1",
37+
input: `const hs = new HubSpotAPI("76a836c8-469d-4426-8a3b-194ca930b7a1");
38+
39+
const blogPosts = hs.blog.getPosts({ name: 'Inbound' });`,
40+
want: []string{"76a836c8-469d-4426-8a3b-194ca930b7a1"},
41+
},
42+
{
43+
name: "hubspot_2",
44+
input: ` 'hubspot' => [
45+
// 'api_key' => 'e9ff285d-6b7f-455a-a56d-9ec8c4abbd47', // @ts dev`,
46+
want: []string{"e9ff285d-6b7f-455a-a56d-9ec8c4abbd47"},
47+
},
48+
{
49+
name: "hubspot_3",
50+
input: `[{
51+
"_id": "1a8d0cca-e1a9-4318-bc2f-f5658ab2dcb5",
52+
"name": "HubSpotAPIKey",
53+
"type": "Detector",
54+
"api": true,
55+
"authentication_type": "",
56+
"verification_url": "https://api.example.com/example",
57+
"test_secrets": {
58+
"hubspot_secret": "hDNxPGyQ-AOMZ-w9Sp-aw5t-TwKLBQjQ85go"
59+
},
60+
"expected_response": "200",
61+
"method": "GET",
62+
"deprecated": false
63+
}]`,
64+
want: []string{""},
65+
},
66+
}
67+
68+
for _, test := range tests {
69+
t.Run(test.name, func(t *testing.T) {
70+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
71+
if len(matchedDetectors) == 0 {
72+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
73+
return
74+
}
75+
76+
results, err := d.FromData(context.Background(), false, []byte(test.input))
77+
if err != nil {
78+
t.Errorf("error = %v", err)
79+
return
80+
}
81+
82+
if len(results) != len(test.want) {
83+
if len(results) == 0 {
84+
t.Errorf("did not receive result")
85+
} else {
86+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
87+
}
88+
return
89+
}
90+
91+
actual := make(map[string]struct{}, len(results))
92+
for _, r := range results {
93+
if len(r.RawV2) > 0 {
94+
actual[string(r.RawV2)] = struct{}{}
95+
} else {
96+
actual[string(r.Raw)] = struct{}{}
97+
}
98+
}
99+
expected := make(map[string]struct{}, len(test.want))
100+
for _, v := range test.want {
101+
expected[v] = struct{}{}
102+
}
103+
104+
if diff := cmp.Diff(expected, actual); diff != "" {
105+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
106+
}
107+
})
108+
}
109+
}
+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
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+
func (s Scanner) Version() int { return 2 }
21+
22+
// Ensure the Scanner satisfies the interface at compile time.
23+
var _ interface {
24+
detectors.Detector
25+
detectors.Versioner
26+
} = (*Scanner)(nil)
27+
28+
var (
29+
defaultClient = common.SaneHttpClient()
30+
31+
keyPat = regexp.MustCompile(`\b(pat-(?:eu|na)1-[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})\b`)
32+
)
33+
34+
// Keywords are used for efficiently pre-filtering chunks.
35+
// Use identifiers in the secret preferably, or the provider name.
36+
func (s Scanner) Keywords() []string {
37+
return []string{"pat-na1-", "pat-eu1-"}
38+
}
39+
40+
// FromData will find and optionally verify HubSpotApiKey secrets in a given set of bytes.
41+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
42+
dataStr := string(data)
43+
44+
uniqueMatches := make(map[string]struct{})
45+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
46+
uniqueMatches[match[1]] = struct{}{}
47+
}
48+
49+
for token := range uniqueMatches {
50+
s1 := detectors.Result{
51+
DetectorType: detectorspb.DetectorType_HubSpotApiKey,
52+
Raw: []byte(token),
53+
Redacted: token[8:] + "...",
54+
}
55+
56+
if verify {
57+
client := s.client
58+
if client == nil {
59+
client = defaultClient
60+
}
61+
62+
verified, verificationErr := verifyToken(ctx, client, token)
63+
s1.Verified = verified
64+
s1.SetVerificationError(verificationErr)
65+
}
66+
67+
results = append(results, s1)
68+
}
69+
70+
return results, nil
71+
}
72+
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
73+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps", nil)
74+
if err != nil {
75+
return false, err
76+
}
77+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
78+
res, err := client.Do(req)
79+
if err != nil {
80+
return false, err
81+
}
82+
defer func() {
83+
_, _ = io.Copy(io.Discard, res.Body)
84+
_ = res.Body.Close()
85+
}()
86+
87+
switch res.StatusCode {
88+
case http.StatusOK:
89+
return true, nil
90+
case http.StatusUnauthorized:
91+
return false, nil
92+
case http.StatusForbidden:
93+
// The token is valid but lacks permission for the endpoint.
94+
return true, nil
95+
default:
96+
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
97+
}
98+
}
99+
100+
func (s Scanner) Type() detectorspb.DetectorType {
101+
return detectorspb.DetectorType_HubSpotApiKey
102+
}
103+
104+
func (s Scanner) Description() string {
105+
return "HubSpot is a CRM platform that provides tools for marketing, sales, and customer service. HubSpot API keys can be used to access and modify data within the HubSpot platform."
106+
}

0 commit comments

Comments
 (0)