Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/scan/charts/echarts.go
2851 views
1
package charts
2
3
import (
4
"fmt"
5
"os"
6
"sort"
7
"time"
8
9
"github.com/go-echarts/go-echarts/v2/charts"
10
"github.com/go-echarts/go-echarts/v2/components"
11
"github.com/go-echarts/go-echarts/v2/opts"
12
"github.com/labstack/echo/v4"
13
"github.com/projectdiscovery/nuclei/v3/pkg/scan/events"
14
sliceutil "github.com/projectdiscovery/utils/slice"
15
)
16
17
const (
18
TopK = 50
19
SpacerHeight = "50px"
20
)
21
22
func (s *ScanEventsCharts) AllCharts(c echo.Context) error {
23
page := s.allCharts(c)
24
return page.Render(c.Response().Writer)
25
}
26
27
func (s *ScanEventsCharts) GenerateHTML(filePath string) error {
28
page := s.allCharts(nil)
29
output, err := os.Create(filePath)
30
if err != nil {
31
return err
32
}
33
defer func() {
34
_ = output.Close()
35
}()
36
return page.Render(output)
37
}
38
39
// AllCharts generates all the charts for the scan events and returns a page component
40
func (s *ScanEventsCharts) allCharts(c echo.Context) *components.Page {
41
page := components.NewPage()
42
page.PageTitle = "Nuclei Charts"
43
line1 := s.totalRequestsOverTime(c)
44
// line1.SetSpacerHeight(SpacerHeight)
45
kline := s.topSlowTemplates(c)
46
// kline.SetSpacerHeight(SpacerHeight)
47
line2 := s.requestsVSInterval(c)
48
// line2.SetSpacerHeight(SpacerHeight)
49
line3 := s.concurrencyVsTime(c)
50
// line3.SetSpacerHeight(SpacerHeight)
51
page.AddCharts(line1, kline, line2, line3)
52
page.SetLayout(components.PageCenterLayout)
53
// page.Theme = "dark"
54
page.Validate()
55
56
return page
57
}
58
59
func (s *ScanEventsCharts) TotalRequestsOverTime(c echo.Context) error {
60
line := s.totalRequestsOverTime(c)
61
return line.Render(c.Response().Writer)
62
}
63
64
// totalRequestsOverTime generates a line chart showing total requests count over time
65
func (s *ScanEventsCharts) totalRequestsOverTime(c echo.Context) *charts.Line {
66
line := charts.NewLine()
67
line.SetGlobalOptions(
68
charts.WithTitleOpts(opts.Title{
69
Title: "Nuclei: Total Requests vs Time",
70
Subtitle: "Chart Shows Total Requests Count Over Time (for each/all Protocols)",
71
}),
72
)
73
74
startTime := time.Now()
75
var endTime time.Time
76
77
for _, event := range s.data {
78
if event.Time.Before(startTime) {
79
startTime = event.Time
80
}
81
if event.Time.After(endTime) {
82
endTime = event.Time
83
}
84
}
85
data := getCategoryRequestCount(s.data)
86
max := 0
87
for _, v := range data {
88
if len(v) > max {
89
max = len(v)
90
}
91
}
92
line.SetXAxis(time.Now().Format(time.RFC3339))
93
for k, v := range data {
94
lineData := make([]opts.LineData, 0)
95
temp := 0
96
for _, scanEvent := range v {
97
temp += scanEvent.MaxRequests
98
val := scanEvent.Time.Sub(startTime)
99
lineData = append(lineData, opts.LineData{
100
Value: []interface{}{val.Milliseconds(), temp},
101
Name: scanEvent.TemplateID,
102
})
103
}
104
line.AddSeries(k, lineData, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
105
}
106
107
line.SetGlobalOptions(
108
charts.WithTitleOpts(opts.Title{Title: "Nuclei: total-req vs time"}),
109
charts.WithXAxisOpts(opts.XAxis{Name: "Time", Type: "time", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
110
charts.WithYAxisOpts(opts.YAxis{Name: "Requests Sent", Type: "value"}),
111
charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
112
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
113
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
114
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
115
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
116
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
117
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
118
}}),
119
)
120
121
line.Validate()
122
return line
123
}
124
125
func (s *ScanEventsCharts) TopSlowTemplates(c echo.Context) error {
126
kline := s.topSlowTemplates(c)
127
return kline.Render(c.Response().Writer)
128
}
129
130
// topSlowTemplates generates a Kline chart showing the top slow templates by time taken
131
func (s *ScanEventsCharts) topSlowTemplates(c echo.Context) *charts.Kline {
132
kline := charts.NewKLine()
133
kline.SetGlobalOptions(
134
charts.WithTitleOpts(opts.Title{
135
Title: "Nuclei: Top Slow Templates",
136
Subtitle: fmt.Sprintf("Chart Shows Top Slow Templates (by time taken) (Top %v)", TopK),
137
}),
138
)
139
ids := map[string][]int64{}
140
startTime := time.Now()
141
for _, event := range s.data {
142
if event.Time.Before(startTime) {
143
startTime = event.Time
144
}
145
}
146
for _, event := range s.data {
147
ids[event.TemplateID] = append(ids[event.TemplateID], event.Time.Sub(startTime).Milliseconds())
148
}
149
150
type entry struct {
151
ID string
152
KlineData opts.KlineData
153
start int64
154
end int64
155
}
156
data := []entry{}
157
158
for a, b := range ids {
159
if len(b) < 2 {
160
continue // Prevents index out of range error
161
}
162
d := entry{
163
ID: a,
164
KlineData: opts.KlineData{Value: []int64{b[0], b[len(b)-1], b[0], b[len(b)-1]}}, // Adjusted to prevent index out of range error
165
start: b[0],
166
end: b[len(b)-1],
167
}
168
data = append(data, d)
169
}
170
171
sort.Slice(data, func(i, j int) bool {
172
return data[i].end-data[i].start > data[j].end-data[j].start
173
})
174
175
// Ensure we don't try to access more elements than available
176
limit := TopK
177
if len(data) < TopK {
178
limit = len(data)
179
}
180
181
x := make([]string, 0)
182
y := make([]opts.KlineData, 0)
183
for _, event := range data[:limit] {
184
x = append(x, event.ID)
185
y = append(y, event.KlineData)
186
}
187
188
kline.SetXAxis(x).AddSeries("templates", y)
189
kline.SetGlobalOptions(
190
charts.WithTitleOpts(opts.Title{Title: fmt.Sprintf("Nuclei: Top %v Slow Templates", limit)}),
191
charts.WithXAxisOpts(opts.XAxis{
192
Type: "category",
193
Show: opts.Bool(true),
194
AxisLabel: &opts.AxisLabel{Rotate: 90, Show: opts.Bool(true), ShowMinLabel: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (value) { return value; }`)},
195
}),
196
charts.WithYAxisOpts(opts.YAxis{
197
Scale: opts.Bool(true),
198
Type: "value",
199
Show: opts.Bool(true),
200
AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), Formatter: opts.FuncOpts(`function (ms) { return Math.floor(ms/60000) + 'm' + Math.floor((ms/60000 - Math.floor(ms/60000))*60) + 's'; }`)},
201
}),
202
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
203
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "40%", Top: "10%"}),
204
charts.WithTooltipOpts(opts.Tooltip{Show: opts.Bool(true), Trigger: "item", TriggerOn: "mousemove|click", Enterable: opts.Bool(true), Formatter: opts.FuncOpts(`function (params) { return params.name ; }`)}),
205
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
206
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
207
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
208
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
209
}}),
210
)
211
212
return kline
213
}
214
215
func (s *ScanEventsCharts) RequestsVSInterval(c echo.Context) error {
216
line := s.requestsVSInterval(c)
217
return line.Render(c.Response().Writer)
218
}
219
220
// requestsVSInterval generates a line chart showing requests per second over time
221
func (s *ScanEventsCharts) requestsVSInterval(c echo.Context) *charts.Line {
222
line := charts.NewLine()
223
line.SetGlobalOptions(
224
charts.WithTitleOpts(opts.Title{
225
Title: "Nuclei: Requests Per Second vs Time",
226
Subtitle: "Chart Shows RPS (Requests Per Second) Over Time",
227
}),
228
)
229
230
sort.Slice(s.data, func(i, j int) bool {
231
return s.data[i].Time.Before(s.data[j].Time)
232
})
233
234
var interval time.Duration
235
236
if c != nil {
237
interval, _ = time.ParseDuration(c.QueryParam("interval"))
238
}
239
if interval <= 3 {
240
interval = 5 * time.Second
241
}
242
243
data := []opts.LineData{}
244
temp := 0
245
if len(s.data) > 0 {
246
orig := s.data[0].Time
247
startTime := orig
248
xaxisData := []int64{}
249
for _, v := range s.data {
250
if v.Time.Sub(startTime) > interval {
251
millisec := v.Time.Sub(orig).Milliseconds()
252
xaxisData = append(xaxisData, millisec)
253
data = append(data, opts.LineData{Value: temp, Name: v.Time.Sub(orig).String()})
254
temp = 0
255
startTime = v.Time
256
}
257
temp += 1
258
}
259
// Handle last interval if exists
260
if temp > 0 {
261
millisec := s.data[len(s.data)-1].Time.Sub(orig).Milliseconds()
262
xaxisData = append(xaxisData, millisec)
263
data = append(data, opts.LineData{Value: temp, Name: s.data[len(s.data)-1].Time.Sub(orig).String()})
264
}
265
line.SetXAxis(xaxisData)
266
line.AddSeries("RPS", data, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
267
}
268
269
line.SetGlobalOptions(
270
charts.WithTitleOpts(opts.Title{Title: "Nuclei: Template Execution", Subtitle: "Time Interval: " + interval.String()}),
271
charts.WithXAxisOpts(opts.XAxis{Name: "Time Intervals", Type: "category", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
272
charts.WithYAxisOpts(opts.YAxis{Name: "RPS Value", Type: "value", Show: opts.Bool(true)}),
273
charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
274
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
275
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
276
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
277
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
278
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
279
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
280
}}),
281
)
282
283
line.Validate()
284
return line
285
}
286
287
func (s *ScanEventsCharts) ConcurrencyVsTime(c echo.Context) error {
288
line := s.concurrencyVsTime(c)
289
return line.Render(c.Response().Writer)
290
}
291
292
// concurrencyVsTime generates a line chart showing concurrency (total workers) over time
293
func (s *ScanEventsCharts) concurrencyVsTime(c echo.Context) *charts.Line {
294
line := charts.NewLine()
295
line.SetGlobalOptions(
296
charts.WithTitleOpts(opts.Title{
297
Title: "Nuclei: Concurrency vs Time",
298
Subtitle: "Chart Shows Concurrency (Total Workers) Over Time",
299
}),
300
)
301
302
dataset := sliceutil.Clone(s.data)
303
304
sort.Slice(dataset, func(i, j int) bool {
305
return dataset[i].Time.Before(dataset[j].Time)
306
})
307
308
var interval time.Duration
309
if c != nil {
310
interval, _ = time.ParseDuration(c.QueryParam("interval"))
311
}
312
if interval <= 3 {
313
interval = 5 * time.Second
314
}
315
316
// create array with time interval as x-axis and worker count as y-axis
317
// entry is a struct with time and poolsize
318
type entry struct {
319
Time time.Duration
320
poolsize int
321
}
322
allEntries := []entry{}
323
324
dataIndex := 0
325
maxIndex := len(dataset) - 1
326
currEntry := entry{}
327
328
lastTime := dataset[0].Time
329
for dataIndex <= maxIndex {
330
currTime := dataset[dataIndex].Time
331
if currTime.Sub(lastTime) > interval {
332
// next batch
333
currEntry.Time = interval
334
allEntries = append(allEntries, currEntry)
335
lastTime = dataset[dataIndex-1].Time
336
}
337
if dataset[dataIndex].EventType == events.ScanStarted {
338
currEntry.poolsize += 1
339
} else {
340
currEntry.poolsize -= 1
341
}
342
dataIndex += 1
343
}
344
345
plotData := []opts.LineData{}
346
xaxisData := []int64{}
347
tempTime := time.Duration(0)
348
for _, v := range allEntries {
349
tempTime += v.Time
350
plotData = append(plotData, opts.LineData{Value: v.poolsize, Name: tempTime.String()})
351
xaxisData = append(xaxisData, tempTime.Milliseconds())
352
}
353
line.SetXAxis(xaxisData)
354
line.AddSeries("Concurrency", plotData, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
355
356
line.SetGlobalOptions(
357
charts.WithTitleOpts(opts.Title{Title: "Nuclei: WorkerPool", Subtitle: "Time Interval: " + interval.String()}),
358
charts.WithXAxisOpts(opts.XAxis{Name: "Time Intervals", Type: "category", AxisLabel: &opts.AxisLabel{Show: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (date) { return (date/1000)+'s'; }`)}}),
359
charts.WithYAxisOpts(opts.YAxis{Name: "Total Workers", Type: "value", Show: opts.Bool(true)}),
360
charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
361
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
362
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
363
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
364
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
365
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
366
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
367
}}),
368
)
369
370
line.Validate()
371
return line
372
}
373
374
// getCategoryRequestCount returns a map of category and request count
375
func getCategoryRequestCount(values []events.ScanEvent) map[string][]events.ScanEvent {
376
mx := make(map[string][]events.ScanEvent)
377
for _, event := range values {
378
mx[event.TemplateType] = append(mx[event.TemplateType], event)
379
}
380
return mx
381
}
382
383