Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

elevenlabs analyzer #3850

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
81005b9
added analyzer initial working structure
kashifkhan0771 Jan 23, 2025
794d176
restructured the resources
kashifkhan0771 Jan 23, 2025
f6c146c
added history read api
kashifkhan0771 Jan 23, 2025
75071f4
added history delete api
kashifkhan0771 Jan 24, 2025
f61faf2
fixed http method issue
kashifkhan0771 Jan 24, 2025
759d9c7
added delete dubbing api and refactored the code
kashifkhan0771 Jan 24, 2025
de93b9b
added dubbing read api
kashifkhan0771 Jan 24, 2025
1704ba2
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Jan 27, 2025
3cb54ca
optimized requests and added projects api
kashifkhan0771 Jan 27, 2025
6e2bfaf
added pronunciation dictionaries apis
kashifkhan0771 Jan 27, 2025
0c79411
fixed linter
kashifkhan0771 Jan 27, 2025
96dc34b
added models,audionative,workspace apis
kashifkhan0771 Jan 27, 2025
cb957aa
added some last apis
kashifkhan0771 Jan 27, 2025
58f1736
Deleted \
kashifkhan0771 Jan 27, 2025
5fd9705
fixed linter
kashifkhan0771 Jan 27, 2025
f05f2fa
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Jan 28, 2025
b36caf3
few more enhancements and test cases added
kashifkhan0771 Jan 28, 2025
1937f36
added go generate command
kashifkhan0771 Jan 28, 2025
3da2a78
optimized the code and resolved the comments
kashifkhan0771 Jan 29, 2025
4975781
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Jan 29, 2025
3fa4402
fixed linter
kashifkhan0771 Jan 29, 2025
6fa3a2e
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Jan 31, 2025
8e3716d
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Feb 3, 2025
8280b40
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Feb 4, 2025
dda4747
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Feb 6, 2025
a2cf1f4
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Feb 6, 2025
16b8bc1
Merge branch 'main' into feat/oss-97-elevenlabs-analyzer
kashifkhan0771 Feb 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pkg/analyzer/analyzers/analyzers.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const (
AnalyzerTypeAirbrake
AnalyzerTypeAsana
AnalyzerTypeBitbucket
AnalyzerTypeElevenLabs
AnalyzerTypeGitHub
AnalyzerTypeGitLab
AnalyzerTypeHuggingFace
Expand All @@ -89,6 +90,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
AnalyzerTypeAirbrake: "Airbrake",
AnalyzerTypeAsana: "Asana",
AnalyzerTypeBitbucket: "Bitbucket",
AnalyzerTypeElevenLabs: "ElevenLabs",
AnalyzerTypeGitHub: "GitHub",
AnalyzerTypeGitLab: "GitLab",
AnalyzerTypeHuggingFace: "HuggingFace",
Expand Down
377 changes: 377 additions & 0 deletions pkg/analyzer/analyzers/elevenlabs/elevenlabs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,377 @@
//go:generate generate_permissions permissions.yaml permissions.go elevenlabs
package elevenlabs

import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"slices"

"github.com/fatih/color"
"github.com/jedib0t/go-pretty/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
Resources []Resource // list of resources the key has access to
Misc map[string]string
}

// User hold the information about user to whom the key belongs to
type User struct {
ID string
Name string
SubscriptionTier string
SubscriptionStatus string
}

// Resources hold information about the resources the key has access
type Resource struct {
ID string
Name string
Type string
Metadata map[string]string
Permission string
Parent *Resource
}

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{}

// validate the key and get user information
if err := validateKey(client, key, secretInfo); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The func name validateKey is confusing as this function is fetching the ElevenLabs User. Moreover, this function is also setting up User, Resource and Permissions in SecretInfo. I would recommend to break it multiple func.

return nil, err
}

if secretInfo.Valid {
// Get resources
if err := getResources(client, key, secretInfo); err != nil {
return nil, err
}
}

if err := getUnboundedResources(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 {
color.Red("[x] Error : %s", err.Error())
return
}

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
printResources(info.Resources)

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),
}

