@@ -2,23 +2,28 @@ package sonarcloud
2
2
3
3
import (
4
4
"context"
5
- regexp "github.com/wasilibs/go-re2"
5
+ "encoding/base64"
6
+ "encoding/json"
7
+ "fmt"
6
8
"io"
7
9
"net/http"
8
- "strings"
10
+
11
+ regexp "github.com/wasilibs/go-re2"
9
12
10
13
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
11
14
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
12
15
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
13
16
)
14
17
15
- type Scanner struct {}
18
+ type Scanner struct {
19
+ client * http.Client
20
+ }
16
21
17
22
// Ensure the Scanner satisfies the interface at compile time.
18
23
var _ detectors.Detector = (* Scanner )(nil )
19
24
20
25
var (
21
- client = common .SaneHttpClient ()
26
+ defaultClient = common .SaneHttpClient ()
22
27
23
28
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
24
29
keyPat = regexp .MustCompile (detectors .PrefixRegex ([]string {"sonar" }) + `(?:^|[^@])\b([0-9a-z]{40})\b` )
@@ -30,43 +35,33 @@ func (s Scanner) Keywords() []string {
30
35
return []string {"sonar" }
31
36
}
32
37
38
+ func (s Scanner ) getClient () * http.Client {
39
+ if s .client != nil {
40
+ return s .client
41
+ }
42
+
43
+ return defaultClient
44
+ }
45
+
33
46
// FromData will find and optionally verify SonarCloud secrets in a given set of bytes.
34
47
func (s Scanner ) FromData (ctx context.Context , verify bool , data []byte ) (results []detectors.Result , err error ) {
35
48
dataStr := string (data )
36
49
37
- matches := keyPat . FindAllStringSubmatch ( dataStr , - 1 )
38
-
39
- for _ , match := range matches {
40
- resMatch := strings . TrimSpace ( match [ 1 ])
50
+ uniqueTokenMatches := make ( map [ string ] struct {} )
51
+ for _ , match := range keyPat . FindAllStringSubmatch ( dataStr , - 1 ) {
52
+ uniqueTokenMatches [ match [ 1 ]] = struct {}{}
53
+ }
41
54
55
+ for match := range uniqueTokenMatches {
42
56
s1 := detectors.Result {
43
57
DetectorType : detectorspb .DetectorType_SonarCloud ,
44
- Raw : []byte (resMatch ),
58
+ Raw : []byte (match ),
45
59
}
46
60
47
61
if verify {
48
- req , err := http .NewRequestWithContext (ctx , "GET" , "https://" + resMatch + "@sonarcloud.io/api/authentication/validate" , nil )
49
- if err != nil {
50
- continue
51
- }
52
- res , err := client .Do (req )
53
- if err == nil {
54
- bodyBytes , err := io .ReadAll (res .Body )
55
- if err != nil {
56
- continue
57
- }
58
-
59
- bodyString := string (bodyBytes )
60
- validResponse := strings .Contains (bodyString , `"valid":true` )
61
-
62
- defer res .Body .Close ()
63
- if res .StatusCode >= 200 && res .StatusCode < 300 {
64
- if validResponse {
65
- s1 .Verified = true
66
- }
67
- }
68
- }
69
-
62
+ isVerified , verificationErr := s .verifyMatch (ctx , s .getClient (), match )
63
+ s1 .Verified = isVerified
64
+ s1 .SetVerificationError (verificationErr , match )
70
65
}
71
66
72
67
results = append (results , s1 )
@@ -75,6 +70,50 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
75
70
return results , nil
76
71
}
77
72
73
+ // verifyMatch attempts to validate a SonarCloud token.
74
+ func (s Scanner ) verifyMatch (ctx context.Context , client * http.Client , token string ) (bool , error ) {
75
+ url := "https://sonarcloud.io/api/authentication/validate"
76
+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , url , http .NoBody )
77
+ if err != nil {
78
+ return false , fmt .Errorf ("failed to create request: %w" , err )
79
+ }
80
+
81
+ encodedToken := base64 .StdEncoding .EncodeToString ([]byte (fmt .Sprintf ("%s:" , token )))
82
+ req .Header .Set ("Authorization" , fmt .Sprintf ("Basic %s" , encodedToken ))
83
+
84
+ res , err := client .Do (req )
85
+ if err != nil {
86
+ return false , fmt .Errorf ("failed to perform request: %w" , err )
87
+ }
88
+
89
+ defer func () {
90
+ _ , _ = io .Copy (io .Discard , res .Body )
91
+ _ = res .Body .Close ()
92
+ }()
93
+
94
+ // The SonarCloud API always returns 200 OK, even for invalid tokens,
95
+ // with the validity indicated in the JSON body.
96
+ if res .StatusCode != http .StatusOK {
97
+ // Treat any non-200 status as a failed attempt to verify.
98
+ return false , fmt .Errorf ("unexpected status code: %d" , res .StatusCode )
99
+ }
100
+
101
+ bodyBytes , err := io .ReadAll (res .Body )
102
+ if err != nil {
103
+ return false , fmt .Errorf ("failed to read response body: %w" , err )
104
+ }
105
+
106
+ var resp struct {
107
+ Valid bool `json:"valid"`
108
+ }
109
+
110
+ if err := json .Unmarshal (bodyBytes , & resp ); err != nil {
111
+ return false , fmt .Errorf ("invalid JSON: %w" , err )
112
+ }
113
+
114
+ return resp .Valid , nil
115
+ }
116
+
78
117
func (s Scanner ) Type () detectorspb.DetectorType {
79
118
return detectorspb .DetectorType_SonarCloud
80
119
}
0 commit comments