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