mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-22 18:06:46 +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
|
package jira
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/andygrunwald/go-jira"
|
"github.com/andygrunwald/go-jira"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@ -25,6 +27,105 @@ type Formatter struct {
|
|||||||
util.MarkdownFormatter
|
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 {
|
func (jiraFormatter *Formatter) MakeBold(text string) string {
|
||||||
return "*" + text + "*"
|
return "*" + text + "*"
|
||||||
}
|
}
|
||||||
@ -155,12 +256,12 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.Create
|
|||||||
if label := i.options.IssueType; label != "" {
|
if label := i.options.IssueType; label != "" {
|
||||||
labels = append(labels, label)
|
labels = append(labels, label)
|
||||||
}
|
}
|
||||||
// for each custom value, take the name of the custom field and
|
// Build template context for evaluating custom field templates
|
||||||
// set the value of the custom field to the value specified in the
|
templateCtx := buildTemplateContext(event)
|
||||||
// configuration options
|
|
||||||
|
// Process custom fields with template evaluation support
|
||||||
customFields := tcontainer.NewMarshalMap()
|
customFields := tcontainer.NewMarshalMap()
|
||||||
for name, value := range i.options.CustomFields {
|
for name, value := range i.options.CustomFields {
|
||||||
//customFields[name] = map[string]interface{}{"value": value}
|
|
||||||
if valueMap, ok := value.(map[interface{}]interface{}); ok {
|
if valueMap, ok := value.(map[interface{}]interface{}); ok {
|
||||||
// Iterate over nested map
|
// Iterate over nested map
|
||||||
for nestedName, nestedValue := range valueMap {
|
for nestedName, nestedValue := range valueMap {
|
||||||
@ -168,32 +269,21 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) (*filters.Create
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue)
|
return nil, fmt.Errorf(`couldn't iterate on nested item "%s": %s`, nestedName, nestedValue)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(fmtNestedValue, "$") {
|
|
||||||
nestedValue = strings.TrimPrefix(fmtNestedValue, "$")
|
// Evaluate template or handle legacy $variable syntax
|
||||||
switch nestedValue {
|
evaluatedValue, err := i.evaluateCustomFieldValue(fmtNestedValue, templateCtx, event)
|
||||||
case "CVSSMetrics":
|
if err != nil {
|
||||||
nestedValue = ptr.Safe(event.Info.Classification).CVSSMetrics
|
gologger.Warning().Msgf("Failed to evaluate template for field %s.%s: %v", name, nestedName, err)
|
||||||
case "CVEID":
|
evaluatedValue = fmtNestedValue // fallback to original value
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch nestedName {
|
switch nestedName {
|
||||||
case "id":
|
case "id":
|
||||||
customFields[name] = map[string]interface{}{"id": nestedValue}
|
customFields[name] = map[string]interface{}{"id": evaluatedValue}
|
||||||
case "name":
|
case "name":
|
||||||
customFields[name] = map[string]interface{}{"value": nestedValue}
|
customFields[name] = map[string]interface{}{"value": evaluatedValue}
|
||||||
case "freeform":
|
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"
|
||||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
"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/output"
|
||||||
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
|
||||||
"github.com/stretchr/testify/require"
|
"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