* 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
294 lines
8.2 KiB
Go
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": {},
|
|
}
|