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,
253 nullColor: '#000000',
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 return kismet.censorMAC(d);
267 var dname = kismet.censorMAC(d);
268 return (dname.length > 24) ? dname.substr(0, 23) + '…' : dname;
273 kismet_ui.AddDeviceColumn('column_type', {
275 field: 'kismet.device.base.type',
276 description: 'Device type',
280 kismet_ui.AddDeviceColumn('column_phy', {
282 field: 'kismet.device.base.phyname',
283 description: 'Capture Phy name',
287 kismet_ui.AddDeviceColumn('column_crypto', {
289 field: 'kismet.device.base.crypt',
290 description: 'Encryption',
292 renderfunc: function(d, t, r, m) {
301 kismet_ui.AddDeviceColumn('column_signal', {
303 field: 'kismet.device.base.signal/kismet.common.signal.last_signal',
304 description: 'Last-seen signal',
306 sClass: "dt-body-right",
307 renderfunc: function(d, t, r, m) {
308 return exports.renderSignal(d, t, r, m);
312 kismet_ui.AddDeviceColumn('column_channel', {
314 field: 'kismet.device.base.channel',
315 description: 'Last-seen channel',
317 sClass: "dt-body-right",
318 renderfunc: function(d, t, r, m) {
321 } else if ('kismet.device.base.frequency' in r &&
322 r['kismet.device.base_frequency'] != 0) {
323 return kismet_ui.GetPhyConvertedChannel(r['kismet.device.base.phyname'], r['kismet.device.base.frequency']);
330 kismet_ui.AddDeviceColumn('column_time', {
332 field: 'kismet.device.base.last_time',
333 description: 'Last-seen time',
334 renderfunc: function(d, t, r, m) {
335 return exports.renderLastTime(d, t, r, m);
343 kismet_ui.AddDeviceColumn('column_first_time', {
344 sTitle: 'First Seen',
345 field: 'kismet.device.base.first_time',
346 description: 'First-seen time',
347 renderfunc: function(d, t, r, m) {
348 return exports.renderLastTime(d, t, r, m);
356 kismet_ui.AddDeviceColumn('column_datasize', {
358 field: 'kismet.device.base.datasize',
359 description: 'Data seen',
361 sClass: "dt-body-right",
363 renderfunc: function(d, t, r, m) {
364 return exports.renderDataSize(d, t, r, m);
368 // Fetch just the last time field, we use the hidden rrd_min_data field to assemble
369 // the rrd. This is a hack to be more efficient and not send the house or day
370 // rrd records along with it.
371 kismet_ui.AddDeviceColumn('column_packet_rrd', {
373 field: ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'packet.rrd.last_time'],
376 description: 'Packet history graph',
377 renderfunc: function(d, t, r, m) {
378 return exports.renderPackets(d, t, r, m);
380 drawfunc: function(d, t, r) {
381 return exports.drawPackets(d, t, r);
387 // Hidden col for packet minute rrd data
388 // We MUST define ONE FIELD and then multiple additional fields are permitted
389 kismet_ui.AddDeviceColumn('column_rrd_minute_hidden', {
390 sTitle: 'packets_rrd_min_data',
392 ['kismet.device.base.packets.rrd/kismet.common.rrd.serial_time', 'kismet.common.rrd.serial_time'],
394 ['kismet.device.base.packets.rrd/kismet.common.rrd.minute_vec', 'kismet.common.rrd.minute_vec'],
395 ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'kismet.common.rrd.last_time'],
397 name: 'packets_rrd_min_data',
404 // Hidden col for key, mappable, we need to be sure to
405 // fetch it so we can use it as an index
406 kismet_ui.AddDeviceColumn('column_device_key_hidden', {
408 field: 'kismet.device.base.key',
415 // HIdden for phy to always turn it on
416 kismet_ui.AddDeviceColumn('column_phy_hidden', {
418 field: 'kismet.device.base.phyname',
425 // Hidden col for mac address, searchable
426 kismet_ui.AddDeviceColumn('column_device_mac_hidden', {
428 field: 'kismet.device.base.macaddr',
435 // Hidden col for mac address, searchable
436 kismet_ui.AddDeviceColumn('column_device_mac', {
438 field: 'kismet.device.base.macaddr',
439 description: 'MAC address',
444 renderfunc: function(d, t, r, m) {
445 return exports.renderMac(d, t, r, m);
449 // Hidden column for computing freq in the absence of channel
450 kismet_ui.AddDeviceColumn('column_frequency_hidden', {
452 field: 'kismet.device.base.frequency',
459 kismet_ui.AddDeviceColumn('column_frequency', {
461 field: 'kismet.device.base.frequency',
462 description: 'Frequency',
470 kismet_ui.AddDeviceColumn('column_manuf', {
472 field: 'kismet.device.base.manuf',
473 description: 'Manufacturer',
479 renderfunc: function(d, t, r, m) {
480 return (d.length > 32) ? d.substr(0, 31) + '…' : d;
485 // Add the (quite complex) device details.
486 // It has a priority of -1000 because we want it to always come first.
488 // There is no filter function because we always have base device
491 // There is no render function because we immediately fill it during draw.
493 // The draw function will populate the kismet devicedata when pinged
494 kismet_ui.AddDeviceDetail("base", "Device Info", -1000, {
495 draw: function(data, target, options, storage) {
496 target.devicedata(data, {
497 "id": "genericDeviceData",
500 field: "kismet.device.base.name",
502 help: "Device name, derived from device characteristics or set as a custom name by the user.",
503 draw: function(opts) {
504 var name = opts['data']['kismet.device.base.username'];
506 if (typeof(name) == 'undefined' || name == "")
507 name = opts['data']['kismet.device.base.commonname'];
509 if (typeof(name) == 'undefined' || name == "")
510 name = opts['data']['kismet.device.base.macaddr'];
512 name = kismet.censorMAC(name);
523 success: function(response, newvalue) {
527 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
528 $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_name.cmd", postdata, "json");
534 container.append(nameobj);
537 'class': 'copyuri pseudolink fa fa-copy',
538 'style': 'padding-left: 5px;',
539 'data-clipboard-text': `${name}`,
548 field: "kismet.device.base.tags/notes",
550 help: "Abritrary notes",
551 draw: function(opts) {
554 if ('kismet.device.base.tags' in opts['data'])
555 notes = opts['data']['kismet.device.base.tags']['notes'];
563 'data-type': 'textarea',
565 .html(notes.convertNewlines());
570 success: function(response, newvalue) {
573 "tagvalue": newvalue.escapeSpecialChars(),
575 var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
576 $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_tag.cmd", postdata, "json");
586 field: "kismet.device.base.macaddr",
587 title: "MAC Address",
588 help: "Unique per-phy address of the transmitting device, when available. Not all phy types provide MAC addresses, however most do.",
589 draw: function(opts) {
590 var mac = kismet.censorMAC(opts['value']);
595 $('<span>').html(mac)
599 'class': 'copyuri pseudolink fa fa-copy',
600 'style': 'padding-left: 5px;',
601 'data-clipboard-text': `${mac}`,
609 field: "kismet.device.base.manuf",
610 title: "Manufacturer",
611 empty: "<i>Unknown</i>",
612 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='",
615 field: "kismet.device.base.type",
618 empty: "<i>Unknown</i>"
621 field: "kismet.device.base.first_time",
624 draw: function(opts) {
625 return new Date(opts['value'] * 1000);
629 field: "kismet.device.base.last_time",
632 draw: function(opts) {
633 return new Date(opts['value'] * 1000);
637 field: "group_frequency",
638 groupTitle: "Frequencies",
639 id: "group_frequency",
644 field: "kismet.device.base.channel",
646 empty: "<i>None Advertised</i>",
647 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.",
650 field: "kismet.device.base.frequency",
651 title: "Main Frequency",
652 help: "The primary frequency of the device, if known. Not all phy types advertise a fixed frequency in packets.",
653 draw: function(opts) {
654 return kismet.HumanReadableFrequency(opts['value']);
659 field: "frequency_map",
662 filter: function(opts) {
664 return (Object.keys(opts['data']['kismet.device.base.freq_khz_map']).length >= 1);
669 render: function(opts) {
672 style: 'width: 80%; height: 250px',
683 draw: function(opts) {
684 var legend = new Array();
685 var data = new Array();
687 for (var fk in opts['data']['kismet.device.base.freq_khz_map']) {
688 legend.push(kismet.HumanReadableFrequency(parseInt(fk)));
689 data.push(opts['data']['kismet.device.base.freq_khz_map'][fk]);
697 backgroundColor: 'rgba(46, 99, 162, 1)',
704 if ('freqchart' in window[storage]) {
705 window[storage].freqchart.data.labels = legend;
706 window[storage].freqchart.data.datasets[0].data = data;
707 window[storage].freqchart.update();
709 window[storage].freqchart =
710 new Chart($('canvas', opts['container']), {
714 maintainAspectRatio: false,
721 text: 'Packet frequency distribution'
726 window[storage].freqchart.update();
733 field: "group_signal_data",
734 groupTitle: "Signal",
735 id: "group_signal_data",
737 filter: function(opts) {
738 var db = kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.last_signal");
748 field: "kismet.device.base.signal/kismet.common.signal.signal_rrd",
750 title: "Monitor Signal",
752 render: function(opts) {
753 return '<div class="monitor pseudolink">Monitor</div>';
755 draw: function(opts) {
756 $('div.monitor', opts['container'])
757 .on('click', function() {
758 exports.DeviceSignalDetails(opts['data']['kismet.device.base.key']);
762 /* RRD - come back to this later
763 render: function(opts) {
764 return '<div class="rrd" id="' + opts['key'] + '" />';
766 draw: function(opts) {
767 var rrdiv = $('div', opts['container']);
769 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'], {});
771 // We assume the 'best' a signal can usefully be is -20dbm,
772 // that means we're right on top of it.
773 // We can assume that -100dbm is a sane floor value for
774 // the weakest signal.
775 // If a signal is 0 it means we haven't seen it at all so
776 // just ignore that data point
777 // We turn signals into a 'useful' graph by clamping to
778 // -100 and -20 and then scaling it as a positive number.
779 var moddata = new Array();
781 for (var x = 0; x < rrdata.length; x++) {
796 // Reverse (weaker is worse), get as percentage
797 var rs = (80 - d) / 80;
799 moddata.push(100*rs);
802 rrdiv.sparkline(moddata, { type: "bar",
805 nullColor: '#000000',
814 field: "kismet.device.base.signal/kismet.common.signal.last_signal",
816 title: "Latest Signal",
817 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.",
818 draw: function(opts) {
819 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
824 field: "kismet.device.base.signal/kismet.common.signal.last_noise",
826 title: "Latest Noise",
827 help: "Most recent noise level seen. Few drivers can report noise levels.",
828 draw: function(opts) {
829 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
834 field: "kismet.device.base.signal/kismet.common.signal.min_signal",
836 title: "Min. Signal",
837 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.",
838 draw: function(opts) {
839 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
845 field: "kismet.device.base.signal/kismet.common.signal.max_signal",
847 title: "Max. Signal",
848 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.",
849 draw: function(opts) {
850 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
855 field: "kismet.device.base.signal/kismet.common.signal.min_noise",
859 help: "Least amount of interference or noise seen. Most capture drivers are not capable of measuring noise levels.",
860 draw: function(opts) {
861 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
865 field: "kismet.device.base.signal/kismet.common.signal.max_noise",
869 help: "Largest amount of interference or noise seen. Most capture drivers are not capable of measuring noise levels.",
870 draw: function(opts) {
871 return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
874 { // Pseudo-field of aggregated location, only show when the location is valid
875 field: "kismet.device.base.signal/kismet.common.signal.peak_loc",
877 title: "Peak Location",
878 help: "When a GPS location is available, the peak location is the coordinates at which the strongest signal level was recorded for this device.",
879 filter: function(opts) {
880 return kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.fix") >= 2;
882 draw: function(opts) {
884 kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[1]")) + ", " +
885 kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[0]"));
894 field: "group_packet_counts",
895 groupTitle: "Packets",
896 id: "group_packet_counts",
900 field: "graph_field_overall",
903 render: function(opts) {
906 style: 'width: 80%; height: 250px; padding-bottom: 5px;',
916 draw: function(opts) {
917 var legend = ['LLC/Management', 'Data'];
919 opts['data']['kismet.device.base.packets.llc'],
920 opts['data']['kismet.device.base.packets.data'],
923 'rgba(46, 99, 162, 1)',
924 'rgba(96, 149, 212, 1)',
932 backgroundColor: colors,
938 if ('packetdonut' in window[storage]) {
939 window[storage].packetdonut.data.datasets[0].data = data;
940 window[storage].packetdonut.update();
942 window[storage].packetdonut =
943 new Chart($('canvas', opts['container']), {
948 maintainAspectRatio: false,
962 window[storage].packetdonut.render();
967 field: "kismet.device.base.packets.total",
969 title: "Total Packets",
970 help: "Count of all packets of all types",
973 field: "kismet.device.base.packets.llc",
975 title: "LLC/Management",
976 help: "LLC (Link Layer Control) and Management packets are typically used for controlling and defining wireless networks. Typically they do not carry data.",
979 field: "kismet.device.base.packets.error",
981 title: "Error/Invalid",
982 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.",
985 field: "kismet.device.base.packets.data",
988 help: "Data frames carry messages and content for the device.",
991 field: "kismet.device.base.packets.crypt",
994 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",
997 field: "kismet.device.base.packets.filtered",
1000 help: "Filtered packets are ignored by Kismet",
1003 field: "kismet.device.base.datasize",
1005 title: "Data Transferred",
1006 help: "Amount of data transferred",
1007 draw: function(opts) {
1008 return kismet.HumanReadableSize(opts['value']);
1017 // Location is its own group
1018 groupTitle: "Avg. Location",
1019 // Spoofed field for ID purposes
1020 field: "group_avg_location",
1022 id: "group_avg_location",
1024 // Don't show location if we don't know it
1025 filter: function(opts) {
1026 return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1029 // Fields in subgroup
1032 field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.geopoint",
1034 draw: function(opts) {
1036 if (opts['value'][1] == 0 || opts['value'][0] == 0)
1037 return "<i>Unknown</i>";
1039 return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1041 return "<i>Unknown</i>";
1046 field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.alt",
1048 filter: function(opts) {
1049 return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1051 draw: function(opts) {
1053 return kismet_ui.renderHeightDistance(opts['value']);
1055 return "<i>Unknown</i>";
1066 kismet_ui.AddDeviceDetail("packets", "Packet Graphs", 10, {
1067 render: function(data) {
1068 // Make 3 divs for s, m, h RRD
1070 '<b>Packet Rates</b><br /><br />' +
1071 'Packets per second (last minute)<br /><div /><br />' +
1072 'Packets per minute (last hour)<br /><div /><br />' +
1073 'Packets per hour (last day)<br /><div />';
1075 if ('kismet.device.base.datasize.rrd' in data)
1076 ret += '<br /><b>Data</b><br /><br />' +
1077 'Data per second (last minute)<br /><div /><br />' +
1078 'Data per minute (last hour)<br /><div /><br />' +
1079 'Data per hour (last day)<br /><div />';
1083 draw: function(data, target) {
1084 var m = $('div:eq(0)', target);
1085 var h = $('div:eq(1)', target);
1086 var d = $('div:eq(2)', target);
1088 var dm = $('div:eq(3)', target);
1089 var dh = $('div:eq(4)', target);
1090 var dd = $('div:eq(5)', target);
1096 if (('kismet.device.base.packets.rrd' in data)) {
1097 mdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_SECOND);
1098 hdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_MINUTE);
1099 ddata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_HOUR);
1101 m.sparkline(mdata, { type: "bar",
1103 barColor: '#000000',
1104 nullColor: '#000000',
1105 zeroColor: '#000000'
1110 barColor: '#000000',
1111 nullColor: '#000000',
1112 zeroColor: '#000000'
1117 barColor: '#000000',
1118 nullColor: '#000000',
1119 zeroColor: '#000000'
1122 m.html("<i>No packet data available</i>");
1123 h.html("<i>No packet data available</i>");
1124 d.html("<i>No packet data available</i>");
1128 if ('kismet.device.base.datasize.rrd' in data) {
1129 var dmdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_SECOND);
1130 var dhdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_MINUTE);
1131 var dddata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_HOUR);
1133 dm.sparkline(dmdata,
1136 barColor: '#000000',
1137 nullColor: '#000000',
1138 zeroColor: '#000000'
1140 dh.sparkline(dhdata,
1143 barColor: '#000000',
1144 nullColor: '#000000',
1145 zeroColor: '#000000'
1147 dd.sparkline(dddata,
1150 barColor: '#000000',
1151 nullColor: '#000000',
1152 zeroColor: '#000000'
1159 kismet_ui.AddDeviceDetail("seenby", "Seen By", 900, {
1160 filter: function(data) {
1161 return (Object.keys(data['kismet.device.base.seenby']).length > 0);
1163 draw: function(data, target, options, storage) {
1164 target.devicedata(data, {
1165 id: "seenbyDeviceData",
1169 field: "kismet.device.base.seenby",
1172 iterateTitle: function(opts) {
1173 var this_uuid = opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1174 $.get(`${local_uri_prefix}datasource/by-uuid/${this_uuid}/source.json`)
1175 .done(function(dsdata) {
1176 dsdata = kismet.sanitizeObject(dsdata);
1177 opts['title'].html(`${dsdata['kismet.datasource.name']} (${dsdata['kismet.datasource.capture_interface']}) ${dsdata['kismet.datasource.uuid']}`);
1179 return opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1183 field: "kismet.common.seenby.uuid",
1185 empty: "<i>None</i>"
1188 field: "kismet.common.seenby.first_time",
1189 title: "First Seen",
1190 draw: kismet_ui.RenderTrimmedTime,
1193 field: "kismet.common.seenby.last_time",
1195 draw: kismet_ui.RenderTrimmedTime,
1203 kismet_ui.AddDeviceDetail("devel", "Dev/Debug Options", 10000, {
1204 render: function(data) {
1205 return 'Device JSON: <a href="devices/by-key/' + data['kismet.device.base.key'] + '/device.prettyjson" target="_new">link</a><br />';
1208 /* Sidebar: Memory monitor
1210 * The memory monitor looks at system_status and plots the amount of
1211 * ram vs number of tracked devices from the RRD
1213 kismet_ui_sidebar.AddSidebarItem({
1214 id: 'memory_sidebar',
1215 listTitle: '<i class="fa fa-tasks"></i> Memory Monitor',
1216 clickCallback: function() {
1217 exports.MemoryMonitor();
1222 kismet_ui_sidebar.AddSidebarItem({
1225 listTitle: '<i class="fa fa-download"></i> Download Pcap-NG',
1226 clickCallback: function() {
1227 location.href = "datasource/pcap/all_sources.pcapng";
1232 var memoryupdate_tid;
1233 var memory_panel = null;
1234 var memory_chart = null;
1236 exports.MemoryMonitor = function() {
1237 var w = $(window).width() * 0.75;
1238 var h = $(window).height() * 0.5;
1241 if ($(window).width() < 450 || $(window).height() < 450) {
1242 w = $(window).width() - 5;
1243 h = $(window).height() - 5;
1247 memory_chart = null;
1251 'style': 'width: 100%; height: 100%;'
1255 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1272 'id': 'k-mm-canvas',
1273 'style': 'k-mm-canvas'
1277 memory_panel = $.jsPanel({
1279 headerTitle: '<i class="fa fa-tasks" /> Memory use',
1281 controls: 'closeonly',
1282 iconfont: 'jsglyph',
1285 onclosed: function() {
1286 clearTimeout(memoryupdate_tid);
1298 memorydisplay_refresh();
1301 function memorydisplay_refresh() {
1302 clearTimeout(memoryupdate_tid);
1304 if (memory_panel == null)
1307 if (memory_panel.is(':hidden'))
1310 $.get(local_uri_prefix + "system/status.json")
1311 .done(function(data) {
1312 // Common rrd type and source field
1313 var rrdtype = kismet.RRD_MINUTE;
1314 var rrddata = 'kismet.common.rrd.hour_vec';
1316 // Common point titles
1317 var pointtitles = new Array();
1319 for (var x = 60; x > 0; x--) {
1321 pointtitles.push(x + 'm');
1323 pointtitles.push(' ');
1328 kismet.RecalcRrdData2(data['kismet.system.memory.rrd'], rrdtype);
1330 for (var p in mem_linedata) {
1331 mem_linedata[p] = Math.round(mem_linedata[p] / 1024);
1335 kismet.RecalcRrdData2(data['kismet.system.devices.rrd'], rrdtype);
1337 $('#k_mm_devs', memory_panel.content).html(`${dev_linedata[dev_linedata.length - 1]} devices`);
1338 $('#k_mm_ram', memory_panel.content).html(`${mem_linedata[mem_linedata.length - 1]} MB`);
1342 label: 'Memory (MB)',
1344 // yAxisID: 'mem-axis',
1345 borderColor: 'black',
1346 backgroundColor: 'transparent',
1352 // yAxisID: 'dev-axis',
1353 borderColor: 'blue',
1354 backgroundColor: 'rgba(100, 100, 255, 0.33)',
1359 if (memory_chart == null) {
1360 var canvas = $('#k-mm-canvas', memory_panel.content);
1362 memory_chart = new Chart(canvas, {
1366 maintainAspectRatio: false,
1388 labels: pointtitles,
1394 memory_chart.data.datasets = datasets;
1395 memory_chart.data.labels = pointtitles;
1396 memory_chart.update(0);
1399 .always(function() {
1400 memoryupdate_tid = setTimeout(memorydisplay_refresh, 5000);
1405 /* Sidebar: Packet queue display
1407 * Packet queue display graphs the amount of packets in the queue, the amount dropped,
1408 * the # of duplicates, and so on
1410 kismet_ui_sidebar.AddSidebarItem({
1411 id: 'packetqueue_sidebar',
1412 listTitle: '<i class="fa fa-area-chart"></i> Packet Rates',
1413 clickCallback: function() {
1414 exports.PacketQueueMonitor();
1418 var packetqueueupdate_tid;
1419 var packetqueue_panel = null;
1421 exports.PacketQueueMonitor = function() {
1422 var w = $(window).width() * 0.75;
1423 var h = $(window).height() * 0.5;
1426 if ($(window).width() < 450 || $(window).height() < 450) {
1427 w = $(window).width() - 5;
1428 h = $(window).height() - 5;
1433 $('<div class="k-pqm-contentdiv">')
1435 $('<div id="pqm-tabs" class="tabs-min">')
1438 packetqueue_panel = $.jsPanel({
1440 headerTitle: '<i class="fa fa-area-chart" /> Packet Rates',
1442 controls: 'closeonly',
1443 iconfont: 'jsglyph',
1446 onclosed: function() {
1447 clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1459 packetqueue_panel.packetqueue_chart = null;
1460 packetqueue_panel.datasource_chart = null;
1462 var f_pqm_packetqueue = function(div) {
1463 packetqueue_panel.pq_content = div;
1466 var f_pqm_ds = function(div) {
1467 packetqueue_panel.ds_content = div;
1470 kismet_ui_tabpane.AddTab({
1472 tabTitle: 'Processing Queue',
1473 createCallback: f_pqm_packetqueue,
1477 kismet_ui_tabpane.AddTab({
1478 id: 'datasources-graph',
1479 tabTitle: 'Per Datasource',
1480 createCallback: f_pqm_ds,
1484 kismet_ui_tabpane.MakeTabPane($('#pqm-tabs', content), 'pqm-tabs');
1486 packetqueuedisplay_refresh();
1487 datasourcepackets_refresh();
1490 function packetqueuedisplay_refresh() {
1491 if (packetqueue_panel == null)
1494 clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1496 if (packetqueue_panel.is(':hidden'))
1499 $.get(local_uri_prefix + "packetchain/packet_stats.json")
1500 .done(function(data) {
1501 // Common rrd type and source field
1502 var rrdtype = kismet.RRD_MINUTE;
1504 // Common point titles
1505 var pointtitles = new Array();
1507 for (var x = 60; x > 0; x--) {
1509 pointtitles.push(x + 'm');
1511 pointtitles.push(' ');
1516 kismet.RecalcRrdData2(data['kismet.packetchain.peak_packets_rrd'], rrdtype);
1518 kismet.RecalcRrdData2(data['kismet.packetchain.packets_rrd'], rrdtype);
1519 var queue_linedata =
1520 kismet.RecalcRrdData2(data['kismet.packetchain.queued_packets_rrd'], rrdtype);
1522 kismet.RecalcRrdData2(data['kismet.packetchain.dropped_packets_rrd'], rrdtype);
1524 kismet.RecalcRrdData2(data['kismet.packetchain.dupe_packets_rrd'], rrdtype);
1525 var processing_linedata =
1526 kismet.RecalcRrdData2(data['kismet.packetchain.processed_packets_rrd'], rrdtype);
1532 borderColor: 'orange',
1533 backgroundColor: 'transparent',
1534 data: processing_linedata,
1535 pointStyle: 'cross',
1538 label: 'Incoming packets (peak)',
1540 borderColor: 'black',
1541 backgroundColor: 'rgba(100, 100, 100, 0.33)',
1542 data: peak_linedata,
1545 label: 'Incoming packets (1 min avg)',
1547 borderColor: 'purple',
1548 backgroundColor: 'transparent',
1549 data: rate_linedata,
1555 borderColor: 'blue',
1556 backgroundColor: 'transparent',
1557 data: queue_linedata,
1558 pointStyle: 'cross',
1561 label: 'Dropped / error packets',
1564 backgroundColor: 'transparent',
1565 data: drop_linedata,
1569 label: 'Duplicates',
1571 borderColor: 'green',
1572 backgroundColor: 'transparent',
1573 data: dupe_linedata,
1574 pointStyle: 'triangle',
1578 if (packetqueue_panel.packetqueue_chart == null) {
1579 packetqueue_panel.pq_content.append(
1584 "class": "k-mm-canvas",
1588 var canvas = $('#pq-canvas', packetqueue_panel.pq_content);
1590 packetqueue_panel.packetqueue_chart = new Chart(canvas, {
1594 maintainAspectRatio: false,
1608 labels: pointtitles,
1614 packetqueue_panel.packetqueue_chart.data.datasets = datasets;
1615 packetqueue_panel.packetqueue_chart.data.labels = pointtitles;
1616 packetqueue_panel.packetqueue_chart.update(0);
1619 .always(function() {
1620 packetqueue_panel.packetqueueupdate_tid = setTimeout(packetqueuedisplay_refresh, 5000);
1624 function datasourcepackets_refresh() {
1625 if (packetqueue_panel == null)
1628 clearTimeout(packetqueue_panel.datasourceupdate_tid);
1630 if (packetqueue_panel.is(':hidden'))
1633 $.get(local_uri_prefix + "datasource/all_sources.json")
1634 .done(function(data) {
1638 // Common point titles
1639 var pointtitles = new Array();
1641 var rval = $('#pq_ds_range', packetqueue_panel.ds_content).val();
1642 var range = kismet.RRD_SECOND;
1645 range = kismet.RRD_MINUTE;
1647 range = kismet.RRD_HOUR;
1649 if (range == kismet.RRD_SECOND || range == kismet.RRD_MINUTE) {
1650 for (var x = 60; x > 0; x--) {
1652 if (range == kismet.RRD_SECOND)
1653 pointtitles.push(x + 's');
1655 pointtitles.push(x + 'm');
1657 pointtitles.push(' ');
1661 for (var x = 23; x > 0; x--) {
1662 pointtitles.push(x + 'h');
1666 for (var source of data) {
1667 var color = parseInt(255 * (num / data.length))
1671 if ($('#pq_ds_type', packetqueue_panel.ds_content).val() == "bps")
1673 kismet.RecalcRrdData2(source['kismet.datasource.packets_datasize_rrd'],
1676 transform: function(data, opt) {
1687 kismet.RecalcRrdData2(source['kismet.datasource.packets_rrd'], range);
1690 "label": source['kismet.datasource.name'],
1691 "borderColor": `hsl(${color}, 100%, 50%)`,
1700 if (packetqueue_panel.datasource_chart == null) {
1701 packetqueue_panel.ds_content.append(
1703 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1712 "selected": "selected",
1718 }).text("Data (kB)")
1723 "id": "pq_ds_range",
1728 "selected": "selected",
1729 }).text("Past Minute")
1734 }).text("Past Hour")
1747 "class": "k-mm-canvas",
1751 packetqueue_panel.datasource_chart =
1752 new Chart($('#dsg-canvas', packetqueue_panel.ds_content), {
1756 "maintainAspectRatio": false,
1763 "beginAtZero": true,
1770 "labels": pointtitles,
1771 "datasets": datasets,
1775 packetqueue_panel.datasource_chart.data.datasets = datasets;
1776 packetqueue_panel.datasource_chart.data.labels = pointtitles;
1777 packetqueue_panel.datasource_chart.update(0);
1780 .always(function() {
1781 packetqueue_panel.datasourceupdate_tid = setTimeout(datasourcepackets_refresh, 1000);
1787 kismet_ui_settings.AddSettingsPane({
1789 listTitle: "GPS Status",
1790 create: function(elem) {
1801 .html("GPS Display")
1817 .append($('<div>', { class: 'spacer' }).html(" "))
1832 .append($('<div>', { class: 'spacer' }).html(" "))
1845 .html("Icon and Text")
1850 $('#form', elem).on('change', function() {
1851 kismet_ui_settings.SettingsModified();
1854 if (kismet.getStorage('kismet.ui.gps.icon', 'True') === 'True') {
1855 if (kismet.getStorage('kismet.ui.gps.text', 'True') === 'True') {
1856 $('#gps_both', elem).attr('checked', 'checked');
1858 $('#gps_icon', elem).attr('checked', 'checked');
1861 $('#gps_text', elem).attr('checked', 'checked');
1864 $('#set_gps', elem).controlgroup();
1866 save: function(elem) {
1867 var val = $("input[name='gps_status']:checked", elem).val();
1869 if (val === "both") {
1870 kismet.putStorage('kismet.ui.gps.text', 'True');
1871 kismet.putStorage('kismet.ui.gps.icon', 'True');
1872 } else if (val === "text") {
1873 kismet.putStorage('kismet.ui.gps.text', 'True');
1874 kismet.putStorage('kismet.ui.gps.icon', 'False');
1875 } else if (val === "icon") {
1876 kismet.putStorage('kismet.ui.gps.icon', 'True');
1877 kismet.putStorage('kismet.ui.gps.text', 'False');
1882 kismet_ui_settings.AddSettingsPane({
1883 id: 'base_units_measurements',
1884 listTitle: 'Units & Measurements',
1885 create: function(elem) {
1922 for: 'dst_imperial',
1962 for: 'spd_imperial',
1976 .html("Temperature")
1988 for: 'temp_celsius',
1995 id: 'temp_fahrenheit',
1997 value: 'fahrenheit',
2002 for: 'temp_fahrenheit',
2009 $('#form', elem).on('change', function() {
2010 kismet_ui_settings.SettingsModified();
2013 if (kismet.getStorage('kismet.base.unit.distance', 'metric') === 'metric') {
2014 $('#dst_metric', elem).attr('checked', 'checked');
2016 $('#dst_imperial', elem).attr('checked', 'checked');
2019 if (kismet.getStorage('kismet.base.unit.speed', 'metric') === 'metric') {
2020 $('#spd_metric', elem).attr('checked', 'checked');
2022 $('#spd_imperial', elem).attr('checked', 'checked');
2025 if (kismet.getStorage('kismet.base.unit.temp', 'celsius') === 'celsius') {
2026 $('#temp_celsius', elem).attr('checked', 'checked');
2028 $('#temp_fahrenheit', elem).attr('checked', 'checked');
2031 $('#set_distance', elem).controlgroup();
2032 $('#set_speed', elem).controlgroup();
2033 $('#set_temp', elem).controlgroup();
2036 save: function(elem) {
2037 var dist = $("input[name='distance']:checked", elem).val();
2038 kismet.putStorage('kismet.base.unit.distance', dist);
2039 var spd = $("input[name='speed']:checked", elem).val();
2040 kismet.putStorage('kismet.base.unit.speed', spd);
2041 var tmp = $("input[name='temp']:checked", elem).val();
2042 kismet.putStorage('kismet.base.unit.temp', tmp);
2048 kismet_ui_settings.AddSettingsPane({
2050 listTitle: 'Plugins',
2051 create: function(elem) {
2052 elem.append($('<i>').html('Loading plugin data...'));
2054 $.get(local_uri_prefix + "plugins/all_plugins.json")
2055 .done(function(data) {
2058 if (data.length == 0) {
2059 elem.append($('<i>').html('No plugins loaded...'));
2062 for (var pi in data) {
2065 var sharedlib = $('<p>');
2067 if (pl['kismet.plugin.shared_object'].length > 0) {
2068 sharedlib.html("Native code from " + pl['kismet.plugin.shared_object']);
2070 sharedlib.html("No native code");
2075 class: 'k-b-s-plugin-title',
2079 class: 'k-b-s-plugin-title',
2081 .html(pl['kismet.plugin.name'])
2085 .html(pl['kismet.plugin.version'])
2090 class: 'k-b-s-plugin-content',
2094 .html(pl['kismet.plugin.description'])
2098 .html(pl['kismet.plugin.author'])
2105 save: function(elem) {
2111 kismet_ui_settings.AddSettingsPane({
2112 id: 'base_login_password',
2113 listTitle: 'Login & Password',
2114 create: function(elem) {
2125 .html('Server Login')
2129 .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.')
2133 .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>')
2137 .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.')
2143 $('<span style="display: inline-block; width: 8em;">')
2144 .html('User name: ')
2157 $('<span style="display: inline-block; width: 8em;">')
2170 style: 'padding-left: 5px',
2175 class: 'fa fa-refresh fa-spin',
2188 $('#form', elem).on('change', function() {
2189 kismet_ui_settings.SettingsModified();
2192 var checker_cb = function() {
2193 // Cancel any pending timer
2194 if (pw_check_tid > -1)
2195 clearTimeout(pw_check_tid);
2197 var checkerdiv = $('#pwsuccessdiv', elem);
2198 var checker = $('#pwsuccess', checkerdiv);
2199 var checkertext = $('#pwsuccesstext', checkerdiv);
2201 checker.removeClass('fa-exclamation-circle');
2202 checker.removeClass('fa-check-square');
2204 checker.addClass('fa-spin');
2205 checker.addClass('fa-refresh');
2206 checkertext.text(" Checking...");
2210 // Set a timer for a second from now to call the actual check
2211 // in case the user is still typing
2212 pw_check_tid = setTimeout(function() {
2213 exports.LoginCheck(function(success) {
2215 checker.removeClass('fa-check-square');
2216 checker.removeClass('fa-spin');
2217 checker.removeClass('fa-refresh');
2218 checker.addClass('fa-exclamation-circle');
2219 checkertext.text(" Invalid login");
2221 checker.removeClass('fa-exclamation-circle');
2222 checker.removeClass('fa-spin');
2223 checker.removeClass('fa-refresh');
2224 checker.addClass('fa-check-square');
2225 checkertext.text("");
2227 }, $('#user', elem).val(), $('#password', elem).val());
2231 var pw_check_tid = -1;
2232 jQuery('#password', elem).on('input propertychange paste', function() {
2233 kismet_ui_settings.SettingsModified();
2236 jQuery('#user', elem).on('input propertychange paste', function() {
2237 kismet_ui_settings.SettingsModified();
2241 $('#user', elem).val(kismet.getStorage('kismet.base.login.username', 'kismet'));
2242 $('#password', elem).val(kismet.getStorage('kismet.base.login.password', 'kismet'));
2244 if ($('#user', elem).val() === 'kismet' &&
2245 $('#password', elem).val() === 'kismet') {
2246 $('#defaultwarning').show();
2249 $('fs_login', elem).controlgroup();
2251 // Check the current pw
2254 save: function(elem) {
2255 kismet.putStorage('kismet.base.login.username', $('#user', elem).val());
2256 kismet.putStorage('kismet.base.login.password', $('#password', elem).val());
2260 function show_role_help(role) {
2261 var rolehelp = `Unknown role ${role}; this could be assigned as a custom role for a Kismet plugin.`;
2263 if (role === "admin")
2264 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.";
2265 else if (role === "readonly")
2266 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.";
2267 else if (role === "datasource")
2268 rolehelp = "The datasource role allows remote capture over websockets. This role only has access to the remote capture datasource endpoint.";
2269 else if (role === "scanreport")
2270 rolehelp = "The scanreport role allows device scan reports. This role only has access to the scan report endpoint."
2271 else if (role === "ADSB")
2272 rolehelp = "The ADSB role allows access to the combined and device-specific ADSB feeds."
2273 else if (role === "__explain__") {
2274 rolehelp = "<p>Kismet uses a basic role system to restrict access to API endpoints. The default roles are:";
2275 rolehelp += "<p>"admin" which has access to all API endpoints.";
2276 rolehelp += "<p>"readonly" which only has access to endpoints which do not alter devices or change the configuration of the server";
2277 rolehelp += "<p>"datasource" which is used for websockets based remote capture and may not access any other endpoints";
2278 rolehelp += "<p>"scanreport" which is used for reporting scanning-mode devices";
2279 rolehelp += "<p>"ADSB" which is used for sharing ADSB feeds";
2280 rolehelp += "<p>Plugins or other code may define other roles.";
2282 role = "Kismet API Roles";
2285 var h = $(window).height() / 4;
2286 var w = $(window).width() / 2;
2289 w = $(window).width() - 5;
2292 h = $(window).height() - 5;
2296 headerTitle: `Role: ${role}`,
2298 controls: 'closeonly',
2299 iconfont: 'jsglyph',
2301 contentSize: `${w} auto`,
2303 content: `<div style="padding: 10px;"><h3>${role}</h3><p>${rolehelp}`,
2312 function delete_role(rolename, elem) {
2313 var deltd = $('.deltd', elem);
2317 'style': 'background-color: #DDAAAA',
2319 .html(`Delete role "${rolename}"`)
2321 .on('click', function() {
2326 var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2328 $.post(local_uri_prefix + "auth/apikey/revoke.cmd", postdata)
2329 .done(function(data) {
2330 var delt = elem.parent();
2334 if ($('tr', delt).length == 1) {
2343 .html("<i>No API keys defined...</i>")
2351 deltd.append(delbt);
2355 function make_role_help_closure(role) {
2356 return function() { show_role_help(role); };
2359 function make_role_delete_closure(rolename, elem) {
2360 return function() { delete_role(rolename, elem); };
2363 kismet_ui_settings.AddSettingsPane({
2364 id: 'base_api_logins',
2365 listTitle: "API Keys",
2366 create: function(elem) {
2367 elem.append($("p").html("Fetching API data..."));
2369 $.get(local_uri_prefix + "auth/apikey/list.json")
2370 .done(function(data) {
2371 data = kismet.sanitizeObject(data);
2374 var tb = $('<table>', {
2375 'class': 'apitable',
2376 'id': 'apikeytable',
2384 'style': 'width: 16em;',
2390 'style': 'width: 8em;',
2396 'style': 'width: 30em;',
2406 if (data.length == 0) {
2415 .html("<i>No API keys defined...</i>")
2420 for (var user of data) {
2421 var name = user['kismet.httpd.auth.name'];
2422 var role = user['kismet.httpd.auth.role'];
2426 if ('kismet.httpd.auth.token' in user) {
2427 key = user['kismet.httpd.auth.token'];
2429 key = "<i>Viewing auth tokens is disabled in the Kismet configuration.</i>";
2439 $('<td>').html(name)
2442 $('<td>').html(role)
2445 'class': 'pseudolink fa fa-question-circle',
2446 'style': 'padding-left: 5px;',
2448 .on('click', make_role_help_closure(role))
2459 'id': name.replace(" ", "_"),
2464 'class': 'copyuri pseudolink fa fa-copy',
2465 'style': 'padding-left: 5px;',
2466 'data-clipboard-target': `#${name.replace(" ", "_")}`,
2476 'class': 'pseudolink fa fa-trash',
2478 .on('click', make_role_delete_closure(name, tr))
2495 'id': 'addapikeybutton',
2497 }).html(`<i class="fa fa-plus"> Create API Key`)
2501 'for': 'addapiname',
2507 'name': 'addapiname',
2515 'for': 'addapirole',
2521 'name': 'addapirole',
2526 'value': 'readonly',
2532 'value': 'datasource',
2533 }).html("datasource")
2537 'value': 'scanreport',
2538 }).html("scanreport")
2553 }).html("<i>custom</i>")
2558 'name': 'addapiroleother',
2559 'id': 'addapiroleother',
2566 'class': 'pseudolink fa fa-question-circle',
2567 'style': 'padding-left: 5px;',
2569 .on('click', make_role_help_closure("__explain__"))
2573 'id': 'addapierror',
2574 'style': 'color: red;'
2579 $('#addapikeybutton', adddiv)
2581 .on('click', function() {
2582 var name = $('#addapiname').val();
2583 var role_select = $('#addapirole option:selected').text();
2584 var role_input = $('#addapiroleother').val();
2586 if (name.length == 0) {
2587 $('#addapierror').show().html("Missing name.");
2591 if (role_select === "custom" && role_input.length == 0) {
2592 $('#addapierror').show().html("Missing custom role.");
2596 $('#addapierror').hide();
2598 var role = role_select;
2600 if (role_select === "custom")
2609 var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2611 $.post(local_uri_prefix + "auth/apikey/generate.cmd", postdata)
2612 .fail(function(response) {
2613 var rt = kismet.sanitizeObject(response.responseText);
2614 $('#addapierror').show().html(`Failed to add API key: ${rt}`);
2616 .done(function(data) {
2617 var key = kismet.sanitizeObject(data);
2626 $('<td>').html(name)
2629 $('<td>').html(role)
2632 'class': 'pseudolink fa fa-question-circle',
2633 'style': 'padding-left: 5px;',
2635 .on('click', make_role_help_closure(role))
2646 'id': name.replace(" ", "_"),
2651 'class': 'copyuri pseudolink fa fa-copy',
2652 'style': 'padding-left: 5px;',
2653 'data-clipboard-target': `#${name.replace(" ", "_")}`,
2663 'class': 'pseudolink fa fa-trash',
2665 .on('click', make_role_delete_closure(name, tr))
2669 $('#apikeytable').append(tr);
2671 $('#addapiname').val('');
2672 $("#addapirole").prop("selectedIndex", 0);
2673 $("#addapirole").show();
2674 $("#addapiroleother").val('').hide();
2678 $('#addapirole', adddiv).on('change', function(e) {
2679 var val = $("#addapirole option:selected" ).text();
2681 if (val === "custom") {
2683 $('#addapiroleother').show();
2688 elem.append(adddiv);
2690 new ClipboardJS('.copyuri');
2693 save: function(elem) {
2700 /* Add the messages and channels tabs */
2701 kismet_ui_tabpane.AddTab({
2703 tabTitle: 'Messages',
2704 createCallback: function(div) {
2710 kismet_ui_tabpane.AddTab({
2712 tabTitle: 'Channels',
2714 createCallback: function(div) {
2720 kismet_ui_tabpane.AddTab({
2722 tabTitle: 'Devices',
2724 createCallback: function(div) {
2727 class: 'resize_wrapper',
2732 class: 'fixeddt stripe hover nowrap',
2739 id: 'devices_status',
2740 style: 'padding-bottom: 10px;',
2744 kismet_ui.CreateDeviceTable($('#devices', div));
2750 exports.DeviceSignalDetails = function(key) {
2751 var w = $(window).width() * 0.75;
2752 var h = $(window).height() * 0.5;
2754 var devsignal_chart = null;
2756 var devsignal_tid = -1;
2760 class: 'k-dsd-container'
2768 class: 'k-dsd-title'
2774 class: 'k-dsd-table'
2783 .html("Last Signal:")
2791 class: 'k-dsd-lastsignal',
2796 class: 'fa k-dsd-arrow k-dsd-arrow-down',
2809 .html("Min Signal:")
2814 class: 'k-dsd-minsignal',
2826 .html("Max Signal:")
2831 class: 'k-dsd-maxsignal',
2840 class: 'k-dsd-graph'
2845 class: 'k-dsd-canvas'
2850 var devsignal_panel = $.jsPanel({
2851 id: 'devsignal' + key,
2852 headerTitle: '<i class="fa fa-signal" /> Signal',
2854 iconfont: 'jsglyph',
2857 onclosed: function() {
2858 clearTimeout(devsignal_tid);
2870 var emptyminute = new Array();
2871 for (var x = 0; x < 60; x++) {
2872 emptyminute.push(0);
2875 devsignal_tid = devsignal_refresh(key, devsignal_panel,
2876 devsignal_chart, devsignal_tid, 0, emptyminute);
2879 function devsignal_refresh(key, devsignal_panel, devsignal_chart,
2880 devsignal_tid, lastsignal, fakerrd) {
2881 clearTimeout(devsignal_tid);
2883 if (devsignal_panel == null)
2886 if (devsignal_panel.is(':hidden'))
2889 var signal = lastsignal;
2891 $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
2892 .done(function(data) {
2893 var title = '<i class="fa fa-signal" /> Signal ' +
2894 kismet.censorMAC(data['kismet.device.base.macaddr']) + ' ' +
2895 kismet.censorMAC(data['kismet.device.base.name']);
2896 devsignal_panel.headerTitle(title);
2898 var sigicon = $('.k-dsd-arrow', devsignal_panel.content);
2900 sigicon.removeClass('k-dsd-arrow-up');
2901 sigicon.removeClass('k-dsd-arrow-down');
2902 sigicon.removeClass('fa-arrow-up');
2903 sigicon.removeClass('fa-arrow-down');
2905 signal = data['kismet.device.base.signal']['kismet.common.signal.last_signal'];
2907 if (signal < lastsignal) {
2908 sigicon.addClass('k-dsd-arrow-down');
2909 sigicon.addClass('fa-arrow-down');
2912 sigicon.addClass('k-dsd-arrow-up');
2913 sigicon.addClass('fa-arrow-up');
2918 if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "dbm")
2920 else if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "rssi")
2923 $('.k-dsd-lastsignal', devsignal_panel.content)
2924 .text(signal + typestr);
2926 $('.k-dsd-minsignal', devsignal_panel.content)
2927 .text(data['kismet.device.base.signal']['kismet.common.signal.min_signal'] + typestr);
2929 $('.k-dsd-maxsignal', devsignal_panel.content)
2930 .text(data['kismet.device.base.signal']['kismet.common.signal.max_signal'] + typestr);
2932 // Common point titles
2933 var pointtitles = new Array();
2935 for (var x = 60; x > 0; x--) {
2937 pointtitles.push(x + 's');
2939 pointtitles.push(' ');
2945 var rrdata = kismet.RecalcRrdData(
2946 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2947 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2949 data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
2951 // We assume the 'best' a signal can usefully be is -20dbm,
2952 // that means we're right on top of it.
2953 // We can assume that -100dbm is a sane floor value for
2954 // the weakest signal.
2955 // If a signal is 0 it means we haven't seen it at all so
2956 // just ignore that data point
2957 // We turn signals into a 'useful' graph by clamping to
2958 // -100 and -20 and then scaling it as a positive number.
2959 var moddata = new Array();
2961 for (var x = 0; x < rrdata.length; x++) {
2975 // Normalize to 0-80
2978 // Reverse (weaker is worse), get as percentage
2979 var rs = (80 - d) / 80;
2981 moddata.push(100*rs);
2985 var msignal = signal;
2989 } else if (msignal < -100) {
2991 } else if (msignal > -20) {
2995 msignal = (msignal * -1) - 20;
2996 var rs = (80 - msignal) / 80;
2998 fakerrd.push(100 * rs);
3000 fakerrd.splice(0, 1);
3002 var moddata = fakerrd;
3006 label: 'Signal (%)',
3008 borderColor: 'blue',
3009 backgroundColor: 'rgba(100, 100, 255, 0.83)',
3014 if (devsignal_chart == null) {
3015 var canvas = $('#k-dsd-canvas', devsignal_panel.content);
3017 devsignal_chart = new Chart(canvas, {
3021 maintainAspectRatio: false,
3033 labels: pointtitles,
3038 devsignal_chart.data.datasets[0].data = moddata;
3039 devsignal_chart.update();
3044 .always(function() {
3045 devsignal_tid = setTimeout(function() {
3046 devsignal_refresh(key, devsignal_panel,
3047 devsignal_chart, devsignal_tid, signal, fakerrd);
3052 exports.login_error = false;
3053 exports.login_pending = false;
3055 exports.ProvisionedPasswordCheck = function(cb) {
3057 url: local_uri_prefix + "session/check_setup_ok",
3059 error: function(jqXHR, textStatus, errorThrown) {
3063 success: function(data, textStatus, jqHXR) {
3069 exports.LoginCheck = function(cb, user, pw) {
3070 user = user || kismet.getStorage('kismet.base.login.username', 'kismet');
3071 pw = pw || kismet.getStorage('kismet.base.login.password', '');
3074 url: local_uri_prefix + "session/check_login",
3076 beforeSend: function (xhr) {
3077 xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
3081 withCredentials: false
3084 error: function(jqXHR, textStatus, errorThrown) {
3088 success: function(data, textStatus, jqXHR) {
3095 exports.FirstLoginCheck = function(first_login_done_cb) {
3096 var loginpanel = null;
3097 var username_deferred = $.Deferred();
3099 $.get(local_uri_prefix + "system/user_status.json")
3100 .done(function(data) {
3101 username_deferred.resolve(data['kismet.system.user']);
3104 username_deferred.resolve("[unknown]");
3107 var username = "[incomplete]";
3108 $.when(username_deferred).done(function(v) {
3112 var required_login_content =
3114 style: 'padding: 10px;'
3118 .html('Kismet requires a login to access data.')
3122 .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>.')
3133 $('<span style="display: inline-block; width: 8em;">')
3134 .html('User name: ')
3147 $('<span style="display: inline-block; width: 8em;">')
3159 style: 'padding-top: 10px;'
3163 class: 'k-wl-button-close',
3172 style: 'padding-left: 5px',
3177 class: 'fa fa-refresh fa-spin',
3191 var login_checker_cb = function(content) {
3192 var savebutton = $('#login_button', content);
3194 var checkerdiv = $('#pwsuccessdiv', content);
3195 var checker = $('#pwsuccess', checkerdiv);
3196 var checkertext = $('#pwsuccesstext', checkerdiv);
3198 checker.removeClass('fa-exclamation-circle');
3199 checker.removeClass('fa-check-square');
3201 checker.addClass('fa-spin');
3202 checker.addClass('fa-refresh');
3203 checkertext.text(" Checking...");
3207 exports.LoginCheck(function(success) {
3209 checker.removeClass('fa-check-square');
3210 checker.removeClass('fa-spin');
3211 checker.removeClass('fa-refresh');
3212 checker.addClass('fa-exclamation-circle');
3213 checkertext.text(" Invalid login");
3215 /* Save the login info */
3216 kismet.putStorage('kismet.base.login.username', $('#req_user', content).val());
3217 kismet.putStorage('kismet.base.login.password', $('#req_password', content).val());
3221 /* Call the primary callback */
3222 first_login_done_cb();
3224 }, $('#req_user', content).val(), $('#req_password', content).val());
3227 $('#login_button', required_login_content)
3229 .on('click', function() {
3230 login_checker_cb(required_login_content);
3233 $('fs_login', required_login_content).controlgroup();
3235 var set_password_content =
3237 style: 'padding: 10px;'
3241 .html('To finish setting up Kismet, you need to configure a login.')
3245 .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>.')
3260 $('<span style="display: inline-block; width: 8em;">')
3261 .html('User name: ')
3274 $('<span style="display: inline-block; width: 8em;">')
3288 $('<span style="display: inline-block; width: 8em;">')
3301 style: 'padding-left: 5px',
3306 class: 'fa fa-refresh fa-spin',
3320 style: 'padding-top: 10px;'
3324 class: 'k-wl-button-close',
3325 id: 'save_password',
3332 var checker_cb = function(content) {
3333 var savebutton = $('#save_password', content);
3334 var checkerdiv = $('#pwsuccessdiv', content);
3335 var checker = $('#pwsuccess', checkerdiv);
3336 var checkertext = $('#pwsuccesstext', checkerdiv);
3338 savebutton.button("disable");
3340 checker.removeClass('fa-exclamation-circle');
3341 checker.removeClass('fa-check-square');
3343 checker.addClass('fa-spin');
3344 checker.addClass('fa-refresh');
3345 checkertext.text("");
3349 if ($('#user', content).val().length == 0) {
3350 checker.removeClass('fa-check-square');
3351 checker.removeClass('fa-spin');
3352 checker.removeClass('fa-refresh');
3353 checker.addClass('fa-exclamation-circle');
3354 checkertext.text(" Username required");
3355 savebutton.button("disable");
3359 if ($('#password', content).val().length == 0) {
3360 checker.removeClass('fa-check-square');
3361 checker.removeClass('fa-spin');
3362 checker.removeClass('fa-refresh');
3363 checker.addClass('fa-exclamation-circle');
3364 checkertext.text(" Password required");
3365 savebutton.button("disable");
3369 if ($('#password', content).val() != $('#password2', content).val()) {
3370 checker.removeClass('fa-check-square');
3371 checker.removeClass('fa-spin');
3372 checker.removeClass('fa-refresh');
3373 checker.addClass('fa-exclamation-circle');
3374 checkertext.text(" Passwords don't match");
3375 savebutton.button("disable");
3379 checker.removeClass('fa-exclamation-circle');
3380 checker.removeClass('fa-spin');
3381 checker.removeClass('fa-refresh');
3382 checker.addClass('fa-check-square');
3383 checkertext.text("");
3384 savebutton.button("enable");
3388 jQuery('#user', set_password_content).on('input propertychange paste', function() {
3391 jQuery('#password', set_password_content).on('input propertychange paste', function() {
3394 jQuery('#password2', set_password_content).on('input propertychange paste', function() {
3398 $('#save_password', set_password_content)
3400 .on('click', function() {
3401 kismet.putStorage('kismet.base.login.username', $('#user', set_password_content).val());
3402 kismet.putStorage('kismet.base.login.password', $('#password', set_password_content).val());
3405 "username": $('#user', set_password_content).val(),
3406 "password": $('#password', set_password_content).val()
3411 url: local_uri_prefix + "session/set_password",
3413 error: function(jqXHR, textStatus, errorThrown) {
3414 alert("Could not set login, check your kismet server logs.")
3420 /* Call the primary callback to load the UI */
3421 first_login_done_cb();
3423 /* Check for the first-time running */
3424 exports.FirstTimeCheck();
3427 $('fs_login', set_password_content).controlgroup();
3429 checker_cb(set_password_content);
3431 var w = ($(window).width() / 2) - 5;
3433 w = $(window).width() - 5;
3436 var content = set_password_content;
3438 exports.ProvisionedPasswordCheck(function(code) {
3439 if (code == 200 || code == 406) {
3440 /* Initial setup has been complete, now check the login itself */
3441 exports.LoginCheck(function(success) {
3443 loginpanel = $.jsPanel({
3445 headerTitle: '<i class="fa fa-exclamation-triangle"></i>Login Required',
3447 controls: 'closeonly',
3448 iconfont: 'jsglyph',
3450 contentSize: w + " auto",
3452 content: required_login_content,
3457 /* Otherwise we're all good, continue to loading the main UI via the callback */
3458 first_login_done_cb();
3461 } else if (code == 500) {
3462 loginpanel = $.jsPanel({
3464 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Set Login',
3466 controls: 'closeonly',
3467 iconfont: 'jsglyph',
3469 contentSize: w + " auto",
3471 content: set_password_content,
3476 loginpanel = $.jsPanel({
3478 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Error connecting',
3480 controls: 'closeonly',
3481 iconfont: 'jsglyph',
3483 contentSize: w + " auto",
3485 content: "Error connecting to Kismet and checking provisioning; try reloading the page!",
3496 exports.FirstTimeCheck = function() {
3497 var welcomepanel = null;
3498 if (kismet.getStorage('kismet.base.seen_welcome', false) == false) {
3501 style: 'padding: 10px;'
3510 .html('This is the first time you\'ve used this Kismet server in this browser.')
3514 .html('Kismet stores local settings in the HTML5 storage of your browser.')
3518 .html('You should configure your preferences and login settings in the settings panel!')
3524 class: 'k-w-button-settings'
3528 .on('click', function() {
3529 welcomepanel.close();
3530 kismet_ui_settings.ShowSettings();
3535 class: 'k-w-button-close',
3536 style: 'position: absolute; right: 5px;',
3540 .on('click', function() {
3541 welcomepanel.close();
3547 welcomepanel = $.jsPanel({
3548 id: "welcome-alert",
3549 headerTitle: '<i class="fa fa-power-off"></i> Welcome',
3551 controls: 'closeonly',
3552 iconfont: 'jsglyph',
3554 contentSize: "auto auto",
3559 kismet.putStorage('kismet.base.seen_welcome', true);
3567 // Keep trying to fetch the servername until we're able to
3568 var servername_tid = -1;
3569 exports.FetchServerName = function(cb) {
3570 $.get(local_uri_prefix + "system/status.json")
3571 .done(function (d) {
3572 d = kismet.sanitizeObject(d);
3573 cb(d['kismet.system.server_name']);
3576 servername_tid = setTimeout(function () {
3577 exports.FetchServerName(cb);
3582 /* Highlight active devices */
3583 kismet_ui.AddDeviceRowHighlight({
3585 description: "Device has been active in the past 10 seconds",
3587 defaultcolor: "#cee1ff",
3588 defaultenable: false,
3590 'kismet.device.base.last_time'
3592 selector: function(data) {
3593 var ts = data['kismet.device.base.last_time'];
3595 return (kismet.timestamp_sec - ts < 10);
3599 /* Bodycam hardware of various types */
3600 kismet_ui.AddDeviceRowHighlight({
3602 description: "Body camera devices",
3604 defaultcolor: "#0089FF",
3605 defaultenable: true,
3607 'kismet.device.base.macaddr',
3608 'kismet.device.base.commonname',
3610 selector: function(data) {
3612 if (data['kismet.device.base.macaddr'].match("^00:25:DF") != null)
3614 if (data['kismet.device.base.macaddr'].match("^12:20:13") != null)
3616 if (data['kismet.device.base.common_name'].match("^Axon-X") != null)
3626 // We're done loading
3627 exports.load_complete = 1;