diff --git a/pkg/reporting/exporters/markdown/util/markdown_formatter.go b/pkg/reporting/exporters/markdown/util/markdown_formatter.go
index 7f84652b0..7ac49a4e9 100644
--- a/pkg/reporting/exporters/markdown/util/markdown_formatter.go
+++ b/pkg/reporting/exporters/markdown/util/markdown_formatter.go
@@ -28,6 +28,10 @@ func (markdownFormatter MarkdownFormatter) CreateHorizontalLine() string {
return CreateHorizontalLine()
}
+func (markdownFormatter MarkdownFormatter) FormatLineBreaks(text string) string {
+ return strings.ReplaceAll(text, "\n", "
")
+}
+
// escapeCodeBlockMarkdown only escapes the bare minimum characters needed
// for code blocks and other sections where readability is important
//
diff --git a/pkg/reporting/format/format.go b/pkg/reporting/format/format.go
index f801add34..0ee1747c9 100644
--- a/pkg/reporting/format/format.go
+++ b/pkg/reporting/format/format.go
@@ -6,4 +6,5 @@ type ResultFormatter interface {
CreateTable(headers []string, rows [][]string) (string, error)
CreateLink(title string, url string) string
CreateHorizontalLine() string
+ FormatLineBreaks(text string) string
}
diff --git a/pkg/reporting/format/format_utils.go b/pkg/reporting/format/format_utils.go
index 92976d30f..ebd66fd10 100644
--- a/pkg/reporting/format/format_utils.go
+++ b/pkg/reporting/format/format_utils.go
@@ -9,7 +9,6 @@ import (
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
- "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
unitutils "github.com/projectdiscovery/utils/unit"
@@ -171,20 +170,20 @@ func CreateTemplateInfoTable(templateInfo *model.Info, formatter ResultFormatter
}
if !utils.IsBlank(templateInfo.Description) {
- rows = append(rows, []string{"Description", lineBreakToHTML(templateInfo.Description)})
+ rows = append(rows, []string{"Description", formatter.FormatLineBreaks(templateInfo.Description)})
}
if !utils.IsBlank(templateInfo.Remediation) {
- rows = append(rows, []string{"Remediation", lineBreakToHTML(templateInfo.Remediation)})
+ rows = append(rows, []string{"Remediation", formatter.FormatLineBreaks(templateInfo.Remediation)})
}
classification := templateInfo.Classification
if classification != nil {
if classification.CVSSMetrics != "" {
- rows = append(rows, []string{"CVSS-Metrics", generateCVSSMetricsFromClassification(classification)})
+ rows = append(rows, []string{"CVSS-Metrics", generateCVSSMetricsFromClassification(classification, formatter)})
}
- rows = append(rows, generateCVECWEIDLinksFromClassification(classification)...)
+ rows = append(rows, generateCVECWEIDLinksFromClassification(classification, formatter)...)
rows = append(rows, []string{"CVSS-Score", strconv.FormatFloat(classification.CVSSScore, 'f', 2, 64)})
}
@@ -202,7 +201,7 @@ func CreateTemplateInfoTable(templateInfo *model.Info, formatter ResultFormatter
return table
}
-func generateCVSSMetricsFromClassification(classification *model.Classification) string {
+func generateCVSSMetricsFromClassification(classification *model.Classification, formatter ResultFormatter) string {
var cvssLinkPrefix string
if strings.Contains(classification.CVSSMetrics, "CVSS:3.0") {
cvssLinkPrefix = "https://www.first.org/cvss/calculator/3.0#"
@@ -213,11 +212,11 @@ func generateCVSSMetricsFromClassification(classification *model.Classification)
if cvssLinkPrefix == "" {
return classification.CVSSMetrics
} else {
- return util.CreateLink(classification.CVSSMetrics, cvssLinkPrefix+classification.CVSSMetrics)
+ return formatter.CreateLink(classification.CVSSMetrics, cvssLinkPrefix+classification.CVSSMetrics)
}
}
-func generateCVECWEIDLinksFromClassification(classification *model.Classification) [][]string {
+func generateCVECWEIDLinksFromClassification(classification *model.Classification, formatter ResultFormatter) [][]string {
cwes := classification.CWEID.ToSlice()
cweIDs := make([]string, 0, len(cwes))
@@ -226,7 +225,7 @@ func generateCVECWEIDLinksFromClassification(classification *model.Classificatio
if len(parts) != 2 {
continue
}
- cweIDs = append(cweIDs, util.CreateLink(strings.ToUpper(value), fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", parts[1])))
+ cweIDs = append(cweIDs, formatter.CreateLink(strings.ToUpper(value), fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", parts[1])))
}
var rows [][]string
@@ -238,7 +237,7 @@ func generateCVECWEIDLinksFromClassification(classification *model.Classificatio
cves := classification.CVEID.ToSlice()
cveIDs := make([]string, 0, len(cves))
for _, value := range cves {
- cveIDs = append(cveIDs, util.CreateLink(strings.ToUpper(value), fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", value)))
+ cveIDs = append(cveIDs, formatter.CreateLink(strings.ToUpper(value), fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", value)))
}
if len(cveIDs) > 0 {
rows = append(rows, []string{"CVE-ID", strings.Join(cveIDs, ",")})
diff --git a/pkg/reporting/trackers/jira/jira.go b/pkg/reporting/trackers/jira/jira.go
index bfb518daa..8e635eddb 100644
--- a/pkg/reporting/trackers/jira/jira.go
+++ b/pkg/reporting/trackers/jira/jira.go
@@ -35,17 +35,57 @@ func (jiraFormatter *Formatter) CreateCodeBlock(title string, content string, _
}
func (jiraFormatter *Formatter) CreateTable(headers []string, rows [][]string) (string, error) {
- table, err := jiraFormatter.MarkdownFormatter.CreateTable(headers, rows)
- if err != nil {
- return "", err
+ if len(headers) == 0 {
+ return "", fmt.Errorf("no headers provided")
}
- tableRows := strings.Split(table, "\n")
- tableRowsWithoutHeaderSeparator := append(tableRows[:1], tableRows[2:]...)
- return strings.Join(tableRowsWithoutHeaderSeparator, "\n"), nil
+
+ var builder strings.Builder
+
+ // Create header row with leading and trailing pipes
+ builder.WriteString("| ")
+ builder.WriteString(strings.Join(headers, " | "))
+ builder.WriteString(" |")
+ builder.WriteString("\n")
+
+ // Create separator row with leading and trailing pipes
+ separators := make([]string, len(headers))
+ for i := range separators {
+ separators[i] = "-----------"
+ }
+ builder.WriteString("|")
+ builder.WriteString(strings.Join(separators, "|"))
+ builder.WriteString("|")
+ builder.WriteString("\n")
+
+ // Create data rows with leading and trailing pipes
+ for _, row := range rows {
+ builder.WriteString("| ")
+ if len(row) < len(headers) {
+ extendedRow := make([]string, len(headers))
+ copy(extendedRow, row)
+ builder.WriteString(strings.Join(extendedRow, " | "))
+ } else if len(row) > len(headers) {
+ builder.WriteString(strings.Join(row[:len(headers)], " | "))
+ } else {
+ builder.WriteString(strings.Join(row, " | "))
+ }
+ builder.WriteString(" |")
+ builder.WriteString("\n")
+ }
+
+ return builder.String(), nil
+}
+
+func (jiraFormatter *Formatter) CreateHorizontalLine() string {
+ return "----\n"
+}
+
+func (jiraFormatter *Formatter) FormatLineBreaks(text string) string {
+ return strings.ReplaceAll(text, "\n", "\\\\")
}
func (jiraFormatter *Formatter) CreateLink(title string, url string) string {
- return fmt.Sprintf("[%s|%s]", title, url)
+ return fmt.Sprintf("[%s](%s)", title, url)
}
// Integration is a client for an issue tracker integration
diff --git a/pkg/reporting/trackers/jira/jira_test.go b/pkg/reporting/trackers/jira/jira_test.go
index d725a97b2..e1a327e42 100644
--- a/pkg/reporting/trackers/jira/jira_test.go
+++ b/pkg/reporting/trackers/jira/jira_test.go
@@ -3,10 +3,13 @@ package jira
import (
"strings"
"testing"
+ "time"
"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/format"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
"github.com/stretchr/testify/require"
)
@@ -14,7 +17,18 @@ import (
func TestLinkCreation(t *testing.T) {
jiraIntegration := &Integration{}
link := jiraIntegration.CreateLink("ProjectDiscovery", "https://projectdiscovery.io")
- require.Equal(t, "[ProjectDiscovery|https://projectdiscovery.io]", link)
+ require.Equal(t, "[ProjectDiscovery](https://projectdiscovery.io)", link)
+}
+
+func TestLinkCreationWithSpecialCharacters(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ link := jiraIntegration.CreateLink("Nuclei [v3.4.4]", "https://github.com/projectdiscovery/nuclei")
+ expected := "[Nuclei [v3.4.4]](https://github.com/projectdiscovery/nuclei)"
+ require.Equal(t, expected, link)
+
+ require.NotContains(t, link, "%5D")
+ require.NotContains(t, link, "%5B")
}
func TestHorizontalLineCreation(t *testing.T) {
@@ -34,6 +48,7 @@ func TestTableCreation(t *testing.T) {
require.Nil(t, err)
expected := `| key | value |
+|-----------|-----------|
| a | b |
| c | |
| d | e |
@@ -41,6 +56,129 @@ func TestTableCreation(t *testing.T) {
require.Equal(t, expected, table)
}
+func TestTableCreationWithComplexData(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ table, err := jiraIntegration.CreateTable([]string{"Key", "Value"}, [][]string{
+ {"Name", "GraphQL CSRF / GET method"},
+ {"Authors", "Dolev Farhi"},
+ {"Tags", "graphql, misconfig"},
+ {"Severity", "info"},
+ })
+
+ require.Nil(t, err)
+
+ require.Contains(t, table, "| Key | Value |")
+ require.Contains(t, table, "|-----------|-----------|")
+ require.Contains(t, table, "| Name | GraphQL CSRF / GET method |")
+
+ require.Contains(t, table, "GraphQL CSRF")
+ require.NotContains(t, table, "| ame |")
+ require.NotContains(t, table, "| uthors |")
+}
+
+func TestFormatLineBreaks(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ input := "Line 1\nLine 2\nLine 3"
+ result := jiraIntegration.FormatLineBreaks(input)
+ expected := "Line 1\\\\Line 2\\\\Line 3"
+
+ require.Equal(t, expected, result)
+
+ require.NotContains(t, result, "
")
+}
+
+func TestFormatLineBreaksWithMultipleBreaks(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ input := "Cross Site Request Forgery happens when an external website gains ability to make API calls impersonating an user.\nAllowing API calls through GET requests can lead to CSRF attacks."
+ result := jiraIntegration.FormatLineBreaks(input)
+ expected := "Cross Site Request Forgery happens when an external website gains ability to make API calls impersonating an user.\\\\Allowing API calls through GET requests can lead to CSRF attacks."
+
+ require.Equal(t, expected, result)
+}
+
+func TestCompleteReportGeneration(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ event := &output.ResultEvent{
+ TemplateID: "graphql-get-method",
+ Info: model.Info{
+ Name: "GraphQL CSRF / GET method",
+ Authors: stringslice.StringSlice{Value: []string{"Dolev Farhi"}},
+ Tags: stringslice.StringSlice{Value: []string{"graphql", "misconfig"}},
+ SeverityHolder: severity.Holder{Severity: severity.Info},
+ Description: "Cross Site Request Forgery happens when an external website gains ability to make API calls impersonating an user.\nAllowing API calls through GET requests can lead to CSRF attacks.",
+ Reference: stringslice.NewRawStringSlice([]string{
+ "https://graphql.org/learn/serving-over-http/#get-request",
+ "https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application",
+ }),
+ },
+ Host: "example.com",
+ Matched: "example.com/graphql",
+ Timestamp: time.Date(2025, 5, 31, 12, 0, 0, 0, time.UTC),
+ Type: "http",
+ }
+
+ description := format.CreateReportDescription(event, jiraIntegration, false)
+
+ require.Contains(t, description, "*Details*: *graphql-get-method* matched at example.com")
+ require.Contains(t, description, "*Protocol*: HTTP")
+ require.Contains(t, description, "*Template Information*")
+
+ require.Contains(t, description, "| Key | Value |")
+ require.Contains(t, description, "|-----------|-----------|")
+ require.Contains(t, description, "| Name | GraphQL CSRF / GET method |")
+
+ require.Contains(t, description, "impersonating an user.\\\\Allowing API calls")
+ require.NotContains(t, description, "
")
+
+ require.Contains(t, description, "[Nuclei")
+ require.Contains(t, description, "](https://github.com/projectdiscovery/nuclei)")
+
+ require.NotContains(t, description, "%5D")
+ require.NotContains(t, description, "%5B")
+
+ require.Contains(t, description, "References:")
+ require.Contains(t, description, "https://graphql.org/learn/serving-over-http/#get-request")
+}
+
+func TestReportWithCVELinks(t *testing.T) {
+ jiraIntegration := &Integration{}
+
+ event := &output.ResultEvent{
+ TemplateID: "test-cve",
+ Info: model.Info{
+ Name: "Test CVE Template",
+ Authors: stringslice.StringSlice{Value: []string{"test-author"}},
+ Tags: stringslice.StringSlice{Value: []string{"cve", "test"}},
+ SeverityHolder: severity.Holder{Severity: severity.High},
+ Description: "Test template with CVE links",
+ Classification: &model.Classification{
+ CVEID: stringslice.StringSlice{Value: []string{"CVE-2021-44228"}},
+ CWEID: stringslice.StringSlice{Value: []string{"CWE-502"}},
+ CVSSMetrics: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
+ CVSSScore: 10.0,
+ },
+ },
+ Host: "example.com",
+ Matched: "example.com/test",
+ Timestamp: time.Date(2025, 5, 31, 12, 0, 0, 0, time.UTC),
+ Type: "http",
+ }
+
+ description := format.CreateReportDescription(event, jiraIntegration, false)
+
+ require.Contains(t, description, "[CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228)")
+ require.Contains(t, description, "[CWE-502](https://cwe.mitre.org/data/definitions/502.html)")
+ require.Contains(t, description, "[CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H](https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H)")
+
+ require.NotContains(t, description, "|https://")
+
+ require.NotContains(t, description, "%5D")
+}
+
func Test_ShouldFilter_Tracker(t *testing.T) {
jiraIntegration := &Integration{
options: &Options{AllowList: &filters.Filter{