Skip to content

Commit 6085d0b

Browse files
committed
fix(detectors): report Todoist verifier errors
1 parent 26eae1f commit 6085d0b

2 files changed

Lines changed: 147 additions & 34 deletions

File tree

pkg/detectors/todoist/todoist.go

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package todoist
33
import (
44
"context"
55
"fmt"
6+
"io"
67
"net/http"
78
"strings"
89

@@ -13,13 +14,15 @@ import (
1314
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
1415
)
1516

16-
type Scanner struct{}
17+
type Scanner struct {
18+
client *http.Client
19+
}
1720

1821
// Ensure the Scanner satisfies the interface at compile time.
1922
var _ detectors.Detector = (*Scanner)(nil)
2023

2124
var (
22-
client = common.SaneHttpClient()
25+
defaultClient = common.SaneHttpClient()
2326

2427
// Make sure that your group is surrounded in boundary characters such as below to reduce false positives.
2528
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"todoist"}) + `\b([0-9a-z]{40})\b`)
@@ -47,18 +50,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
4750
}
4851

4952
if verify {
50-
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.todoist.com/api/v1/projects", nil)
51-
if err != nil {
52-
continue
53-
}
54-
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", resMatch))
55-
res, err := client.Do(req)
56-
if err == nil {
57-
defer func() { _ = res.Body.Close() }()
58-
if res.StatusCode >= 200 && res.StatusCode < 300 {
59-
s1.Verified = true
60-
}
53+
client := s.client
54+
if client == nil {
55+
client = defaultClient
6156
}
57+
58+
isVerified, verificationErr := verifyMatch(ctx, client, resMatch)
59+
s1.Verified = isVerified
60+
s1.SetVerificationError(verificationErr, resMatch)
6261
}
6362

6463
results = append(results, s1)
@@ -67,6 +66,37 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
6766
return results, nil
6867
}
6968

69+
func verifyMatch(ctx context.Context, client *http.Client, token string) (bool, error) {
70+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.todoist.com/api/v1/projects", nil)
71+
if err != nil {
72+
return false, err
73+
}
74+
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
75+
76+
res, err := client.Do(req)
77+
if err != nil {
78+
return false, err
79+
}
80+
defer func() {
81+
if res.Body != nil {
82+
_, _ = io.Copy(io.Discard, res.Body)
83+
_ = res.Body.Close()
84+
}
85+
}()
86+
87+
switch res.StatusCode {
88+
case http.StatusOK:
89+
return true, nil
90+
case http.StatusUnauthorized, http.StatusForbidden:
91+
return false, nil
92+
default:
93+
if res.StatusCode >= 200 && res.StatusCode < 300 {
94+
return true, nil
95+
}
96+
return false, fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
97+
}
98+
}
99+
70100
func (s Scanner) Type() detector_typepb.DetectorType {
71101
return detector_typepb.DetectorType_Todoist
72102
}

pkg/detectors/todoist/todoist_test.go

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package todoist
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"io"
78
"net/http"
@@ -100,31 +101,27 @@ func TestTodoist_Pattern(t *testing.T) {
100101
}
101102

102103
func TestTodoist_VerificationEndpoint(t *testing.T) {
103-
d := Scanner{}
104104
input := fmt.Sprintf("%s token = '%s'", keyword, validPattern)
105105

106-
prevClient := client
107-
t.Cleanup(func() {
108-
client = prevClient
109-
})
110-
111106
called := false
112-
client = &http.Client{
113-
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
114-
called = true
115-
if req.URL.String() != "https://api.todoist.com/api/v1/projects" {
116-
t.Fatalf("unexpected verification URL: %s", req.URL.String())
117-
}
118-
if req.Header.Get("Authorization") == "" {
119-
t.Fatal("missing Authorization header")
120-
}
107+
d := Scanner{
108+
client: &http.Client{
109+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
110+
called = true
111+
if req.URL.String() != "https://api.todoist.com/api/v1/projects" {
112+
t.Fatalf("unexpected verification URL: %s", req.URL.String())
113+
}
114+
if req.Header.Get("Authorization") == "" {
115+
t.Fatal("missing Authorization header")
116+
}
121117

122-
return &http.Response{
123-
StatusCode: http.StatusOK,
124-
Body: io.NopCloser(strings.NewReader("{}")),
125-
Header: make(http.Header),
126-
}, nil
127-
}),
118+
return &http.Response{
119+
StatusCode: http.StatusOK,
120+
Body: io.NopCloser(strings.NewReader("{}")),
121+
Header: make(http.Header),
122+
}, nil
123+
}),
124+
},
128125
}
129126

130127
results, err := d.FromData(context.Background(), true, []byte(input))
@@ -141,3 +138,89 @@ func TestTodoist_VerificationEndpoint(t *testing.T) {
141138
t.Fatal("expected result to be verified")
142139
}
143140
}
141+
142+
func TestTodoist_VerificationResponses(t *testing.T) {
143+
input := fmt.Sprintf("%s token = '%s'", keyword, validPattern)
144+
145+
tests := []struct {
146+
name string
147+
response *http.Response
148+
responseErr error
149+
wantVerified bool
150+
wantVerificationErr bool
151+
}{
152+
{
153+
name: "valid token",
154+
response: &http.Response{
155+
StatusCode: http.StatusOK,
156+
Body: io.NopCloser(strings.NewReader("{}")),
157+
Header: make(http.Header),
158+
},
159+
wantVerified: true,
160+
},
161+
{
162+
name: "valid token with nil body",
163+
response: &http.Response{
164+
StatusCode: http.StatusOK,
165+
Header: make(http.Header),
166+
},
167+
wantVerified: true,
168+
},
169+
{
170+
name: "unauthorized token",
171+
response: &http.Response{
172+
StatusCode: http.StatusUnauthorized,
173+
Body: io.NopCloser(strings.NewReader("{}")),
174+
Header: make(http.Header),
175+
},
176+
},
177+
{
178+
name: "forbidden token",
179+
response: &http.Response{
180+
StatusCode: http.StatusForbidden,
181+
Body: io.NopCloser(strings.NewReader("{}")),
182+
Header: make(http.Header),
183+
},
184+
},
185+
{
186+
name: "client error",
187+
responseErr: errors.New("network failure"),
188+
wantVerificationErr: true,
189+
},
190+
{
191+
name: "unexpected status",
192+
response: &http.Response{
193+
StatusCode: http.StatusInternalServerError,
194+
Body: io.NopCloser(strings.NewReader("{}")),
195+
Header: make(http.Header),
196+
},
197+
wantVerificationErr: true,
198+
},
199+
}
200+
201+
for _, tt := range tests {
202+
t.Run(tt.name, func(t *testing.T) {
203+
d := Scanner{
204+
client: &http.Client{
205+
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
206+
return tt.response, tt.responseErr
207+
}),
208+
},
209+
}
210+
211+
results, err := d.FromData(context.Background(), true, []byte(input))
212+
if err != nil {
213+
t.Fatalf("FromData returned error: %v", err)
214+
}
215+
if len(results) != 1 {
216+
t.Fatalf("expected 1 result, got %d", len(results))
217+
}
218+
if results[0].Verified != tt.wantVerified {
219+
t.Fatalf("Verified = %v, want %v", results[0].Verified, tt.wantVerified)
220+
}
221+
if (results[0].VerificationError() != nil) != tt.wantVerificationErr {
222+
t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, results[0].VerificationError())
223+
}
224+
})
225+
}
226+
}

0 commit comments

Comments
 (0)