[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:
Nabeel Alam
2025-05-13 15:57:26 +05:00
committed by GitHub
parent 57856affc6
commit 9551a08837
11 changed files with 1034 additions and 0 deletions
+2
View File
@@ -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}
+88
View File
@@ -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
}
+292
View File
@@ -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"))
}
+103
View File
@@ -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
+374
View File
@@ -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",
}
}
+105
View File
@@ -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,
},
}
+3
View File
@@ -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"])
}
}
+3
View File
@@ -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)