Skip to content

Commit 3f0de96

Browse files
authored
MongoDB Reporting (#5688)
* Initial setup of Mongo reporting * Fix slice pop logic * Switch to config-file logic * Parse database name from connection string * Switch to url.Parse for connection string parsing * Address return/logging feedback
1 parent 1cd42c4 commit 3f0de96

File tree

6 files changed

+196
-0
lines changed

6 files changed

+196
-0
lines changed

cmd/nuclei/issue-tracker-config.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,14 @@
162162
# duplicate-issue-check: false
163163
# # open-state-id is the ID of the open state in Linear
164164
# open-state-id: ""
165+
#mongodb:
166+
# # the connection string to the MongoDB database
167+
# # (e.g., mongodb://root:example@localhost:27017/nuclei?ssl=false&authSource=admin)
168+
# connection-string: ""
169+
# # the name of the collection to store the issues
170+
# collection-name: ""
171+
# # excludes the Request and Response from the results (helps with filesize)
172+
# omit-raw: false
173+
# # determines the number of results to be kept in memory before writing it to the database or 0 to
174+
# # persist all in memory and write all results at the end (default)
175+
# batch-size: 0

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ require (
105105
github.com/stretchr/testify v1.9.0
106106
github.com/tarunKoyalwar/goleak v0.0.0-20240429141123-0efa90dbdcf9
107107
github.com/zmap/zgrab2 v0.1.8-0.20230806160807-97ba87c0e706
108+
go.mongodb.org/mongo-driver v1.17.0
108109
golang.org/x/term v0.24.0
109110
gopkg.in/yaml.v3 v3.0.1
110111
moul.io/http2curl v1.0.0
@@ -195,6 +196,7 @@ require (
195196
github.com/mitchellh/mapstructure v1.5.0 // indirect
196197
github.com/moby/term v0.5.0 // indirect
197198
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
199+
github.com/montanaflynn/stats v0.7.1 // indirect
198200
github.com/muesli/reflow v0.3.0 // indirect
199201
github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect
200202
github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -228,9 +230,13 @@ require (
228230
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
229231
github.com/ugorji/go/codec v1.2.11 // indirect
230232
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
233+
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
234+
github.com/xdg-go/scram v1.1.2 // indirect
235+
github.com/xdg-go/stringprep v1.0.4 // indirect
231236
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
232237
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
233238
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
239+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
234240
github.com/ysmood/fetchup v0.2.3 // indirect
235241
github.com/ysmood/got v0.34.1 // indirect
236242
github.com/yuin/goldmark v1.7.4 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
755755
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
756756
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
757757
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
758+
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
759+
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
758760
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
759761
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
760762
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -1085,6 +1087,12 @@ github.com/xanzy/go-gitlab v0.107.0 h1:P2CT9Uy9yN9lJo3FLxpMZ4xj6uWcpnigXsjvqJ6nd
10851087
github.com/xanzy/go-gitlab v0.107.0/go.mod h1:wKNKh3GkYDMOsGmnfuX+ITCmDuSDWFO0G+C4AygL9RY=
10861088
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
10871089
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
1090+
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
1091+
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
1092+
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
1093+
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
1094+
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
1095+
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
10881096
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
10891097
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
10901098
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
@@ -1098,6 +1106,8 @@ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMx
10981106
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
10991107
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
11001108
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
1109+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
1110+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
11011111
github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ=
11021112
github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns=
11031113
github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ=
@@ -1150,6 +1160,8 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
11501160
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
11511161
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
11521162
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
1163+
go.mongodb.org/mongo-driver v1.17.0 h1:Hp4q2MCjvY19ViwimTs00wHi7G4yzxh4/2+nTx8r40k=
1164+
go.mongodb.org/mongo-driver v1.17.0/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
11531165
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
11541166
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
11551167
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package mongo
2+
3+
import (
4+
"context"
5+
"github.com/pkg/errors"
6+
"github.com/projectdiscovery/gologger"
7+
"github.com/projectdiscovery/nuclei/v3/pkg/output"
8+
"go.mongodb.org/mongo-driver/mongo"
9+
"net/url"
10+
"os"
11+
"strings"
12+
"sync"
13+
14+
mongooptions "go.mongodb.org/mongo-driver/mongo/options"
15+
)
16+
17+
type Exporter struct {
18+
options *Options
19+
mutex *sync.Mutex
20+
rows []output.ResultEvent
21+
collection *mongo.Collection
22+
connection *mongo.Client
23+
}
24+
25+
// Options contains the configuration options for MongoDB exporter client
26+
type Options struct {
27+
// ConnectionString is the connection string to the MongoDB database
28+
ConnectionString string `yaml:"connection-string"`
29+
// CollectionName is the name of the MongoDB collection in which to store the results
30+
CollectionName string `yaml:"collection-name"`
31+
// OmitRaw excludes the Request and Response from the results (helps with filesize)
32+
OmitRaw bool `yaml:"omit-raw"`
33+
// BatchSize determines the number of results to be kept in memory before writing it to the database or 0 to
34+
// persist all in memory and write all results at the end (default)
35+
BatchSize int `yaml:"batch-size"`
36+
}
37+
38+
// New creates a new MongoDB exporter integration client based on options.
39+
func New(options *Options) (*Exporter, error) {
40+
exporter := &Exporter{
41+
mutex: &sync.Mutex{},
42+
options: options,
43+
rows: []output.ResultEvent{},
44+
}
45+
46+
// If the environment variable for the connection string is set, then use that instead. This allows for easier
47+
// management of sensitive items such as credentials
48+
envConnectionString := os.Getenv("MONGO_CONNECTION_STRING")
49+
if envConnectionString != "" {
50+
options.ConnectionString = envConnectionString
51+
gologger.Info().Msgf("Using connection string from environment variable MONGO_CONNECTION_STRING")
52+
}
53+
54+
// Create the connection to the database
55+
clientOptions := mongooptions.Client().ApplyURI(options.ConnectionString)
56+
57+
// Create a new client and connect to the MongoDB server
58+
client, err := mongo.Connect(context.TODO(), clientOptions)
59+
if err != nil {
60+
gologger.Error().Msgf("Error creating MongoDB client: %s", err)
61+
return nil, err
62+
}
63+
64+
// Ensure the connection is valid
65+
err = client.Ping(context.Background(), nil)
66+
if err != nil {
67+
gologger.Error().Msgf("Error connecting to MongoDB: %s", err)
68+
return nil, err
69+
}
70+
71+
// Get the database from the connection string to set the database and collection
72+
parsed, err := url.Parse(options.ConnectionString)
73+
if err != nil {
74+
gologger.Error().Msgf("Error parsing connection string: %s", options.ConnectionString)
75+
return nil, err
76+
}
77+
78+
databaseName := strings.TrimPrefix(parsed.Path, "/")
79+
80+
if databaseName == "" {
81+
return nil, errors.New("error getting database name from connection string")
82+
}
83+
84+
exporter.connection = client
85+
exporter.collection = client.Database(databaseName).Collection(options.CollectionName)
86+
87+
return exporter, nil
88+
}
89+
90+
// Export writes a result document to the configured MongoDB collection
91+
// in the database configured by the connection string
92+
func (exporter *Exporter) Export(event *output.ResultEvent) error {
93+
exporter.mutex.Lock()
94+
defer exporter.mutex.Unlock()
95+
96+
if exporter.options.OmitRaw {
97+
event.Request = ""
98+
event.Response = ""
99+
}
100+
101+
// Add the row to the queue to be processed
102+
exporter.rows = append(exporter.rows, *event)
103+
104+
// If the batch size is greater than 0 and the number of rows has reached the batch, flush it to the database
105+
if exporter.options.BatchSize > 0 && len(exporter.rows) >= exporter.options.BatchSize {
106+
err := exporter.WriteRows()
107+
if err != nil {
108+
// The error is already logged, return it to bubble up to the caller
109+
return err
110+
}
111+
}
112+
113+
return nil
114+
}
115+
116+
// WriteRows writes all rows from the rows list to the MongoDB collection and removes them from the list
117+
func (exporter *Exporter) WriteRows() error {
118+
// Loop through the rows and write them, removing them as they're entered
119+
for len(exporter.rows) > 0 {
120+
data := exporter.rows[0]
121+
122+
// Write the data to the database
123+
_, err := exporter.collection.InsertOne(context.TODO(), data)
124+
if err != nil {
125+
gologger.Fatal().Msgf("Error inserting record into MongoDB collection: %s", err)
126+
return err
127+
}
128+
129+
// Remove the item from the list
130+
exporter.rows = exporter.rows[1:]
131+
}
132+
133+
return nil
134+
}
135+
136+
func (exporter *Exporter) Close() error {
137+
exporter.mutex.Lock()
138+
defer exporter.mutex.Unlock()
139+
140+
// Write all pending rows
141+
err := exporter.WriteRows()
142+
if err != nil {
143+
// The error is already logged, return it to bubble up to the caller
144+
return err
145+
}
146+
147+
// Close the database connection
148+
err = exporter.connection.Disconnect(context.TODO())
149+
if err != nil {
150+
gologger.Error().Msgf("Error disconnecting from MongoDB: %s", err)
151+
return err
152+
}
153+
154+
return nil
155+
}

pkg/reporting/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonexporter"
66
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/jsonl"
77
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown"
8+
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
89
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/sarif"
910
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/splunk"
1011
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/trackers/filters"
@@ -44,6 +45,8 @@ type Options struct {
4445
JSONExporter *jsonexporter.Options `yaml:"json"`
4546
// JSONLExporter contains configuration options for JSONL Exporter Module
4647
JSONLExporter *jsonl.Options `yaml:"jsonl"`
48+
// MongoDBExporter containers the configuration options for the MongoDB Exporter Module
49+
MongoDBExporter *mongo.Options `yaml:"mongodb"`
4750

4851
HttpClient *retryablehttp.Client `yaml:"-"`
4952
OmitRaw bool `yaml:"-"`

pkg/reporting/reporting.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package reporting
22

33
import (
44
"fmt"
5+
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/mongo"
56
"os"
67
"strings"
78
"sync/atomic"
@@ -166,6 +167,13 @@ func New(options *Options, db string, doNotDedupe bool) (Client, error) {
166167
}
167168
client.exporters = append(client.exporters, exporter)
168169
}
170+
if options.MongoDBExporter != nil {
171+
exporter, err := mongo.New(options.MongoDBExporter)
172+
if err != nil {
173+
return nil, errorutil.NewWithErr(err).Wrap(ErrExportClientCreation)
174+
}
175+
client.exporters = append(client.exporters, exporter)
176+
}
169177

170178
if doNotDedupe {
171179
return client, nil
@@ -212,6 +220,7 @@ func CreateConfigIfNotExists() error {
212220
SplunkExporter: &splunk.Options{},
213221
JSONExporter: &json_exporter.Options{},
214222
JSONLExporter: &jsonl.Options{},
223+
MongoDBExporter: &mongo.Options{},
215224
}
216225
reportingFile, err := os.Create(reportingConfig)
217226
if err != nil {

0 commit comments

Comments
 (0)