diff --git a/pkg/detectors/larksuite/larksuite.go b/pkg/detectors/larksuite/larksuite.go index 2fb041e86..ebd18fe93 100644 --- a/pkg/detectors/larksuite/larksuite.go +++ b/pkg/detectors/larksuite/larksuite.go @@ -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": {}, +} diff --git a/pkg/detectors/larksuite/larksuite_integration_test.go b/pkg/detectors/larksuite/larksuite_integration_test.go index f0845c336..c98fd813d 100644 --- a/pkg/detectors/larksuite/larksuite_integration_test.go +++ b/pkg/detectors/larksuite/larksuite_integration_test.go @@ -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, }, {