From f573da368a503b5004f24ff55f026213d9953e85 Mon Sep 17 00:00:00 2001 From: Kashif Khan <70996046+kashifkhan0771@users.noreply.github.com> Date: Thu, 8 May 2025 18:37:56 +0500 Subject: [PATCH] Monday App Analyzer (#4120) * initial commit * updated monday detector * close resp body --- pkg/analyzer/analyzers/analyzers.go | 2 + pkg/analyzer/analyzers/monday/monday.go | 149 ++++++++++ pkg/analyzer/analyzers/monday/monday_test.go | 102 +++++++ pkg/analyzer/analyzers/monday/permissions.go | 61 ++++ .../analyzers/monday/permissions.yaml | 2 + pkg/analyzer/analyzers/monday/query.go | 240 +++++++++++++++ pkg/analyzer/analyzers/monday/query.graphql | 75 +++++ .../analyzers/monday/result_output.json | 276 ++++++++++++++++++ pkg/analyzer/cli.go | 3 + pkg/detectors/monday/monday.go | 51 +++- .../monday/monday_integration_test.go | 1 + 11 files changed, 950 insertions(+), 12 deletions(-) create mode 100644 pkg/analyzer/analyzers/monday/monday.go create mode 100644 pkg/analyzer/analyzers/monday/monday_test.go create mode 100644 pkg/analyzer/analyzers/monday/permissions.go create mode 100644 pkg/analyzer/analyzers/monday/permissions.yaml create mode 100644 pkg/analyzer/analyzers/monday/query.go create mode 100644 pkg/analyzer/analyzers/monday/query.graphql create mode 100644 pkg/analyzer/analyzers/monday/result_output.json diff --git a/pkg/analyzer/analyzers/analyzers.go b/pkg/analyzer/analyzers/analyzers.go index 968113719..aa5382ce4 100644 --- a/pkg/analyzer/analyzers/analyzers.go +++ b/pkg/analyzer/analyzers/analyzers.go @@ -96,6 +96,7 @@ const ( AnalyzerTypePlaid AnalyzerTypeNetlify AnalyzerTypeFastly + AnalyzerTypeMonday // Add new items here with AnalyzerType prefix ) @@ -137,6 +138,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{ AnalyzerTypePlaid: "Plaid", AnalyzerTypeNetlify: "Netlify", AnalyzerTypeFastly: "Fastly", + AnalyzerTypeMonday: "Monday", // Add new mappings here } diff --git a/pkg/analyzer/analyzers/monday/monday.go b/pkg/analyzer/analyzers/monday/monday.go new file mode 100644 index 000000000..f66388132 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/monday.go @@ -0,0 +1,149 @@ +//go:generate generate_permissions permissions.yaml permissions.go monday +package monday + +import ( + "errors" + "fmt" + "os" + + "github.com/fatih/color" + "github.com/jedib0t/go-pretty/v6/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 +} + +type SecretInfo struct { + User Me + Account Account + Resources []MondayResource +} + +func (s *SecretInfo) appendResource(resource MondayResource) { + s.Resources = append(s.Resources, resource) +} + +type MondayResource struct { + ID string + Name string + Type string + MetaData map[string]string + Parent *MondayResource +} + +func (a Analyzer) Type() analyzers.AnalyzerType { + return analyzers.AnalyzerTypeMonday +} + +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 { + // just print the error in cli and continue as a partial success + color.Red("[x] Error : %s", err.Error()) + } + + if info == nil { + color.Red("[x] Error : %s", "No information found") + return + } + + color.Green("[!] Valid Monday Personal Access Token\n\n") + // print user information + printUser(info.User) + printResources(info.Resources) + + color.Yellow("\n[i] Expires: Never") +} + +func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) { + // create http client + client := analyzers.NewAnalyzeClientUnrestricted(cfg) + + var secretInfo = &SecretInfo{} + + // captureMondayData make a query to graphql API of monday to fetch all data and store it in secret info + if err := captureMondayData(client, key, secretInfo); err != nil { + return nil, err + } + + return secretInfo, nil +} + +// secretInfoToAnalyzerResult translate secret info to Analyzer Result +func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult { + if info == nil { + return nil + } + + result := analyzers.AnalyzerResult{ + AnalyzerType: analyzers.AnalyzerTypeMonday, + Metadata: map[string]any{}, + Bindings: make([]analyzers.Binding, 0), + } + + // extract information from resource to create bindings and append to result bindings + for _, resource := range info.Resources { + binding := analyzers.Binding{ + Resource: analyzers.Resource{ + Name: resource.Name, + FullyQualifiedName: fmt.Sprintf("%s/%s", resource.Type, resource.ID), // e.g: Board/123 + Type: resource.Type, + Metadata: map[string]any{}, // to avoid panic + }, + Permission: analyzers.Permission{ + Value: PermissionStrings[FullAccess], // token always has full access + }, + } + + for key, value := range resource.MetaData { + binding.Resource.Metadata[key] = value + } + + result.Bindings = append(result.Bindings, binding) + + } + + return &result +} + +// cli print functions +func printUser(user Me) { + color.Green("\n[i] User Information:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"ID", "Name", "Email", "Title", "Is Admin", "Is Guest"}) + t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Email), + color.GreenString(user.Title), color.GreenString(fmt.Sprintf("%t", user.IsAdmin)), color.GreenString(fmt.Sprintf("%t", user.IsGuest))}) + t.Render() +} + +func printResources(resources []MondayResource) { + color.Green("\n[i] Resources:") + t := table.NewWriter() + t.SetOutputMirror(os.Stdout) + t.AppendHeader(table.Row{"Name", "Type"}) + for _, resource := range resources { + t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)}) + } + t.Render() +} diff --git a/pkg/analyzer/analyzers/monday/monday_test.go b/pkg/analyzer/analyzers/monday/monday_test.go new file mode 100644 index 000000000..df16590cb --- /dev/null +++ b/pkg/analyzer/analyzers/monday/monday_test.go @@ -0,0 +1,102 @@ +package monday + +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 result_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("MONDAY_PAT") + + tests := []struct { + name string + key string + want []byte // JSON string + wantErr bool + }{ + { + name: "valid monday personal access token", + key: key, + want: 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 + }) +} diff --git a/pkg/analyzer/analyzers/monday/permissions.go b/pkg/analyzer/analyzers/monday/permissions.go new file mode 100644 index 000000000..8662212b9 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/permissions.go @@ -0,0 +1,61 @@ +// Code generated by go generate; DO NOT EDIT. +package monday + +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") +} diff --git a/pkg/analyzer/analyzers/monday/permissions.yaml b/pkg/analyzer/analyzers/monday/permissions.yaml new file mode 100644 index 000000000..8efa67e19 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/permissions.yaml @@ -0,0 +1,2 @@ +permissions: + - full_access diff --git a/pkg/analyzer/analyzers/monday/query.go b/pkg/analyzer/analyzers/monday/query.go new file mode 100644 index 000000000..a0e06ae83 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/query.go @@ -0,0 +1,240 @@ +package monday + +import ( + "bytes" + _ "embed" + "encoding/json" + "fmt" + "io" + "net/http" +) + +//go:embed query.graphql +var requestQuery string + +const ( + // resource types + TypeBoard = "Board" + TypeBoardGroup = "Board Group" + TypeBoardColumn = "Board Column" + TypeDoc = "Document" + TypeFolder = "Folder" + TypeTag = "Tag" + TypeTeam = "Team" + TypeWorkspace = "Workspace" +) + +type Request struct { + Query string `json:"query"` +} + +// Response is the Monday Graphql API response in case of success +type Response struct { + Data Data `json:"data"` +} + +type Data struct { + Me Me `json:"me"` + Account Account `json:"account"` + Users []User `json:"users"` + Boards []Board `json:"boards"` + Docs []Doc `json:"docs"` + Folders []EntityRef `json:"folders"` + Tags []EntityRef `json:"tags"` + Teams []EntityRef `json:"teams"` + Workspaces []Workspace `json:"workspaces"` +} + +type EntityRef struct { + ID string `json:"id"` + Name string `json:"name"` +} + +type Me struct { + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Title string `json:"title"` + IsAdmin bool `json:"is_admin"` + IsGuest bool `json:"is_guest"` + IsViewOnly bool `json:"is_view_only"` + IsPending bool `json:"is_pending"` + IsVerified bool `json:"is_verified"` + Teams []EntityRef `json:"teams"` +} + +type Account struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + Tier string `json:"tier"` +} + +type User struct { + Email string `json:"email"` + Account Account `json:"account"` +} + +type Board struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + Permissions string `json:"permissions"` + Groups []Group `json:"groups"` + Columns []Column `json:"column"` + Owners []EntityRef `json:"owner"` +} + +type Group struct { + Title string `json:"title"` + ID string `json:"id"` +} + +type Column struct { + ID string `json:"id"` + Title string `json:"title"` + Type string `json:"type"` +} + +type Doc struct { + ID string `json:"id"` + ObjectID string `json:"object_id"` + Name string `json:"name"` + CreatedBy EntityRef `json:"created_by"` +} + +type Workspace struct { + ID string `json:"id"` + Name string `json:"name"` + Kind string `json:"kind"` +} + +// captureMondayData send a request to Monday graphql API to get all data and capture it in secret info +func captureMondayData(client *http.Client, key string, secretInfo *SecretInfo) error { + jsonData, err := json.Marshal(Request{Query: requestQuery}) + if err != nil { + panic(err) + } + + req, err := http.NewRequest(http.MethodPost, "https://api.monday.com/v2", bytes.NewBuffer(jsonData)) + if err != nil { + return err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", key) + resp, err := client.Do(req) + if err != nil { + return err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + var apiResponse Response + + if err := json.NewDecoder(resp.Body).Decode(&apiResponse); err != nil { + return err + } + + // capture details in secret info + responseToSecretInfo(apiResponse, secretInfo) + + return nil + case http.StatusUnauthorized: + return fmt.Errorf("expired/invalid access token") + default: + return fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} + +// responseToSecretInfo translate api response to secret info +func responseToSecretInfo(apiResponse Response, secretInfo *SecretInfo) { + secretInfo.User = apiResponse.Data.Me + secretInfo.Account = apiResponse.Data.Account + + processBoards(apiResponse.Data.Boards, secretInfo) + processDocs(apiResponse.Data.Docs, secretInfo) + processSimpleEntities(apiResponse.Data.Folders, TypeFolder, secretInfo) + processSimpleEntities(apiResponse.Data.Tags, TypeTag, secretInfo) + processSimpleEntities(apiResponse.Data.Teams, TypeTeam, secretInfo) + processWorkspaces(apiResponse.Data.Workspaces, secretInfo) +} + +func processBoards(boards []Board, secretInfo *SecretInfo) { + for _, board := range boards { + boardResource := MondayResource{ + ID: board.ID, + Name: board.Name, + Type: TypeBoard, + MetaData: map[string]string{ + "state": board.State, + "permissions": board.Permissions, + }, + } + + secretInfo.appendResource(boardResource) + + // sub resources of board + for _, group := range board.Groups { + secretInfo.appendResource(MondayResource{ + ID: group.ID, + Name: group.Title, + Type: TypeBoardGroup, + Parent: &boardResource, + }) + } + + for _, column := range board.Columns { + secretInfo.appendResource(MondayResource{ + ID: column.ID, + Name: column.Title, + Type: TypeBoardColumn, + MetaData: map[string]string{ + "Column Type": column.Type, + }, + Parent: &boardResource, + }) + } + } +} + +func processDocs(docs []Doc, secretInfo *SecretInfo) { + for _, doc := range docs { + secretInfo.appendResource(MondayResource{ + ID: doc.ID, + Name: doc.Name, + Type: TypeDoc, + MetaData: map[string]string{ + "created_by": doc.CreatedBy.Name, + }, + }) + } +} + +func processSimpleEntities(entities []EntityRef, entityType string, secretInfo *SecretInfo) { + for _, entity := range entities { + secretInfo.appendResource(MondayResource{ + ID: entity.ID, + Name: entity.Name, + Type: entityType, + }) + } +} + +func processWorkspaces(workspaces []Workspace, secretInfo *SecretInfo) { + for _, workspace := range workspaces { + secretInfo.appendResource(MondayResource{ + ID: workspace.ID, + Name: workspace.Name, + Type: TypeWorkspace, + MetaData: map[string]string{ + "workspace_kind": workspace.Kind, + }, + }) + } +} diff --git a/pkg/analyzer/analyzers/monday/query.graphql b/pkg/analyzer/analyzers/monday/query.graphql new file mode 100644 index 000000000..8fe74d086 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/query.graphql @@ -0,0 +1,75 @@ +{ + me { + id + name + email + title + is_admin + is_guest + is_view_only + is_pending + is_verified + teams { + id + name + } + } + account { + id + name + slug + tier + } + users { + email + account { + name + id + } + } + boards { + id + name + state + permissions + groups { + title + id + } + columns { + id + title + type + } + owners { + id + name + } + } + docs { + id + object_id + name + created_by { + id + name + } + } + folders { + name + id + } + tags { + id + name + } + teams { + id + name + } + workspaces { + id + name + kind + } +} diff --git a/pkg/analyzer/analyzers/monday/result_output.json b/pkg/analyzer/analyzers/monday/result_output.json new file mode 100644 index 000000000..fadd7b589 --- /dev/null +++ b/pkg/analyzer/analyzers/monday/result_output.json @@ -0,0 +1,276 @@ +{ + "AnalyzerType": 35, + "Bindings": [ + { + "Resource": { + "Name": "All Tasks", + "FullyQualifiedName": "Board Group/topics", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Bugs Queue", + "FullyQualifiedName": "Board/2007387485", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Development Work", + "FullyQualifiedName": "Board Group/new_group24572", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Epics", + "FullyQualifiedName": "Board/2007387484", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Epics Backlog", + "FullyQualifiedName": "Board Group/new_group", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Getting Started", + "FullyQualifiedName": "Board/2007387480", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Getting Started", + "FullyQualifiedName": "Document/1126907", + "Type": "Document", + "Metadata": { + "created_by": "Truffle Security Detectors" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Group Title", + "FullyQualifiedName": "Board Group/topics", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Incoming Bugs", + "FullyQualifiedName": "Board Group/topics", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Managed in sprints", + "FullyQualifiedName": "Board Group/new_group", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "My Team", + "FullyQualifiedName": "Workspace/1857558", + "Type": "Workspace", + "Metadata": { + "workspace_kind": "open" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Q1 2025", + "FullyQualifiedName": "Board Group/new_group313", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Resolved", + "FullyQualifiedName": "Board Group/group_title", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Retrospectives", + "FullyQualifiedName": "Board/2007387481", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Sprint 1", + "FullyQualifiedName": "Board Group/group_title", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Sprints", + "FullyQualifiedName": "Board Group/topics", + "Type": "Board Group", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Sprints", + "FullyQualifiedName": "Board/2007387482", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Sprints order", + "FullyQualifiedName": "Board/2007387483", + "Type": "Board", + "Metadata": { + "permissions": "everyone", + "state": "active" + }, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + }, + { + "Resource": { + "Name": "Team OSS team", + "FullyQualifiedName": "Folder/6205823", + "Type": "Folder", + "Metadata": {}, + "Parent": null + }, + "Permission": { + "Value": "full_access", + "Parent": null + } + } + ], + "UnboundedResources": null, + "Metadata": {} +} \ No newline at end of file diff --git a/pkg/analyzer/cli.go b/pkg/analyzer/cli.go index cb3f72039..6136403a8 100644 --- a/pkg/analyzer/cli.go +++ b/pkg/analyzer/cli.go @@ -23,6 +23,7 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/launchdarkly" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailchimp" "github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers/mailgun" + "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/notion" @@ -127,5 +128,7 @@ func Run(keyType string, secretInfo SecretInfo) { netlify.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) case "fastly": fastly.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) + case "monday": + monday.AnalyzeAndPrintPermissions(secretInfo.Cfg, secretInfo.Parts["key"]) } } diff --git a/pkg/detectors/monday/monday.go b/pkg/detectors/monday/monday.go index eda044859..6acc746cb 100644 --- a/pkg/detectors/monday/monday.go +++ b/pkg/detectors/monday/monday.go @@ -2,6 +2,8 @@ package monday import ( "context" + "fmt" + "io" "net/http" "strings" @@ -45,18 +47,13 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result } if verify { - payload := strings.NewReader(`{"query":"{boards(limit:1){id name}}"}`) - req, err := http.NewRequestWithContext(ctx, "POST", "https://api.monday.com/v2", payload) - if err != nil { - continue - } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("Authorization", resMatch) - res, err := client.Do(req) - if err == nil { - defer res.Body.Close() - if res.StatusCode >= 200 && res.StatusCode < 300 { - s1.Verified = true + isVerified, verificationErr := verifyMondayPAT(ctx, client, resMatch) + s1.Verified = isVerified + s1.SetVerificationError(verificationErr) + + if s1.Verified { + s1.AnalysisInfo = map[string]string{ + "key": resMatch, } } } @@ -74,3 +71,33 @@ func (s Scanner) Type() detectorspb.DetectorType { func (s Scanner) Description() string { return "Monday.com is a work operating system that powers teams to run projects and workflows with confidence. Monday API keys can be used to access and modify data and workflows on the platform." } + +func verifyMondayPAT(ctx context.Context, client *http.Client, token string) (bool, error) { + payload := strings.NewReader(`{"query":"{boards(limit:1){id name}}"}`) + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.monday.com/v2", payload) + if err != nil { + return false, err + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Authorization", token) + + resp, err := client.Do(req) + if err != nil { + return false, err + } + + defer func() { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() + + switch resp.StatusCode { + case http.StatusOK: + return true, nil + case http.StatusUnauthorized: + return false, nil + default: + return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } +} diff --git a/pkg/detectors/monday/monday_integration_test.go b/pkg/detectors/monday/monday_integration_test.go index 659c980d5..be92662ab 100644 --- a/pkg/detectors/monday/monday_integration_test.go +++ b/pkg/detectors/monday/monday_integration_test.go @@ -95,6 +95,7 @@ func TestMonday_FromChunk(t *testing.T) { t.Fatalf("no raw secret present: \n %+v", got[i]) } got[i].Raw = nil + got[i].AnalysisInfo = nil } if diff := pretty.Compare(got, tt.want); diff != "" { t.Errorf("Monday.FromData() %s diff: (-got +want)\n%s", tt.name, diff)