feat: added new text/template syntax to jira custom fields

This commit is contained in:
Ice3man 2025-09-10 16:51:20 +05:30
parent ff5734ba15
commit 218a2f69a5
2 changed files with 176 additions and 25 deletions

View File

@ -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
}
}
}

View File

@ -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)
})
}