From 6882993358ae68f9c9f925a19cfc60828cdaeed6 Mon Sep 17 00:00:00 2001 From: ganoes Date: Fri, 4 Jun 2021 17:14:26 +0200 Subject: [PATCH 1/4] The first version of Jira issue merger --- v2/pkg/reporting/trackers/jira/jira.go | 75 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/v2/pkg/reporting/trackers/jira/jira.go b/v2/pkg/reporting/trackers/jira/jira.go index 3e7f65ee..4dd3e750 100644 --- a/v2/pkg/reporting/trackers/jira/jira.go +++ b/v2/pkg/reporting/trackers/jira/jira.go @@ -57,38 +57,77 @@ func New(options *Options) (*Integration, error) { func (i *Integration) CreateIssue(event *output.ResultEvent) error { summary := format.Summary(event) - fields := &jira.IssueFields{ - Assignee: &jira.User{AccountID: i.options.AccountID}, - Reporter: &jira.User{AccountID: i.options.AccountID}, - Description: jiraFormatDescription(event), - Type: jira.IssueType{Name: i.options.IssueType}, - Project: jira.Project{Key: i.options.ProjectName}, - Summary: summary, - } - // On-prem version of Jira server does not use AccountID - if !i.options.Cloud { - fields = &jira.IssueFields{ - Assignee: &jira.User{Name: i.options.AccountID}, + issue_id, err := i.FindExistingIssue(event) + if issue_id != "" { + i.jira.Issue.AddComment(issue_id, &jira.Comment{ + Body: jiraFormatDescription(event), + }) + if err != nil { + return err + } + } else { + fields := &jira.IssueFields{ + Assignee: &jira.User{AccountID: i.options.AccountID}, + Reporter: &jira.User{AccountID: i.options.AccountID}, Description: jiraFormatDescription(event), Type: jira.IssueType{Name: i.options.IssueType}, Project: jira.Project{Key: i.options.ProjectName}, Summary: summary, } + // On-prem version of Jira server does not use AccountID + if !i.options.Cloud { + fields = &jira.IssueFields{ + Assignee: &jira.User{Name: i.options.AccountID}, + Description: jiraFormatDescription(event), + Type: jira.IssueType{Name: i.options.IssueType}, + Project: jira.Project{Key: i.options.ProjectName}, + Summary: summary, + } + } + + issueData := &jira.Issue{ + Fields: fields, + } + _, resp, err := i.jira.Issue.Create(issueData) + if err != nil { + var data string + if resp != nil && resp.Body != nil { + d, _ := ioutil.ReadAll(resp.Body) + data = string(d) + } + return fmt.Errorf("%s => %s", err, data) + } + } + return nil +} + +// FindExistingIssue checks if the issue already exists and returns its ID +func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) { + template := format.GetMatchedTemplate(event) + jql := fmt.Sprintf("text ~ \"%s\" AND text ~ \"%s\" AND status = \"Open\"", template, event.Host) + + search_options := &jira.SearchOptions{ + MaxResults: 1, // if any issue exists, then we won't create a new one } - issueData := &jira.Issue{ - Fields: fields, - } - _, resp, err := i.jira.Issue.Create(issueData) + chunk, resp, err := i.jira.Issue.Search(jql, search_options) if err != nil { var data string if resp != nil && resp.Body != nil { d, _ := ioutil.ReadAll(resp.Body) data = string(d) } - return fmt.Errorf("%s => %s", err, data) + return "", fmt.Errorf("%s => %s", err, data) + } + + switch resp.Total { + case 0: + return "", nil + case 1: + return chunk[0].ID, nil + default: + return chunk[0].ID, fmt.Errorf("multiple opened issues found for \"%s\" -> \"%s\" - the first one will be used", template, event.Host) } - return nil } // jiraFormatDescription formats a short description of the generated From 3700cd8c0bc5a369f0e73d4b02826ed1f83ee108 Mon Sep 17 00:00:00 2001 From: ganoes Date: Fri, 4 Jun 2021 18:19:23 +0200 Subject: [PATCH 2/4] UpdateExisting flag for Jira reporting was added --- v2/cmd/nuclei/issue-tracker-config.yaml | 4 +- v2/pkg/reporting/trackers/jira/jira.go | 84 ++++++++++++++----------- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/v2/cmd/nuclei/issue-tracker-config.yaml b/v2/cmd/nuclei/issue-tracker-config.yaml index 3fffce49..49751e8d 100644 --- a/v2/cmd/nuclei/issue-tracker-config.yaml +++ b/v2/cmd/nuclei/issue-tracker-config.yaml @@ -33,8 +33,10 @@ # jira contains configuration options for jira issue tracker #jira: -# # Cloud is the boolean which tells if Jira instance is running in the cloud or on-prem version is used +# # cloud is the boolean which tells if Jira instance is running in the cloud or on-prem version is used # cloud: true +# # update-existing is the boolean which tells if the existing, opened issue should be updated or new one should be created +# update-existing: false # # URL is the jira application url # url: "" # # account-id is the account-id of the jira user or username in case of on-prem Jira diff --git a/v2/pkg/reporting/trackers/jira/jira.go b/v2/pkg/reporting/trackers/jira/jira.go index 4dd3e750..77d6e913 100644 --- a/v2/pkg/reporting/trackers/jira/jira.go +++ b/v2/pkg/reporting/trackers/jira/jira.go @@ -7,6 +7,7 @@ import ( "strings" jira "github.com/andygrunwald/go-jira" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/format" "github.com/projectdiscovery/nuclei/v2/pkg/types" @@ -22,6 +23,8 @@ type Integration struct { type Options struct { // Cloud value is set to true when Jira cloud is used Cloud bool `yaml:"cloud"` + // UpdateExisting value if true, the existing opened issue is updated + UpdateExisting bool `yaml:"update-existing"` // URL is the URL of the jira server URL string `yaml:"url"` // AccountID is the accountID of the jira user. @@ -53,58 +56,64 @@ func New(options *Options) (*Integration, error) { return &Integration{jira: jiraClient, options: options}, nil } -// CreateIssue creates an issue in the tracker -func (i *Integration) CreateIssue(event *output.ResultEvent) error { +// CreateNewIssue creates a new issue in the tracker +func (i *Integration) CreateNewIssue(event *output.ResultEvent) error { summary := format.Summary(event) - issue_id, err := i.FindExistingIssue(event) - if issue_id != "" { - i.jira.Issue.AddComment(issue_id, &jira.Comment{ - Body: jiraFormatDescription(event), - }) - if err != nil { - return err - } - } else { - fields := &jira.IssueFields{ - Assignee: &jira.User{AccountID: i.options.AccountID}, - Reporter: &jira.User{AccountID: i.options.AccountID}, + fields := &jira.IssueFields{ + Assignee: &jira.User{AccountID: i.options.AccountID}, + Reporter: &jira.User{AccountID: i.options.AccountID}, + Description: jiraFormatDescription(event), + Type: jira.IssueType{Name: i.options.IssueType}, + Project: jira.Project{Key: i.options.ProjectName}, + Summary: summary, + } + // On-prem version of Jira server does not use AccountID + if !i.options.Cloud { + fields = &jira.IssueFields{ + Assignee: &jira.User{Name: i.options.AccountID}, Description: jiraFormatDescription(event), Type: jira.IssueType{Name: i.options.IssueType}, Project: jira.Project{Key: i.options.ProjectName}, Summary: summary, } - // On-prem version of Jira server does not use AccountID - if !i.options.Cloud { - fields = &jira.IssueFields{ - Assignee: &jira.User{Name: i.options.AccountID}, - Description: jiraFormatDescription(event), - Type: jira.IssueType{Name: i.options.IssueType}, - Project: jira.Project{Key: i.options.ProjectName}, - Summary: summary, - } - } + } - issueData := &jira.Issue{ - Fields: fields, - } - _, resp, err := i.jira.Issue.Create(issueData) - if err != nil { - var data string - if resp != nil && resp.Body != nil { - d, _ := ioutil.ReadAll(resp.Body) - data = string(d) - } - return fmt.Errorf("%s => %s", err, data) + issueData := &jira.Issue{ + Fields: fields, + } + _, resp, err := i.jira.Issue.Create(issueData) + if err != nil { + var data string + if resp != nil && resp.Body != nil { + d, _ := ioutil.ReadAll(resp.Body) + data = string(d) } + return fmt.Errorf("%s => %s", err, data) } return nil } +// CreateIssue creates an issue in the tracker or updates the existing one +func (i *Integration) CreateIssue(event *output.ResultEvent) error { + if i.options.UpdateExisting { + issue_id, err := i.FindExistingIssue(event) + if err != nil { + return err + } else if issue_id != "" { + _, _, err = i.jira.Issue.AddComment(issue_id, &jira.Comment{ + Body: jiraFormatDescription(event), + }) + return err + } + } + return i.CreateNewIssue(event) +} + // FindExistingIssue checks if the issue already exists and returns its ID func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) { template := format.GetMatchedTemplate(event) - jql := fmt.Sprintf("text ~ \"%s\" AND text ~ \"%s\" AND status = \"Open\"", template, event.Host) + jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status = \"Open\"", template, event.Host) search_options := &jira.SearchOptions{ MaxResults: 1, // if any issue exists, then we won't create a new one @@ -126,7 +135,8 @@ func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, erro case 1: return chunk[0].ID, nil default: - return chunk[0].ID, fmt.Errorf("multiple opened issues found for \"%s\" -> \"%s\" - the first one will be used", template, event.Host) + gologger.Warning().Msgf("Discovered multiple opened issues %s for the host %s: The issue [%s] will be updated.", template, event.Host, chunk[0].ID) + return chunk[0].ID, nil } } From 88ed8a64cdfb6d59ef673b809180186a5844d6b6 Mon Sep 17 00:00:00 2001 From: ganoes Date: Wed, 1 Sep 2021 11:30:22 +0200 Subject: [PATCH 3/4] Updated names of variables to follow guidelines --- v2/pkg/reporting/trackers/jira/jira.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/v2/pkg/reporting/trackers/jira/jira.go b/v2/pkg/reporting/trackers/jira/jira.go index 4df57e14..9d243f4a 100644 --- a/v2/pkg/reporting/trackers/jira/jira.go +++ b/v2/pkg/reporting/trackers/jira/jira.go @@ -97,11 +97,11 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error { // CreateIssue creates an issue in the tracker or updates the existing one func (i *Integration) CreateIssue(event *output.ResultEvent) error { if i.options.UpdateExisting { - issue_id, err := i.FindExistingIssue(event) + issueID, err := i.FindExistingIssue(event) if err != nil { return err - } else if issue_id != "" { - _, _, err = i.jira.Issue.AddComment(issue_id, &jira.Comment{ + } else if issueID != "" { + _, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{ Body: jiraFormatDescription(event), }) return err @@ -115,11 +115,11 @@ func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, erro template := format.GetMatchedTemplate(event) jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status = \"Open\"", template, event.Host) - search_options := &jira.SearchOptions{ + searchOptions := &jira.SearchOptions{ MaxResults: 1, // if any issue exists, then we won't create a new one } - chunk, resp, err := i.jira.Issue.Search(jql, search_options) + chunk, resp, err := i.jira.Issue.Search(jql, searchOptions) if err != nil { var data string if resp != nil && resp.Body != nil { From 426b95bdf420e6eaf80d20c384cab489e3c73541 Mon Sep 17 00:00:00 2001 From: ganoes Date: Wed, 1 Sep 2021 11:43:02 +0200 Subject: [PATCH 4/4] Added logger --- v2/pkg/reporting/trackers/jira/jira.go | 1 + 1 file changed, 1 insertion(+) diff --git a/v2/pkg/reporting/trackers/jira/jira.go b/v2/pkg/reporting/trackers/jira/jira.go index 7f4ca6ad..a3d50db5 100644 --- a/v2/pkg/reporting/trackers/jira/jira.go +++ b/v2/pkg/reporting/trackers/jira/jira.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/andygrunwald/go-jira" + "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/format" "github.com/projectdiscovery/nuclei/v2/pkg/types"