nuclei/pkg/reporting/reporting.go

352 lines
10 KiB
Go
Raw Normal View History

package reporting
import (
"fmt"
"os"
"strings"
"sync/atomic"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
"github.com/projectdiscovery/gologger"
"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-02-07 10:16:37 +01:00
"go.uber.org/multierr"
"gopkg.in/yaml.v2"
"errors"
"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"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/gitea"
"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"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/linear"
errorutil "github.com/projectdiscovery/utils/errors"
fileutil "github.com/projectdiscovery/utils/file"
)
var (
ErrReportingClientCreation = errors.New("could not create reporting client")
ErrExportClientCreation = errors.New("could not create exporting client")
)
// Tracker is an interface implemented by an issue tracker
type Tracker interface {
// Name returns the name of the tracker
Name() string
// CreateIssue creates an issue in the tracker
CreateIssue(event *output.ResultEvent) (*filters.CreateIssueResponse, error)
// CloseIssue closes an issue in the tracker
CloseIssue(event *output.ResultEvent) error
// ShouldFilter determines if the event should be filtered out
ShouldFilter(event *output.ResultEvent) bool
}
// 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
// Export exports an issue to an exporter
Export(event *output.ResultEvent) error
}
// ReportingClient is a client for nuclei issue tracking module
type ReportingClient struct {
trackers []Tracker
exporters []Exporter
options *Options
dedupe *dedupe.Storage
stats map[string]*IssueTrackerStats
}
type IssueTrackerStats struct {
Created atomic.Int32
Failed atomic.Int32
}
// New creates a new nuclei issue tracker reporting client
func New(options *Options, db string, doNotDedupe bool) (Client, error) {
client := &ReportingClient{options: options}
if options.GitHub != nil {
options.GitHub.HttpClient = options.HttpClient
options.GitHub.OmitRaw = options.OmitRaw
tracker, err := github.New(options.GitHub)
if err != nil {
2023-02-07 10:16:37 +01:00
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
}
client.trackers = append(client.trackers, tracker)
}
if options.GitLab != nil {
options.GitLab.HttpClient = options.HttpClient
options.GitLab.OmitRaw = options.OmitRaw
tracker, err := gitlab.New(options.GitLab)
if err != nil {
2023-02-07 10:16:37 +01:00
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
}
client.trackers = append(client.trackers, tracker)
}
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)
}
if options.Jira != nil {
options.Jira.HttpClient = options.HttpClient
options.Jira.OmitRaw = options.OmitRaw
tracker, err := jira.New(options.Jira)
if err != nil {
2023-02-07 10:16:37 +01:00
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
}
client.trackers = append(client.trackers, tracker)
}
if options.Linear != nil {
options.Linear.HttpClient = options.HttpClient
options.Linear.OmitRaw = options.OmitRaw
tracker, err := linear.New(options.Linear)
if err != nil {
return nil, errorutil.NewWithErr(err).Wrap(ErrReportingClientCreation)
}
client.trackers = append(client.trackers, tracker)
}
2021-09-19 16:26:47 +05:30
if options.MarkdownExporter != nil {
exporter, err := markdown.New(options.MarkdownExporter)
if err != nil {
2023-02-07 10:16:37 +01:00
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
}
client.exporters = append(client.exporters, exporter)
}
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)
}
JSON Export Handling Updates (#3466) * Switch -json to -jsonl * Add JSON output file * Update docs for EN and ID * Fix linting issue with error wrap * Add -j flag * Fix call for short flag * Correct typo "Ciper" to "Cipher" (#3468) * migrate dsl helper functions to dsl repo (#3461) * migrate dsl pkg code to dsl repo * fix lint error * upgrade dsl dependency * upgrade deps --------- Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io> * chore(deps): bump github.com/projectdiscovery/httpx in /v2 (#3469) Bumps [github.com/projectdiscovery/httpx](https://github.com/projectdiscovery/httpx) from 1.2.7 to 1.2.9. - [Release notes](https://github.com/projectdiscovery/httpx/releases) - [Changelog](https://github.com/projectdiscovery/httpx/blob/main/.goreleaser.yml) - [Commits](https://github.com/projectdiscovery/httpx/compare/v1.2.7...v1.2.9) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/httpx dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/weppos/publicsuffix-go in /v2 (#3472) Bumps [github.com/weppos/publicsuffix-go](https://github.com/weppos/publicsuffix-go) from 0.20.0 to 0.30.0. - [Release notes](https://github.com/weppos/publicsuffix-go/releases) - [Changelog](https://github.com/weppos/publicsuffix-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/weppos/publicsuffix-go/compare/v0.20.0...v0.30.0) --- updated-dependencies: - dependency-name: github.com/weppos/publicsuffix-go dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/wappalyzergo in /v2 (#3473) Bumps [github.com/projectdiscovery/wappalyzergo](https://github.com/projectdiscovery/wappalyzergo) from 0.0.81 to 0.0.88. - [Release notes](https://github.com/projectdiscovery/wappalyzergo/releases) - [Commits](https://github.com/projectdiscovery/wappalyzergo/compare/v0.0.81...v0.0.88) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/wappalyzergo dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/projectdiscovery/hmap in /v2 (#3470) Bumps [github.com/projectdiscovery/hmap](https://github.com/projectdiscovery/hmap) from 0.0.10 to 0.0.11. - [Release notes](https://github.com/projectdiscovery/hmap/releases) - [Commits](https://github.com/projectdiscovery/hmap/compare/v0.0.10...v0.0.11) --- updated-dependencies: - dependency-name: github.com/projectdiscovery/hmap dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * debug catalog path * use paths instead of filepath for aws path * deps update (#3477) * deps update * fixing gologger via callback * Moved `json-export` flag to the other exporters * Switch "json[-_]exporter to jsonexporter" --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Ramana Reddy <90540245+RamanaReddy0M@users.noreply.github.com> Co-authored-by: Sandeep Singh <sandeep@projectdiscovery.io> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Tarun Koyalwar <tarun@projectdiscovery.io> Co-authored-by: Mzack9999 <mzack9999@protonmail.com> Co-authored-by: shubhamrasal <shubhamdharmarasal@gmail.com>
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)
}
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)
}
if options.ElasticsearchExporter != nil {
options.ElasticsearchExporter.HttpClient = options.HttpClient
exporter, err := es.New(options.ElasticsearchExporter)
if err != nil {
2023-02-07 10:16:37 +01:00
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
}
client.exporters = append(client.exporters, exporter)
}
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)
}
client.exporters = append(client.exporters, exporter)
}
if options.MongoDBExporter != nil {
exporter, err := mongo.New(options.MongoDBExporter)
if err != nil {
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
}
client.exporters = append(client.exporters, exporter)
}
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)
if err != nil {
return nil, err
}
client.dedupe = storage
return client, nil
}
// CreateConfigIfNotExists creates report-config if it doesn't exist
func CreateConfigIfNotExists() error {
reportingConfig := config.DefaultConfig.GetReportingConfigFilePath()
if fileutil.FileExists(reportingConfig) {
return nil
}
values := stringslice.StringSlice{Value: []string{}}
options := &Options{
AllowList: &filters.Filter{Tags: values},
DenyList: &filters.Filter{Tags: values},
GitHub: &github.Options{},
GitLab: &gitlab.Options{},
Gitea: &gitea.Options{},
Jira: &jira.Options{},
Linear: &linear.Options{},
MarkdownExporter: &markdown.Options{},
SarifExporter: &sarif.Options{},
ElasticsearchExporter: &es.Options{},
SplunkExporter: &splunk.Options{},
JSONExporter: &json_exporter.Options{},
JSONLExporter: &jsonl.Options{},
MongoDBExporter: &mongo.Options{},
}
reportingFile, err := os.Create(reportingConfig)
if err != nil {
return errorutil.NewWithErr(err).Msgf("could not create config file")
}
defer reportingFile.Close()
err = yaml.NewEncoder(reportingFile).Encode(options)
return err
}
// RegisterTracker registers a custom tracker to the reporter
func (c *ReportingClient) RegisterTracker(tracker Tracker) {
c.trackers = append(c.trackers, tracker)
}
// RegisterExporter registers a custom exporter to the reporter
func (c *ReportingClient) RegisterExporter(exporter Exporter) {
c.exporters = append(c.exporters, exporter)
}
// Close closes the issue tracker reporting client
func (c *ReportingClient) Close() {
// 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("%v", msgBuilder.String())
}
}
}
if c.dedupe != nil {
c.dedupe.Close()
}
2021-06-05 18:01:08 +05:30
for _, exporter := range c.exporters {
exporter.Close()
}
}
// CreateIssue creates an issue in the tracker
func (c *ReportingClient) CreateIssue(event *output.ResultEvent) error {
// 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
}
var err error
unique := true
if c.dedupe != nil {
unique, err = c.dedupe.Index(event)
}
if unique {
event.IssueTrackers = make(map[string]output.IssueTrackerMetadata)
for _, tracker := range c.trackers {
// process tracker specific allow/deny list
if !tracker.ShouldFilter(event) {
continue
}
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)
continue
}
if statsOk {
_ = stats.Created.Add(1)
}
event.IssueTrackers[tracker.Name()] = output.IssueTrackerMetadata{
IssueID: reportData.IssueID,
IssueURL: reportData.IssueURL,
}
}
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)
}
}
}
return err
}
2021-02-08 01:43:51 +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
}
func (c *ReportingClient) GetReportingOptions() *Options {
return c.options
2021-02-08 01:43:51 +05:30
}
func (c *ReportingClient) Clear() {
if c.dedupe != nil {
c.dedupe.Clear()
}
}