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]>
This commit is contained in:
Shahzad Haider
2025-08-01 15:22:31 +05:00
committed by GitHub
co-authored by Amaan Ullah
parent 9df896bc25
commit d612d325ea
2 changed files with 179 additions and 29 deletions
+175 -23
View File
@@ -6,6 +6,8 @@ import (
"fmt"
"io"
"net/http"
"path"
"strings"
regexp "github.com/wasilibs/go-re2"
@@ -15,6 +17,33 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
)
// Define error code constants for better maintainability
const (
// Success codes
CodeSuccess = 0
CodeSpecialSuccess = 99991672 // Based on old code - couldn't find documentation for this code
// HTTP 200 response codes - token invalidity
CodeInvalidToken = 20005 // The user access token passed is invalid
CodeUserNotExist = 20008 // User not exist
CodeUserResigned = 20021 // User resigned
CodeUserFrozen = 20022 // User frozen
CodeUserNotRegistered = 20023 // User not registered
// Undocumented error codes - these are found in the API responses but not in the documentation
CodeUndocumentedInvalid1 = 99991663 // Invalid access token for authorization. Please make a request with token attached.
CodeUndocumentedInvalid2 = 99991668 // Invalid access token for authorization. Please make a request with token attached.
)
// VerificationResult represents the outcome of token verification
type VerificationResult int
const (
VerificationValid VerificationResult = iota // Token is valid
VerificationInvalid // Token is definitively invalid
VerificationError // Error occurred during verification
)
type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
client *http.Client
@@ -22,6 +51,7 @@ type Scanner struct {
// Check that the LarkSuite scanner implements the SecretScanner interface at compile time.
var _ detectors.Detector = Scanner{}
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
type tokenType string
@@ -52,6 +82,14 @@ func (s Scanner) Keywords() []string {
return []string{"lark", "larksuite"}
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_LarkSuite
}
func (s Scanner) Description() string {
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."
}
// FromData will find and optionally verify Larksuite 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)
@@ -93,20 +131,14 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
return results, nil
}
func (s Scanner) Type() detectorspb.DetectorType {
return detectorspb.DetectorType_LarkSuite
}
func (s Scanner) Description() string {
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."
}
func verifyAccessToken(ctx context.Context, client *http.Client, url string, token string) (bool, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return false, err
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
res, err := client.Do(req)
if err != nil {
return false, err
@@ -115,22 +147,31 @@ func verifyAccessToken(ctx context.Context, client *http.Client, url string, tok
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
switch res.StatusCode {
case http.StatusOK, http.StatusBadRequest:
var bodyResponse verificationResponse
if err := json.NewDecoder(res.Body).Decode(&bodyResponse); err != nil {
err = fmt.Errorf("failed to decode response: %w", err)
return false, err
} else {
if bodyResponse.Code == 0 || bodyResponse.Code == 99991672 {
return true, nil
} else {
return false, fmt.Errorf("unexpected verification response code %d, message %s", bodyResponse.Code, bodyResponse.Message)
}
}
// For LarkSuite API, we expect JSON responses for all documented status codes
var bodyResponse verificationResponse
if err := json.NewDecoder(res.Body).Decode(&bodyResponse); err != nil {
return false, fmt.Errorf("failed to decode response (status %d): %w", res.StatusCode, err)
}
// Handle based on error code classification
result := classifyErrorCode(bodyResponse.Code)
switch result {
case VerificationValid:
return true, nil
case VerificationInvalid:
return false, nil
case VerificationError:
// All other cases: system errors, rate limits, permission issues, etc.
// Return error so token is marked as "unverified" (couldn't verify)
return false, fmt.Errorf("verification failed (status %d, code %d): %s",
res.StatusCode, bodyResponse.Code, bodyResponse.Message)
default:
// 500 larksuite was unable to generate a result
return false, err
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
@@ -138,3 +179,114 @@ type verificationResponse struct {
Code int `json:"code"`
Message string `json:"msg"`
}
// classifyErrorCode determines how to handle different error codes based on their meaning
func classifyErrorCode(code int) VerificationResult {
// based on the documentation, API fails if response code is not zero
// https://open.larksuite.com/document/uAjLw4CM/ukTMukTMukTM/reference/authen-v1/user_info/get
// https://open.larksuite.com/document/server-docs/calendar-v4/calendar/list
// https://open.larksuite.com/document/server-docs/tenant-v2/tenant/query
switch code {
case CodeSuccess, CodeSpecialSuccess:
return VerificationValid
// HTTP 200 codes that indicate definitive token invalidity
case CodeInvalidToken, CodeUserNotExist, CodeUserResigned, CodeUserFrozen, CodeUserNotRegistered, CodeUndocumentedInvalid1,
CodeUndocumentedInvalid2:
return VerificationInvalid
// All other error codes are treated as verification errors
// This includes:
// - Invalid requests (20001)
// - System errors (20050, 190003)
// - Rate limits (190004, 190005, 190010)
// - Permission issues (190006, 191002, 191003, 191004, 1184001)
// - Resource not found (190007, 191000, 195100, 1184000)
// - Parameter issues (190002, 190008, 190009, 191001)
default:
return VerificationError
}
}
func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
if _, ok := commonFileExtensions[strings.ToLower(path.Ext(string(result.Raw)))]; ok {
return ok, ""
}
// back to the default false positive checks
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
}
var commonFileExtensions = map[string]struct{}{
// Web files
".html": {},
".htm": {},
".css": {},
".js": {},
".json": {},
".xml": {},
".svg": {},
".php": {},
".asp": {},
".aspx": {},
".jsp": {},
// Document files
".txt": {},
".md": {},
".pdf": {},
".doc": {},
".docx": {},
".rtf": {},
// Data files
".csv": {},
".xlsx": {},
".xls": {},
".sql": {},
".db": {},
// Config files
".conf": {},
".config": {},
".ini": {},
".yaml": {},
".yml": {},
".toml": {},
// Log files
".log": {},
".out": {},
".err": {},
// Archive files
".zip": {},
".tar": {},
".gz": {},
".rar": {},
// Image files
".png": {},
".jpg": {},
".jpeg": {},
".gif": {},
".bmp": {},
".ico": {},
// Source code files
".go": {},
".py": {},
".java": {},
".cpp": {},
".c": {},
".h": {},
".rb": {},
".rs": {},
".ts": {},
// Other common files
".tmp": {},
".bak": {},
".old": {},
".lock": {},
}
@@ -78,14 +78,12 @@ func TestLarksuite_FromChunk(t *testing.T) {
data: []byte(fmt.Sprintf("You can find a larksuite app %s within", appToken)),
verify: true,
},
want: func() []detectors.Result {
r := detectors.Result{
want: []detectors.Result{
{
DetectorType: detectorspb.DetectorType_LarkSuite,
Verified: false,
}
r.SetVerificationError(fmt.Errorf("unexpected verification response code 99991668, message Invalid access token for authorization. Please make a request with token attached."))
return []detectors.Result{r}
}(),
},
},
wantErr: false,
},
{