mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-18 04:35:24 +00:00
feat: added new text/template syntax to jira custom fields
This commit is contained in:
parent
ff5734ba15
commit
218a2f69a5
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user