[analyze] Add analyzer for Postman (#3180)

* implement analyzer interface for postman and add unit test

* analyzer interface inplementation for postman

linked detector with analyzer for postman
add permission for postman

* [fix] linter in postman

* considered Miccah comments about fullyqualifiedName and code refactoring.

* moved want result to expected output file.

---------

Co-authored-by: Abdul Basit <[email protected]>
This commit is contained in:
Abdul Basit
2024-09-04 15:40:12 -07:00
committed by GitHub
co-authored by Abdul Basit
parent 02c508da11
commit 5ce1578a6f
8 changed files with 577 additions and 2 deletions
@@ -0,0 +1,76 @@
{
"AnalyzerType": 13,
"Bindings": [
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "[email protected]",
"Type": "user",
"Metadata": {
"email": "[email protected]",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "usage_data:view",
"Parent": null
}
},
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "[email protected]",
"Type": "user",
"Metadata": {
"email": "[email protected]",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "team_workspaces:create",
"Parent": null
}
},
{
"Resource": {
"Name": "rendy",
"FullyQualifiedName": "[email protected]",
"Type": "user",
"Metadata": {
"email": "[email protected]",
"role": "user",
"team_domain": "",
"team_name": "",
"username": "rendyplayground"
},
"Parent": null
},
"Permission": {
"Value": "team_workspaces:view",
"Parent": null
}
}
],
"UnboundedResources": [
{
"Name": "My Workspace",
"FullyQualifiedName": "4d06fc0c-6402-4a26-857d-80787b10eabf",
"Type": "workspace",
"Metadata": {
"id": "4d06fc0c-6402-4a26-857d-80787b10eabf",
"type": "personal",
"visibility": "personal"
},
"Parent": null
}
],
"Metadata": null
}
@@ -0,0 +1,181 @@
// Code generated by go generate; DO NOT EDIT.
package postman
import "errors"
type Permission int
const (
NoAccess Permission = iota
UserAdd Permission = iota
UserRemove Permission = iota
TeamAdminManage Permission = iota
TeamDevelopersManage Permission = iota
SsoManage Permission = iota
CustomDomainAdd Permission = iota
CustomDomainEdit Permission = iota
CustomDomainRemove Permission = iota
AuditLogsView Permission = iota
UsageDataView Permission = iota
BillingMembersManage Permission = iota
PaymentManage Permission = iota
PlanUpdate Permission = iota
TeamWorkspacesView Permission = iota
TeamWorkspacesCreate Permission = iota
TeamPublicProfileEnable Permission = iota
TeamPrivateApiNetworkManage Permission = iota
ParternerWorkspaceView Permission = iota
ParternerWorkspaceManage Permission = iota
ParternerWorkspaceVisibilityManage Permission = iota
PartnersManage Permission = iota
FlowAdd Permission = iota
FlowEdit Permission = iota
FlowRun Permission = iota
FlowPublish Permission = iota
)
var (
PermissionStrings = map[Permission]string{
UserAdd: "user:add",
UserRemove: "user:remove",
TeamAdminManage: "team_admin:manage",
TeamDevelopersManage: "team_developers:manage",
SsoManage: "sso:manage",
CustomDomainAdd: "custom_domain:add",
CustomDomainEdit: "custom_domain:edit",
CustomDomainRemove: "custom_domain:remove",
AuditLogsView: "audit_logs:view",
UsageDataView: "usage_data:view",
BillingMembersManage: "billing_members:manage",
PaymentManage: "payment:manage",
PlanUpdate: "plan:update",
TeamWorkspacesView: "team_workspaces:view",
TeamWorkspacesCreate: "team_workspaces:create",
TeamPublicProfileEnable: "team_public_profile:enable",
TeamPrivateApiNetworkManage: "team_private_api_network:manage",
ParternerWorkspaceView: "parterner_workspace:view",
ParternerWorkspaceManage: "parterner_workspace:manage",
ParternerWorkspaceVisibilityManage: "parterner_workspace_visibility:manage",
PartnersManage: "partners:manage",
FlowAdd: "flow:add",
FlowEdit: "flow:edit",
FlowRun: "flow:run",
FlowPublish: "flow:publish",
}
StringToPermission = map[string]Permission{
"user:add": UserAdd,
"user:remove": UserRemove,
"team_admin:manage": TeamAdminManage,
"team_developers:manage": TeamDevelopersManage,
"sso:manage": SsoManage,
"custom_domain:add": CustomDomainAdd,
"custom_domain:edit": CustomDomainEdit,
"custom_domain:remove": CustomDomainRemove,
"audit_logs:view": AuditLogsView,
"usage_data:view": UsageDataView,
"billing_members:manage": BillingMembersManage,
"payment:manage": PaymentManage,
"plan:update": PlanUpdate,
"team_workspaces:view": TeamWorkspacesView,
"team_workspaces:create": TeamWorkspacesCreate,
"team_public_profile:enable": TeamPublicProfileEnable,
"team_private_api_network:manage": TeamPrivateApiNetworkManage,
"parterner_workspace:view": ParternerWorkspaceView,
"parterner_workspace:manage": ParternerWorkspaceManage,
"parterner_workspace_visibility:manage": ParternerWorkspaceVisibilityManage,
"partners:manage": PartnersManage,
"flow:add": FlowAdd,
"flow:edit": FlowEdit,
"flow:run": FlowRun,
"flow:publish": FlowPublish,
}
PermissionIDs = map[Permission]int{
UserAdd: 0,
UserRemove: 1,
TeamAdminManage: 2,
TeamDevelopersManage: 3,
SsoManage: 4,
CustomDomainAdd: 5,
CustomDomainEdit: 6,
CustomDomainRemove: 7,
AuditLogsView: 8,
UsageDataView: 9,
BillingMembersManage: 10,
PaymentManage: 11,
PlanUpdate: 12,
TeamWorkspacesView: 13,
TeamWorkspacesCreate: 14,
TeamPublicProfileEnable: 15,
TeamPrivateApiNetworkManage: 16,
ParternerWorkspaceView: 17,
ParternerWorkspaceManage: 18,
ParternerWorkspaceVisibilityManage: 19,
PartnersManage: 20,
FlowAdd: 21,
FlowEdit: 22,
FlowRun: 23,
FlowPublish: 24,
}
IdToPermission = map[int]Permission{
0: UserAdd,
1: UserRemove,
2: TeamAdminManage,
3: TeamDevelopersManage,
4: SsoManage,
5: CustomDomainAdd,
6: CustomDomainEdit,
7: CustomDomainRemove,
8: AuditLogsView,
9: UsageDataView,
10: BillingMembersManage,
11: PaymentManage,
12: PlanUpdate,
13: TeamWorkspacesView,
14: TeamWorkspacesCreate,
15: TeamPublicProfileEnable,
16: TeamPrivateApiNetworkManage,
17: ParternerWorkspaceView,
18: ParternerWorkspaceManage,
19: ParternerWorkspaceVisibilityManage,
20: PartnersManage,
21: FlowAdd,
22: FlowEdit,
23: FlowRun,
24: FlowPublish,
}
)
// 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,27 @@
permissions:
- user:add
- user:remove
- team_admin:manage
- team_developers:manage
- sso:manage
- custom_domain:add
- custom_domain:edit
- custom_domain:remove
- audit_logs:view
- usage_data:view
- billing_members:manage
- payment:manage
- plan:update
- team_workspaces:view
- team_workspaces:create
- team_public_profile:enable
- team_private_api_network:manage
- parterner_workspace:view
- parterner_workspace:manage
- parterner_workspace_visibility:manage
- partners:manage
- flow:add
- flow:edit
- flow:run
- flow:publish
+97
View File
@@ -1,3 +1,4 @@
//go:generate generate_permissions permissions.yaml permissions.go postman
package postman
import (
@@ -5,13 +6,109 @@ import (
"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/analyzer/pb/analyzerpb"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzerpb.AnalyzerType { return analyzerpb.AnalyzerType_Postman }
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: analyzerpb.AnalyzerType_Postman,
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"`
@@ -0,0 +1,100 @@
package postman
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.Second*15)
defer cancel()
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors3")
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 Postman key",
key: testSecrets.MustGetField("POSTMAN_TOKEN"),
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.key})
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.Name == bindings[j].Resource.Name {
return bindings[i].Permission.Value < bindings[j].Permission.Value
}
return bindings[i].Resource.Name < bindings[j].Resource.Name
})
}
+90
View File
@@ -11,3 +11,93 @@ var roleDescriptions = map[string]string{
"guest": "Views collections and sends requests in collections that have been shared with them. This role can't be directly assigned to a user.",
"flow-editor": "(Basic and Professional plans only) - Can create, edit, run, and publish Postman Flows.",
}
var rolePermission = map[string][]Permission{
"super-admin": {
UserAdd,
UserRemove,
TeamAdminManage,
TeamDevelopersManage,
SsoManage,
CustomDomainAdd,
CustomDomainEdit,
CustomDomainRemove,
AuditLogsView,
UsageDataView,
BillingMembersManage,
PaymentManage,
PlanUpdate,
TeamWorkspacesView,
TeamWorkspacesCreate,
TeamPublicProfileEnable,
TeamPrivateApiNetworkManage,
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
"admin": {
UserAdd,
UserRemove,
TeamAdminManage,
TeamDevelopersManage,
SsoManage,
CustomDomainAdd,
CustomDomainEdit,
CustomDomainRemove,
AuditLogsView,
UsageDataView,
BillingMembersManage,
TeamPublicProfileEnable,
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
"billing": {
UsageDataView,
BillingMembersManage,
PaymentManage,
PlanUpdate,
},
"user": {
UsageDataView,
TeamWorkspacesCreate,
TeamWorkspacesView,
},
"community-manager": {
CustomDomainAdd,
CustomDomainEdit,
AuditLogsView,
UsageDataView,
TeamWorkspacesView,
TeamWorkspacesCreate,
TeamPublicProfileEnable,
},
"partner-manager": {
PartnersManage,
ParternerWorkspaceManage,
ParternerWorkspaceView,
ParternerWorkspaceVisibilityManage,
},
"partner": {
ParternerWorkspaceView,
},
"guest": {
TeamWorkspacesView,
},
"flow-editor": {
FlowAdd,
FlowEdit,
FlowRun,
FlowPublish,
},
}
+5 -1
View File
@@ -3,10 +3,11 @@ package postman
import (
"context"
"fmt"
regexp "github.com/wasilibs/go-re2"
"net/http"
"strings"
regexp "github.com/wasilibs/go-re2"
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
@@ -56,6 +57,9 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
isVerified, verificationErr := verifyPostman(ctx, client, resMatch)
s1.Verified = isVerified
s1.SetVerificationError(verificationErr, resMatch)
s1.AnalysisInfo = map[string]string{
"key": resMatch,
}
}
results = append(results, s1)
+1 -1
View File
@@ -133,7 +133,7 @@ func TestPostman_FromChunk(t *testing.T) {
t.Errorf("Postman.FromData() error = %v, wantErr %v", got[i].VerificationError(), tt.wantVerificationErr)
}
}
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("Postman.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
}