Skip to content

Commit d612d32

Browse files
Improve LarkSuite Detector Accuracy and Error Handling (#4334)
* added error codes from docs * handled undocumented error codes * fixed larksuite detector tests * added filename likelihood check * implemented CustomFalsePositiveChecker interface for larksuite detector --------- Co-authored-by: Amaan Ullah <[email protected]>
1 parent 9df896b commit d612d32

File tree

2 files changed

+179
-29
lines changed

2 files changed

+179
-29
lines changed

pkg/detectors/larksuite/larksuite.go

Lines changed: 175 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"io"
88
"net/http"
9+
"path"
10+
"strings"
911

1012
regexp "github.com/wasilibs/go-re2"
1113

@@ -15,13 +17,41 @@ import (
1517
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
1618
)
1719

20+
// Define error code constants for better maintainability
21+
const (
22+
// Success codes
23+
CodeSuccess = 0
24+
CodeSpecialSuccess = 99991672 // Based on old code - couldn't find documentation for this code
25+
26+
// HTTP 200 response codes - token invalidity
27+
CodeInvalidToken = 20005 // The user access token passed is invalid
28+
CodeUserNotExist = 20008 // User not exist
29+
CodeUserResigned = 20021 // User resigned
30+
CodeUserFrozen = 20022 // User frozen
31+
CodeUserNotRegistered = 20023 // User not registered
32+
33+
// Undocumented error codes - these are found in the API responses but not in the documentation
34+
CodeUndocumentedInvalid1 = 99991663 // Invalid access token for authorization. Please make a request with token attached.
35+
CodeUndocumentedInvalid2 = 99991668 // Invalid access token for authorization. Please make a request with token attached.
36+
)
37+
38+
// VerificationResult represents the outcome of token verification
39+
type VerificationResult int
40+
41+
const (
42+
VerificationValid VerificationResult = iota // Token is valid
43+
VerificationInvalid // Token is definitively invalid
44+
VerificationError // Error occurred during verification
45+
)
46+
1847
type Scanner struct {
1948
detectors.DefaultMultiPartCredentialProvider
2049
client *http.Client
2150
}
2251

2352
// Check that the LarkSuite scanner implements the SecretScanner interface at compile time.
2453
var _ detectors.Detector = Scanner{}
54+
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
2555

2656
type tokenType string
2757

@@ -52,6 +82,14 @@ func (s Scanner) Keywords() []string {
5282
return []string{"lark", "larksuite"}
5383
}
5484

85+
func (s Scanner) Type() detectorspb.DetectorType {
86+
return detectorspb.DetectorType_LarkSuite
87+
}
88+
89+
func (s Scanner) Description() string {
90+
return "LarkSuite is a collaborative suite that includes chat, calendar, and cloud storage features. The detected token can be used to access and interact with these services."
91+
}
92+
5593
// FromData will find and optionally verify Larksuite secrets in a given set of bytes.
5694
func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
5795
dataStr := string(data)
@@ -93,20 +131,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
93131
return results, nil
94132
}
95133

