Improve Copper Detector verification with stricter status code and email matching (#4594)
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run

* feat(detector/copper): improve verification with stricter email matching

* revert tags in int test

---------

Co-authored-by: Kashif Khan <[email protected]>
This commit is contained in:
Amaan Ullah
2025-12-09 21:07:45 +05:00
committed by GitHub
co-authored by Kashif Khan
parent 1d87fba935
commit 7583da5f26
2 changed files with 65 additions and 24 deletions
+59 -20
View File
@@ -2,6 +2,9 @@ package copper
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
@@ -27,6 +30,11 @@ var (
idPat = regexp.MustCompile(`\b([a-z0-9]{4,25}@[a-zA-Z0-9]{2,12}.[a-zA-Z0-9]{2,6})\b`)
)
type UserApiResponse struct {
Id int `json:"id"`
Email string `json:"email"`
}
// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
@@ -52,26 +60,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
}
if verify {
payload := strings.NewReader(`{
"page_size": 25,
"sort_by": "name"
}`)
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.copper.com/developer_api/v1/tasks/search", payload)
if err != nil {
continue
}
req.Header.Add("X-PW-AccessToken", resMatch)
req.Header.Add("X-PW-Application", "developer_api")
req.Header.Add("X-PW-UserEmail", resIdMatch)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err == nil {
defer res.Body.Close()
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
}
}
isVerified, verificationErr := verifyCopper(ctx, client, resIdMatch, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
}
results = append(results, s1)
@@ -83,6 +74,54 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
return results, nil
}
func verifyCopper(ctx context.Context, client *http.Client, email, apiKey string) (bool, error) {
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://api.copper.com/developer_api/v1/users/me",
http.NoBody,
)
if err != nil {
return false, err
}
req.Header.Add("X-PW-AccessToken", apiKey)
req.Header.Add("X-PW-Application", "developer_api")
req.Header.Add("X-PW-UserEmail", email)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
return false, err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK:
respBytes, err := io.ReadAll(res.Body)
if err != nil {
return false, err
}
var respBody UserApiResponse
if err := json.Unmarshal(respBytes, &respBody); err != nil {
return false, err
}
// strict verification with email in credentials
if respBody.Email == email {
return true, nil
}
return false, fmt.Errorf("email mismatch in verification response")
case http.StatusUnauthorized:
return false, nil
default:
return false, fmt.Errorf("unexpected status code :%d", res.StatusCode)
}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_Copper
}
@@ -9,8 +9,8 @@ import (
"testing"
"time"
"github.com/kylelemons/godebug/pretty"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -96,9 +96,11 @@ func TestCopper_FromChunk(t *testing.T) {
t.Fatalf("no raw secret present: \n %+v", got[i])
}
got[i].Raw = nil
got[i].RawV2 = nil
}
if diff := pretty.Compare(got, tt.want); diff != "" {
t.Errorf("Copper.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "RawV2", "verificationError", "primarySecret")
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
t.Errorf("Abstract.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}
})
}