@@ -3,8 +3,8 @@ package dovico
33import (
44 "context"
55 "fmt"
6+ "io"
67 "net/http"
7- "strings"
88
99 regexp "github.com/wasilibs/go-re2"
1010
@@ -14,14 +14,15 @@ import (
1414)
1515
1616type Scanner struct {
17+ client * http.Client
1718 detectors.DefaultMultiPartCredentialProvider
1819}
1920
2021// Ensure the Scanner satisfies the interface at compile time.
2122var _ detectors.Detector = (* Scanner )(nil )
2223
2324var (
24- client = common .SaneHttpClient ()
25+ defaultClient = common .SaneHttpClient ()
2526
2627 // Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
2728 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 {
3435 return []string {"dovico" }
3536}
3637
38+ func (s Scanner ) getClient () * http.Client {
39+ if s .client != nil {
40+ return s .client
41+ }
42+ return defaultClient
43+ }
44+
3745// FromData will find and optionally verify Dovico secrets in a given set of bytes.
3846func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
3947 dataStr := string (data )
4048
41- matches := keyPat .FindAllStringSubmatch (dataStr , - 1 )
42- userMatches := userPat .FindAllStringSubmatch (dataStr , - 1 )
49+ uniqueKeys := make (map [string ]struct {})
50+ for _ , matches := range keyPat .FindAllStringSubmatch (dataStr , - 1 ) {
51+ uniqueKeys [matches [1 ]] = struct {}{}
52+ }
4353
44- for _ , match := range matches {
45- resMatch := strings .TrimSpace (match [1 ])
46- for _ , user := range userMatches {
47- resUser := strings .TrimSpace (user [1 ])
54+ uniqueUserKeys := make (map [string ]struct {})
55+ for _ , matches := range userPat .FindAllStringSubmatch (dataStr , - 1 ) {
56+ uniqueUserKeys [matches [1 ]] = struct {}{}
57+ }
58+
59+ for key := range uniqueKeys {
60+ for userKey := range uniqueUserKeys {
61+ if key == userKey {
62+ continue // Skip if ID and secret are the same.
63+ }
4864
4965 s1 := detectors.Result {
5066 DetectorType : detectorspb .DetectorType_Dovico ,
51- Raw : []byte (resMatch ),
67+ Raw : []byte (key ),
68+ RawV2 : []byte (fmt .Sprintf ("%s:%s" , key , userKey )),
5269 }
5370
5471 if verify {
55- req , err := http .NewRequestWithContext (ctx , "GET" , "https://api.dovico.com/Employees/?version=7" , nil )
56- if err != nil {
57- continue
58- }
59- req .Header .Add ("Content-Type" , "application/json" )
60- req .Header .Add ("Authorization" , fmt .Sprintf (`WRAP access_token="client=%s&user_token=%s"` , resMatch , resUser ))
61- res , err := client .Do (req )
62- if err == nil {
63- defer res .Body .Close ()
64- if res .StatusCode >= 200 && res .StatusCode < 300 {
65- s1 .Verified = true
66- }
67- }
72+ client := s .getClient ()
73+ isVerified , err := verifyMatch (ctx , client , key , userKey )
74+ s1 .Verified = isVerified
75+ s1 .SetVerificationError (err , key , userKey )
6876 }
6977
7078 results = append (results , s1 )
79+
80+ // Credentials have 1:1 mapping so we can stop checking other user keys once it is verified
81+ if s1 .Verified {
82+ break
83+ }
7184 }
7285 }
7386
7487 return results , nil
7588}
7689
90+ func verifyMatch (ctx context.Context , client * http.Client , key , user string ) (bool , error ) {
91+ // Reference: https://timesheet.dovico.com/developer/API_doc/#t=API_Overview.html
92+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , "https://api.dovico.com/employees/?version=7" , http .NoBody )
93+ if err != nil {
94+ return false , err
95+ }
96+ req .Header .Add ("Content-Type" , "application/json" )
97+ req .Header .Add ("Authorization" , fmt .Sprintf (`WRAP access_token="client=%s&user_token=%s"` , key , user ))
98+ res , err := client .Do (req )
99+ if err != nil {
100+ return false , err
101+ }
102+ defer func () {
103+ _ , _ = io .Copy (io .Discard , res .Body )
104+ _ = res .Body .Close ()
105+ }()
106+
107+ switch res .StatusCode {
108+ case http .StatusOK :
109+ return true , nil
110+ case http .StatusUnauthorized :
111+ return false , nil
112+ default :
113+ return false , fmt .Errorf ("unexpected status code %d" , res .StatusCode )
114+ }
115+ }
116+
77117func (s Scanner ) Type () detectorspb.DetectorType {
78118 return detectorspb .DetectorType_Dovico
79119}
0 commit comments