mirror of
https://github.com/trufflesecurity/trufflehog.git
synced 2024-11-10 07:04:24 +00:00
add twilio analyze relationships (#3148)
* add twilio analyze relationships * unused struct
This commit is contained in:
parent
b193febab5
commit
605d037e45
4 changed files with 403 additions and 44 deletions
|
@ -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
|
||||
}
|
||||
|
|
288
pkg/analyzer/analyzers/twilio/twilio_test.go
Normal file
288
pkg/analyzer/analyzers/twilio/twilio_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue