d864aaa7d72ec50e9d1eda85a818ebde209d8a66
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.alerts.js
1 (
2   typeof define === "function" ? function (m) { define("kismet-ui-dot11-js", m); } :
3   typeof exports === "object" ? function (m) { module.exports = m(); } :
4   function(m){ this.kismet_ui_alerts = m(); }
5 )(function () {
6
7 "use strict";
8
9 var exports = {};
10
11 var local_uri_prefix = ""; 
12 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
13     local_uri_prefix = KISMET_URI_PREFIX;
14
15 exports.load_complete = 0;
16
17 function severity_to_string(sev) {
18     switch (sev) {
19         case 0:
20             return "INFO";
21         case 5:
22             return "LOW";
23         case 10:
24             return "MEDIUM";
25         case 15:
26             return "HIGH";
27         case 20:
28             return "CRITICAL";
29         default:
30             return "UNKNOWN";
31     }
32 }
33
34 function severity_to_color(sev) {
35     switch (sev) {
36         case 0:
37             return "#03e3fc";
38         case 5:
39             return "#fbff00";
40         case 10:
41             return "#fce303";
42         case 15:
43             return "#fcba03";
44         case 20:
45             return "#fc031c";
46         default:
47             return "UNKNOWN";
48     }
49
50 }
51
52 var alertTid = -1;
53 var alert_element;
54 var alert_status_element;
55 var AlertColumns = new Array();
56
57 exports.AddAlertColumn = function(id, options) {
58     var coldef = {
59         kismetId: id,
60         sTitle: options.sTitle,
61         field: null,
62         fields: null,
63     };
64
65     if ('field' in options) {
66         coldef.field = options.field;
67     }
68
69     if ('fields' in options) {
70         coldef.fields = options.fields;
71     }
72
73     if ('description' in options) {
74         coldef.description = options.description;
75     }
76
77     if ('name' in options) {
78         coldef.name = options.name;
79     }
80
81     if ('orderable' in options) {
82         coldef.bSortable = options.orderable;
83     }
84
85     if ('visible' in options) {
86         coldef.bVisible = options.visible;
87     } else {
88         coldef.bVisible = true;
89     }
90
91     if ('selectable' in options) {
92         coldef.user_selectable = options.selectable;
93     } else {
94         coldef.user_selectable = true;
95     }
96
97     if ('searchable' in options) {
98         coldef.bSearchable = options.searchable;
99     }
100
101     if ('width' in options) {
102         coldef.width = options.width;
103     }
104
105     var f;
106     if (typeof(coldef.field) === 'string') {
107         var fs = coldef.field.split('/');
108         f = fs[fs.length - 1];
109     } else if (Array.isArray(coldef.field)) {
110         f = coldef.field[1];
111     }
112
113     coldef.mData = function(row, type, set) {
114         return kismet.ObjectByString(row, f);
115     }
116
117     if ('renderfunc' in options) {
118         coldef.mRender = options.renderfunc;
119     }
120
121     if ('drawfunc' in options) {
122         coldef.kismetdrawfunc = options.drawfunc;
123     }
124
125     AlertColumns.push(coldef);
126 }
127
128 exports.GetAlertColumns = function(showall = false) {
129     var ret = new Array();
130
131     var order = kismet.getStorage('kismet.alerttable.columns', []);
132
133     if (order.length == 0) {
134         // sort invisible columns to the end
135         for (var i in AlertColumns) {
136             if (!AlertColumns[i].bVisible)
137                 continue;
138             ret.push(AlertColumns[i]);
139         }
140
141         for (var i in AlertColumns) {
142             if (AlertColumns[i].bVisible)
143                 continue;
144             ret.push(AlertColumns[i]);
145         }
146
147         return ret;
148     }
149
150     for (var oi in order) {
151         var o = order[oi];
152
153         if (!o.enable)
154             continue;
155
156         var sc = AlertColumns.find(function(e, i, a) {
157             if (e.kismetId === o.id)
158                 return true;
159             return false;
160         });
161
162         if (sc != undefined && sc.user_selectable) {
163             sc.bVisible = true;
164             ret.push(sc);
165         }
166     }
167
168     // Fallback if no columns were selected somehow
169     if (ret.length == 0) {
170         // sort invisible columns to the end
171         for (var i in AlertColumns) {
172             if (!AlertColumns[i].bVisible)
173                 continue;
174             ret.push(AlertColumns[i]);
175         }
176
177         for (var i in AlertColumns) {
178             if (AlertColumns[i].bVisible)
179                 continue;
180             ret.push(AlertColumns[i]);
181         }
182
183         return ret;
184     }
185
186     if (showall) {
187         for (var sci in AlertColumns) {
188             var sc = AlertColumns[sci];
189
190             var rc = ret.find(function(e, i, a) {
191                 if (e.kismetId === sc.kismetId)
192                     return true;
193                 return false;
194             });
195
196             if (rc == undefined) {
197                 sc.bVisible = false;
198                 ret.push(sc);
199             }
200         }
201
202         return ret;
203     }
204
205     for (var sci in AlertColumns) {
206         if (!AlertColumns[sci].user_selectable) {
207             ret.push(AlertColumns[sci]);
208         }
209     }
210
211     return ret;
212 }
213
214 exports.GetAlertColumnMap = function(columns) {
215     var ret = {};
216
217     for (var ci in columns) {
218         var fields = new Array();
219
220         if ('field' in columns[ci])
221             fields.push(columns[ci]['field']);
222
223         if ('fields' in columns[ci])
224             fields.push.apply(fields, columns[ci]['fields']);
225
226         ret[ci] = fields;
227     }
228
229     return ret;
230 }
231
232 exports.GetAlertFields = function(selected) {
233     var rawret = new Array();
234     var cols = exports.GetAlertColumns();
235
236     for (var i in cols) {
237         if ('field' in cols[i])
238             rawret.push(cols[i]['field']);
239
240         if ('fields' in cols[i])
241             rawret.push.apply(rawret, cols[i]['fields']);
242     }
243
244     // de-dupe
245     var ret = rawret.filter(function(item, pos, self) {
246         return self.indexOf(item) == pos;
247     });
248
249     return ret;
250 }
251
252
253 function ScheduleAlertSummary() {
254     try {
255         if (kismet_ui.window_visible && alert_element.is(":visible")) {
256             var dt = alert_element.DataTable();
257
258             // Save the state.  We can't use proper state saving because it seems to break
259             // the table position
260             kismet.putStorage('kismet.base.alerttable.order', JSON.stringify(dt.order()));
261             kismet.putStorage('kismet.base.alertttable.search', JSON.stringify(dt.search()));
262
263             // Snapshot where we are, because the 'don't reset page' in ajax.reload
264             // DOES still reset the scroll position
265             var prev_pos = {
266                 'top': $(dt.settings()[0].nScrollBody).scrollTop(),
267                 'left': $(dt.settings()[0].nScrollBody).scrollLeft()
268             };
269             dt.ajax.reload(function(d) {
270                 // Restore our scroll position
271                 $(dt.settings()[0].nScrollBody).scrollTop( prev_pos.top );
272                 $(dt.settings()[0].nScrollBody).scrollLeft( prev_pos.left );
273             }, false);
274         }
275
276     } catch (error) {
277         ;
278     }
279     
280     // Set our timer outside of the datatable callback so that we get called even
281     // if the ajax load fails
282     alertTid = setTimeout(ScheduleAlertSummary, 2000);
283 }
284
285 function InitializeAlertTable() {
286     var cols = exports.GetAlertColumns();
287     var colmap = exports.GetAlertColumnMap(cols);
288     var fields = exports.GetAlertFields();
289
290     var json = {
291         fields: fields,
292         colmap: colmap,
293         datatable: true,
294     };
295
296     if ($.fn.dataTable.isDataTable(alert_element)) {
297         alert_element.DataTable().destroy();
298         alert_element.empty();
299     }
300
301     alert_element
302         .on('xhr.dt', function(e, settings, json, xhr) {
303             json = kismet.sanitizeObject(json);
304
305             try {
306                 if (json['recordsFiltered'] != json['recordsTotal'])
307                     alert_status_element.html(`${json['recordsTotal']} alerts (${json['recordsFiltered']} shown after filter)`);
308                 else
309                     alert_status_element.html(`${json['recordsTotal']} alerts`);
310             } catch (error) {
311                 ;
312             }
313         })
314         .DataTable({
315             destroy: true,
316             scrollResize: true,
317             scrollY: 200,
318             serverSide: true,
319             processing: true,
320             dom: 'ft',
321             deferRender: true,
322             lengthChange: false,
323             scroller: {
324                 loadingIndicator: true,
325             },
326             ajax: {
327                 url: local_uri_prefix + "alerts/alerts_view.json",
328                 data: {
329                     json: JSON.stringify(json)
330                 },
331                 method: 'POST',
332                 timeout: 5000,
333             },
334             columns: cols,
335             order: [ [ 0, "desc" ] ],
336             createdRow: function(row, data, index) {
337                 row.id = data['kismet.alert.hash'];
338             },
339             drawCallback: function(settings) {
340                 var dt = this.api();
341
342                 dt.rows({
343                     page: 'current'
344                 }).every(function(rowIdx, tableLoop, rowLoop) {
345                     for (var c in AlertColumns) {
346                         var col = AlertColumns[c];
347
348                         if (!('kismetdrawfunc') in col)
349                             continue;
350
351                         try {
352                             col.kismetdrawfunc(col, dt, this);
353                         } catch (error) {
354                             ;
355                         }
356                     }
357
358                     $('td', this.node()).css('background-color', severity_to_color(this.data()['kismet.alert.severity']));
359                 });
360             },
361         });
362
363     var alert_dt = alert_element.DataTable();
364
365     // Restore the order
366     var saved_order = kismet.getStorage('kismet.base.alerttable.order', "");
367     if (saved_order !== "")
368         alert_dt.order(JSON.parse(saved_order));
369
370     // Restore the search
371     var saved_search = kismet.getStorage('kismet.base.alerttable.search', "");
372     if (saved_search !== "")
373         alert_dt.search(JSON.parse(saved_search));
374
375     // Set an onclick handler to spawn the device details dialog
376     $('tbody', alert_element).on('click', 'tr', function () {
377         exports.AlertDetailWindow(this.id);
378     } );
379
380     $('tbody', alert_element)
381         .on( 'mouseenter', 'td', function () {
382             var alert_dt = alert_element.DataTable();
383
384             if (typeof(alert_dt.cell(this).index()) === 'undefined')
385                 return;
386
387             var colIdx = alert_dt.cell(this).index().column;
388             var rowIdx = alert_dt.cell(this).index().row;
389
390             // Remove from all cells
391             $(alert_dt.cells().nodes()).removeClass('kismet-highlight');
392             // Highlight the td in this row
393             $('td', alert_dt.row(rowIdx).nodes()).addClass('kismet-highlight');
394         } );
395
396     return alert_dt;
397 }
398
399 kismet_ui_tabpane.AddTab({
400     id: 'tab_alerts',
401     tabTitle: 'Alerts',
402     createCallback: function(div) {
403         div.append(
404             $('<div>', {
405                 class: 'resize_wrapper',
406             })
407             .append(
408                 $('<table>', {
409                     id: 'alerts_dt',
410                     class: 'stripe hover nowrap',
411                     'cell-spacing': 0,
412                     width: '100%',
413                 })
414             )
415         ).append(
416             $('<div>', {
417                 id: 'alerts_status',
418                 style: 'padding-bottom: 10px;',
419             })
420         );
421
422         alert_element = $('#alerts_dt', div);
423         alert_status_element = $('#alerts_status', div);
424
425         InitializeAlertTable();
426         ScheduleAlertSummary();
427     },
428     priority: -1001,
429 }, 'center');
430
431 exports.AddAlertColumn('col_header', {
432     sTitle: 'Type',
433     field: 'kismet.alert.header',
434     name: 'Alert type',
435 });
436
437 exports.AddAlertColumn('col_class', {
438     sTitle: 'Class',
439     field: 'kismet.alert.class',
440     name: 'Class',
441 });
442
443 exports.AddAlertColumn('col_severity', {
444     sTitle: 'Severity',
445     field: 'kismet.alert.severity',
446     name: 'Severity',
447     renderfunc: function(d, t, r, m) {
448         return severity_to_string(d);
449     }
450 });
451
452 exports.AddAlertColumn('col_time', {
453     sTitle: 'Time',
454     field: 'kismet.alert.timestamp',
455     name: 'Timestamp',
456     renderfunc: function(d, t, r, m) {
457         return kismet_ui_base.renderLastTime(d, t, r, m);
458     }
459 });
460
461 exports.AddAlertColumn('col_tx', {
462     sTitle: 'Transmitter',
463     field: 'kismet.alert.transmitter_mac',
464     name: 'Transmitter MAC',
465     renderfunc: function(d, t, r, m) {
466         if (d === "00:00:00:00:00:00")
467             return "<i>n/a</i>";
468         return kismet.censorMAC(d);
469     }
470 });
471
472 exports.AddAlertColumn('col_sx', {
473     sTitle: 'Source',
474     field: 'kismet.alert.source_mac',
475     name: 'Source MAC',
476     renderfunc: function(d, t, r, m) {
477         if (d === "00:00:00:00:00:00")
478             return "<i>n/a</i>";
479         return kismet.censorMAC(d);
480     }
481 });
482
483 exports.AddAlertColumn('col_dx', {
484     sTitle: 'Destination',
485     field: 'kismet.alert.dest_mac',
486     name: 'Destination MAC',
487     renderfunc: function(d, t, r, m) {
488         if (d === "00:00:00:00:00:00")
489             return "<i>n/a</i>";
490         if (d === "FF:FF:FF:FF:FF:FF")
491             return "<i>all</i>";
492
493         return kismet.censorMAC(d);
494     }
495 });
496
497 exports.AddAlertColumn('content', {
498     sTitle: 'Alert',
499     field: 'kismet.alert.text',
500     name: 'Alert content',
501     renderfunc: function(d, t, r, m) {
502         return kismet.censorMAC(d);
503     }
504 });
505
506 exports.AddAlertColumn('hash_hidden', {
507     sTitle: 'Hash key',
508     field: 'kismet.alert.hash',
509     searchable: false,
510     visible: false,
511     orderable: false,
512 });
513
514 exports.AlertDetails = new Array();
515
516 exports.AddAlertDetail = function(id, title, pos, options) {
517     kismet_ui.AddDetail(exports.AlertDetails, id, title, pos, options);
518 }
519
520 exports.AlertDetailWindow = function(key) {
521     kismet_ui.DetailWindow(key, "Alert Details", 
522         {
523             storage: {},
524         },
525
526         function(panel, options) {
527             var content = panel.content;
528
529             panel.active = true;
530
531             window['storage_detail_' + key] = {};
532             window['storage_detail_' + key]['foobar'] = 'bar';
533
534             panel.updater = function() {
535                 if (kismet_ui.window_visible) {
536                     $.get(local_uri_prefix + "alerts/by-id/" + key + "/alert.json")
537                         .done(function(fulldata) {
538                             fulldata = kismet.sanitizeObject(fulldata);
539
540                             $('.loadoops', panel.content).hide();
541
542                             panel.headerTitle(`Alert: ${fulldata['kismet.alert.header']}`);
543
544                             var accordion = $('div#accordion', content);
545
546                             if (accordion.length == 0) {
547                                 accordion = $('<div />', {
548                                     id: 'accordion'
549                                 });
550
551                                 content.append(accordion);
552                             }
553
554                             var detailslist = exports.AlertDetails;
555
556                             for (var dii in detailslist) {
557                                 var di = detailslist[dii];
558
559                                 // Do we skip?
560                                 if ('filter' in di.options &&
561                                     typeof(di.options.filter) === 'function') {
562                                     if (di.options.filter(fulldata) == false) {
563                                         continue;
564                                     }
565                                 }
566
567                                 var vheader = $('h3#header_' + di.id, accordion);
568
569                                 if (vheader.length == 0) {
570                                     vheader = $('<h3>', {
571                                         id: "header_" + di.id,
572                                     })
573                                         .html(di.title);
574
575                                     accordion.append(vheader);
576                                 }
577
578                                 var vcontent = $('div#' + di.id, accordion);
579
580                                 if (vcontent.length == 0) {
581                                     vcontent = $('<div>', {
582                                         id: di.id,
583                                     });
584                                     accordion.append(vcontent);
585                                 }
586
587                                 // Do we have pre-rendered content?
588                                 if ('render' in di.options &&
589                                     typeof(di.options.render) === 'function') {
590                                     vcontent.html(di.options.render(fulldata));
591                                 }
592
593                                 if ('draw' in di.options && typeof(di.options.draw) === 'function') {
594                                     di.options.draw(fulldata, vcontent, options, 'storage_alert_' + key);
595                                 }
596
597                                 if ('finalize' in di.options &&
598                                     typeof(di.options.finalize) === 'function') {
599                                     di.options.finalize(fulldata, vcontent, options, 'storage_alert_' + key);
600                                 }
601                             }
602                             accordion.accordion({ heightStyle: 'fill' });
603                         })
604                         .fail(function(jqxhr, texterror) {
605                             content.html("<div class=\"loadoops\" style=\"padding: 10px;\"><h1>Oops!</h1><p>An error occurred loading alert details for key <code>" + key + 
606                                 "</code>: HTTP code <code>" + jqxhr.status + "</code>, " + texterror + "</div>");
607                         })
608                         .always(function() {
609                             panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
610                         })
611                 } else {
612                     panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
613                 }
614
615             };
616
617             panel.updater();
618         },
619
620         function(panel, options) {
621             clearTimeout(panel.timerid);
622             panel.active = false;
623             window['storage_alert_' + key] = {};
624         });
625 };
626
627 exports.AddAlertDetail("alert", "Alert", 0, {
628     draw: function(data, target, options, storage) {
629         target.devicedata(data, {
630             id: "alertdetails",
631             fields: [
632                 {
633                     field: 'kismet.alert.header',
634                     title: 'Alert',
635                     liveupdate: false,
636                     help: 'Alert type / identifier; each alert has a unique type name.',
637                 },
638                 {
639                     field: 'kismet.alert.class',
640                     title: 'Class',
641                     liveupdate: false,
642                     help: 'Each alert has a class, such as spoofing, denial of service, known exploit, etc.',
643                 },
644                 {
645                     field: 'kismet.alert.severity',
646                     title: 'Severity',
647                     liveupdate: false,
648                     draw: function(opts) {
649                         return severity_to_string(opts['value']);
650                     },
651                     help: 'General severity of alert; in increasing severity, alerts are categorized as info, low, medium, high, and critical.',
652                 },
653                 {
654                     field: 'kismet.alert.timestamp',
655                     title: 'Time',
656                     liveupdate: false,
657                     draw: function(opts) {
658                         console.log(Math.floor(opts['value']));
659                         return kismet_ui.RenderTrimmedTime({'value': Math.floor(opts['value'])});
660                     }
661                 },
662                 {
663                     field: 'kismet.alert.location/kismet.common.location.geopoint',
664                     filter: function(opts) {
665                         return opts['data']['kismet.alert.location']['kismet.common.location.fix'] >= 2;
666                     },
667                     title: 'Location',
668                     draw: function(opts) {
669                         try {
670                             if (opts['value'][1] == 0 || opts['value'][0] == 0)
671                                 return "<i>Unknown</i>";
672
673                             return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
674                         } catch (error) {
675                             return "<i>Unknown</i>";
676                         }
677                     },
678                     help: 'Location where alert occurred, either as the location of the Kismet server at the time of the alert or as the location of the packet, if per-packet location was available.',
679                 },
680                 {
681                     field: 'kismet.alert.text',
682                     title: 'Alert content',
683                     draw: function(opts) {
684                         return kismet.censorMAC(opts['value']);
685                     },
686                     help: 'Human-readable alert content',
687                 },
688                 {
689                     groupTitle: 'Addresses',
690                     id: 'addresses',
691                     filter: function(opts) {
692                         return opts['data']['kismet.alert.transmitter_mac'] != '00:00:00:00:00:00' ||
693                             opts['data']['kismet.alert.source_mac'] != '00:00:00:00:00:00' ||
694                             opts['data']['kismet.alert.dest_mac'] != '00:00:00:00:00:00';
695                     },
696                     fields: [
697                         {
698                             field: 'kismet.alert.source_mac',
699                             title: 'Source',
700                             filter: function(opts) {
701                                 return opts['value'] !== '00:00:00:00:00:00';
702                             },
703                             draw: function(opts) {
704                                 return kismet.censorMAC(opts['value']);
705                             },
706                             help: 'MAC address of the source of the packet triggering this alert.',
707                         },
708                         {
709                             field: 'kismet.alert.transmitter_mac',
710                             title: 'Transmitter',
711                             filter: function(opts) {
712                                 return opts['value'] !== '00:00:00:00:00:00' &&
713                                     opts['data']['kismet.alert.source_mac'] !== opts['value'];
714                             },
715                             draw: function(opts) {
716                                 return kismet.censorMAC(opts['value']);
717                             },
718                             help: 'MAC address of the transmitter of the packet triggering this alert, if not the same as the source.  On Wi-Fi this is typically the BSSID of the AP',
719                         },
720                         {
721                             field: 'kismet.alert.dest_mac',
722                             title: 'Destination',
723                             filter: function(opts) {
724                                 return opts['value'] !== '00:00:00:00:00:00';
725                             },
726                             draw: function(opts) {
727                                 if (opts['value'] === 'FF:FF:FF:FF:FF:FF')
728                                     return '<i>All / Broadcast</i>'
729                                 return kismet.censorMAC(opts['value']);
730                             },
731                             help: 'MAC address of the destionation the packet triggering this alert.',
732                         },
733                     ]
734                 },
735             ]
736         })
737     }
738 });
739
740 exports.AddAlertDetail("devel", "Dev/Debug Options", 10000, {
741     render: function(data) {
742         return 'Alert JSON: <a href="alerts/by-id/' + data['kismet.alert.hash'] + '/alert.prettyjson" target="_new">link</a><br />';
743     }});
744
745 exports.load_complete = 1;
746
747 return exports;
748
749 });