diff --git a/pkg/analyzer/analyzers/analyzers.go b/pkg/analyzer/analyzers/analyzers.go index 505a62b40794..809058a2ce35 100644 --- a/pkg/analyzer/analyzers/analyzers.go +++ b/pkg/analyzer/analyzers/analyzers.go @@ -65,6 +65,7 @@ const ( AnalyzerTypeAsana AnalyzerTypeBitbucket AnalyzerTypeDockerHub + AnalyzerTypeElevenLabs AnalyzerTypeGitHub AnalyzerTypeGitLab AnalyzerTypeHuggingFace @@ -97,7 +98,9 @@ var analyzerTypeStrings = map[AnalyzerType]string{ AnalyzerAnthropic: "Anthropic", AnalyzerTypeAsana: "Asana", AnalyzerTypeBitbucket: "Bitbucket", + AnalyzerTypeDigitalOcean: "DigitalOcean", AnalyzerTypeDockerHub: "DockerHub", + AnalyzerTypeElevenLabs: "ElevenLabs", AnalyzerTypeGitHub: "GitHub", AnalyzerTypeGitLab: "GitLab", AnalyzerTypeHuggingFace: "HuggingFace", @@ -117,7 +120,6 @@ var analyzerTypeStrings = map[AnalyzerType]string{ AnalyzerTypeTwilio: "Twilio", AnalyzerTypePrivateKey: "PrivateKey", AnalyzerTypeNotion: "Notion", - AnalyzerTypeDigitalOcean: "DigitalOcean", // Add new mappings here } diff --git a/pkg/analyzer/analyzers/elevenlabs/elevenlabs.go b/pkg/analyzer/analyzers/elevenlabs/elevenlabs.go new file mode 100644 index 000000000000..472498c965a0 --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/elevenlabs.go @@ -0,0 +1,468 @@ +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 +} + +// AppendPermissions 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, errors.New("key not found in credentials info") + } + + info, err := AnalyzePermissions(a.Cfg, key) + if err != nil { + return nil, 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: /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 + } + + if errorResp.Detail.Status == InvalidAPIKey || errorResp.Detail.Status == NotVerifiable { + return nil, errors.New("invalid api key") + } else if errorResp.Detail.Status == 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() +} diff --git a/pkg/analyzer/analyzers/elevenlabs/elevenlabs_test.go b/pkg/analyzer/analyzers/elevenlabs/elevenlabs_test.go new file mode 100644 index 000000000000..772542177848 --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/elevenlabs_test.go @@ -0,0 +1,102 @@ +package elevenlabs + +import ( + _ "embed" + "encoding/json" + "sort" + "testing" + "time" + + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config" + "github.com/trufflesecurity/trufflehog/v3/pkg/common" + "github.com/trufflesecurity/trufflehog/v3/pkg/context" +) + +//go:embed result_output.json +var expectedOutput []byte + +func TestAnalyzer_Analyze(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5") + if err != nil { + t.Fatalf("could not get test secrets from GCP: %s", err) + } + + key := testSecrets.MustGetField("ELEVENLABS") + + tests := []struct { + name string + key string + want []byte // JSON string + wantErr bool + }{ + { + name: "valid ElevenLabs full access key", + key: key, + want: expectedOutput, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := Analyzer{Cfg: &config.Config{}} + got, err := a.Analyze(ctx, map[string]string{"key": tt.key}) + if (err != nil) != tt.wantErr { + t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Bindings need to be in the same order to be comparable + sortBindings(got.Bindings) + + // Marshal the actual result to JSON + gotJSON, err := json.Marshal(got) + if err != nil { + t.Fatalf("could not marshal got to JSON: %s", err) + } + + // Parse the expected JSON string + var wantObj analyzers.AnalyzerResult + if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil { + t.Fatalf("could not unmarshal want JSON string: %s", err) + } + + // Bindings need to be in the same order to be comparable + sortBindings(wantObj.Bindings) + + // Marshal the expected result to JSON (to normalize) + wantJSON, err := json.Marshal(wantObj) + if err != nil { + t.Fatalf("could not marshal want to JSON: %s", err) + } + + // Compare the JSON strings + if string(gotJSON) != string(wantJSON) { + // Pretty-print both JSON strings for easier comparison + var gotIndented, wantIndented []byte + gotIndented, err = json.MarshalIndent(got, "", " ") + if err != nil { + t.Fatalf("could not marshal got to indented JSON: %s", err) + } + wantIndented, err = json.MarshalIndent(wantObj, "", " ") + if err != nil { + t.Fatalf("could not marshal want to indented JSON: %s", err) + } + t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented) + } + }) + } +} + +// Helper function to sort bindings +func sortBindings(bindings []analyzers.Binding) { + sort.SliceStable(bindings, func(i, j int) bool { + if bindings[i].Resource.Name == bindings[j].Resource.Name { + return bindings[i].Permission.Value < bindings[j].Permission.Value + } + return bindings[i].Resource.Name < bindings[j].Resource.Name + }) +} diff --git a/pkg/analyzer/analyzers/elevenlabs/permissions.go b/pkg/analyzer/analyzers/elevenlabs/permissions.go new file mode 100644 index 000000000000..e08445987017 --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/permissions.go @@ -0,0 +1,151 @@ +// Code generated by go generate; DO NOT EDIT. +package elevenlabs + +import "errors" + +type Permission int + +const ( + Invalid Permission = iota + TextToSpeech Permission = iota + SpeechToSpeech Permission = iota + AudioIsolation Permission = iota + DubbingRead Permission = iota + DubbingWrite Permission = iota + ProjectsRead Permission = iota + ProjectsWrite Permission = iota + AudioNativeRead Permission = iota + AudioNativeWrite Permission = iota + PronunciationDictionariesRead Permission = iota + PronunciationDictionariesWrite Permission = iota + VoicesRead Permission = iota + VoicesWrite Permission = iota + ModelsRead Permission = iota + SpeechHistoryRead Permission = iota + SpeechHistoryWrite Permission = iota + UserRead Permission = iota + WorkspaceRead Permission = iota + WorkspaceWrite Permission = iota +) + +var ( + PermissionStrings = map[Permission]string{ + TextToSpeech: "text_to_speech", + SpeechToSpeech: "speech_to_speech", + AudioIsolation: "audio_isolation", + DubbingRead: "dubbing_read", + DubbingWrite: "dubbing_write", + ProjectsRead: "projects_read", + ProjectsWrite: "projects_write", + AudioNativeRead: "audio_native_read", + AudioNativeWrite: "audio_native_write", + PronunciationDictionariesRead: "pronunciation_dictionaries_read", + PronunciationDictionariesWrite: "pronunciation_dictionaries_write", + VoicesRead: "voices_read", + VoicesWrite: "voices_write", + ModelsRead: "models_read", + SpeechHistoryRead: "speech_history_read", + SpeechHistoryWrite: "speech_history_write", + UserRead: "user_read", + WorkspaceRead: "workspace_read", + WorkspaceWrite: "workspace_write", + } + + StringToPermission = map[string]Permission{ + "text_to_speech": TextToSpeech, + "speech_to_speech": SpeechToSpeech, + "audio_isolation": AudioIsolation, + "dubbing_read": DubbingRead, + "dubbing_write": DubbingWrite, + "projects_read": ProjectsRead, + "projects_write": ProjectsWrite, + "audio_native_read": AudioNativeRead, + "audio_native_write": AudioNativeWrite, + "pronunciation_dictionaries_read": PronunciationDictionariesRead, + "pronunciation_dictionaries_write": PronunciationDictionariesWrite, + "voices_read": VoicesRead, + "voices_write": VoicesWrite, + "models_read": ModelsRead, + "speech_history_read": SpeechHistoryRead, + "speech_history_write": SpeechHistoryWrite, + "user_read": UserRead, + "workspace_read": WorkspaceRead, + "workspace_write": WorkspaceWrite, + } + + PermissionIDs = map[Permission]int{ + TextToSpeech: 1, + SpeechToSpeech: 2, + AudioIsolation: 3, + DubbingRead: 4, + DubbingWrite: 5, + ProjectsRead: 6, + ProjectsWrite: 7, + AudioNativeRead: 8, + AudioNativeWrite: 9, + PronunciationDictionariesRead: 10, + PronunciationDictionariesWrite: 11, + VoicesRead: 12, + VoicesWrite: 13, + ModelsRead: 14, + SpeechHistoryRead: 15, + SpeechHistoryWrite: 16, + UserRead: 17, + WorkspaceRead: 18, + WorkspaceWrite: 19, + } + + IdToPermission = map[int]Permission{ + 1: TextToSpeech, + 2: SpeechToSpeech, + 3: AudioIsolation, + 4: DubbingRead, + 5: DubbingWrite, + 6: ProjectsRead, + 7: ProjectsWrite, + 8: AudioNativeRead, + 9: AudioNativeWrite, + 10: PronunciationDictionariesRead, + 11: PronunciationDictionariesWrite, + 12: VoicesRead, + 13: VoicesWrite, + 14: ModelsRead, + 15: SpeechHistoryRead, + 16: SpeechHistoryWrite, + 17: UserRead, + 18: WorkspaceRead, + 19: WorkspaceWrite, + } +) + +// ToString converts a Permission enum to its string representation +func (p Permission) ToString() (string, error) { + if str, ok := PermissionStrings[p]; ok { + return str, nil + } + return "", errors.New("invalid permission") +} + +// ToID converts a Permission enum to its ID +func (p Permission) ToID() (int, error) { + if id, ok := PermissionIDs[p]; ok { + return id, nil + } + return 0, errors.New("invalid permission") +} + +// PermissionFromString converts a string representation to its Permission enum +func PermissionFromString(s string) (Permission, error) { + if p, ok := StringToPermission[s]; ok { + return p, nil + } + return 0, errors.New("invalid permission string") +} + +// PermissionFromID converts an ID to its Permission enum +func PermissionFromID(id int) (Permission, error) { + if p, ok := IdToPermission[id]; ok { + return p, nil + } + return 0, errors.New("invalid permission ID") +} diff --git a/pkg/analyzer/analyzers/elevenlabs/permissions.yaml b/pkg/analyzer/analyzers/elevenlabs/permissions.yaml new file mode 100644 index 000000000000..05977065582f --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/permissions.yaml @@ -0,0 +1,24 @@ +permissions: + - text_to_speech + - speech_to_speech + # - sound_generation + - audio_isolation + # - voice_generation + - dubbing_read + - dubbing_write + - projects_read + - projects_write + - audio_native_read + - audio_native_write + - pronunciation_dictionaries_read + - pronunciation_dictionaries_write + - voices_read + - voices_write + - models_read + # - models_write + - speech_history_read + - speech_history_write + - user_read + # - user_write + - workspace_read + - workspace_write diff --git a/pkg/analyzer/analyzers/elevenlabs/requests.go b/pkg/analyzer/analyzers/elevenlabs/requests.go new file mode 100644 index 000000000000..ea4d3ec54b33 --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/requests.go @@ -0,0 +1,782 @@ +package elevenlabs + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "net/http" + "slices" + "strings" +) + +// permissionToAPIMap contain the API endpoints for each scope/permission +// api docs: https://elevenlabs.io/docs/api-reference/introduction +var permissionToAPIMap = map[Permission]string{ + TextToSpeech: "https://api.elevenlabs.io/v1/text-to-speech/%s", // require voice id + SpeechToSpeech: "https://api.elevenlabs.io/v1/speech-to-speech/%s", // require voice id + AudioIsolation: "https://api.elevenlabs.io/v1/audio-isolation", + DubbingRead: "https://api.elevenlabs.io/v1/dubbing/%s", // require dubbing id + DubbingWrite: "https://api.elevenlabs.io/v1/dubbing/%s", // require dubbing id + ProjectsRead: "https://api.elevenlabs.io/v1/projects", + ProjectsWrite: "https://api.elevenlabs.io/v1/projects/%s", // require project id + AudioNativeWrite: "https://api.elevenlabs.io/v1/audio-native/%s/content", // require project id + PronunciationDictionariesRead: "https://api.elevenlabs.io/v1/pronunciation-dictionaries", + PronunciationDictionariesWrite: "https://api.elevenlabs.io/v1/pronunciation-dictionaries/%s/remove-rules", // require pronunciation dictionary id + VoicesRead: "https://api.elevenlabs.io/v1/voices", + VoicesWrite: "https://api.elevenlabs.io/v1/voices/%s", // require voice id + ModelsRead: "https://api.elevenlabs.io/v1/models", + SpeechHistoryRead: "https://api.elevenlabs.io/v1/history", + SpeechHistoryWrite: "https://api.elevenlabs.io/v1/history/%s", // require history item id + UserRead: "https://api.elevenlabs.io/v1/user", + WorkspaceWrite: "https://api.elevenlabs.io/v1/workspace/invites", +} + +var ( + // not exist key + fakeID = "_thou_shalt_not_exist_" + // error statuses + NotVerifiable = "api_key_not_verifiable" + InvalidAPIKey = "invalid_api_key" + MissingPermissions = "missing_permissions" + DubbingNotFound = "dubbing_not_found" + ProjectNotFound = "project_not_found" + VoiceDoesNotExist = "voice_does_not_exist" + InvalidSubscription = "invalid_subscription" + PronunciationDictionaryNotFound = "pronunciation_dictionary_not_found" + InternalServerError = "internal_server_error" + InvalidProjectID = "invalid_project_id" + ModelNotFound = "model_not_found" + VoiceNotFound = "voice_not_found" + InvalidContent = "invalid_content" +) + +// ErrorResponse is the error response for all APIs +type ErrorResponse struct { + Detail struct { + Status string `json:"status"` + } `json:"detail"` +} + +// UserResponse is the /user API response +type UserResponse struct { + UserID string `json:"user_id"` + FirstName string `json:"first_name"` + Subscription struct { + Tier string `json:"tier"` + Status string `json:"status"` + } `json:"subscription"` +} + +// HistoryResponse is the /history API response +type HistoryResponse struct { + History []struct { + ID string `json:"history_item_id"` + ModelID string `json:"model_id"` + VoiceID string `json:"voice_id"` + } `json:"history"` +} + +// VoiceResponse is the /voices API response +type VoicesResponse struct { + Voices []struct { + ID string `json:"voice_id"` + Name string `json:"name"` + Category string `json:"category"` + } `json:"voices"` +} + +// ProjectsResponse is the /projects API response +type ProjectsResponse struct { + Projects []struct { + ID string `json:"project_id"` + Name string `json:"name"` + State string `json:"state"` + AccessLevel string `json:"access_level"` + } `json:"projects"` +} + +// PronunciationDictionaries is the /pronunciation-dictionaries API response +type PronunciationDictionariesResponse struct { + PronunciationDictionaries []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"pronunciation_dictionaries"` +} + +// Models is the /models API response +type ModelsResponse struct { + ID string `json:"model_id"` + Name string `json:"name"` +} + +// AgentsResponse is the /agents API response +type AgentsResponse struct { + Agents []struct { + ID string `json:"agent_id"` + Name string `json:"name"` + AccessLevel string `json:"access_level"` + } `json:"agents"` +} + +// ConversationResponse is the /conversation API response +type ConversationResponse struct { + Conversations []struct { + AgentID string `json:"agent_id"` + ID string `json:"conversation_id"` + Status string `json:"status"` + } +} + +// getAPIUrl return the API Url mapped to the permission +func getAPIUrl(permission Permission) string { + apiUrl := permissionToAPIMap[permission] + if strings.Contains(apiUrl, "%s") { + return fmt.Sprintf(apiUrl, fakeID) + } + + return apiUrl +} + +// makeElevenLabsRequest send the API request to passed url with passed key as API Key and return response body and status code +func makeElevenLabsRequest(client *http.Client, url, method, key string) ([]byte, int, error) { + // create request + req, err := http.NewRequest(method, url, http.NoBody) + if err != nil { + return nil, 0, err + } + + // add key in the header + req.Header.Add("xi-api-key", key) + + resp, err := client.Do(req) + if err != nil { + return nil, 0, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + /* + the reason to translate body to byte and does not directly return http.Response + is if we return http.Response we cannot close the body in defer. If we do we will get an error + when reading body outside this function + */ + responseBodyByte, err := io.ReadAll(resp.Body) + if err != nil { + return nil, 0, err + } + + return responseBodyByte, resp.StatusCode, nil +} + +// makeElevenLabsRequestWithPayload sends a POST/PATCH API request to the passed URL with the given key as the API Key +// and an optional payload. It returns the response body and status code. +func makeElevenLabsRequestWithPayload(client *http.Client, url, method, contentType, key string, payload []byte) ([]byte, int, error) { + // Create request with payload + req, err := http.NewRequest(method, url, bytes.NewBuffer(payload)) + if err != nil { + return nil, 0, err + } + + // Add headers + req.Header.Add("xi-api-key", key) + req.Header.Add("Content-Type", contentType) + + // Send the request + resp, err := client.Do(req) + if err != nil { + return nil, 0, err + } + + // ensure the response body is properly closed + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + // read the response body + responseBodyByte, err := io.ReadAll(resp.Body) + if err != nil { + return nil, 0, err + } + + return responseBodyByte, resp.StatusCode, nil +} + +// getHistory get history item using the key passed and add them to secret info +func getHistory(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(SpeechHistoryRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var history HistoryResponse + + if err := json.Unmarshal(response, &history); err != nil { + return err + } + + // add history read scope to secret info + secretInfo.AppendPermission(PermissionStrings[SpeechHistoryRead]) + // map resource to secret info + for _, historyItem := range history.History { + secretInfo.AppendResource(ElevenLabsResource{ + ID: historyItem.ID, + Name: "", // no name + Type: "History", + Permission: PermissionStrings[SpeechHistoryRead], + }) + } + + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking history read scope", statusCode) + } +} + +// deleteHistory try to delete a history item. The item must not exist. +func deleteHistory(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(SpeechHistoryWrite), http.MethodDelete, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusInternalServerError: + // for some reason if we send fake id and token has the permission, the history api return 500 error instead of 404 + // issue opened in elevenlabs-docs: https://github.com/elevenlabs/elevenlabs-docs/issues/649 + return handleErrorStatus(response, PermissionStrings[SpeechHistoryWrite], secretInfo, InternalServerError) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking history write scope", statusCode) + } +} + +// deleteDubbing try to delete a dubbing. The item must not exist. +func deleteDubbing(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(DubbingWrite), http.MethodDelete, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusNotFound: + // as we send fake id, if permission is assigned to token we must get 404 dubbing not found + if err := handleErrorStatus(response, PermissionStrings[DubbingWrite], secretInfo, DubbingNotFound); err != nil { + return err + } + + // add read scope of dubbing to avoid get dubbing api call + secretInfo.AppendPermission(PermissionStrings[DubbingRead]) + + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking dubbing write scope", statusCode) + } +} + +// getDebugging try to get a dubbing. The item must not exist. +func getDebugging(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(DubbingRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusNotFound: + // as we send fake id, if permission is assigned to token we must get 404 dubbing not found + return handleErrorStatus(response, PermissionStrings[DubbingRead], secretInfo, DubbingNotFound) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking dubbing read scope", statusCode) + } +} + +// getVoices get list of voices using the key passed and add them to secret info +func getVoices(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(VoicesRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var voices VoicesResponse + + if err := json.Unmarshal(response, &voices); err != nil { + return err + } + + // add voices read scope to secret info + secretInfo.AppendPermission(PermissionStrings[VoicesRead]) + // map resource to secret info + for _, voice := range voices.Voices { + secretInfo.AppendResource(ElevenLabsResource{ + ID: voice.ID, + Name: voice.Name, + Type: "Voice", + Permission: PermissionStrings[VoicesRead], + Metadata: map[string]string{ + "category": voice.Category, + }, + }) + } + + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking voice read scope", statusCode) + } +} + +// deleteVoice try to delete a voice. The item must not exist. +func deleteVoice(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(VoicesWrite), http.MethodDelete, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + // if permission was assigned to scope we should get 400 error with voice not found status + return handleErrorStatus(response, PermissionStrings[VoicesWrite], secretInfo, VoiceDoesNotExist) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking voice write scope", statusCode) + } +} + +// getProjects get list of projects using the key passed and add them to secret info +func getProjects(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ProjectsRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var projects ProjectsResponse + + if err := json.Unmarshal(response, &projects); err != nil { + return err + } + + // add project read scope to secret info + secretInfo.AppendPermission(PermissionStrings[ProjectsRead]) + // map resource to secret info + for _, project := range projects.Projects { + secretInfo.AppendResource(ElevenLabsResource{ + ID: project.ID, + Name: project.Name, + Type: "Project", + Permission: PermissionStrings[ProjectsRead], + Metadata: map[string]string{ + "state": project.State, + "access level": project.AccessLevel, // access level of project + }, + }) + } + + return nil + case http.StatusForbidden: + // if token has the permission but trail is free, projects are not accessible + return handleErrorStatus(response, PermissionStrings[ProjectsRead], secretInfo, InvalidSubscription) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking projects read scope", statusCode) + } +} + +// deleteProject try to delete a project. The item must not exist. +func deleteProject(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ProjectsWrite), http.MethodDelete, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + // if permission was assigned to token we should get 400 error with project not found status + return handleErrorStatus(response, PermissionStrings[ProjectsWrite], secretInfo, ProjectNotFound) + case http.StatusForbidden: + // if token has the permission but trail is free, projects are not accessible + return handleErrorStatus(response, PermissionStrings[ProjectsWrite], secretInfo, InvalidSubscription) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking project write scope", statusCode) + } +} + +// getPronunciationDictionaries get list of pronunciation dictionaries using the key passed and add them to secret info +func getPronunciationDictionaries(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(PronunciationDictionariesRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var PDs PronunciationDictionariesResponse + + if err := json.Unmarshal(response, &PDs); err != nil { + return err + } + + // add voices read scope to secret info + secretInfo.AppendPermission(PermissionStrings[PronunciationDictionariesRead]) + // map resource to secret info + for _, pd := range PDs.PronunciationDictionaries { + secretInfo.AppendResource(ElevenLabsResource{ + ID: pd.ID, + Name: pd.Name, + Type: "Pronunciation Dictionary", + Permission: PermissionStrings[PronunciationDictionariesRead], + }) + } + + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking pronunciation dictionaries read scope", statusCode) + } +} + +// removePronunciationDictionariesRule try to remove a rule from pronunciation dictionaries. The item must not exist. +func removePronunciationDictionariesRule(client *http.Client, key string, secretInfo *SecretInfo) error { + // send empty list of rule strings + payload := map[string]interface{}{ + "rule_strings": []string{""}, + } + + payloadBytes, _ := json.Marshal(payload) + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(PronunciationDictionariesWrite), http.MethodPost, + "application/json", key, payloadBytes) + if err != nil { + return err + } + + switch statusCode { + case http.StatusNotFound: + // if permission was assigned to token we should get 404 error with pronunciation_dictionary_not_found status + return handleErrorStatus(response, PermissionStrings[PronunciationDictionariesWrite], secretInfo, PronunciationDictionaryNotFound) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking pronunciation dictionary write scope", statusCode) + } +} + +// getModels list models using the key passed and add them to secret info +func getModels(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, getAPIUrl(ModelsRead), http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var models []ModelsResponse + + if err := json.Unmarshal(response, &models); err != nil { + return err + } + + // add models read scope to secret info + secretInfo.AppendPermission(PermissionStrings[ModelsRead]) + // map resource to secret info + for _, model := range models { + secretInfo.AppendResource(ElevenLabsResource{ + ID: model.ID, + Name: model.Name, + Type: "Model", + Permission: PermissionStrings[ModelsRead], + }) + } + + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode) + } +} + +// updateAudioNativeProject try to update a project content. The item must not exist. +func updateAudioNativeProject(client *http.Client, key string, secretInfo *SecretInfo) error { + // create a buffer to hold the multipart form data + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // add required fields to multipart form body + _ = writer.WriteField("auto_convert", "false") + _ = writer.WriteField("auto_publish", "false") + // close the writer + _ = writer.Close() + + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(AudioNativeWrite), http.MethodPost, + writer.FormDataContentType(), key, body.Bytes()) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + // if the permission is assigned to token, the api should return 400 with invalid project id + if err := handleErrorStatus(response, PermissionStrings[AudioNativeWrite], secretInfo, InvalidProjectID); err != nil { + return err + } + + // add read permission as no separate API exist to check read audio native permission + secretInfo.AppendPermission(PermissionStrings[AudioNativeRead]) + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking audio native write scope", statusCode) + } +} + +// deleteInviteFromWorkspace try to remove a invite from workspace. The item must not exist. +func deleteInviteFromWorkspace(client *http.Client, key string, secretInfo *SecretInfo) error { + // send fake email in payload + payload := map[string]interface{}{ + "email": fakeID + "@example.com", + } + + payloadBytes, _ := json.Marshal(payload) + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(WorkspaceWrite), http.MethodDelete, + "application/json", key, payloadBytes) + if err != nil { + return err + } + + switch statusCode { + case http.StatusInternalServerError: + // for some reason if we send fake email and token has the permission, the workspace invite api return 500 error instead of 404 + if err := handleErrorStatus(response, PermissionStrings[WorkspaceWrite], secretInfo, InternalServerError); err != nil { + return err + } + + // add read permission as no separate API exist to check workspace read permission + secretInfo.AppendPermission(PermissionStrings[WorkspaceRead]) + return nil + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking workspace write scope", statusCode) + } +} + +// textToSpeech try to convert text to speech. The model id and voice id is fake so it actually never happens. +func textToSpeech(client *http.Client, key string, secretInfo *SecretInfo) error { + // send fake model id in payload + payload := map[string]interface{}{ + "text": "This is trufflehog trying to check text to speech permission of the token", + "model_id": fakeID, + } + + payloadBytes, _ := json.Marshal(payload) + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(TextToSpeech), http.MethodPost, + "application/json", key, payloadBytes) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + // if permission is assigned to token, error status will be either model not found or voice not found as we sent both fake ;) + return handleErrorStatus(response, PermissionStrings[TextToSpeech], secretInfo, ModelNotFound, VoiceNotFound) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking text to speech scope", statusCode) + } +} + +// speechToSpeech try to change a voice in speech. The model id and voice id is fake so it actually never happens. +func speechToSpeech(client *http.Client, key string, secretInfo *SecretInfo) error { + // create a buffer to hold the multipart form data + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // add required fields to multipart form body + _ = writer.WriteField("model_id", fakeID) + _ = writer.WriteField("seed", "1") + _ = writer.WriteField("remove_background_noise", "false") + audio, _ := writer.CreateFormFile("audio", "") + _, _ = audio.Write([]byte("This is example fake audio for api call")) + // close the writer + _ = writer.Close() + + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(SpeechToSpeech), http.MethodPost, + writer.FormDataContentType(), key, body.Bytes()) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + return handleErrorStatus(response, PermissionStrings[SpeechToSpeech], secretInfo, InvalidContent) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking speech to speech scope", statusCode) + } +} + +// audioIsolation try to remove background noise from a voice. The file will be corrupted so it should return an error. +func audioIsolation(client *http.Client, key string, secretInfo *SecretInfo) error { + // create a buffer to hold the multipart form data + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + audio, _ := writer.CreateFormFile("audio", "") + _, _ = audio.Write([]byte("This is example fake audio for api call")) + // close the writer + _ = writer.Close() + + response, statusCode, err := makeElevenLabsRequestWithPayload(client, getAPIUrl(AudioIsolation), http.MethodPost, + writer.FormDataContentType(), key, body.Bytes()) + if err != nil { + return err + } + + switch statusCode { + case http.StatusBadRequest: + return handleErrorStatus(response, PermissionStrings[AudioIsolation], secretInfo, InvalidContent) + case http.StatusUnauthorized: + return handleErrorStatus(response, "", secretInfo, MissingPermissions) + default: + return fmt.Errorf("unexpected status code: %d while checking audio isolation speech scope", statusCode) + } +} + +/* +getAgents get all user agents which are not bound with any permission +call APIs in pattern: agents->conversation +*/ +func getAgents(client *http.Client, key string, secretInfo *SecretInfo) error { + response, statusCode, err := makeElevenLabsRequest(client, "https://api.elevenlabs.io/v1/convai/agents", http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var agents AgentsResponse + + if err := json.Unmarshal(response, &agents); err != nil { + return err + } + + // map resource to secret info + for _, agent := range agents.Agents { + resource := ElevenLabsResource{ + ID: agent.ID, + Name: agent.Name, + Type: "Agent", + Permission: "", // not binded with any permission + Metadata: map[string]string{ + "access level": agent.AccessLevel, + }, + } + secretInfo.AppendResource(resource) + // get agent conversations + if err := getConversation(client, key, agent.ID, secretInfo); err != nil { + return err + } + } + + return nil + default: + return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode) + } +} + +// getConversation list all agent conversations using the key and agentID passed and add them to secret info +func getConversation(client *http.Client, key, agentID string, secretInfo *SecretInfo) error { + apiUrl := fmt.Sprintf("https://api.elevenlabs.io/v1/convai/conversations?agent_id=%s", agentID) + response, statusCode, err := makeElevenLabsRequest(client, apiUrl, http.MethodGet, key) + if err != nil { + return err + } + + switch statusCode { + case http.StatusOK: + var conversations ConversationResponse + + if err := json.Unmarshal(response, &conversations); err != nil { + return err + } + + // map resource to secret info + for _, conversation := range conversations.Conversations { + secretInfo.AppendResource(ElevenLabsResource{ + ID: conversation.ID, + Name: "", // no name + Type: "Conversation", + Permission: "", // not binded with any permission + Metadata: map[string]string{ + "status": conversation.Status, + }, + }) + } + + return nil + default: + return fmt.Errorf("unexpected status code: %d while checking models read scope", statusCode) + } +} + +// handleErrorStatus handle error response, check if expected error status is in the response and add require permission to secret info +// this is used in case where we expect error response with specific status mostly in write calls +func handleErrorStatus(response []byte, permissionToAdd string, secretInfo *SecretInfo, expectedErrStatuses ...string) error { + // check if status in response is what is expected to be + ok, err := checkErrorStatus(response, expectedErrStatuses...) + if err != nil { + return err + } + + // if permission to add was passed and it was expected error status add the permission + if permissionToAdd != "" && ok { + secretInfo.AppendPermission(permissionToAdd) + } else if permissionToAdd != "" && !ok { + // if permission to add was passed and it was unexpected error status - return error + return errors.New("unexpected error response") + } + + return nil +} + +// checkErrorStatus check if any of expected error status exist in actual API error response +func checkErrorStatus(response []byte, expectedStatuses ...string) (bool, error) { + var errorResp ErrorResponse + + if err := json.Unmarshal(response, &errorResp); err != nil { + return false, err + } + + if slices.Contains(expectedStatuses, errorResp.Detail.Status) { + return true, nil + } + + return false, nil +} diff --git a/pkg/analyzer/analyzers/elevenlabs/result_output.json b/pkg/analyzer/analyzers/elevenlabs/result_output.json new file mode 100644 index 000000000000..aeef5b7633bf --- /dev/null +++ b/pkg/analyzer/analyzers/elevenlabs/result_output.json @@ -0,0 +1,439 @@ +{ + "AnalyzerType": 6, + "Bindings": [ + { + "Resource": { + "Name": "Ahmed", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/User/b9Rou9mHDmTYd8cdWkg2Yk4P2lq1", + "Type": "User", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "user_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Alice", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/Xb7hH8MSUJpSbSDYk0k2", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Aria", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/9BWtsMINqrJLrRacOk9x", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Bill", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/pqHfZKP75CvOlQylNhV4", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Brian", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/nPczCjzI2devNBz1zQrb", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Callum", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/N2lVS1w4EtoT3dr4eOWO", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Charlie", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/IKne3meq5aSn9XLyUdCD", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Charlotte", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/XB0fDUnXU5powFXDhCwa", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Chris", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/iP95p4xoKVk53GoZ742B", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Daniel", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/onwK4e9ZLuTAKqWW03F9", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven English v1", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_monolingual_v1", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven English v2", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_english_sts_v2", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Flash v2", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_flash_v2", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Flash v2.5", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_flash_v2_5", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Multilingual v1", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_v1", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Multilingual v2", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_v2", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Multilingual v2", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_multilingual_sts_v2", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Turbo v2", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_turbo_v2", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eleven Turbo v2.5", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Model/eleven_turbo_v2_5", + "Type": "Model", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "models_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Eric", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/cjVigY5qzO86Huf0OWal", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "George", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/JBFqnCBsd6RMkjVDRZzb", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Jessica", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/cgSgspJ2msm6clMCkdW9", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Laura", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/FGY2WhTYpPnrIDTdsKH5", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Liam", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/TX3LPaxmHKxFdv7VOQHJ", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Lily", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/pFZP5JQG7iQjIQuC4Bku", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Matilda", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/XrExE9yKIg1WjnnlVkGX", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "River", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/SAz9YHcvj6GT2YYXdXww", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Roger", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/CwhRBWXzGAHq8TQ4Fs17", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Sarah", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/EXAVITQu4vr4xnSDxMaL", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + }, + { + "Resource": { + "Name": "Will", + "FullyQualifiedName": "b9Rou9mHDmTYd8cdWkg2Yk4P2lq1/Voice/bIHbv24MWmeRgasZH58o", + "Type": "Voice", + "Metadata": { + "category": "premade" + }, + "Parent": null + }, + "Permission": { + "Value": "voices_read", + "Parent": null + } + } + ], + "UnboundedResources": null, + "Metadata": { + "Valid_Key": true + } +} \ No newline at end of file diff --git a/pkg/analyzer/cli.go b/pkg/analyzer/cli.go index 8a60394f70fb..da388a2f42c9 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -12,6 +12,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/bitbucket" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/digitalocean" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/dockerhub" + "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/elevenlabs" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/github" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/gitlab" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/huggingface" @@ -100,5 +101,7 @@ func Run(keyType string, secretInfo SecretInfo) { airtable.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) case "digitalocean": digitalocean.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) + case "elevenlabs": + elevenlabs.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) } } diff --git a/pkg/detectors/elevenlabs/v1/elevenlabs.go b/pkg/detectors/elevenlabs/v1/elevenlabs.go index 0beb2346fd01..2edda0cdce8d 100644 --- a/pkg/detectors/elevenlabs/v1/elevenlabs.go +++ b/pkg/detectors/elevenlabs/v1/elevenlabs.go @@ -74,6 +74,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result s1.ExtraData["Tier"] = userResponse.Subscription.Tier } s1.SetVerificationError(verificationErr, match) + + if s1.Verified { + s1.AnalysisInfo = map[string]string{ + "key": match, + } + } } results = append(results, s1) diff --git a/pkg/detectors/elevenlabs/v2/elevenlabs.go b/pkg/detectors/elevenlabs/v2/elevenlabs.go index ed74d06296bd..1e094d12e91a 100644 --- a/pkg/detectors/elevenlabs/v2/elevenlabs.go +++ b/pkg/detectors/elevenlabs/v2/elevenlabs.go @@ -71,6 +71,12 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result s1.ExtraData["Tier"] = userResponse.Subscription.Tier } s1.SetVerificationError(verificationErr, match) + + if s1.Verified { + s1.AnalysisInfo = map[string]string{ + "key": match, + } + } } results = append(results, s1) diff --git a/pkg/detectors/elevenlabs/v2/elevenlabs_integration_test.go b/pkg/detectors/elevenlabs/v2/elevenlabs_integration_test.go index 1d439f377cd7..759c58c439e4 100644 --- a/pkg/detectors/elevenlabs/v2/elevenlabs_integration_test.go +++ b/pkg/detectors/elevenlabs/v2/elevenlabs_integration_test.go @@ -54,7 +54,7 @@ func TestElevenlabs_FromChunk(t *testing.T) { Verified: true, ExtraData: map[string]string{ "version": "2", - "Name": "Ahmed", + "Name": "Trufflesecurity", "Tier": "free", }, }, @@ -150,7 +150,7 @@ func TestElevenlabs_FromChunk(t *testing.T) { t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError()) } } - ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError") + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "verificationError", "AnalysisInfo") if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" { t.Errorf("Elevenlabs.FromData() %s diff: (-got +want)\n%s", tt.name, diff) }