249 lines
7.5 KiB
Go
Raw Normal View History

package jira
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"github.com/andygrunwald/go-jira"
2021-09-01 11:43:02 +02:00
"github.com/projectdiscovery/gologger"
2021-09-09 19:56:39 +05:30
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
"github.com/projectdiscovery/nuclei/v2/pkg/types"
)
// Integration is a client for an issue tracker integration
type Integration struct {
jira *jira.Client
options *Options
}
// Options contains the configuration options for jira client
type Options struct {
2021-06-04 13:11:09 +02:00
// 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.
AccountID string `yaml:"account-id"`
// Email is the email of the user for jira instance
Email string `yaml:"email"`
// Token is the token for jira instance.
Token string `yaml:"token"`
// ProjectName is the name of the project.
ProjectName string `yaml:"project-name"`
// IssueType is the name of the created issue type
IssueType string `yaml:"issue-type"`
}
// New creates a new issue tracker integration client based on options.
func New(options *Options) (*Integration, error) {
2021-06-04 13:11:09 +02:00
username := options.Email
if !options.Cloud {
username = options.AccountID
}
tp := jira.BasicAuthTransport{
2021-06-04 13:11:09 +02:00
Username: username,
Password: options.Token,
}
jiraClient, err := jira.NewClient(tp.Client(), options.URL)
if err != nil {
return nil, err
}
return &Integration{jira: jiraClient, options: options}, nil
}
// CreateNewIssue creates a new issue in the tracker
func (i *Integration) CreateNewIssue(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},
Description: jiraFormatDescription(event),
Type: jira.IssueType{Name: i.options.IssueType},
Project: jira.Project{Key: i.options.ProjectName},
Summary: summary,
2021-06-04 13:11:09 +02:00
}
}
2021-06-04 17:14:26 +02:00
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)
2021-06-04 17:14:26 +02:00
}
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 {
issueID, err := i.FindExistingIssue(event)
2021-06-04 17:14:26 +02:00
if err != nil {
return err
} else if issueID != "" {
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
Body: jiraFormatDescription(event),
})
return err
2021-06-04 17:14:26 +02:00
}
2021-06-04 13:11:09 +02:00
}
return i.CreateNewIssue(event)
2021-06-04 17:14:26 +02:00
}
// 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("summary ~ \"%s\" AND summary ~ \"%s\" AND status = \"Open\"", template, event.Host)
2021-06-04 13:11:09 +02:00
searchOptions := &jira.SearchOptions{
2021-06-04 17:14:26 +02:00
MaxResults: 1, // if any issue exists, then we won't create a new one
}
2021-06-04 17:14:26 +02:00
chunk, resp, err := i.jira.Issue.Search(jql, searchOptions)
if err != nil {
var data string
if resp != nil && resp.Body != nil {
d, _ := ioutil.ReadAll(resp.Body)
data = string(d)
}
2021-06-04 17:14:26 +02:00
return "", fmt.Errorf("%s => %s", err, data)
}
switch resp.Total {
case 0:
return "", nil
case 1:
return chunk[0].ID, nil
default:
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
}
}
// jiraFormatDescription formats a short description of the generated
// event by the nuclei scanner in Jira format.
func jiraFormatDescription(event *output.ResultEvent) string { // TODO remove the code duplication: format.go <-> jira.go
2021-02-26 13:13:11 +05:30
template := format.GetMatchedTemplate(event)
builder := &bytes.Buffer{}
builder.WriteString("*Details*: *")
builder.WriteString(template)
builder.WriteString("* ")
builder.WriteString(" matched at ")
2021-02-26 13:13:11 +05:30
builder.WriteString(event.Host)
builder.WriteString("\n\n*Protocol*: ")
2021-02-26 13:13:11 +05:30
builder.WriteString(strings.ToUpper(event.Type))
builder.WriteString("\n\n*Full URL*: ")
2021-02-26 13:13:11 +05:30
builder.WriteString(event.Matched)
builder.WriteString("\n\n*Timestamp*: ")
2021-02-26 13:13:11 +05:30
builder.WriteString(event.Timestamp.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
builder.WriteString("\n\n*Template Information*\n\n| Key | Value |\n")
builder.WriteString(format.ToMarkdownTableString(&event.Info))
builder.WriteString("\n*Request*\n\n{code}\n")
2021-02-26 13:13:11 +05:30
builder.WriteString(event.Request)
builder.WriteString("\n{code}\n")
builder.WriteString("\n*Response*\n\n{code}\n")
2021-03-22 14:05:49 +05:30
// If the response is larger than 5 kb, truncate it before writing.
if len(event.Response) > 5*1024 {
builder.WriteString(event.Response[:5*1024])
builder.WriteString(".... Truncated ....")
} else {
builder.WriteString(event.Response)
}
builder.WriteString("\n{code}\n\n")
2021-02-26 13:13:11 +05:30
if len(event.ExtractedResults) > 0 || len(event.Metadata) > 0 {
2021-06-06 17:38:39 +05:30
builder.WriteString("\n*Extra Information*\n\n")
2021-02-26 13:13:11 +05:30
if len(event.ExtractedResults) > 0 {
builder.WriteString("*Extracted results*:\n\n")
2021-02-26 13:13:11 +05:30
for _, v := range event.ExtractedResults {
builder.WriteString("- ")
builder.WriteString(v)
builder.WriteString("\n")
}
builder.WriteString("\n")
}
2021-02-26 13:13:11 +05:30
if len(event.Metadata) > 0 {
builder.WriteString("*Metadata*:\n\n")
2021-02-26 13:13:11 +05:30
for k, v := range event.Metadata {
builder.WriteString("- ")
builder.WriteString(k)
builder.WriteString(": ")
builder.WriteString(types.ToString(v))
builder.WriteString("\n")
}
builder.WriteString("\n")
}
}
if event.Interaction != nil {
builder.WriteString("*Interaction Data*\n---\n")
builder.WriteString(event.Interaction.Protocol)
if event.Interaction.QType != "" {
builder.WriteString(" (")
builder.WriteString(event.Interaction.QType)
builder.WriteString(")")
}
builder.WriteString(" Interaction from ")
builder.WriteString(event.Interaction.RemoteAddress)
builder.WriteString(" at ")
builder.WriteString(event.Interaction.UniqueID)
if event.Interaction.RawRequest != "" {
builder.WriteString("\n\n*Interaction Request*\n\n{code}\n")
builder.WriteString(event.Interaction.RawRequest)
builder.WriteString("\n{code}\n")
}
if event.Interaction.RawResponse != "" {
builder.WriteString("\n*Interaction Response*\n\n{code}\n")
builder.WriteString(event.Interaction.RawResponse)
builder.WriteString("\n{code}\n")
}
}
2021-06-05 23:00:59 +05:30
reference := event.Info.Reference
if !reference.IsEmpty() {
builder.WriteString("\nReferences: \n")
2021-06-05 23:00:59 +05:30
referenceSlice := reference.ToSlice()
for i, item := range referenceSlice {
builder.WriteString("- ")
builder.WriteString(item)
if len(referenceSlice)-1 != i {
builder.WriteString("\n")
2021-06-05 23:00:59 +05:30
}
}
}
2021-09-09 19:56:39 +05:30
builder.WriteString("\n---\nGenerated by [Nuclei|https://github.com/projectdiscovery/nuclei] ")
builder.WriteString(config.Version)
data := builder.String()
return data
}