add twilio analyze relationships (#3148)

* add twilio analyze relationships

* unused struct
This commit is contained in:
Dustin Decker 2024-08-01 17:04:44 -07:00 committed by GitHub
parent b193febab5
commit 605d037e45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 403 additions and 44 deletions

View file

@ -34,6 +34,12 @@ func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string
return nil, err
}
// List parent and subaccounts
accounts, err := listTwilioAccounts(cfg, key)
if err != nil {
return nil, err
}
var permissions []Permission
if info.AccountStatusCode == 200 {
permissions = []Permission{
@ -69,21 +75,37 @@ func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string
}
}
// Can we get org information?
resource := analyzers.Resource{
Name: "Twilio API",
Type: "API",
}
var bindings []analyzers.Binding
for _, perm := range permissions {
permStr, _ := perm.ToString()
bindings = append(bindings, analyzers.Binding{
Resource: resource,
Permission: analyzers.Permission{
Value: permStr,
},
})
parentAccountSID := info.ServicesRes.Services[0].AccountSID
parentAccountFriendlyName := info.ServicesRes.Services[0].FriendlyName
for _, account := range accounts {
accountType := "Account"
if account.SID != parentAccountSID {
accountType = "SubAccount"
}
resource := analyzers.Resource{
Name: account.FriendlyName,
FullyQualifiedName: "twilio.com/account/" + account.SID,
Type: accountType,
}
if 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{
@ -92,12 +114,8 @@ func (a *Analyzer) Analyze(ctx context.Context, credentialInfo map[string]string
}, nil
}
type VerifyJSON struct {
Code int `json:"code"`
}
type SecretInfo struct {
VerifyJson VerifyJSON
type secretInfo struct {
ServicesRes serviceResponse
AccountStatusCode int
}
@ -127,11 +145,6 @@ func getAccountsStatusCode(cfg *config.Config, sid string, secret string) (int,
return 0, err
}
// add query params
q := req.URL.Query()
q.Add("FriendlyName", "zpoOnD08HdLLZGFnGUMTxbX3qQ1kS")
req.URL.RawQuery = q.Encode()
// add basicAuth
req.SetBasicAuth(sid, secret)
@ -144,10 +157,21 @@ func getAccountsStatusCode(cfg *config.Config, sid string, secret string) (int,
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) (VerifyJSON, error) {
var verifyJSON VerifyJSON
func getVerifyServicesStatusCode(cfg *config.Config, sid string, secret string) (serviceResponse, error) {
var serviceRes serviceResponse
// create http client
client := analyzers.NewAnalyzeClient(cfg)
@ -155,39 +179,71 @@ func getVerifyServicesStatusCode(cfg *config.Config, sid string, secret string)
// create request
req, err := http.NewRequest("GET", "https://verify.twilio.com/v2/Services", nil)
if err != nil {
return verifyJSON, err
return serviceRes, err
}
// add query params
q := req.URL.Query()
q.Add("FriendlyName", "zpoOnD08HdLLZGFnGUMTxbX3qQ1kS")
req.URL.RawQuery = q.Encode()
// add basicAuth
req.SetBasicAuth(sid, secret)
// send request
resp, err := client.Do(req)
if err != nil {
return verifyJSON, err
return serviceRes, err
}
defer resp.Body.Close()
// read response
if err := json.NewDecoder(resp.Body).Decode(&verifyJSON); err != nil {
return verifyJSON, err
if err := json.NewDecoder(resp.Body).Decode(&serviceRes); err != nil {
return serviceRes, err
}
return verifyJSON, nil
return serviceRes, nil
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
func listTwilioAccounts(cfg *config.Config, key string) ([]service, error) {
sid, secret, err := splitKey(key)
if err != nil {
return nil, err
}
verifyJSON, err := getVerifyServicesStatusCode(cfg, sid, secret)
// 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, key string) (*secretInfo, error) {
sid, secret, err := splitKey(key)
if err != nil {
return nil, err
}
servicesRes, err := getVerifyServicesStatusCode(cfg, sid, secret)
if err != nil {
return nil, err
}
@ -197,8 +253,8 @@ func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
return nil, err
}
return &SecretInfo{
VerifyJson: verifyJSON,
return &secretInfo{
ServicesRes: servicesRes,
AccountStatusCode: statusCode,
}, nil
}
@ -216,12 +272,12 @@ func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
return
}
if info.VerifyJson.Code == INVALID_CREDENTIALS {
if info.ServicesRes.Code == INVALID_CREDENTIALS {
color.Red("[x] Invalid Twilio API Key")
return
}
if info.VerifyJson.Code == AUTHENTICATED_NO_PERMISSION {
if info.ServicesRes.Code == AUTHENTICATED_NO_PERMISSION {
printRestrictedKeyMsg()
return
}

View file

@ -0,0 +1,288 @@
package twilio
import (
"encoding/json"
"testing"
"time"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
func TestAnalyzer_Analyze(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors4")
if err != nil {
t.Fatalf("could not get test secrets from GCP: %s", err)
}
tests := []struct {
name string
key string
want string // JSON string
wantErr bool
}{
{
name: "valid Twilio key",
key: testSecrets.MustGetField("TWILLIO_ID") + ":" + testSecrets.MustGetField("TWILLIO_API"),
want: ` {
"AnalyzerType": 20,
"Bindings": [
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "account_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "account_management:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "subaccount_configuration:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "subaccount_configuration:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "key_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "key_management:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "service_verification:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "service_verification:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "sms:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "sms:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "voice:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "voice:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "messaging:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "messaging:write",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "call_management:read",
"Parent": null
}
},
{
"Resource": {
"Name": "My first Twilio account",
"FullyQualifiedName": "twilio.com/account/ACa5b6165773490f33f226d71e7ffacff5",
"Type": "Account",
"Metadata": null,
"Parent": null
},
"Permission": {
"Value": "call_management:write",
"Parent": null
}
}
],
"UnboundedResources": null,
"Metadata": null
}`,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := Analyzer{}
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
}
// 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)
}
// 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 []byte
gotIndented, err = json.MarshalIndent(got, "", " ")
if err != nil {
t.Fatalf("could not marshal got to indented JSON: %s", err)
}
t.Errorf("Analyzer.Analyze() = \n%s", gotIndented)
}
})
}
}

View file

@ -90,6 +90,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
if res.StatusCode >= 200 && res.StatusCode < 300 {
s1.Verified = true
s1.AnalysisInfo = map[string]string{"key": sid + ":" + key}
var serviceResponse serviceResponse
if err := json.NewDecoder(res.Body).Decode(&serviceResponse); err == nil && len(serviceResponse.Services) > 0 { // no error in parsing and have at least one service
service := serviceResponse.Services[0]

View file

@ -55,6 +55,11 @@ func TestTwilio_FromChunk(t *testing.T) {
Verified: true,
Redacted: id,
RawV2: []byte(id + secret),
ExtraData: map[string]string{
"account_sid": "ACa5b6165773490f33f226d71e7ffacff5",
"friendly_name": "MyServiceName",
"rotation_guide": "https://howtorotate.com/docs/tutorials/twilio/",
},
},
},
wantErr: false,
@ -73,6 +78,9 @@ func TestTwilio_FromChunk(t *testing.T) {
Verified: false,
Redacted: id,
RawV2: []byte(id + secretInactive),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/twilio/",
},
},
},
wantErr: false,
@ -102,6 +110,9 @@ func TestTwilio_FromChunk(t *testing.T) {
Verified: false,
Redacted: id,
RawV2: []byte(id + secret),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/twilio/",
},
},
},
wantErr: false,
@ -121,6 +132,9 @@ func TestTwilio_FromChunk(t *testing.T) {
Verified: false,
Redacted: id,
RawV2: []byte(id + secret),
ExtraData: map[string]string{
"rotation_guide": "https://howtorotate.com/docs/tutorials/twilio/",
},
},
},
wantErr: false,
@ -142,7 +156,7 @@ func TestTwilio_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("Twilio.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}