[Feat] Added Ngrok API Key Analyzer (#4110)
* added ngrok analyzer * updated analyzer test * added analysis info in ngrok detector * updated and added pagination in ngrok analyzer * updated ngrok analyzer expected output detector type * updated ngrok analyzer removed unused models
This commit is contained in:
@@ -97,6 +97,7 @@ const (
|
||||
AnalyzerTypeNetlify
|
||||
AnalyzerTypeFastly
|
||||
AnalyzerTypeMonday
|
||||
AnalyzerTypeNgrok
|
||||
// Add new items here with AnalyzerType prefix
|
||||
)
|
||||
|
||||
@@ -139,6 +140,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
|
||||
AnalyzerTypeNetlify: "Netlify",
|
||||
AnalyzerTypeFastly: "Fastly",
|
||||
AnalyzerTypeMonday: "Monday",
|
||||
AnalyzerTypeNgrok: "Ngrok",
|
||||
// Add new mappings here
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"AnalyzerType":37,"Bindings":[{"Resource":{"Name":"ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI","FullyQualifiedName":"endpoint/ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI","Type":"endpoint","Metadata":{"bindings":["public"],"createdAt":"2025-04-30T11:37:16Z","host":"","hostport":"lightly-communal-lizard.ngrok-free.app:443","metadata":"","port":0,"proto":"https","publicURL":"https://lightly-communal-lizard.ngrok-free.app","region":"","type":"cloud","updatedAt":"2025-04-30T11:37:16Z","uri":"https://api.ngrok.com/endpoints/ep_2wRn1EAlf7JqFe3RPJBRNW1IkTI"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"rd_2wRmxH1k3oaR4HFnXkzac9uz9wr","FullyQualifiedName":"domain/rd_2wRmxH1k3oaR4HFnXkzac9uz9wr","Type":"domain","Metadata":{"createdAt":"2025-04-30T11:36:44Z","domain":"lightly-communal-lizard.ngrok-free.app","metadata":"","uri":"https://api.ngrok.com/reserved_domains/rd_2wRmxH1k3oaR4HFnXkzac9uz9wr"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"ak_2wRnGU2AMIxg7O737nC6geKeVlX","FullyQualifiedName":"api_key/ak_2wRnGU2AMIxg7O737nC6geKeVlX","Type":"api_key","Metadata":{"createdAt":"2025-04-30T11:39:17Z","description":"API Key for \"Truffle Detector\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","uri":"https://api.ngrok.com/api_keys/ak_2wRnGU2AMIxg7O737nC6geKeVlX"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"ak_2wRnJq02PcmYrlH38sdE4rKZKZY","FullyQualifiedName":"api_key/ak_2wRnJq02PcmYrlH38sdE4rKZKZY","Type":"api_key","Metadata":{"createdAt":"2025-04-30T11:39:44Z","description":"API Key for \"Elliot\"","metadata":"","ownerID":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","uri":"https://api.ngrok.com/api_keys/ak_2wRnJq02PcmYrlH38sdE4rKZKZY"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb","FullyQualifiedName":"authtoken/cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb","Type":"authtoken","Metadata":{"acl":[],"createdAt":"2025-04-30T11:38:35Z","description":"Tunnel Authtoken for \"Elliot\"","metadata":"","ownerID":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","uri":"https://api.ngrok.com/credentials/cr_2wRnBCGRkEgPBKb5G6SLxr0h8tb"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ","FullyQualifiedName":"authtoken/cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ","Type":"authtoken","Metadata":{"acl":[],"createdAt":"2025-04-30T11:35:19Z","description":"credential for \"[email protected]\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","uri":"https://api.ngrok.com/credentials/cr_2wRmmYJ2BRKy3SFnusPWX5dRPVQ"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa","FullyQualifiedName":"ssh_credential/sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa","Type":"ssh_credential","Metadata":{"acl":[],"createdAt":"2025-04-30T11:40:26Z","description":"SSH Key for \"Truffle Detector\"","metadata":"","ownerID":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","publicKey":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCo9S+bLqFTCzDA0TxJWaiPqddDnrHojOHCOnl+ZlRcbBrG9hM8IUmaJ+ZG63NIOkaqrlHGed7MK+SLqZIqi/TkuyHwu8kkBcPCayrHdgdb9NWLpRFaWN2A67Ww+/14rPEzY7KA5EDlmWow2IPK9Ayb+J5El6NRAhLS8AChupfmRjAOxciMUTdckTI2avr5R1sOddI8cutjfvuwQvFpJI1oJLbewUxZv8gOXuqbZScIx72NiZvtCDtktVjNVm6sib129P+vD3QzCwSuNGZIv9fUcQK7Y/rmMHyjDNfvaqm8HunINBV+kDxubfbIQBMCpj/HeuUVToQ3xyfqGaON0EPa","uri":"https://api.ngrok.com/ssh_credentials/sshcr_2wRnP5xXhZ2uhXBPQcFOFqhynaa"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","FullyQualifiedName":"bot_user/bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq","Type":"bot_user","Metadata":{"active":true,"createdAt":"2025-04-30T11:38:17Z","name":"Elliot","uri":"https://api.ngrok.com/bot_users/bot_2wRn8sBjuvH9ZmLbBzrKKJKFmHq"},"Parent":null},"Permission":{"Value":"full_access","Parent":null}},{"Resource":{"Name":"usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","FullyQualifiedName":"user/usr_2wRmmWE9P3ivK6dpF8sCaMd0X0V","Type":"user","Metadata":null,"Parent":null},"Permission":{"Value":"full_access","Parent":null}}],"UnboundedResources":[{"Name":"Account Plan","FullyQualifiedName":"account_plan/Free","Type":"account_plan","Metadata":null,"Parent":null}],"Metadata":null}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ngrok
|
||||
|
||||
type apiKey struct {
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
Description string `json:"description"`
|
||||
Metadata string `json:"metadata"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type authtoken struct {
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
Description string `json:"description"`
|
||||
Metadata string `json:"metadata"`
|
||||
ACL []string `json:"acl"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type sshCredential struct {
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
Description string `json:"description"`
|
||||
PublicKey string `json:"public_key"`
|
||||
Metadata string `json:"metadata"`
|
||||
ACL []string `json:"acl"`
|
||||
OwnerID string `json:"owner_id"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type domain struct {
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
Domain string `json:"domain"`
|
||||
Metadata string `json:"metadata"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type endpoint struct {
|
||||
ID string `json:"id"`
|
||||
Region string `json:"region"`
|
||||
Host string `json:"host"`
|
||||
Port int64 `json:"port"`
|
||||
PublicURL string `json:"public_url"`
|
||||
Proto string `json:"proto"`
|
||||
Hostport string `json:"hostport"`
|
||||
Type string `json:"type"`
|
||||
Bindings []string `json:"bindings"`
|
||||
URI string `json:"uri"`
|
||||
Metadata string `json:"metadata"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type botUser struct {
|
||||
ID string `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
Name string `json:"name"`
|
||||
Active bool `json:"active"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type user struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type paginatedResponse struct {
|
||||
NextPageURI string `json:"next_page_uri"`
|
||||
APIKeys []apiKey `json:"keys,omitempty"`
|
||||
Authtokens []authtoken `json:"credentials,omitempty"`
|
||||
SSHCredentials []sshCredential `json:"ssh_credentials,omitempty"`
|
||||
Domains []domain `json:"reserved_domains,omitempty"`
|
||||
Endpoints []endpoint `json:"endpoints,omitempty"`
|
||||
BotUsers []botUser `json:"bot_users,omitempty"`
|
||||
}
|
||||
|
||||
type secretInfo struct {
|
||||
Users []user
|
||||
BotUsers []botUser
|
||||
APIKeys []apiKey
|
||||
Authtokens []authtoken
|
||||
SSHCredentials []sshCredential
|
||||
Domains []domain
|
||||
Endpoints []endpoint
|
||||
AccountType AccountType
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//go:generate generate_permissions permissions.yaml permissions.go ngrok
|
||||
package ngrok
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
type AccountType string
|
||||
|
||||
const (
|
||||
AccountFree AccountType = "Free"
|
||||
AccountPaid AccountType = "Paid"
|
||||
)
|
||||
|
||||
func (a Analyzer) Type() analyzers.AnalyzerType {
|
||||
return analyzers.AnalyzerTypeNgrok
|
||||
}
|
||||
|
||||
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
|
||||
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
|
||||
}
|
||||
|
||||
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
|
||||
info, err := AnalyzePermissions(cfg, key)
|
||||
if err != nil {
|
||||
color.Red("[x] Invalid Ngrok Key\n")
|
||||
color.Red("[x] Error : %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if info == nil {
|
||||
color.Red("[x] Error : %s", "No information found")
|
||||
return
|
||||
}
|
||||
|
||||
color.Green("[i] Valid Ngrok API Key\n")
|
||||
printAccountAndPermissions(info)
|
||||
}
|
||||
|
||||
func AnalyzePermissions(cfg *config.Config, key string) (*secretInfo, error) {
|
||||
// Ngrok API keys provide full access to all resources depending on the account type
|
||||
// Free accounts have access to a limited set of resources.
|
||||
client := analyzers.NewAnalyzeClient(cfg)
|
||||
secretInfo := &secretInfo{}
|
||||
|
||||
if err := determineAccountType(client, secretInfo, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := populateAllResources(client, secretInfo, key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return secretInfo, nil
|
||||
}
|
||||
|
||||
func secretInfoToAnalyzerResult(info *secretInfo) *analyzers.AnalyzerResult {
|
||||
if info == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindings := []analyzers.Binding{}
|
||||
fullAccessPermission := analyzers.Permission{
|
||||
Value: PermissionStrings[FullAccess],
|
||||
}
|
||||
|
||||
for _, endpoint := range info.Endpoints {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createEndpointResource(endpoint),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, domain := range info.Domains {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createDomainResource(domain),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, apiKey := range info.APIKeys {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createAPIKeyResource(apiKey),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, authtoken := range info.Authtokens {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createAuthtokenResource(authtoken),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, sshCredential := range info.SSHCredentials {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createSSHKeyResource(sshCredential),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, botUser := range info.BotUsers {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createBotUserResource(botUser),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
for _, user := range info.Users {
|
||||
bindings = append(bindings, analyzers.Binding{
|
||||
Resource: createUserResource(user),
|
||||
Permission: fullAccessPermission,
|
||||
})
|
||||
}
|
||||
|
||||
result := analyzers.AnalyzerResult{
|
||||
AnalyzerType: analyzers.AnalyzerTypeNgrok,
|
||||
Metadata: nil,
|
||||
Bindings: bindings,
|
||||
UnboundedResources: []analyzers.Resource{{
|
||||
Name: "Account Plan",
|
||||
FullyQualifiedName: "account_plan/" + string(info.AccountType),
|
||||
Type: "account_plan",
|
||||
}},
|
||||
}
|
||||
return &result
|
||||
}
|
||||
|
||||
func printAccountAndPermissions(info *secretInfo) {
|
||||
accountIsFree := info.AccountType != AccountPaid
|
||||
color.Yellow("[i] Account Type: %s", info.AccountType)
|
||||
|
||||
color.Yellow("\n[i] Permissions:")
|
||||
t1 := table.NewWriter()
|
||||
t1.AppendHeader(table.Row{"Resource", "Access Level"})
|
||||
|
||||
// Printing the access level to Ngrok resources
|
||||
for _, resource := range ngrokResources {
|
||||
accessLevel := "Full Access"
|
||||
if resource.IsPaidFeature && accountIsFree {
|
||||
accessLevel = "None"
|
||||
}
|
||||
t1.AppendRow(table.Row{
|
||||
color.GreenString(resource.Name),
|
||||
color.GreenString(accessLevel),
|
||||
})
|
||||
t1.AppendSeparator()
|
||||
}
|
||||
|
||||
t1.SetOutputMirror(os.Stdout)
|
||||
t1.Render()
|
||||
|
||||
color.Yellow("\n[i] Resources:")
|
||||
|
||||
t2 := table.NewWriter()
|
||||
t2.SetTitle("User IDs")
|
||||
t2.AppendHeader(table.Row{"ID"})
|
||||
|
||||
for _, user := range info.Users {
|
||||
t2.AppendRow(table.Row{
|
||||
color.GreenString(user.ID),
|
||||
})
|
||||
}
|
||||
|
||||
t2.SetOutputMirror(os.Stdout)
|
||||
t2.Render()
|
||||
|
||||
t3 := table.NewWriter()
|
||||
t3.SetTitle("Endpoints")
|
||||
t3.AppendHeader(table.Row{"ID", "Region", "Public URL", "Type", "Created At", "Updated At"})
|
||||
for _, endpoint := range info.Endpoints {
|
||||
t3.AppendRow(table.Row{
|
||||
color.GreenString(endpoint.ID),
|
||||
color.GreenString(endpoint.Region),
|
||||
color.GreenString(endpoint.PublicURL),
|
||||
color.GreenString(endpoint.Type),
|
||||
color.GreenString(endpoint.CreatedAt),
|
||||
color.GreenString(endpoint.UpdatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t3.SetOutputMirror(os.Stdout)
|
||||
t3.Render()
|
||||
|
||||
t4 := table.NewWriter()
|
||||
t4.SetTitle("Domains")
|
||||
t4.AppendHeader(table.Row{"ID", "Domain", "URI", "Created At"})
|
||||
for _, domain := range info.Domains {
|
||||
t4.AppendRow(table.Row{
|
||||
color.GreenString(domain.ID),
|
||||
color.GreenString(domain.Domain),
|
||||
color.GreenString(domain.URI),
|
||||
color.GreenString(domain.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t4.SetOutputMirror(os.Stdout)
|
||||
t4.Render()
|
||||
|
||||
t5 := table.NewWriter()
|
||||
t5.SetTitle("API Keys")
|
||||
t5.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
|
||||
for _, key := range info.APIKeys {
|
||||
t5.AppendRow(table.Row{
|
||||
color.GreenString(key.ID),
|
||||
color.GreenString(key.Description),
|
||||
color.GreenString(key.OwnerID),
|
||||
color.GreenString(key.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t5.SetOutputMirror(os.Stdout)
|
||||
t5.Render()
|
||||
|
||||
t6 := table.NewWriter()
|
||||
t6.SetTitle("Authtokens")
|
||||
t6.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
|
||||
for _, token := range info.Authtokens {
|
||||
t6.AppendRow(table.Row{
|
||||
color.GreenString(token.ID),
|
||||
color.GreenString(token.Description),
|
||||
color.GreenString(token.OwnerID),
|
||||
color.GreenString(token.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t6.SetOutputMirror(os.Stdout)
|
||||
t6.Render()
|
||||
|
||||
t7 := table.NewWriter()
|
||||
t7.SetTitle("SSH Credentials")
|
||||
t7.AppendHeader(table.Row{"ID", "Description", "Owner ID", "Created At"})
|
||||
for _, key := range info.SSHCredentials {
|
||||
t7.AppendRow(table.Row{
|
||||
color.GreenString(key.ID),
|
||||
color.GreenString(key.Description),
|
||||
color.GreenString(key.OwnerID),
|
||||
color.GreenString(key.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t7.SetOutputMirror(os.Stdout)
|
||||
t7.Render()
|
||||
|
||||
t8 := table.NewWriter()
|
||||
t8.SetTitle("Bot Users")
|
||||
t8.AppendHeader(table.Row{"ID", "Name", "Is Active", "Created At"})
|
||||
for _, endpoint := range info.BotUsers {
|
||||
isActive := "No"
|
||||
if endpoint.Active {
|
||||
isActive = "Yes"
|
||||
}
|
||||
t8.AppendRow(table.Row{
|
||||
color.GreenString(endpoint.ID),
|
||||
color.GreenString(endpoint.Name),
|
||||
color.GreenString(isActive),
|
||||
color.GreenString(endpoint.CreatedAt),
|
||||
})
|
||||
}
|
||||
|
||||
t8.SetOutputMirror(os.Stdout)
|
||||
t8.Render()
|
||||
|
||||
fmt.Printf("%s: https://www.ngrok.com/developers/documentation\n\n", color.GreenString("Ref"))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package ngrok
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
)
|
||||
|
||||
//go:embed expected_output.json
|
||||
var expectedOutput []byte
|
||||
|
||||
func TestAnalyzer_Analyze(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
|
||||
key := testSecrets.MustGetField("NGROK")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secret string
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid ngrok credentials",
|
||||
secret: key,
|
||||
want: string(expectedOutput),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := Analyzer{Cfg: &config.Config{}}
|
||||
got, err := a.Analyze(ctx, map[string]string{
|
||||
"key": tt.secret,
|
||||
})
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
// bindings need to be in the same order to be comparable
|
||||
sortBindings(got.Bindings)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// bindings need to be in the same order to be comparable
|
||||
sortBindings(wantObj.Bindings)
|
||||
|
||||
// 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, wantIndented []byte
|
||||
gotIndented, err = json.MarshalIndent(got, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal got to indented JSON: %s", err)
|
||||
}
|
||||
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
|
||||
if err != nil {
|
||||
t.Fatalf("could not marshal want to indented JSON: %s", err)
|
||||
}
|
||||
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to sort bindings
|
||||
func sortBindings(bindings []analyzers.Binding) {
|
||||
sort.SliceStable(bindings, func(i, j int) bool {
|
||||
if bindings[i].Resource.Type == bindings[j].Resource.Type {
|
||||
return bindings[i].Permission.Value < bindings[j].Permission.Value
|
||||
}
|
||||
return bindings[i].Resource.Type < bindings[j].Resource.Type
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
package ngrok
|
||||
|
||||
import "errors"
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
FullAccess Permission = iota
|
||||
)
|
||||
|
||||
var (
|
||||
PermissionStrings = map[Permission]string{
|
||||
FullAccess: "full_access",
|
||||
}
|
||||
|
||||
StringToPermission = map[string]Permission{
|
||||
"full_access": FullAccess,
|
||||
}
|
||||
|
||||
PermissionIDs = map[Permission]int{
|
||||
FullAccess: 1,
|
||||
}
|
||||
|
||||
IdToPermission = map[int]Permission{
|
||||
1: FullAccess,
|
||||
}
|
||||
)
|
||||
|
||||
// ToString converts a Permission enum to its string representation
|
||||
func (p Permission) ToString() (string, error) {
|
||||
if str, ok := PermissionStrings[p]; ok {
|
||||
return str, nil
|
||||
}
|
||||
return "", errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// ToID converts a Permission enum to its ID
|
||||
func (p Permission) ToID() (int, error) {
|
||||
if id, ok := PermissionIDs[p]; ok {
|
||||
return id, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission")
|
||||
}
|
||||
|
||||
// PermissionFromString converts a string representation to its Permission enum
|
||||
func PermissionFromString(s string) (Permission, error) {
|
||||
if p, ok := StringToPermission[s]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission string")
|
||||
}
|
||||
|
||||
// PermissionFromID converts an ID to its Permission enum
|
||||
func PermissionFromID(id int) (Permission, error) {
|
||||
if p, ok := IdToPermission[id]; ok {
|
||||
return p, nil
|
||||
}
|
||||
return 0, errors.New("invalid permission ID")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
permissions:
|
||||
- full_access
|
||||
@@ -0,0 +1,374 @@
|
||||
package ngrok
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
|
||||
)
|
||||
|
||||
const (
|
||||
ngrokAPIBaseURL = "https://api.ngrok.com"
|
||||
reservedAddressesEndpoint = "/reserved_addrs"
|
||||
domainsEndpoint = "/reserved_domains"
|
||||
endpointsEndpoint = "/endpoints"
|
||||
apiKeysEndpoint = "/api_keys"
|
||||
sshCredentialsEndpoint = "/ssh_credentials"
|
||||
authtokensEndpoint = "/credentials"
|
||||
botUsersEndpoint = "/bot_users"
|
||||
)
|
||||
|
||||
func determineAccountType(client *http.Client, info *secretInfo, key string) error {
|
||||
// To determine if the account is free or paid, we can attempt to create a reserved address
|
||||
// Reserved Addresses are only available to paid accounts, so if the response contains the
|
||||
// error "ERR_NGROK_501", we can assume the account is on a free plan.
|
||||
// Ref: https://ngrok.com/docs/errors/err_ngrok_501
|
||||
|
||||
const errorCodeFreeAccount = "ERR_NGROK_501"
|
||||
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, reservedAddressesEndpoint)
|
||||
body, statusCode, err := makeAPIRequest(client, http.MethodPost, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The response should be a 400 Bad Request based on our request. Any other status code indicates an error.
|
||||
if statusCode != http.StatusBadRequest {
|
||||
return fmt.Errorf("unexpected status code: %d while determining account type", statusCode)
|
||||
}
|
||||
|
||||
switch statusCode {
|
||||
case http.StatusBadRequest:
|
||||
if strings.Contains(string(body), errorCodeFreeAccount) {
|
||||
info.AccountType = AccountFree
|
||||
} else {
|
||||
info.AccountType = AccountPaid
|
||||
}
|
||||
case http.StatusForbidden:
|
||||
return fmt.Errorf("invalid API key or access forbidden: %s", body)
|
||||
default:
|
||||
return fmt.Errorf("unexpected status code: %d while determining account type", statusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAllResources(client *http.Client, info *secretInfo, key string) error {
|
||||
// Fetch all resources and populate the secretInfo struct with the data
|
||||
// This is a placeholder function. The actual implementation will depend on the API endpoints and response formats.
|
||||
// For example, you might want to call different endpoints to fetch API keys, SSH keys, etc.
|
||||
|
||||
// Example of populating API keys
|
||||
if err := populateEndpoints(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := populateDomains(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := populateAPIKeys(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := populateAuthtokens(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := populateSSHCredentials(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := populateBotUsers(client, info, key); err != nil {
|
||||
return err
|
||||
}
|
||||
populateUsers(info)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateEndpoints(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, endpointsEndpoint)
|
||||
info.Endpoints = []endpoint{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Endpoints = append(info.Endpoints, res.Endpoints...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAPIKeys(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, apiKeysEndpoint)
|
||||
info.APIKeys = []apiKey{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.APIKeys = append(info.APIKeys, res.APIKeys...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateSSHCredentials(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, sshCredentialsEndpoint)
|
||||
info.SSHCredentials = []sshCredential{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.SSHCredentials = append(info.SSHCredentials, res.SSHCredentials...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateAuthtokens(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, authtokensEndpoint)
|
||||
info.Authtokens = []authtoken{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Authtokens = append(info.Authtokens, res.Authtokens...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateDomains(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, domainsEndpoint)
|
||||
info.Domains = []domain{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.Domains = append(info.Domains, res.Domains...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateBotUsers(client *http.Client, info *secretInfo, key string) error {
|
||||
url := fmt.Sprintf("%s%s", ngrokAPIBaseURL, botUsersEndpoint)
|
||||
info.BotUsers = []botUser{}
|
||||
for {
|
||||
res, err := fetchResources(client, url, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info.BotUsers = append(info.BotUsers, res.BotUsers...)
|
||||
url = res.NextPageURI
|
||||
if url == "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func fetchResources(client *http.Client, url string, key string) (*paginatedResponse, error) {
|
||||
for {
|
||||
body, status, err := makeAPIRequest(client, http.MethodGet, url, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch status {
|
||||
case http.StatusOK:
|
||||
var resource paginatedResponse
|
||||
if err := json.Unmarshal(body, &resource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resource, nil
|
||||
case http.StatusForbidden:
|
||||
return nil, fmt.Errorf("invalid API key or access forbidden: %s", body)
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected status code: %d", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func populateUsers(info *secretInfo) {
|
||||
// Creating a map to track unique user IDs to help in avoiding
|
||||
// duplicates when adding users to the info.Users slice
|
||||
uniqueUserIDs := map[string]bool{}
|
||||
|
||||
processOwnerID := func(ownerID string) {
|
||||
if strings.HasPrefix(ownerID, "usr_") {
|
||||
if uniqueUserIDs[ownerID] {
|
||||
return
|
||||
}
|
||||
uniqueUserIDs[ownerID] = true
|
||||
info.Users = append(info.Users, user{ID: ownerID})
|
||||
}
|
||||
}
|
||||
|
||||
for _, token := range info.Authtokens {
|
||||
processOwnerID(token.OwnerID)
|
||||
}
|
||||
|
||||
for _, sshKey := range info.SSHCredentials {
|
||||
processOwnerID(sshKey.OwnerID)
|
||||
}
|
||||
|
||||
for _, apiKey := range info.APIKeys {
|
||||
processOwnerID(apiKey.OwnerID)
|
||||
}
|
||||
}
|
||||
|
||||
func makeAPIRequest(client *http.Client, method string, url string, key string) ([]byte, int, error) {
|
||||
var reqBody io.Reader = nil
|
||||
if method == http.MethodPost {
|
||||
reqBody = strings.NewReader("{}")
|
||||
}
|
||||
req, err := http.NewRequest(method, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+key)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Ngrok-Version", "2")
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
_, _ = io.Copy(io.Discard, res.Body)
|
||||
_ = res.Body.Close()
|
||||
}()
|
||||
|
||||
bodyBytes, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
return bodyBytes, res.StatusCode, nil
|
||||
}
|
||||
|
||||
// Functions to create analyzers.Resource objects for different resource types
|
||||
|
||||
func createEndpointResource(endpoint endpoint) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: endpoint.ID,
|
||||
FullyQualifiedName: "endpoint/" + endpoint.ID,
|
||||
Type: "endpoint",
|
||||
Metadata: map[string]any{
|
||||
"region": endpoint.Region,
|
||||
"host": endpoint.Host,
|
||||
"port": endpoint.Port,
|
||||
"publicURL": endpoint.PublicURL,
|
||||
"proto": endpoint.Proto,
|
||||
"hostport": endpoint.Hostport,
|
||||
"type": endpoint.Type,
|
||||
"uri": endpoint.URI,
|
||||
"bindings": endpoint.Bindings,
|
||||
"metadata": endpoint.Metadata,
|
||||
"createdAt": endpoint.CreatedAt,
|
||||
"updatedAt": endpoint.UpdatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createDomainResource(domain domain) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: domain.ID,
|
||||
FullyQualifiedName: "domain/" + domain.ID,
|
||||
Type: "domain",
|
||||
Metadata: map[string]any{
|
||||
"uri": domain.URI,
|
||||
"domain": domain.Domain,
|
||||
"metadata": domain.Metadata,
|
||||
"createdAt": domain.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createAPIKeyResource(apiKey apiKey) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: apiKey.ID,
|
||||
FullyQualifiedName: "api_key/" + apiKey.ID,
|
||||
Type: "api_key",
|
||||
Metadata: map[string]any{
|
||||
"uri": apiKey.URI,
|
||||
"description": apiKey.Description,
|
||||
"metadata": apiKey.Metadata,
|
||||
"ownerID": apiKey.OwnerID,
|
||||
"createdAt": apiKey.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
func createSSHKeyResource(sshCredential sshCredential) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: sshCredential.ID,
|
||||
FullyQualifiedName: "ssh_credential/" + sshCredential.ID,
|
||||
Type: "ssh_credential",
|
||||
Metadata: map[string]any{
|
||||
"uri": sshCredential.URI,
|
||||
"description": sshCredential.Description,
|
||||
"publicKey": sshCredential.PublicKey,
|
||||
"metadata": sshCredential.Metadata,
|
||||
"acl": sshCredential.ACL,
|
||||
"ownerID": sshCredential.OwnerID,
|
||||
"createdAt": sshCredential.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createAuthtokenResource(authtoken authtoken) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: authtoken.ID,
|
||||
FullyQualifiedName: "authtoken/" + authtoken.ID,
|
||||
Type: "authtoken",
|
||||
Metadata: map[string]any{
|
||||
"uri": authtoken.URI,
|
||||
"description": authtoken.Description,
|
||||
"metadata": authtoken.Metadata,
|
||||
"acl": authtoken.ACL,
|
||||
"ownerID": authtoken.OwnerID,
|
||||
"createdAt": authtoken.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createBotUserResource(botUser botUser) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: botUser.ID,
|
||||
FullyQualifiedName: "bot_user/" + botUser.ID,
|
||||
Type: "bot_user",
|
||||
Metadata: map[string]any{
|
||||
"uri": botUser.URI,
|
||||
"name": botUser.Name,
|
||||
"active": botUser.Active,
|
||||
"createdAt": botUser.CreatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func createUserResource(user user) analyzers.Resource {
|
||||
return analyzers.Resource{
|
||||
Name: user.ID,
|
||||
FullyQualifiedName: "user/" + user.ID,
|
||||
Type: "user",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package ngrok
|
||||
|
||||
type ngrokResource struct {
|
||||
Name string
|
||||
IsPaidFeature bool
|
||||
}
|
||||
|
||||
var ngrokResources = []ngrokResource{
|
||||
{
|
||||
Name: "Endpoints",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Domains",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Reserved Addresses",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "TLS Certificates",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Kubernetes Operators",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Certificate Authorities",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "IP Policies",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Policy Rules",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Application Users",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Application Sessions",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Agent Ingress",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "Tunnels",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Tunnel Sessions",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Event Destinations",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Event Sources",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Event Subscriptions",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "IP Restrictions",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "API Keys",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "SSH Credentials",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Authtokens",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "Bot Users",
|
||||
IsPaidFeature: false,
|
||||
},
|
||||
{
|
||||
Name: "SSH Certificate Authorities",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "SSH Host Certificates",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
{
|
||||
Name: "SSH User Certificates",
|
||||
IsPaidFeature: true,
|
||||
},
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/monday"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mysql"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/netlify"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/ngrok"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/notion"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/openai"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/opsgenie"
|
||||
@@ -130,5 +131,7 @@ func Run(keyType string, secretInfo SecretInfo) {
|
||||
fastly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "monday":
|
||||
monday.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
case "ngrok":
|
||||
ngrok.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
isVerified, vErr := verifyMatch(ctx, s.client, token)
|
||||
r.Verified = isVerified
|
||||
r.SetVerificationError(vErr, token)
|
||||
if isVerified {
|
||||
r.AnalysisInfo = map[string]string{"key": token}
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, r)
|
||||
|
||||
Reference in New Issue
Block a user