Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/reporting/exporters/markdown/markdown.go
2070 views
1
package markdown
2
3
import (
4
"bytes"
5
"os"
6
"path/filepath"
7
"strings"
8
9
"github.com/google/uuid"
10
"github.com/projectdiscovery/gologger"
11
12
"github.com/projectdiscovery/nuclei/v3/pkg/output"
13
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
14
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
15
fileutil "github.com/projectdiscovery/utils/file"
16
stringsutil "github.com/projectdiscovery/utils/strings"
17
)
18
19
const indexFileName = "index.md"
20
const extension = ".md"
21
22
type Exporter struct {
23
directory string
24
options *Options
25
}
26
27
// Options contains the configuration options for GitHub issue tracker client
28
type Options struct {
29
// Directory is the directory to export found results to
30
Directory string `yaml:"directory"`
31
OmitRaw bool `yaml:"omit-raw"`
32
SortMode string `yaml:"sort-mode"`
33
}
34
35
// New creates a new markdown exporter integration client based on options.
36
func New(options *Options) (*Exporter, error) {
37
directory := options.Directory
38
if options.Directory == "" {
39
dir, err := os.Getwd()
40
if err != nil {
41
return nil, err
42
}
43
directory = dir
44
}
45
_ = os.MkdirAll(directory, 0755)
46
47
// index generation header
48
dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")
49
50
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
51
if err != nil {
52
return nil, err
53
}
54
55
return &Exporter{options: options, directory: directory}, nil
56
}
57
58
// Export exports a passed result event to markdown
59
func (exporter *Exporter) Export(event *output.ResultEvent) error {
60
// index file generation
61
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)
62
if err != nil {
63
return err
64
}
65
defer func() {
66
_ = file.Close()
67
}()
68
69
filename := createFileName(event)
70
71
// If the sort mode is set to severity, host, or template, then we need to get a safe version of the name for a
72
// subdirectory to store the file in.
73
// This will allow us to sort the files into subdirectories based on the sort mode. The subdirectory will need to
74
// be created if it does not exist.
75
fileUrl := filename
76
subdirectory := ""
77
switch exporter.options.SortMode {
78
case "severity":
79
subdirectory = event.Info.SeverityHolder.Severity.String()
80
case "host":
81
subdirectory = event.Host
82
case "template":
83
subdirectory = event.TemplateID
84
}
85
if subdirectory != "" {
86
// Sanitize the subdirectory name to remove any characters that are not allowed in a directory name
87
subdirectory = sanitizeFilename(subdirectory)
88
89
// Prepend the subdirectory name to the filename for the fileUrl
90
fileUrl = filepath.Join(subdirectory, filename)
91
92
// Create the subdirectory if it does not exist
93
if err = fileutil.CreateFolders(filepath.Join(exporter.directory, subdirectory)); err != nil {
94
gologger.Warning().Msgf("Could not create subdirectory for markdown report: %s", err)
95
}
96
}
97
98
host := util.CreateLink(event.Host, fileUrl)
99
finding := event.TemplateID + " " + event.MatcherName
100
severity := event.Info.SeverityHolder.Severity.String()
101
102
_, err = file.WriteString(util.CreateTableRow(host, finding, severity))
103
if err != nil {
104
return err
105
}
106
107
dataBuilder := &bytes.Buffer{}
108
dataBuilder.WriteString(util.CreateHeading3(format.Summary(event)))
109
dataBuilder.WriteString("\n")
110
dataBuilder.WriteString(util.CreateHorizontalLine())
111
dataBuilder.WriteString(format.CreateReportDescription(event, util.MarkdownFormatter{}, exporter.options.OmitRaw))
112
data := dataBuilder.Bytes()
113
114
return os.WriteFile(filepath.Join(exporter.directory, subdirectory, filename), data, 0644)
115
}
116
117
func createFileName(event *output.ResultEvent) string {
118
filenameBuilder := &strings.Builder{}
119
filenameBuilder.WriteString(event.TemplateID)
120
filenameBuilder.WriteString("-")
121
filenameBuilder.WriteString(event.Host)
122
filenameBuilder.WriteString("-")
123
filenameBuilder.WriteString(uuid.NewString())
124
125
var suffix string
126
if event.MatcherName != "" {
127
suffix = event.MatcherName
128
} else if event.ExtractorName != "" {
129
suffix = event.ExtractorName
130
}
131
if suffix != "" {
132
filenameBuilder.WriteRune('-')
133
filenameBuilder.WriteString(event.MatcherName)
134
}
135
filenameBuilder.WriteString(extension)
136
return sanitizeFilename(filenameBuilder.String())
137
}
138
139
// Close closes the exporter after operation
140
func (exporter *Exporter) Close() error {
141
return nil
142
}
143
144
func sanitizeFilename(filename string) string {
145
if len(filename) > 256 {
146
filename = filename[0:255]
147
}
148
return stringsutil.ReplaceAll(filename, "_", "?", "/", ">", "|", ":", ";", "*", "<", "\"", "'", " ")
149
}
150
151