diff --git a/pkg/detectors/opsgenie/opsgenie.go b/pkg/detectors/opsgenie/opsgenie.go index 580cc8426759..9bf5e1c1ee7c 100644 --- a/pkg/detectors/opsgenie/opsgenie.go +++ b/pkg/detectors/opsgenie/opsgenie.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "strings" @@ -14,15 +15,16 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" ) -type Scanner struct{} +type Scanner struct { + client *http.Client +} // Ensure the Scanner satisfies the interface at compile time. var _ detectors.Detector = (*Scanner)(nil) var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() - // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"opsgenie"}) + `\b([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`) ) @@ -45,63 +47,94 @@ func (s Scanner) Description() 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 { + keyMatches := make(map[string]struct{}) + for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { + if strings.Contains(match[0], "opsgenie.com/alert/detail/") { continue } - resMatch := strings.TrimSpace(match[1]) - s1 := detectors.Result{ - DetectorType: detectorspb.DetectorType_Opsgenie, - Raw: []byte(resMatch), + k := match[1] + if detectors.StringShannonEntropy(k) < 3 { + continue } + keyMatches[k] = struct{}{} + } - if strings.Contains(match[0], "opsgenie.com/alert/detail/") { - continue + for key := range keyMatches { + r := detectors.Result{ + DetectorType: detectorspb.DetectorType_Opsgenie, + Raw: []byte(key), } if verify { - - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.opsgenie.com/v2/alerts", nil) - if err != nil { - continue - } - req.Header.Add("Authorization", fmt.Sprintf("GenieKey %s", resMatch)) - res, err := client.Do(req) - if err != nil { - continue + client := s.client + if client == nil { + client = defaultClient } - defer res.Body.Close() - - // Check for 200 status code - if res.StatusCode == 200 { - var data map[string]interface{} - err := json.NewDecoder(res.Body).Decode(&data) - if err != nil { - s1.Verified = false - // set verification error in result if failed to decode the body of response - s1.SetVerificationError(err, resMatch) - continue - } - // Check if "data" is one of the top-level attributes - if _, ok := data["data"]; ok { - s1.Verified = true - } else { - s1.Verified = false + isVerified, extraData, vErr := verifyMatch(ctx, client, key) + if isVerified { + r.Verified = isVerified + r.ExtraData = extraData + r.AnalysisInfo = map[string]string{ + "key": key, } - } else { - s1.Verified = false - } - s1.AnalysisInfo = map[string]string{ - "key": resMatch, } + r.SetVerificationError(vErr, key) } - results = append(results, s1) + results = append(results, r) } return results, nil } + +func verifyMatch(ctx context.Context, client *http.Client, key string) (bool, map[string]string, error) { + // https://docs.opsgenie.com/docs/account-api + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.opsgenie.com/v2/account", nil) + if err != nil { + return false, nil, err + } + + req.Header.Add("Authorization", fmt.Sprintf("GenieKey %s", key)) + 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: + var accountRes accountResponse + if err := json.NewDecoder(res.Body).Decode(&accountRes); err != nil { + return false, nil, nil + } + + extraData := map[string]string{ + "account": accountRes.Data.Name, + "plan": accountRes.Data.Plan.Name, + } + return true, extraData, nil + case http.StatusUnauthorized: + // Key is not valid + return false, nil, nil + case http.StatusForbidden: + // Key is valid but lacks permissions + return true, nil, nil + default: + return false, nil, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode) + } +} + +type accountResponse struct { + Data struct { + Name string `json:"name"` + UserCount int `json:"userCount"` + Plan struct { + Name string `json:"name"` + } `json:"plan"` + } `json:"data"` +}