Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/reporting/reporting.go
2070 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
storage, err := dedupe.New(db)
196
if err != nil {
197
return nil, err
198
}
199
client.dedupe = storage
200
return client, nil
201
}
202
203
// CreateConfigIfNotExists creates report-config if it doesn't exist
204
func CreateConfigIfNotExists() error {
205
reportingConfig := config.DefaultConfig.GetReportingConfigFilePath()
206
207
if fileutil.FileExists(reportingConfig) {
208
return nil
209
}
210
values := stringslice.StringSlice{Value: []string{}}
211
212
options := &Options{
213
AllowList: &filters.Filter{Tags: values},
214
DenyList: &filters.Filter{Tags: values},
215
GitHub: &github.Options{},
216
GitLab: &gitlab.Options{},
217
Gitea: &gitea.Options{},
218
Jira: &jira.Options{},
219
Linear: &linear.Options{},
220
MarkdownExporter: &markdown.Options{},
221
SarifExporter: &sarif.Options{},
222
ElasticsearchExporter: &es.Options{},
223
SplunkExporter: &splunk.Options{},
224
JSONExporter: &json_exporter.Options{},
225
JSONLExporter: &jsonl.Options{},
226
MongoDBExporter: &mongo.Options{},
227
}
228
reportingFile, err := os.Create(reportingConfig)
229
if err != nil {
230
return errkit.Wrap(err, "could not create config file")
231
}
232
defer func() {
233
_ = reportingFile.Close()
234
}()
235
236
err = yaml.NewEncoder(reportingFile).Encode(options)
237
return err
238
}
239
240
// RegisterTracker registers a custom tracker to the reporter
241
func (c *ReportingClient) RegisterTracker(tracker Tracker) {
242
c.trackers = append(c.trackers, tracker)
243
}
244
245
// RegisterExporter registers a custom exporter to the reporter
246
func (c *ReportingClient) RegisterExporter(exporter Exporter) {
247
c.exporters = append(c.exporters, exporter)
248
}
249
250
// Close closes the issue tracker reporting client
251
func (c *ReportingClient) Close() {
252
// If we have stats for the trackers, print them
253
if len(c.stats) > 0 {
254
for _, tracker := range c.trackers {
255
trackerName := tracker.Name()
256
257
if stats, ok := c.stats[trackerName]; ok {
258
created := stats.Created.Load()
259
if created == 0 {
260
continue
261
}
262
var msgBuilder strings.Builder
263
msgBuilder.WriteString(fmt.Sprintf("%d %s tickets created successfully", created, trackerName))
264
failed := stats.Failed.Load()
265
if failed > 0 {
266
msgBuilder.WriteString(fmt.Sprintf(", %d failed", failed))
267
}
268
gologger.Info().Msgf("%v", msgBuilder.String())
269
}
270
}
271
}
272
273
if c.dedupe != nil {
274
c.dedupe.Close()
275
}
276
for _, exporter := range c.exporters {
277
_ = exporter.Close()
278
}
279
}
280
281
// CreateIssue creates an issue in the tracker
282
func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
283
// process global allow/deny list
284
if c.options.AllowList != nil && !c.options.AllowList.GetMatch(event) {
285
return nil
286
}
287
if c.options.DenyList != nil && c.options.DenyList.GetMatch(event) {
288
return nil
289
}
290
291
if c.options.ValidatorCallback != nil && !c.options.ValidatorCallback(event) {
292
return nil
293
}
294
295
var err error
296
unique := true
297
if c.dedupe != nil {
298
unique, err = c.dedupe.Index(event)
299
}
300
if unique {
301
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
302
303
for _, tracker := range c.trackers {
304
// process tracker specific allow/deny list
305
if !tracker.ShouldFilter(event) {
306
continue
307
}
308
309
trackerName := tracker.Name()
310
stats, statsOk := c.stats[trackerName]
311
312
reportData, trackerErr := tracker.CreateIssue(event)
313
if trackerErr != nil {
314
if statsOk {
315
_ = stats.Failed.Add(1)
316
}
317
err = multierr.Append(err, trackerErr)
318
continue
319
}
320
if statsOk {
321
_ = stats.Created.Add(1)
322
}
323
324
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
325
IssueID: reportData.IssueID,
326
IssueURL: reportData.IssueURL,
327
}
328
}
329
for _, exporter := range c.exporters {
330
if exportErr := exporter.Export(event); exportErr != nil {
331
err = multierr.Append(err, exportErr)
332
}
333
}
334
}
335
return err
336
}
337
338
// CloseIssue closes an issue in the tracker
339
func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
340
for _, tracker := range c.trackers {
341
if !tracker.ShouldFilter(event) {
342
continue
343
}
344
if err := tracker.CloseIssue(event); err != nil {
345
return err
346
}
347
}
348
return nil
349
}
350
351
func (c *ReportingClient) GetReportingOptions() *Options {
352
return c.options
353
}
354
355
func (c *ReportingClient) Clear() {
356
if c.dedupe != nil {
357
c.dedupe.Clear()
358
}
359
}
360
361