Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 132 additions & 25 deletions pkg/reporting/trackers/jira/jira.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package jira

import (
"bytes"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"text/template"

"github.com/andygrunwald/go-jira"
"github.com/pkg/errors"
"github.com/trivago/tgo/tcontainer"
"golang.org/x/text/cases"
"golang.org/x/text/language"

"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
Expand All @@ -25,6 +29,120 @@ 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
}

// 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)
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 + "*"
}
Expand Down Expand Up @@ -155,45 +273,34 @@ 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 {
fmtNestedValue, ok := nestedValue.(string)
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
}
}
}
Expand Down
118 changes: 118 additions & 0 deletions pkg/reporting/trackers/jira/jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -70,3 +71,120 @@ 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)
})

t.Run("template functions", func(t *testing.T) {
// Test case conversion functions
result, err := integration.evaluateCustomFieldValue("{{.Severity | upper}}", buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "CRITICAL", result)

result, err = integration.evaluateCustomFieldValue("{{.Name | lower}}", buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "test vulnerability", result)

result, err = integration.evaluateCustomFieldValue("{{.Name | title}}", buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "Test Vulnerability", result)

// Test string check functions
result, err = integration.evaluateCustomFieldValue(`{{if contains .Name "Test"}}has-test{{else}}no-test{{end}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "has-test", result)

result, err = integration.evaluateCustomFieldValue(`{{if hasPrefix .Host "example"}}starts-with-example{{else}}other{{end}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "starts-with-example", result)

result, err = integration.evaluateCustomFieldValue(`{{if hasSuffix .Host ".com"}}ends-with-com{{else}}other{{end}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "ends-with-com", result)

// Test string manipulation functions
result, err = integration.evaluateCustomFieldValue(`{{replace .Name " " "-"}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "Test-vulnerability", result)

result, err = integration.evaluateCustomFieldValue(`{{trimSpace " test "}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "test", result)

result, err = integration.evaluateCustomFieldValue(`{{trim "...test..." "."}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "test", result)

// Test split and join functions
result, err = integration.evaluateCustomFieldValue(`{{join (split .Name " ") "-"}}`, buildTemplateContext(event), event)
require.NoError(t, err)
require.Equal(t, "Test-vulnerability", result)
})

t.Run("complex template with functions", func(t *testing.T) {
templateStr := `{{.Name | upper}} on {{.Host}}
{{if contains .Name "SQL"}}SQL-INJECTION{{else if contains .Name "XSS"}}XSS-ATTACK{{else}}OTHER{{end}}
Priority: {{if eq .Severity "critical"}}{{.Severity | upper}}{{else}}{{.Severity}}{{end}}`
result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event)
require.NoError(t, err)
require.Contains(t, result, "TEST VULNERABILITY on example.com", result)
require.Contains(t, result, "OTHER")
require.Contains(t, result, "CRITICAL")
})
}
Loading