Support for Jira custom fields

This commit is contained in:
Jordan Potti 2023-03-10 15:55:54 -07:00
parent 62bc659914
commit 6ab4bf25f9
2 changed files with 65 additions and 8 deletions

View File

@ -50,8 +50,23 @@
# # issue-type is the name of the created issue type (case sensitive) # # issue-type is the name of the created issue type (case sensitive)
# issue-type: Bug # issue-type: Bug
# # SeverityAsLabel (optional) sends the severity as the label of the created issue # # SeverityAsLabel (optional) sends the severity as the label of the created issue
# # User custom fields for Jira Cloud instead
# severity-as-label: true # 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 contains configuration options for elasticsearch exporter
#elasticsearch: #elasticsearch:
# # IP for elasticsearch instance # # IP for elasticsearch instance

View File

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/andygrunwald/go-jira" "github.com/andygrunwald/go-jira"
"github.com/trivago/tgo/tcontainer"
"github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config" "github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
@ -44,9 +45,13 @@ type Options struct {
// issue. // issue.
SeverityAsLabel bool `yaml:"severity-as-label" json:"severity_as_label"` SeverityAsLabel bool `yaml:"severity-as-label" json:"severity_as_label"`
// Severity (optional) is the severity of the issue. // 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:"-"` 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. // 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 != "" { if label := i.options.IssueType; label != "" {
labels = append(labels, 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{ fields := &jira.IssueFields{
Assignee: &jira.User{AccountID: i.options.AccountID},
Reporter: &jira.User{AccountID: i.options.AccountID},
Description: jiraFormatDescription(event), Description: jiraFormatDescription(event),
Unknowns: customFields,
Type: jira.IssueType{Name: i.options.IssueType}, Type: jira.IssueType{Name: i.options.IssueType},
Project: jira.Project{Key: i.options.ProjectName}, Project: jira.Project{Key: i.options.ProjectName},
Summary: summary, Summary: summary,
Labels: labels,
} }
// On-prem version of Jira server does not use AccountID // On-prem version of Jira server does not use AccountID
if !i.options.Cloud { if !i.options.Cloud {
@ -99,6 +140,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
Project: jira.Project{Key: i.options.ProjectName}, Project: jira.Project{Key: i.options.ProjectName},
Summary: summary, Summary: summary,
Labels: labels, 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 // FindExistingIssue checks if the issue already exists and returns its ID
func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) { func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) {
template := format.GetMatchedTemplate(event) 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{ searchOptions := &jira.SearchOptions{
MaxResults: 1, // if any issue exists, then we won't create a new one MaxResults: 1, // if any issue exists, then we won't create a new one