Skip to content

Commit b9ab701

Browse files
authored
(fix) Indeterminate verification issue in Clickup detector (#4047)
* handle unexpected status codes and some refactoring * reusable http client some code refactoring. * linter fixes. * added test case for indeterminate verification errors. * added regex prefix to increase confidence. removed commented code from integration test.
1 parent 70fb3fa commit b9ab701

File tree

2 files changed

+83
-31
lines changed

2 files changed

+83
-31
lines changed

pkg/detectors/clickuppersonaltoken/clickuppersonaltoken.go

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package clickuppersonaltoken
22

33
import (
44
"context"
5+
"fmt"
6+
"io"
57
"net/http"
68
"strings"
79

@@ -12,16 +14,18 @@ import (
1214
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
1315
)
1416

15-
type Scanner struct{}
17+
type Scanner struct {
18+
client *http.Client
19+
}
1620

1721
// Ensure the Scanner satisfies the interface at compile time.
1822
var _ detectors.Detector = (*Scanner)(nil)
1923

2024
var (
21-
client = common.SaneHttpClient()
25+
defaultClient = common.SaneHttpClient()
2226

2327
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
24-
keyPat = regexp.MustCompile(`\b(pk_[0-9]{7,8}_[0-9A-Z]{32})\b`)
28+
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"clickup"}) + `\b(pk_[0-9]{7,9}_[0-9A-Z]{32})\b`)
2529
)
2630

2731
// Keywords are used for efficiently pre-filtering chunks.
@@ -34,29 +38,25 @@ func (s Scanner) Keywords() []string {
3438
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
3539
dataStr := string(data)
3640

37-
matches := keyPat.FindAllStringSubmatch(dataStr, -1)
38-
39-
for _, match := range matches {
40-
resMatch := strings.TrimSpace(match[1])
41+
uniqueMatches := make(map[string]struct{})
42+
for _, match := range keyPat.FindAllStringSubmatch(dataStr, -1) {
43+
uniqueMatches[strings.TrimSpace(match[1])] = struct{}{}
44+
}
4145

46+
for resMatch := range uniqueMatches {
4247
s1 := detectors.Result{
4348
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
4449
Raw: []byte(resMatch),
4550
}
4651

4752
if verify {
48-
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.clickup.com/api/v2/user", nil)
49-
if err != nil {
50-
continue
51-
}
52-
req.Header.Add("Authorization", resMatch)
53-
res, err := client.Do(req)
54-
if err == nil {
55-
defer res.Body.Close()
56-
if res.StatusCode >= 200 && res.StatusCode < 300 {
57-
s1.Verified = true
58-
}
53+
client := s.client
54+
if client == nil {
55+
client = defaultClient
5956
}
57+
isVerified, err := verifyToken(ctx, client, resMatch)
58+
s1.Verified = isVerified
59+
s1.SetVerificationError(err, resMatch)
6060
}
6161

6262
results = append(results, s1)
@@ -72,3 +72,28 @@ func (s Scanner) Type() detectorspb.DetectorType {
7272
func (s Scanner) Description() string {
7373
return "ClickUp is a project management tool. Personal tokens can be used to access and modify data within ClickUp on behalf of a user."
7474
}
75+
76+
func verifyToken(ctx context.Context, client *http.Client, token string) (bool, error) {
77+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.clickup.com/api/v2/user", nil)
78+
if err != nil {
79+
return false, err
80+
}
81+
req.Header.Add("Authorization", token)
82+
res, err := client.Do(req)
83+
if err != nil {
84+
return false, err
85+
}
86+
defer func() {
87+
_, _ = io.Copy(io.Discard, res.Body)
88+
_ = res.Body.Close()
89+
}()
90+
91+
switch res.StatusCode {
92+
case http.StatusOK:
93+
return true, nil
94+
case http.StatusUnauthorized:
95+
return false, nil
96+
default:
97+
return false, fmt.Errorf("unexpected status code %d", res.StatusCode)
98+
}
99+
}

pkg/detectors/clickuppersonaltoken/clickuppersonaltoken_integration_test.go

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import (
99
"testing"
1010
"time"
1111

12-
"github.com/kylelemons/godebug/pretty"
12+
"github.com/google/go-cmp/cmp"
13+
"github.com/google/go-cmp/cmp/cmpopts"
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
1415

1516
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
@@ -32,11 +33,12 @@ func TestClickupPersonalToken_FromChunk(t *testing.T) {
3233
verify bool
3334
}
3435
tests := []struct {
35-
name string
36-
s Scanner
37-
args args
38-
want []detectors.Result
39-
wantErr bool
36+
name string
37+
s Scanner
38+
args args
39+
want []detectors.Result
40+
wantErr bool
41+
wantVerificationErr bool
4042
}{
4143
{
4244
name: "found, verified",
@@ -52,7 +54,8 @@ func TestClickupPersonalToken_FromChunk(t *testing.T) {
5254
Verified: true,
5355
},
5456
},
55-
wantErr: false,
57+
wantErr: false,
58+
wantVerificationErr: false,
5659
},
5760
{
5861
name: "found, unverified",
@@ -68,7 +71,8 @@ func TestClickupPersonalToken_FromChunk(t *testing.T) {
6871
Verified: false,
6972
},
7073
},
71-
wantErr: false,
74+
wantErr: false,
75+
wantVerificationErr: false,
7276
},
7377
{
7478
name: "not found",
@@ -78,14 +82,31 @@ func TestClickupPersonalToken_FromChunk(t *testing.T) {
7882
data: []byte("You cannot find the secret within"),
7983
verify: true,
8084
},
81-
want: nil,
82-
wantErr: false,
85+
want: nil,
86+
wantErr: false,
87+
wantVerificationErr: false,
88+
},
89+
{
90+
name: "found verifiable secret, verification failed due to unexpected API response",
91+
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
92+
args: args{
93+
ctx: context.Background(),
94+
data: []byte(fmt.Sprintf("You can find a clickuppersonaltoken secret %s within but not valid", inactiveSecret)), // the secret would satisfy the regex but not pass validation
95+
verify: true,
96+
},
97+
want: []detectors.Result{
98+
{
99+
DetectorType: detectorspb.DetectorType_ClickupPersonalToken,
100+
Verified: false,
101+
},
102+
},
103+
wantErr: false,
104+
wantVerificationErr: true,
83105
},
84106
}
85107
for _, tt := range tests {
86108
t.Run(tt.name, func(t *testing.T) {
87-
s := Scanner{}
88-
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
109+
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
89110
if (err != nil) != tt.wantErr {
90111
t.Errorf("ClickupPersonalToken.FromData() error = %v, wantErr %v", err, tt.wantErr)
91112
return
@@ -94,9 +115,15 @@ func TestClickupPersonalToken_FromChunk(t *testing.T) {
94115
if len(got[i].Raw) == 0 {
95116
t.Fatalf("no raw secret present: \n %+v", got[i])
96117
}
118+
119+
if (got[i].VerificationError() != nil) != tt.wantVerificationErr {
120+
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError())
121+
}
97122
got[i].Raw = nil
98123
}
99-
if diff := pretty.Compare(got, tt.want); diff != "" {
124+
125+
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError")
126+
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
100127
t.Errorf("ClickupPersonalToken.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
101128
}
102129
})

0 commit comments

Comments
 (0)