309 lines
7.7 KiB
Go
309 lines
7.7 KiB
Go
//go:generate generate_permissions permissions.yaml permissions.go twilio
|
|
|
|
package twilio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
|
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
|
)
|
|
|
|
type Analyzer struct {
|
|
Cfg *config.Config
|
|
}
|
|
|
|
func (a *Analyzer) Type() analyzers.AnalyzerType {
|
|
return analyzers.AnalyzerTypeTwilio
|
|
}
|
|
|
|
func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
|
key, ok := credentialInfo["key"]
|
|
if !ok {
|
|
return nil, errors.New("key not found in credentialInfo")
|
|
}
|
|
|
|
sid, ok := credentialInfo["sid"]
|
|
if !ok {
|
|
return nil, errors.New("sid not found in credentialInfo")
|
|
}
|
|
|
|
if a.Cfg == nil {
|
|
a.Cfg = &config.Config{} // You might need to adjust this based on how you want to handle config
|
|
}
|
|
|
|
info, err := AnalyzePermissions(a.Cfg, sid, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// List parent and subaccounts
|
|
accounts, err := listTwilioAccounts(a.Cfg, sid, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var permissions []Permission
|
|
if info.AccountStatusCode == 200 {
|
|
permissions = []Permission{
|
|
AccountManagementRead,
|
|
AccountManagementWrite,
|
|
SubaccountConfigurationRead,
|
|
SubaccountConfigurationWrite,
|
|
KeyManagementRead,
|
|
KeyManagementWrite,
|
|
ServiceVerificationRead,
|
|
ServiceVerificationWrite,
|
|
SmsRead,
|
|
SmsWrite,
|
|
VoiceRead,
|
|
VoiceWrite,
|
|
MessagingRead,
|
|
MessagingWrite,
|
|
CallManagementRead,
|
|
CallManagementWrite,
|
|
}
|
|
} else if info.AccountStatusCode == 401 {
|
|
permissions = []Permission{
|
|
ServiceVerificationRead,
|
|
ServiceVerificationWrite,
|
|
SmsRead,
|
|
SmsWrite,
|
|
VoiceRead,
|
|
VoiceWrite,
|
|
MessagingRead,
|
|
MessagingWrite,
|
|
CallManagementRead,
|
|
CallManagementWrite,
|
|
}
|
|
}
|
|
|
|
var (
|
|
bindings []analyzers.Binding
|
|
parentAccountSID = ""
|
|
parentAccountFriendlyName = ""
|
|
)
|
|
|
|
if len(info.ServicesRes.Services) > 0 {
|
|
parentAccountSID = info.ServicesRes.Services[0].AccountSID
|
|
parentAccountFriendlyName = info.ServicesRes.Services[0].FriendlyName
|
|
}
|
|
|
|
for _, account := range accounts {
|
|
accountType := "Account"
|
|
if parentAccountSID != "" && account.SID != parentAccountSID {
|
|
accountType = "SubAccount"
|
|
}
|
|
resource := analyzers.Resource{
|
|
Name: account.FriendlyName,
|
|
FullyQualifiedName: "twilio.com/account/" + account.SID,
|
|
Type: accountType,
|
|
}
|
|
if parentAccountSID != "" && account.SID != parentAccountSID {
|
|
resource.Parent = &analyzers.Resource{
|
|
Name: parentAccountFriendlyName,
|
|
FullyQualifiedName: "twilio.com/account/" + parentAccountSID,
|
|
Type: "Account",
|
|
}
|
|
}
|
|
|
|
for _, perm := range permissions {
|
|
permStr, _ := perm.ToString()
|
|
bindings = append(bindings, analyzers.Binding{
|
|
Resource: resource,
|
|
Permission: analyzers.Permission{
|
|
Value: permStr,
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
return &analyzers.AnalyzerResult{
|
|
AnalyzerType: analyzers.AnalyzerTypeTwilio,
|
|
Bindings: bindings,
|
|
}, nil
|
|
}
|
|
|
|
type secretInfo struct {
|
|
ServicesRes serviceResponse
|
|
AccountStatusCode int
|
|
}
|
|
|
|
const (
|
|
AUTHENTICATED_NO_PERMISSION = 70051
|
|
INVALID_CREDENTIALS = 20003
|
|
)
|
|
|
|
// getAccountsStatusCode returns the status code from the Accounts endpoint
|
|
// this is used to determine whether the key is scoped as main or standard, since standard has no access here.
|
|
func getAccountsStatusCode(cfg *config.Config, sid string, secret string) (int, error) {
|
|
// create http client
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
|
|
// create request
|
|
req, err := http.NewRequest("GET", "https://api.twilio.com/2010-04-01/Accounts", nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// add basicAuth
|
|
req.SetBasicAuth(sid, secret)
|
|
|
|
// send request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
type serviceResponse struct {
|
|
Code int `json:"code"`
|
|
Services []service `json:"services"`
|
|
}
|
|
|
|
type service struct {
|
|
FriendlyName string `json:"friendly_name"` // friendly name of a service
|
|
SID string `json:"sid"` // object id of service
|
|
AccountSID string `json:"account_sid"` // account sid
|
|
}
|
|
|
|
// getVerifyServicesStatusCode returns the status code and the JSON response from the Verify Services endpoint
|
|
// only the code value is captured in the JSON response and this is only shown when the key is invalid or has no permissions
|
|
func getVerifyServicesStatusCode(cfg *config.Config, sid string, secret string) (serviceResponse, error) {
|
|
var serviceRes serviceResponse
|
|
|
|
// create http client
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
|
|
// create request
|
|
req, err := http.NewRequest("GET", "https://verify.twilio.com/v2/Services", nil)
|
|
if err != nil {
|
|
return serviceRes, err
|
|
}
|
|
|
|
// add basicAuth
|
|
req.SetBasicAuth(sid, secret)
|
|
|
|
// send request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return serviceRes, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
// read response
|
|
if err := json.NewDecoder(resp.Body).Decode(&serviceRes); err != nil {
|
|
return serviceRes, err
|
|
}
|
|
|
|
return serviceRes, nil
|
|
}
|
|
|
|
func listTwilioAccounts(cfg *config.Config, sid, secret string) ([]service, error) {
|
|
// create http client
|
|
client := analyzers.NewAnalyzeClient(cfg)
|
|
|
|
// create request
|
|
req, err := http.NewRequest("GET", "https://api.twilio.com/2010-04-01/Accounts.json", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// add basicAuth
|
|
req.SetBasicAuth(sid, secret)
|
|
|
|
// send request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var result struct {
|
|
Accounts []service `json:"accounts"`
|
|
}
|
|
|
|
// read response
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return result.Accounts, nil
|
|
}
|
|
|
|
func AnalyzePermissions(cfg *config.Config, sid, secret string) (*secretInfo, error) {
|
|
servicesRes, err := getVerifyServicesStatusCode(cfg, sid, secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
statusCode, err := getAccountsStatusCode(cfg, sid, secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &secretInfo{
|
|
ServicesRes: servicesRes,
|
|
AccountStatusCode: statusCode,
|
|
}, nil
|
|
}
|
|
|
|
func AnalyzeAndPrintPermissions(cfg *config.Config, sid, secret string) {
|
|
info, err := AnalyzePermissions(cfg, sid, secret)
|
|
if err != nil {
|
|
color.Red("[x] Error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
if info.ServicesRes.Code == INVALID_CREDENTIALS {
|
|
color.Red("[x] Invalid Twilio API Key")
|
|
return
|
|
}
|
|
|
|
if info.ServicesRes.Code == AUTHENTICATED_NO_PERMISSION {
|
|
printRestrictedKeyMsg()
|
|
return
|
|
}
|
|
|
|
printPermissions(info.AccountStatusCode)
|
|
}
|
|
|
|
// printPermissions prints the permissions based on the status code
|
|
// 200 means the key is main, 401 means the key is standard
|
|
func printPermissions(statusCode int) {
|
|
|
|
if statusCode != 200 && statusCode != 401 {
|
|
color.Red("[x] Invalid Twilio API Key")
|
|
return
|
|
}
|
|
|
|
color.Green("[!] Valid Twilio API Key\n")
|
|
color.Green("[i] Expires: Never")
|
|
|
|
if statusCode == 401 {
|
|
color.Yellow("[i] Key type: Standard")
|
|
color.Yellow("[i] Permissions: All EXCEPT key management and account/subaccount configuration.")
|
|
|
|
} else if statusCode == 200 {
|
|
color.Green("[i] Key type: Main (aka Admin)")
|
|
color.Green("[i] Permissions: All")
|
|
}
|
|
}
|
|
|
|
// printRestrictedKeyMsg prints the message for a restricted key
|
|
// this is a temporary measure since the restricted key type is still in beta
|
|
func printRestrictedKeyMsg() {
|
|
color.Green("[!] Valid Twilio API Key\n")
|
|
color.Green("[i] Expires: Never")
|
|
color.Yellow("[i] Key type: Restricted")
|
|
color.Yellow("[i] Permissions: Limited")
|
|
fmt.Println("[*] Note: Twilio is rolling out a Restricted API Key type, which provides fine-grained control over API endpoints. Since it's still in a Public Beta, this has not been incorporated into this tool.")
|
|
}
|