diff --git a/pkg/reporting/trackers/jira/jira.go b/pkg/reporting/trackers/jira/jira.go index bfb518daa..5e8d45bff 100644 --- a/pkg/reporting/trackers/jira/jira.go +++ b/pkg/reporting/trackers/jira/jira.go @@ -1,12 +1,14 @@ package jira import ( + "bytes" "fmt" "io" "net/http" "net/url" "strings" "sync" + "text/template" "github.com/andygrunwald/go-jira" "github.com/pkg/errors" @@ -25,6 +27,105 @@ type Formatter struct { util.MarkdownFormatter } +// 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 + } + + tmpl, err := template.New("field").Parse(templateStr) + 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 +} + func (jiraFormatter *Formatter) MakeBold(text string) string { return "*" + text + "*" } @@ -155,12 +256,12 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.Create 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 + // Build template context for evaluating custom field templates + templateCtx := buildTemplateContext(event) + + // Process custom fields with template evaluation support 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 { @@ -168,32 +269,21 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.Create if !ok { return nil, fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue) } - if strings.HasPrefix(fmtNestedValue, "$") { - nestedValue = strings.TrimPrefix(fmtNestedValue, "$") - switch nestedValue { - case "CVSSMetrics": - nestedValue = ptr.Safe(event.Info.Classification).CVSSMetrics - case "CVEID": - nestedValue = ptr.Safe(event.Info.Classification).CVEID - case "CWEID": - nestedValue = ptr.Safe(event.Info.Classification).CWEID - case "CVSSScore": - nestedValue = ptr.Safe(event.Info.Classification).CVSSScore - case "Host": - nestedValue = event.Host - case "Severity": - nestedValue = event.Info.SeverityHolder - case "Name": - nestedValue = event.Info.Name - } + + // 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 } + switch nestedName { case "id": - customFields[name] = map[string]interface{}{"id": nestedValue} + customFields[name] = map[string]interface{}{"id": evaluatedValue} case "name": - customFields[name] = map[string]interface{}{"value": nestedValue} + customFields[name] = map[string]interface{}{"value": evaluatedValue} case "freeform": - customFields[name] = nestedValue + customFields[name] = evaluatedValue } } } diff --git a/pkg/reporting/trackers/jira/jira_test.go b/pkg/reporting/trackers/jira/jira_test.go index d725a97b2..49ae365e6 100644 --- a/pkg/reporting/trackers/jira/jira_test.go +++ b/pkg/reporting/trackers/jira/jira_test.go @@ -6,6 +6,7 @@ import ( "github.com/projectdiscovery/nuclei/v3/pkg/model" "github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity" + "github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice" "github.com/projectdiscovery/nuclei/v3/pkg/output" "github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters" "github.com/stretchr/testify/require" @@ -70,3 +71,63 @@ func Test_ShouldFilter_Tracker(t *testing.T) { }})) }) } + +func TestTemplateEvaluation(t *testing.T) { + event := &output.ResultEvent{ + Host: "example.com", + Info: model.Info{ + Name: "Test Vulnerability", + SeverityHolder: severity.Holder{Severity: severity.Critical}, + Classification: &model.Classification{ + CVSSScore: 9.8, + CVEID: stringslice.StringSlice{Value: []string{"CVE-2023-1234"}}, + CWEID: stringslice.StringSlice{Value: []string{"CWE-79"}}, + CVSSMetrics: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + }, + }, + } + + integration := &Integration{} + + t.Run("conditional template", func(t *testing.T) { + templateStr := `{{if eq .Severity "critical"}}11187{{else if eq .Severity "high"}}11186{{else if eq .Severity "medium"}}11185{{else}}11184{{end}}` + result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event) + require.NoError(t, err) + require.Equal(t, "11187", result) + }) + + t.Run("freeform description template", func(t *testing.T) { + templateStr := `Vulnerability detected by Nuclei. Name: {{.Name}}, Severity: {{.Severity}}, Host: {{.Host}}` + result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event) + require.NoError(t, err) + expected := "Vulnerability detected by Nuclei. Name: Test Vulnerability, Severity: critical, Host: example.com" + require.Equal(t, expected, result) + }) + + t.Run("legacy variable syntax", func(t *testing.T) { + result, err := integration.evaluateCustomFieldValue("$Severity", buildTemplateContext(event), event) + require.NoError(t, err) + require.Equal(t, "critical", result) + + result, err = integration.evaluateCustomFieldValue("$Host", buildTemplateContext(event), event) + require.NoError(t, err) + require.Equal(t, "example.com", result) + }) + + t.Run("complex template with conditionals", func(t *testing.T) { + templateStr := `{{.Name}} on {{.Host}} +{{if .CVSSScore}}CVSS: {{.CVSSScore}}{{end}} +{{if eq .Severity "critical"}}⚠️ CRITICAL{{else}}Standard{{end}}` + result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event) + require.NoError(t, err) + require.Contains(t, result, "Test Vulnerability on example.com") + require.Contains(t, result, "CVSS: 9.80") + require.Contains(t, result, "⚠️ CRITICAL") + }) + + t.Run("no template syntax", func(t *testing.T) { + result, err := integration.evaluateCustomFieldValue("plain text", buildTemplateContext(event), event) + require.NoError(t, err) + require.Equal(t, "plain text", result) + }) +}