96-
func (s Scanner) Type() detectorspb.DetectorType {
97-
return detectorspb.DetectorType_LarkSuite
98-
}
99-
100-
func (s Scanner) Description() string {
101-
return "LarkSuite is a collaborative suite that includes chat, calendar, and cloud storage features. The detected token can be used to access and interact with these services."
102-
}
103-
104134
func verifyAccessToken(ctx context.Context, client *http.Client, url string, token string) (bool, error) {
105135
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
106136
if err != nil {
107137
return false, err
108138
}
139+
109140
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
141+
110142
res, err := client.Do(req)
111143
if err != nil {
112144
return false, err
@@ -115,26 +147,146 @@ func verifyAccessToken(ctx context.Context, client *http.Client, url string, tok
115147
_, _ = io.Copy(io.Discard, res.Body)
116148
_ = res.Body.Close()
117149
}()
118-
switch res.StatusCode {
119-
case http.StatusOK, http.StatusBadRequest:
120-
var bodyResponse verificationResponse
121-
if err := json.NewDecoder(res.Body).Decode(&bodyResponse); err != nil {
122-
err = fmt.Errorf("failed to decode response: %w", err)
123-
return false, err
124-
} else {
125-
if bodyResponse.Code == 0 || bodyResponse.Code == 99991672 {
126-
return true, nil
127-
} else {
128-
return false, fmt.Errorf("unexpected verification response code %d, message %s", bodyResponse.Code, bodyResponse.Message)
129-
}
130-
}
150+
151+
// For LarkSuite API, we expect JSON responses for all documented status codes
152+
var bodyResponse verificationResponse
153+
if err := json.NewDecoder(res.Body).Decode(&bodyResponse); err != nil {
154+
return false, fmt.Errorf("failed to decode response (status %d): %w", res.StatusCode, err)
155+
}
156+
157+
// Handle based on error code classification
158+
result := classifyErrorCode(bodyResponse.Code)
159+
160+
switch result {
161+
case VerificationValid:
162+
return true, nil
163+
164+
case VerificationInvalid:
165+
return false, nil
166+
167+
case VerificationError:
168+
// All other cases: system errors, rate limits, permission issues, etc.
169+
// Return error so token is marked as "unverified" (couldn't verify)
170+
return false, fmt.Errorf("verification failed (status %d, code %d): %s",
171+
res.StatusCode, bodyResponse.Code, bodyResponse.Message)
172+
131173
default:
132-
// 500 larksuite was unable to generate a result
133-
return false, err
174+
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
134175
}
135176
}
136177

137178
type verificationResponse struct {
138179
Code int `json:"code"`
139180
Message string `json:"msg"`
140181
}
182+
183+
// classifyErrorCode determines how to handle different error codes based on their meaning
184+
func classifyErrorCode(code int) VerificationResult {
185+
// based on the documentation, API fails if response code is not zero
186+
// https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get
187+
// https://open.larksuite.com/document/server-docs/calendar-v4/calendar/list
188+
// https://open.larksuite.com/document/server-docs/tenant-v2/tenant/query
189+
switch code {
190+
case CodeSuccess, CodeSpecialSuccess:
191+
return VerificationValid
192+
193+
// HTTP 200 codes that indicate definitive token invalidity
194+
case CodeInvalidToken, CodeUserNotExist, CodeUserResigned, CodeUserFrozen, CodeUserNotRegistered, CodeUndocumentedInvalid1,
195+
CodeUndocumentedInvalid2:
196+
return VerificationInvalid
197+
198+
// All other error codes are treated as verification errors
199+
// This includes:
200+
// - Invalid requests (20001)
201+
// - System errors (20050, 190003)
202+
// - Rate limits (190004, 190005, 190010)
203+
// - Permission issues (190006, 191002, 191003, 191004, 1184001)
204+
// - Resource not found (190007, 191000, 195100, 1184000)
205+
// - Parameter issues (190002, 190008, 190009, 191001)
206+
default:
207+
return VerificationError
208+
}
209+
}
210+
211+
func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
212+
if _, ok := commonFileExtensions[strings.ToLower(path.Ext(string(result.Raw)))]; ok {
213+
return ok, ""
214+
}
215+
216+
// back to the default false positive checks
217+
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
218+
}
219+
220+
var commonFileExtensions = map[string]struct{}{
221+
// Web files
222+
".html": {},
223+
".htm": {},
224+
".css": {},
225+
".js": {},
226+
".json": {},
227+
".xml": {},
228+
".svg": {},
229+
".php": {},
230+
".asp": {},
231+
".aspx": {},
232+
".jsp": {},
233+
234+
// Document files
235+
".txt": {},
236+
".md": {},
237+
".pdf": {},
238+
".doc": {},
239+
".docx": {},
240+
".rtf": {},
241+
242+
// Data files
243+
".csv": {},
244+
".xlsx": {},
245+
".xls": {},
246+
".sql": {},
247+
".db": {},
248+
249+
// Config files
250+
".conf": {},
251+
".config": {},
252+
".ini": {},
253+
".yaml": {},
254+
".yml": {},
255+
".toml": {},
256+
257+
// Log files
258+
".log": {},
259+
".out": {},
260+
".err": {},
261+
262+
// Archive files
263+
".zip": {},
264+
".tar": {},
265+
".gz": {},
266+
".rar": {},
267+
268+
// Image files
269+
".png": {},
270+
".jpg": {},
271+
".jpeg": {},
272+
".gif": {},
273+
".bmp": {},
274+
".ico": {},
275+
276+
// Source code files
277+
".go": {},
278+
".py": {},
279+
".java": {},
280+
".cpp": {},
281+
".c": {},
282+
".h": {},
283+
".rb": {},
284+
".rs": {},
285+
".ts": {},
286+
287+
// Other common files
288+
".tmp": {},
289+
".bak": {},
290+
".old": {},
291+
".lock": {},
292+
}

pkg/detectors/larksuite/larksuite_integration_test.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,12 @@ func TestLarksuite_FromChunk(t *testing.T) {
7878
data: []byte(fmt.Sprintf("You can find a larksuite app %s within", appToken)),
7979
verify: true,
8080
},
81-
want: func() []detectors.Result {
82-
r := detectors.Result{
81+
want: []detectors.Result{
82+
{
8383
DetectorType: detectorspb.DetectorType_LarkSuite,
8484
Verified: false,
85-
}
86-
r.SetVerificationError(fmt.Errorf("unexpected verification response code 99991668, message Invalid access token for authorization. Please make a request with token attached."))
87-
return []detectors.Result{r}
88-
}(),
85+
},
86+
},
8987
wantErr: false,
9088
},
9189
{

0 commit comments

Comments
 (0)