//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-") }