Files
trufflehog/pkg/detectors/larksuite/larksuite.go
Miccah ad2f9a4b98 Populate SecretParts on all detectors (#4919)
* Populate SecretParts on single-part detectors

Adds SecretParts: map[string]string{"key": <secret>} to every detector
package that constructs detectors.Result with a single captured secret
value. This is the single-part half of the SecretParts migration (the
linter's common case, ~695 packages).

* Populate SecretParts on multi-part detectors
2026-04-24 08:32:51 -07:00

294 lines
8.2 KiB
Go

package larksuite
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"path"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
"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
}
// Check that the LarkSuite scanner implements the SecretScanner interface at compile time.
var _ detectors.Detector = Scanner{}
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)
type tokenType string
const (
TenantAccessToken tokenType = "Tenant Access Token"
UserAccessToken tokenType = "User Access Token"
AppAccessToken tokenType = "App Access Token"
)
var (
defaultClient = common.SaneHttpClient()
tokenPats = map[tokenType]*regexp.Regexp{
TenantAccessToken: regexp.MustCompile(detectors.PrefixRegex([]string{"lark", "larksuite", "tenant"}) + `(?:^|[^-])\b(t-[a-z0-9A-Z_.]{14,50})\b(?:[^-]|$)`),
UserAccessToken: regexp.MustCompile(detectors.PrefixRegex([]string{"lark", "larksuite", "user"}) + `(?:^|[^-])\b(u-[a-z0-9A-Z_.]{14,50})\b(?:[^-]|$)`),
AppAccessToken: regexp.MustCompile(detectors.PrefixRegex([]string{"lark", "larksuite", "app"}) + `(?:^|[^-])\b(a-[a-z0-9A-Z_.]{14,50})\b(?:[^-]|$)`),
}
verificationUrls = map[tokenType]string{
TenantAccessToken: "https://open.larksuite.com/open-apis/tenant/v2/tenant/query",
UserAccessToken: "https://open.larksuite.com/open-apis/authen/v1/user_info",
AppAccessToken: "https://open.larksuite.com/open-apis/calendar/v4/calendars",
}
)
// Keywords are used for efficiently pre-filtering chunks.
// Use identifiers in the secret preferably, or the provider name.
func (s Scanner) Keywords() []string {
return []string{"lark", "larksuite"}
}
func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.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)
for key, tokenPat := range tokenPats {
uniqueMatches := make(map[string]struct{})
for _, match := range tokenPat.FindAllStringSubmatch(dataStr, -1) {
uniqueMatches[match[1]] = struct{}{}
}
for token := range uniqueMatches {
s1 := detectors.Result{
DetectorType: detector_typepb.DetectorType_LarkSuite,
Raw: []byte(token),
SecretParts: map[string]string{"key": token},
}
s1.ExtraData = map[string]string{
"token_type": string(key),
}
if verify {
client := s.client
if s.client == nil {
client = defaultClient
}
var (
isVerified bool
err error
)
isVerified, err = verifyAccessToken(ctx, client, verificationUrls[key], token)
s1.Verified = isVerified
s1.SetVerificationError(err, token)
}
results = append(results, s1)
}
}
return results, nil
}
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
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
// 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:
return false, fmt.Errorf("unexpected status code: %d", res.StatusCode)
}
}
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": {},
}