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