Skip to content

Commit aeffc12

Browse files
rgmzRichard Gomez
authored and
Richard Gomez
committed
feat(hubspot): unify v1 and v2 implementation
1 parent 4a86abf commit aeffc12

8 files changed

+293
-88
lines changed

pkg/detectors/hubspotapikey/hubspotapikey.go renamed to pkg/detectors/hubspot_apikey/v1/hubspot_apikey_v1.go

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package hubspotapikey
1+
package v1
22

33
import (
44
"context"
@@ -17,11 +17,13 @@ type Scanner struct {
1717
client *http.Client
1818
}
1919

20-
func (s Scanner) Version() int { return 1 }
21-
2220
// Ensure the Scanner satisfies the interface at compile time.
23-
var _ detectors.Detector = (*Scanner)(nil)
24-
var _ detectors.Versioner = (*Scanner)(nil)
21+
var _ interface {
22+
detectors.Detector
23+
detectors.Versioner
24+
} = (*Scanner)(nil)
25+
26+
func (s Scanner) Version() int { return 1 }
2527

2628
var (
2729
defaultClient = common.SaneHttpClient()
@@ -32,7 +34,7 @@ var (
3234
// Keywords are used for efficiently pre-filtering chunks.
3335
// Use identifiers in the secret preferably, or the provider name.
3436
func (s Scanner) Keywords() []string {
35-
return []string{"hubspot", "hubapi", "hapikey", "hapi_key"}
37+
return []string{"hubapi", "hapikey", "hapi_key", "hubspot"}
3638
}
3739

3840
// FromData will find and optionally verify HubSpotApiKey secrets in a given set of bytes.

pkg/detectors/hubspotapikey/hubspotapikey_test.go renamed to pkg/detectors/hubspot_apikey/v1/hubspot_apikey_v1_integration_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//go:build detectors
22
// +build detectors
33

4-
package hubspotapikey
4+
package v1
55

66
import (
77
"context"
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/kylelemons/godebug/pretty"
13+
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1415

1516
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
50+
for _, test := range tests {
51+
t.Run(test.name, func(t *testing.T) {
52+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
53+
if len(matchedDetectors) == 0 {
54+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
55+
return
56+
}
57+
58+
results, err := d.FromData(context.Background(), false, []byte(test.input))
59+
if err != nil {
60+
t.Errorf("error = %v", err)
61+
return
62+
}
63+
64+
if len(results) != len(test.want) {
65+
if len(results) == 0 {
66+
t.Errorf("did not receive result")
67+
} else {
68+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
69+
}
70+
return
71+
}
72+
73+
actual := make(map[string]struct{}, len(results))
74+
for _, r := range results {
75+
if len(r.RawV2) > 0 {
76+
actual[string(r.RawV2)] = struct{}{}
77+
} else {
78+
actual[string(r.Raw)] = struct{}{}
79+
}
80+
}
81+
expected := make(map[string]struct{}, len(test.want))
82+
for _, v := range test.want {
83+
expected[v] = struct{}{}
84+
}
85+
86+
if diff := cmp.Diff(expected, actual); diff != "" {
87+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
88+
}
89+
})
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package v2
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
// "log"
8+
"fmt"
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+
func (s Scanner) Version() int { return 2 }
23+
24+
// Ensure the Scanner satisfies the interface at compile time.
25+
var _ interface {
26+
detectors.Detector
27+
detectors.Versioner
28+
} = (*Scanner)(nil)
29+
30+
var (
31+
defaultClient = common.SaneHttpClient()
32+
33+
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`)
34+
)
35+
36+
// Keywords are used for efficiently pre-filtering chunks.
37+
// Use identifiers in the secret preferably, or the provider name.
38+
func (s Scanner) Keywords() []string {
39+
return []string{"pat-na1-", "pat-eu1-"}
40+
}
41+
42+
// FromData will find and optionally verify HubSpotApiKey secrets in a given set of bytes.
43+
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
44+
dataStr := string(data)
45+
46+
uniqueMatches := make(map[string]struct{})
47+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
48+
uniqueMatches[match[1]] = struct{}{}
49+
}
50+
51+
for token := range uniqueMatches {
52+
s1 := detectors.Result{
53+
DetectorType: detectorspb.DetectorType_HubSpotApiKey,
54+
Raw: []byte(token),
55+
Redacted: token[8:] + "...",
56+
}
57+
58+
if verify {
59+
client := s.client
60+
if client == nil {
61+
client = defaultClient
62+
}
63+
64+
verified, verificationErr := verifyToken(ctx, client, token)
65+
s1.Verified = verified
66+
s1.SetVerificationError(verificationErr)
67+
}
68+
69+
results = append(results, s1)
70+
}
71+
72+
return results, nil
73+
}
74+
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
75+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps", nil)
76+
if err != nil {
77+
return false, err
78+
}
79+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
80+
res, err := client.Do(req)
81+
if err != nil {
82+
return false, err
83+
}
84+
defer func() {
85+
_, _ = io.Copy(io.Discard, res.Body)
86+
_ = res.Body.Close()
87+
}()
88+
89+
switch res.StatusCode {
90+
case http.StatusOK:
91+
return true, nil
92+
case http.StatusUnauthorized:
93+
return false, nil
94+
case http.StatusForbidden:
95+
// The token is valid but lacks permission for the endpoint.
96+
return true, nil
97+
default:
98+
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
99+
}
100+
}
101+
102+
func (s Scanner) Type() detectorspb.DetectorType {
103+
return detectorspb.DetectorType_HubSpotApiKey
104+
}

pkg/detectors/hubspotapikeyv2/hubspotapikeyv2_test.go renamed to pkg/detectors/hubspot_apikey/v2/hubspot_apikey_v2_integration_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//go:build detectors
22
// +build detectors
33

4-
package hubspotapikeyv2
4+
package v2
55

66
import (
77
"context"
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/kylelemons/godebug/pretty"
13+
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1415

1516
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package v2
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 TestHubspotV2_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: "eu key",
23+
input: `
24+
const private_app_token = 'pat-eu1-1457aed5-04c6-40e2-83ad-a862d3cf19f2';
25+
26+
app.get('/homepage', async (req, res) => {
27+
const contactsEndpoint = 'https://api.hubspot.com/crm/v3/objects/contacts';`,
28+
want: []string{"pat-eu1-1457aed5-04c6-40e2-83ad-a862d3cf19f2"},
29+
},
30+
{
31+
name: "na key",
32+
input: `hubspot:
33+
api:
34+
url: https://api.hubapi.com
35+
auth-token: pat-na1-ffbb9f50-d96b-4abc-84f1-b986617be1b5
36+
subscriptions:`,
37+
want: []string{"pat-na1-ffbb9f50-d96b-4abc-84f1-b986617be1b5"},
38+
},
39+
}
40+
41+
for _, test := range tests {
42+
t.Run(test.name, func(t *testing.T) {
43+
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
44+
if len(matchedDetectors) == 0 {
45+
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input)
46+
return
47+
}
48+
49+
results, err := d.FromData(context.Background(), false, []byte(test.input))
50+
if err != nil {
51+
t.Errorf("error = %v", err)
52+
return
53+
}
54+
55+
if len(results) != len(test.want) {
56+
if len(results) == 0 {
57+
t.Errorf("did not receive result")
58+
} else {
59+
t.Errorf("expected %d results, only received %d", len(test.want), len(results))
60+
}
61+
return
62+
}
63+
64+
actual := make(map[string]struct{}, len(results))
65+
for _, r := range results {
66+
if len(r.RawV2) > 0 {
67+
actual[string(r.RawV2)] = struct{}{}
68+
} else {
69+
actual[string(r.Raw)] = struct{}{}
70+
}
71+
}
72+
expected := make(map[string]struct{}, len(test.want))
73+
for _, v := range test.want {
74+
expected[v] = struct{}{}
75+
}
76+
77+
if diff := cmp.Diff(expected, actual); diff != "" {
78+
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
79+
}
80+
})
81+
}
82+
}

0 commit comments

Comments
 (0)