mirror of https://github.com/daffainfo/nuclei.git
fix(reporting): Markdown and Jira exporter fixes (#3849)
* fix(reporting): Markdown and Jira exporter fixes * removed the code duplication between the Markdown and Jira exporter * markdown requires at least 3 dashes in the cells to separate headers from contents in a table * fixed the Jira link creation in the description * Jira requires at least 4 dashes for a horizontal line * added tests * Jira doesn't use dashed separators between table headers and contents * fix(reporting): Markdown and Jira exporter fixes * satisfying the linter * minor syntax changes --------- Co-authored-by: Mzack9999 <mzack9999@protonmail.com>dev
parent
4d8c4b7024
commit
442fc0f060
|
@ -7,11 +7,13 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
|
||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||
)
|
||||
|
||||
const indexFileName = "index.md"
|
||||
const extension = ".md"
|
||||
|
||||
type Exporter struct {
|
||||
directory string
|
||||
|
@ -37,9 +39,7 @@ func New(options *Options) (*Exporter, error) {
|
|||
_ = os.MkdirAll(directory, 0755)
|
||||
|
||||
// index generation header
|
||||
dataHeader := "" +
|
||||
"|Hostname/IP|Finding|Severity|\n" +
|
||||
"|-|-|-|\n"
|
||||
dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")
|
||||
|
||||
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
|
||||
if err != nil {
|
||||
|
@ -51,9 +51,34 @@ func New(options *Options) (*Exporter, error) {
|
|||
|
||||
// Export exports a passed result event to markdown
|
||||
func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
||||
summary := format.Summary(event)
|
||||
description := format.MarkdownDescription(event)
|
||||
// index file generation
|
||||
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename := createFileName(event)
|
||||
host := util.CreateLink(event.Host, filename)
|
||||
finding := event.TemplateID + " " + event.MatcherName
|
||||
severity := event.Info.SeverityHolder.Severity.String()
|
||||
|
||||
_, err = file.WriteString(util.CreateTableRow(host, finding, severity))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dataBuilder := &bytes.Buffer{}
|
||||
dataBuilder.WriteString(util.CreateHeading3(format.Summary(event)))
|
||||
dataBuilder.WriteString("\n")
|
||||
dataBuilder.WriteString(util.CreateHorizontalLine())
|
||||
dataBuilder.WriteString(format.CreateReportDescription(event, util.MarkdownFormatter{}))
|
||||
data := dataBuilder.Bytes()
|
||||
|
||||
return os.WriteFile(filepath.Join(exporter.directory, filename), data, 0644)
|
||||
}
|
||||
|
||||
func createFileName(event *output.ResultEvent) string {
|
||||
filenameBuilder := &strings.Builder{}
|
||||
filenameBuilder.WriteString(event.TemplateID)
|
||||
filenameBuilder.WriteString("-")
|
||||
|
@ -69,29 +94,8 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||
filenameBuilder.WriteRune('-')
|
||||
filenameBuilder.WriteString(event.MatcherName)
|
||||
}
|
||||
filenameBuilder.WriteString(".md")
|
||||
finalFilename := sanitizeFilename(filenameBuilder.String())
|
||||
|
||||
dataBuilder := &bytes.Buffer{}
|
||||
dataBuilder.WriteString("### ")
|
||||
dataBuilder.WriteString(summary)
|
||||
dataBuilder.WriteString("\n---\n")
|
||||
dataBuilder.WriteString(description)
|
||||
data := dataBuilder.Bytes()
|
||||
|
||||
// index generation
|
||||
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.WriteString("|[" + event.Host + "](" + finalFilename + ")" + "|" + event.TemplateID + " " + event.MatcherName + "|" + event.Info.SeverityHolder.Severity.String() + "|\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Join(exporter.directory, finalFilename), data, 0644)
|
||||
filenameBuilder.WriteString(extension)
|
||||
return sanitizeFilename(filenameBuilder.String())
|
||||
}
|
||||
|
||||
// Close closes the exporter after operation
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type MarkdownFormatter struct{}
|
||||
|
||||
func (markdownFormatter MarkdownFormatter) MakeBold(text string) string {
|
||||
return MakeBold(text)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (markdownFormatter MarkdownFormatter) CreateTable(headers []string, rows [][]string) (string, error) {
|
||||
return CreateTable(headers, rows)
|
||||
}
|
||||
|
||||
func (markdownFormatter MarkdownFormatter) CreateLink(title string, url string) string {
|
||||
return CreateLink(title, url)
|
||||
}
|
||||
|
||||
func (markdownFormatter MarkdownFormatter) CreateHorizontalLine() string {
|
||||
return CreateHorizontalLine()
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
errorutil "github.com/projectdiscovery/utils/errors"
|
||||
)
|
||||
|
||||
func CreateLink(title string, url string) string {
|
||||
return fmt.Sprintf("[%s](%s)", title, url)
|
||||
}
|
||||
|
||||
func MakeBold(text string) string {
|
||||
return "**" + text + "**"
|
||||
}
|
||||
|
||||
func CreateTable(headers []string, rows [][]string) (string, error) {
|
||||
builder := &bytes.Buffer{}
|
||||
headerSize := len(headers)
|
||||
if headers == nil || headerSize == 0 {
|
||||
return "", errorutil.New("No headers provided")
|
||||
}
|
||||
|
||||
builder.WriteString(CreateTableHeader(headers...))
|
||||
|
||||
for _, row := range rows {
|
||||
rowSize := len(row)
|
||||
if rowSize == headerSize {
|
||||
builder.WriteString(CreateTableRow(row...))
|
||||
} else if rowSize < headerSize {
|
||||
extendedRows := make([]string, headerSize)
|
||||
copy(extendedRows, row)
|
||||
builder.WriteString(CreateTableRow(extendedRows...))
|
||||
} else {
|
||||
return "", errorutil.New("Too many columns for the given headers")
|
||||
}
|
||||
}
|
||||
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
func CreateTableHeader(headers ...string) string {
|
||||
headerSize := len(headers)
|
||||
if headers == nil || headerSize == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return CreateTableRow(headers...) +
|
||||
"|" + strings.Repeat(" --- |", headerSize) + "\n"
|
||||
}
|
||||
|
||||
func CreateTableRow(elements ...string) string {
|
||||
return fmt.Sprintf("| %s |\n", strings.Join(elements, " | "))
|
||||
}
|
||||
|
||||
func CreateHeading3(text string) string {
|
||||
return "### " + text + "\n"
|
||||
}
|
||||
|
||||
func CreateHorizontalLine() string {
|
||||
// for regular markdown 3 dashes are enough, but for Jira the minimum is 4
|
||||
return "----\n"
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMarkDownHeaderCreation(t *testing.T) {
|
||||
testCases := []struct {
|
||||
headers []string
|
||||
expectedValue string
|
||||
}{
|
||||
{nil, ""},
|
||||
{[]string{}, ""},
|
||||
{[]string{"one"}, "| one |\n| --- |\n"},
|
||||
{[]string{"one", "two"}, "| one | two |\n| --- | --- |\n"},
|
||||
{[]string{"one", "two", "three"}, "| one | two | three |\n| --- | --- | --- |\n"},
|
||||
}
|
||||
|
||||
for _, currentTestCase := range testCases {
|
||||
t.Run(strings.Join(currentTestCase.headers, ","), func(t1 *testing.T) {
|
||||
assert.Equal(t1, CreateTableHeader(currentTestCase.headers...), currentTestCase.expectedValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateTemplateInfoTableTooManyColumns(t *testing.T) {
|
||||
table, err := CreateTable([]string{"one", "two"}, [][]string{
|
||||
{"a", "b", "c"},
|
||||
{"d"},
|
||||
{"e", "f", "g"},
|
||||
{"h", "i"},
|
||||
})
|
||||
|
||||
assert.NotNil(t, err)
|
||||
assert.Empty(t, table)
|
||||
}
|
||||
|
||||
func TestCreateTemplateInfoTable1Column(t *testing.T) {
|
||||
table, err := CreateTable([]string{"one"}, [][]string{{"a"}, {"b"}, {"c"}})
|
||||
|
||||
expected := `| one |
|
||||
| --- |
|
||||
| a |
|
||||
| b |
|
||||
| c |
|
||||
`
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, table)
|
||||
}
|
||||
|
||||
func TestCreateTemplateInfoTable2Columns(t *testing.T) {
|
||||
table, err := CreateTable([]string{"one", "two"}, [][]string{
|
||||
{"a", "b"},
|
||||
{"c"},
|
||||
{"d", "e"},
|
||||
})
|
||||
|
||||
expected := `| one | two |
|
||||
| --- | --- |
|
||||
| a | b |
|
||||
| c | |
|
||||
| d | e |
|
||||
`
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, table)
|
||||
}
|
||||
|
||||
func TestCreateTemplateInfoTable3Columns(t *testing.T) {
|
||||
table, err := CreateTable([]string{"one", "two", "three"}, [][]string{
|
||||
{"a", "b", "c"},
|
||||
{"d"},
|
||||
{"e", "f", "g"},
|
||||
{"h", "i"},
|
||||
})
|
||||
|
||||
expected := `| one | two | three |
|
||||
| --- | --- | --- |
|
||||
| a | b | c |
|
||||
| d | | |
|
||||
| e | f | g |
|
||||
| h | i | |
|
||||
`
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, expected, table)
|
||||
}
|
|
@ -60,7 +60,7 @@ func (exporter *Exporter) addToolDetails() {
|
|||
}
|
||||
exporter.sarif.RegisterTool(driver)
|
||||
|
||||
reportloc := sarif.ArtifactLocation{
|
||||
reportLocation := sarif.ArtifactLocation{
|
||||
Uri: "file:///" + exporter.options.File,
|
||||
Description: &sarif.Message{
|
||||
Text: "Nuclei Sarif Report",
|
||||
|
@ -70,7 +70,7 @@ func (exporter *Exporter) addToolDetails() {
|
|||
invocation := sarif.Invocation{
|
||||
CommandLine: os.Args[0],
|
||||
Arguments: os.Args[1:],
|
||||
ResponseFiles: []sarif.ArtifactLocation{reportloc},
|
||||
ResponseFiles: []sarif.ArtifactLocation{reportLocation},
|
||||
}
|
||||
exporter.sarif.RegisterToolInvocation(invocation)
|
||||
}
|
||||
|
@ -102,10 +102,10 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||
resultHeader := fmt.Sprintf("%v (%v) found on %v", event.Info.Name, event.TemplateID, event.Host)
|
||||
resultLevel, vulnRating := exporter.getSeverity(severity)
|
||||
|
||||
// Extra metdata if generated sarif is uploaded to github security page
|
||||
ghmeta := map[string]interface{}{}
|
||||
ghmeta["tags"] = []string{"security"}
|
||||
ghmeta["security-severity"] = vulnRating
|
||||
// Extra metadata if generated sarif is uploaded to GitHub security page
|
||||
ghMeta := map[string]interface{}{}
|
||||
ghMeta["tags"] = []string{"security"}
|
||||
ghMeta["security-severity"] = vulnRating
|
||||
|
||||
// rule contain details of template
|
||||
rule := sarif.ReportingDescriptor{
|
||||
|
@ -115,10 +115,10 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||
// Points to template URL
|
||||
Text: event.Info.Description + "\nMore details at\n" + event.TemplateURL + "\n",
|
||||
},
|
||||
Properties: ghmeta,
|
||||
Properties: ghMeta,
|
||||
}
|
||||
|
||||
// Github Uses ShortDescription as title
|
||||
// GitHub Uses ShortDescription as title
|
||||
if event.Info.Description != "" {
|
||||
rule.ShortDescription = &sarif.MultiformatMessageString{
|
||||
Text: resultHeader,
|
||||
|
@ -141,7 +141,7 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||
},
|
||||
PhysicalLocation: sarif.PhysicalLocation{
|
||||
ArtifactLocation: sarif.ArtifactLocation{
|
||||
// github only accepts file:// protocol and local & relative files only
|
||||
// GitHub only accepts file:// protocol and local & relative files only
|
||||
// to avoid errors // is used which also translates to file according to specification
|
||||
Uri: "/" + event.Path,
|
||||
Description: &sarif.Message{
|
||||
|
@ -193,5 +193,4 @@ func (exporter *Exporter) Close() error {
|
|||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
|
|
@ -1,245 +1,9 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
)
|
||||
|
||||
// Summary returns a formatted built one line summary of the event
|
||||
func Summary(event *output.ResultEvent) string {
|
||||
template := GetMatchedTemplate(event)
|
||||
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString(types.ToString(event.Info.Name))
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(template)
|
||||
builder.WriteString(") found on ")
|
||||
builder.WriteString(event.Host)
|
||||
data := builder.String()
|
||||
return data
|
||||
}
|
||||
|
||||
// MarkdownDescription formats a short description of the generated
|
||||
// event by the nuclei scanner in Markdown format.
|
||||
func MarkdownDescription(event *output.ResultEvent) string { // TODO remove the code duplication: format.go <-> jira.go
|
||||
template := GetMatchedTemplate(event)
|
||||
builder := &bytes.Buffer{}
|
||||
builder.WriteString("**Details**: **")
|
||||
builder.WriteString(template)
|
||||
builder.WriteString("** ")
|
||||
|
||||
builder.WriteString(" matched at ")
|
||||
builder.WriteString(event.Host)
|
||||
|
||||
builder.WriteString("\n\n**Protocol**: ")
|
||||
builder.WriteString(strings.ToUpper(event.Type))
|
||||
|
||||
builder.WriteString("\n\n**Full URL**: ")
|
||||
builder.WriteString(event.Matched)
|
||||
|
||||
builder.WriteString("\n\n**Timestamp**: ")
|
||||
builder.WriteString(event.Timestamp.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
|
||||
|
||||
builder.WriteString("\n\n**Template Information**\n\n| Key | Value |\n|---|---|\n")
|
||||
builder.WriteString(ToMarkdownTableString(&event.Info))
|
||||
|
||||
if event.Request != "" {
|
||||
builder.WriteString(createMarkdownCodeBlock("Request", types.ToHexOrString(event.Request), "http"))
|
||||
}
|
||||
if event.Response != "" {
|
||||
var responseString string
|
||||
// If the response is larger than 5 kb, truncate it before writing.
|
||||
if len(event.Response) > 5*1024 {
|
||||
responseString = (event.Response[:5*1024])
|
||||
responseString += ".... Truncated ...."
|
||||
} else {
|
||||
responseString = event.Response
|
||||
}
|
||||
builder.WriteString(createMarkdownCodeBlock("Response", responseString, "http"))
|
||||
}
|
||||
|
||||
if len(event.ExtractedResults) > 0 || len(event.Metadata) > 0 {
|
||||
builder.WriteString("\n**Extra Information**\n\n")
|
||||
|
||||
if len(event.ExtractedResults) > 0 {
|
||||
builder.WriteString("**Extracted results**:\n\n")
|
||||
for _, v := range event.ExtractedResults {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(v)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if len(event.Metadata) > 0 {
|
||||
builder.WriteString("**Metadata**:\n\n")
|
||||
for k, v := range event.Metadata {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(k)
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(types.ToString(v))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if event.Interaction != nil {
|
||||
builder.WriteString("**Interaction Data**\n---\n")
|
||||
builder.WriteString(event.Interaction.Protocol)
|
||||
if event.Interaction.QType != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(event.Interaction.QType)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString(" Interaction from ")
|
||||
builder.WriteString(event.Interaction.RemoteAddress)
|
||||
builder.WriteString(" at ")
|
||||
builder.WriteString(event.Interaction.UniqueID)
|
||||
|
||||
if event.Interaction.RawRequest != "" {
|
||||
builder.WriteString(createMarkdownCodeBlock("Interaction Request", event.Interaction.RawRequest, ""))
|
||||
}
|
||||
if event.Interaction.RawResponse != "" {
|
||||
builder.WriteString(createMarkdownCodeBlock("Interaction Response", event.Interaction.RawResponse, ""))
|
||||
}
|
||||
}
|
||||
|
||||
reference := event.Info.Reference
|
||||
if !reference.IsEmpty() {
|
||||
builder.WriteString("\nReferences: \n")
|
||||
|
||||
referenceSlice := reference.ToSlice()
|
||||
for i, item := range referenceSlice {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(item)
|
||||
if len(referenceSlice)-1 != i {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
|
||||
if event.CURLCommand != "" {
|
||||
builder.WriteString("\n**CURL Command**\n```\n")
|
||||
builder.WriteString(types.ToHexOrString(event.CURLCommand))
|
||||
builder.WriteString("\n```")
|
||||
}
|
||||
|
||||
builder.WriteString(fmt.Sprintf("\n---\nGenerated by [Nuclei %s](https://github.com/projectdiscovery/nuclei)", config.Version))
|
||||
data := builder.String()
|
||||
return data
|
||||
}
|
||||
|
||||
// GetMatchedTemplate returns the matched template from a result event
|
||||
func GetMatchedTemplate(event *output.ResultEvent) string {
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString(event.TemplateID)
|
||||
if event.MatcherName != "" {
|
||||
builder.WriteString(":")
|
||||
builder.WriteString(event.MatcherName)
|
||||
}
|
||||
if event.ExtractorName != "" {
|
||||
builder.WriteString(":")
|
||||
builder.WriteString(event.ExtractorName)
|
||||
}
|
||||
template := builder.String()
|
||||
return template
|
||||
}
|
||||
|
||||
func ToMarkdownTableString(templateInfo *model.Info) string {
|
||||
fields := utils.NewEmptyInsertionOrderedStringMap(5)
|
||||
fields.Set("Name", templateInfo.Name)
|
||||
fields.Set("Authors", templateInfo.Authors.String())
|
||||
fields.Set("Tags", templateInfo.Tags.String())
|
||||
fields.Set("Severity", templateInfo.SeverityHolder.Severity.String())
|
||||
fields.Set("Description", lineBreakToHTML(templateInfo.Description))
|
||||
fields.Set("Remediation", lineBreakToHTML(templateInfo.Remediation))
|
||||
|
||||
classification := templateInfo.Classification
|
||||
if classification != nil {
|
||||
if classification.CVSSMetrics != "" {
|
||||
generateCVSSMetricsFromClassification(classification, fields)
|
||||
}
|
||||
generateCVECWEIDLinksFromClassification(classification, fields)
|
||||
fields.Set("CVSS-Score", strconv.FormatFloat(classification.CVSSScore, 'f', 2, 64))
|
||||
}
|
||||
|
||||
builder := &bytes.Buffer{}
|
||||
|
||||
toMarkDownTable := func(insertionOrderedStringMap *utils.InsertionOrderedStringMap) {
|
||||
insertionOrderedStringMap.ForEach(func(key string, value interface{}) {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if !utils.IsBlank(value) {
|
||||
builder.WriteString(fmt.Sprintf("| %s | %s |\n", key, value))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
toMarkDownTable(fields)
|
||||
toMarkDownTable(utils.NewInsertionOrderedStringMap(templateInfo.Metadata))
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func generateCVSSMetricsFromClassification(classification *model.Classification, fields *utils.InsertionOrderedStringMap) {
|
||||
// Generate cvss link
|
||||
var cvssLinkPrefix string
|
||||
if strings.Contains(classification.CVSSMetrics, "CVSS:3.0") {
|
||||
cvssLinkPrefix = "https://www.first.org/cvss/calculator/3.0#"
|
||||
} else if strings.Contains(classification.CVSSMetrics, "CVSS:3.1") {
|
||||
cvssLinkPrefix = "https://www.first.org/cvss/calculator/3.1#"
|
||||
}
|
||||
if cvssLinkPrefix != "" {
|
||||
fields.Set("CVSS-Metrics", fmt.Sprintf("[%s](%s%s)", classification.CVSSMetrics, cvssLinkPrefix, classification.CVSSMetrics))
|
||||
} else {
|
||||
fields.Set("CVSS-Metrics", classification.CVSSMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
func generateCVECWEIDLinksFromClassification(classification *model.Classification, fields *utils.InsertionOrderedStringMap) {
|
||||
cwes := classification.CWEID.ToSlice()
|
||||
|
||||
cweIDs := make([]string, 0, len(cwes))
|
||||
for _, value := range cwes {
|
||||
parts := strings.Split(value, "-")
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
cweIDs = append(cweIDs, fmt.Sprintf("[%s](https://cwe.mitre.org/data/definitions/%s.html)", strings.ToUpper(value), parts[1]))
|
||||
}
|
||||
if len(cweIDs) > 0 {
|
||||
fields.Set("CWE-ID", strings.Join(cweIDs, ","))
|
||||
}
|
||||
|
||||
cves := classification.CVEID.ToSlice()
|
||||
|
||||
cveIDs := make([]string, 0, len(cves))
|
||||
for _, value := range cves {
|
||||
cveIDs = append(cveIDs, fmt.Sprintf("[%s](https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s)", strings.ToUpper(value), value))
|
||||
}
|
||||
if len(cveIDs) > 0 {
|
||||
fields.Set("CVE-ID", strings.Join(cveIDs, ","))
|
||||
}
|
||||
}
|
||||
|
||||
func createMarkdownCodeBlock(title string, content string, language string) string {
|
||||
return "\n" + createBoldMarkdown(title) + "\n```" + language + "\n" + content + "\n```\n"
|
||||
}
|
||||
|
||||
func createBoldMarkdown(value string) string {
|
||||
return "**" + value + "**"
|
||||
}
|
||||
|
||||
func lineBreakToHTML(text string) string {
|
||||
return strings.ReplaceAll(text, "\n", "<br>")
|
||||
type ResultFormatter interface {
|
||||
MakeBold(text string) string
|
||||
CreateCodeBlock(title string, content string, language string) string
|
||||
CreateTable(headers []string, rows [][]string) (string, error)
|
||||
CreateLink(title string, url string) string
|
||||
CreateHorizontalLine() string
|
||||
}
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// Summary returns a formatted built one line summary of the event
|
||||
func Summary(event *output.ResultEvent) string {
|
||||
return fmt.Sprintf("%s (%s) found on %s", types.ToString(event.Info.Name), GetMatchedTemplateName(event), event.Host)
|
||||
}
|
||||
|
||||
// GetMatchedTemplateName returns the matched template name from a result event
|
||||
// together with the found matcher and extractor name, if present
|
||||
func GetMatchedTemplateName(event *output.ResultEvent) string {
|
||||
matchedTemplateName := event.TemplateID
|
||||
if event.MatcherName != "" {
|
||||
matchedTemplateName += ":" + event.MatcherName
|
||||
}
|
||||
|
||||
if event.ExtractorName != "" {
|
||||
matchedTemplateName += ":" + event.ExtractorName
|
||||
}
|
||||
|
||||
return matchedTemplateName
|
||||
}
|
||||
|
||||
func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatter) string {
|
||||
template := GetMatchedTemplateName(event)
|
||||
builder := &bytes.Buffer{}
|
||||
builder.WriteString(fmt.Sprintf("%s: %s matched at %s\n\n", formatter.MakeBold("Details"), formatter.MakeBold(template), event.Host))
|
||||
|
||||
attributes := utils.NewEmptyInsertionOrderedStringMap(3)
|
||||
attributes.Set("Protocol", strings.ToUpper(event.Type))
|
||||
attributes.Set("Full URL", event.Matched)
|
||||
attributes.Set("Timestamp", event.Timestamp.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
|
||||
attributes.ForEach(func(key string, data interface{}) {
|
||||
builder.WriteString(fmt.Sprintf("%s: %s\n\n", formatter.MakeBold(key), types.ToString(data)))
|
||||
})
|
||||
|
||||
builder.WriteString(formatter.MakeBold("Template Information"))
|
||||
builder.WriteString("\n\n")
|
||||
builder.WriteString(CreateTemplateInfoTable(&event.Info, formatter))
|
||||
|
||||
if event.Request != "" {
|
||||
builder.WriteString(formatter.CreateCodeBlock("Request", types.ToHexOrString(event.Request), "http"))
|
||||
}
|
||||
if event.Response != "" {
|
||||
var responseString string
|
||||
// If the response is larger than 5 kb, truncate it before writing.
|
||||
maxKbSize := 5 * 1024
|
||||
if len(event.Response) > maxKbSize {
|
||||
responseString = event.Response[:maxKbSize]
|
||||
responseString += ".... Truncated ...."
|
||||
} else {
|
||||
responseString = event.Response
|
||||
}
|
||||
builder.WriteString(formatter.CreateCodeBlock("Response", responseString, "http"))
|
||||
}
|
||||
|
||||
if len(event.ExtractedResults) > 0 || len(event.Metadata) > 0 {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString(formatter.MakeBold("Extra Information"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
if len(event.ExtractedResults) > 0 {
|
||||
builder.WriteString(formatter.MakeBold("Extracted results:"))
|
||||
builder.WriteString("\n\n")
|
||||
|
||||
for _, v := range event.ExtractedResults {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(v)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if len(event.Metadata) > 0 {
|
||||
builder.WriteString(formatter.MakeBold("Metadata:"))
|
||||
builder.WriteString("\n\n")
|
||||
for k, v := range event.Metadata {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(k)
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(types.ToString(v))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if event.Interaction != nil {
|
||||
builder.WriteString(fmt.Sprintf("%s\n%s", formatter.MakeBold("Interaction Data"), formatter.CreateHorizontalLine()))
|
||||
builder.WriteString(event.Interaction.Protocol)
|
||||
if event.Interaction.QType != "" {
|
||||
builder.WriteString(fmt.Sprintf(" (%s)", event.Interaction.QType))
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf(" Interaction from %s at %s", event.Interaction.RemoteAddress, event.Interaction.UniqueID))
|
||||
|
||||
if event.Interaction.RawRequest != "" {
|
||||
builder.WriteString(formatter.CreateCodeBlock("Interaction Request", event.Interaction.RawRequest, ""))
|
||||
}
|
||||
if event.Interaction.RawResponse != "" {
|
||||
builder.WriteString(formatter.CreateCodeBlock("Interaction Response", event.Interaction.RawResponse, ""))
|
||||
}
|
||||
}
|
||||
|
||||
reference := event.Info.Reference
|
||||
if !reference.IsEmpty() {
|
||||
builder.WriteString("\nReferences: \n")
|
||||
|
||||
referenceSlice := reference.ToSlice()
|
||||
for i, item := range referenceSlice {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(item)
|
||||
if len(referenceSlice)-1 != i {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
|
||||
if event.CURLCommand != "" {
|
||||
builder.WriteString(
|
||||
formatter.CreateCodeBlock("CURL command", types.ToHexOrString(event.CURLCommand), "sh"),
|
||||
)
|
||||
}
|
||||
|
||||
builder.WriteString("\n" + formatter.CreateHorizontalLine() + "\n")
|
||||
builder.WriteString(fmt.Sprintf("Generated by %s", formatter.CreateLink("Nuclei "+config.Version, "https://github.com/projectdiscovery/nuclei")))
|
||||
data := builder.String()
|
||||
return data
|
||||
}
|
||||
|
||||
func CreateTemplateInfoTable(templateInfo *model.Info, formatter ResultFormatter) string {
|
||||
rows := [][]string{
|
||||
{"Name", templateInfo.Name},
|
||||
{"Authors", templateInfo.Authors.String()},
|
||||
{"Tags", templateInfo.Tags.String()},
|
||||
{"Severity", templateInfo.SeverityHolder.Severity.String()},
|
||||
}
|
||||
|
||||
if !utils.IsBlank(templateInfo.Description) {
|
||||
rows = append(rows, []string{"Description", lineBreakToHTML(templateInfo.Description)})
|
||||
}
|
||||
|
||||
if !utils.IsBlank(templateInfo.Remediation) {
|
||||
rows = append(rows, []string{"Remediation", lineBreakToHTML(templateInfo.Remediation)})
|
||||
}
|
||||
|
||||
classification := templateInfo.Classification
|
||||
if classification != nil {
|
||||
if classification.CVSSMetrics != "" {
|
||||
rows = append(rows, []string{"CVSS-Metrics", generateCVSSMetricsFromClassification(classification)})
|
||||
}
|
||||
|
||||
rows = append(rows, generateCVECWEIDLinksFromClassification(classification)...)
|
||||
rows = append(rows, []string{"CVSS-Score", strconv.FormatFloat(classification.CVSSScore, 'f', 2, 64)})
|
||||
}
|
||||
|
||||
for key, value := range templateInfo.Metadata {
|
||||
switch value := value.(type) {
|
||||
case string:
|
||||
if !utils.IsBlank(value) {
|
||||
rows = append(rows, []string{key, value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table, _ := formatter.CreateTable([]string{"Key", "Value"}, rows)
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
func generateCVSSMetricsFromClassification(classification *model.Classification) string {
|
||||
var cvssLinkPrefix string
|
||||
if strings.Contains(classification.CVSSMetrics, "CVSS:3.0") {
|
||||
cvssLinkPrefix = "https://www.first.org/cvss/calculator/3.0#"
|
||||
} else if strings.Contains(classification.CVSSMetrics, "CVSS:3.1") {
|
||||
cvssLinkPrefix = "https://www.first.org/cvss/calculator/3.1#"
|
||||
}
|
||||
|
||||
if cvssLinkPrefix == "" {
|
||||
return classification.CVSSMetrics
|
||||
} else {
|
||||
return util.CreateLink(classification.CVSSMetrics, cvssLinkPrefix+classification.CVSSMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
func generateCVECWEIDLinksFromClassification(classification *model.Classification) [][]string {
|
||||
cwes := classification.CWEID.ToSlice()
|
||||
|
||||
cweIDs := make([]string, 0, len(cwes))
|
||||
for _, value := range cwes {
|
||||
parts := strings.Split(value, "-")
|
||||
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])))
|
||||
}
|
||||
|
||||
var rows [][]string
|
||||
|
||||
if len(cweIDs) > 0 {
|
||||
rows = append(rows, []string{"CWE-ID", strings.Join(cweIDs, ",")})
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
if len(cveIDs) > 0 {
|
||||
rows = append(rows, []string{"CVE-ID", strings.Join(cveIDs, ",")})
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
func lineBreakToHTML(text string) string {
|
||||
return strings.ReplaceAll(text, "\n", "<br>")
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
package format
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/stringslice"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
)
|
||||
|
||||
func TestToMarkdownTableString(t *testing.T) {
|
||||
|
@ -25,9 +25,11 @@ func TestToMarkdownTableString(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
result := ToMarkdownTableString(&info)
|
||||
result := CreateTemplateInfoTable(&info, &util.MarkdownFormatter{})
|
||||
|
||||
expectedOrderedAttributes := `| Name | Test Template Name |
|
||||
expectedOrderedAttributes := `| Key | Value |
|
||||
| --- | --- |
|
||||
| Name | Test Template Name |
|
||||
| Authors | forgedhallpass, ice3man |
|
||||
| Tags | cve, misc |
|
||||
| Severity | high |
|
|
@ -7,12 +7,12 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/google/go-github/github"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
|
@ -28,7 +28,7 @@ type Integration struct {
|
|||
type Options struct {
|
||||
// BaseURL (optional) is the self-hosted GitHub application url
|
||||
BaseURL string `yaml:"base-url" validate:"omitempty,url"`
|
||||
// Username is the username of the github user
|
||||
// Username is the username of the GitHub user
|
||||
Username string `yaml:"username" validate:"required"`
|
||||
// Owner is the owner name of the repository for issues.
|
||||
Owner string `yaml:"owner" validate:"required"`
|
||||
|
@ -78,7 +78,7 @@ func New(options *Options) (*Integration, error) {
|
|||
// CreateIssue creates an issue in the tracker
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
summary := format.Summary(event)
|
||||
description := format.MarkdownDescription(event)
|
||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{})
|
||||
labels := []string{}
|
||||
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
||||
if i.options.SeverityAsLabel && severityLabel != "" {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/xanzy/go-gitlab"
|
||||
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
@ -59,7 +60,7 @@ func New(options *Options) (*Integration, error) {
|
|||
// CreateIssue creates an issue in the tracker
|
||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||
summary := format.Summary(event)
|
||||
description := format.MarkdownDescription(event)
|
||||
description := format.CreateReportDescription(event, util.MarkdownFormatter{})
|
||||
labels := []string{}
|
||||
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
||||
if i.options.SeverityAsLabel && severityLabel != "" {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
@ -10,15 +9,41 @@ import (
|
|||
"github.com/trivago/tgo/tcontainer"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
"github.com/projectdiscovery/retryablehttp-go"
|
||||
)
|
||||
|
||||
type Formatter struct {
|
||||
util.MarkdownFormatter
|
||||
}
|
||||
|
||||
func (jiraFormatter *Formatter) MakeBold(text string) string {
|
||||
return "*" + text + "*"
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (jiraFormatter *Formatter) CreateTable(headers []string, rows [][]string) (string, error) {
|
||||
table, err := jiraFormatter.MarkdownFormatter.CreateTable(headers, rows)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tableRows := strings.Split(table, "\n")
|
||||
tableRowsWithoutHeaderSeparator := append(tableRows[:1], tableRows[2:]...)
|
||||
return strings.Join(tableRowsWithoutHeaderSeparator, "\n"), nil
|
||||
}
|
||||
|
||||
func (jiraFormatter *Formatter) CreateLink(title string, url string) string {
|
||||
return fmt.Sprintf("[%s|%s]", title, url)
|
||||
}
|
||||
|
||||
// Integration is a client for an issue tracker integration
|
||||
type Integration struct {
|
||||
Formatter
|
||||
jira *jira.Client
|
||||
options *Options
|
||||
}
|
||||
|
@ -129,7 +154,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||
}
|
||||
}
|
||||
fields := &jira.IssueFields{
|
||||
Description: jiraFormatDescription(event),
|
||||
Description: format.CreateReportDescription(event, i),
|
||||
Unknowns: customFields,
|
||||
Type: jira.IssueType{Name: i.options.IssueType},
|
||||
Project: jira.Project{Key: i.options.ProjectName},
|
||||
|
@ -139,7 +164,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||
if !i.options.Cloud {
|
||||
fields = &jira.IssueFields{
|
||||
Assignee: &jira.User{Name: i.options.AccountID},
|
||||
Description: jiraFormatDescription(event),
|
||||
Description: format.CreateReportDescription(event, i),
|
||||
Type: jira.IssueType{Name: i.options.IssueType},
|
||||
Project: jira.Project{Key: i.options.ProjectName},
|
||||
Summary: summary,
|
||||
|
@ -171,7 +196,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||
return err
|
||||
} else if issueID != "" {
|
||||
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
|
||||
Body: jiraFormatDescription(event),
|
||||
Body: format.CreateReportDescription(event, i),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
@ -181,7 +206,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||
|
||||
// FindExistingIssue checks if the issue already exists and returns its ID
|
||||
func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) {
|
||||
template := format.GetMatchedTemplate(event)
|
||||
template := format.GetMatchedTemplateName(event)
|
||||
jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\"", template, event.Host, i.options.StatusNot)
|
||||
|
||||
searchOptions := &jira.SearchOptions{
|
||||
|
@ -208,117 +233,3 @@ func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, erro
|
|||
return chunk[0].ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
// jiraFormatDescription formats a short description of the generated
|
||||
// event by the nuclei scanner in Jira format.
|
||||
func jiraFormatDescription(event *output.ResultEvent) string { // TODO remove the code duplication: format.go <-> jira.go
|
||||
template := format.GetMatchedTemplate(event)
|
||||
|
||||
builder := &bytes.Buffer{}
|
||||
builder.WriteString("*Details*: *")
|
||||
builder.WriteString(template)
|
||||
builder.WriteString("* ")
|
||||
|
||||
builder.WriteString(" matched at ")
|
||||
builder.WriteString(event.Host)
|
||||
|
||||
builder.WriteString("\n\n*Protocol*: ")
|
||||
builder.WriteString(strings.ToUpper(event.Type))
|
||||
|
||||
builder.WriteString("\n\n*Full URL*: ")
|
||||
builder.WriteString(event.Matched)
|
||||
|
||||
builder.WriteString("\n\n*Timestamp*: ")
|
||||
builder.WriteString(event.Timestamp.Format("Mon Jan 2 15:04:05 -0700 MST 2006"))
|
||||
|
||||
builder.WriteString("\n\n*Template Information*\n\n| Key | Value |\n")
|
||||
builder.WriteString(format.ToMarkdownTableString(&event.Info))
|
||||
|
||||
builder.WriteString(createMarkdownCodeBlock("Request", event.Request))
|
||||
|
||||
builder.WriteString("\n*Response*\n\n{code}\n")
|
||||
// If the response is larger than 5 kb, truncate it before writing.
|
||||
if len(event.Response) > 5*1024 {
|
||||
builder.WriteString(event.Response[:5*1024])
|
||||
builder.WriteString(".... Truncated ....")
|
||||
} else {
|
||||
builder.WriteString(event.Response)
|
||||
}
|
||||
builder.WriteString("\n{code}\n\n")
|
||||
|
||||
if len(event.ExtractedResults) > 0 || len(event.Metadata) > 0 {
|
||||
builder.WriteString("\n*Extra Information*\n\n")
|
||||
if len(event.ExtractedResults) > 0 {
|
||||
builder.WriteString("*Extracted results*:\n\n")
|
||||
for _, v := range event.ExtractedResults {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(v)
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
if len(event.Metadata) > 0 {
|
||||
builder.WriteString("*Metadata*:\n\n")
|
||||
for k, v := range event.Metadata {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(k)
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(types.ToString(v))
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if event.Interaction != nil {
|
||||
builder.WriteString("*Interaction Data*\n---\n")
|
||||
builder.WriteString(event.Interaction.Protocol)
|
||||
if event.Interaction.QType != "" {
|
||||
builder.WriteString(" (")
|
||||
builder.WriteString(event.Interaction.QType)
|
||||
builder.WriteString(")")
|
||||
}
|
||||
builder.WriteString(" Interaction from ")
|
||||
builder.WriteString(event.Interaction.RemoteAddress)
|
||||
builder.WriteString(" at ")
|
||||
builder.WriteString(event.Interaction.UniqueID)
|
||||
|
||||
if event.Interaction.RawRequest != "" {
|
||||
builder.WriteString(createMarkdownCodeBlock("Interaction Request", event.Interaction.RawRequest))
|
||||
}
|
||||
if event.Interaction.RawResponse != "" {
|
||||
builder.WriteString(createMarkdownCodeBlock("Interaction Response", event.Interaction.RawResponse))
|
||||
}
|
||||
}
|
||||
|
||||
reference := event.Info.Reference
|
||||
if !reference.IsEmpty() {
|
||||
builder.WriteString("\nReferences: \n")
|
||||
|
||||
referenceSlice := reference.ToSlice()
|
||||
for i, item := range referenceSlice {
|
||||
builder.WriteString("- ")
|
||||
builder.WriteString(item)
|
||||
if len(referenceSlice)-1 != i {
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
|
||||
if event.CURLCommand != "" {
|
||||
builder.WriteString("\n*CURL Command*\n{code}\n")
|
||||
builder.WriteString(event.CURLCommand)
|
||||
builder.WriteString("\n{code}")
|
||||
}
|
||||
builder.WriteString(fmt.Sprintf("\n---\nGenerated by [Nuclei %s](https://github.com/projectdiscovery/nuclei)", config.Version))
|
||||
data := builder.String()
|
||||
return data
|
||||
}
|
||||
|
||||
func createMarkdownCodeBlock(title string, content string) string {
|
||||
return "\n" + createBoldMarkdown(title) + "\n" + content + "*\n\n{code}"
|
||||
}
|
||||
|
||||
func createBoldMarkdown(value string) string {
|
||||
return "*" + value + "*\n\n{code}"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package jira
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinkCreation(t *testing.T) {
|
||||
jiraIntegration := &Integration{}
|
||||
link := jiraIntegration.CreateLink("ProjectDiscovery", "https://projectdiscovery.io")
|
||||
assert.Equal(t, "[ProjectDiscovery|https://projectdiscovery.io]", link)
|
||||
}
|
||||
|
||||
func TestHorizontalLineCreation(t *testing.T) {
|
||||
jiraIntegration := &Integration{}
|
||||
horizontalLine := jiraIntegration.CreateHorizontalLine()
|
||||
assert.True(t, strings.Contains(horizontalLine, "----"))
|
||||
}
|
||||
|
||||
func TestTableCreation(t *testing.T) {
|
||||
jiraIntegration := &Integration{}
|
||||
|
||||
table, err := jiraIntegration.CreateTable([]string{"key", "value"}, [][]string{
|
||||
{"a", "b"},
|
||||
{"c"},
|
||||
{"d", "e"},
|
||||
})
|
||||
|
||||
assert.Nil(t, err)
|
||||
expected := `| key | value |
|
||||
| a | b |
|
||||
| c | |
|
||||
| d | e |
|
||||
`
|
||||
assert.Equal(t, expected, table)
|
||||
}
|
Loading…
Reference in New Issue