Files
trufflehog/pkg/analyzer/analyzers/postman/postman.go
Bill Rich abecab0d8f
Lint / golangci-lint (push) Waiting to run
Lint / semgrep (push) Waiting to run
Release / Release (push) Waiting to run
Scan for secrets / test (push) Waiting to run
Snifftest / Run Snifftest (push) Waiting to run
Test / test (push) Waiting to run
Test / test-community (push) Waiting to run
Remove proto (#3489)
Only used for tracking analyzer types
2024-10-21 17:16:26 -07:00

270 lines
6.8 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go postman
package postman
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"github.com/fatih/color"
"github.com/jedib0t/go-pretty/table"
"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
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypePostman }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
key, ok := credInfo["key"]
if !ok {
return nil, fmt.Errorf("missing key in credInfo")
}
info, err := AnalyzePermissions(a.Cfg, key)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypePostman,
Metadata: nil,
UnboundedResources: []analyzers.Resource{},
}
resource := analyzers.Resource{
Name: info.User.User.FullName,
FullyQualifiedName: info.User.User.Email,
Type: "user",
Metadata: map[string]any{
"role": strings.Join(info.User.User.Roles, ","),
"username": info.User.User.Username,
"email": info.User.User.Email,
"team_name": info.User.User.TeamName,
"team_domain": info.User.User.TeamDomain,
},
}
permissions := bakePermissions(info.User.User.Roles)
// bind all permissions with resources
result.Bindings = analyzers.BindAllPermissions(resource, permissions...)
for _, workspace := range info.Workspace.Workspaces {
result.UnboundedResources = append(result.UnboundedResources, analyzers.Resource{
Name: workspace.Name,
FullyQualifiedName: workspace.ID,
Type: "workspace",
Metadata: map[string]any{
"id": workspace.ID,
"type": workspace.Type,
"visibility": workspace.Visibility,
},
})
}
return &result
}
func bakePermissions(roles []string) []analyzers.Permission {
permissionMap := map[Permission]struct{}{}
for _, role := range roles {
permissions, ok := rolePermission[role]
if !ok {
continue
}
for _, permission := range permissions {
permissionMap[permission] = struct{}{}
}
}
permissions := make([]analyzers.Permission, 0, len(permissionMap))
for perm := range permissionMap {
permStr, err := perm.ToString()
if err != nil {
continue
}
permissions = append(permissions, analyzers.Permission{
Value: permStr,
Parent: nil,
})
}
return permissions
}
type UserInfoJSON struct {
User struct {
Username string `json:"username"`
Email string `json:"email"`
FullName string `json:"fullName"`
Roles []string `json:"roles"`
TeamName string `json:"teamName"`
TeamDomain string `json:"teamDomain"`
} `json:"user"`
}
type WorkspaceJSON struct {
Workspaces []struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Visibility string `json:"visibility"`
} `json:"workspaces"`
}
func getUserInfo(cfg *config.Config, key string) (UserInfoJSON, error) {
var me UserInfoJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/me", nil)
if err != nil {
return me, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return me, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&me)
}
return me, err
}
func getWorkspaces(cfg *config.Config, key string) (WorkspaceJSON, error) {
var workspaces WorkspaceJSON
client := analyzers.NewAnalyzeClient(cfg)
req, err := http.NewRequest("GET", "https://api.getpostman.com/workspaces", nil)
if err != nil {
return workspaces, err
}
req.Header.Add("X-API-Key", key)
// send request
resp, err := client.Do(req)
if err != nil {
return workspaces, err
}
// read response
defer resp.Body.Close()
// if status code is 200, decode response
if resp.StatusCode == 200 {
err = json.NewDecoder(resp.Body).Decode(&workspaces)
}
return workspaces, err
}
type SecretInfo struct {
User UserInfoJSON
Workspace WorkspaceJSON
WorkspaceError error
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[!] Valid Postman API Key")
printUserInfo(info.User)
if info.WorkspaceError != nil {
color.Red("[x] Error Fetching Workspaces: %s", info.WorkspaceError.Error())
} else if len(info.Workspace.Workspaces) == 0 {
color.Red("[x] No Workspaces Found")
} else {
printWorkspaces(info.Workspace)
}
}
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
// validate key & get user info
me, err := getUserInfo(cfg, key)
if err != nil {
return nil, err
}
if me.User.Username == "" {
return nil, fmt.Errorf("Invalid Postman API Key")
}
// get workspaces, if there is error user with empty workspaces will be returned
workspaces, err := getWorkspaces(cfg, key)
return &SecretInfo{
User: me,
Workspace: workspaces,
WorkspaceError: err,
}, nil
}
func printUserInfo(me UserInfoJSON) {
color.Yellow("\n[i] User Information")
color.Green("Username: " + me.User.Username)
color.Green("Email: " + me.User.Email)
color.Green("Full Name: " + me.User.FullName)
color.Yellow("\n[i] Team Information")
color.Green("Name: " + me.User.TeamName)
color.Green("Domain: https://" + me.User.TeamDomain + ".postman.co")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Scope", "Permissions"})
for _, role := range me.User.Roles {
t.AppendRow([]interface{}{color.GreenString(role), color.GreenString(roleDescriptions[role])})
}
t.Render()
fmt.Println("Reference: https://learning.postman.com/docs/collaborating-in-postman/roles-and-permissions/#team-roles")
}
func printWorkspaces(workspaces WorkspaceJSON) {
color.Yellow("[i] Accessible Workspaces")
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Workspace Name", "Type", "Visibility", "Link"})
for _, workspace := range workspaces.Workspaces {
t.AppendRow([]interface{}{color.GreenString(workspace.Name), color.GreenString(workspace.Type), color.GreenString(workspace.Visibility), color.GreenString("https://go.postman.co/workspaces/" + workspace.ID)})
}
t.Render()
}