Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/output/output.go
2070 views
1
package output
2
3
import (
4
"encoding/base64"
5
"fmt"
6
"io"
7
"log/slog"
8
"maps"
9
"os"
10
"path/filepath"
11
"regexp"
12
"strings"
13
"sync"
14
"sync/atomic"
15
"time"
16
17
"github.com/pkg/errors"
18
"go.uber.org/multierr"
19
20
jsoniter "github.com/json-iterator/go"
21
"github.com/logrusorgru/aurora"
22
23
"github.com/projectdiscovery/gologger"
24
"github.com/projectdiscovery/interactsh/pkg/server"
25
"github.com/projectdiscovery/nuclei/v3/internal/colorizer"
26
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
27
"github.com/projectdiscovery/nuclei/v3/pkg/model"
28
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
29
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
30
protocolUtils "github.com/projectdiscovery/nuclei/v3/pkg/protocols/utils"
31
"github.com/projectdiscovery/nuclei/v3/pkg/types"
32
"github.com/projectdiscovery/nuclei/v3/pkg/types/nucleierr"
33
"github.com/projectdiscovery/nuclei/v3/pkg/utils"
34
"github.com/projectdiscovery/utils/errkit"
35
fileutil "github.com/projectdiscovery/utils/file"
36
osutils "github.com/projectdiscovery/utils/os"
37
unitutils "github.com/projectdiscovery/utils/unit"
38
urlutil "github.com/projectdiscovery/utils/url"
39
)
40
41
// Writer is an interface which writes output to somewhere for nuclei events.
42
type Writer interface {
43
// Close closes the output writer interface
44
Close()
45
// Colorizer returns the colorizer instance for writer
46
Colorizer() aurora.Aurora
47
// Write writes the event to file and/or screen.
48
Write(*ResultEvent) error
49
// WriteFailure writes the optional failure event for template to file and/or screen.
50
WriteFailure(*InternalWrappedEvent) error
51
// Request logs a request in the trace log
52
Request(templateID, url, requestType string, err error)
53
// RequestStatsLog logs a request stats log
54
RequestStatsLog(statusCode, response string)
55
// WriteStoreDebugData writes the request/response debug data to file
56
WriteStoreDebugData(host, templateID, eventType string, data string)
57
// ResultCount returns the total number of results written
58
ResultCount() int
59
}
60
61
// StandardWriter is a writer writing output to file and screen for results.
62
type StandardWriter struct {
63
json bool
64
jsonReqResp bool
65
timestamp bool
66
noMetadata bool
67
matcherStatus bool
68
mutex *sync.Mutex
69
aurora aurora.Aurora
70
outputFile io.WriteCloser
71
traceFile io.WriteCloser
72
errorFile io.WriteCloser
73
severityColors func(severity.Severity) string
74
storeResponse bool
75
storeResponseDir string
76
omitTemplate bool
77
DisableStdout bool
78
AddNewLinesOutputFile bool // by default this is only done for stdout
79
KeysToRedact []string
80
81
// JSONLogRequestHook is a hook that can be used to log request/response
82
// when using custom server code with output
83
JSONLogRequestHook func(*JSONLogRequest)
84
85
resultCount atomic.Int32
86
}
87
88
var _ Writer = &StandardWriter{}
89
90
var decolorizerRegex = regexp.MustCompile(`\x1B\[[0-9;]*[a-zA-Z]`)
91
92
// InternalEvent is an internal output generation structure for nuclei.
93
type InternalEvent map[string]interface{}
94
95
func (ie InternalEvent) Set(k string, v interface{}) {
96
ie[k] = v
97
}
98
99
// InternalWrappedEvent is a wrapped event with operators result added to it.
100
type InternalWrappedEvent struct {
101
// Mutex is internal field which is implicitly used
102
// to synchronize callback(event) and interactsh polling updates
103
// Refer protocols/http.Request.ExecuteWithResults for more details
104
sync.RWMutex
105
106
InternalEvent InternalEvent
107
Results []*ResultEvent
108
OperatorsResult *operators.Result
109
UsesInteractsh bool
110
// Only applicable if interactsh is used
111
// This is used to avoid duplicate successful interactsh events
112
InteractshMatched atomic.Bool
113
}
114
115
func (iwe *InternalWrappedEvent) CloneShallow() *InternalWrappedEvent {
116
return &InternalWrappedEvent{
117
InternalEvent: maps.Clone(iwe.InternalEvent),
118
Results: nil,
119
OperatorsResult: nil,
120
UsesInteractsh: iwe.UsesInteractsh,
121
}
122
}
123
124
func (iwe *InternalWrappedEvent) HasOperatorResult() bool {
125
iwe.RLock()
126
defer iwe.RUnlock()
127
128
return iwe.OperatorsResult != nil
129
}
130
131
func (iwe *InternalWrappedEvent) HasResults() bool {
132
iwe.RLock()
133
defer iwe.RUnlock()
134
135
return len(iwe.Results) > 0
136
}
137
138
func (iwe *InternalWrappedEvent) SetOperatorResult(operatorResult *operators.Result) {
139
iwe.Lock()
140
defer iwe.Unlock()
141
142
iwe.OperatorsResult = operatorResult
143
}
144
145
// ResultEvent is a wrapped result event for a single nuclei output.
146
type ResultEvent struct {
147
// Template is the relative filename for the template
148
Template string `json:"template,omitempty"`
149
// TemplateURL is the URL of the template for the result inside the nuclei
150
// templates repository if it belongs to the repository.
151
TemplateURL string `json:"template-url,omitempty"`
152
// TemplateID is the ID of the template for the result.
153
TemplateID string `json:"template-id"`
154
// TemplatePath is the path of template
155
TemplatePath string `json:"template-path,omitempty"`
156
// TemplateEncoded is the base64 encoded template
157
TemplateEncoded string `json:"template-encoded,omitempty"`
158
// Info contains information block of the template for the result.
159
Info model.Info `json:"info,inline"`
160
// MatcherName is the name of the matcher matched if any.
161
MatcherName string `json:"matcher-name,omitempty"`
162
// ExtractorName is the name of the extractor matched if any.
163
ExtractorName string `json:"extractor-name,omitempty"`
164
// Type is the type of the result event.
165
Type string `json:"type"`
166
// Host is the host input on which match was found.
167
Host string `json:"host,omitempty"`
168
// Port is port of the host input on which match was found (if applicable).
169
Port string `json:"port,omitempty"`
170
// Scheme is the scheme of the host input on which match was found (if applicable).
171
Scheme string `json:"scheme,omitempty"`
172
// URL is the Base URL of the host input on which match was found (if applicable).
173
URL string `json:"url,omitempty"`
174
// Path is the path input on which match was found.
175
Path string `json:"path,omitempty"`
176
// Matched contains the matched input in its transformed form.
177
Matched string `json:"matched-at,omitempty"`
178
// ExtractedResults contains the extraction result from the inputs.
179
ExtractedResults []string `json:"extracted-results,omitempty"`
180
// Request is the optional, dumped request for the match.
181
Request string `json:"request,omitempty"`
182
// Response is the optional, dumped response for the match.
183
Response string `json:"response,omitempty"`
184
// Metadata contains any optional metadata for the event
185
Metadata map[string]interface{} `json:"meta,omitempty"`
186
// IP is the IP address for the found result event.
187
IP string `json:"ip,omitempty"`
188
// Timestamp is the time the result was found at.
189
Timestamp time.Time `json:"timestamp"`
190
// Interaction is the full details of interactsh interaction.
191
Interaction *server.Interaction `json:"interaction,omitempty"`
192
// CURLCommand is an optional curl command to reproduce the request
193
// Only applicable if the report is for HTTP.
194
CURLCommand string `json:"curl-command,omitempty"`
195
// MatcherStatus is the status of the match
196
MatcherStatus bool `json:"matcher-status"`
197
// Lines is the line count for the specified match
198
Lines []int `json:"matched-line,omitempty"`
199
// GlobalMatchers identifies whether the matches was detected in the response
200
// of another template's result event
201
GlobalMatchers bool `json:"global-matchers,omitempty"`
202
203
// IssueTrackers is the metadata for issue trackers
204
IssueTrackers map[string]IssueTrackerMetadata `json:"issue_trackers,omitempty"`
205
// ReqURLPattern when enabled contains base URL pattern that was used to generate the request
206
// must be enabled by setting protocols.ExecuterOptions.ExportReqURLPattern to true
207
ReqURLPattern string `json:"req_url_pattern,omitempty"`
208
209
// Fields related to HTTP Fuzzing functionality of nuclei.
210
// The output contains additional fields when the result is
211
// for a fuzzing template.
212
IsFuzzingResult bool `json:"is_fuzzing_result,omitempty"`
213
FuzzingMethod string `json:"fuzzing_method,omitempty"`
214
FuzzingParameter string `json:"fuzzing_parameter,omitempty"`
215
FuzzingPosition string `json:"fuzzing_position,omitempty"`
216
AnalyzerDetails string `json:"analyzer_details,omitempty"`
217
218
FileToIndexPosition map[string]int `json:"-"`
219
TemplateVerifier string `json:"-"`
220
Error string `json:"error,omitempty"`
221
}
222
223
type IssueTrackerMetadata struct {
224
// IssueID is the ID of the issue created
225
IssueID string `json:"id,omitempty"`
226
// IssueURL is the URL of the issue created
227
IssueURL string `json:"url,omitempty"`
228
}
229
230
// NewStandardWriter creates a new output writer based on user configurations
231
func NewStandardWriter(options *types.Options) (*StandardWriter, error) {
232
resumeBool := options.Resume != ""
233
234
auroraColorizer := aurora.NewAurora(!options.NoColor)
235
236
var outputFile io.WriteCloser
237
if options.Output != "" {
238
output, err := newFileOutputWriter(options.Output, resumeBool)
239
if err != nil {
240
return nil, errors.Wrap(err, "could not create output file")
241
}
242
outputFile = output
243
}
244
var traceOutput io.WriteCloser
245
if options.TraceLogFile != "" {
246
output, err := newFileOutputWriter(options.TraceLogFile, resumeBool)
247
if err != nil {
248
return nil, errors.Wrap(err, "could not create output file")
249
}
250
traceOutput = output
251
}
252
var errorOutput io.WriteCloser
253
if options.ErrorLogFile != "" {
254
output, err := newFileOutputWriter(options.ErrorLogFile, resumeBool)
255
if err != nil {
256
return nil, errors.Wrap(err, "could not create error file")
257
}
258
errorOutput = output
259
}
260
// Try to create output folder if it doesn't exist
261
if options.StoreResponse && !fileutil.FolderExists(options.StoreResponseDir) {
262
if err := fileutil.CreateFolder(options.StoreResponseDir); err != nil {
263
gologger.Fatal().Msgf("Could not create output directory '%s': %s\n", options.StoreResponseDir, err)
264
}
265
}
266
267
writer := &StandardWriter{
268
json: options.JSONL,
269
jsonReqResp: !options.OmitRawRequests,
270
noMetadata: options.NoMeta,
271
matcherStatus: options.MatcherStatus,
272
timestamp: options.Timestamp,
273
aurora: auroraColorizer,
274
mutex: &sync.Mutex{},
275
outputFile: outputFile,
276
traceFile: traceOutput,
277
errorFile: errorOutput,
278
severityColors: colorizer.New(auroraColorizer),
279
storeResponse: options.StoreResponse,
280
storeResponseDir: options.StoreResponseDir,
281
omitTemplate: options.OmitTemplate,
282
KeysToRedact: options.Redact,
283
}
284
285
if v := os.Getenv("DISABLE_STDOUT"); v == "true" || v == "1" {
286
writer.DisableStdout = true
287
}
288
289
return writer, nil
290
}
291
292
func (w *StandardWriter) ResultCount() int {
293
return int(w.resultCount.Load())
294
}
295
296
// Write writes the event to file and/or screen.
297
func (w *StandardWriter) Write(event *ResultEvent) error {
298
if event.Error != "" && !w.matcherStatus {
299
return nil
300
}
301
302
// Enrich the result event with extra metadata on the template-path and url.
303
if event.TemplatePath != "" {
304
event.Template, event.TemplateURL = utils.TemplatePathURL(types.ToString(event.TemplatePath), types.ToString(event.TemplateID), event.TemplateVerifier)
305
}
306
307
if len(w.KeysToRedact) > 0 {
308
event.Request = redactKeys(event.Request, w.KeysToRedact)
309
event.Response = redactKeys(event.Response, w.KeysToRedact)
310
event.CURLCommand = redactKeys(event.CURLCommand, w.KeysToRedact)
311
event.Matched = redactKeys(event.Matched, w.KeysToRedact)
312
}
313
314
event.Timestamp = time.Now()
315
316
var data []byte
317
var err error
318
319
if w.json {
320
data, err = w.formatJSON(event)
321
} else {
322
data = w.formatScreen(event)
323
}
324
if err != nil {
325
return errors.Wrap(err, "could not format output")
326
}
327
if len(data) == 0 {
328
return nil
329
}
330
w.mutex.Lock()
331
defer w.mutex.Unlock()
332
333
if !w.DisableStdout {
334
_, _ = os.Stdout.Write(data)
335
_, _ = os.Stdout.Write([]byte("\n"))
336
}
337
338
if w.outputFile != nil {
339
if !w.json {
340
data = decolorizerRegex.ReplaceAll(data, []byte(""))
341
}
342
if _, writeErr := w.outputFile.Write(data); writeErr != nil {
343
return errors.Wrap(writeErr, "could not write to output")
344
}
345
if w.AddNewLinesOutputFile && w.json {
346
_, _ = w.outputFile.Write([]byte("\n"))
347
}
348
}
349
w.resultCount.Add(1)
350
return nil
351
}
352
353
func redactKeys(data string, keysToRedact []string) string {
354
for _, key := range keysToRedact {
355
keyPattern := regexp.MustCompile(fmt.Sprintf(`(?i)(%s\s*[:=]\s*["']?)[^"'\r\n&]+(["'\r\n]?)`, regexp.QuoteMeta(key)))
356
data = keyPattern.ReplaceAllString(data, `$1***$2`)
357
}
358
return data
359
}
360
361
// JSONLogRequest is a trace/error log request written to file
362
type JSONLogRequest struct {
363
Template string `json:"template"`
364
Type string `json:"type"`
365
Input string `json:"input"`
366
Timestamp *time.Time `json:"timestamp,omitempty"`
367
Address string `json:"address"`
368
Error string `json:"error"`
369
Kind string `json:"kind,omitempty"`
370
Attrs interface{} `json:"attrs,omitempty"`
371
}
372
373
// Request writes a log the requests trace log
374
func (w *StandardWriter) Request(templatePath, input, requestType string, requestErr error) {
375
if w.traceFile == nil && w.errorFile == nil && w.JSONLogRequestHook == nil {
376
return
377
}
378
379
request := getJSONLogRequestFromError(templatePath, input, requestType, requestErr)
380
if w.timestamp {
381
ts := time.Now()
382
request.Timestamp = &ts
383
}
384
data, err := jsoniter.Marshal(request)
385
if err != nil {
386
return
387
}
388
389
if w.JSONLogRequestHook != nil {
390
w.JSONLogRequestHook(request)
391
}
392
393
if w.traceFile != nil {
394
_, _ = w.traceFile.Write(data)
395
}
396
397
if requestErr != nil && w.errorFile != nil {
398
_, _ = w.errorFile.Write(data)
399
}
400
}
401
402
func getJSONLogRequestFromError(templatePath, input, requestType string, requestErr error) *JSONLogRequest {
403
request := &JSONLogRequest{
404
Template: templatePath,
405
Input: input,
406
Type: requestType,
407
}
408
409
parsed, _ := urlutil.ParseAbsoluteURL(input, false)
410
if parsed != nil {
411
request.Address = parsed.Hostname()
412
port := parsed.Port()
413
if port == "" {
414
switch parsed.Scheme {
415
case urlutil.HTTP:
416
port = "80"
417
case urlutil.HTTPS:
418
port = "443"
419
}
420
}
421
request.Address += ":" + port
422
}
423
errX := errkit.FromError(requestErr)
424
if errX == nil {
425
request.Error = "none"
426
} else {
427
request.Kind = errkit.ErrKindUnknown.String()
428
var cause error
429
if len(errX.Errors()) > 1 {
430
cause = errX.Errors()[0]
431
}
432
if cause == nil {
433
cause = errX
434
}
435
cause = tryParseCause(cause)
436
request.Error = cause.Error()
437
request.Kind = errkit.GetErrorKind(requestErr, nucleierr.ErrTemplateLogic).String()
438
if len(errX.Attrs()) > 0 {
439
request.Attrs = slog.GroupValue(errX.Attrs()...)
440
}
441
}
442
// check if address slog attr is avaiable in error if set use it
443
if val := errkit.GetAttrValue(requestErr, "address"); val.Any() != nil {
444
request.Address = val.String()
445
}
446
return request
447
}
448
449
// Colorizer returns the colorizer instance for writer
450
func (w *StandardWriter) Colorizer() aurora.Aurora {
451
return w.aurora
452
}
453
454
// Close closes the output writing interface
455
func (w *StandardWriter) Close() {
456
if w.outputFile != nil {
457
_ = w.outputFile.Close()
458
}
459
if w.traceFile != nil {
460
_ = w.traceFile.Close()
461
}
462
if w.errorFile != nil {
463
_ = w.errorFile.Close()
464
}
465
}
466
467
// WriteFailure writes the failure event for template to file and/or screen.
468
func (w *StandardWriter) WriteFailure(wrappedEvent *InternalWrappedEvent) error {
469
if !w.matcherStatus {
470
return nil
471
}
472
if len(wrappedEvent.Results) > 0 {
473
errs := []error{}
474
for _, result := range wrappedEvent.Results {
475
result.MatcherStatus = false // just in case
476
if err := w.Write(result); err != nil {
477
errs = append(errs, err)
478
}
479
}
480
if len(errs) > 0 {
481
return multierr.Combine(errs...)
482
}
483
return nil
484
}
485
// if no results were found, manually create a failure event
486
event := wrappedEvent.InternalEvent
487
488
templatePath, templateURL := utils.TemplatePathURL(types.ToString(event["template-path"]), types.ToString(event["template-id"]), types.ToString(event["template-verifier"]))
489
var templateInfo model.Info
490
if event["template-info"] != nil {
491
templateInfo = event["template-info"].(model.Info)
492
}
493
fields := protocolUtils.GetJsonFieldsFromURL(types.ToString(event["host"]))
494
if types.ToString(event["ip"]) != "" {
495
fields.Ip = types.ToString(event["ip"])
496
}
497
if types.ToString(event["path"]) != "" {
498
fields.Path = types.ToString(event["path"])
499
}
500
501
data := &ResultEvent{
502
Template: templatePath,
503
TemplateURL: templateURL,
504
TemplateID: types.ToString(event["template-id"]),
505
TemplatePath: types.ToString(event["template-path"]),
506
Info: templateInfo,
507
Type: types.ToString(event["type"]),
508
Host: fields.Host,
509
Path: fields.Path,
510
Port: fields.Port,
511
Scheme: fields.Scheme,
512
URL: fields.URL,
513
IP: fields.Ip,
514
Request: types.ToString(event["request"]),
515
Response: types.ToString(event["response"]),
516
MatcherStatus: false,
517
Timestamp: time.Now(),
518
//FIXME: this is workaround to encode the template when no results were found
519
TemplateEncoded: w.encodeTemplate(types.ToString(event["template-path"])),
520
Error: types.ToString(event["error"]),
521
}
522
return w.Write(data)
523
}
524
525
var maxTemplateFileSizeForEncoding = unitutils.Mega
526
527
func (w *StandardWriter) encodeTemplate(templatePath string) string {
528
data, err := os.ReadFile(templatePath)
529
if err == nil && !w.omitTemplate && len(data) <= maxTemplateFileSizeForEncoding && config.DefaultConfig.IsCustomTemplate(templatePath) {
530
return base64.StdEncoding.EncodeToString(data)
531
}
532
return ""
533
}
534
535
func sanitizeFileName(fileName string) string {
536
fileName = strings.ReplaceAll(fileName, "http:", "")
537
fileName = strings.ReplaceAll(fileName, "https:", "")
538
fileName = strings.ReplaceAll(fileName, "/", "_")
539
fileName = strings.ReplaceAll(fileName, "\\", "_")
540
fileName = strings.ReplaceAll(fileName, "-", "_")
541
fileName = strings.ReplaceAll(fileName, ".", "_")
542
if osutils.IsWindows() {
543
fileName = strings.ReplaceAll(fileName, ":", "_")
544
}
545
fileName = strings.TrimPrefix(fileName, "__")
546
return fileName
547
}
548
func (w *StandardWriter) WriteStoreDebugData(host, templateID, eventType string, data string) {
549
if w.storeResponse {
550
if len(host) > 60 {
551
host = host[:57] + "..."
552
}
553
if len(templateID) > 100 {
554
templateID = templateID[:97] + "..."
555
}
556
557
filename := sanitizeFileName(fmt.Sprintf("%s_%s", host, templateID))
558
subFolder := filepath.Join(w.storeResponseDir, sanitizeFileName(eventType))
559
if !fileutil.FolderExists(subFolder) {
560
_ = fileutil.CreateFolder(subFolder)
561
}
562
filename = filepath.Join(subFolder, fmt.Sprintf("%s.txt", filename))
563
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
564
if err != nil {
565
gologger.Error().Msgf("Could not open debug output file: %s", err)
566
return
567
}
568
_, _ = fmt.Fprintln(f, data)
569
_ = f.Close()
570
}
571
}
572
573
// tryParseCause tries to parse the cause of given error
574
// this is legacy support due to use of errorutil in existing libraries
575
// but this should not be required once all libraries are updated
576
func tryParseCause(err error) error {
577
if err == nil {
578
return nil
579
}
580
msg := err.Error()
581
if strings.HasPrefix(msg, "ReadStatusLine:") {
582
// last index is actual error (from rawhttp)
583
parts := strings.Split(msg, ":")
584
return errkit.New(strings.TrimSpace(parts[len(parts)-1]))
585
}
586
if strings.Contains(msg, "read ") {
587
// same here
588
parts := strings.Split(msg, ":")
589
return errkit.New(strings.TrimSpace(parts[len(parts)-1]))
590
}
591
return err
592
}
593
594
func (w *StandardWriter) RequestStatsLog(statusCode, response string) {}
595
596