2021-03-22 14:03:05 +05:30
|
|
|
package reporting
|
2021-02-02 12:10:47 +05:30
|
|
|
|
|
|
|
|
import (
|
2024-03-10 22:02:42 +05:30
|
|
|
"fmt"
|
2022-09-27 02:40:34 +05:30
|
|
|
"os"
|
2024-03-10 22:02:42 +05:30
|
|
|
"strings"
|
|
|
|
|
"sync/atomic"
|
2021-02-02 12:10:47 +05:30
|
|
|
|
2024-03-10 22:02:42 +05:30
|
|
|
"github.com/projectdiscovery/gologger"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
|
|
|
|
json_exporter "github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
|
2023-04-08 08:14:41 -04:00
|
|
|
|
2023-02-07 10:16:37 +01:00
|
|
|
"go.uber.org/multierr"
|
2022-09-27 02:40:34 +05:30
|
|
|
"gopkg.in/yaml.v2"
|
2021-07-19 21:04:08 +03:00
|
|
|
|
2023-02-07 09:45:49 +01:00
|
|
|
"errors"
|
|
|
|
|
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/dedupe"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/es"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/sarif"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/splunk"
|
2024-03-02 14:55:13 +02:00
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
|
2024-02-19 00:04:37 +02:00
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/gitea"
|
2023-10-17 17:44:13 +05:30
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/github"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/gitlab"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/jira"
|
2023-02-07 09:45:49 +01:00
|
|
|
errorutil "github.com/projectdiscovery/utils/errors"
|
2022-11-06 21:24:23 +01:00
|
|
|
fileutil "github.com/projectdiscovery/utils/file"
|
2021-02-02 12:10:47 +05:30
|
|
|
)
|
|
|
|
|
|
2023-02-07 09:45:49 +01:00
|
|
|
var (
|
|
|
|
|
ErrReportingClientCreation = errors.New("could not create reporting client")
|
|
|
|
|
ErrExportClientCreation = errors.New("could not create exporting client")
|
2022-05-12 05:10:14 -05:00
|
|
|
)
|
|
|
|
|
|
2021-02-02 12:10:47 +05:30
|
|
|
// Tracker is an interface implemented by an issue tracker
|
|
|
|
|
type Tracker interface {
|
2024-03-10 22:02:42 +05:30
|
|
|
// Name returns the name of the tracker
|
|
|
|
|
Name() string
|
2021-02-02 12:10:47 +05:30
|
|
|
// CreateIssue creates an issue in the tracker
|
2024-03-10 22:02:42 +05:30
|
|
|
CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error)
|
|
|
|
|
// CloseIssue closes an issue in the tracker
|
|
|
|
|
CloseIssue(event *output.ResultEvent) error
|
2024-03-02 14:55:13 +02:00
|
|
|
// ShouldFilter determines if the event should be filtered out
|
|
|
|
|
ShouldFilter(event *output.ResultEvent) bool
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
2021-03-22 14:03:05 +05:30
|
|
|
// Exporter is an interface implemented by an issue exporter
|
|
|
|
|
type Exporter interface {
|
2021-06-05 18:01:08 +05:30
|
|
|
// Close closes the exporter after operation
|
|
|
|
|
Close() error
|
2021-03-22 14:03:05 +05:30
|
|
|
// Export exports an issue to an exporter
|
|
|
|
|
Export(event *output.ResultEvent) error
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-07 09:45:49 +01:00
|
|
|
// ReportingClient is a client for nuclei issue tracking module
|
|
|
|
|
type ReportingClient struct {
|
2021-03-22 14:03:05 +05:30
|
|
|
trackers []Tracker
|
|
|
|
|
exporters []Exporter
|
|
|
|
|
options *Options
|
|
|
|
|
dedupe *dedupe.Storage
|
2024-03-10 22:02:42 +05:30
|
|
|
|
|
|
|
|
stats map[string]*IssueTrackerStats
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type IssueTrackerStats struct {
|
|
|
|
|
Created atomic.Int32
|
|
|
|
|
Failed atomic.Int32
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates a new nuclei issue tracker reporting client
|
2024-03-10 22:02:42 +05:30
|
|
|
func New(options *Options, db string, doNotDedupe bool) (Client, error) {
|
2023-02-07 09:45:49 +01:00
|
|
|
client := &ReportingClient{options: options}
|
2022-09-27 02:40:34 +05:30
|
|
|
|
2021-11-25 18:54:16 +02:00
|
|
|
if options.GitHub != nil {
|
2022-03-09 12:31:12 +01:00
|
|
|
options.GitHub.HttpClient = options.HttpClient
|
2024-01-27 01:36:25 +03:00
|
|
|
options.GitHub.OmitRaw = options.OmitRaw
|
2021-11-25 18:54:16 +02:00
|
|
|
tracker, err := github.New(options.GitHub)
|
2021-03-22 14:03:05 +05:30
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
client.trackers = append(client.trackers, tracker)
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2021-11-25 18:54:16 +02:00
|
|
|
if options.GitLab != nil {
|
2022-03-09 12:31:12 +01:00
|
|
|
options.GitLab.HttpClient = options.HttpClient
|
2024-01-27 01:36:25 +03:00
|
|
|
options.GitLab.OmitRaw = options.OmitRaw
|
2021-11-25 18:54:16 +02:00
|
|
|
tracker, err := gitlab.New(options.GitLab)
|
2021-03-22 14:03:05 +05:30
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
client.trackers = append(client.trackers, tracker)
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2024-02-19 00:04:37 +02:00
|
|
|
if options.Gitea != nil {
|
|
|
|
|
options.Gitea.HttpClient = options.HttpClient
|
|
|
|
|
options.Gitea.OmitRaw = options.OmitRaw
|
|
|
|
|
tracker, err := gitea.New(options.Gitea)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
|
|
|
|
|
}
|
|
|
|
|
client.trackers = append(client.trackers, tracker)
|
|
|
|
|
}
|
2021-02-02 12:10:47 +05:30
|
|
|
if options.Jira != nil {
|
2022-03-09 12:31:12 +01:00
|
|
|
options.Jira.HttpClient = options.HttpClient
|
2024-01-27 01:36:25 +03:00
|
|
|
options.Jira.OmitRaw = options.OmitRaw
|
2021-03-22 14:03:05 +05:30
|
|
|
tracker, err := jira.New(options.Jira)
|
|
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
client.trackers = append(client.trackers, tracker)
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2021-09-19 16:26:47 +05:30
|
|
|
if options.MarkdownExporter != nil {
|
|
|
|
|
exporter, err := markdown.New(options.MarkdownExporter)
|
2021-03-22 14:03:05 +05:30
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2021-06-05 18:01:08 +05:30
|
|
|
if options.SarifExporter != nil {
|
|
|
|
|
exporter, err := sarif.New(options.SarifExporter)
|
|
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
2021-06-05 18:01:08 +05:30
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
|
|
|
|
}
|
2023-03-31 05:59:29 -04:00
|
|
|
if options.JSONExporter != nil {
|
|
|
|
|
exporter, err := json_exporter.New(options.JSONExporter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
|
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
|
|
|
|
}
|
2023-04-08 08:14:41 -04:00
|
|
|
if options.JSONLExporter != nil {
|
|
|
|
|
exporter, err := jsonl.New(options.JSONLExporter)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
|
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
|
|
|
|
}
|
2021-08-20 07:27:19 -05:00
|
|
|
if options.ElasticsearchExporter != nil {
|
2022-03-09 12:31:12 +01:00
|
|
|
options.ElasticsearchExporter.HttpClient = options.HttpClient
|
2021-08-20 07:27:19 -05:00
|
|
|
exporter, err := es.New(options.ElasticsearchExporter)
|
|
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
2021-08-20 07:27:19 -05:00
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
|
|
|
|
}
|
2022-12-12 07:56:32 -08:00
|
|
|
if options.SplunkExporter != nil {
|
|
|
|
|
options.SplunkExporter.HttpClient = options.HttpClient
|
|
|
|
|
exporter, err := splunk.New(options.SplunkExporter)
|
|
|
|
|
if err != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
|
2022-12-12 07:56:32 -08:00
|
|
|
}
|
|
|
|
|
client.exporters = append(client.exporters, exporter)
|
|
|
|
|
}
|
2021-07-24 22:39:59 -07:00
|
|
|
|
2024-03-10 22:02:42 +05:30
|
|
|
if doNotDedupe {
|
|
|
|
|
return client, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client.stats = make(map[string]*IssueTrackerStats)
|
|
|
|
|
for _, tracker := range client.trackers {
|
|
|
|
|
trackerName := tracker.Name()
|
|
|
|
|
|
|
|
|
|
client.stats[trackerName] = &IssueTrackerStats{
|
|
|
|
|
Created: atomic.Int32{},
|
|
|
|
|
Failed: atomic.Int32{},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-07 23:41:33 +05:30
|
|
|
storage, err := dedupe.New(db)
|
2021-02-02 12:10:47 +05:30
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2021-03-22 14:03:05 +05:30
|
|
|
client.dedupe = storage
|
|
|
|
|
return client, nil
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
2022-09-27 02:40:34 +05:30
|
|
|
// CreateConfigIfNotExists creates report-config if it doesn't exists
|
|
|
|
|
func CreateConfigIfNotExists() error {
|
2023-04-19 21:58:48 +05:30
|
|
|
reportingConfig := config.DefaultConfig.GetReportingConfigFilePath()
|
2022-09-27 02:40:34 +05:30
|
|
|
|
|
|
|
|
if fileutil.FileExists(reportingConfig) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
values := stringslice.StringSlice{Value: []string{}}
|
|
|
|
|
|
|
|
|
|
options := &Options{
|
2024-03-02 14:55:13 +02:00
|
|
|
AllowList: &filters.Filter{Tags: values},
|
|
|
|
|
DenyList: &filters.Filter{Tags: values},
|
2022-09-27 02:40:34 +05:30
|
|
|
GitHub: &github.Options{},
|
|
|
|
|
GitLab: &gitlab.Options{},
|
2024-03-02 14:55:13 +02:00
|
|
|
Gitea: &gitea.Options{},
|
2022-09-27 02:40:34 +05:30
|
|
|
Jira: &jira.Options{},
|
|
|
|
|
MarkdownExporter: &markdown.Options{},
|
|
|
|
|
SarifExporter: &sarif.Options{},
|
|
|
|
|
ElasticsearchExporter: &es.Options{},
|
2022-12-12 07:56:32 -08:00
|
|
|
SplunkExporter: &splunk.Options{},
|
2023-04-08 08:14:41 -04:00
|
|
|
JSONExporter: &json_exporter.Options{},
|
|
|
|
|
JSONLExporter: &jsonl.Options{},
|
2022-09-27 02:40:34 +05:30
|
|
|
}
|
|
|
|
|
reportingFile, err := os.Create(reportingConfig)
|
|
|
|
|
if err != nil {
|
2023-02-07 09:45:49 +01:00
|
|
|
return errorutil.NewWithErr(err).Msgf("could not create config file")
|
2022-09-27 02:40:34 +05:30
|
|
|
}
|
|
|
|
|
defer reportingFile.Close()
|
|
|
|
|
|
|
|
|
|
err = yaml.NewEncoder(reportingFile).Encode(options)
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-24 22:39:59 -07:00
|
|
|
// RegisterTracker registers a custom tracker to the reporter
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) RegisterTracker(tracker Tracker) {
|
2021-07-24 22:39:59 -07:00
|
|
|
c.trackers = append(c.trackers, tracker)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RegisterExporter registers a custom exporter to the reporter
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) RegisterExporter(exporter Exporter) {
|
2021-07-24 22:39:59 -07:00
|
|
|
c.exporters = append(c.exporters, exporter)
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-02 12:10:47 +05:30
|
|
|
// Close closes the issue tracker reporting client
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) Close() {
|
2024-03-10 22:02:42 +05:30
|
|
|
// If we have stats for the trackers, print them
|
|
|
|
|
if len(c.stats) > 0 {
|
|
|
|
|
for _, tracker := range c.trackers {
|
|
|
|
|
trackerName := tracker.Name()
|
|
|
|
|
|
|
|
|
|
if stats, ok := c.stats[trackerName]; ok {
|
|
|
|
|
created := stats.Created.Load()
|
|
|
|
|
if created == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
var msgBuilder strings.Builder
|
|
|
|
|
msgBuilder.WriteString(fmt.Sprintf("%d %s tickets created successfully", created, trackerName))
|
|
|
|
|
failed := stats.Failed.Load()
|
|
|
|
|
if failed > 0 {
|
|
|
|
|
msgBuilder.WriteString(fmt.Sprintf(", %d failed", failed))
|
|
|
|
|
}
|
|
|
|
|
gologger.Info().Msgf(msgBuilder.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.dedupe != nil {
|
|
|
|
|
c.dedupe.Close()
|
|
|
|
|
}
|
2021-06-05 18:01:08 +05:30
|
|
|
for _, exporter := range c.exporters {
|
|
|
|
|
exporter.Close()
|
|
|
|
|
}
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CreateIssue creates an issue in the tracker
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
|
2024-03-02 14:55:13 +02:00
|
|
|
// process global allow/deny list
|
2021-02-08 01:43:51 +05:30
|
|
|
if c.options.AllowList != nil && !c.options.AllowList.GetMatch(event) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if c.options.DenyList != nil && c.options.DenyList.GetMatch(event) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-10 22:02:42 +05:30
|
|
|
var err error
|
|
|
|
|
unique := true
|
|
|
|
|
if c.dedupe != nil {
|
|
|
|
|
unique, err = c.dedupe.Index(event)
|
|
|
|
|
}
|
2021-03-22 14:03:05 +05:30
|
|
|
if unique {
|
2024-03-10 22:02:42 +05:30
|
|
|
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
|
|
|
|
|
|
2021-03-22 14:03:05 +05:30
|
|
|
for _, tracker := range c.trackers {
|
2024-03-02 14:55:13 +02:00
|
|
|
// process tracker specific allow/deny list
|
|
|
|
|
if tracker.ShouldFilter(event) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2024-03-10 22:02:42 +05:30
|
|
|
trackerName := tracker.Name()
|
|
|
|
|
stats, statsOk := c.stats[trackerName]
|
|
|
|
|
|
|
|
|
|
reportData, trackerErr := tracker.CreateIssue(event)
|
|
|
|
|
if trackerErr != nil {
|
|
|
|
|
if statsOk {
|
|
|
|
|
_ = stats.Failed.Add(1)
|
|
|
|
|
}
|
2023-02-07 10:16:37 +01:00
|
|
|
err = multierr.Append(err, trackerErr)
|
2024-03-10 22:02:42 +05:30
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if statsOk {
|
|
|
|
|
_ = stats.Created.Add(1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
|
|
|
|
|
IssueID: reportData.IssueID,
|
|
|
|
|
IssueURL: reportData.IssueURL,
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, exporter := range c.exporters {
|
|
|
|
|
if exportErr := exporter.Export(event); exportErr != nil {
|
2023-02-07 10:16:37 +01:00
|
|
|
err = multierr.Append(err, exportErr)
|
2021-03-22 14:03:05 +05:30
|
|
|
}
|
|
|
|
|
}
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2021-03-22 14:03:05 +05:30
|
|
|
return err
|
2021-02-02 12:10:47 +05:30
|
|
|
}
|
2021-02-08 01:43:51 +05:30
|
|
|
|
2024-03-10 22:02:42 +05:30
|
|
|
// CloseIssue closes an issue in the tracker
|
|
|
|
|
func (c *ReportingClient) CloseIssue(event *output.ResultEvent) error {
|
|
|
|
|
for _, tracker := range c.trackers {
|
|
|
|
|
if tracker.ShouldFilter(event) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if err := tracker.CloseIssue(event); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) GetReportingOptions() *Options {
|
|
|
|
|
return c.options
|
2021-02-08 01:43:51 +05:30
|
|
|
}
|
2023-01-10 22:49:01 +05:30
|
|
|
|
2023-02-07 09:45:49 +01:00
|
|
|
func (c *ReportingClient) Clear() {
|
2024-03-10 22:02:42 +05:30
|
|
|
if c.dedupe != nil {
|
|
|
|
|
c.dedupe.Clear()
|
|
|
|
|
}
|
2023-01-10 22:49:01 +05:30
|
|
|
}
|