From dd7b82b0fbb3e040b987f85b666bb178ecf2c0b1 Mon Sep 17 00:00:00 2001 From: Richard Gomez Date: Sat, 11 May 2024 15:50:48 -0400 Subject: [PATCH] feat(sendgrid): update detector --- pkg/detectors/sendgrid/sendgrid.go | 103 ++++++++++++++++++----------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/pkg/detectors/sendgrid/sendgrid.go b/pkg/detectors/sendgrid/sendgrid.go index e00cb2a2e748..936004a047e0 100644 --- a/pkg/detectors/sendgrid/sendgrid.go +++ b/pkg/detectors/sendgrid/sendgrid.go @@ -2,11 +2,14 @@ package sendgrid import ( "context" + "encoding/json" "fmt" - regexp "github.com/wasilibs/go-re2" + "io" "net/http" "strings" + regexp "github.com/wasilibs/go-re2" + "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" @@ -23,7 +26,7 @@ var _ detectors.Detector = (*Scanner)(nil) var ( defaultClient = common.SaneHttpClient() - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sendgrid"}) + `(SG\.[\w\-_]{20,24}\.[\w\-_]{39,50})\b`) + keyPat = regexp.MustCompile(`\bSG\.[\w\-]{20,24}\.[\w\-]{39,50}\b`) ) // Keywords are used for efficiently pre-filtering chunks. @@ -36,53 +39,27 @@ func (s Scanner) Keywords() []string { 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[0]] = struct{}{} + } + for token := range uniqueMatches { s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_SendGrid, - Raw: []byte(resMatch), - } - s1.ExtraData = map[string]string{ - "rotation_guide": "https://howtorotate.com/docs/tutorials/sendgrid/", + Raw: []byte(token), } if verify { - // there are a few endpoints we can check, but templates seems the least sensitive. - // 403 will be issued if the scope is wrong but the key is correct - baseURL := "https://api.sendgrid.com/v3/templates" - client := s.client if client == nil { client = defaultClient } - // test `read_user` scope - req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch)) - req.Header.Add("Content-Type", "application/json") - res, err := client.Do(req) - if err == nil { - res.Body.Close() // The request body is unused. - - // 200 means good key and has `templates` scope - // 403 means good key but not the right scope - // 401 is bad key - if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusForbidden { - s1.Verified = true - } else if res.StatusCode != http.StatusUnauthorized { - err = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) - s1.SetVerificationError(err, resMatch) - } - } + verified, extraData, verificationErr := verifyToken(ctx, client, token) + s1.Verified = verified + s1.ExtraData = extraData + s1.SetVerificationError(verificationErr) } results = append(results, s1) @@ -91,6 +68,56 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return } +func verifyToken(ctx context.Context, client *http.Client, token string) (bool, map[string]string, error) { + // Check the scopes assigned to the api key. + // https://docs.sendgrid.com/api-reference/api-key-permissions/retrieve-a-list-of-scopes-for-which-this-user-has-access + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.sendgrid.com/v3/scopes", nil) + if err != nil { + return false, nil, err + } + + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Content-Type", "application/json") + res, err := client.Do(req) + if err != nil { + return false, nil, err + } + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + switch res.StatusCode { + case http.StatusOK: + extraData := map[string]string{ + "rotation_guide": "https://howtorotate.com/docs/tutorials/sendgrid/", + } + + var scopesRes scopesResponse + if err := json.NewDecoder(res.Body).Decode(&scopesRes); err != nil { + return false, nil, err + } + + if len(scopesRes.Scopes) > 0 { + extraData["scopes"] = strings.Join(scopesRes.Scopes, ",") + } + + return true, extraData, nil + case http.StatusUnauthorized: + // 401 means the key is definitively invalid. + return false, nil, nil + case http.StatusForbidden: + // 403 means good key but not the right scope + return true, nil, nil + default: + return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + +type scopesResponse struct { + Scopes []string `json:"scopes"` +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_SendGrid }