Files
trufflehog/pkg/analyzer/analyzers/launchdarkly/launchdarkly.go
John Elliott 761be8877b Add AnalysisError type and wrap all analyzer error paths (#4779)
* Add AnalysisError type and AnalysisErrorInfo interface

Introduce a shared error type that provides structured metadata
(analyzer type, operation, service, resource) for analysis failures.
This allows the scanner to extract context from errors without
depending on concrete types.

* Wrap errors in simple API analyzers with AnalysisError

Batch A: Airbrake, Anthropic, Asana, DigitalOcean, DockerHub,
ElevenLabs, Fastly, Groq, HuggingFace, Mailchimp, Mailgun, Mux,
Netlify, Ngrok, Notion, OpenAI, Opsgenie, Posthog, Postman,
Sendgrid, Sourcegraph.

Wraps credential validation errors with operation
"validate_credentials" and AnalyzePermissions errors with
operation "analyze_permissions".

* Wrap errors in remaining analyzers with AnalysisError (Batches B-E)

Batch B (OAuth/multi-credential): airtableoauth, airtablepat, datadog,
dropbox, figma, launchdarkly, plaid
Batch C (Complex): bitbucket, databricks, github, gitlab, jira, monday,
planetscale, shopify, slack, square, stripe, twilio
Batch D (Database): mysql, postgres (service: Database)
Batch E (PrivateKey): privatekey (service: crypto)

* Use Type().String() and constants for NewAnalysisError calls

Address PR feedback: replace hardcoded analyzer type strings with
a.Type().String() and replace raw operation/service strings with
package-level constants (OperationValidateCredentials,
OperationAnalyzePermissions, ServiceAPI, ServiceConfig, etc.).

* Omit empty resource parenthetical from AnalysisError messages

Conditionally include "(resource: ...)" only when non-empty,
avoiding cluttered messages like "... (resource: ): ..." that
appear for the majority of analyzers that don't set a resource.

* Wrap no-data error path in GitHub analyzer with AnalysisError
2026-04-14 17:18:48 -07:00

222 lines
7.2 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go launchdarkly
package launchdarkly
import (
"errors"
"fmt"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (a Analyzer) Type() analyzers.AnalyzerType {
return analyzers.AnalyzerTypeLaunchDarkly
}
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
// check if the `key` exist in the credentials info
key, exist := credInfo["key"]
if !exist {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("key not found in credentials info"))
}
if isSDKKey(key) {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationValidateCredentials, analyzers.ServiceConfig, "", errors.New("sdk keys cannot be analyzed"))
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, analyzers.NewAnalysisError(a.Type().String(), analyzers.OperationAnalyzePermissions, analyzers.ServiceAPI, "", err)
}
return secretInfoToAnalyzerResult(info), nil
}
func AnalyzeAndPrintPermissions(cfg *config.Config, token string) {
if isSDKKey(token) {
color.Yellow("\n[!] The Provided key is an SDK Key. SDK Keys are sensitive but used to configure LaunchDarkly SDKs")
color.Green("\n[i] Docs: https://launchdarkly.com/docs/home/account/environment/settings#copy-and-reset-sdk-credentials-for-an-environment")
return
}
info, err := AnalyzePermissions(cfg, token)
if err != nil {
// just print the error in cli and continue as a partial success
color.Red("[x] Error : %s", err.Error())
}
if info == nil {
color.Red("[x] Error : %s", "No information found")
return
}
color.Green("[i] Valid LaunchDarkly Token\n")
printUser(info.User)
printPermissionsType(info.User.Token)
printResources(info.Resources)
color.Yellow("\n[!] Expires: Never")
}
// AnalyzePermissions will collect all the scopes assigned to token along with resource it can access
func AnalyzePermissions(cfg *config.Config, token string) (*SecretInfo, error) {
// create the http client
client := analyzers.NewAnalyzeClient(cfg)
var secretInfo = &SecretInfo{}
// capture user information in secretInfo
if err := CaptureUserInformation(client, token, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch caller identity: %v", err)
}
// capture resources in secretInfo
if err := CaptureResources(client, token, secretInfo); err != nil {
return nil, fmt.Errorf("failed to fetch resources: %v", err)
}
return secretInfo, nil
}
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeLaunchDarkly,
Metadata: map[string]any{},
Bindings: make([]analyzers.Binding, 0),
}
// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
binding := analyzers.Binding{
Resource: *secretInfoResourceToAnalyzerResource(resource),
Permission: analyzers.Permission{
Value: getPermissionType(info.User.Token),
},
}
if resource.ParentResource != nil {
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.ParentResource)
}
result.Bindings = append(result.Bindings, binding)
}
return &result
}
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
func secretInfoResourceToAnalyzerResource(resource Resource) *analyzers.Resource {
analyzerRes := analyzers.Resource{
FullyQualifiedName: resource.ID,
Name: resource.Name,
Type: resource.Type,
Metadata: map[string]any{},
}
for key, value := range resource.MetaData {
analyzerRes.Metadata[key] = value
}
return &analyzerRes
}
// getPermissionType return what type of permission is assigned to token
func getPermissionType(token Token) string {
switch {
case token.Role != "":
return token.Role
case token.hasInlineRole():
return "Inline Policy"
case token.hasCustomRoles():
return "Custom Roles"
default:
return ""
}
}
// printUser print User information from secret info to cli
func printUser(user User) {
// print caller information
color.Green("\n[i] User Information:")
callerTable := table.NewWriter()
callerTable.SetOutputMirror(os.Stdout)
callerTable.AppendHeader(table.Row{"Account ID", "Member ID", "Name", "Email", "Role"})
callerTable.AppendRow(table.Row{color.GreenString(user.AccountID), color.GreenString(user.MemberID),
color.GreenString(user.Name), color.GreenString(user.Email), color.GreenString(user.Role)})
callerTable.Render()
// print token information
color.Green("\n[i] Token Information")
tokenTable := table.NewWriter()
tokenTable.SetOutputMirror(os.Stdout)
tokenTable.AppendHeader(table.Row{"ID", "Name", "Role", "Is Service Token", "Default API Version",
"No of Custom Roles Assigned", "Has Inline Policy"})
tokenTable.AppendRow(table.Row{color.GreenString(user.Token.ID), color.GreenString(user.Token.Name), color.GreenString(user.Token.Role),
color.GreenString(fmt.Sprintf("%t", user.Token.IsServiceToken)), color.GreenString(fmt.Sprintf("%d", user.Token.APIVersion)),
color.GreenString(fmt.Sprintf("%d", len(user.Token.CustomRoles))), color.GreenString(fmt.Sprintf("%t", user.Token.hasInlineRole()))})
tokenTable.Render()
// print custom roles information
if !user.Token.hasCustomRoles() {
return
}
// print token information
color.Green("\n[i] Custom Roles Assigned to Token")
rolesTable := table.NewWriter()
rolesTable.SetOutputMirror(os.Stdout)
rolesTable.AppendHeader(table.Row{"ID", "Key", "Name", "Base Permission", "Assigned to members", "Assigned to teams"})
for _, customRole := range user.Token.CustomRoles {
rolesTable.AppendRow(table.Row{color.GreenString(customRole.ID), color.GreenString(customRole.Key), color.GreenString(customRole.Name),
color.GreenString(customRole.BasePermission), color.GreenString(fmt.Sprintf("%d", customRole.AssignedToMembers)),
color.GreenString(fmt.Sprintf("%d", customRole.AssignedToTeams))})
}
rolesTable.Render()
}
// printPermissionsType print permissions type token has
func printPermissionsType(token Token) {
// print permission type. It can be either admin, writer, reader or has inline policy or any custom roles assigned
color.Green("\n[i] Permission Type: %s", getPermissionType(token))
}
func printResources(resources []Resource) {
// print resources
color.Green("\n[i] Resources:")
callerTable := table.NewWriter()
callerTable.SetOutputMirror(os.Stdout)
callerTable.AppendHeader(table.Row{"Name", "Type"})
for _, resource := range resources {
callerTable.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
}
callerTable.Render()
}
// isSDKKey check if the key provided is an SDK Key or not
func isSDKKey(key string) bool {
return strings.HasPrefix(key, "sdk-")
}