Monday App Analyzer (#4120)
* initial commit * updated monday detector * close resp body
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
permissions:
|
||||
- full_access
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user