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:
co-authored by
Amaan Ullah
parent
9df896bc25
commit
d612d325ea
@@ -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,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user