* enable errcheck and staticcheck for golangci-lint v2 and resolve all issues * skip lint on intentional reference of deprecated DetectorType values
470 lines
12 KiB
Go
470 lines
12 KiB
Go
package elevenlabs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/google/uuid"
|
|
"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
|
|
}
|
|
|
|
// SecretInfo hold information about key
|
|
type SecretInfo struct {
|
|
User User // the owner of key
|
|
Valid bool
|
|
Reference string
|
|
Permissions []string // list of Permissions assigned to the key
|
|
ElevenLabsResources []ElevenLabsResource // list of resources the key has access to
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// AppendPermission safely append new permission to secret info permissions list.
|
|
func (s *SecretInfo) AppendPermission(perm string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.Permissions = append(s.Permissions, perm)
|
|
}
|
|
|
|
// HasPermission safely read secret info permission list to check if passed permission exist in the list.
|
|
func (s *SecretInfo) HasPermission(perm Permission) bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
permissionString, _ := perm.ToString()
|
|
|
|
return slices.Contains(s.Permissions, permissionString)
|
|
}
|
|
|
|
// AppendResource safely append new resource to secret info elevenlabs resource list.
|
|
func (s *SecretInfo) AppendResource(resource ElevenLabsResource) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
s.ElevenLabsResources = append(s.ElevenLabsResources, resource)
|
|
}
|
|
|
|
// User hold the information about user to whom the key belongs to
|
|
type User struct {
|
|
ID string
|
|
Name string
|
|
SubscriptionTier string
|
|
SubscriptionStatus string
|
|
}
|
|
|
|
// ElevenLabsResource hold information about the elevenlabs resource the key has access
|
|
type ElevenLabsResource struct {
|
|
ID string
|
|
Name string
|
|
Type string
|
|
Metadata map[string]string
|
|
Permission string
|
|
}
|
|
|
|
func (a Analyzer) Type() analyzers.AnalyzerType {
|
|
return analyzers.AnalyzerTypeElevenLabs
|
|
}
|
|
|
|
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"))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// AnalyzePermissions check if key is valid and analyzes the permission for the key
|
|
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
|
|
// create http client
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
|
|
var secretInfo = &SecretInfo{}
|
|
|
|
// fetch user information using the key
|
|
user, err := fetchUser(client, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secretInfo.Valid = true
|
|
|
|
// if user is not nil, that means the key has user read permission. Set the user information in secret info user
|
|
// user can only be nil when the key is valid but it does not have a user read permission
|
|
if user != nil {
|
|
elevenLabsUserToSecretInfoUser(*user, secretInfo)
|
|
}
|
|
|
|
// get elevenlabs resources with permissions
|
|
if err := getElevenLabsResources(client, key, secretInfo); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return secretInfo, nil
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
|
info, err := AnalyzePermissions(cfg, key)
|
|
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
|
|
}
|
|
|
|
if info.Valid {
|
|
color.Green("[!] Valid ElevenLabs API key\n\n")
|
|
// print user information
|
|
printUser(info.User)
|
|
// print permissions
|
|
printPermissions(info.Permissions)
|
|
// print resources
|
|
printElevenLabsResources(info.ElevenLabsResources)
|
|
|
|
color.Yellow("\n[i] Expires: Never")
|
|
}
|
|
}
|
|
|
|
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
|
|
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
|
|
if info == nil {
|
|
return nil
|
|
}
|
|
|
|
result := analyzers.AnalyzerResult{
|
|
AnalyzerType: analyzers.AnalyzerTypeElevenLabs,
|
|
Metadata: map[string]any{},
|
|
Bindings: make([]analyzers.Binding, 0),
|
|
}
|
|
|
|
// for resources to be uniquely identified, we need a unique id to be appended in resource fully qualified name
|
|
uniqueId := info.User.ID
|
|
if uniqueId == "" {
|
|
uniqueId = uuid.NewString()
|
|
}
|
|
|
|
// extract information from resource to create bindings and append to result bindings
|
|
for _, resource := range info.ElevenLabsResources {
|
|
// if resource has permission it is binded resource
|
|
if resource.Permission != "" {
|
|
binding := analyzers.Binding{
|
|
Resource: analyzers.Resource{
|
|
Name: resource.Name,
|
|
FullyQualifiedName: fmt.Sprintf("%s/%s/%s", uniqueId, resource.Type, resource.ID), // e.g: <user_id>/Model/eleven_flash_v2_5
|
|
Type: resource.Type,
|
|
Metadata: map[string]any{}, // to avoid panic
|
|
},
|
|
Permission: analyzers.Permission{
|
|
Value: resource.Permission,
|
|
},
|
|
}
|
|
|
|
for key, value := range resource.Metadata {
|
|
binding.Resource.Metadata[key] = value
|
|
}
|
|
|
|
result.Bindings = append(result.Bindings, binding)
|
|
} else {
|
|
// if resource is missing permission it is an unbounded resource
|
|
unboundedResource := analyzers.Resource{
|
|
Name: resource.Name,
|
|
FullyQualifiedName: fmt.Sprintf("%s/%s/%s", uniqueId, resource.Type, resource.ID),
|
|
Type: resource.Type,
|
|
Metadata: map[string]any{},
|
|
}
|
|
|
|
for key, value := range resource.Metadata {
|
|
unboundedResource.Metadata[key] = value
|
|
}
|
|
|
|
result.UnboundedResources = append(result.UnboundedResources, unboundedResource)
|
|
}
|
|
}
|
|
|
|
result.Metadata["Valid_Key"] = info.Valid
|
|
|
|
return &result
|
|
}
|
|
|
|
// fetchUser fetch elevenlabs user information associated with the key
|
|
func fetchUser(client *http.Client, key string) (*User, error) {
|
|
response, statusCode, err := makeElevenLabsRequest(client, permissionToAPIMap[UserRead], http.MethodGet, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch statusCode {
|
|
case http.StatusOK:
|
|
var user UserResponse
|
|
|
|
if err := json.Unmarshal(response, &user); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &User{
|
|
ID: user.UserID,
|
|
Name: user.FirstName,
|
|
SubscriptionTier: user.Subscription.Tier,
|
|
SubscriptionStatus: user.Subscription.Status,
|
|
}, nil
|
|
case http.StatusUnauthorized:
|
|
var errorResp ErrorResponse
|
|
|
|
if err := json.Unmarshal(response, &errorResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch errorResp.Detail.Status {
|
|
case InvalidAPIKey, NotVerifiable:
|
|
return nil, errors.New("invalid api key")
|
|
case MissingPermissions:
|
|
// key is missing user read permissions but is valid
|
|
return nil, nil
|
|
}
|
|
|
|
return nil, nil
|
|
default:
|
|
return nil, fmt.Errorf("unexpected status code: %d", statusCode)
|
|
}
|
|
}
|
|
|
|
// elevenLabsUserToSecretInfoUser set the elevenlabs user information to secretInfo user
|
|
func elevenLabsUserToSecretInfoUser(user User, secretInfo *SecretInfo) {
|
|
secretInfo.User = user
|
|
// add user read scope to secret info
|
|
secretInfo.Permissions = append(secretInfo.Permissions, PermissionStrings[UserRead])
|
|
// map resource to secret info
|
|
// as user is accessible through a specific permission and has a unique id it is also a resource
|
|
secretInfo.ElevenLabsResources = append(secretInfo.ElevenLabsResources, ElevenLabsResource{
|
|
ID: user.ID,
|
|
Name: user.Name,
|
|
Type: "User",
|
|
Permission: PermissionStrings[UserRead],
|
|
})
|
|
}
|
|
|
|
/*
|
|
getElevenLabsResources gather resources the key can access
|
|
|
|
Note: The permissions in eleven labs is either Read or Read and Write. There is not separate permission for Write.
|
|
*/
|
|
func getElevenLabsResources(client *http.Client, key string, secretInfo *SecretInfo) error {
|
|
var (
|
|
aggregatedErrs = make([]string, 0)
|
|
errChan = make(chan error, 17) // buffer for 17 errors - one per API call
|
|
wg sync.WaitGroup
|
|
)
|
|
|
|
// history
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := getHistory(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
if err := deleteHistory(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// dubbings
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := deleteDubbing(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
// if dubbing write permission was not added
|
|
if !secretInfo.HasPermission(DubbingWrite) {
|
|
if err := getDebugging(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}
|
|
}()
|
|
|
|
// voices
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := getVoices(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
if err := deleteVoice(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// projects
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := getProjects(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
if err := deleteProject(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// pronunciation dictionaries
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := getPronunciationDictionaries(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
if err := removePronunciationDictionariesRule(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// models
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := getModels(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// audio native
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := updateAudioNativeProject(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// workspace
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := deleteInviteFromWorkspace(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// speech
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := textToSpeech(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
|
|
// voice changer
|
|
if err := speechToSpeech(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// audio isolation
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
if err := audioIsolation(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// agent
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
|
|
// each agent can have a conversations which we get inside this function
|
|
if err := getAgents(client, key, secretInfo); err != nil {
|
|
errChan <- err
|
|
}
|
|
}()
|
|
|
|
// wait for all API calls to finish
|
|
wg.Wait()
|
|
close(errChan)
|
|
|
|
// collect all errors
|
|
for err := range errChan {
|
|
aggregatedErrs = append(aggregatedErrs, err.Error())
|
|
}
|
|
|
|
if len(aggregatedErrs) > 0 {
|
|
return errors.New(strings.Join(aggregatedErrs, ", "))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// cli print functions
|
|
func printUser(user User) {
|
|
color.Green("\n[i] User:")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"ID", "Name", "Subscription Tier", "Subscription Status"})
|
|
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.SubscriptionTier), color.GreenString(user.SubscriptionStatus)})
|
|
t.Render()
|
|
}
|
|
|
|
func printPermissions(permissions []string) {
|
|
color.Yellow("[i] Permissions:")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Permission"})
|
|
for _, permission := range permissions {
|
|
t.AppendRow(table.Row{color.GreenString(permission)})
|
|
}
|
|
t.Render()
|
|
}
|
|
|
|
func printElevenLabsResources(resources []ElevenLabsResource) {
|
|
color.Green("\n[i] Resources:")
|
|
t := table.NewWriter()
|
|
t.SetOutputMirror(os.Stdout)
|
|
t.AppendHeader(table.Row{"Resource Type", "Resource ID", "Resource Name", "Permission"})
|
|
for _, resource := range resources {
|
|
t.AppendRow(table.Row{color.GreenString(resource.Type), color.GreenString(resource.ID), color.GreenString(resource.Name), color.GreenString(resource.Permission)})
|
|
}
|
|
t.Render()
|
|
}
|