diff --git a/pkg/detectors/hubspotapikey/hubspotapikey.go b/pkg/detectors/hubspotapikey/hubspotapikey.go index eecf6e023632..24f81b55bd8f 100644 --- a/pkg/detectors/hubspotapikey/hubspotapikey.go +++ b/pkg/detectors/hubspotapikey/hubspotapikey.go @@ -2,17 +2,20 @@ package hubspotapikey import ( "context" + "fmt" + "io" + "net/http" // "log" regexp "github.com/wasilibs/go-re2" - "net/http" - "strings" "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{} +type Scanner struct { + client *http.Client +} func (s Scanner) Version() int { return 1 } @@ -21,45 +24,41 @@ var _ detectors.Detector = (*Scanner)(nil) var _ detectors.Versioner = (*Scanner)(nil) var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"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`) + 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`) ) // 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{"hubspot"} + return []string{"hubspot", "hubapi", "hapikey", "hapi_key"} } // 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) - matches := keyPat.FindAllStringSubmatch(dataStr, -1) - for _, match := range matches { - if len(match) != 2 { - continue - } - resMatch := strings.TrimSpace(match[1]) + 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(resMatch), + Raw: []byte(token), } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.hubapi.com/contacts/v1/lists?hapikey="+resMatch, nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } + client := s.client + if client == nil { + client = defaultClient } + + verified, verificationErr := verifyToken(ctx, client, token) + s1.Verified = verified + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -68,6 +67,35 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +// See https://legacydocs.hubspot.com/docs/methods/auth/oauth-overview +func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.hubapi.com/contacts/v1/lists?hapikey="+token, nil) + if err != nil { + return false, err + } + + 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 }