Skip to content

Commit f573da3

Browse files
Monday App Analyzer (#4120)
* initial commit * updated monday detector * close resp body
1 parent 18c009f commit f573da3

File tree

11 files changed

+950
-12
lines changed

11 files changed

+950
-12
lines changed

pkg/analyzer/analyzers/analyzers.go

+2
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ const (
9696
AnalyzerTypePlaid
9797
AnalyzerTypeNetlify
9898
AnalyzerTypeFastly
99+
AnalyzerTypeMonday
99100
// Add new items here with AnalyzerType prefix
100101
)
101102

@@ -137,6 +138,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
137138
AnalyzerTypePlaid: "Plaid",
138139
AnalyzerTypeNetlify: "Netlify",
139140
AnalyzerTypeFastly: "Fastly",
141+
AnalyzerTypeMonday: "Monday",
140142
// Add new mappings here
141143
}
142144

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go monday
2+
package monday
3+
4+
import (
5+
"errors"
6+
"fmt"
7+
"os"
8+
9+
"github.com/fatih/color"
10+
"github.com/jedib0t/go-pretty/v6/table"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
14+
)
15+
16+
var _ analyzers.Analyzer = (*Analyzer)(nil)
17+
18+
type Analyzer struct {
19+
Cfg *config.Config
20+
}
21+
22+
type SecretInfo struct {
23+
User Me
24+
Account Account
25+
Resources []MondayResource
26+
}
27+
28+
func (s *SecretInfo) appendResource(resource MondayResource) {
29+
s.Resources = append(s.Resources, resource)
30+
}
31+
32+
type MondayResource struct {
33+
ID string
34+
Name string
35+
Type string
36+
MetaData map[string]string
37+
Parent *MondayResource
38+
}
39+
40+
func (a Analyzer) Type() analyzers.AnalyzerType {
41+
return analyzers.AnalyzerTypeMonday
42+
}
43+
44+
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
45+
key, exist := credInfo["key"]
46+
if !exist {
47+
return nil, errors.New("key not found in credentials info")
48+
}
49+
50+
info, err := AnalyzePermissions(a.Cfg, key)
51+
if err != nil {
52+
return nil, err
53+
}
54+
55+
return secretInfoToAnalyzerResult(info), nil
56+
}
57+
58+
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
59+
info, err := AnalyzePermissions(cfg, key)
60+
if err != nil {
61+
// just print the error in cli and continue as a partial success
62+
color.Red("[x] Error : %s", err.Error())
63+
}
64+
65+
if info == nil {
66+
color.Red("[x] Error : %s", "No information found")
67+
return
68+
}
69+
70+
color.Green("[!] Valid Monday Personal Access Token\n\n")
71+
// print user information
72+
printUser(info.User)
73+
printResources(info.Resources)
74+
75+
color.Yellow("\n[i] Expires: Never")
76+
}
77+
78+
func AnalyzePermissions(cfg *config.Config, key string) (*SecretInfo, error) {
79+
// create http client
80+
client := analyzers.NewAnalyzeClientUnrestricted(cfg)
81+
82+
var secretInfo = &SecretInfo{}
83+
84+
// captureMondayData make a query to graphql API of monday to fetch all data and store it in secret info
85+
if err := captureMondayData(client, key, secretInfo); err != nil {
86+
return nil, err
87+
}
88+
89+
return secretInfo, nil
90+
}
91+
92+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
93+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
94+
if info == nil {
95+
return nil
96+
}
97+
98+
result := analyzers.AnalyzerResult{
99+
AnalyzerType: analyzers.AnalyzerTypeMonday,
100+
Metadata: map[string]any{},
101+
Bindings: make([]analyzers.Binding, 0),
102+
}
103+
104+
// extract information from resource to create bindings and append to result bindings
105+
for _, resource := range info.Resources {
106+
binding := analyzers.Binding{
107+
Resource: analyzers.Resource{
108+
Name: resource.Name,
109+
FullyQualifiedName: fmt.Sprintf("%s/%s", resource.Type, resource.ID), // e.g: Board/123
110+
Type: resource.Type,
111+
Metadata: map[string]any{}, // to avoid panic
112+
},
113+
Permission: analyzers.Permission{
114+
Value: PermissionStrings[FullAccess], // token always has full access
115+
},
116+
}
117+
118+
for key, value := range resource.MetaData {
119+
binding.Resource.Metadata[key] = value
120+
}
121+
122+
result.Bindings = append(result.Bindings, binding)
123+
124+
}
125+
126+
return &result
127+
}
128+
129+
// cli print functions
130+
func printUser(user Me) {
131+
color.Green("\n[i] User Information:")
132+
t := table.NewWriter()
133+
t.SetOutputMirror(os.Stdout)
134+
t.AppendHeader(table.Row{"ID", "Name", "Email", "Title", "Is Admin", "Is Guest"})
135+
t.AppendRow(table.Row{color.GreenString(user.ID), color.GreenString(user.Name), color.GreenString(user.Email),
136+
color.GreenString(user.Title), color.GreenString(fmt.Sprintf("%t", user.IsAdmin)), color.GreenString(fmt.Sprintf("%t", user.IsGuest))})
137+
t.Render()
138+
}
139+
140+
func printResources(resources []MondayResource) {
141+
color.Green("\n[i] Resources:")
142+
t := table.NewWriter()
143+
t.SetOutputMirror(os.Stdout)
144+
t.AppendHeader(table.Row{"Name", "Type"})
145+
for _, resource := range resources {
146+
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
147+
}
148+
t.Render()
149+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package monday
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"sort"
7+
"testing"
8+
"time"
9+
10+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
11+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
12+
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
14+
)
15+
16+
//go:embed result_output.json
17+
var expectedOutput []byte
18+
19+
func TestAnalyzer_Analyze(t *testing.T) {
20+
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
21+
defer cancel()
22+
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "analyzers1")
23+
if err != nil {
24+
t.Fatalf("could not get test secrets from GCP: %s", err)
25+
}
26+
27+
key := testSecrets.MustGetField("MONDAY_PAT")
28+
29+
tests := []struct {
30+
name string
31+
key string
32+
want []byte // JSON string
33+
wantErr bool
34+
}{
35+
{
36+
name: "valid monday personal access token",
37+
key: key,
38+
want: expectedOutput,
39+
wantErr: false,
40+
},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
a := Analyzer{Cfg: &config.Config{}}
46+
got, err := a.Analyze(ctx, map[string]string{"key": tt.key})
47+
if (err != nil) != tt.wantErr {
48+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
49+
return
50+
}
51+
52+
// Bindings need to be in the same order to be comparable
53+
sortBindings(got.Bindings)
54+
55+
// Marshal the actual result to JSON
56+
gotJSON, err := json.Marshal(got)
57+
if err != nil {
58+
t.Fatalf("could not marshal got to JSON: %s", err)
59+
}
60+
61+
// Parse the expected JSON string
62+
var wantObj analyzers.AnalyzerResult
63+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
64+
t.Fatalf("could not unmarshal want JSON string: %s", err)
65+
}
66+
67+
// Bindings need to be in the same order to be comparable
68+
sortBindings(wantObj.Bindings)
69+
70+
// Marshal the expected result to JSON (to normalize)
71+
wantJSON, err := json.Marshal(wantObj)
72+
if err != nil {
73+
t.Fatalf("could not marshal want to JSON: %s", err)
74+
}
75+
76+
// Compare the JSON strings
77+
if string(gotJSON) != string(wantJSON) {
78+
// Pretty-print both JSON strings for easier comparison
79+
var gotIndented, wantIndented []byte
80+
gotIndented, err = json.MarshalIndent(got, "", " ")
81+
if err != nil {
82+
t.Fatalf("could not marshal got to indented JSON: %s", err)
83+
}
84+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
85+
if err != nil {
86+
t.Fatalf("could not marshal want to indented JSON: %s", err)
87+
}
88+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
89+
}
90+
})
91+
}
92+
}
93+
94+
// Helper function to sort bindings
95+
func sortBindings(bindings []analyzers.Binding) {
96+
sort.SliceStable(bindings, func(i, j int) bool {
97+
if bindings[i].Resource.Name == bindings[j].Resource.Name {
98+
return bindings[i].Permission.Value < bindings[j].Permission.Value
99+
}
100+
return bindings[i].Resource.Name < bindings[j].Resource.Name
101+
})
102+
}

pkg/analyzer/analyzers/monday/permissions.go

+61
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
permissions:
2+
- full_access

0 commit comments

Comments
 (0)