diff --git a/pkg/detectors/sumologickey/sumologickey.go b/pkg/detectors/sumologickey/sumologickey.go index 3dc310edd117..f719656c6cf6 100644 --- a/pkg/detectors/sumologickey/sumologickey.go +++ b/pkg/detectors/sumologickey/sumologickey.go @@ -2,88 +2,163 @@ package sumologickey import ( "context" - b64 "encoding/base64" "fmt" - 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" + regexp "github.com/wasilibs/go-re2" + "golang.org/x/exp/maps" + "io" + "net/http" + "strings" ) -type Scanner struct{ +type Scanner struct { + client *http.Client + detectors.EndpointSetter detectors.DefaultMultiPartCredentialProvider } // Ensure the Scanner satisfies the interface at compile time. -var _ detectors.Detector = (*Scanner)(nil) +var ( + _ detectors.Detector = (*Scanner)(nil) + _ detectors.EndpointCustomizer = (*Scanner)(nil) +) var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() + + // Detect which instance the key is associated with. + // https://help.sumologic.com/docs/api/getting-started/#documentation + urlPat = regexp.MustCompile(`(?i)api\.(?:au|ca|de|eu|fed|jp|kr|in|us2)\.sumologic\.com`) // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. - idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sumo"}) + `\b([A-Za-z0-9]{14})\b`) - keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sumo"}) + `\b([A-Za-z0-9]{64})\b`) + idPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sumo", "accessId"}) + `\b(su[A-Za-z0-9]{12})\b`) + keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sumo", "accessKey"}) + `\b([A-Za-z0-9]{64})\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{"sumologic"} + return []string{"sumo", "accessId", "accessKey"} } +// Default US API endpoint. +func (Scanner) CloudEndpoint() string { return "api.sumologic.com" } + // FromData will find and optionally verify SumoLogicKey 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) - idMatches := idPat.FindAllStringSubmatch(dataStr, -1) - matches := keyPat.FindAllStringSubmatch(dataStr, -1) - for _, idMatch := range idMatches { - if len(idMatch) != 2 { - continue - } - resIdMatch := strings.TrimSpace(idMatch[1]) - for _, match := range matches { - if len(match) != 2 { - continue - } - resMatch := strings.TrimSpace(match[1]) - - s1 := detectors.Result{ - DetectorType: detectorspb.DetectorType_SumoLogicKey, - Raw: []byte(resMatch), - ExtraData: map[string]string{ - "rotation_guide": "https://howtorotate.com/docs/tutorials/sumologic/", - }, - } + idMatches := make(map[string]struct{}) + for _, match := range idPat.FindAllStringSubmatch(dataStr, -1) { + idMatches[match[1]] = struct{}{} + } + keyMatches := make(map[string]struct{}) + for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { + keyMatches[match[1]] = struct{}{} + } + endpointMatches := make(map[string]struct{}) + for _, match := range urlPat.FindAllStringSubmatch(dataStr, -1) { + endpointMatches[match[0]] = struct{}{} + } + endpoints := s.Endpoints(maps.Keys(endpointMatches)...) + + for accessKey := range keyMatches { + var ( + r *detectors.Result + accessId string + apiEndpoint string + ) + + IdLoop: + for id := range idMatches { + accessId = id + + for _, e := range endpoints { + apiEndpoint = e + + if verify { + client := s.client + if client == nil { + client = defaultClient + } - if verify { - data := fmt.Sprintf("%s:%s", resIdMatch, resMatch) - encoded := b64.StdEncoding.EncodeToString([]byte(data)) - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.us2.sumologic.com/api/v1/users", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encoded)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true + isVerified, verificationErr := verifyMatch(ctx, client, apiEndpoint, accessId, accessKey) + if isVerified || (len(idMatches) == 1 && len(endpoints) == 1) { + r = createResult(accessId, accessKey, apiEndpoint, isVerified, verificationErr) + break IdLoop } } } - - results = append(results, s1) } + if r == nil { + // Only include the accessId if we're confident which one it is. + if len(idMatches) != 1 { + accessId = "" + } + r = createResult(accessId, accessKey, apiEndpoint, false, nil) + } + results = append(results, *r) } return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, endpoint string, id string, key string) (bool, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://%s/api/v1/users", endpoint), nil) + if err != nil { + return false, nil + } + + req.SetBasicAuth(id, key) + 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: + // If the endpoint returns useful information, we can return it as a map. + return true, nil + case http.StatusUnauthorized: + // The secret is determinately not verified (nothing to do) + return false, nil + default: + return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + +func createResult(accessId string, accessKey string, endpoint string, verified bool, err error) *detectors.Result { + r := &detectors.Result{ + DetectorType: detectorspb.DetectorType_SumoLogicKey, + Raw: []byte(accessKey), + Verified: verified, + ExtraData: map[string]string{ + "rotation_guide": "https://howtorotate.com/docs/tutorials/sumologic/", + }, + } + r.SetVerificationError(err, accessKey) + + // |endpoint| and |accessId| won't be specified unless there's a confident match. + if endpoint != "" && accessId != "" { + var sb strings.Builder + sb.WriteString(`{`) + sb.WriteString(`"url":"` + endpoint + `"`) + sb.WriteString(`,"accessId":"` + accessId + `"`) + sb.WriteString(`,"accessKey":"` + accessKey + `"`) + sb.WriteString(`}`) + r.RawV2 = []byte(sb.String()) + } + + return r +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_SumoLogicKey }