2021-02-02 12:10:47 +05:30
|
|
|
package jira
|
|
|
|
|
|
|
|
|
|
import (
|
2025-09-10 16:51:20 +05:30
|
|
|
"bytes"
|
2021-02-02 12:10:47 +05:30
|
|
|
"fmt"
|
2022-02-23 13:54:46 +01:00
|
|
|
"io"
|
2025-04-03 16:52:57 +05:30
|
|
|
"net/http"
|
2024-03-10 22:02:42 +05:30
|
|
|
"net/url"
|
2021-02-02 12:10:47 +05:30
|
|
|
"strings"
|
2024-03-10 22:02:42 +05:30
|
|
|
"sync"
|
2025-09-10 16:51:20 +05:30
|
|
|
"text/template"
|
2021-02-02 12:10:47 +05:30
|
|
|
|
2021-07-19 21:04:08 +03:00
|
|
|
"github.com/andygrunwald/go-jira"
|
2024-11-07 02:56:04 +05:30
|
|
|
"github.com/pkg/errors"
|
2023-03-10 15:55:54 -07:00
|
|
|
"github.com/trivago/tgo/tcontainer"
|
2025-09-10 17:32:43 +05:30
|
|
|
"golang.org/x/text/cases"
|
|
|
|
|
"golang.org/x/text/language"
|
2021-11-25 15:18:46 +02:00
|
|
|
|
2021-09-01 11:43:02 +02:00
|
|
|
"github.com/projectdiscovery/gologger"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
|
2024-03-02 14:55:13 +02:00
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
|
2022-03-09 12:31:12 +01:00
|
|
|
"github.com/projectdiscovery/retryablehttp-go"
|
2024-06-16 01:00:21 +03:00
|
|
|
"github.com/projectdiscovery/utils/ptr"
|
2021-02-02 12:10:47 +05:30
|
|
|
)
|
|
|
|
|
|
2023-06-22 14:27:32 +03:00
|
|
|
type Formatter struct {
|
|
|
|
|
util.MarkdownFormatter
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 16:51:20 +05:30
|
|
|
// TemplateContext holds the data available for template evaluation
|
|
|
|
|
type TemplateContext struct {
|
|
|
|
|
Severity string
|
|
|
|
|
Name string
|
|
|
|
|
Host string
|
|
|
|
|
CVSSScore string
|
|
|
|
|
CVEID string
|
|
|
|
|
CWEID string
|
|
|
|
|
CVSSMetrics string
|
|
|
|
|
Tags []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// buildTemplateContext creates a template context from a ResultEvent
|
|
|
|
|
func buildTemplateContext(event *output.ResultEvent) *TemplateContext {
|
|
|
|
|
ctx := &TemplateContext{
|
|
|
|
|
Host: event.Host,
|
|
|
|
|
Name: event.Info.Name,
|
|
|
|
|
Tags: event.Info.Tags.ToSlice(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set severity string
|
|
|
|
|
ctx.Severity = event.Info.SeverityHolder.Severity.String()
|
|
|
|
|
|
|
|
|
|
if event.Info.Classification != nil {
|
|
|
|
|
ctx.CVSSScore = fmt.Sprintf("%.2f", ptr.Safe(event.Info.Classification).CVSSScore)
|
|
|
|
|
ctx.CVEID = strings.Join(ptr.Safe(event.Info.Classification).CVEID.ToSlice(), ", ")
|
|
|
|
|
ctx.CWEID = strings.Join(ptr.Safe(event.Info.Classification).CWEID.ToSlice(), ", ")
|
|
|
|
|
ctx.CVSSMetrics = ptr.Safe(event.Info.Classification).CVSSMetrics
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ctx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// evaluateTemplate executes a template string with the given context
|
|
|
|
|
func evaluateTemplate(templateStr string, ctx *TemplateContext) (string, error) {
|
|
|
|
|
// If no template markers found, return as-is for backward compatibility
|
|
|
|
|
if !strings.Contains(templateStr, "{{") {
|
|
|
|
|
return templateStr, nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 17:32:43 +05:30
|
|
|
// Create template with useful functions for JIRA custom fields
|
|
|
|
|
funcMap := template.FuncMap{
|
|
|
|
|
"upper": strings.ToUpper,
|
|
|
|
|
"lower": strings.ToLower,
|
|
|
|
|
"title": cases.Title(language.English).String,
|
|
|
|
|
"contains": strings.Contains,
|
|
|
|
|
"hasPrefix": strings.HasPrefix,
|
|
|
|
|
"hasSuffix": strings.HasSuffix,
|
|
|
|
|
"trim": strings.Trim,
|
|
|
|
|
"trimSpace": strings.TrimSpace,
|
|
|
|
|
"replace": strings.ReplaceAll,
|
|
|
|
|
"split": strings.Split,
|
|
|
|
|
"join": strings.Join,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tmpl, err := template.New("field").Funcs(funcMap).Parse(templateStr)
|
2025-09-10 16:51:20 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return templateStr, fmt.Errorf("failed to parse template: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
if err := tmpl.Execute(&buf, ctx); err != nil {
|
|
|
|
|
return templateStr, fmt.Errorf("failed to execute template: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return buf.String(), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// evaluateCustomFieldValue evaluates a custom field value, supporting both new template syntax and legacy $variable syntax
|
|
|
|
|
func (i *Integration) evaluateCustomFieldValue(value string, templateCtx *TemplateContext, event *output.ResultEvent) (interface{}, error) {
|
|
|
|
|
// Try template evaluation first (supports {{...}} syntax)
|
|
|
|
|
if strings.Contains(value, "{{") {
|
|
|
|
|
return evaluateTemplate(value, templateCtx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle legacy $variable syntax for backward compatibility
|
|
|
|
|
if strings.HasPrefix(value, "$") {
|
|
|
|
|
variableName := strings.TrimPrefix(value, "$")
|
|
|
|
|
switch variableName {
|
|
|
|
|
case "CVSSMetrics":
|
|
|
|
|
if event.Info.Classification != nil {
|
|
|
|
|
return ptr.Safe(event.Info.Classification).CVSSMetrics, nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
case "CVEID":
|
|
|
|
|
if event.Info.Classification != nil {
|
|
|
|
|
return strings.Join(ptr.Safe(event.Info.Classification).CVEID.ToSlice(), ", "), nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
case "CWEID":
|
|
|
|
|
if event.Info.Classification != nil {
|
|
|
|
|
return strings.Join(ptr.Safe(event.Info.Classification).CWEID.ToSlice(), ", "), nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
case "CVSSScore":
|
|
|
|
|
if event.Info.Classification != nil {
|
|
|
|
|
return fmt.Sprintf("%.2f", ptr.Safe(event.Info.Classification).CVSSScore), nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
case "Host":
|
|
|
|
|
return event.Host, nil
|
|
|
|
|
case "Severity":
|
|
|
|
|
return event.Info.SeverityHolder.Severity.String(), nil
|
|
|
|
|
case "Name":
|
|
|
|
|
return event.Info.Name, nil
|
|
|
|
|
default:
|
|
|
|
|
return value, nil // return as-is if variable not found
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return as-is if no template or variable syntax found
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-22 14:27:32 +03:00
|
|
|
func (jiraFormatter *Formatter) MakeBold(text string) string {
|
|
|
|
|
return "*" + text + "*"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (jiraFormatter *Formatter) CreateCodeBlock(title string, content string, _ string) string {
|
2025-03-07 14:45:39 +05:30
|
|
|
escapedContent := strings.ReplaceAll(content, "{code}", "")
|
|
|
|
|
return fmt.Sprintf("\n%s\n{code}\n%s\n{code}\n", jiraFormatter.MakeBold(title), escapedContent)
|
2023-06-22 14:27:32 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (jiraFormatter *Formatter) CreateTable(headers []string, rows [][]string) (string, error) {
|
|
|
|
|
table, err := jiraFormatter.MarkdownFormatter.CreateTable(headers, rows)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
tableRows := strings.Split(table, "\n")
|
|
|
|
|
tableRowsWithoutHeaderSeparator := append(tableRows[:1], tableRows[2:]...)
|
|
|
|
|
return strings.Join(tableRowsWithoutHeaderSeparator, "\n"), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (jiraFormatter *Formatter) CreateLink(title string, url string) string {
|
|
|
|
|
return fmt.Sprintf("[%s|%s]", title, url)
|
|
|
|
|
}
|
|
|
|
|
|
2021-08-05 18:19:59 +03:00
|
|
|
// Integration is a client for an issue tracker integration
|
2021-02-02 12:10:47 +05:30
|
|
|
type Integration struct {
|
2023-06-22 14:27:32 +03:00
|
|
|
Formatter
|
2021-02-02 12:10:47 +05:30
|
|
|
jira *jira.Client
|
|
|
|
|
options *Options
|
2024-03-10 22:02:42 +05:30
|
|
|
|
|
|
|
|
once *sync.Once
|
|
|
|
|
transitionID string
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Options contains the configuration options for jira client
|
|
|
|
|
type Options struct {
|
2021-10-18 20:52:35 +02:00
|
|
|
// Cloud value (optional) is set to true when Jira cloud is used
|
2023-01-10 22:49:01 +05:30
|
|
|
Cloud bool `yaml:"cloud" json:"cloud"`
|
2021-10-18 20:52:35 +02:00
|
|
|
// UpdateExisting value (optional) if true, the existing opened issue is updated
|
2023-01-10 22:49:01 +05:30
|
|
|
UpdateExisting bool `yaml:"update-existing" json:"update_existing"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// URL is the URL of the jira server
|
2023-01-10 22:49:01 +05:30
|
|
|
URL string `yaml:"url" json:"url" validate:"required"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// AccountID is the accountID of the jira user.
|
2023-01-10 22:49:01 +05:30
|
|
|
AccountID string `yaml:"account-id" json:"account_id" validate:"required"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// Email is the email of the user for jira instance
|
2025-04-03 16:52:57 +05:30
|
|
|
Email string `yaml:"email" json:"email"`
|
|
|
|
|
// PersonalAccessToken is the personal access token for jira instance.
|
|
|
|
|
// If this is set, Bearer Auth is used instead of Basic Auth.
|
|
|
|
|
PersonalAccessToken string `yaml:"personal-access-token" json:"personal_access_token"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// Token is the token for jira instance.
|
2025-04-03 16:52:57 +05:30
|
|
|
Token string `yaml:"token" json:"token"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// ProjectName is the name of the project.
|
2024-08-19 06:20:12 +05:30
|
|
|
ProjectName string `yaml:"project-name" json:"project_name"`
|
|
|
|
|
// ProjectID is the ID of the project (optional)
|
|
|
|
|
ProjectID string `yaml:"project-id" json:"project_id"`
|
2021-10-18 20:52:35 +02:00
|
|
|
// IssueType (optional) is the name of the created issue type
|
2023-01-10 22:49:01 +05:30
|
|
|
IssueType string `yaml:"issue-type" json:"issue_type"`
|
2024-08-19 06:20:12 +05:30
|
|
|
// IssueTypeID (optional) is the ID of the created issue type
|
|
|
|
|
IssueTypeID string `yaml:"issue-type-id" json:"issue_type_id"`
|
2021-10-19 17:15:58 +02:00
|
|
|
// SeverityAsLabel (optional) sends the severity as the label of the created
|
|
|
|
|
// issue.
|
2023-01-10 22:49:01 +05:30
|
|
|
SeverityAsLabel bool `yaml:"severity-as-label" json:"severity_as_label"`
|
2024-03-02 14:55:13 +02:00
|
|
|
// AllowList contains a list of allowed events for this tracker
|
|
|
|
|
AllowList *filters.Filter `yaml:"allow-list"`
|
|
|
|
|
// DenyList contains a list of denied events for this tracker
|
|
|
|
|
DenyList *filters.Filter `yaml:"deny-list"`
|
2023-01-10 22:49:01 +05:30
|
|
|
// Severity (optional) is the severity of the issue.
|
2023-03-10 15:55:54 -07:00
|
|
|
Severity []string `yaml:"severity" json:"severity"`
|
2023-01-10 22:49:01 +05:30
|
|
|
HttpClient *retryablehttp.Client `yaml:"-" json:"-"`
|
2023-03-10 15:55:54 -07:00
|
|
|
// 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
|
2023-03-20 00:48:39 +01:00
|
|
|
CustomFields map[string]interface{} `yaml:"custom-fields" json:"custom_fields"`
|
2023-03-10 15:55:54 -07:00
|
|
|
StatusNot string `yaml:"status-not" json:"status_not"`
|
2024-01-27 01:36:25 +03:00
|
|
|
OmitRaw bool `yaml:"-"`
|
2022-09-27 02:40:34 +05:30
|
|
|
}
|
2022-05-12 05:10:14 -05:00
|
|
|
|
2021-02-02 12:10:47 +05:30
|
|
|
// 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
|
|
|
|
|
}
|
2025-04-03 16:52:57 +05:30
|
|
|
|
|
|
|
|
var httpclient *http.Client
|
|
|
|
|
if options.PersonalAccessToken != "" {
|
|
|
|
|
bearerTp := jira.BearerAuthTransport{
|
|
|
|
|
Token: options.PersonalAccessToken,
|
|
|
|
|
}
|
|
|
|
|
if options.HttpClient != nil {
|
|
|
|
|
bearerTp.Transport = options.HttpClient.HTTPClient.Transport
|
|
|
|
|
}
|
|
|
|
|
httpclient = bearerTp.Client()
|
|
|
|
|
} else {
|
|
|
|
|
basicTp := jira.BasicAuthTransport{
|
|
|
|
|
Username: username,
|
|
|
|
|
Password: options.Token,
|
|
|
|
|
}
|
|
|
|
|
if options.HttpClient != nil {
|
|
|
|
|
basicTp.Transport = options.HttpClient.HTTPClient.Transport
|
|
|
|
|
}
|
|
|
|
|
httpclient = basicTp.Client()
|
2022-03-09 12:31:12 +01:00
|
|
|
}
|
2025-04-03 16:52:57 +05:30
|
|
|
|
|
|
|
|
jiraClient, err := jira.NewClient(httpclient, options.URL)
|
2021-02-02 12:10:47 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
integration := &Integration{
|
|
|
|
|
jira: jiraClient,
|
|
|
|
|
options: options,
|
|
|
|
|
once: &sync.Once{},
|
|
|
|
|
}
|
|
|
|
|
return integration, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *Integration) Name() string {
|
|
|
|
|
return "jira"
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
2021-06-04 18:19:23 +02:00
|
|
|
// CreateNewIssue creates a new issue in the tracker
|
2024-03-10 22:02:42 +05:30
|
|
|
func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
2021-02-02 12:10:47 +05:30
|
|
|
summary := format.Summary(event)
|
2021-10-18 20:45:46 +02:00
|
|
|
labels := []string{}
|
2022-03-07 17:18:27 +01:00
|
|
|
severityLabel := fmt.Sprintf("Severity:%s", event.Info.SeverityHolder.Severity.String())
|
2021-10-19 17:15:58 +02:00
|
|
|
if i.options.SeverityAsLabel && severityLabel != "" {
|
2021-10-18 20:45:46 +02:00
|
|
|
labels = append(labels, severityLabel)
|
|
|
|
|
}
|
|
|
|
|
if label := i.options.IssueType; label != "" {
|
|
|
|
|
labels = append(labels, label)
|
|
|
|
|
}
|
2025-09-10 16:51:20 +05:30
|
|
|
// Build template context for evaluating custom field templates
|
|
|
|
|
templateCtx := buildTemplateContext(event)
|
|
|
|
|
|
|
|
|
|
// Process custom fields with template evaluation support
|
2023-03-10 15:55:54 -07:00
|
|
|
customFields := tcontainer.NewMarshalMap()
|
|
|
|
|
for name, value := range i.options.CustomFields {
|
|
|
|
|
if valueMap, ok := value.(map[interface{}]interface{}); ok {
|
|
|
|
|
// Iterate over nested map
|
|
|
|
|
for nestedName, nestedValue := range valueMap {
|
2023-03-20 00:48:39 +01:00
|
|
|
fmtNestedValue, ok := nestedValue.(string)
|
|
|
|
|
if !ok {
|
2024-03-10 22:02:42 +05:30
|
|
|
return nil, fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue)
|
2023-03-20 00:48:39 +01:00
|
|
|
}
|
2025-09-10 16:51:20 +05:30
|
|
|
|
|
|
|
|
// Evaluate template or handle legacy $variable syntax
|
|
|
|
|
evaluatedValue, err := i.evaluateCustomFieldValue(fmtNestedValue, templateCtx, event)
|
|
|
|
|
if err != nil {
|
|
|
|
|
gologger.Warning().Msgf("Failed to evaluate template for field %s.%s: %v", name, nestedName, err)
|
|
|
|
|
evaluatedValue = fmtNestedValue // fallback to original value
|
2023-03-10 15:55:54 -07:00
|
|
|
}
|
2025-09-10 16:51:20 +05:30
|
|
|
|
2023-03-10 15:55:54 -07:00
|
|
|
switch nestedName {
|
|
|
|
|
case "id":
|
2025-09-10 16:51:20 +05:30
|
|
|
customFields[name] = map[string]interface{}{"id": evaluatedValue}
|
2023-03-10 15:55:54 -07:00
|
|
|
case "name":
|
2025-09-10 16:51:20 +05:30
|
|
|
customFields[name] = map[string]interface{}{"value": evaluatedValue}
|
2023-03-10 15:55:54 -07:00
|
|
|
case "freeform":
|
2025-09-10 16:51:20 +05:30
|
|
|
customFields[name] = evaluatedValue
|
2023-03-10 15:55:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-06-04 18:19:23 +02:00
|
|
|
fields := &jira.IssueFields{
|
2024-03-10 22:02:42 +05:30
|
|
|
Assignee: &jira.User{Name: i.options.AccountID},
|
2024-01-27 01:36:25 +03:00
|
|
|
Description: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
2023-03-10 15:55:54 -07:00
|
|
|
Unknowns: customFields,
|
2024-03-10 22:02:42 +05:30
|
|
|
Labels: labels,
|
2021-06-04 18:19:23 +02:00
|
|
|
Type: jira.IssueType{Name: i.options.IssueType},
|
|
|
|
|
Project: jira.Project{Key: i.options.ProjectName},
|
|
|
|
|
Summary: summary,
|
|
|
|
|
}
|
2024-08-19 06:20:12 +05:30
|
|
|
|
2021-06-04 18:19:23 +02:00
|
|
|
// On-prem version of Jira server does not use AccountID
|
|
|
|
|
if !i.options.Cloud {
|
|
|
|
|
fields = &jira.IssueFields{
|
|
|
|
|
Assignee: &jira.User{Name: i.options.AccountID},
|
2024-01-27 01:36:25 +03:00
|
|
|
Description: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
2021-02-02 12:10:47 +05:30
|
|
|
Type: jira.IssueType{Name: i.options.IssueType},
|
|
|
|
|
Project: jira.Project{Key: i.options.ProjectName},
|
|
|
|
|
Summary: summary,
|
2022-03-07 17:11:20 +01:00
|
|
|
Labels: labels,
|
2023-03-10 15:55:54 -07:00
|
|
|
Unknowns: customFields,
|
2021-06-04 13:11:09 +02:00
|
|
|
}
|
2021-06-04 18:19:23 +02:00
|
|
|
}
|
2024-08-19 06:20:12 +05:30
|
|
|
if i.options.IssueTypeID != "" {
|
|
|
|
|
fields.Type = jira.IssueType{ID: i.options.IssueTypeID}
|
|
|
|
|
}
|
|
|
|
|
if i.options.ProjectID != "" {
|
|
|
|
|
fields.Project = jira.Project{ID: i.options.ProjectID}
|
|
|
|
|
}
|
2021-06-04 17:14:26 +02:00
|
|
|
|
2021-06-04 18:19:23 +02:00
|
|
|
issueData := &jira.Issue{
|
|
|
|
|
Fields: fields,
|
|
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
createdIssue, resp, err := i.jira.Issue.Create(issueData)
|
2021-06-04 18:19:23 +02:00
|
|
|
if err != nil {
|
|
|
|
|
var data string
|
|
|
|
|
if resp != nil && resp.Body != nil {
|
2022-02-23 13:54:46 +01:00
|
|
|
d, _ := io.ReadAll(resp.Body)
|
2021-06-04 18:19:23 +02:00
|
|
|
data = string(d)
|
2021-06-04 17:14:26 +02:00
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
return nil, fmt.Errorf("%w => %s", err, data)
|
2021-06-04 18:19:23 +02:00
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
return getIssueResponseFromJira(createdIssue)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getIssueResponseFromJira(issue *jira.Issue) (*filters.CreateIssueResponse, error) {
|
|
|
|
|
parsed, err := url.Parse(issue.Self)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
parsed.Path = fmt.Sprintf("/browse/%s", issue.Key)
|
|
|
|
|
issueURL := parsed.String()
|
|
|
|
|
|
|
|
|
|
return &filters.CreateIssueResponse{
|
|
|
|
|
IssueID: issue.ID,
|
|
|
|
|
IssueURL: issueURL,
|
|
|
|
|
}, nil
|
2021-06-04 18:19:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateIssue creates an issue in the tracker or updates the existing one
|
2024-03-10 22:02:42 +05:30
|
|
|
func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
|
2021-06-04 18:19:23 +02:00
|
|
|
if i.options.UpdateExisting {
|
2025-03-06 22:49:21 +05:30
|
|
|
issue, err := i.FindExistingIssue(event, true)
|
2021-06-04 17:14:26 +02:00
|
|
|
if err != nil {
|
2024-11-07 02:56:04 +05:30
|
|
|
return nil, errors.Wrap(err, "could not find existing issue")
|
2024-03-10 22:02:42 +05:30
|
|
|
} else if issue.ID != "" {
|
|
|
|
|
_, _, err = i.jira.Issue.AddComment(issue.ID, &jira.Comment{
|
2024-01-27 01:36:25 +03:00
|
|
|
Body: format.CreateReportDescription(event, i, i.options.OmitRaw),
|
2021-06-04 18:19:23 +02:00
|
|
|
})
|
2024-03-10 22:02:42 +05:30
|
|
|
if err != nil {
|
2024-11-07 02:56:04 +05:30
|
|
|
return nil, errors.Wrap(err, "could not add comment to existing issue")
|
2024-03-10 22:02:42 +05:30
|
|
|
}
|
|
|
|
|
return getIssueResponseFromJira(&issue)
|
2021-06-04 17:14:26 +02:00
|
|
|
}
|
2021-06-04 13:11:09 +02:00
|
|
|
}
|
2024-11-07 02:56:04 +05:30
|
|
|
resp, err := i.CreateNewIssue(event)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errors.Wrap(err, "could not create new issue")
|
|
|
|
|
}
|
|
|
|
|
return resp, nil
|
2021-06-04 17:14:26 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-10 22:02:42 +05:30
|
|
|
func (i *Integration) CloseIssue(event *output.ResultEvent) error {
|
|
|
|
|
if i.options.StatusNot == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-06 22:49:21 +05:30
|
|
|
issue, err := i.FindExistingIssue(event, false)
|
2024-03-10 22:02:42 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
} else if issue.ID != "" {
|
|
|
|
|
// Lazy load the transitions ID in case it's not set
|
|
|
|
|
i.once.Do(func() {
|
|
|
|
|
transitions, _, err := i.jira.Issue.GetTransitions(issue.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, transition := range transitions {
|
|
|
|
|
if transition.Name == i.options.StatusNot {
|
|
|
|
|
i.transitionID = transition.ID
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
if i.transitionID == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
transition := jira.CreateTransitionPayload{
|
|
|
|
|
Transition: jira.TransitionPayload{
|
|
|
|
|
ID: i.transitionID,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = i.jira.Issue.DoTransitionWithPayload(issue.ID, transition)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-04 17:14:26 +02:00
|
|
|
// FindExistingIssue checks if the issue already exists and returns its ID
|
2025-03-06 22:49:21 +05:30
|
|
|
func (i *Integration) FindExistingIssue(event *output.ResultEvent, useStatus bool) (jira.Issue, error) {
|
2023-06-22 14:27:32 +03:00
|
|
|
template := format.GetMatchedTemplateName(event)
|
2024-11-07 02:56:04 +05:30
|
|
|
project := i.options.ProjectName
|
|
|
|
|
if i.options.ProjectID != "" {
|
|
|
|
|
project = i.options.ProjectID
|
|
|
|
|
}
|
2025-03-06 22:49:21 +05:30
|
|
|
jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND project = \"%s\"", template, event.Host, project)
|
|
|
|
|
if useStatus {
|
|
|
|
|
jql = fmt.Sprintf("%s AND status != \"%s\"", jql, i.options.StatusNot)
|
|
|
|
|
}
|
2021-06-04 13:11:09 +02:00
|
|
|
|
2021-09-01 11:30:22 +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-02-02 12:10:47 +05:30
|
|
|
}
|
2021-06-04 17:14:26 +02:00
|
|
|
|
2021-09-01 11:30:22 +02:00
|
|
|
chunk, resp, err := i.jira.Issue.Search(jql, searchOptions)
|
2021-02-02 12:10:47 +05:30
|
|
|
if err != nil {
|
|
|
|
|
var data string
|
|
|
|
|
if resp != nil && resp.Body != nil {
|
2022-02-23 13:54:46 +01:00
|
|
|
d, _ := io.ReadAll(resp.Body)
|
2021-02-02 12:10:47 +05:30
|
|
|
data = string(d)
|
|
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
return jira.Issue{}, fmt.Errorf("%w => %s", err, data)
|
2021-06-04 17:14:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch resp.Total {
|
|
|
|
|
case 0:
|
2024-03-10 22:02:42 +05:30
|
|
|
return jira.Issue{}, nil
|
2021-06-04 17:14:26 +02:00
|
|
|
case 1:
|
2024-03-10 22:02:42 +05:30
|
|
|
return chunk[0], nil
|
2021-06-04 17:14:26 +02:00
|
|
|
default:
|
2021-06-04 18:19:23 +02:00
|
|
|
gologger.Warning().Msgf("Discovered multiple opened issues %s for the host %s: The issue [%s] will be updated.", template, event.Host, chunk[0].ID)
|
2024-03-10 22:02:42 +05:30
|
|
|
return chunk[0], nil
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
}
|
2024-03-02 14:55:13 +02:00
|
|
|
|
|
|
|
|
// ShouldFilter determines if an issue should be logged to this tracker
|
|
|
|
|
func (i *Integration) ShouldFilter(event *output.ResultEvent) bool {
|
2024-06-16 19:14:43 +05:30
|
|
|
if i.options.AllowList != nil && !i.options.AllowList.GetMatch(event) {
|
2024-05-10 21:59:03 +05:30
|
|
|
return false
|
2024-03-02 14:55:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i.options.DenyList != nil && i.options.DenyList.GetMatch(event) {
|
2024-06-16 19:14:43 +05:30
|
|
|
return false
|
2024-03-02 14:55:13 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-16 19:14:43 +05:30
|
|
|
return true
|
2024-03-02 14:55:13 +02:00
|
|
|
}
|