Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 62 additions & 22 deletions pkg/detectors/dovico/dovico.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package dovico
import (
"context"
"fmt"
"io"
"net/http"
"strings"

regexp "github.com/wasilibs/go-re2"

Expand All @@ -14,14 +14,15 @@ import (
)

type Scanner struct {
client *http.Client
detectors.DefaultMultiPartCredentialProvider
}

// 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{"dovico"}) + `\b([0-9a-z]{32}\.[0-9a-z]{1,}\b)`)
Expand All @@ -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),
Copy link
Contributor

@abmussani abmussani Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't RAW contain both key + userkey ? or may be set RawV2 for both keys? Otherwise, it will be difficult to find which combination worked.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could changing the Raw value have an impact on existing usage of this detector? Same question about adding a RawV2 value?
I can make those updates if this is a safe option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

umm.... Considering only OSS, I could not think of any impact. @trufflesteeeve what are your thoughts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abmussani I added a RawV2 value

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)

if s1.Verified {
// Data keys are mapped to users and consumer secrets so we can skip it once 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
}
Expand Down
11 changes: 10 additions & 1 deletion pkg/detectors/dovico/dovico_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -53,6 +53,10 @@ func TestDovico_FromChunk(t *testing.T) {
DetectorType: detectorspb.DetectorType_Dovico,
Verified: true,
},
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
},
wantErr: false,
},
Expand All @@ -69,6 +73,10 @@ func TestDovico_FromChunk(t *testing.T) {
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
{
DetectorType: detectorspb.DetectorType_Dovico,
Verified: false,
},
},
wantErr: false,
},
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions pkg/detectors/dovico/dovico_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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",
}
)

Expand Down
Loading