Path: blob/dev/pkg/reporting/trackers/jira/jira_test.go
2871 views
package jira12import (3"net/http"4"os"5"strings"6"testing"78"github.com/projectdiscovery/nuclei/v3/pkg/model"9"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"10"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"11"github.com/projectdiscovery/nuclei/v3/pkg/output"12"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"13"github.com/projectdiscovery/retryablehttp-go"14"github.com/stretchr/testify/require"15)1617type recordingTransport struct {18inner http.RoundTripper19paths []string20}2122func (rt *recordingTransport) RoundTrip(req *http.Request) (*http.Response, error) {23if rt.inner == nil {24rt.inner = http.DefaultTransport25}26rt.paths = append(rt.paths, req.URL.Path)27return rt.inner.RoundTrip(req)28}2930func TestLinkCreation(t *testing.T) {31jiraIntegration := &Integration{}32link := jiraIntegration.CreateLink("ProjectDiscovery", "https://projectdiscovery.io")33require.Equal(t, "[ProjectDiscovery|https://projectdiscovery.io]", link)34}3536func TestHorizontalLineCreation(t *testing.T) {37jiraIntegration := &Integration{}38horizontalLine := jiraIntegration.CreateHorizontalLine()39require.True(t, strings.Contains(horizontalLine, "----"))40}4142func TestTableCreation(t *testing.T) {43jiraIntegration := &Integration{}4445table, err := jiraIntegration.CreateTable([]string{"key", "value"}, [][]string{46{"a", "b"},47{"c"},48{"d", "e"},49})5051require.Nil(t, err)52expected := `| key | value |53| a | b |54| c | |55| d | e |56`57require.Equal(t, expected, table)58}5960func Test_ShouldFilter_Tracker(t *testing.T) {61jiraIntegration := &Integration{62options: &Options{AllowList: &filters.Filter{63Severities: severity.Severities{severity.Critical},64}},65}6667require.False(t, jiraIntegration.ShouldFilter(&output.ResultEvent{Info: model.Info{68SeverityHolder: severity.Holder{Severity: severity.Info},69}}))70require.True(t, jiraIntegration.ShouldFilter(&output.ResultEvent{Info: model.Info{71SeverityHolder: severity.Holder{Severity: severity.Critical},72}}))7374t.Run("deny-list", func(t *testing.T) {75jiraIntegration := &Integration{76options: &Options{DenyList: &filters.Filter{77Severities: severity.Severities{severity.Critical},78}},79}8081require.True(t, jiraIntegration.ShouldFilter(&output.ResultEvent{Info: model.Info{82SeverityHolder: severity.Holder{Severity: severity.Info},83}}))84require.False(t, jiraIntegration.ShouldFilter(&output.ResultEvent{Info: model.Info{85SeverityHolder: severity.Holder{Severity: severity.Critical},86}}))87})88}8990func TestTemplateEvaluation(t *testing.T) {91event := &output.ResultEvent{92Host: "example.com",93Info: model.Info{94Name: "Test vulnerability",95SeverityHolder: severity.Holder{Severity: severity.Critical},96Classification: &model.Classification{97CVSSScore: 9.8,98CVEID: stringslice.StringSlice{Value: []string{"CVE-2023-1234"}},99CWEID: stringslice.StringSlice{Value: []string{"CWE-79"}},100CVSSMetrics: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",101},102},103}104105integration := &Integration{}106107t.Run("conditional template", func(t *testing.T) {108templateStr := `{{if eq .Severity "critical"}}11187{{else if eq .Severity "high"}}11186{{else if eq .Severity "medium"}}11185{{else}}11184{{end}}`109result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event)110require.NoError(t, err)111require.Equal(t, "11187", result)112})113114t.Run("freeform description template", func(t *testing.T) {115templateStr := `Vulnerability detected by Nuclei. Name: {{.Name}}, Severity: {{.Severity}}, Host: {{.Host}}`116result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event)117require.NoError(t, err)118expected := "Vulnerability detected by Nuclei. Name: Test vulnerability, Severity: critical, Host: example.com"119require.Equal(t, expected, result)120})121122t.Run("legacy variable syntax", func(t *testing.T) {123result, err := integration.evaluateCustomFieldValue("$Severity", buildTemplateContext(event), event)124require.NoError(t, err)125require.Equal(t, "critical", result)126127result, err = integration.evaluateCustomFieldValue("$Host", buildTemplateContext(event), event)128require.NoError(t, err)129require.Equal(t, "example.com", result)130})131132t.Run("complex template with conditionals", func(t *testing.T) {133templateStr := `{{.Name}} on {{.Host}}134{{if .CVSSScore}}CVSS: {{.CVSSScore}}{{end}}135{{if eq .Severity "critical"}}⚠️ CRITICAL{{else}}Standard{{end}}`136result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event)137require.NoError(t, err)138require.Contains(t, result, "Test vulnerability on example.com")139require.Contains(t, result, "CVSS: 9.80")140require.Contains(t, result, "⚠️ CRITICAL")141})142143t.Run("no template syntax", func(t *testing.T) {144result, err := integration.evaluateCustomFieldValue("plain text", buildTemplateContext(event), event)145require.NoError(t, err)146require.Equal(t, "plain text", result)147})148149t.Run("template functions", func(t *testing.T) {150// Test case conversion functions151result, err := integration.evaluateCustomFieldValue("{{.Severity | upper}}", buildTemplateContext(event), event)152require.NoError(t, err)153require.Equal(t, "CRITICAL", result)154155result, err = integration.evaluateCustomFieldValue("{{.Name | lower}}", buildTemplateContext(event), event)156require.NoError(t, err)157require.Equal(t, "test vulnerability", result)158159result, err = integration.evaluateCustomFieldValue("{{.Name | title}}", buildTemplateContext(event), event)160require.NoError(t, err)161require.Equal(t, "Test Vulnerability", result)162163// Test string check functions164result, err = integration.evaluateCustomFieldValue(`{{if contains .Name "Test"}}has-test{{else}}no-test{{end}}`, buildTemplateContext(event), event)165require.NoError(t, err)166require.Equal(t, "has-test", result)167168result, err = integration.evaluateCustomFieldValue(`{{if hasPrefix .Host "example"}}starts-with-example{{else}}other{{end}}`, buildTemplateContext(event), event)169require.NoError(t, err)170require.Equal(t, "starts-with-example", result)171172result, err = integration.evaluateCustomFieldValue(`{{if hasSuffix .Host ".com"}}ends-with-com{{else}}other{{end}}`, buildTemplateContext(event), event)173require.NoError(t, err)174require.Equal(t, "ends-with-com", result)175176// Test string manipulation functions177result, err = integration.evaluateCustomFieldValue(`{{replace .Name " " "-"}}`, buildTemplateContext(event), event)178require.NoError(t, err)179require.Equal(t, "Test-vulnerability", result)180181result, err = integration.evaluateCustomFieldValue(`{{trimSpace " test "}}`, buildTemplateContext(event), event)182require.NoError(t, err)183require.Equal(t, "test", result)184185result, err = integration.evaluateCustomFieldValue(`{{trim "...test..." "."}}`, buildTemplateContext(event), event)186require.NoError(t, err)187require.Equal(t, "test", result)188189// Test split and join functions190result, err = integration.evaluateCustomFieldValue(`{{join (split .Name " ") "-"}}`, buildTemplateContext(event), event)191require.NoError(t, err)192require.Equal(t, "Test-vulnerability", result)193})194195t.Run("complex template with functions", func(t *testing.T) {196templateStr := `{{.Name | upper}} on {{.Host}}197{{if contains .Name "SQL"}}SQL-INJECTION{{else if contains .Name "XSS"}}XSS-ATTACK{{else}}OTHER{{end}}198Priority: {{if eq .Severity "critical"}}{{.Severity | upper}}{{else}}{{.Severity}}{{end}}`199result, err := integration.evaluateCustomFieldValue(templateStr, buildTemplateContext(event), event)200require.NoError(t, err)201require.Contains(t, result, "TEST VULNERABILITY on example.com", result)202require.Contains(t, result, "OTHER")203require.Contains(t, result, "CRITICAL")204})205}206207// Live test to verify SearchV2JQL hits /rest/api/3/search/jql when creds are provided via env208func TestJiraLive_SearchV2UsesJqlEndpoint(t *testing.T) {209jiraURL := os.Getenv("JIRA_URL")210jiraEmail := os.Getenv("JIRA_EMAIL")211jiraAccountID := os.Getenv("JIRA_ACCOUNT_ID")212jiraToken := os.Getenv("JIRA_TOKEN")213jiraPAT := os.Getenv("JIRA_PAT")214jiraProjectName := os.Getenv("JIRA_PROJECT_NAME")215jiraProjectID := os.Getenv("JIRA_PROJECT_ID")216jiraStatusNot := os.Getenv("JIRA_STATUS_NOT")217jiraCloud := os.Getenv("JIRA_CLOUD")218219if jiraURL == "" || (jiraPAT == "" && jiraToken == "") || (jiraEmail == "" && jiraAccountID == "") || (jiraProjectName == "" && jiraProjectID == "") {220t.Skip("live Jira test skipped: missing JIRA_* env vars")221}222223statusNot := jiraStatusNot224if statusNot == "" {225statusNot = "Done"226}227228isCloud := !strings.EqualFold(jiraCloud, "false") && jiraCloud != "0"229230rec := &recordingTransport{}231rc := retryablehttp.NewClient(retryablehttp.DefaultOptionsSingle)232rc.HTTPClient.Transport = rec233234opts := &Options{235Cloud: isCloud,236URL: jiraURL,237Email: jiraEmail,238AccountID: jiraAccountID,239Token: jiraToken,240PersonalAccessToken: jiraPAT,241ProjectName: jiraProjectName,242ProjectID: jiraProjectID,243IssueType: "Task",244StatusNot: statusNot,245HttpClient: rc,246}247248integration, err := New(opts)249require.NoError(t, err)250251event := &output.ResultEvent{252Host: "example.com",253Info: model.Info{254Name: "Nuclei Live Verify",255SeverityHolder: severity.Holder{Severity: severity.Low},256},257}258259_, _ = integration.FindExistingIssue(event, true)260261var hitSearchV2 bool262for _, p := range rec.paths {263if strings.HasSuffix(p, "/rest/api/3/search/jql") {264hitSearchV2 = true265break266}267}268require.True(t, hitSearchV2, "expected client to call /rest/api/3/search/jql, got paths: %v", rec.paths)269}270271272