Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/reporting/reporting.go
2843 views
1
package reporting
2
3
import (
4
"fmt"
5
"os"
6
"strings"
7
"sync/atomic"
8
9
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
10
11
"github.com/projectdiscovery/gologger"
12
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
13
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
14
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
15
16
"go.uber.org/multierr"
17
"gopkg.in/yaml.v2"
18
19
"errors"
20
21
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
22
"github.com/projectdiscovery/nuclei/v3/pkg/output"
23
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/dedupe"
24
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/es"
25
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
26
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/sarif"
27
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/splunk"
28
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
29
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/gitea"
30
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/github"
31
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/gitlab"
32
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/jira"
33
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/linear"
34
"github.com/projectdiscovery/utils/errkit"
35
fileutil "github.com/projectdiscovery/utils/file"
36
)
37
38
var (
39
ErrReportingClientCreation = errors.New("could not create reporting client")
40
ErrExportClientCreation = errors.New("could not create exporting client")
41
)
42
43
// Tracker is an interface implemented by an issue tracker
44
type Tracker interface {
45
// Name returns the name of the tracker
46
Name() string
47
// CreateIssue creates an issue in the tracker
48
CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error)
49
// CloseIssue closes an issue in the tracker
50
CloseIssue(event *output.ResultEvent) error
51
// ShouldFilter determines if the event should be filtered out
52
ShouldFilter(event *output.ResultEvent) bool
53
}
54
55
// Exporter is an interface implemented by an issue exporter
56
type Exporter interface {
57
// Close closes the exporter after operation
58
Close() error
59
// Export exports an issue to an exporter
60
Export(event *output.ResultEvent) error
61
}
62
63
// ReportingClient is a client for nuclei issue tracking module
64
type ReportingClient struct {
65
trackers []Tracker
66
exporters []Exporter
67
options *Options
68
dedupe *dedupe.Storage
69
70
stats map[string]*IssueTrackerStats
71
}
72
73
type IssueTrackerStats struct {
74
Created atomic.Int32
75
Failed atomic.Int32
76
}
77
78
// New creates a new nuclei issue tracker reporting client
79
func New(options *Options, db string, doNotDedupe bool) (Client, error) {
80
client := &ReportingClient{options: options}
81
82
if options.GitHub != nil {
83
options.GitHub.HttpClient = options.HttpClient
84
options.GitHub.OmitRaw = options.OmitRaw
85
tracker, err := github.New(options.GitHub)
86
if err != nil {
87
return nil, errkit.Wrapf(err, "could not create reporting client: %v", ErrReportingClientCreation)
88
}
89
client.trackers = append(client.trackers, tracker)
90
}
91
if options.GitLab != nil {
92
options.GitLab.HttpClient = options.HttpClient
93
options.GitLab.OmitRaw = options.OmitRaw
94
tracker, err := gitlab.New(options.GitLab)
95
if err != nil {
96
return nil, errkit.Wrapf(err, "could not create reporting client: %v", ErrReportingClientCreation)
97
}
98
client.trackers = append(client.trackers, tracker)
99
}
100
if options.Gitea != nil {
101
options.Gitea.HttpClient = options.HttpClient
102
options.Gitea.OmitRaw = options.OmitRaw
103
tracker, err := gitea.New(options.Gitea)
104
if err != nil {
105
return nil, errkit.Wrapf(err, "could not create reporting client: %v", ErrReportingClientCreation)
106
}
107
client.trackers = append(client.trackers, tracker)
108
}
109
if options.Jira != nil {
110
options.Jira.HttpClient = options.HttpClient
111
options.Jira.OmitRaw = options.OmitRaw
112
tracker, err := jira.New(options.Jira)
113
if err != nil {
114
return nil, errkit.Wrapf(err, "could not create reporting client: %v", ErrReportingClientCreation)
115
}
116
client.trackers = append(client.trackers, tracker)
117
}
118
if options.Linear != nil {
119
options.Linear.HttpClient = options.HttpClient
120
options.Linear.OmitRaw = options.OmitRaw
121
tracker, err := linear.New(options.Linear)
122
if err != nil {
123
return nil, errkit.Wrapf(err, "could not create reporting client: %v", ErrReportingClientCreation)
124
}
125
client.trackers = append(client.trackers, tracker)
126
}
127
if options.MarkdownExporter != nil {
128
exporter, err := markdown.New(options.MarkdownExporter)
129
if err != nil {
130
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
131
}
132
client.exporters = append(client.exporters, exporter)
133
}
134
if options.SarifExporter != nil {
135
exporter, err := sarif.New(options.SarifExporter)
136
if err != nil {
137
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
138
}
139
client.exporters = append(client.exporters, exporter)
140
}
141
if options.JSONExporter != nil {
142
exporter, err := json_exporter.New(options.JSONExporter)
143
if err != nil {
144
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
145
}
146
client.exporters = append(client.exporters, exporter)
147
}
148
if options.JSONLExporter != nil {
149
exporter, err := jsonl.New(options.JSONLExporter)
150
if err != nil {
151
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
152
}
153
client.exporters = append(client.exporters, exporter)
154
}
155
if options.ElasticsearchExporter != nil {
156
options.ElasticsearchExporter.HttpClient = options.HttpClient
157
options.ElasticsearchExporter.ExecutionId = options.ExecutionId
158
exporter, err := es.New(options.ElasticsearchExporter)
159
if err != nil {
160
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
161
}
162
client.exporters = append(client.exporters, exporter)
163
}
164
if options.SplunkExporter != nil {
165
options.SplunkExporter.HttpClient = options.HttpClient
166
options.SplunkExporter.ExecutionId = options.ExecutionId
167
exporter, err := splunk.New(options.SplunkExporter)
168
if err != nil {
169
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
170
}
171
client.exporters = append(client.exporters, exporter)
172
}
173
if options.MongoDBExporter != nil {
174
exporter, err := mongo.New(options.MongoDBExporter)
175
if err != nil {
176
return nil, errkit.Wrapf(err, "could not create export client: %v", ErrExportClientCreation)
177
}
178
client.exporters = append(client.exporters, exporter)
179
}
180
181
if doNotDedupe {
182
return client, nil
183
}
184
185
client.stats = make(map[string]*IssueTrackerStats)
186
for _, tracker := range client.trackers {
187
trackerName := tracker.Name()
188
189
client.stats[trackerName] = &IssueTrackerStats{
190
Created: atomic.Int32{},
191
Failed: atomic.Int32{},
192
}
193
}
194
195
if db != "" || len(client.trackers) > 0 || len(client.exporters) > 0 {
196
storage, err := dedupe.New(db)
197
if err != nil {
198
return nil, err
199
}
200
client.dedupe = storage
201
}
202
return client, nil
203
}
204
205
// CreateConfigIfNotExists creates report-config if it doesn't exist
206
func CreateConfigIfNotExists() error {
207
reportingConfig := config.DefaultConfig.GetReportingConfigFilePath()
208
209
if fileutil.FileExists(reportingConfig) {
210
return nil
211
}
212
values := stringslice.StringSlice{Value: []string{}}
213
214
options := &Options{
215
AllowList: &filters.Filter{Tags: values},
216
DenyList: &filters.Filter{Tags: values},
217
GitHub: &github.Options{},
218
GitLab: &gitlab.Options{},
219
Gitea: &gitea.Options{},
220
Jira: &jira.Options{},
221
Linear: &linear.Options{},
222
MarkdownExporter: &markdown.Options{},
223
SarifExporter: &sarif.Options{},
224
ElasticsearchExporter: &es.Options{},
225
SplunkExporter: &splunk.Options{},
226
JSONExporter: &json_exporter.Options{},
227
JSONLExporter: &jsonl.Options{},
228
MongoDBExporter: &mongo.Options{},
229
}
230
reportingFile, err := os.Create(reportingConfig)
231
if err != nil {
232
return errkit.Wrap(err, "could not create config file")
233
}
234
defer func() {
235
_ = reportingFile.Close()
236
}()
237
238
err = yaml.NewEncoder(reportingFile).Encode(options)
239
return err
240
}
241
242
// RegisterTracker registers a custom tracker to the reporter
243
func (c *ReportingClient) RegisterTracker(tracker Tracker) {
244
c.trackers = append(c.trackers, tracker)
245
}
246
247
// RegisterExporter registers a custom exporter to the reporter
248
func (c *ReportingClient) RegisterExporter(exporter Exporter) {
249
c.exporters = append(c.exporters, exporter)
250
}
251
252
// Close closes the issue tracker reporting client
253
func (c *ReportingClient) Close() {
254
// If we have stats for the trackers, print them
255
if len(c.stats) > 0 {
256
for _, tracker := range c.trackers {
257
trackerName := tracker.Name()
258
259
if stats, ok := c.stats[trackerName]; ok {
260
created := stats.Created.Load()
261
if created == 0 {
262
continue
263
}
264
var msgBuilder strings.Builder
265
msgBuilder.WriteString(fmt.Sprintf("%d %s tickets created successfully", created, trackerName))
266
failed := stats.Failed.Load()
267
if failed > 0 {
268
msgBuilder.WriteString(fmt.Sprintf(", %d failed", failed))
269
}
270
gologger.Info().Msgf("%v", msgBuilder.String())
271
}
272
}
273
}
274
275
if c.dedupe != nil {
276
c.dedupe.Close()
277
}
278
for _, exporter := range c.exporters {
279
_ = exporter.Close()
280
}
281
}
282
283
// CreateIssue creates an issue in the tracker
284
func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
285
// process global allow/deny list
286
if c.options.AllowList != nil && !c.options.AllowList.GetMatch(event) {
287
return nil
288
}
289
if c.options.DenyList != nil && c.options.DenyList.GetMatch(event) {
290
return nil
291
}
292
293
if c.options.ValidatorCallback != nil && !c.options.ValidatorCallback(event) {
294
return nil
295
}
296
297
var err error
298
unique := true
299
if c.dedupe != nil {
300
unique, err = c.dedupe.Index(event)
301
}
302
if unique {
303
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
304
305
for _, tracker := range c.trackers {
306
// process tracker specific allow/deny list
307
if !tracker.ShouldFilter(event) {
308
continue
309
}
310
311
trackerName := tracker.Name()
312
stats, statsOk := c.stats[trackerName]
313
314
reportData, trackerErr := tracker.CreateIssue(event)
315
if trackerErr != nil {
316
if statsOk {
317
_ = stats.Failed.Add(1)
318
}
319
err = multierr.Append(err, trackerErr)
320
continue
321
}
322
if statsOk {
323
_ = stats.Created.Add(1)
324
}
325
326
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
327
IssueID: reportData.IssueID,
328
IssueURL: reportData.IssueURL,
329
}
330
}
331
for _, exporter := range c.exporters {
332
if exportErr := exporter.Export(event); exportErr != nil {
333
err = multierr.Append(err, exportErr)
334
}
335
}
336
}
337
return err
338
}
339
340
// CloseIssue closes an issue in the tracker
341
func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
342
for _, tracker := range c.trackers {
343
if !tracker.ShouldFilter(event) {
344
continue
345
}
346
if err := tracker.CloseIssue(event); err != nil {
347
return err
348
}
349
}
350
return nil
351
}
352
353
func (c *ReportingClient) GetReportingOptions() *Options {
354
return c.options
355
}
356
357
func (c *ReportingClient) Clear() {
358
if c.dedupe != nil {
359
c.dedupe.Clear()
360
}
361
}
362
363