-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hubspot): unify v1 and v2 implementation
- Loading branch information
Showing
8 changed files
with
296 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
package v1 | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" | ||
) | ||
|
||
func TestHubspotV1_Pattern(t *testing.T) { | ||
d := Scanner{} | ||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) | ||
tests := []struct { | ||
name string | ||
input string | ||
want []string | ||
}{ | ||
{ | ||
name: "hapikey", | ||
input: `// const hapikey = 'b714cac4-a45c-42af-9905-da4de8838d75'; | ||
const { HAPI_KEY } = process.env; | ||
const hs = new HubSpotAPI({ hapikey: HAPI_KEY });`, | ||
want: []string{"b714cac4-a45c-42af-9905-da4de8838d75"}, | ||
}, | ||
// TODO: Doesn't work because it's more than 40 characters. | ||
// { | ||
// name: "hubapi", | ||
// input: `curl https://api.hubapi.com/contacts/v1/lists/all/contacts/all \ | ||
//--header "Authorization: Bearer b71aa2ed-9c76-417d-bd8e-c5f4980d21ef"`, | ||
// want: []string{"b71aa2ed-9c76-417d-bd8e-c5f4980d21ef"}, | ||
// }, | ||
{ | ||
name: "hubspot_1", | ||
input: `const hs = new HubSpotAPI("76a836c8-469d-4426-8a3b-194ca930b7a1"); | ||
const blogPosts = hs.blog.getPosts({ name: 'Inbound' });`, | ||
want: []string{"76a836c8-469d-4426-8a3b-194ca930b7a1"}, | ||
}, | ||
{ | ||
name: "hubspot_2", | ||
input: ` 'hubspot' => [ | ||
// 'api_key' => 'e9ff285d-6b7f-455a-a56d-9ec8c4abbd47', // @ts dev`, | ||
want: []string{"e9ff285d-6b7f-455a-a56d-9ec8c4abbd47"}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) | ||
if len(matchedDetectors) == 0 { | ||
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) | ||
return | ||
} | ||
|
||
results, err := d.FromData(context.Background(), false, []byte(test.input)) | ||
if err != nil { | ||
t.Errorf("error = %v", err) | ||
return | ||
} | ||
|
||
if len(results) != len(test.want) { | ||
if len(results) == 0 { | ||
t.Errorf("did not receive result") | ||
} else { | ||
t.Errorf("expected %d results, only received %d", len(test.want), len(results)) | ||
} | ||
return | ||
} | ||
|
||
actual := make(map[string]struct{}, len(results)) | ||
for _, r := range results { | ||
if len(r.RawV2) > 0 { | ||
actual[string(r.RawV2)] = struct{}{} | ||
} else { | ||
actual[string(r.Raw)] = struct{}{} | ||
} | ||
} | ||
expected := make(map[string]struct{}, len(test.want)) | ||
for _, v := range test.want { | ||
expected[v] = struct{}{} | ||
} | ||
|
||
if diff := cmp.Diff(expected, actual); diff != "" { | ||
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package v2 | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
|
||
regexp "github.com/wasilibs/go-re2" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/common" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" | ||
) | ||
|
||
type Scanner struct { | ||
client *http.Client | ||
} | ||
|
||
func (s Scanner) Version() int { return 2 } | ||
|
||
// Ensure the Scanner satisfies the interface at compile time. | ||
var _ interface { | ||
detectors.Detector | ||
detectors.Versioner | ||
} = (*Scanner)(nil) | ||
|
||
var ( | ||
defaultClient = common.SaneHttpClient() | ||
|
||
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`) | ||
) | ||
|
||
// Keywords are used for efficiently pre-filtering chunks. | ||
// Use identifiers in the secret preferably, or the provider name. | ||
func (s Scanner) Keywords() []string { | ||
return []string{"pat-na1-", "pat-eu1-"} | ||
} | ||
|
||
// FromData will find and optionally verify HubSpotApiKey secrets in a given set of bytes. | ||
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { | ||
dataStr := string(data) | ||
|
||
uniqueMatches := make(map[string]struct{}) | ||
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { | ||
uniqueMatches[match[1]] = struct{}{} | ||
} | ||
|
||
for token := range uniqueMatches { | ||
s1 := detectors.Result{ | ||
DetectorType: detectorspb.DetectorType_HubSpotApiKey, | ||
Raw: []byte(token), | ||
Redacted: token[8:] + "...", | ||
} | ||
|
||
if verify { | ||
client := s.client | ||
if client == nil { | ||
client = defaultClient | ||
} | ||
|
||
verified, verificationErr := verifyToken(ctx, client, token) | ||
s1.Verified = verified | ||
s1.SetVerificationError(verificationErr) | ||
} | ||
|
||
results = append(results, s1) | ||
} | ||
|
||
return results, nil | ||
} | ||
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) { | ||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.hubapi.com/account-info/v3/api-usage/daily/private-apps", nil) | ||
if err != nil { | ||
return false, err | ||
} | ||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) | ||
res, err := client.Do(req) | ||
if err != nil { | ||
return false, err | ||
} | ||
defer func() { | ||
_, _ = io.Copy(io.Discard, res.Body) | ||
_ = res.Body.Close() | ||
}() | ||
|
||
switch res.StatusCode { | ||
case http.StatusOK: | ||
return true, nil | ||
case http.StatusUnauthorized: | ||
return false, nil | ||
case http.StatusForbidden: | ||
// The token is valid but lacks permission for the endpoint. | ||
return true, nil | ||
default: | ||
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) | ||
} | ||
} | ||
|
||
func (s Scanner) Type() detectorspb.DetectorType { | ||
return detectorspb.DetectorType_HubSpotApiKey | ||
} | ||
|
||
func (s Scanner) Description() string { | ||
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." | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package v2 | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
|
||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors" | ||
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick" | ||
) | ||
|
||
func TestHubspotV2_Pattern(t *testing.T) { | ||
d := Scanner{} | ||
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d}) | ||
tests := []struct { | ||
name string | ||
input string | ||
want []string | ||
}{ | ||
{ | ||
name: "eu key", | ||
input: ` | ||
const private_app_token = 'pat-eu1-1457aed5-04c6-40e2-83ad-a862d3cf19f2'; | ||
app.get('/homepage', async (req, res) => { | ||
const contactsEndpoint = 'https://api.hubspot.com/crm/v3/objects/contacts';`, | ||
want: []string{"pat-eu1-1457aed5-04c6-40e2-83ad-a862d3cf19f2"}, | ||
}, | ||
{ | ||
name: "na key", | ||
input: `hubspot: | ||
api: | ||
url: https://api.hubapi.com | ||
auth-token: pat-na1-ffbb9f50-d96b-4abc-84f1-b986617be1b5 | ||
subscriptions:`, | ||
want: []string{"pat-na1-ffbb9f50-d96b-4abc-84f1-b986617be1b5"}, | ||
}, | ||
} | ||
|
||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input)) | ||
if len(matchedDetectors) == 0 { | ||
t.Errorf("keywords '%v' not matched by: %s", d.Keywords(), test.input) | ||
return | ||
} | ||
|
||
results, err := d.FromData(context.Background(), false, []byte(test.input)) | ||
if err != nil { | ||
t.Errorf("error = %v", err) | ||
return | ||
} | ||
|
||
if len(results) != len(test.want) { | ||
if len(results) == 0 { | ||
t.Errorf("did not receive result") | ||
} else { | ||
t.Errorf("expected %d results, only received %d", len(test.want), len(results)) | ||
} | ||
return | ||
} | ||
|
||
actual := make(map[string]struct{}, len(results)) | ||
for _, r := range results { | ||
if len(r.RawV2) > 0 { | ||
actual[string(r.RawV2)] = struct{}{} | ||
} else { | ||
actual[string(r.Raw)] = struct{}{} | ||
} | ||
} | ||
expected := make(map[string]struct{}, len(test.want)) | ||
for _, v := range test.want { | ||
expected[v] = struct{}{} | ||
} | ||
|
||
if diff := cmp.Diff(expected, actual); diff != "" { | ||
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.