-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
base: main
Are you sure you want to change the base?
elevenlabs analyzer #3850
Changes from 18 commits
81005b9
794d176
f6c146c
75071f4
f61faf2
759d9c7
de93b9b
1704ba2
3cb54ca
6e2bfaf
0c79411
96dc34b
cb957aa
58f1736
5fd9705
f05f2fa
b36caf3
1937f36
3da2a78
4975781
3fa4402
6fa3a2e
8e3716d
8280b40
dda4747
a2cf1f4
16b8bc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 { | ||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Points to Consider for Implementation while getting resource:
Let me know your thoughts on these points. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
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() | ||
} |
There was a problem hiding this comment.
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.