Skip to content

Commit fd024a3

Browse files
authored
feat: issue tracker URLs in JSON + misc fixes (#4855)
* feat: issue tracker URLs in JSON + misc fixes * misc changes * feat: status update support for issues * feat: report metadata generation hook support * feat: added CLI summary of tickets created * misc changes
1 parent b1b4f0f commit fd024a3

File tree

15 files changed

+350
-53
lines changed

15 files changed

+350
-53
lines changed

cmd/integration-test/library.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func executeNucleiAsLibrary(templatePath, templateURL string) ([]string, error)
6969
defer cache.Close()
7070

7171
mockProgress := &testutils.MockProgressClient{}
72-
reportingClient, err := reporting.New(&reporting.Options{}, "")
72+
reportingClient, err := reporting.New(&reporting.Options{}, "", false)
7373
if err != nil {
7474
return nil, err
7575
}

internal/runner/options.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,14 @@ func createReportingOptions(options *types.Options) (*reporting.Options, error)
290290
// configureOutput configures the output logging levels to be displayed on the screen
291291
func configureOutput(options *types.Options) {
292292
// If the user desires verbose output, show verbose output
293-
if options.Verbose || options.Validate {
294-
gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose)
295-
}
296293
if options.Debug || options.DebugRequests || options.DebugResponse {
297294
gologger.DefaultLogger.SetMaxLevel(levels.LevelDebug)
298295
}
296+
// Debug takes precedence before verbose
297+
// because debug is a lower logging level.
298+
if options.Verbose || options.Validate {
299+
gologger.DefaultLogger.SetMaxLevel(levels.LevelVerbose)
300+
}
299301
if options.NoColor {
300302
gologger.DefaultLogger.SetFormatter(formatter.NewCLI(true))
301303
}

internal/runner/runner.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ func New(options *types.Options) (*Runner, error) {
190190
}
191191

192192
if reportingOptions != nil {
193-
client, err := reporting.New(reportingOptions, options.ReportingDB)
193+
client, err := reporting.New(reportingOptions, options.ReportingDB, false)
194194
if err != nil {
195195
return nil, errors.Wrap(err, "could not create issue reporting client")
196196
}

lib/sdk_private.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func (e *NucleiEngine) init() error {
128128
return err
129129
}
130130
// we don't support reporting config in sdk mode
131-
if e.rc, err = reporting.New(&reporting.Options{}, ""); err != nil {
131+
if e.rc, err = reporting.New(&reporting.Options{}, "", false); err != nil {
132132
return err
133133
}
134134
e.interactshOpts.IssuesClient = e.rc

pkg/output/output.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,20 @@ type ResultEvent struct {
165165
// Lines is the line count for the specified match
166166
Lines []int `json:"matched-line,omitempty"`
167167

168+
// IssueTrackers is the metadata for issue trackers
169+
IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"`
170+
168171
FileToIndexPosition map[string]int `json:"-"`
169172
Error string `json:"error,omitempty"`
170173
}
171174

175+
type IssueTrackerMetadata struct {
176+
// IssueID is the ID of the issue created
177+
IssueID string `json:"id,omitempty"`
178+
// IssueURL is the URL of the issue created
179+
IssueURL string `json:"url,omitempty"`
180+
}
181+
172182
// NewStandardWriter creates a new output writer based on user configurations
173183
func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
174184
resumeBool := false

pkg/protocols/common/helpers/writer/writer.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,18 @@ func WriteResult(data *output.InternalWrappedEvent, output output.Writer, progre
1717
}
1818
var matched bool
1919
for _, result := range data.Results {
20+
if issuesClient != nil {
21+
if err := issuesClient.CreateIssue(result); err != nil {
22+
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
23+
}
24+
}
2025
if err := output.Write(result); err != nil {
2126
gologger.Warning().Msgf("Could not write output event: %s\n", err)
2227
}
2328
if !matched {
2429
matched = true
2530
}
2631
progress.IncrementMatched()
27-
28-
if issuesClient != nil {
29-
if err := issuesClient.CreateIssue(result); err != nil {
30-
gologger.Warning().Msgf("Could not create issue on tracker: %s", err)
31-
}
32-
}
3332
}
3433
return matched
3534
}

pkg/reporting/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ type Client interface {
1111
Close()
1212
Clear()
1313
CreateIssue(event *output.ResultEvent) error
14+
CloseIssue(event *output.ResultEvent) error
1415
GetReportingOptions() *Options
1516
}

pkg/reporting/format/format_utils.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ func GetMatchedTemplateName(event *output.ResultEvent) string {
3434
return matchedTemplateName
3535
}
3636

37+
type reportMetadataEditorHook func(event *output.ResultEvent, formatter ResultFormatter) string
38+
39+
var (
40+
// ReportGenerationMetadataHooks are the hooks for adding metadata to the report
41+
ReportGenerationMetadataHooks []reportMetadataEditorHook
42+
)
43+
3744
func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatter, omitRaw bool) string {
3845
template := GetMatchedTemplateName(event)
3946
builder := &bytes.Buffer{}
@@ -137,6 +144,12 @@ func CreateReportDescription(event *output.ResultEvent, formatter ResultFormatte
137144

138145
builder.WriteString("\n" + formatter.CreateHorizontalLine() + "\n")
139146
builder.WriteString(fmt.Sprintf("Generated by %s", formatter.CreateLink("Nuclei "+config.Version, "https://github.com/projectdiscovery/nuclei")))
147+
148+
if len(ReportGenerationMetadataHooks) > 0 {
149+
for _, hook := range ReportGenerationMetadataHooks {
150+
builder.WriteString(hook(event, formatter))
151+
}
152+
}
140153
data := builder.String()
141154
return data
142155
}

pkg/reporting/reporting.go

Lines changed: 95 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package reporting
22

33
import (
4+
"fmt"
45
"os"
6+
"strings"
7+
"sync/atomic"
58

9+
"github.com/projectdiscovery/gologger"
610
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
711
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
812
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
@@ -35,8 +39,12 @@ var (
3539

3640
// Tracker is an interface implemented by an issue tracker
3741
type Tracker interface {
42+
// Name returns the name of the tracker
43+
Name() string
3844
// CreateIssue creates an issue in the tracker
39-
CreateIssue(event *output.ResultEvent) error
45+
CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error)
46+
// CloseIssue closes an issue in the tracker
47+
CloseIssue(event *output.ResultEvent) error
4048
// ShouldFilter determines if the event should be filtered out
4149
ShouldFilter(event *output.ResultEvent) bool
4250
}
@@ -55,10 +63,17 @@ type ReportingClient struct {
5563
exporters []Exporter
5664
options *Options
5765
dedupe *dedupe.Storage
66+
67+
stats map[string]*IssueTrackerStats
68+
}
69+
70+
type IssueTrackerStats struct {
71+
Created atomic.Int32
72+
Failed atomic.Int32
5873
}
5974

6075
// New creates a new nuclei issue tracker reporting client
61-
func New(options *Options, db string) (Client, error) {
76+
func New(options *Options, db string, doNotDedupe bool) (Client, error) {
6277
client := &ReportingClient{options: options}
6378

6479
if options.GitHub != nil {
@@ -142,6 +157,20 @@ func New(options *Options, db string) (Client, error) {
142157
client.exporters = append(client.exporters, exporter)
143158
}
144159

160+
if doNotDedupe {
161+
return client, nil
162+
}
163+
164+
client.stats = make(map[string]*IssueTrackerStats)
165+
for _, tracker := range client.trackers {
166+
trackerName := tracker.Name()
167+
168+
client.stats[trackerName] = &IssueTrackerStats{
169+
Created: atomic.Int32{},
170+
Failed: atomic.Int32{},
171+
}
172+
}
173+
145174
storage, err := dedupe.New(db)
146175
if err != nil {
147176
return nil, err
@@ -195,7 +224,30 @@ func (c *ReportingClient) RegisterExporter(exporter Exporter) {
195224

196225
// Close closes the issue tracker reporting client
197226
func (c *ReportingClient) Close() {
198-
c.dedupe.Close()
227+
// If we have stats for the trackers, print them
228+
if len(c.stats) > 0 {
229+
for _, tracker := range c.trackers {
230+
trackerName := tracker.Name()
231+
232+
if stats, ok := c.stats[trackerName]; ok {
233+
created := stats.Created.Load()
234+
if created == 0 {
235+
continue
236+
}
237+
var msgBuilder strings.Builder
238+
msgBuilder.WriteString(fmt.Sprintf("%d %s tickets created successfully", created, trackerName))
239+
failed := stats.Failed.Load()
240+
if failed > 0 {
241+
msgBuilder.WriteString(fmt.Sprintf(", %d failed", failed))
242+
}
243+
gologger.Info().Msgf(msgBuilder.String())
244+
}
245+
}
246+
}
247+
248+
if c.dedupe != nil {
249+
c.dedupe.Close()
250+
}
199251
for _, exporter := range c.exporters {
200252
exporter.Close()
201253
}
@@ -211,15 +263,37 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
211263
return nil
212264
}
213265

214-
unique, err := c.dedupe.Index(event)
266+
var err error
267+
unique := true
268+
if c.dedupe != nil {
269+
unique, err = c.dedupe.Index(event)
270+
}
215271
if unique {
272+
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
273+
216274
for _, tracker := range c.trackers {
217275
// process tracker specific allow/deny list
218276
if tracker.ShouldFilter(event) {
219277
continue
220278
}
221-
if trackerErr := tracker.CreateIssue(event); trackerErr != nil {
279+
trackerName := tracker.Name()
280+
stats, statsOk := c.stats[trackerName]
281+
282+
reportData, trackerErr := tracker.CreateIssue(event)
283+
if trackerErr != nil {
284+
if statsOk {
285+
_ = stats.Failed.Add(1)
286+
}
222287
err = multierr.Append(err, trackerErr)
288+
continue
289+
}
290+
if statsOk {
291+
_ = stats.Created.Add(1)
292+
}
293+
294+
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
295+
IssueID: reportData.IssueID,
296+
IssueURL: reportData.IssueURL,
223297
}
224298
}
225299
for _, exporter := range c.exporters {
@@ -231,10 +305,25 @@ func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
231305
return err
232306
}
233307

308+
// CloseIssue closes an issue in the tracker
309+
func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
310+
for _, tracker := range c.trackers {
311+
if tracker.ShouldFilter(event) {
312+
continue
313+
}
314+
if err := tracker.CloseIssue(event); err != nil {
315+
return err
316+
}
317+
}
318+
return nil
319+
}
320+
234321
func (c *ReportingClient) GetReportingOptions() *Options {
235322
return c.options
236323
}
237324

238325
func (c *ReportingClient) Clear() {
239-
c.dedupe.Clear()
326+
if c.dedupe != nil {
327+
c.dedupe.Clear()
328+
}
240329
}

pkg/reporting/trackers/filters/filters.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ import (
88
sliceutil "github.com/projectdiscovery/utils/slice"
99
)
1010

11+
// CreateIssueResponse is a response to creating an issue
12+
// in a tracker
13+
type CreateIssueResponse struct {
14+
IssueID string `json:"issue_id"`
15+
IssueURL string `json:"issue_url"`
16+
}
17+
1118
// Filter filters the received event and decides whether to perform
1219
// reporting for it or not.
1320
type Filter struct {

0 commit comments

Comments
 (0)