mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-21 17:45:27 +00:00
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>
This commit is contained in:
parent
4d8c4b7024
commit
442fc0f060
@ -7,11 +7,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
"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/reporting/format"
|
||||||
stringsutil "github.com/projectdiscovery/utils/strings"
|
stringsutil "github.com/projectdiscovery/utils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const indexFileName = "index.md"
|
const indexFileName = "index.md"
|
||||||
|
const extension = ".md"
|
||||||
|
|
||||||
type Exporter struct {
|
type Exporter struct {
|
||||||
directory string
|
directory string
|
||||||
@ -37,9 +39,7 @@ func New(options *Options) (*Exporter, error) {
|
|||||||
_ = os.MkdirAll(directory, 0755)
|
_ = os.MkdirAll(directory, 0755)
|
||||||
|
|
||||||
// index generation header
|
// index generation header
|
||||||
dataHeader := "" +
|
dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")
|
||||||
"|Hostname/IP|Finding|Severity|\n" +
|
|
||||||
"|-|-|-|\n"
|
|
||||||
|
|
||||||
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
|
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -51,9 +51,34 @@ func New(options *Options) (*Exporter, error) {
|
|||||||
|
|
||||||
// Export exports a passed result event to markdown
|
// Export exports a passed result event to markdown
|
||||||
func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
||||||
summary := format.Summary(event)
|
// index file generation
|
||||||
description := format.MarkdownDescription(event)
|
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 := &strings.Builder{}
|
||||||
filenameBuilder.WriteString(event.TemplateID)
|
filenameBuilder.WriteString(event.TemplateID)
|
||||||
filenameBuilder.WriteString("-")
|
filenameBuilder.WriteString("-")
|
||||||
@ -69,29 +94,8 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||||||
filenameBuilder.WriteRune('-')
|
filenameBuilder.WriteRune('-')
|
||||||
filenameBuilder.WriteString(event.MatcherName)
|
filenameBuilder.WriteString(event.MatcherName)
|
||||||
}
|
}
|
||||||
filenameBuilder.WriteString(".md")
|
filenameBuilder.WriteString(extension)
|
||||||
finalFilename := sanitizeFilename(filenameBuilder.String())
|
return 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the exporter after operation
|
// 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()
|
||||||
|
}
|
||||||
65
v2/pkg/reporting/exporters/markdown/util/markdown_utils.go
Normal file
65
v2/pkg/reporting/exporters/markdown/util/markdown_utils.go
Normal file
@ -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)
|
exporter.sarif.RegisterTool(driver)
|
||||||
|
|
||||||
reportloc := sarif.ArtifactLocation{
|
reportLocation := sarif.ArtifactLocation{
|
||||||
Uri: "file:///" + exporter.options.File,
|
Uri: "file:///" + exporter.options.File,
|
||||||
Description: &sarif.Message{
|
Description: &sarif.Message{
|
||||||
Text: "Nuclei Sarif Report",
|
Text: "Nuclei Sarif Report",
|
||||||
@ -70,7 +70,7 @@ func (exporter *Exporter) addToolDetails() {
|
|||||||
invocation := sarif.Invocation{
|
invocation := sarif.Invocation{
|
||||||
CommandLine: os.Args[0],
|
CommandLine: os.Args[0],
|
||||||
Arguments: os.Args[1:],
|
Arguments: os.Args[1:],
|
||||||
ResponseFiles: []sarif.ArtifactLocation{reportloc},
|
ResponseFiles: []sarif.ArtifactLocation{reportLocation},
|
||||||
}
|
}
|
||||||
exporter.sarif.RegisterToolInvocation(invocation)
|
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)
|
resultHeader := fmt.Sprintf("%v (%v) found on %v", event.Info.Name, event.TemplateID, event.Host)
|
||||||
resultLevel, vulnRating := exporter.getSeverity(severity)
|
resultLevel, vulnRating := exporter.getSeverity(severity)
|
||||||
|
|
||||||
// Extra metdata if generated sarif is uploaded to github security page
|
// Extra metadata if generated sarif is uploaded to GitHub security page
|
||||||
ghmeta := map[string]interface{}{}
|
ghMeta := map[string]interface{}{}
|
||||||
ghmeta["tags"] = []string{"security"}
|
ghMeta["tags"] = []string{"security"}
|
||||||
ghmeta["security-severity"] = vulnRating
|
ghMeta["security-severity"] = vulnRating
|
||||||
|
|
||||||
// rule contain details of template
|
// rule contain details of template
|
||||||
rule := sarif.ReportingDescriptor{
|
rule := sarif.ReportingDescriptor{
|
||||||
@ -115,10 +115,10 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||||||
// Points to template URL
|
// Points to template URL
|
||||||
Text: event.Info.Description + "\nMore details at\n" + event.TemplateURL + "\n",
|
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 != "" {
|
if event.Info.Description != "" {
|
||||||
rule.ShortDescription = &sarif.MultiformatMessageString{
|
rule.ShortDescription = &sarif.MultiformatMessageString{
|
||||||
Text: resultHeader,
|
Text: resultHeader,
|
||||||
@ -141,7 +141,7 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|||||||
},
|
},
|
||||||
PhysicalLocation: sarif.PhysicalLocation{
|
PhysicalLocation: sarif.PhysicalLocation{
|
||||||
ArtifactLocation: sarif.ArtifactLocation{
|
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
|
// to avoid errors // is used which also translates to file according to specification
|
||||||
Uri: "/" + event.Path,
|
Uri: "/" + event.Path,
|
||||||
Description: &sarif.Message{
|
Description: &sarif.Message{
|
||||||
@ -193,5 +193,4 @@ func (exporter *Exporter) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,245 +1,9 @@
|
|||||||
package format
|
package format
|
||||||
|
|
||||||
import (
|
type ResultFormatter interface {
|
||||||
"bytes"
|
MakeBold(text string) string
|
||||||
"fmt"
|
CreateCodeBlock(title string, content string, language string) string
|
||||||
"strconv"
|
CreateTable(headers []string, rows [][]string) (string, error)
|
||||||
"strings"
|
CreateLink(title string, url string) string
|
||||||
|
CreateHorizontalLine() string
|
||||||
"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>")
|
|
||||||
}
|
}
|
||||||
|
|||||||
229
v2/pkg/reporting/format/format_utils.go
Normal file
229
v2/pkg/reporting/format/format_utils.go
Normal file
@ -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
|
package format
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/model"
|
"github.com/projectdiscovery/nuclei/v2/pkg/model"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity"
|
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/severity"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/stringslice"
|
"github.com/projectdiscovery/nuclei/v2/pkg/model/types/stringslice"
|
||||||
|
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/exporters/markdown/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestToMarkdownTableString(t *testing.T) {
|
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 |
|
| Authors | forgedhallpass, ice3man |
|
||||||
| Tags | cve, misc |
|
| Tags | cve, misc |
|
||||||
| Severity | high |
|
| Severity | high |
|
||||||
@ -7,12 +7,12 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
|
||||||
|
|
||||||
"github.com/google/go-github/github"
|
"github.com/google/go-github/github"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
"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/reporting/format"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||||
"github.com/projectdiscovery/retryablehttp-go"
|
"github.com/projectdiscovery/retryablehttp-go"
|
||||||
@ -28,7 +28,7 @@ type Integration struct {
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
// BaseURL (optional) is the self-hosted GitHub application url
|
// BaseURL (optional) is the self-hosted GitHub application url
|
||||||
BaseURL string `yaml:"base-url" validate:"omitempty,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"`
|
Username string `yaml:"username" validate:"required"`
|
||||||
// Owner is the owner name of the repository for issues.
|
// Owner is the owner name of the repository for issues.
|
||||||
Owner string `yaml:"owner" validate:"required"`
|
Owner string `yaml:"owner" validate:"required"`
|
||||||
@ -78,7 +78,7 @@ func New(options *Options) (*Integration, error) {
|
|||||||
// CreateIssue creates an issue in the tracker
|
// CreateIssue creates an issue in the tracker
|
||||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||||
summary := format.Summary(event)
|
summary := format.Summary(event)
|
||||||
description := format.MarkdownDescription(event)
|
description := format.CreateReportDescription(event, util.MarkdownFormatter{})
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
||||||
if i.options.SeverityAsLabel && severityLabel != "" {
|
if i.options.SeverityAsLabel && severityLabel != "" {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/xanzy/go-gitlab"
|
"github.com/xanzy/go-gitlab"
|
||||||
|
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
"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/reporting/format"
|
||||||
"github.com/projectdiscovery/retryablehttp-go"
|
"github.com/projectdiscovery/retryablehttp-go"
|
||||||
)
|
)
|
||||||
@ -59,7 +60,7 @@ func New(options *Options) (*Integration, error) {
|
|||||||
// CreateIssue creates an issue in the tracker
|
// CreateIssue creates an issue in the tracker
|
||||||
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
||||||
summary := format.Summary(event)
|
summary := format.Summary(event)
|
||||||
description := format.MarkdownDescription(event)
|
description := format.CreateReportDescription(event, util.MarkdownFormatter{})
|
||||||
labels := []string{}
|
labels := []string{}
|
||||||
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
severityLabel := fmt.Sprintf("Severity: %s", event.Info.SeverityHolder.Severity.String())
|
||||||
if i.options.SeverityAsLabel && severityLabel != "" {
|
if i.options.SeverityAsLabel && severityLabel != "" {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package jira
|
package jira
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
@ -10,15 +9,41 @@ import (
|
|||||||
"github.com/trivago/tgo/tcontainer"
|
"github.com/trivago/tgo/tcontainer"
|
||||||
|
|
||||||
"github.com/projectdiscovery/gologger"
|
"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/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/reporting/format"
|
||||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
|
||||||
"github.com/projectdiscovery/retryablehttp-go"
|
"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
|
// Integration is a client for an issue tracker integration
|
||||||
type Integration struct {
|
type Integration struct {
|
||||||
|
Formatter
|
||||||
jira *jira.Client
|
jira *jira.Client
|
||||||
options *Options
|
options *Options
|
||||||
}
|
}
|
||||||
@ -129,7 +154,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fields := &jira.IssueFields{
|
fields := &jira.IssueFields{
|
||||||
Description: jiraFormatDescription(event),
|
Description: format.CreateReportDescription(event, i),
|
||||||
Unknowns: customFields,
|
Unknowns: customFields,
|
||||||
Type: jira.IssueType{Name: i.options.IssueType},
|
Type: jira.IssueType{Name: i.options.IssueType},
|
||||||
Project: jira.Project{Key: i.options.ProjectName},
|
Project: jira.Project{Key: i.options.ProjectName},
|
||||||
@ -139,7 +164,7 @@ func (i *Integration) CreateNewIssue(event *output.ResultEvent) error {
|
|||||||
if !i.options.Cloud {
|
if !i.options.Cloud {
|
||||||
fields = &jira.IssueFields{
|
fields = &jira.IssueFields{
|
||||||
Assignee: &jira.User{Name: i.options.AccountID},
|
Assignee: &jira.User{Name: i.options.AccountID},
|
||||||
Description: jiraFormatDescription(event),
|
Description: format.CreateReportDescription(event, i),
|
||||||
Type: jira.IssueType{Name: i.options.IssueType},
|
Type: jira.IssueType{Name: i.options.IssueType},
|
||||||
Project: jira.Project{Key: i.options.ProjectName},
|
Project: jira.Project{Key: i.options.ProjectName},
|
||||||
Summary: summary,
|
Summary: summary,
|
||||||
@ -171,7 +196,7 @@ func (i *Integration) CreateIssue(event *output.ResultEvent) error {
|
|||||||
return err
|
return err
|
||||||
} else if issueID != "" {
|
} else if issueID != "" {
|
||||||
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
|
_, _, err = i.jira.Issue.AddComment(issueID, &jira.Comment{
|
||||||
Body: jiraFormatDescription(event),
|
Body: format.CreateReportDescription(event, i),
|
||||||
})
|
})
|
||||||
return err
|
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
|
// FindExistingIssue checks if the issue already exists and returns its ID
|
||||||
func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, error) {
|
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)
|
jql := fmt.Sprintf("summary ~ \"%s\" AND summary ~ \"%s\" AND status != \"%s\"", template, event.Host, i.options.StatusNot)
|
||||||
|
|
||||||
searchOptions := &jira.SearchOptions{
|
searchOptions := &jira.SearchOptions{
|
||||||
@ -208,117 +233,3 @@ func (i *Integration) FindExistingIssue(event *output.ResultEvent) (string, erro
|
|||||||
return chunk[0].ID, nil
|
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}"
|
|
||||||
}
|
|
||||||
|
|||||||
37
v2/pkg/reporting/trackers/jira/jira_test.go
Normal file
37
v2/pkg/reporting/trackers/jira/jira_test.go
Normal file
@ -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…
x
Reference in New Issue
Block a user