Skip to content

Commit da08d9b

Browse files
Jiratoken Analyzer (#4193)
* jira token analyzer init * added api requests in jiratoken analyzer * more apis added for jira resources * removed unnecessary code * added test for jira token analyzer * repeat resources in analyzer result based on assigned permissions of jira token * simplified the code * updated secret manager for analyzer tests
1 parent d1feae1 commit da08d9b

File tree

10 files changed

+10815
-0
lines changed

10 files changed

+10815
-0
lines changed

pkg/analyzer/analyzers/analyzers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ const (
102102
AnalyzerTypePosthog
103103
AnalyzerTypeDropbox
104104
AnalyzerTypeDataBricks
105+
AnalyzerTypeJira
105106
// Add new items here with AnalyzerType prefix
106107
)
107108

@@ -149,6 +150,7 @@ var analyzerTypeStrings = map[AnalyzerType]string{
149150
AnalyzerTypePosthog: "Posthog",
150151
AnalyzerTypeDropbox: "Dropbox",
151152
AnalyzerTypeDataBricks: "DataBricks",
153+
AnalyzerTypeJira: "Jira",
152154
// Add new mappings here
153155
}
154156

pkg/analyzer/analyzers/jira/jira.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//go:generate generate_permissions permissions.yaml permissions.go jira
2+
3+
package jira
4+
5+
import (
6+
"encoding/json"
7+
"fmt"
8+
"os"
9+
"slices"
10+
11+
"github.com/fatih/color"
12+
"github.com/jedib0t/go-pretty/v6/table"
13+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
14+
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
15+
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
16+
)
17+
18+
var _ analyzers.Analyzer = (*Analyzer)(nil)
19+
20+
type Analyzer struct {
21+
Cfg *config.Config
22+
}
23+
24+
func (a Analyzer) Type() analyzers.AnalyzerType {
25+
return analyzers.AnalyzerTypeJira
26+
}
27+
28+
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
29+
token, exist := credInfo["token"]
30+
if !exist {
31+
return nil, fmt.Errorf("token not found in credential info")
32+
}
33+
domain, exist := credInfo["domain"]
34+
if !exist {
35+
return nil, fmt.Errorf("domain not found in credential info")
36+
}
37+
email, exist := credInfo["email"]
38+
if !exist {
39+
return nil, fmt.Errorf("email not found in credential info")
40+
}
41+
42+
info, err := AnalyzePermissions(a.Cfg, token, domain, email)
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
return secretInfoToAnalyzerResult(info), nil
48+
}
49+
50+
func AnalyzeAndPrintPermissions(cfg *config.Config, domain, email, token string) {
51+
info, err := AnalyzePermissions(cfg, token, domain, email)
52+
if err != nil {
53+
// just print the error in cli and continue as a partial success
54+
color.Red("[x] Error : %s", err.Error())
55+
} else {
56+
color.Green("[!] Valid Jira API token\n\n")
57+
}
58+
59+
if info == nil {
60+
color.Red("[x] Error : %s", "No information found")
61+
return
62+
}
63+
64+
printUserInfo(info.UserInfo)
65+
printPermissions(info.Permissions)
66+
printResources(info.Resources)
67+
}
68+
69+
func AnalyzePermissions(cfg *config.Config, token, domain, email string) (*SecretInfo, error) {
70+
// create http client
71+
client := analyzers.NewAnalyzeClient(cfg)
72+
73+
var secretInfo = &SecretInfo{}
74+
75+
// capture the user information
76+
if err := captureUserInfo(client, token, domain, email, secretInfo); err != nil {
77+
return nil, err
78+
}
79+
80+
body, _, err := capturePermissions(client, domain, email, token)
81+
if err != nil {
82+
return secretInfo, fmt.Errorf("failed to check permissions: %w", err)
83+
}
84+
85+
var permissionsResp JiraPermissionsResponse
86+
if err := json.Unmarshal(body, &permissionsResp); err != nil {
87+
return secretInfo, fmt.Errorf("failed to unmarshal permissions response: %w", err)
88+
}
89+
90+
var grantedPermissions []string
91+
for key, perm := range permissionsResp.Permissions {
92+
if perm.HavePermission {
93+
grantedPermissions = append(grantedPermissions, key)
94+
}
95+
}
96+
slices.Sort(grantedPermissions)
97+
secretInfo.Permissions = grantedPermissions
98+
99+
// capture the resources
100+
if err := captureResources(client, domain, email, token, secretInfo, grantedPermissions); err != nil {
101+
// return secretInfo as well in case of error for partial success
102+
return secretInfo, err
103+
}
104+
105+
return secretInfo, nil
106+
}
107+
108+
// secretInfoToAnalyzerResult translate secret info to Analyzer Result
109+
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
110+
if info == nil {
111+
return nil
112+
}
113+
114+
result := analyzers.AnalyzerResult{
115+
AnalyzerType: analyzers.AnalyzerTypeJira,
116+
Metadata: map[string]any{},
117+
Bindings: make([]analyzers.Binding, 0),
118+
}
119+
120+
for _, resource := range info.Resources {
121+
for _, perm := range resource.Permissions {
122+
binding := analyzers.Binding{
123+
Resource: *secretInfoResourceToAnalyzerResource(resource),
124+
Permission: analyzers.Permission{
125+
Value: perm,
126+
},
127+
}
128+
129+
if resource.Parent != nil {
130+
binding.Resource.Parent = secretInfoResourceToAnalyzerResource(*resource.Parent)
131+
}
132+
133+
result.Bindings = append(result.Bindings, binding)
134+
}
135+
}
136+
137+
return &result
138+
}
139+
140+
// secretInfoResourceToAnalyzerResource translate secret info resource to analyzer resource for binding
141+
func secretInfoResourceToAnalyzerResource(resource JiraResource) *analyzers.Resource {
142+
analyzerRes := analyzers.Resource{
143+
// make fully qualified name unique
144+
FullyQualifiedName: resource.Type + "/" + resource.ID,
145+
Name: resource.Name,
146+
Type: resource.Type,
147+
Metadata: map[string]any{},
148+
}
149+
150+
for key, value := range resource.Metadata {
151+
analyzerRes.Metadata[key] = value
152+
}
153+
154+
return &analyzerRes
155+
}
156+
157+
// cli print functions
158+
func printUserInfo(user JiraUser) {
159+
if user.AccountID == "" {
160+
color.Red("[x] No user information found")
161+
return
162+
}
163+
color.Yellow("[i] User Information:")
164+
t := table.NewWriter()
165+
t.SetOutputMirror(os.Stdout)
166+
t.AppendHeader(table.Row{"ID", "Name", "Account Type", "Email", "Active"})
167+
t.AppendRow(table.Row{color.GreenString(user.AccountID), color.GreenString(user.DisplayName), color.GreenString(user.AccountType), color.GreenString(user.EmailAddress), color.GreenString(fmt.Sprintf("%t", user.Active))})
168+
169+
t.Render()
170+
}
171+
172+
func printPermissions(permissions []string) {
173+
if len(permissions) == 0 {
174+
color.Red("[x] No permissions found")
175+
return
176+
}
177+
color.Yellow("[i] Permissions:")
178+
t := table.NewWriter()
179+
t.SetOutputMirror(os.Stdout)
180+
t.AppendHeader(table.Row{"Permission"})
181+
for _, scope := range permissions {
182+
t.AppendRow(table.Row{color.GreenString(scope)})
183+
}
184+
t.Render()
185+
}
186+
187+
func printResources(resources []JiraResource) {
188+
if len(resources) == 0 {
189+
color.Red("[x] No resources found")
190+
return
191+
}
192+
color.Yellow("[i] Resources:")
193+
t := table.NewWriter()
194+
t.SetOutputMirror(os.Stdout)
195+
t.AppendHeader(table.Row{"Name", "Type"})
196+
for _, resource := range resources {
197+
t.AppendRow(table.Row{color.GreenString(resource.Name), color.GreenString(resource.Type)})
198+
}
199+
200+
t.Render()
201+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package jira
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+
jiraDomain := testSecrets.MustGetField("JIRA_DOMAIN_ANALYZE")
28+
jiraEmail := testSecrets.MustGetField("JIRA_EMAIL_ANALYZE")
29+
jiraToken := testSecrets.MustGetField("JIRA_TOKEN_ANALYZE")
30+
31+
tests := []struct {
32+
name string
33+
domain string
34+
email string
35+
token string
36+
want []byte
37+
wantErr bool
38+
}{
39+
{
40+
name: "valid jira token",
41+
domain: jiraDomain,
42+
email: jiraEmail,
43+
token: jiraToken,
44+
want: expectedOutput,
45+
wantErr: false,
46+
},
47+
{
48+
name: "invalid jira token",
49+
domain: jiraDomain,
50+
email: jiraEmail,
51+
token: "invalid",
52+
want: nil,
53+
wantErr: true,
54+
},
55+
}
56+
57+
for _, tt := range tests {
58+
t.Run(tt.name, func(t *testing.T) {
59+
a := Analyzer{Cfg: &config.Config{}}
60+
got, err := a.Analyze(ctx, map[string]string{"token": tt.token, "domain": tt.domain, "email": tt.email})
61+
if (err != nil) != tt.wantErr {
62+
t.Errorf("Analyzer.Analyze() error = %v, wantErr %v", err, tt.wantErr)
63+
return
64+
}
65+
66+
if tt.wantErr {
67+
if got != nil {
68+
t.Errorf("Analyzer.Analyze() got = %v, want nil", got)
69+
}
70+
return
71+
}
72+
73+
// Bindings need to be in the same order to be comparable
74+
sortBindings(got.Bindings)
75+
76+
// Marshal the actual result to JSON
77+
gotJSON, err := json.Marshal(got)
78+
if err != nil {
79+
t.Fatalf("could not marshal got to JSON: %s", err)
80+
}
81+
82+
// Parse the expected JSON string
83+
var wantObj analyzers.AnalyzerResult
84+
if err := json.Unmarshal([]byte(tt.want), &wantObj); err != nil {
85+
t.Fatalf("could not unmarshal want JSON string: %s", err)
86+
}
87+
88+
// Bindings need to be in the same order to be comparable
89+
sortBindings(wantObj.Bindings)
90+
91+
// Marshal the expected result to JSON (to normalize)
92+
wantJSON, err := json.Marshal(wantObj)
93+
if err != nil {
94+
t.Fatalf("could not marshal want to JSON: %s", err)
95+
}
96+
97+
// Compare the JSON strings
98+
if string(gotJSON) != string(wantJSON) {
99+
// Pretty-print both JSON strings for easier comparison
100+
var gotIndented, wantIndented []byte
101+
gotIndented, err = json.MarshalIndent(got, "", " ")
102+
if err != nil {
103+
t.Fatalf("could not marshal got to indented JSON: %s", err)
104+
}
105+
wantIndented, err = json.MarshalIndent(wantObj, "", " ")
106+
if err != nil {
107+
t.Fatalf("could not marshal want to indented JSON: %s", err)
108+
}
109+
t.Errorf("Analyzer.Analyze() = %s, want %s", gotIndented, wantIndented)
110+
}
111+
})
112+
}
113+
}
114+
115+
// Helper function to sort bindings
116+
func sortBindings(bindings []analyzers.Binding) {
117+
sort.SliceStable(bindings, func(i, j int) bool {
118+
if bindings[i].Resource.FullyQualifiedName == bindings[j].Resource.FullyQualifiedName {
119+
return bindings[i].Permission.Value < bindings[j].Permission.Value
120+
}
121+
return bindings[i].Resource.FullyQualifiedName < bindings[j].Resource.FullyQualifiedName
122+
})
123+
}

0 commit comments

Comments
 (0)