@@ -2,6 +2,7 @@ package digitaloceanv2
2
2
3
3
import (
4
4
"context"
5
+ "encoding/json"
5
6
"fmt"
6
7
"io"
7
8
"net/http"
@@ -14,13 +15,15 @@ import (
14
15
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
15
16
)
16
17
17
- type Scanner struct {}
18
+ type Scanner struct {
19
+ client * http.Client
20
+ }
18
21
19
22
// Ensure the Scanner satisfies the interface at compile time.
20
23
var _ detectors.Detector = (* Scanner )(nil )
21
24
22
25
var (
23
- client = common .SaneHttpClient ()
26
+ defaultClient = common .SaneHttpClient ()
24
27
25
28
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
26
29
keyPat = regexp .MustCompile (`\b((?:dop|doo|dor)_v1_[a-f0-9]{64})\b` )
@@ -36,55 +39,42 @@ func (s Scanner) Keywords() []string {
36
39
func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
37
40
dataStr := string (data )
38
41
39
- matches := keyPat . FindAllStringSubmatch ( dataStr , - 1 )
42
+ var uniqueTokens = make ( map [ string ] struct {} )
40
43
41
- for _ , match := range matches {
42
- resMatch := strings .TrimSpace (match [1 ])
44
+ for _ , matches := range keyPat .FindAllStringSubmatch (dataStr , - 1 ) {
45
+ uniqueTokens [matches [0 ]] = struct {}{}
46
+ }
43
47
48
+ for token := range uniqueTokens {
44
49
s1 := detectors.Result {
45
50
DetectorType : detectorspb .DetectorType_DigitalOceanV2 ,
46
- Raw : []byte (resMatch ),
51
+ Raw : []byte (token ),
47
52
}
48
53
49
54
if verify {
50
- switch {
51
- case strings .HasPrefix (resMatch , "dor_v1_" ):
52
- req , err := http .NewRequestWithContext (ctx , "GET" , "https://cloud.digitalocean.com/v1/oauth/token?grant_type=refresh_token&refresh_token=" + resMatch , nil )
53
- if err != nil {
54
- continue
55
- }
56
-
57
- res , err := client .Do (req )
58
- if err == nil {
59
- bodyBytes , err := io .ReadAll (res .Body )
60
-
61
- if err != nil {
62
- continue
63
- }
64
-
65
- bodyString := string (bodyBytes )
66
- validResponse := strings .Contains (bodyString , `"access_token"` )
67
- defer res .Body .Close ()
55
+ client := s .client
56
+ if client == nil {
57
+ client = defaultClient
58
+ }
68
59
69
- if res .StatusCode >= 200 && res .StatusCode < 300 && validResponse {
70
- s1 .Verified = true
60
+ // Check if the token is a refresh token or an access token
61
+ switch {
62
+ case strings .HasPrefix (token , "dor_v1_" ):
63
+ verified , verificationErr , newAccessToken := verifyRefreshToken (ctx , client , token )
64
+ s1 .SetVerificationError (verificationErr )
65
+ s1 .Verified = verified
66
+ if s1 .Verified {
67
+ s1 .AnalysisInfo = map [string ]string {
68
+ "key" : newAccessToken ,
71
69
}
72
70
}
73
-
74
- case strings .HasPrefix (resMatch , "doo_v1_" ), strings .HasPrefix (resMatch , "dop_v1_" ):
75
- req , err := http .NewRequestWithContext (ctx , "GET" , "https://api.digitalocean.com/v2/account" , nil )
76
- if err != nil {
77
- continue
78
- }
79
- req .Header .Add ("Authorization" , fmt .Sprintf ("Bearer %s" , resMatch ))
80
- res , err := client .Do (req )
81
- if err == nil {
82
- defer res .Body .Close ()
83
- if res .StatusCode >= 200 && res .StatusCode < 300 {
84
- s1 .Verified = true
85
- s1 .AnalysisInfo = map [string ]string {
86
- "key" : resMatch ,
87
- }
71
+ case strings .HasPrefix (token , "doo_v1_" ), strings .HasPrefix (token , "dop_v1_" ):
72
+ verified , verificationErr := verifyAccessToken (ctx , client , token )
73
+ s1 .Verified = verified
74
+ s1 .SetVerificationError (verificationErr )
75
+ if s1 .Verified {
76
+ s1 .AnalysisInfo = map [string ]string {
77
+ "key" : token ,
88
78
}
89
79
}
90
80
}
@@ -96,6 +86,79 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
96
86
return results , nil
97
87
}
98
88
89
+ // verifyRefreshToken verifies the refresh token by making a request to the DigitalOcean API.
90
+ // If the token is valid, it returns the new access token and no error.
91
+ // If the token is invalid/expired, it returns an empty string and no error.
92
+ // If an error is encountered, it returns an empty string along and the error.
93
+ func verifyRefreshToken (ctx context.Context , client * http.Client , token string ) (bool , error , string ) {
94
+ // Ref: https://docs.digitalocean.com/reference/api/oauth/
95
+
96
+ url := "https://cloud.digitalocean.com/v1/oauth/token?grant_type=refresh_token&refresh_token=" + token
97
+ req , err := http .NewRequestWithContext (ctx , http .MethodPost , url , nil )
98
+ if err != nil {
99
+ return false , fmt .Errorf ("failed to create request: %w" , err ), ""
100
+ }
101
+
102
+ res , err := client .Do (req )
103
+ if err != nil {
104
+ return false , fmt .Errorf ("failed to make request: %w" , err ), ""
105
+ }
106
+
107
+ bodyBytes , err := io .ReadAll (res .Body )
108
+ if err != nil {
109
+ return false , fmt .Errorf ("failed to read response body: %w" , err ), ""
110
+ }
111
+ defer res .Body .Close ()
112
+
113
+ switch res .StatusCode {
114
+ case http .StatusOK :
115
+ var responseMap map [string ]interface {}
116
+ if err := json .Unmarshal (bodyBytes , & responseMap ); err != nil {
117
+ return false , fmt .Errorf ("failed to parse response body: %w" , err ), ""
118
+ }
119
+ // Extract the access token from the response
120
+ accessToken , exists := responseMap ["access_token" ].(string )
121
+ if ! exists {
122
+ return false , fmt .Errorf ("access_token not found in response: %s" , string (bodyBytes )), ""
123
+ }
124
+ return true , nil , accessToken
125
+ case http .StatusUnauthorized :
126
+ return false , nil , ""
127
+ default :
128
+ return false , fmt .Errorf ("unexpected status code: %d" , res .StatusCode ), ""
129
+ }
130
+ }
131
+
132
+ // verifyAccessToken verifies the access token by making a request to the DigitalOcean API.
133
+ // If the token is valid, it returns true and no error.
134
+ // If the token is invalid, it returns false and no error.
135
+ // If an error is encountered, it returns false along with the error.
136
+ func verifyAccessToken (ctx context.Context , client * http.Client , token string ) (bool , error ) {
137
+ // Ref: https://docs.digitalocean.com/reference/api/digitalocean/#tag/Account
138
+
139
+ url := "https://api.digitalocean.com/v2/account"
140
+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , nil )
141
+ if err != nil {
142
+ return false , fmt .Errorf ("failed to create request: %w" , err )
143
+ }
144
+
145
+ req .Header .Add ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
146
+ res , err := client .Do (req )
147
+ if err != nil {
148
+ return false , fmt .Errorf ("failed to make request: %w" , err )
149
+ }
150
+ defer res .Body .Close ()
151
+
152
+ switch res .StatusCode {
153
+ case http .StatusOK :
154
+ return true , nil
155
+ case http .StatusUnauthorized :
156
+ return false , nil
157
+ default :
158
+ return false , fmt .Errorf ("unexpected status code: %d" , res .StatusCode )
159
+ }
160
+ }
161
+
99
162
func (s Scanner ) Type () detectorspb.DetectorType {
100
163
return detectorspb .DetectorType_DigitalOceanV2
101
164
}
0 commit comments