2 typeof define === "function" ? function (m) { define("kismet-ui-base-js", m); } :
3 typeof exports === "object" ? function (m) { module.exports = m(); } :
4 function(m){ this.kismet_ui_base = m(); }
11 var local_uri_prefix = "";
12 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
13 local_uri_prefix = KISMET_URI_PREFIX;
15 // Flag we're still loading
16 exports.load_complete = 0;
18 /* Fetch the system user */
19 $.get(local_uri_prefix + "system/user_status.json")
20 .done(function(data) {
21 exports.system_user = data['kismet.system.user'];
24 exports.system_user = "[unknown]";
33 href: local_uri_prefix + 'css/kismet.ui.base.css'
36 /* Call from index to set the required ajax parameters universally */
37 exports.ConfigureAjax = function() {
39 beforeSend: function (xhr) {
40 var user = kismet.getStorage('kismet.base.login.username', 'kismet');
41 var pw = kismet.getStorage('kismet.base.login.password', '');
43 xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
47 dataFilter: function(data, type) {
49 var json = JSON.parse(data);
50 var sjson = kismet.sanitizeObject(json);
51 console.log(JSON.stringify(sjson));
52 return JSON.stringify(sjson);
61 exports.ConfigureAjax();
63 var eventbus_ws_listeners = [];
65 exports.eventbus_ws = null;
67 exports.SubscribeEventbus = function(topic, fields, callback) {
73 if (fields.length > 0)
74 sub["fields"] = fields;
76 eventbus_ws_listeners.push(sub);
78 if (exports.eventbus_ws != null && exports.eventbus_ws.readyState == 1) {
80 "SUBSCRIBE": sub["topic"],
84 sub_req["fields"] = sub["fields"]
86 exports.eventbus_ws.send(JSON.stringify(sub_req));
90 exports.OpenEventbusWs = function() {
93 if (document.location.protocol == "https:")
98 var user = kismet.getStorage('kismet.base.login.username', 'kismet');
99 var pw = kismet.getStorage('kismet.base.login.password', '');
101 var host = new URL(document.URL);
103 var ws_url = `${proto}://${host.host}/${KISMET_PROXY_PREFIX}eventbus/events.ws?user=${encodeURIComponent(user)}&password=${encodeURIComponent(pw)}`
105 exports.eventbus_ws = new WebSocket(ws_url);
107 exports.eventbus_ws.onclose = function(event) {
108 console.log("eventbus ws closed");
110 setTimeout(function() { exports.OpenEventbusWs(); }, 500);
113 exports.eventbus_ws.onmessage = function(event) {
115 var json = JSON.parse(event.data);
117 for (var x in json) {
118 for (var sub of eventbus_ws_listeners) {
119 if (sub["topic"] === x) {
120 sub["callback"](json[x]);
129 exports.eventbus_ws.onopen = function(event) {
130 for (var sub of eventbus_ws_listeners) {
132 "SUBSCRIBE": sub["topic"],
136 sub_req["fields"] = sub["fields"]
138 exports.eventbus_ws.send(JSON.stringify(sub_req));
143 exports.SubscribeEventbus("TIMESTAMP", [], function(data) {
144 data = kismet.sanitizeObject(data);
145 kismet.timestamp_sec = data['kismet.system.timestamp.sec'];
146 kismet.timestamp_usec = data['kismet.system.timestamp.usec'];
149 // exports.SubscribeEventbus("MESSAGE", [], function(e) { console.log(e); });
151 /* Define some callback functions for the table */
153 exports.renderLastTime = function(data, type, row, meta) {
154 return (new Date(data * 1000).toString()).substring(4, 25);
157 exports.renderDataSize = function(data, type, row, meta) {
158 if (type === 'display')
159 return kismet.HumanReadableSize(data);
164 exports.renderMac = function(data, type, row, meta) {
165 if (typeof(data) === 'undefined') {
169 return kismet.censorMAC(data);
172 exports.renderSignal = function(data, type, row, meta) {
178 exports.renderChannel = function(data, type, row, meta) {
184 exports.renderPackets = function(data, type, row, meta) {
185 return "<i>Preparing graph</i>";
188 exports.renderUsecTime = function(data, type, row, meta) {
192 var data_sec = data / 1000000;
194 var days = Math.floor(data_sec / 86400);
195 var hours = Math.floor((data_sec / 3600) % 24);
196 var minutes = Math.floor((data_sec / 60) % 60);
197 var seconds = Math.floor(data_sec % 60);
202 ret = ret + days + "d ";
203 if (hours > 0 || days > 0)
204 ret = ret + hours + "h ";
205 if (minutes > 0 || hours > 0 || days > 0)
206 ret = ret + minutes + "m ";
207 ret = ret + seconds + "s";
212 exports.drawPackets = function(dyncolumn, table, row) {
214 var rid = table.column(dyncolumn.name + ':name').index();
215 var match = "td:eq(" + rid + ")";
217 var data = row.data();
219 // Simplify the RRD so that the bars are thicker in the graph, which
220 // I think looks better. We do this with a transform function on the
221 // RRD function, and we take the peak value of each triplet of samples
222 // because it seems to be more stable, visually
224 // We use the aliased field names we extracted from just the minute
225 // component of the per-device packet RRD
227 kismet.RecalcRrdData2(data, kismet.RRD_SECOND,
229 transform: function(data, opt) {
232 var ret = new Array();
234 for (var ri = 0; ri < data.length; ri++) {
235 peak = Math.max(peak, data[ri]);
237 if ((ri % slices) == (slices - 1)) {
247 // Render the sparkline
248 $(match, row.node()).sparkline(simple_rrd,
252 barColor: kismet_theme.sparkline_main,
253 nullColor: kismet_theme.sparkline_main,
254 zeroColor: kismet_theme.sparkline_main,
258 // Define the basic columns
259 kismet_ui.AddDeviceColumn('column_name', {
261 field: 'kismet.device.base.commonname',
262 description: 'Device name',
264 renderfunc: function(d, t, r, m) {
265 d = kismet.censorMAC(d);
266 return kismet.censorString(d);
267 // return kismet.censorMAC(d);
269 var dname = kismet.censorMAC(d);
270 return (dname.length > 24) ? dname.substr(0, 23) + '…' : dname;
275 kismet_ui.AddDeviceColumn('column_type', {
277 field: 'kismet.device.base.type',
278 description: 'Device type',
282 kismet_ui.AddDeviceColumn('column_phy', {
284 field: 'kismet.device.base.phyname',
285 description: 'Capture Phy name',
289 kismet_ui.AddDeviceColumn('column_crypto', {
291 field: 'kismet.device.base.crypt',
292 description: 'Encryption',
294 renderfunc: function(d, t, r, m) {
303 kismet_ui.AddDeviceColumn('column_signal', {
305 field: 'kismet.device.base.signal/kismet.common.signal.last_signal',
306 description: 'Last-seen signal',
308 sClass: "dt-body-right",
309 renderfunc: function(d, t, r, m) {
310 return exports.renderSignal(d, t, r, m);
314 kismet_ui.AddDeviceColumn('column_channel', {
316 field: 'kismet.device.base.channel',
317 description: 'Last-seen channel',
319 sClass: "dt-body-right",
320 renderfunc: function(d, t, r, m) {
323 } else if ('kismet.device.base.frequency' in r &&
324 r['kismet.device.base_frequency'] != 0) {
325 return kismet_ui.GetPhyConvertedChannel(r['kismet.device.base.phyname'], r['kismet.device.base.frequency']);
332 kismet_ui.AddDeviceColumn('column_time', {
334 field: 'kismet.device.base.last_time',
335 description: 'Last-seen time',
336 renderfunc: function(d, t, r, m) {
337 return exports.renderLastTime(d, t, r, m);
345 kismet_ui.AddDeviceColumn('column_first_time', {
346 sTitle: 'First Seen',
347 field: 'kismet.device.base.first_time',
348 description: 'First-seen time',
349 renderfunc: function(d, t, r, m) {
350 return exports.renderLastTime(d, t, r, m);
358 kismet_ui.AddDeviceColumn('column_datasize', {
360 field: 'kismet.device.base.datasize',
361 description: 'Data seen',
363 sClass: "dt-body-right",
365 renderfunc: function(d, t, r, m) {
366 return exports.renderDataSize(d, t, r, m);
370 // Fetch just the last time field, we use the hidden rrd_min_data field to assemble
371 // the rrd. This is a hack to be more efficient and not send the house or day
372 // rrd records along with it.
373 kismet_ui.AddDeviceColumn('column_packet_rrd', {
375 field: ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'packet.rrd.last_time'],
378 description: 'Packet history graph',
379 renderfunc: function(d, t, r, m) {
380 return exports.renderPackets(d, t, r, m);
382 drawfunc: function(d, t, r) {
383 return exports.drawPackets(d, t, r);
389 // Hidden col for packet minute rrd data
390 // We MUST define ONE FIELD and then multiple additional fields are permitted
391 kismet_ui.AddDeviceColumn('column_rrd_minute_hidden', {
392 sTitle: 'packets_rrd_min_data',
394 ['kismet.device.base.packets.rrd/kismet.common.rrd.serial_time', 'kismet.common.rrd.serial_time'],
396 ['kismet.device.base.packets.rrd/kismet.common.rrd.minute_vec', 'kismet.common.rrd.minute_vec'],
397 ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'kismet.common.rrd.last_time'],
399 name: 'packets_rrd_min_data',
406 // Hidden col for key, mappable, we need to be sure to
407 // fetch it so we can use it as an index
408 kismet_ui.AddDeviceColumn('column_device_key_hidden', {
410 field: 'kismet.device.base.key',
417 // HIdden for phy to always turn it on
418 kismet_ui.AddDeviceColumn('column_phy_hidden', {
420 field: 'kismet.device.base.phyname',
427 // Hidden col for mac address, searchable
428 kismet_ui.AddDeviceColumn('column_device_mac_hidden', {
430 field: 'kismet.device.base.macaddr',
437 // Hidden col for mac address, searchable
438 kismet_ui.AddDeviceColumn('column_device_mac', {
440 field: 'kismet.device.base.macaddr',
441 description: 'MAC address',
446 renderfunc: function(d, t, r, m) {
447 return exports.renderMac(d, t, r, m);
451 // Hidden column for computing freq in the absence of channel
452 kismet_ui.AddDeviceColumn('column_frequency_hidden', {
454 field: 'kismet.device.base.frequency',
461 kismet_ui.AddDeviceColumn('column_frequency', {
463 field: 'kismet.device.base.frequency',
464 description: 'Frequency',
472 kismet_ui.AddDeviceColumn('column_manuf', {
474 field: 'kismet.device.base.manuf',
475 description: 'Manufacturer',
481 renderfunc: function(d, t, r, m) {
482 return (d.length > 32) ? d.substr(0, 31) + '…' : d;
487 // Add the (quite complex) device details.
488 // It has a priority of -1000 because we want it to always come first.
490 // There is no filter function because we always have base device
493 // There is no render function because we immediately fill it during draw.
495 // The draw function will populate the kismet devicedata when pinged
496 kismet_ui.AddDeviceDetail("base", "Device Info", -1000, {
497 draw: function(data, target, options, storage) {
498 target.devicedata(data, {
499 "id": "genericDeviceData",
502 field: "kismet.device.base.name",
504 help: "Device name, derived from device characteristics or set as a custom name by the user.",
505 draw: function(opts) {
506 var name = opts['data']['kismet.device.base.username'];
508 if (typeof(name) != 'undefined' && name != "") {
509 name = kismet.censorString(name);
512 if (typeof(name) == 'undefined' || name == "") {
513 name = opts['data']['kismet.device.base.commonname'];
514 name = kismet.censorString(name);
517 if (typeof(name) == 'undefined' || name == "") {
518 name = opts['data']['kismet.device.base.macaddr'];
519 name = kismet.censorMAC(name);
532 success: function(response, newvalue) {
536 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
537 $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_name.cmd", postdata, "json");
543 container.append(nameobj);
546 'class': 'copyuri pseudolink fa fa-copy',
547 'style': 'padding-left: 5px;',
548 'data-clipboard-text': `${name}`,
557 field: "kismet.device.base.tags/notes",
559 help: "Abritrary notes",
560 draw: function(opts) {
563 if ('kismet.device.base.tags' in opts['data'])
564 notes = opts['data']['kismet.device.base.tags']['notes'];
572 'data-type': 'textarea',
574 .html(notes.convertNewlines());
579 success: function(response, newvalue) {
582 "tagvalue": newvalue.escapeSpecialChars(),
584 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
585 $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_tag.cmd", postdata, "json");
595 field: "kismet.device.base.macaddr",
596 title: "MAC Address",
597 help: "Unique per-phy address of the transmitting device, when available. Not all phy types provide MAC addresses, however most do.",
598 draw: function(opts) {
599 var mac = kismet.censorMAC(opts['value']);
604 $('<span>').html(mac)
608 'class': 'copyuri pseudolink fa fa-copy',
609 'style': 'padding-left: 5px;',
610 'data-clipboard-text': `${mac}`,
618 field: "kismet.device.base.manuf",
619 title: "Manufacturer",
620 empty: "<i>Unknown</i>",
621 help: "Manufacturer of the device, derived from the MAC address. Manufacturers are registered with the IEEE and resolved in the files specified in kismet.conf under 'manuf='",
624 field: "kismet.device.base.type",
627 empty: "<i>Unknown</i>"
630 field: "kismet.device.base.first_time",
633 draw: function(opts) {
634 return new Date(opts['value'] * 1000);
638 field: "kismet.device.base.last_time",
641 draw: function(opts) {
642 return new Date(opts['value'] * 1000);
646 field: "group_frequency",
647 groupTitle: "Frequencies",
648 id: "group_frequency",
653 field: "kismet.device.base.channel",
655 empty: "<i>None Advertised</i>",
656 help: "The phy-specific channel of the device, if known. The advertised channel defines a specific, known channel, which is not affected by channel overlap. Not all phy types advertise fixed channels, and not all device types have fixed channels. If an advertised channel is not available, the primary frequency is used.",
659 field: "kismet.device.base.frequency",
660 title: "Main Frequency",
661 help: "The primary frequency of the device, if known. Not all phy types advertise a fixed frequency in packets.",
662 draw: function(opts) {
663 return kismet.HumanReadableFrequency(opts['value']);
668 field: "frequency_map",
671 filter: function(opts) {
673 return (Object.keys(opts['data']['kismet.device.base.freq_khz_map']).length >= 1);
678 render: function(opts) {
681 style: 'width: 80%; height: 250px',
692 draw: function(opts) {
693 var legend = new Array();
694 var data = new Array();
696 for (var fk in opts['data']['kismet.device.base.freq_khz_map']) {
697 legend.push(kismet.HumanReadableFrequency(parseInt(fk)));
698 data.push(opts['data']['kismet.device.base.freq_khz_map'][fk]);
706 backgroundColor: 'rgba(46, 99, 162, 1)',
713 if ('freqchart' in window[storage]) {
714 window[storage].freqchart.data.labels = legend;
715 window[storage].freqchart.data.datasets[0].data = data;
716 window[storage].freqchart.update();
718 window[storage].freqchart =
719 new Chart($('canvas', opts['container']), {
723 maintainAspectRatio: false,
731 text: 'Packet frequency distribution'
737 window[storage].freqchart.update();
744 field: "group_signal_data",
745 groupTitle: "Signal",
746 id: "group_signal_data",
748 filter: function(opts) {
749 var db = kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.last_signal");
759 field: "kismet.device.base.signal/kismet.common.signal.signal_rrd",
761 title: "Monitor Signal",
763 render: function(opts) {
764 return '<div class="monitor pseudolink">Monitor</div>';
766 draw: function(opts) {
767 $('div.monitor', opts['container'])
768 .on('click', function() {
769 exports.DeviceSignalDetails(opts['data']['kismet.device.base.key']);
773 /* RRD - come back to this later
774 render: function(opts) {
775 return '<div class="rrd" id="' + opts['key'] + '" />';
777 draw: function(opts) {
778 var rrdiv = $('div', opts['container']);
780 var rrdata = kismet.RecalcRrdData(opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'], last_devicelist_time, kismet.RRD_MINUTE, opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
782 // We assume the 'best' a signal can usefully be is -20dbm,
783 // that means we're right on top of it.
784 // We can assume that -100dbm is a sane floor value for
785 // the weakest signal.
786 // If a signal is 0 it means we haven't seen it at all so
787 // just ignore that data point
788 // We turn signals into a 'useful' graph by clamping to
789 // -100 and -20 and then scaling it as a positive number.
790 var moddata = new Array();
792 for (var x = 0; x < rrdata.length; x++) {
807 // Reverse (weaker is worse), get as percentage
808 var rs = (80 - d) / 80;
810 moddata.push(100*rs);
813 rrdiv.sparkline(moddata, { type: "bar",
815 barColor: kismet_theme.sparkline_main,
816 nullColor: kismet_theme.sparkline_main,
817 zeroColor: kismet_theme.sparkline_main,
825 field: "kismet.device.base.signal/kismet.common.signal.last_signal",
827 title: "Latest Signal",
828 help: "Most recent signal level seen. Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
829 draw: function(opts) {
830 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
835 field: "kismet.device.base.signal/kismet.common.signal.last_noise",
837 title: "Latest Noise",
838 help: "Most recent noise level seen. Few drivers can report noise levels.",
839 draw: function(opts) {
840 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
845 field: "kismet.device.base.signal/kismet.common.signal.min_signal",
847 title: "Min. Signal",
848 help: "Weakest signal level seen. Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
849 draw: function(opts) {
850 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
856 field: "kismet.device.base.signal/kismet.common.signal.max_signal",
858 title: "Max. Signal",
859 help: "Strongest signal level seen. Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
860 draw: function(opts) {
861 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
866 field: "kismet.device.base.signal/kismet.common.signal.min_noise",
870 help: "Least amount of interference or noise seen. Most capture drivers are not capable of measuring noise levels.",
871 draw: function(opts) {
872 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
876 field: "kismet.device.base.signal/kismet.common.signal.max_noise",
880 help: "Largest amount of interference or noise seen. Most capture drivers are not capable of measuring noise levels.",
881 draw: function(opts) {
882 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
885 { // Pseudo-field of aggregated location, only show when the location is valid
886 field: "kismet.device.base.signal/kismet.common.signal.peak_loc",
888 title: "Peak Location",
889 help: "When a GPS location is available, the peak location is the coordinates at which the strongest signal level was recorded for this device.",
890 filter: function(opts) {
891 return kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.fix") >= 2;
893 draw: function(opts) {
895 kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[1]")) + ", " +
896 kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[0]"));
905 field: "group_packet_counts",
906 groupTitle: "Packets",
907 id: "group_packet_counts",
911 field: "graph_field_overall",
914 render: function(opts) {
917 style: 'width: 80%; height: 250px; padding-bottom: 5px;',
927 draw: function(opts) {
928 var legend = ['LLC/Management', 'Data'];
930 opts['data']['kismet.device.base.packets.llc'],
931 opts['data']['kismet.device.base.packets.data'],
934 'rgba(46, 99, 162, 1)',
935 'rgba(96, 149, 212, 1)',
943 backgroundColor: colors,
949 if ('packetdonut' in window[storage]) {
950 window[storage].packetdonut.data.datasets[0].data = data;
951 window[storage].packetdonut.update();
953 window[storage].packetdonut =
954 new Chart($('canvas', opts['container']), {
959 maintainAspectRatio: false,
973 window[storage].packetdonut.render();
978 field: "kismet.device.base.packets.total",
980 title: "Total Packets",
981 help: "Count of all packets of all types",
984 field: "kismet.device.base.packets.llc",
986 title: "LLC/Management",
987 help: "LLC (Link Layer Control) and Management packets are typically used for controlling and defining wireless networks. Typically they do not carry data.",
990 field: "kismet.device.base.packets.error",
992 title: "Error/Invalid",
993 help: "Error and invalid packets indicate a packet was received and was partially processable, but was damaged or incorrect in some way. Most error packets are dropped completely as it is not possible to associate them with a specific device.",
996 field: "kismet.device.base.packets.data",
999 help: "Data frames carry messages and content for the device.",
1002 field: "kismet.device.base.packets.crypt",
1005 help: "Some data frames can be identified by Kismet as carrying encryption, either by the contents or by packet flags, depending on the phy type",
1008 field: "kismet.device.base.packets.filtered",
1011 help: "Filtered packets are ignored by Kismet",
1014 field: "kismet.device.base.datasize",
1016 title: "Data Transferred",
1017 help: "Amount of data transferred",
1018 draw: function(opts) {
1019 return kismet.HumanReadableSize(opts['value']);
1028 // Location is its own group
1029 groupTitle: "Avg. Location",
1030 // Spoofed field for ID purposes
1031 field: "group_avg_location",
1033 id: "group_avg_location",
1035 // Don't show location if we don't know it
1036 filter: function(opts) {
1037 return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1040 // Fields in subgroup
1043 field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.geopoint",
1045 draw: function(opts) {
1047 if (opts['value'][1] == 0 || opts['value'][0] == 0)
1048 return "<i>Unknown</i>";
1050 return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1052 return "<i>Unknown</i>";
1057 field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.alt",
1059 filter: function(opts) {
1060 return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1062 draw: function(opts) {
1064 return kismet_ui.renderHeightDistance(opts['value']);
1066 return "<i>Unknown</i>";
1077 kismet_ui.AddDeviceDetail("packets", "Packet Graphs", 10, {
1078 render: function(data) {
1079 // Make 3 divs for s, m, h RRD
1081 '<b>Packet Rates</b><br /><br />' +
1082 'Packets per second (last minute)<br /><div /><br />' +
1083 'Packets per minute (last hour)<br /><div /><br />' +
1084 'Packets per hour (last day)<br /><div />';
1086 if ('kismet.device.base.datasize.rrd' in data)
1087 ret += '<br /><b>Data</b><br /><br />' +
1088 'Data per second (last minute)<br /><div /><br />' +
1089 'Data per minute (last hour)<br /><div /><br />' +
1090 'Data per hour (last day)<br /><div />';
1094 draw: function(data, target) {
1095 var m = $('div:eq(0)', target);
1096 var h = $('div:eq(1)', target);
1097 var d = $('div:eq(2)', target);
1099 var dm = $('div:eq(3)', target);
1100 var dh = $('div:eq(4)', target);
1101 var dd = $('div:eq(5)', target);
1107 if (('kismet.device.base.packets.rrd' in data)) {
1108 mdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_SECOND);
1109 hdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_MINUTE);
1110 ddata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_HOUR);
1112 m.sparkline(mdata, { type: "bar",
1114 barColor: kismet_theme.sparkline_main,
1115 nullColor: kismet_theme.sparkline_main,
1116 zeroColor: kismet_theme.sparkline_main,
1121 barColor: kismet_theme.sparkline_main,
1122 nullColor: kismet_theme.sparkline_main,
1123 zeroColor: kismet_theme.sparkline_main,
1128 barColor: kismet_theme.sparkline_main,
1129 nullColor: kismet_theme.sparkline_main,
1130 zeroColor: kismet_theme.sparkline_main,
1133 m.html("<i>No packet data available</i>");
1134 h.html("<i>No packet data available</i>");
1135 d.html("<i>No packet data available</i>");
1139 if ('kismet.device.base.datasize.rrd' in data) {
1140 var dmdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_SECOND);
1141 var dhdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_MINUTE);
1142 var dddata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_HOUR);
1144 dm.sparkline(dmdata,
1147 barColor: kismet_theme.sparkline_main,
1148 nullColor: kismet_theme.sparkline_main,
1149 zeroColor: kismet_theme.sparkline_main,
1151 dh.sparkline(dhdata,
1154 barColor: kismet_theme.sparkline_main,
1155 nullColor: kismet_theme.sparkline_main,
1156 zeroColor: kismet_theme.sparkline_main,
1158 dd.sparkline(dddata,
1161 barColor: kismet_theme.sparkline_main,
1162 nullColor: kismet_theme.sparkline_main,
1163 zeroColor: kismet_theme.sparkline_main,
1170 kismet_ui.AddDeviceDetail("seenby", "Seen By", 900, {
1171 filter: function(data) {
1172 return (Object.keys(data['kismet.device.base.seenby']).length > 0);
1174 draw: function(data, target, options, storage) {
1175 target.devicedata(data, {
1176 id: "seenbyDeviceData",
1180 field: "kismet.device.base.seenby",
1183 iterateTitle: function(opts) {
1184 var this_uuid = opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1185 $.get(`${local_uri_prefix}datasource/by-uuid/${this_uuid}/source.json`)
1186 .done(function(dsdata) {
1187 dsdata = kismet.sanitizeObject(dsdata);
1188 opts['title'].html(`${dsdata['kismet.datasource.name']} (${dsdata['kismet.datasource.capture_interface']}) ${dsdata['kismet.datasource.uuid']}`);
1190 return opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1194 field: "kismet.common.seenby.uuid",
1196 empty: "<i>None</i>"
1199 field: "kismet.common.seenby.first_time",
1200 title: "First Seen",
1201 draw: kismet_ui.RenderTrimmedTime,
1204 field: "kismet.common.seenby.last_time",
1206 draw: kismet_ui.RenderTrimmedTime,
1214 kismet_ui.AddDeviceDetail("devel", "Dev/Debug Options", 10000, {
1215 render: function(data) {
1216 return 'Device JSON: <a href="devices/by-key/' + data['kismet.device.base.key'] + '/device.prettyjson" target="_new">link</a><br />';
1219 /* Sidebar: Memory monitor
1221 * The memory monitor looks at system_status and plots the amount of
1222 * ram vs number of tracked devices from the RRD
1224 kismet_ui_sidebar.AddSidebarItem({
1225 id: 'memory_sidebar',
1226 listTitle: '<i class="fa fa-tasks"></i> Memory Monitor',
1227 clickCallback: function() {
1228 exports.MemoryMonitor();
1233 kismet_ui_sidebar.AddSidebarItem({
1236 listTitle: '<i class="fa fa-download"></i> Download Pcap-NG',
1237 clickCallback: function() {
1238 location.href = "datasource/pcap/all_sources.pcapng";
1243 var memoryupdate_tid;
1244 var memory_panel = null;
1245 var memory_chart = null;
1247 exports.MemoryMonitor = function() {
1248 var w = $(window).width() * 0.75;
1249 var h = $(window).height() * 0.5;
1252 if ($(window).width() < 450 || $(window).height() < 450) {
1253 w = $(window).width() - 5;
1254 h = $(window).height() - 5;
1258 memory_chart = null;
1262 'style': 'width: 100%; height: 100%;'
1266 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1283 'id': 'k-mm-canvas',
1284 'style': 'k-mm-canvas'
1288 memory_panel = $.jsPanel({
1290 headerTitle: '<i class="fa fa-tasks" /> Memory use',
1292 controls: 'closeonly',
1293 iconfont: 'jsglyph',
1296 onclosed: function() {
1297 clearTimeout(memoryupdate_tid);
1309 memorydisplay_refresh();
1312 function memorydisplay_refresh() {
1313 clearTimeout(memoryupdate_tid);
1315 if (memory_panel == null)
1318 if (memory_panel.is(':hidden'))
1321 $.get(local_uri_prefix + "system/status.json")
1322 .done(function(data) {
1323 // Common rrd type and source field
1324 var rrdtype = kismet.RRD_MINUTE;
1325 var rrddata = 'kismet.common.rrd.hour_vec';
1327 // Common point titles
1328 var pointtitles = new Array();
1330 for (var x = 60; x > 0; x--) {
1332 pointtitles.push(x + 'm');
1334 pointtitles.push(' ');
1339 kismet.RecalcRrdData2(data['kismet.system.memory.rrd'], rrdtype);
1341 for (var p in mem_linedata) {
1342 mem_linedata[p] = Math.round(mem_linedata[p] / 1024);
1346 kismet.RecalcRrdData2(data['kismet.system.devices.rrd'], rrdtype);
1348 $('#k_mm_devs', memory_panel.content).html(`${dev_linedata[dev_linedata.length - 1]} devices`);
1349 $('#k_mm_ram', memory_panel.content).html(`${mem_linedata[mem_linedata.length - 1]} MB`);
1351 if (memory_chart == null) {
1354 label: 'Memory (MB)',
1356 // yAxisID: 'mem-axis',
1357 borderColor: 'black',
1358 backgroundColor: 'transparent',
1364 // yAxisID: 'dev-axis',
1365 borderColor: 'blue',
1366 backgroundColor: 'rgba(100, 100, 255, 0.33)',
1371 var canvas = $('#k-mm-canvas', memory_panel.content);
1373 memory_chart = new Chart(canvas, {
1377 maintainAspectRatio: false,
1399 labels: pointtitles,
1405 memory_chart.data.datasets[0].data = mem_linedata;
1406 memory_chart.data.datasets[1].data = dev_linedata;
1407 // memory_chart.data.datasets = datasets;
1408 memory_chart.data.labels = pointtitles;
1409 memory_chart.update();
1412 .always(function() {
1413 memoryupdate_tid = setTimeout(memorydisplay_refresh, 5000);
1418 /* Sidebar: Packet queue display
1420 * Packet queue display graphs the amount of packets in the queue, the amount dropped,
1421 * the # of duplicates, and so on
1423 kismet_ui_sidebar.AddSidebarItem({
1424 id: 'packetqueue_sidebar',
1425 listTitle: '<i class="fa fa-area-chart"></i> Packet Rates',
1426 clickCallback: function() {
1427 exports.PacketQueueMonitor();
1431 var packetqueueupdate_tid;
1432 var packetqueue_panel = null;
1434 exports.PacketQueueMonitor = function() {
1435 var w = $(window).width() * 0.75;
1436 var h = $(window).height() * 0.5;
1439 if ($(window).width() < 450 || $(window).height() < 450) {
1440 w = $(window).width() - 5;
1441 h = $(window).height() - 5;
1446 $('<div class="k-pqm-contentdiv">')
1448 $('<div id="pqm-tabs" class="tabs-min">')
1451 packetqueue_panel = $.jsPanel({
1453 headerTitle: '<i class="fa fa-area-chart" /> Packet Rates',
1455 controls: 'closeonly',
1456 iconfont: 'jsglyph',
1459 onclosed: function() {
1460 clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1472 packetqueue_panel.packetqueue_chart = null;
1473 packetqueue_panel.datasource_chart = null;
1475 var f_pqm_packetqueue = function(div) {
1476 packetqueue_panel.pq_content = div;
1479 var f_pqm_ds = function(div) {
1480 packetqueue_panel.ds_content = div;
1483 kismet_ui_tabpane.AddTab({
1485 tabTitle: 'Processing Queue',
1486 createCallback: f_pqm_packetqueue,
1490 kismet_ui_tabpane.AddTab({
1491 id: 'datasources-graph',
1492 tabTitle: 'Per Datasource',
1493 createCallback: f_pqm_ds,
1497 kismet_ui_tabpane.MakeTabPane($('#pqm-tabs', content), 'pqm-tabs');
1499 packetqueuedisplay_refresh();
1500 datasourcepackets_refresh();
1503 function packetqueuedisplay_refresh() {
1504 if (packetqueue_panel == null)
1507 clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1509 if (packetqueue_panel.is(':hidden'))
1512 $.get(local_uri_prefix + "packetchain/packet_stats.json")
1513 .done(function(data) {
1514 // Common rrd type and source field
1515 var rrdtype = kismet.RRD_MINUTE;
1517 // Common point titles
1518 var pointtitles = new Array();
1520 for (var x = 60; x > 0; x--) {
1522 pointtitles.push(x + 'm');
1524 pointtitles.push(' ');
1529 kismet.RecalcRrdData2(data['kismet.packetchain.peak_packets_rrd'], rrdtype);
1531 kismet.RecalcRrdData2(data['kismet.packetchain.packets_rrd'], rrdtype);
1532 var queue_linedata =
1533 kismet.RecalcRrdData2(data['kismet.packetchain.queued_packets_rrd'], rrdtype);
1535 kismet.RecalcRrdData2(data['kismet.packetchain.dropped_packets_rrd'], rrdtype);
1537 kismet.RecalcRrdData2(data['kismet.packetchain.dupe_packets_rrd'], rrdtype);
1538 var processing_linedata =
1539 kismet.RecalcRrdData2(data['kismet.packetchain.processed_packets_rrd'], rrdtype);
1545 borderColor: 'orange',
1546 backgroundColor: 'transparent',
1547 data: processing_linedata,
1548 pointStyle: 'cross',
1551 label: 'Incoming packets (peak)',
1553 borderColor: kismet_theme.graphBasicColor,
1554 backgroundColor: kismet_theme.graphBasicBackgroundColor,
1555 data: peak_linedata,
1558 label: 'Incoming packets (1 min avg)',
1560 borderColor: 'purple',
1561 backgroundColor: 'transparent',
1562 data: rate_linedata,
1568 borderColor: 'blue',
1569 backgroundColor: 'transparent',
1570 data: queue_linedata,
1571 pointStyle: 'cross',
1574 label: 'Dropped / error packets',
1577 backgroundColor: 'transparent',
1578 data: drop_linedata,
1582 label: 'Duplicates',
1584 borderColor: 'green',
1585 backgroundColor: 'transparent',
1586 data: dupe_linedata,
1587 pointStyle: 'triangle',
1591 if (packetqueue_panel.packetqueue_chart == null) {
1592 packetqueue_panel.pq_content.append(
1597 "class": "k-mm-canvas",
1601 var canvas = $('#pq-canvas', packetqueue_panel.pq_content);
1603 packetqueue_panel.packetqueue_chart = new Chart(canvas, {
1607 maintainAspectRatio: false,
1621 labels: pointtitles,
1627 packetqueue_panel.packetqueue_chart.data.datasets = datasets;
1628 packetqueue_panel.packetqueue_chart.data.labels = pointtitles;
1629 packetqueue_panel.packetqueue_chart.update(0);
1632 .always(function() {
1633 packetqueue_panel.packetqueueupdate_tid = setTimeout(packetqueuedisplay_refresh, 5000);
1637 function datasourcepackets_refresh() {
1638 if (packetqueue_panel == null)
1641 clearTimeout(packetqueue_panel.datasourceupdate_tid);
1643 if (packetqueue_panel.is(':hidden'))
1646 $.get(local_uri_prefix + "datasource/all_sources.json")
1647 .done(function(data) {
1651 // Common point titles
1652 var pointtitles = new Array();
1654 var rval = $('#pq_ds_range', packetqueue_panel.ds_content).val();
1655 var range = kismet.RRD_SECOND;
1658 range = kismet.RRD_MINUTE;
1660 range = kismet.RRD_HOUR;
1662 if (range == kismet.RRD_SECOND || range == kismet.RRD_MINUTE) {
1663 for (var x = 60; x > 0; x--) {
1665 if (range == kismet.RRD_SECOND)
1666 pointtitles.push(x + 's');
1668 pointtitles.push(x + 'm');
1670 pointtitles.push(' ');
1674 for (var x = 23; x > 0; x--) {
1675 pointtitles.push(x + 'h');
1679 for (var source of data) {
1680 var color = parseInt(255 * (num / data.length))
1684 if ($('#pq_ds_type', packetqueue_panel.ds_content).val() == "bps")
1686 kismet.RecalcRrdData2(source['kismet.datasource.packets_datasize_rrd'],
1689 transform: function(data, opt) {
1700 kismet.RecalcRrdData2(source['kismet.datasource.packets_rrd'], range);
1703 "label": source['kismet.datasource.name'],
1704 "borderColor": `hsl(${color}, 100%, 50%)`,
1713 if (packetqueue_panel.datasource_chart == null) {
1714 packetqueue_panel.ds_content.append(
1716 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1725 "selected": "selected",
1731 }).text("Data (kB)")
1736 "id": "pq_ds_range",
1741 "selected": "selected",
1742 }).text("Past Minute")
1747 }).text("Past Hour")
1760 "class": "k-mm-canvas",
1764 packetqueue_panel.datasource_chart =
1765 new Chart($('#dsg-canvas', packetqueue_panel.ds_content), {
1769 "maintainAspectRatio": false,
1776 "beginAtZero": true,
1783 "labels": pointtitles,
1784 "datasets": datasets,
1788 packetqueue_panel.datasource_chart.data.datasets = datasets;
1789 packetqueue_panel.datasource_chart.data.labels = pointtitles;
1790 packetqueue_panel.datasource_chart.update(0);
1793 .always(function() {
1794 packetqueue_panel.datasourceupdate_tid = setTimeout(datasourcepackets_refresh, 1000);
1800 kismet_ui_settings.AddSettingsPane({
1802 listTitle: "GPS Status",
1803 create: function(elem) {
1814 .html("GPS Display")
1830 .append($('<div>', { class: 'spacer' }).html(" "))
1845 .append($('<div>', { class: 'spacer' }).html(" "))
1858 .html("Icon and Text")
1863 $('#form', elem).on('change', function() {
1864 kismet_ui_settings.SettingsModified();
1867 if (kismet.getStorage('kismet.ui.gps.icon', 'True') === 'True') {
1868 if (kismet.getStorage('kismet.ui.gps.text', 'True') === 'True') {
1869 $('#gps_both', elem).attr('checked', 'checked');
1871 $('#gps_icon', elem).attr('checked', 'checked');
1874 $('#gps_text', elem).attr('checked', 'checked');
1877 $('#set_gps', elem).controlgroup();
1879 save: function(elem) {
1880 var val = $("input[name='gps_status']:checked", elem).val();
1882 if (val === "both") {
1883 kismet.putStorage('kismet.ui.gps.text', 'True');
1884 kismet.putStorage('kismet.ui.gps.icon', 'True');
1885 } else if (val === "text") {
1886 kismet.putStorage('kismet.ui.gps.text', 'True');
1887 kismet.putStorage('kismet.ui.gps.icon', 'False');
1888 } else if (val === "icon") {
1889 kismet.putStorage('kismet.ui.gps.icon', 'True');
1890 kismet.putStorage('kismet.ui.gps.text', 'False');
1895 kismet_ui_settings.AddSettingsPane({
1896 id: 'base_units_measurements',
1897 listTitle: 'Units & Measurements',
1898 create: function(elem) {
1935 for: 'dst_imperial',
1975 for: 'spd_imperial',
1989 .html("Temperature")
2001 for: 'temp_celsius',
2008 id: 'temp_fahrenheit',
2010 value: 'fahrenheit',
2015 for: 'temp_fahrenheit',
2022 $('#form', elem).on('change', function() {
2023 kismet_ui_settings.SettingsModified();
2026 if (kismet.getStorage('kismet.base.unit.distance', 'metric') === 'metric') {
2027 $('#dst_metric', elem).attr('checked', 'checked');
2029 $('#dst_imperial', elem).attr('checked', 'checked');
2032 if (kismet.getStorage('kismet.base.unit.speed', 'metric') === 'metric') {
2033 $('#spd_metric', elem).attr('checked', 'checked');
2035 $('#spd_imperial', elem).attr('checked', 'checked');
2038 if (kismet.getStorage('kismet.base.unit.temp', 'celsius') === 'celsius') {
2039 $('#temp_celsius', elem).attr('checked', 'checked');
2041 $('#temp_fahrenheit', elem).attr('checked', 'checked');
2044 $('#set_distance', elem).controlgroup();
2045 $('#set_speed', elem).controlgroup();
2046 $('#set_temp', elem).controlgroup();
2049 save: function(elem) {
2050 var dist = $("input[name='distance']:checked", elem).val();
2051 kismet.putStorage('kismet.base.unit.distance', dist);
2052 var spd = $("input[name='speed']:checked", elem).val();
2053 kismet.putStorage('kismet.base.unit.speed', spd);
2054 var tmp = $("input[name='temp']:checked", elem).val();
2055 kismet.putStorage('kismet.base.unit.temp', tmp);
2061 kismet_ui_settings.AddSettingsPane({
2063 listTitle: 'Plugins',
2064 create: function(elem) {
2065 elem.append($('<i>').html('Loading plugin data...'));
2067 $.get(local_uri_prefix + "plugins/all_plugins.json")
2068 .done(function(data) {
2071 if (data.length == 0) {
2072 elem.append($('<i>').html('No plugins loaded...'));
2075 for (var pi in data) {
2078 var sharedlib = $('<p>');
2080 if (pl['kismet.plugin.shared_object'].length > 0) {
2081 sharedlib.html("Native code from " + pl['kismet.plugin.shared_object']);
2083 sharedlib.html("No native code");
2088 class: 'k-b-s-plugin-title',
2092 class: 'k-b-s-plugin-title',
2094 .html(pl['kismet.plugin.name'])
2098 .html(pl['kismet.plugin.version'])
2103 class: 'k-b-s-plugin-content',
2107 .html(pl['kismet.plugin.description'])
2111 .html(pl['kismet.plugin.author'])
2118 save: function(elem) {
2124 kismet_ui_settings.AddSettingsPane({
2125 id: 'base_login_password',
2126 listTitle: 'Login & Password',
2127 create: function(elem) {
2138 .html('Server Login')
2142 .html('Kismet requires a username and password for functionality which changes the server, such as adding interfaces or changing configuration, or accessing some types of data.')
2146 .html('The Kismet password is stored in <code>~/.kismet/kismet_httpd.conf</code> in the home directory of the user running Kismet. You will need this password to configure data sources, download pcap and other logs, or change server-side settings.<br>This server is running as <code>' + exports.system_user + '</code>, so the password can be found in <code>~' + exports.system_user + '/.kismet/kismet_httpd.conf</code>')
2150 .html('If you are a guest on this server you may continue without entering an admin password, but you will not be able to perform some actions or view some data.')
2156 $('<span style="display: inline-block; width: 8em;">')
2157 .html('User name: ')
2170 $('<span style="display: inline-block; width: 8em;">')
2183 style: 'padding-left: 5px',
2188 class: 'fa fa-refresh fa-spin',
2201 $('#form', elem).on('change', function() {
2202 kismet_ui_settings.SettingsModified();
2205 var checker_cb = function() {
2206 // Cancel any pending timer
2207 if (pw_check_tid > -1)
2208 clearTimeout(pw_check_tid);
2210 var checkerdiv = $('#pwsuccessdiv', elem);
2211 var checker = $('#pwsuccess', checkerdiv);
2212 var checkertext = $('#pwsuccesstext', checkerdiv);
2214 checker.removeClass('fa-exclamation-circle');
2215 checker.removeClass('fa-check-square');
2217 checker.addClass('fa-spin');
2218 checker.addClass('fa-refresh');
2219 checkertext.text(" Checking...");
2223 // Set a timer for a second from now to call the actual check
2224 // in case the user is still typing
2225 pw_check_tid = setTimeout(function() {
2226 exports.LoginCheck(function(success) {
2228 checker.removeClass('fa-check-square');
2229 checker.removeClass('fa-spin');
2230 checker.removeClass('fa-refresh');
2231 checker.addClass('fa-exclamation-circle');
2232 checkertext.text(" Invalid login");
2234 checker.removeClass('fa-exclamation-circle');
2235 checker.removeClass('fa-spin');
2236 checker.removeClass('fa-refresh');
2237 checker.addClass('fa-check-square');
2238 checkertext.text("");
2240 }, $('#user', elem).val(), $('#password', elem).val());
2244 var pw_check_tid = -1;
2245 jQuery('#password', elem).on('input propertychange paste', function() {
2246 kismet_ui_settings.SettingsModified();
2249 jQuery('#user', elem).on('input propertychange paste', function() {
2250 kismet_ui_settings.SettingsModified();
2254 $('#user', elem).val(kismet.getStorage('kismet.base.login.username', 'kismet'));
2255 $('#password', elem).val(kismet.getStorage('kismet.base.login.password', 'kismet'));
2257 if ($('#user', elem).val() === 'kismet' &&
2258 $('#password', elem).val() === 'kismet') {
2259 $('#defaultwarning').show();
2262 $('fs_login', elem).controlgroup();
2264 // Check the current pw
2267 save: function(elem) {
2268 kismet.putStorage('kismet.base.login.username', $('#user', elem).val());
2269 kismet.putStorage('kismet.base.login.password', $('#password', elem).val());
2273 function show_role_help(role) {
2274 var rolehelp = `Unknown role ${role}; this could be assigned as a custom role for a Kismet plugin.`;
2276 if (role === "admin")
2277 rolehelp = "The admin role is assigned to the primary web interface, external API plugins which automatically request API access, and other privileged instances. The admin role has access to all endpoints.";
2278 else if (role === "readonly")
2279 rolehelp = "The readonly role has access to any endpoint which does not modify data. It can not issue commands to the Kismet server, configure sources, or alter devices. The readonly role is well suited for external data gathering from a Kismet server.";
2280 else if (role === "datasource")
2281 rolehelp = "The datasource role allows remote capture over websockets. This role only has access to the remote capture datasource endpoint.";
2282 else if (role === "scanreport")
2283 rolehelp = "The scanreport role allows device scan reports. This role only has access to the scan report endpoint."
2284 else if (role === "ADSB")
2285 rolehelp = "The ADSB role allows access to the combined and device-specific ADSB feeds."
2286 else if (role === "__explain__") {
2287 rolehelp = "<p>Kismet uses a basic role system to restrict access to API endpoints. The default roles are:";
2288 rolehelp += "<p>"admin" which has access to all API endpoints.";
2289 rolehelp += "<p>"readonly" which only has access to endpoints which do not alter devices or change the configuration of the server";
2290 rolehelp += "<p>"datasource" which is used for websockets based remote capture and may not access any other endpoints";
2291 rolehelp += "<p>"scanreport" which is used for reporting scanning-mode devices";
2292 rolehelp += "<p>"ADSB" which is used for sharing ADSB feeds";
2293 rolehelp += "<p>Plugins or other code may define other roles.";
2295 role = "Kismet API Roles";
2298 var h = $(window).height() / 4;
2299 var w = $(window).width() / 2;
2302 w = $(window).width() - 5;
2305 h = $(window).height() - 5;
2309 headerTitle: `Role: ${role}`,
2311 controls: 'closeonly',
2312 iconfont: 'jsglyph',
2314 contentSize: `${w} auto`,
2316 content: `<div style="padding: 10px;"><h3>${role}</h3><p>${rolehelp}`,
2325 function delete_role(rolename, elem) {
2326 var deltd = $('.deltd', elem);
2330 'style': 'background-color: #DDAAAA',
2332 .html(`Delete role "${rolename}"`)
2334 .on('click', function() {
2339 var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2341 $.post(local_uri_prefix + "auth/apikey/revoke.cmd", postdata)
2342 .done(function(data) {
2343 var delt = elem.parent();
2347 if ($('tr', delt).length == 1) {
2356 .html("<i>No API keys defined...</i>")
2364 deltd.append(delbt);
2368 function make_role_help_closure(role) {
2369 return function() { show_role_help(role); };
2372 function make_role_delete_closure(rolename, elem) {
2373 return function() { delete_role(rolename, elem); };
2376 kismet_ui_settings.AddSettingsPane({
2377 id: 'base_api_logins',
2378 listTitle: "API Keys",
2379 create: function(elem) {
2380 elem.append($("p").html("Fetching API data..."));
2382 $.get(local_uri_prefix + "auth/apikey/list.json")
2383 .done(function(data) {
2384 data = kismet.sanitizeObject(data);
2387 var tb = $('<table>', {
2388 'class': 'apitable',
2389 'id': 'apikeytable',
2397 'style': 'width: 16em;',
2403 'style': 'width: 8em;',
2409 'style': 'width: 30em;',
2419 if (data.length == 0) {
2428 .html("<i>No API keys defined...</i>")
2433 for (var user of data) {
2434 var name = user['kismet.httpd.auth.name'];
2435 var role = user['kismet.httpd.auth.role'];
2439 if ('kismet.httpd.auth.token' in user) {
2440 key = user['kismet.httpd.auth.token'];
2442 key = "<i>Viewing auth tokens is disabled in the Kismet configuration.</i>";
2452 $('<td>').html(name)
2455 $('<td>').html(role)
2458 'class': 'pseudolink fa fa-question-circle',
2459 'style': 'padding-left: 5px;',
2461 .on('click', make_role_help_closure(role))
2472 'id': name.replace(" ", "_"),
2477 'class': 'copyuri pseudolink fa fa-copy',
2478 'style': 'padding-left: 5px;',
2479 'data-clipboard-target': `#${name.replace(" ", "_")}`,
2489 'class': 'pseudolink fa fa-trash',
2491 .on('click', make_role_delete_closure(name, tr))
2508 'id': 'addapikeybutton',
2510 }).html(`<i class="fa fa-plus"> Create API Key`)
2514 'for': 'addapiname',
2520 'name': 'addapiname',
2528 'for': 'addapirole',
2534 'name': 'addapirole',
2539 'value': 'readonly',
2545 'value': 'datasource',
2546 }).html("datasource")
2550 'value': 'scanreport',
2551 }).html("scanreport")
2566 }).html("<i>custom</i>")
2571 'name': 'addapiroleother',
2572 'id': 'addapiroleother',
2579 'class': 'pseudolink fa fa-question-circle',
2580 'style': 'padding-left: 5px;',
2582 .on('click', make_role_help_closure("__explain__"))
2586 'id': 'addapierror',
2587 'style': 'color: red;'
2592 $('#addapikeybutton', adddiv)
2594 .on('click', function() {
2595 var name = $('#addapiname').val();
2596 var role_select = $('#addapirole option:selected').text();
2597 var role_input = $('#addapiroleother').val();
2599 if (name.length == 0) {
2600 $('#addapierror').show().html("Missing name.");
2604 if (role_select === "custom" && role_input.length == 0) {
2605 $('#addapierror').show().html("Missing custom role.");
2609 $('#addapierror').hide();
2611 var role = role_select;
2613 if (role_select === "custom")
2622 var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2624 $.post(local_uri_prefix + "auth/apikey/generate.cmd", postdata)
2625 .fail(function(response) {
2626 var rt = kismet.sanitizeObject(response.responseText);
2627 $('#addapierror').show().html(`Failed to add API key: ${rt}`);
2629 .done(function(data) {
2630 var key = kismet.sanitizeObject(data);
2639 $('<td>').html(name)
2642 $('<td>').html(role)
2645 'class': 'pseudolink fa fa-question-circle',
2646 'style': 'padding-left: 5px;',
2648 .on('click', make_role_help_closure(role))
2659 'id': name.replace(" ", "_"),
2664 'class': 'copyuri pseudolink fa fa-copy',
2665 'style': 'padding-left: 5px;',
2666 'data-clipboard-target': `#${name.replace(" ", "_")}`,
2676 'class': 'pseudolink fa fa-trash',
2678 .on('click', make_role_delete_closure(name, tr))
2682 $('#apikeytable').append(tr);
2684 $('#addapiname').val('');
2685 $("#addapirole").prop("selectedIndex", 0);
2686 $("#addapirole").show();
2687 $("#addapiroleother").val('').hide();
2691 $('#addapirole', adddiv).on('change', function(e) {
2692 var val = $("#addapirole option:selected" ).text();
2694 if (val === "custom") {
2696 $('#addapiroleother').show();
2701 elem.append(adddiv);
2703 new ClipboardJS('.copyuri');
2706 save: function(elem) {
2713 /* Add the messages and channels tabs */
2714 kismet_ui_tabpane.AddTab({
2716 tabTitle: 'Messages',
2717 createCallback: function(div) {
2723 kismet_ui_tabpane.AddTab({
2725 tabTitle: 'Channels',
2727 createCallback: function(div) {
2733 kismet_ui_tabpane.AddTab({
2735 tabTitle: 'Devices',
2737 createCallback: function(div) {
2741 class: 'fixeddt stripe hover nowrap pageResize',
2747 kismet_ui.CreateDeviceTable($('#devices', div));
2753 exports.DeviceSignalDetails = function(key) {
2754 var w = $(window).width() * 0.75;
2755 var h = $(window).height() * 0.5;
2757 var devsignal_chart = null;
2759 var devsignal_tid = -1;
2763 class: 'k-dsd-container'
2771 class: 'k-dsd-title'
2777 class: 'k-dsd-table'
2786 .html("Last Signal:")
2794 class: 'k-dsd-lastsignal',
2799 class: 'fa k-dsd-arrow k-dsd-arrow-down',
2812 .html("Min Signal:")
2817 class: 'k-dsd-minsignal',
2829 .html("Max Signal:")
2834 class: 'k-dsd-maxsignal',
2843 class: 'k-dsd-graph'
2848 class: 'k-dsd-canvas'
2853 var devsignal_panel = $.jsPanel({
2854 id: 'devsignal' + key,
2855 headerTitle: '<i class="fa fa-signal" /> Signal',
2857 iconfont: 'jsglyph',
2860 onclosed: function() {
2861 clearTimeout(devsignal_tid);
2873 var emptyminute = new Array();
2874 for (var x = 0; x < 60; x++) {
2875 emptyminute.push(0);
2878 devsignal_tid = devsignal_refresh(key, devsignal_panel,
2879 devsignal_chart, devsignal_tid, 0, emptyminute);
2882 function devsignal_refresh(key, devsignal_panel, devsignal_chart,
2883 devsignal_tid, lastsignal, fakerrd) {
2884 clearTimeout(devsignal_tid);
2886 if (devsignal_panel == null)
2889 if (devsignal_panel.is(':hidden'))
2892 var signal = lastsignal;
2894 $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
2895 .done(function(data) {
2896 var title = '<i class="fa fa-signal" /> Signal ' +
2897 kismet.censorMAC(data['kismet.device.base.macaddr']) + ' ' +
2898 kismet.censorMAC(data['kismet.device.base.name']);
2899 devsignal_panel.headerTitle(title);
2901 var sigicon = $('.k-dsd-arrow', devsignal_panel.content);
2903 sigicon.removeClass('k-dsd-arrow-up');
2904 sigicon.removeClass('k-dsd-arrow-down');
2905 sigicon.removeClass('fa-arrow-up');
2906 sigicon.removeClass('fa-arrow-down');
2908 signal = data['kismet.device.base.signal']['kismet.common.signal.last_signal'];
2910 if (signal < lastsignal) {
2911 sigicon.addClass('k-dsd-arrow-down');
2912 sigicon.addClass('fa-arrow-down');
2915 sigicon.addClass('k-dsd-arrow-up');
2916 sigicon.addClass('fa-arrow-up');
2921 if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "dbm")
2923 else if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "rssi")
2926 $('.k-dsd-lastsignal', devsignal_panel.content)
2927 .text(signal + typestr);
2929 $('.k-dsd-minsignal', devsignal_panel.content)
2930 .text(data['kismet.device.base.signal']['kismet.common.signal.min_signal'] + typestr);
2932 $('.k-dsd-maxsignal', devsignal_panel.content)
2933 .text(data['kismet.device.base.signal']['kismet.common.signal.max_signal'] + typestr);
2935 // Common point titles
2936 var pointtitles = new Array();
2938 for (var x = 60; x > 0; x--) {
2940 pointtitles.push(x + 's');
2942 pointtitles.push(' ');
2948 var rrdata = kismet.RecalcRrdData(
2949 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2950 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2952 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
2954 // We assume the 'best' a signal can usefully be is -20dbm,
2955 // that means we're right on top of it.
2956 // We can assume that -100dbm is a sane floor value for
2957 // the weakest signal.
2958 // If a signal is 0 it means we haven't seen it at all so
2959 // just ignore that data point
2960 // We turn signals into a 'useful' graph by clamping to
2961 // -100 and -20 and then scaling it as a positive number.
2962 var moddata = new Array();
2964 for (var x = 0; x < rrdata.length; x++) {
2978 // Normalize to 0-80
2981 // Reverse (weaker is worse), get as percentage
2982 var rs = (80 - d) / 80;
2984 moddata.push(100*rs);
2988 var msignal = signal;
2992 } else if (msignal < -100) {
2994 } else if (msignal > -20) {
2998 msignal = (msignal * -1) - 20;
2999 var rs = (80 - msignal) / 80;
3001 fakerrd.push(100 * rs);
3003 fakerrd.splice(0, 1);
3005 var moddata = fakerrd;
3009 label: 'Signal (%)',
3011 borderColor: 'blue',
3012 backgroundColor: 'rgba(100, 100, 255, 0.83)',
3017 if (devsignal_chart == null) {
3018 var canvas = $('#k-dsd-canvas', devsignal_panel.content);
3020 devsignal_chart = new Chart(canvas, {
3024 maintainAspectRatio: false,
3036 labels: pointtitles,
3041 devsignal_chart.data.datasets[0].data = moddata;
3042 devsignal_chart.update();
3047 .always(function() {
3048 devsignal_tid = setTimeout(function() {
3049 devsignal_refresh(key, devsignal_panel,
3050 devsignal_chart, devsignal_tid, signal, fakerrd);
3055 exports.login_error = false;
3056 exports.login_pending = false;
3058 exports.ProvisionedPasswordCheck = function(cb) {
3060 url: local_uri_prefix + "session/check_setup_ok",
3062 error: function(jqXHR, textStatus, errorThrown) {
3066 success: function(data, textStatus, jqHXR) {
3072 exports.LoginCheck = function(cb, user, pw) {
3073 user = user || kismet.getStorage('kismet.base.login.username', 'kismet');
3074 pw = pw || kismet.getStorage('kismet.base.login.password', '');
3077 url: local_uri_prefix + "session/check_login",
3079 beforeSend: function (xhr) {
3080 xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
3084 withCredentials: false
3087 error: function(jqXHR, textStatus, errorThrown) {
3091 success: function(data, textStatus, jqXHR) {
3098 exports.FirstLoginCheck = function(first_login_done_cb) {
3099 var loginpanel = null;
3100 var username_deferred = $.Deferred();
3102 $.get(local_uri_prefix + "system/user_status.json")
3103 .done(function(data) {
3104 username_deferred.resolve(data['kismet.system.user']);
3107 username_deferred.resolve("[unknown]");
3110 var username = "[incomplete]";
3111 $.when(username_deferred).done(function(v) {
3115 var required_login_content =
3117 style: 'padding: 10px;'
3121 .html('Kismet requires a login to access data.')
3125 .html('Your login is stored in in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>; This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3136 $('<span style="display: inline-block; width: 8em;">')
3137 .html('User name: ')
3150 $('<span style="display: inline-block; width: 8em;">')
3162 style: 'padding-top: 10px;'
3166 class: 'k-wl-button-close',
3175 style: 'padding-left: 5px',
3180 class: 'fa fa-refresh fa-spin',
3194 var login_checker_cb = function(content) {
3195 var savebutton = $('#login_button', content);
3197 var checkerdiv = $('#pwsuccessdiv', content);
3198 var checker = $('#pwsuccess', checkerdiv);
3199 var checkertext = $('#pwsuccesstext', checkerdiv);
3201 checker.removeClass('fa-exclamation-circle');
3202 checker.removeClass('fa-check-square');
3204 checker.addClass('fa-spin');
3205 checker.addClass('fa-refresh');
3206 checkertext.text(" Checking...");
3210 exports.LoginCheck(function(success) {
3212 checker.removeClass('fa-check-square');
3213 checker.removeClass('fa-spin');
3214 checker.removeClass('fa-refresh');
3215 checker.addClass('fa-exclamation-circle');
3216 checkertext.text(" Invalid login");
3218 /* Save the login info */
3219 kismet.putStorage('kismet.base.login.username', $('#req_user', content).val());
3220 kismet.putStorage('kismet.base.login.password', $('#req_password', content).val());
3224 /* Call the primary callback */
3225 first_login_done_cb();
3227 }, $('#req_user', content).val(), $('#req_password', content).val());
3230 $('#login_button', required_login_content)
3232 .on('click', function() {
3233 login_checker_cb(required_login_content);
3236 $('fs_login', required_login_content).controlgroup();
3238 var set_password_content =
3240 style: 'padding: 10px;'
3244 .html('To finish setting up Kismet, you need to configure a login.')
3248 .html('This login will be stored in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>; This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3263 $('<span style="display: inline-block; width: 8em;">')
3264 .html('User name: ')
3277 $('<span style="display: inline-block; width: 8em;">')
3291 $('<span style="display: inline-block; width: 8em;">')
3304 style: 'padding-left: 5px',
3309 class: 'fa fa-refresh fa-spin',
3323 style: 'padding-top: 10px;'
3327 class: 'k-wl-button-close',
3328 id: 'save_password',
3335 var checker_cb = function(content) {
3336 var savebutton = $('#save_password', content);
3337 var checkerdiv = $('#pwsuccessdiv', content);
3338 var checker = $('#pwsuccess', checkerdiv);
3339 var checkertext = $('#pwsuccesstext', checkerdiv);
3341 savebutton.button("disable");
3343 checker.removeClass('fa-exclamation-circle');
3344 checker.removeClass('fa-check-square');
3346 checker.addClass('fa-spin');
3347 checker.addClass('fa-refresh');
3348 checkertext.text("");
3352 if ($('#user', content).val().length == 0) {
3353 checker.removeClass('fa-check-square');
3354 checker.removeClass('fa-spin');
3355 checker.removeClass('fa-refresh');
3356 checker.addClass('fa-exclamation-circle');
3357 checkertext.text(" Username required");
3358 savebutton.button("disable");
3362 if ($('#password', content).val().length == 0) {
3363 checker.removeClass('fa-check-square');
3364 checker.removeClass('fa-spin');
3365 checker.removeClass('fa-refresh');
3366 checker.addClass('fa-exclamation-circle');
3367 checkertext.text(" Password required");
3368 savebutton.button("disable");
3372 if ($('#password', content).val() != $('#password2', content).val()) {
3373 checker.removeClass('fa-check-square');
3374 checker.removeClass('fa-spin');
3375 checker.removeClass('fa-refresh');
3376 checker.addClass('fa-exclamation-circle');
3377 checkertext.text(" Passwords don't match");
3378 savebutton.button("disable");
3382 checker.removeClass('fa-exclamation-circle');
3383 checker.removeClass('fa-spin');
3384 checker.removeClass('fa-refresh');
3385 checker.addClass('fa-check-square');
3386 checkertext.text("");
3387 savebutton.button("enable");
3391 jQuery('#user', set_password_content).on('input propertychange paste', function() {
3394 jQuery('#password', set_password_content).on('input propertychange paste', function() {
3397 jQuery('#password2', set_password_content).on('input propertychange paste', function() {
3401 $('#save_password', set_password_content)
3403 .on('click', function() {
3404 kismet.putStorage('kismet.base.login.username', $('#user', set_password_content).val());
3405 kismet.putStorage('kismet.base.login.password', $('#password', set_password_content).val());
3408 "username": $('#user', set_password_content).val(),
3409 "password": $('#password', set_password_content).val()
3414 url: local_uri_prefix + "session/set_password",
3416 error: function(jqXHR, textStatus, errorThrown) {
3417 alert("Could not set login, check your kismet server logs.")
3423 /* Call the primary callback to load the UI */
3424 first_login_done_cb();
3426 /* Check for the first-time running */
3427 exports.FirstTimeCheck();
3430 $('fs_login', set_password_content).controlgroup();
3432 checker_cb(set_password_content);
3434 var w = ($(window).width() / 2) - 5;
3436 w = $(window).width() - 5;
3439 var content = set_password_content;
3441 exports.ProvisionedPasswordCheck(function(code) {
3442 if (code == 200 || code == 406) {
3443 /* Initial setup has been complete, now check the login itself */
3444 exports.LoginCheck(function(success) {
3446 loginpanel = $.jsPanel({
3448 headerTitle: '<i class="fa fa-exclamation-triangle"></i>Login Required',
3450 controls: 'closeonly',
3451 iconfont: 'jsglyph',
3453 contentSize: w + " auto",
3455 content: required_login_content,
3460 /* Otherwise we're all good, continue to loading the main UI via the callback */
3461 first_login_done_cb();
3464 } else if (code == 500) {
3465 loginpanel = $.jsPanel({
3467 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Set Login',
3469 controls: 'closeonly',
3470 iconfont: 'jsglyph',
3472 contentSize: w + " auto",
3474 content: set_password_content,
3479 loginpanel = $.jsPanel({
3481 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Error connecting',
3483 controls: 'closeonly',
3484 iconfont: 'jsglyph',
3486 contentSize: w + " auto",
3488 content: "Error connecting to Kismet and checking provisioning; try reloading the page!",
3499 exports.FirstTimeCheck = function() {
3500 var welcomepanel = null;
3501 if (kismet.getStorage('kismet.base.seen_welcome', false) == false) {
3504 style: 'padding: 10px;'
3513 .html('This is the first time you\'ve used this Kismet server in this browser.')
3517 .html('Kismet stores local settings in the HTML5 storage of your browser.')
3521 .html('You should configure your preferences and login settings in the settings panel!')
3527 class: 'k-w-button-settings'
3531 .on('click', function() {
3532 welcomepanel.close();
3533 kismet_ui_settings.ShowSettings();
3538 class: 'k-w-button-close',
3539 style: 'position: absolute; right: 5px;',
3543 .on('click', function() {
3544 welcomepanel.close();
3550 welcomepanel = $.jsPanel({
3551 id: "welcome-alert",
3552 headerTitle: '<i class="fa fa-power-off"></i> Welcome',
3554 controls: 'closeonly',
3555 iconfont: 'jsglyph',
3557 contentSize: "auto auto",
3562 kismet.putStorage('kismet.base.seen_welcome', true);
3570 // Keep trying to fetch the servername until we're able to
3571 var servername_tid = -1;
3572 exports.FetchServerName = function(cb) {
3573 $.get(local_uri_prefix + "system/status.json")
3574 .done(function (d) {
3575 d = kismet.sanitizeObject(d);
3576 cb(d['kismet.system.server_name']);
3579 servername_tid = setTimeout(function () {
3580 exports.FetchServerName(cb);
3585 /* Highlight active devices */
3586 kismet_ui.AddDeviceRowHighlight({
3588 description: "Device has been active in the past 10 seconds",
3590 defaultcolor: "#cee1ff",
3591 defaultenable: false,
3593 'kismet.device.base.last_time'
3595 selector: function(data) {
3596 var ts = data['kismet.device.base.last_time'];
3598 return (kismet.timestamp_sec - ts < 10);
3602 /* Bodycam hardware of various types */
3603 kismet_ui.AddDeviceRowHighlight({
3605 description: "Body camera devices",
3607 defaultcolor: "#0089FF",
3608 defaultenable: true,
3610 'kismet.device.base.macaddr',
3611 'kismet.device.base.commonname',
3613 selector: function(data) {
3615 if (data['kismet.device.base.macaddr'].match("^00:25:DF") != null)
3617 if (data['kismet.device.base.macaddr'].match("^12:20:13") != null)
3619 if (data['kismet.device.base.common_name'].match("^Axon-X") != null)
3629 // We're done loading
3630 exports.load_complete = 1;