// extract information from resource to create bindings and append to result bindings
for _, resource := range info.Resources {
// if unique identifier is empty do not map the resource to analyzer result
if resource.ID == "" {
continue
}
// if resource has permission it is binded resource
if resource.Permission != "" {
binding := analyzers.Binding{
Resource: analyzers.Resource{
Name: resource.Name,
FullyQualifiedName: resource.ID,
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: 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
}

// validateKey check if the key is valid and get the user information if it's valid
func validateKey(client *http.Client, key string, secretInfo *SecretInfo) error {
response, statusCode, err := makeElevenLabsRequest(client, permissionToAPIMap[UserRead], http.MethodGet, key)
if err != nil {
return err
}

switch statusCode {
case http.StatusOK:
var user UserResponse

if err := json.Unmarshal(response, &user); err != nil {
return err
}

// map info to secretInfo
secretInfo.Valid = true
secretInfo.User = User{
ID: user.UserID,
Name: user.FirstName,
SubscriptionTier: user.Subscription.Tier,
SubscriptionStatus: user.Subscription.Status,
}
// add user read scope to secret info
secretInfo.Permissions = append(secretInfo.Permissions, PermissionStrings[UserRead])
// map resource to secret info
secretInfo.Resources = append(secretInfo.Resources, Resource{
ID: user.UserID,
Name: user.FirstName,
Type: "User",
Permission: PermissionStrings[UserRead],
})

return nil
case http.StatusUnauthorized:
var errorResp ErrorResponse

if err := json.Unmarshal(response, &errorResp); err != nil {
return err
}

if errorResp.Detail.Status == InvalidAPIKey || errorResp.Detail.Status == NotVerifiable {
return errors.New("invalid api key")
} else if errorResp.Detail.Status == MissingPermissions {
// key is missing user read permissions but is valid
secretInfo.Valid = true
color.Yellow("\n[!] API Key missing user read permissions")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnalyzePermission should only be gathering information from the service. As it is the common code calling from CLI and detectors.

}

return nil
default:
return fmt.Errorf("unexpected status code: %d", statusCode)
}
}

/*
getResources 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.
If a particular write permission exist that means the read also exist.
So for API calls that does not return any resource data, we make the write permissions API calls first
and if they were as expected we skip the read API calls and add read permission directly.
If write permission API calls was not as expected than only we make read permission API calls
This we only do for those API calls which does not add any resources to secretInfo
*/
func getResources(client *http.Client, key string, secretInfo *SecretInfo) error {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Points to Consider for Implementation while getting resource:

  1. Aggregate Errors:
    Instead of failing on the first error, collect all errors encountered during the process and return them as a single aggregated error at the end. This ensures that users get a complete picture of what went wrong, rather than having to address issues one at a time.

  2. Graceful Error Handling in CLI:
    For the CLI, log errors when checking a specific scope or fetching resource fails, but continue processing other tasks. Improving the user experience and allowing for partial success.

  3. Concurrent API Calls:
    Use Go routines to call APIs concurrently. This will significantly improve performance by reducing the total time spent waiting for API responses.

Let me know your thoughts on these points.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My vote will be for concurrent API calls with error handling for each permission.

// history
if err := getHistory(client, key, secretInfo); err != nil {
return err
}

if err := deleteHistory(client, key, secretInfo); err != nil {
return err
}

// dubbings
if err := deleteDubbing(client, key, secretInfo); err != nil {
return err
}

// if dubbing write permission was not added
if !permissionExist(secretInfo.Permissions, DubbingWrite) {
if err := getDebugging(client, key, secretInfo); err != nil {
return err
}
}

// voices
if err := getVoices(client, key, secretInfo); err != nil {
return err
}

if err := deleteVoice(client, key, secretInfo); err != nil {
return err
}

// projects
if err := getProjects(client, key, secretInfo); err != nil {
return err
}

if err := deleteProject(client, key, secretInfo); err != nil {
return err
}

// pronunciation dictionaries
if err := getPronunciationDictionaries(client, key, secretInfo); err != nil {
return err
}

if err := removePronunciationDictionariesRule(client, key, secretInfo); err != nil {
return err
}

// models
if err := getModels(client, key, secretInfo); err != nil {
return err
}

// audio native
if err := updateAudioNativeProject(client, key, secretInfo); err != nil {
return err
}

// workspace
if err := deleteInviteFromWorkspace(client, key, secretInfo); err != nil {
return err
}

// text to speech
if err := textToSpeech(client, key, secretInfo); err != nil {
return err
}

// voice changer
if err := speechToSpeech(client, key, secretInfo); err != nil {
return err
}

// audio isolation
if err := audioIsolation(client, key, secretInfo); err != nil {
return err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are too many calls. is it possible to optimize them ? may be using go routines


return nil
}

// getUnboundedResources gather resources which can be accessed without any permission
func getUnboundedResources(client *http.Client, key string, secretInfo *SecretInfo) error {
// each agent can have a conversations which we get inside this function
if err := getAgents(client, key, secretInfo); err != nil {
return err
}

return nil
}

// permissionExist returns if particular permission exist in the list
func permissionExist(permissionsList []string, permission Permission) bool {
permissionString, _ := permission.ToString()

return slices.Contains(permissionsList, permissionString)
}

// 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 printResources(resources []Resource) {
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()
}
Loading
Loading