Skip to content

Commit 53bc3f2

Browse files
enhanced the api response handling in the secret verification
1 parent 7746b2c commit 53bc3f2

File tree

1 file changed

+70
-31
lines changed

1 file changed

+70
-31
lines changed

pkg/detectors/sonarcloud/sonarcloud.go

Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@ package sonarcloud
22

33
import (
44
"context"
5-
regexp "github.com/wasilibs/go-re2"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
68
"io"
79
"net/http"
8-
"strings"
10+
11+
regexp "github.com/wasilibs/go-re2"
912

1013
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1114
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1215
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
1316
)
1417

15-
type Scanner struct{}
18+
type Scanner struct {
19+
client *http.Client
20+
}
1621

1722
// Ensure the Scanner satisfies the interface at compile time.
1823
var _ detectors.Detector = (*Scanner)(nil)
1924

2025
var (
21-
client = common.SaneHttpClient()
26+
defaultClient = common.SaneHttpClient()
2227

2328
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
2429
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sonar"}) + `(?:^|[^@])\b([0-9a-z]{40})\b`)
@@ -30,43 +35,33 @@ func (s Scanner) Keywords() []string {
3035
return []string{"sonar"}
3136
}
3237

38+
func (s Scanner) getClient() *http.Client {
39+
if s.client != nil {
40+
return s.client
41+
}
42+
43+
return defaultClient
44+
}
45+
3346
// FromData will find and optionally verify SonarCloud secrets in a given set of bytes.
3447
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
3548
dataStr := string(data)
3649

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+
}
4154

55+
for match := range uniqueTokenMatches {
4256
s1 := detectors.Result{
4357
DetectorType: detectorspb.DetectorType_SonarCloud,
44-
Raw: []byte(resMatch),
58+
Raw: []byte(match),
4559
}
4660

4761
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)
7065
}
7166

7267
results = append(results, s1)
@@ -75,6 +70,50 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
7570
return results, nil
7671
}
7772

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+
78117
func (s Scanner) Type() detectorspb.DetectorType {
79118
return detectorspb.DetectorType_SonarCloud
80119
}

0 commit comments

Comments
 (0)