From 84d5a49ba45cf1d427d8d8fa6351280b56d440b1 Mon Sep 17 00:00:00 2001 From: Shahzad Haider <76992801+shahzadhaider1@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:49:21 +0500 Subject: [PATCH] enhanced the api response handling in the secret verification (#4482) --- pkg/detectors/sonarcloud/sonarcloud.go | 98 ++++++++++++++++++-------- 1 file changed, 67 insertions(+), 31 deletions(-) diff --git a/pkg/detectors/sonarcloud/sonarcloud.go b/pkg/detectors/sonarcloud/sonarcloud.go index dfc88b106..d74226daa 100644 --- a/pkg/detectors/sonarcloud/sonarcloud.go +++ b/pkg/detectors/sonarcloud/sonarcloud.go @@ -2,23 +2,27 @@ package sonarcloud import ( "context" - regexp "github.com/wasilibs/go-re2" + "encoding/json" + "fmt" "io" "net/http" - "strings" + + regexp "github.com/wasilibs/go-re2" "github.com/trufflesecurity/trufflehog/v3/pkg/common" "github.com/trufflesecurity/trufflehog/v3/pkg/detectors" "github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb" ) -type Scanner struct{} +type Scanner struct { + client *http.Client +} // Ensure the Scanner satisfies the interface at compile time. var _ detectors.Detector = (*Scanner)(nil) var ( - client = common.SaneHttpClient() + defaultClient = common.SaneHttpClient() // Make sure that your group is surrounded in boundary characters such as below to reduce false positives. keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sonar"}) + `(?:^|[^@])\b([0-9a-z]{40})\b`) @@ -30,43 +34,33 @@ func (s Scanner) Keywords() []string { return []string{"sonar"} } +func (s Scanner) getClient() *http.Client { + if s.client != nil { + return s.client + } + + return defaultClient +} + // FromData will find and optionally verify SonarCloud secrets in a given set of bytes. func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) { dataStr := string(data) - matches := keyPat.FindAllStringSubmatch(dataStr, -1) - - for _, match := range matches { - resMatch := strings.TrimSpace(match[1]) + uniqueTokenMatches := make(map[string]struct{}) + for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) { + uniqueTokenMatches[match[1]] = struct{}{} + } + for match := range uniqueTokenMatches { s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_SonarCloud, - Raw: []byte(resMatch), + Raw: []byte(match), } if verify { - req, err := http.NewRequestWithContext(ctx, "GET", "https://"+resMatch+"@sonarcloud.io/api/authentication/validate", nil) - if err != nil { - continue - } - res, err := client.Do(req) - if err == nil { - bodyBytes, err := io.ReadAll(res.Body) - if err != nil { - continue - } - - bodyString := string(bodyBytes) - validResponse := strings.Contains(bodyString, `"valid":true`) - - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - if validResponse { - s1.Verified = true - } - } - } - + isVerified, verificationErr := s.verifyMatch(ctx, s.getClient(), match) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr, match) } results = append(results, s1) @@ -75,6 +69,48 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result return results, nil } +// verifyMatch attempts to validate a SonarCloud token. +func (s Scanner) verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) { + url := "https://sonarcloud.io/api/authentication/validate" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return false, fmt.Errorf("failed to create request: %w", err) + } + + req.SetBasicAuth(token, "") + res, err := client.Do(req) + if err != nil { + return false, fmt.Errorf("failed to perform request: %w", err) + } + + defer func() { + _, _ = io.Copy(io.Discard, res.Body) + _ = res.Body.Close() + }() + + // The SonarCloud API always returns 200 OK, even for invalid tokens, + // with the validity indicated in the JSON body. + if res.StatusCode != http.StatusOK { + // Treat any non-200 status as a failed attempt to verify. + return false, fmt.Errorf("unexpected status code: %d", res.StatusCode) + } + + bodyBytes, err := io.ReadAll(res.Body) + if err != nil { + return false, fmt.Errorf("failed to read response body: %w", err) + } + + var resp struct { + Valid bool `json:"valid"` + } + + if err := json.Unmarshal(bodyBytes, &resp); err != nil { + return false, fmt.Errorf("invalid JSON: %w", err) + } + + return resp.Valid, nil +} + func (s Scanner) Type() detectorspb.DetectorType { return detectorspb.DetectorType_SonarCloud }