Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/progress/progress.go
2070 views
1
package progress
2
3
import (
4
"context"
5
"fmt"
6
"os"
7
"strings"
8
"time"
9
10
"github.com/projectdiscovery/clistats"
11
"github.com/projectdiscovery/gologger"
12
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
13
)
14
15
// Progress is an interface implemented by nuclei progress display
16
// driver.
17
type Progress interface {
18
// Stop stops the progress recorder.
19
Stop()
20
// Init inits the progress bar with initial details for scan
21
Init(hostCount int64, rulesCount int, requestCount int64)
22
// AddToTotal adds a value to the total request count
23
AddToTotal(delta int64)
24
// IncrementRequests increments the requests counter by 1.
25
IncrementRequests()
26
// SetRequests sets the counter by incrementing it with a delta
27
SetRequests(count uint64)
28
// IncrementMatched increments the matched counter by 1.
29
IncrementMatched()
30
// IncrementErrorsBy increments the error counter by count.
31
IncrementErrorsBy(count int64)
32
// IncrementFailedRequestsBy increments the number of requests counter by count
33
// along with errors.
34
IncrementFailedRequestsBy(count int64)
35
}
36
37
var _ Progress = &StatsTicker{}
38
39
// StatsTicker is a progress instance for showing program stats
40
type StatsTicker struct {
41
cloud bool
42
active bool
43
outputJSON bool
44
stats clistats.StatisticsClient
45
tickDuration time.Duration
46
}
47
48
// NewStatsTicker creates and returns a new progress tracking object.
49
func NewStatsTicker(duration int, active, outputJSON, cloud bool, port int) (Progress, error) {
50
var tickDuration time.Duration
51
if active && duration != -1 {
52
tickDuration = time.Duration(duration) * time.Second
53
} else {
54
tickDuration = -1
55
}
56
57
progress := &StatsTicker{}
58
59
statsOpts := &clistats.DefaultOptions
60
statsOpts.ListenPort = port
61
// metrics port is enabled by default and is not configurable with new version of clistats
62
// by default 63636 is used and than can be modified with -mp flag
63
64
stats, err := clistats.NewWithOptions(context.TODO(), statsOpts)
65
if err != nil {
66
return nil, err
67
}
68
// only print in verbose mode
69
gologger.Verbose().Msgf("Started metrics server at localhost:%v", stats.Options.ListenPort)
70
progress.cloud = cloud
71
progress.active = active
72
progress.stats = stats
73
progress.tickDuration = tickDuration
74
progress.outputJSON = outputJSON
75
76
return progress, nil
77
}
78
79
// Init initializes the progress display mechanism by setting counters, etc.
80
func (p *StatsTicker) Init(hostCount int64, rulesCount int, requestCount int64) {
81
p.stats.AddStatic("templates", rulesCount)
82
p.stats.AddStatic("hosts", hostCount)
83
p.stats.AddStatic("startedAt", time.Now())
84
p.stats.AddCounter("requests", uint64(0))
85
p.stats.AddCounter("errors", uint64(0))
86
p.stats.AddCounter("matched", uint64(0))
87
p.stats.AddCounter("total", uint64(requestCount))
88
89
if p.active {
90
var printCallbackFunc clistats.DynamicCallback
91
if p.outputJSON {
92
printCallbackFunc = printCallbackJSON
93
} else {
94
printCallbackFunc = p.makePrintCallback()
95
}
96
p.stats.AddDynamic("summary", printCallbackFunc)
97
if err := p.stats.Start(); err != nil {
98
gologger.Warning().Msgf("Couldn't start statistics: %s", err)
99
}
100
101
// Note: this is needed and is responsible for the tick event
102
p.stats.GetStatResponse(p.tickDuration, func(s string, err error) error {
103
if err != nil {
104
gologger.Warning().Msgf("Could not read statistics: %s\n", err)
105
}
106
return nil
107
})
108
}
109
}
110
111
// AddToTotal adds a value to the total request count
112
func (p *StatsTicker) AddToTotal(delta int64) {
113
p.stats.IncrementCounter("total", int(delta))
114
}
115
116
// IncrementRequests increments the requests counter by 1.
117
func (p *StatsTicker) IncrementRequests() {
118
p.stats.IncrementCounter("requests", 1)
119
}
120
121
// SetRequests sets the counter by incrementing it with a delta
122
func (p *StatsTicker) SetRequests(count uint64) {
123
p.stats.IncrementCounter("requests", int(count))
124
}
125
126
// IncrementMatched increments the matched counter by 1.
127
func (p *StatsTicker) IncrementMatched() {
128
p.stats.IncrementCounter("matched", 1)
129
}
130
131
// IncrementErrorsBy increments the error counter by count.
132
func (p *StatsTicker) IncrementErrorsBy(count int64) {
133
p.stats.IncrementCounter("errors", int(count))
134
}
135
136
// IncrementFailedRequestsBy increments the number of requests counter by count along with errors.
137
func (p *StatsTicker) IncrementFailedRequestsBy(count int64) {
138
// mimic dropping by incrementing the completed requests
139
p.stats.IncrementCounter("requests", int(count))
140
p.stats.IncrementCounter("errors", int(count))
141
}
142
143
func (p *StatsTicker) makePrintCallback() func(stats clistats.StatisticsClient) interface{} {
144
return func(stats clistats.StatisticsClient) interface{} {
145
builder := &strings.Builder{}
146
147
var duration time.Duration
148
if startedAt, ok := stats.GetStatic("startedAt"); ok {
149
if startedAtTime, ok := startedAt.(time.Time); ok {
150
duration = time.Since(startedAtTime)
151
_, _ = fmt.Fprintf(builder, "[%s]", fmtDuration(duration))
152
}
153
}
154
155
if templates, ok := stats.GetStatic("templates"); ok {
156
builder.WriteString(" | Templates: ")
157
builder.WriteString(clistats.String(templates))
158
}
159
160
if hosts, ok := stats.GetStatic("hosts"); ok {
161
builder.WriteString(" | Hosts: ")
162
builder.WriteString(clistats.String(hosts))
163
}
164
165
requests, okRequests := stats.GetCounter("requests")
166
total, okTotal := stats.GetCounter("total")
167
168
// If input is not given, total is 0 which cause percentage overflow
169
if total == 0 {
170
total = requests
171
}
172
173
if okRequests && okTotal && duration > 0 && !p.cloud {
174
builder.WriteString(" | RPS: ")
175
builder.WriteString(clistats.String(uint64(float64(requests) / duration.Seconds())))
176
}
177
178
if matched, ok := stats.GetCounter("matched"); ok {
179
builder.WriteString(" | Matched: ")
180
builder.WriteString(clistats.String(matched))
181
}
182
183
if errors, ok := stats.GetCounter("errors"); ok && !p.cloud {
184
builder.WriteString(" | Errors: ")
185
builder.WriteString(clistats.String(errors))
186
}
187
188
if okRequests && okTotal {
189
if p.cloud {
190
builder.WriteString(" | Task: ")
191
} else {
192
builder.WriteString(" | Requests: ")
193
}
194
builder.WriteString(clistats.String(requests))
195
builder.WriteRune('/')
196
builder.WriteString(clistats.String(total))
197
builder.WriteRune(' ')
198
builder.WriteRune('(')
199
//nolint:gomnd // this is not a magic number
200
builder.WriteString(clistats.String(uint64(float64(requests) / float64(total) * 100.0)))
201
builder.WriteRune('%')
202
builder.WriteRune(')')
203
builder.WriteRune('\n')
204
}
205
206
_, _ = fmt.Fprintf(os.Stderr, "%s", builder.String())
207
return builder.String()
208
}
209
}
210
211
func printCallbackJSON(stats clistats.StatisticsClient) interface{} {
212
builder := &strings.Builder{}
213
if err := json.NewEncoder(builder).Encode(metricsMap(stats)); err == nil {
214
_, _ = fmt.Fprintf(os.Stderr, "%s", builder.String())
215
}
216
return builder.String()
217
}
218
219
func metricsMap(stats clistats.StatisticsClient) map[string]interface{} {
220
results := make(map[string]interface{})
221
222
var (
223
startedAt time.Time
224
duration time.Duration
225
)
226
227
if stAt, ok := stats.GetStatic("startedAt"); ok {
228
startedAt = stAt.(time.Time)
229
duration = time.Since(startedAt)
230
}
231
232
results["startedAt"] = startedAt
233
results["duration"] = fmtDuration(duration)
234
templates, _ := stats.GetStatic("templates")
235
results["templates"] = clistats.String(templates)
236
hosts, _ := stats.GetStatic("hosts")
237
results["hosts"] = clistats.String(hosts)
238
matched, _ := stats.GetCounter("matched")
239
results["matched"] = clistats.String(matched)
240
requests, _ := stats.GetCounter("requests")
241
results["requests"] = clistats.String(requests)
242
total, _ := stats.GetCounter("total")
243
results["total"] = clistats.String(total)
244
results["rps"] = clistats.String(uint64(float64(requests) / duration.Seconds()))
245
errors, _ := stats.GetCounter("errors")
246
results["errors"] = clistats.String(errors)
247
248
// nolint:gomnd // this is not a magic number
249
percentData := (float64(requests) * float64(100)) / float64(total)
250
percent := clistats.String(uint64(percentData))
251
results["percent"] = percent
252
return results
253
}
254
255
// fmtDuration formats the duration for the time elapsed
256
func fmtDuration(d time.Duration) string {
257
d = d.Round(time.Second)
258
h := d / time.Hour
259
d -= h * time.Hour
260
m := d / time.Minute
261
d -= m * time.Minute
262
s := d / time.Second
263
return fmt.Sprintf("%d:%02d:%02d", h, m, s)
264
}
265
266
// Stop stops the progress bar execution
267
func (p *StatsTicker) Stop() {
268
if p.active {
269
// Print one final summary
270
if p.outputJSON {
271
printCallbackJSON(p.stats)
272
} else {
273
p.makePrintCallback()(p.stats)
274
}
275
if err := p.stats.Stop(); err != nil {
276
gologger.Warning().Msgf("Couldn't stop statistics: %s", err)
277
}
278
}
279
}
280
281