Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/scan/charts/echarts.go
2070 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
x := make([]string, 0)
176
y := make([]opts.KlineData, 0)
177
for _, event := range data[:TopK] {
178
x = append(x, event.ID)
179
y = append(y, event.KlineData)
180
}
181
182
kline.SetXAxis(x).AddSeries("templates", y)
183
kline.SetGlobalOptions(
184
charts.WithTitleOpts(opts.Title{Title: fmt.Sprintf("Nuclei: Top %v Slow Templates", TopK)}),
185
charts.WithXAxisOpts(opts.XAxis{
186
Type: "category",
187
Show: opts.Bool(true),
188
AxisLabel: &opts.AxisLabel{Rotate: 90, Show: opts.Bool(true), ShowMinLabel: opts.Bool(true), ShowMaxLabel: opts.Bool(true), Formatter: opts.FuncOpts(`function (value) { return value; }`)},
189
}),
190
charts.WithYAxisOpts(opts.YAxis{
191
Scale: opts.Bool(true),
192
Type: "value",
193
Show: opts.Bool(true),
194
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'; }`)},
195
}),
196
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
197
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "40%", Top: "10%"}),
198
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 ; }`)}),
199
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
200
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
201
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
202
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
203
}}),
204
)
205
206
return kline
207
}
208
209
func (s *ScanEventsCharts) RequestsVSInterval(c echo.Context) error {
210
line := s.requestsVSInterval(c)
211
return line.Render(c.Response().Writer)
212
}
213
214
// requestsVSInterval generates a line chart showing requests per second over time
215
func (s *ScanEventsCharts) requestsVSInterval(c echo.Context) *charts.Line {
216
line := charts.NewLine()
217
line.SetGlobalOptions(
218
charts.WithTitleOpts(opts.Title{
219
Title: "Nuclei: Requests Per Second vs Time",
220
Subtitle: "Chart Shows RPS (Requests Per Second) Over Time",
221
}),
222
)
223
224
sort.Slice(s.data, func(i, j int) bool {
225
return s.data[i].Time.Before(s.data[j].Time)
226
})
227
228
var interval time.Duration
229
230
if c != nil {
231
interval, _ = time.ParseDuration(c.QueryParam("interval"))
232
}
233
if interval <= 3 {
234
interval = 5 * time.Second
235
}
236
237
data := []opts.LineData{}
238
temp := 0
239
if len(s.data) > 0 {
240
orig := s.data[0].Time
241
startTime := orig
242
xaxisData := []int64{}
243
for _, v := range s.data {
244
if v.Time.Sub(startTime) > interval {
245
millisec := v.Time.Sub(orig).Milliseconds()
246
xaxisData = append(xaxisData, millisec)
247
data = append(data, opts.LineData{Value: temp, Name: v.Time.Sub(orig).String()})
248
temp = 0
249
startTime = v.Time
250
}
251
temp += 1
252
}
253
// Handle last interval if exists
254
if temp > 0 {
255
millisec := s.data[len(s.data)-1].Time.Sub(orig).Milliseconds()
256
xaxisData = append(xaxisData, millisec)
257
data = append(data, opts.LineData{Value: temp, Name: s.data[len(s.data)-1].Time.Sub(orig).String()})
258
}
259
line.SetXAxis(xaxisData)
260
line.AddSeries("RPS", data, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
261
}
262
263
line.SetGlobalOptions(
264
charts.WithTitleOpts(opts.Title{Title: "Nuclei: Template Execution", Subtitle: "Time Interval: " + interval.String()}),
265
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'; }`)}}),
266
charts.WithYAxisOpts(opts.YAxis{Name: "RPS Value", Type: "value", Show: opts.Bool(true)}),
267
charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
268
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
269
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
270
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
271
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
272
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
273
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
274
}}),
275
)
276
277
line.Validate()
278
return line
279
}
280
281
func (s *ScanEventsCharts) ConcurrencyVsTime(c echo.Context) error {
282
line := s.concurrencyVsTime(c)
283
return line.Render(c.Response().Writer)
284
}
285
286
// concurrencyVsTime generates a line chart showing concurrency (total workers) over time
287
func (s *ScanEventsCharts) concurrencyVsTime(c echo.Context) *charts.Line {
288
line := charts.NewLine()
289
line.SetGlobalOptions(
290
charts.WithTitleOpts(opts.Title{
291
Title: "Nuclei: Concurrency vs Time",
292
Subtitle: "Chart Shows Concurrency (Total Workers) Over Time",
293
}),
294
)
295
296
dataset := sliceutil.Clone(s.data)
297
298
sort.Slice(dataset, func(i, j int) bool {
299
return dataset[i].Time.Before(dataset[j].Time)
300
})
301
302
var interval time.Duration
303
if c != nil {
304
interval, _ = time.ParseDuration(c.QueryParam("interval"))
305
}
306
if interval <= 3 {
307
interval = 5 * time.Second
308
}
309
310
// create array with time interval as x-axis and worker count as y-axis
311
// entry is a struct with time and poolsize
312
type entry struct {
313
Time time.Duration
314
poolsize int
315
}
316
allEntries := []entry{}
317
318
dataIndex := 0
319
maxIndex := len(dataset) - 1
320
currEntry := entry{}
321
322
lastTime := dataset[0].Time
323
for dataIndex <= maxIndex {
324
currTime := dataset[dataIndex].Time
325
if currTime.Sub(lastTime) > interval {
326
// next batch
327
currEntry.Time = interval
328
allEntries = append(allEntries, currEntry)
329
lastTime = dataset[dataIndex-1].Time
330
}
331
if dataset[dataIndex].EventType == events.ScanStarted {
332
currEntry.poolsize += 1
333
} else {
334
currEntry.poolsize -= 1
335
}
336
dataIndex += 1
337
}
338
339
plotData := []opts.LineData{}
340
xaxisData := []int64{}
341
tempTime := time.Duration(0)
342
for _, v := range allEntries {
343
tempTime += v.Time
344
plotData = append(plotData, opts.LineData{Value: v.poolsize, Name: tempTime.String()})
345
xaxisData = append(xaxisData, tempTime.Milliseconds())
346
}
347
line.SetXAxis(xaxisData)
348
line.AddSeries("Concurrency", plotData, charts.WithLineChartOpts(opts.LineChart{Smooth: opts.Bool(false)}), charts.WithLabelOpts(opts.Label{Show: opts.Bool(true), Position: "top"}))
349
350
line.SetGlobalOptions(
351
charts.WithTitleOpts(opts.Title{Title: "Nuclei: WorkerPool", Subtitle: "Time Interval: " + interval.String()}),
352
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'; }`)}}),
353
charts.WithYAxisOpts(opts.YAxis{Name: "Total Workers", Type: "value", Show: opts.Bool(true)}),
354
charts.WithInitializationOpts(opts.Initialization{Theme: "dark"}),
355
charts.WithDataZoomOpts(opts.DataZoom{Type: "slider", Start: 0, End: 100}),
356
charts.WithGridOpts(opts.Grid{Left: "10%", Right: "10%", Bottom: "15%", Top: "20%"}),
357
charts.WithToolboxOpts(opts.Toolbox{Show: opts.Bool(true), Feature: &opts.ToolBoxFeature{
358
SaveAsImage: &opts.ToolBoxFeatureSaveAsImage{Show: opts.Bool(true), Name: "save", Title: "save"},
359
DataZoom: &opts.ToolBoxFeatureDataZoom{Show: opts.Bool(true), Title: map[string]string{"zoom": "zoom", "back": "back"}},
360
DataView: &opts.ToolBoxFeatureDataView{Show: opts.Bool(true), Title: "raw", Lang: []string{"raw", "exit", "refresh"}},
361
}}),
362
)
363
364
line.Validate()
365
return line
366
}
367
368
// getCategoryRequestCount returns a map of category and request count
369
func getCategoryRequestCount(values []events.ScanEvent) map[string][]events.ScanEvent {
370
mx := make(map[string][]events.ScanEvent)
371
for _, event := range values {
372
mx[event.TemplateType] = append(mx[event.TemplateType], event)
373
}
374
return mx
375
}
376
377