diff --git a/v2/cmd/nuclei/issue-tracker-config.yaml b/v2/cmd/nuclei/issue-tracker-config.yaml index 4eb2f2e75..6413e3369 100644 --- a/v2/cmd/nuclei/issue-tracker-config.yaml +++ b/v2/cmd/nuclei/issue-tracker-config.yaml @@ -50,8 +50,23 @@ # # issue-type is the name of the created issue type (case sensitive) # issue-type: Bug # # SeverityAsLabel (optional) sends the severity as the label of the created issue +# # User custom fields for Jira Cloud instead # severity-as-label: true -# +# # Whatever your final status is that you want to use as a closed ticket - Closed, Done, Remediated, etc +# # When checking for duplicates, the JQL query will filter out status's that match this. +# # If it finds a match _and_ the ticket does have this status, a new one will be created. +# status-not: Closed +# # Customfield supports name, id and freeform. name and id are to be used when the custom field is a dropdown. +# # freeform can be used if the custom field is just a text entry +# # Variables can be used to pull various pieces of data from the finding itself. +# # Supported variables: $CVSSMetrics, $CVEID, $CWEID, $Host, $Severity, $CVSSScore, $Name +# custom_fields: +# customfield_00001: +# name: "Nuclei" +# customfield_00002: +# freeform: $CVSSMetrics +# customfield_00003: +# freeform: $CVSSScore # elasticsearch contains configuration options for elasticsearch exporter #elasticsearch: # # IP for elasticsearch instance diff --git a/v2/pkg/reporting/trackers/jira/jira.go b/v2/pkg/reporting/trackers/jira/jira.go index 323fab05c..753d39d00 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/trivago/tgo/tcontainer" "github.com/projectdiscovery/gologger" "github.com/projectdiscovery/nuclei/v2/pkg/catalog/config" @@ -44,9 +45,13 @@ type Options struct { // issue. SeverityAsLabel bool `yaml:"severity-as-label" json:"severity_as_label"` // Severity (optional) is the severity of the issue. - Severity []string `yaml:"severity" json:"severity"` - + Severity []string `yaml:"severity" json:"severity"` HttpClient *retryablehttp.Client `yaml:"-" json:"-"` + // for each customfield specified in the configuration options + // we will create a map of customfield name to the value + // that will be used to create the issue + CustomFields map[string]interface{} `yaml:"custom_fields"` + StatusNot string `yaml:"status-not" json:"status_not"` } // New creates a new issue tracker integration client based on options. @@ -80,15 +85,51 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error { if label := i.options.IssueType; label != "" { labels = append(labels, label) } - + // for each custom value, take the name of the custom field and + // set the value of the custom field to the value specified in the + // configuration options + customFields := tcontainer.NewMarshalMap() + for name, value := range i.options.CustomFields { + //customFields[name] = map[string]interface{}{"value": value} + if valueMap, ok := value.(map[interface{}]interface{}); ok { + // Iterate over nested map + for nestedName, nestedValue := range valueMap { + if strings.HasPrefix(nestedValue.(string), "$") { + nestedValue = strings.TrimPrefix(nestedValue.(string), "$") + switch nestedValue { + case "CVSSMetrics": + nestedValue = event.Info.Classification.CVSSMetrics + case "CVEID": + nestedValue = event.Info.Classification.CVEID + case "CWEID": + nestedValue = event.Info.Classification.CWEID + case "CVSSScore": + nestedValue = event.Info.Classification.CVSSScore + case "Host": + nestedValue = event.Host + case "Severity": + nestedValue = event.Info.SeverityHolder + case "Name": + nestedValue = event.Info.Name + } + } + switch nestedName { + case "id": + customFields[name] = map[string]interface{}{"id": nestedValue} + case "name": + customFields[name] = map[string]interface{}{"value": nestedValue} + case "freeform": + customFields[name] = nestedValue + } + } + } + } fields := &jira.IssueFields{ - Assignee: &jira.User{AccountID: i.options.AccountID}, - Reporter: &jira.User{AccountID: i.options.AccountID}, Description: jiraFormatDescription(event), + Unknowns: customFields, Type: jira.IssueType{Name: i.options.IssueType}, Project: jira.Project{Key: i.options.ProjectName}, Summary: summary, - Labels: labels, } // On-prem version of Jira server does not use AccountID if !i.options.Cloud { @@ -99,6 +140,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error { Project: jira.Project{Key: i.options.ProjectName}, Summary: summary, Labels: labels, + Unknowns: customFields, } } @@ -136,7 +178,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error { // 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) + jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\"", template, event.Host, i.options.StatusNot) searchOptions := &jira.SearchOptions{ MaxResults: 1, // if any issue exists, then we won't create a new one