diff --git a/pkg/detectors/dovico/dovico.go b/pkg/detectors/dovico/dovico.go index 4f4f847ffe8c..f1282cb3b367 100644 --- a/pkg/detectors/dovico/dovico.go +++ b/pkg/detectors/dovico/dovico.go @@ -3,8 +3,8 @@ package dovico import ( "context" "fmt" + "io" "net/http" - "strings" regexp "github.com/wasilibs/go-re2" @@ -14,6 +14,7 @@ import ( ) type Scanner struct { + client *http.Client detectors.DefaultMultiPartCredentialProvider } @@ -21,7 +22,7 @@ type Scanner struct { 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{"dovico"}) + `\b([0-9a-z]{32}\.[0-9a-z]{1,}\b)`) @@ -34,46 +35,85 @@ func (s Scanner) Keywords() []string { return []string{"dovico"} } +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + return defaultClient +} + // FromData will find and optionally verify Dovico 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) - userMatches := userPat.FindAllStringSubmatch(dataStr, -1) + uniqueKeys := make(map[string]struct{}) + for _, matches := range keyPat.FindAllStringSubmatch(dataStr, -1) { + uniqueKeys[matches[1]] = struct{}{} + } - for _, match := range matches { - resMatch := strings.TrimSpace(match[1]) - for _, user := range userMatches { - resUser := strings.TrimSpace(user[1]) + uniqueUserKeys := make(map[string]struct{}) + for _, matches := range userPat.FindAllStringSubmatch(dataStr, -1) { + uniqueUserKeys[matches[1]] = struct{}{} + } + + for key := range uniqueKeys { + for userKey := range uniqueUserKeys { + if key == userKey { + continue // Skip if ID and secret are the same. + } s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_Dovico, - Raw: []byte(resMatch), + Raw: []byte(key), + RawV2: []byte(fmt.Sprintf("%s:%s", key, userKey)), } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://api.dovico.com/Employees/?version=7", nil) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", fmt.Sprintf(`WRAP access_token="client=%s&user_token=%s"`, resMatch, resUser)) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true - } - } + client := s.getClient() + isVerified, err := verifyMatch(ctx, client, key, userKey) + s1.Verified = isVerified + s1.SetVerificationError(err, key, userKey) } results = append(results, s1) + + // Credentials have 1:1 mapping so we can stop checking other user keys once it is verified + if s1.Verified { + break + } } } return results, nil } +func verifyMatch(ctx context.Context, client *http.Client, key, user string) (bool, error) { + // Reference: https://timesheet.dovico.com/developer/API_doc/#t=API_Overview.html + req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.dovico.com/employees/?version=7", http.NoBody) + if err != nil { + return false, err + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", fmt.Sprintf(`WRAP access_token="client=%s&user_token=%s"`, key, user)) + 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 + default: + return false, fmt.Errorf("unexpected status code %d", res.StatusCode) + } +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_Dovico } diff --git a/pkg/detectors/dovico/dovico_integration_test.go b/pkg/detectors/dovico/dovico_integration_test.go index c63828a0b2fc..17ddedc462c3 100644 --- a/pkg/detectors/dovico/dovico_integration_test.go +++ b/pkg/detectors/dovico/dovico_integration_test.go @@ -19,7 +19,7 @@ import ( func TestDovico_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors1") + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors6") if err != nil { t.Fatalf("could not get test secrets from GCP: %s", err) } @@ -53,6 +53,10 @@ func TestDovico_FromChunk(t *testing.T) { DetectorType: detectorspb.DetectorType_Dovico, Verified: true, }, + { + DetectorType: detectorspb.DetectorType_Dovico, + Verified: false, + }, }, wantErr: false, }, @@ -69,6 +73,10 @@ func TestDovico_FromChunk(t *testing.T) { DetectorType: detectorspb.DetectorType_Dovico, Verified: false, }, + { + DetectorType: detectorspb.DetectorType_Dovico, + Verified: false, + }, }, wantErr: false, }, @@ -97,6 +105,7 @@ func TestDovico_FromChunk(t *testing.T) { t.Fatalf("no raw secret present: \n %+v", got[i]) } got[i].Raw = nil + got[i].RawV2 = nil } if diff := pretty.Compare(got, tt.want); diff != "" { t.Errorf("Dovico.FromData() %s diff: (-got +want)\n%s", tt.name, diff) diff --git a/pkg/detectors/dovico/dovico_test.go b/pkg/detectors/dovico/dovico_test.go index c79c3a6d9f3e..cf941cbf950e 100644 --- a/pkg/detectors/dovico/dovico_test.go +++ b/pkg/detectors/dovico/dovico_test.go @@ -23,8 +23,8 @@ var ( auth_type: "Token" in: "Header" api_version: v1 - dovico_user: "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4olz2l5jzw54yv3ai0qwdri6l1f4iyruc7f" - dovico_token: "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6evkkrqz3onrg2epqj9i2lgkb0wxf8lq0gdzvw6macc9br1qi9ry335u173dr3gzcgy9v6" + dovico_user: "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol" + dovico_token: "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e" base_url: "https://api.example.com/$api_version/example" response_code: 200 @@ -33,10 +33,8 @@ var ( # - The above credentials should only be used in a secure environment. ` secrets = []string{ - "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6evkkrqz3onrg2epqj9i2lgkb0wxf8lq0gdzvw6macc9br1qi9ry335u173dr3gzcgy9v6", - "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6evkkrqz3onrg2epqj9i2lgkb0wxf8lq0gdzvw6macc9br1qi9ry335u173dr3gzcgy9v6", - "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4olz2l5jzw54yv3ai0qwdri6l1f4iyruc7f", - "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4olz2l5jzw54yv3ai0qwdri6l1f4iyruc7f", + "nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e:ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol", + "ntb4fnhk5iot7hzbfjw08jm661iocdd4.3ws4ol:nuhkw7nsrybuvmetium29a6oajxr3xdg.sbpi6e", } )