3c05145f03d59f3e07b07e017238d21fcb8aecd2
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.dot11.js
1 "use strict";
2
3 var local_uri_prefix = ""; 
4 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
5     local_uri_prefix = KISMET_URI_PREFIX;
6
7 // Crypt set from packet_ieee80211.h
8 export const crypt_none = 0;
9 export const crypt_unknown = 1;
10 export const crypt_wep = (1 << 1);
11 export const crypt_layer3 = (1 << 2);
12 export const crypt_wep40 = (1 << 3);
13 export const crypt_wep104 = (1 << 4);
14 export const crypt_tkip = (1 << 5);
15 export const crypt_wpa = (1 << 6);
16 export const crypt_psk = (1 << 7);
17 export const crypt_aes_ocb = (1 << 8);
18 export const crypt_aes_ccm = (1 << 9);
19 export const crypt_wpa_migmode = (1 << 10);
20 export const crypt_eap = (1 << 11);
21 export const crypt_leap = (1 << 12);
22 export const crypt_ttls = (1 << 13);
23 export const crypt_tls = (1 << 14);
24 export const crypt_peap = (1 << 15);
25 export const crypt_sae = (1 << 16);
26 export const crypt_wpa_owe = (1 << 17);
27
28 export const crypt_protectmask = 0xFFFFF;
29 export const crypt_isakmp = (1 << 20);
30 export const crypt_pptp = (1 << 21);
31 export const crypt_fortress = (1 << 22);
32 export const crypt_keyguard = (1 << 23);
33 export const crypt_unknown_protected = (1 << 24);
34 export const crypt_unknown_nonwep = (1 << 25);
35 export const crypt_wps = (1 << 26);
36 export const crypt_version_wpa = (1 << 27);
37 export const crypt_version_wpa2 = (1 << 28);
38 export const crypt_version_wpa3 = (1 << 29);
39
40 export const crypt_l3_mask = 0x300004;
41 export const crypt_l2_mask = 0xFBFA;
42
43 // Some hex and ascii manipulation
44 function hexstr_to_bytes(hex) {
45     var bytes = [];
46
47     try {
48         for (var i = 0; i < hex.length - 1; i += 2) {
49             bytes.push(parseInt(hex.substr(i, 2), 16));
50         }
51     } catch (error) {
52         ;
53     }
54
55     return bytes;
56 }
57
58 function hexdump(b) {
59     if (typeof(b) === 'undefined' || b.length == 0)
60         return "..".repeat(8);
61
62     return b.reduce((output, elem) =>
63         (output + ('0' + elem.toString(16)).slice(-2) + ""), '') + "..".repeat(8 - b.length);
64 }
65
66 function asciidump(b) {
67     var ret = "";
68
69     if (typeof(b) === 'undefined' || b.length == 0)
70         return '.'.repeat(8);
71
72     for (var i = 0; i < b.length; i++) {
73         if (b[i] >= 32 && b[i] <= 127) {
74             var c = String.fromCharCode(b[i]);
75
76             if (c == "<")
77                 c = "&lt;";
78             if (c == ">")
79                 c = "&gt;";
80             if (c == "&")
81                 c = "&amp;";
82
83             ret = ret + c;
84         } else {
85             ret = ret + ".";
86         }
87     }
88
89     ret = ret + ".".repeat(8 - b.length);
90
91     return ret;
92 }
93
94 function pretty_hexdump(b) {
95     var groups = [];
96     var ret_groups = [];
97
98     if (typeof(b) === 'undefined')
99         return "";
100
101     for (var i = 0; i < b.length; i += 8) {
102         groups.push(b.slice(i, i + 8));
103     }
104
105     if (b.length % 2)
106         b.push([]);
107
108     // Print 2 groups of 8, followed by ascii
109     for (var i = 0; i < groups.length; i += 2) {
110         var hex_str = hexdump(groups[i]) + "  " + hexdump(groups[i + 1]);
111         var ascii_str = asciidump(groups[i]) + "  " + asciidump(groups[i + 1]);
112
113         ret_groups.push(hex_str + '&nbsp;&nbsp;&nbsp;&nbsp;' + ascii_str);
114     }
115
116     return ret_groups;
117 }
118
119 export const CryptToHumanReadable = (cryptset) => {
120     var ret = [];
121
122     if (cryptset == crypt_none)
123         return "None / Open";
124
125     if (cryptset == crypt_unknown)
126         return "Unknown";
127
128     if (cryptset & crypt_wps)
129         ret.push("WPS");
130
131     if ((cryptset & crypt_protectmask) == crypt_wep) {
132         ret.push("WEP");
133         return ret.join(" ");
134     }
135
136     if (cryptset == crypt_wpa_owe)
137         return "Open (OWE)";
138
139     if (cryptset & crypt_wpa_owe)
140         return "OWE";
141
142     var WPAVER = "WPA";
143
144     if (cryptset & crypt_version_wpa2)
145         WPAVER = "WPA2";
146
147     if (cryptset & crypt_version_wpa3)
148         WPAVER = "WPA3";
149
150     if (cryptset & crypt_wpa)
151         ret.push(WPAVER);
152
153         if ((cryptset & crypt_version_wpa3) && (cryptset & crypt_psk) && (cryptset & crypt_sae))
154         ret.push("WPA3-TRANSITION");
155
156     if (cryptset & crypt_psk)
157         ret.push(WPAVER + "-PSK");
158
159     if (cryptset & crypt_sae)
160         ret.push(WPAVER + "-SAE");
161
162     if (cryptset & crypt_eap)
163         ret.push(WPAVER + "-EAP");
164
165     if (cryptset & crypt_peap)
166         ret.push(WPAVER + "-PEAP");
167
168     if (cryptset & crypt_leap)
169         ret.push("EAP-LEAP");
170
171     if (cryptset & crypt_ttls)
172         ret.push("EAP-TTLS");
173
174     if (cryptset & crypt_tls)
175         ret.push("EAP-TLS");
176
177     if (cryptset & crypt_wpa_migmode)
178         ret.push("WPA-MIGRATION");
179
180     if (cryptset & crypt_wep40)
181         ret.push("WEP40");
182
183     if (cryptset & crypt_wep104)
184         ret.push("WEP104");
185
186     if (cryptset & crypt_tkip)
187         ret.push("TKIP");
188
189     if (cryptset & crypt_aes_ocb)
190         ret.push("AES-OCB");
191
192     if (cryptset & crypt_aes_ccm)
193         ret.push("AES-CCM");
194
195     if (cryptset & crypt_layer3)
196         ret.push("Layer3");
197
198     if (cryptset & crypt_isakmp)
199         ret.push("Layer3-ISA-KMP");
200
201     if (cryptset & crypt_pptp)
202         ret.push("Layer3-PPTP");
203
204     if (cryptset & crypt_fortress)
205         ret.push("Fortress");
206
207     if (cryptset & crypt_keyguard)
208         ret.push("Keyguard");
209
210     if (cryptset & crypt_unknown_protected)
211         ret.push("Unknown");
212
213     if (cryptset & crypt_unknown_nonwep)
214         ret.push("Unknown-Non-WEP");
215
216     return ret.join(" ");
217 };
218
219 kismet_ui.AddDeviceView("Wi-Fi Access Points", "phydot11_accesspoints", -10000, "Wi-Fi");
220
221 kismet_ui.AddChannelList("IEEE802.11", "Wi-Fi (802.11)", function(in_freq) {
222     if (in_freq == 0)
223         return "n/a";
224
225     in_freq = parseInt(in_freq / 1000);
226
227     if (in_freq == 2484)
228         return 14;
229     else if (in_freq < 2484)
230         return (in_freq - 2407) / 5;
231     else if (in_freq >= 4910 && in_freq <= 4980)
232         return (in_freq - 4000) / 5;
233     else if (in_freq <= 45000)
234         return (in_freq - 5000) / 5;
235     else if (in_freq >= 58320 && in_freq <= 64800)
236         return (in_freq - 56160) / 2160;
237     else
238         return kismet.HumanReadableFrequency(in_freq);
239 });
240
241 /* Highlight WPA handshakes */
242 kismet_ui.AddDeviceRowHighlight({
243     name: "WPA Handshake",
244     description: "Network contains a complete WPA handshake",
245     priority: 10,
246     defaultcolor: "#F00",
247     defaultenable: true,
248     fields: [
249         'dot11.device/dot11.device.wpa_handshake_list'
250     ],
251     selector: function(data) {
252         try {
253             for (const dev in data['dot11.device']['dot11.device.wpa_handshake_list']) {
254                 var pmask = 0;
255                 
256                 for (const p of data['dot11.device']['dot11.device.wpa_handshake_list'][dev]) {
257                     pmask = pmask | (1 << p['dot11.eapol.message_num']);
258
259                     if ((pmask & 0x06) == 0x06 || (pmask & 0x0C) == 0x0C)
260                         return true;
261                 }
262             }
263
264             return false;
265
266         } catch (e) {
267             return false;
268         }
269     }
270 });
271
272 /* Highlight WPA RSN PMKID */
273 kismet_ui.AddDeviceRowHighlight({
274     name: "RSN PMKID",
275     description: "Network contains a RSN PMKID packet",
276     priority: 10,
277     defaultcolor: "#F55",
278     defaultenable: true,
279     fields: [
280         'dot11.device/dot11.device.pmkid_packet'
281     ],
282     selector: function(data) {
283         try {
284             return 'dot11.device.pmkid_packet' in data && data['dot11.device.pmkid_packet'] != 0;
285         } catch (e) {
286             return false;
287         }
288     }
289 });
290
291 kismet_ui.AddDeviceRowHighlight({
292     name: "Wi-Fi Device",
293     description: "Highlight all Wi-Fi devices",
294     priority: 100,
295     defaultcolor: "#99ff99",
296     defaultenable: false,
297     fields: [
298         'kismet.device.base.phyname',
299     ],
300     selector: function(data) {
301         return ('kismet.device.base.phyname' in data && data['kismet.device.base.phyname'] === 'IEEE802.11');
302     }
303 });
304
305 kismet_ui.AddDeviceColumn('wifi_clients', {
306     sTitle: 'Clients',
307     field: 'dot11.device/dot11.device.num_associated_clients',
308     description: 'Related Wi-Fi devices (associated and bridged)',
309     width: '35px',
310     sClass: "dt-body-right",
311 });
312
313 kismet_ui.AddDeviceColumn('wifi_last_bssid', {
314     sTitle: 'BSSID',
315     field: 'dot11.device/dot11.device.last_bssid',
316     description: 'Last associated BSSID',
317     width: '70px',
318     sortable: true,
319     searchable: true,
320     renderfunc: function(d, t, r, m) {
321         try {
322             if (d == 0)
323                 return '<i>n/a</i>';
324         } catch (e) {
325             ;
326         }
327
328         return kismet.censorMAC(d);
329     }
330 });
331
332 kismet_ui.AddDeviceColumn('wifi_bss_uptime', {
333     sTitle: 'Uptime',
334     field: 'dot11.device/dot11.device.bss_timestamp',
335     description: 'Estimated device uptime (from BSS timestamp)',
336     width: '5em;',
337     sortable: true,
338     searchable: true,
339     visible: false, // Off by default
340     renderfunc: function(d, t, r, m) {
341         return kismet_ui_base.renderUsecTime(d, t, r, m);
342     },
343 });
344
345 // Hidden column to fetch qbss state
346 kismet_ui.AddDeviceColumn('column_qbss_hidden', {
347     sTitle: 'qbss_available',
348     field: 'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.dot11e_qbss',
349     name: 'qbss_available',
350     searchable: false,
351     visible: false,
352     selectable: false,
353     orderable: false
354 });
355
356 kismet_ui.AddDeviceColumn('wifi_qbss_usage', {
357     sTitle: 'QBSS Chan Usage',
358     // field: 'dot11.device/dot11.device.bss_timestamp',
359     field: 'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.dot11e_channel_utilization_perc',
360     description: '802.11e QBSS channel utilization',
361     width: '5em;',
362     sortable: true,
363     searchable: true,
364     visiable: false,
365     width: "100px",
366     renderfunc: function(d, t, r, m) {
367         var perc = "n/a";
368
369         if (r['dot11.advertisedssid.dot11e_qbss'] == 1) {
370             if (d == 0)
371                 perc = "0%";
372             else
373                 perc = Number.parseFloat(d).toPrecision(4) + "%";
374         }
375
376         return '<div class="percentage-border"><span class="percentage-text">' + perc + '</span><div class="percentage-fill" style="width:' + d + '%"></div></div>';
377     }
378 });
379
380 kismet_ui.AddDeviceColumn('wifi_qbss_clients', {
381     sTitle: 'QBSS #',
382     field: 'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.dot11e_qbss_stations',
383     description: '802.11e QBSS user count',
384     sortable: true,
385     visiable: false,
386     sClass: "dt-body-right",
387     renderfunc: function(d, t, r, m) {
388         if (r['dot11.advertisedssid.dot11e_qbss'] == 1) {
389             return d;
390         }
391
392         return "<i>n/a</i>"
393     }
394 });
395
396 /* Custom device details for dot11 data */
397 kismet_ui.AddDeviceDetail("dot11", "Wi-Fi (802.11)", 0, {
398     filter: function(data) {
399         try {
400             return (data['kismet.device.base.phyname'] === "IEEE802.11");
401         } catch (error) {
402             return false;
403         }
404     },
405     draw: function(data, target, options, storage) {
406         target.devicedata(data, {
407             "id": "dot11DeviceData",
408             "fields": [
409             {
410                 field: 'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.ssid',
411                 title: "Last Beaconed SSID (AP)",
412                 liveupdate: true,
413                 draw: function(opts) {
414                     if (typeof(opts['value']) === 'undefined')
415                         return '<i>None</i>';
416                     if (opts['value'].replace(/\s/g, '').length == 0) 
417                         return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
418                     return opts['value'];
419                 },
420                 help: "If present, the last SSID (network name) advertised by a device as an access point beacon or as an access point issuing a probe response",
421             },
422             {
423                 field: "dot11.device/dot11.device.last_probed_ssid_record/dot11.probedssid.ssid",
424                 liveupdate: true,
425                 title: "Last Probed SSID (Client)",
426                 empty: "<i>None</i>",
427                 help: "If present, the last SSID (network name) probed for by a device as a client looking for a network.",
428                 draw: function(opts) {
429                     if (typeof(opts['value']) === 'undefined')
430                         return '<i>None</i>';
431                     if (opts['value'].replace(/\s/g, '').length == 0) 
432                         return '<i>Empty (' + opts['value'].length + ' spaces)</i>'
433                     return opts['value'];
434                 },
435             },
436             {
437                 field: "dot11.device/dot11.device.last_bssid",
438                 liveupdate: true,
439                 title: "Last BSSID",
440                 filter: function(opts) {
441                     try {
442                         return opts['value'].replace(/0:/g, '').length != 0;
443                     } catch (e) {
444                         return false;
445                     }
446                 },
447                 help: "If present, the BSSID (MAC address) of the last network this device was part of.  Each Wi-Fi access point, even those with the same SSID, has a unique BSSID.",
448                 draw: function(opts) {
449                     var mac = kismet.censorMAC(opts['value']);
450
451                     var container =
452                         $('<span>');
453                     container.append(
454                         $('<span>').html(mac)
455                     );
456                     container.append(
457                         $('<i>', {
458                             'class': 'copyuri pseudolink fa fa-copy',
459                             'style': 'padding-left: 5px;',
460                             'data-clipboard-text': `${mac}`, 
461                         })
462                     );
463
464                     return container;
465                 },
466
467             },
468             {
469                 field: "dot11.device/dot11.device.bss_timestamp",
470                 liveupdate: true,
471                 title: "Uptime",
472                 draw: function(opts) {
473                     if (opts['value'] == 0)
474                         return "<i>n/a</i>";
475
476                     var data_sec = opts['value'] / 1000000;
477
478                     var days = Math.floor(data_sec / 86400);
479                     var hours = Math.floor((data_sec / 3600) % 24);
480                     var minutes = Math.floor((data_sec / 60) % 60);
481                     var seconds = Math.floor(data_sec % 60);
482
483                     var ret = "";
484
485                     if (days > 0)
486                         ret = ret + days + "d ";
487                     if (hours > 0 || days > 0)
488                         ret = ret + hours + "h ";
489                     if (minutes > 0 || hours > 0 || days > 0)
490                         ret = ret + minutes + "m ";
491                     ret = ret + seconds + "s";
492
493                     return ret;
494                 },
495                 help: "Access points contain a high-precision timestamp which can be used to estimate how long the access point has been running.  Typically access points start this value at zero on boot, but it may be set to an arbitrary number and is not always accurate.",
496             },
497
498             {
499                 field: "dot11_fingerprint_group",
500                 liveupdate: true,
501                 groupTitle: "Fingerprints",
502                 id: "dot11_fingerprint_group",
503                 fields: [
504                 {
505                     field: "dot11.device/dot11.device.beacon_fingerprint",
506                     liveupdate: true,
507                     title: "Beacon",
508                     empty: "<i>None</i>",
509                     help: "Kismet uses attributes included in beacons to build a fingerprint of a device.  This fingerprint is used to identify spoofed devices, whitelist devices, and to attempt to provide attestation about devices.  The beacon fingerprint is only available when a beacon is seen from an access point.",
510                 }
511                 ],
512             },
513
514             {
515                 field: "dot11_packet_group",
516                 liveupdate: true,
517                 groupTitle: "Packets",
518                 id: "dot11_packet_group",
519
520                 fields: [
521                 {
522                     field: "graph_field_dot11",
523                     liveupdate: true,
524                     span: true,
525                     render: function(opts) {
526                         var d = 
527                             $('<div>')
528                             .append(
529                                 $('<div>', {
530                                     style: 'width: 50%; height: 200px; padding-bottom: 5px; float: left;',
531                                 })
532                                 .append('<div><b><center>Overall Packets</center></b></div>')
533                                 .append(
534                                     $('<canvas>', {
535                                         id: 'overalldonut',
536                                     })
537                                 )
538                             )
539                             .append(
540                                 $('<div>', {
541                                     style: 'width: 50%; height: 200px; padding-bottom: 5px; float: right;',
542                                 })
543                                 .append('<div><b><center>Data Packets</center></b></div>')
544                                 .append(
545                                     $('<canvas>', {
546                                         id: 'datadonut',
547                                     })
548                                 )
549                             );
550
551                         return d;
552                     },
553                     draw: function(opts) {
554
555                         var overalllegend = ['Management', 'Data'];
556                         var overalldata = [
557                             opts['data']['kismet.device.base.packets.llc'],
558                             opts['data']['kismet.device.base.packets.data'],
559                         ];
560                         var colors = [
561                             'rgba(46, 99, 162, 1)',
562                             'rgba(96, 149, 212, 1)',
563                             'rgba(136, 189, 252, 1)',
564                         ];
565
566                         var barChartData = {
567                             labels: overalllegend,
568
569                             datasets: [{
570                                 label: 'Dataset 1',
571                                 backgroundColor: colors,
572                                 borderWidth: 0,
573                                 data: overalldata,
574                             }],
575                         };
576
577                         if ('dot11overalldonut' in window[storage]) {
578                             window[storage].dot11overalldonut.data.datasets[0].data = overalldata;
579                             window[storage].dot11overalldonut.update();
580                         } else {
581                             window[storage].dot11overalldonut = 
582                                 new Chart($('#overalldonut', opts['container']), {
583                                     type: 'doughnut',
584                                     data: barChartData,
585                                     options: {
586                                         global: {
587                                             maintainAspectRatio: false,
588                                         },
589                                         animation: false,
590                                         legend: {
591                                             display: true,
592                                             position: 'bottom',
593                                         },
594                                         title: {
595                                             display: false,
596                                             text: 'Packet Types'
597                                         },
598                                         height: '200px',
599                                     }
600                                 });
601
602                             window[storage].dot11overalldonut.render();
603                         }
604
605                         var datalegend = ['Data', 'Retry', 'Frag'];
606                         var datadata = [
607                             opts['data']['kismet.device.base.packets.data'],
608                             opts['data']['dot11.device']['dot11.device.num_retries'],
609                             opts['data']['dot11.device']['dot11.device.num_fragments'],
610                         ];
611
612                         var databarChartData = {
613                             labels: datalegend,
614
615                             datasets: [{
616                                 label: 'Dataset 1',
617                                 backgroundColor: colors,
618                                 borderWidth: 0,
619                                 data: datadata,
620                             }],
621                         };
622
623                         if ('dot11datadonut' in window[storage]) {
624                             window[storage].dot11datadonut.data.datasets[0].data = datadata;
625                             window[storage].dot11datadonut.update();
626                         } else {
627                             window[storage].dot11datadonut = 
628                                 new Chart($('#datadonut', opts['container']), {
629                                     type: 'doughnut',
630                                     data: databarChartData,
631                                     options: {
632                                         global: {
633                                             maintainAspectRatio: false,
634                                         },
635                                         animation: false,
636                                         legend: {
637                                             display: true,
638                                             position: 'bottom'
639                                         },
640                                         title: {
641                                             display: false,
642                                             text: 'Packet Types'
643                                         },
644                                         height: '200px',
645                                     }
646                                 });
647
648                             window[storage].dot11datadonut.render();
649                         }
650
651                     }
652                 },
653                 {
654                     field: "kismet.device.base.packets.total",
655                     liveupdate: true,
656                     title: "Total Packets",
657                     help: "Total packet count seen of all packet types",
658                 },
659                 {
660                     field: "kismet.device.base.packets.llc",
661                     liveupdate: true,
662                     title: "LLC/Management",
663                     help: "LLC and Management packets define Wi-Fi networks.  They include packets like beacons, probe requests and responses, and other packets.  Access points will almost always have significantly more management packets than any other type.",
664                 },
665                 {
666                     field: "kismet.device.base.packets.data",
667                     liveupdate: true,
668                     title: "Data Packets",
669                     help: "Wi-Fi data packets encode the actual data being sent by the device.",
670                 },
671                 {
672                     field: "kismet.device.base.packets.error",
673                     liveupdate: true,
674                     title: "Error/Invalid Packets",
675                     help: "Invalid Wi-Fi packets are packets which have become corrupted in the air or which are otherwise invalid.  Typically these packets are discarded instead of tracked because the validity of their contents cannot be verified, so this will often be 0.",
676                 },
677                 {
678                     field: "dot11.device/dot11.device.num_fragments",
679                     liveupdate: true,
680                     title: "Fragmented Packets",
681                     help: "The data being sent over Wi-Fi can be fragmented into smaller packets.  Typically this is not desirable because it increases the packet load and can add latency to TCP connections.",
682                 },
683                 {
684                     field: "dot11.device/dot11.device.num_retries",
685                     liveupdate: true,
686                     title: "Retried Packets",
687                     help: "If a Wi-Fi data packet cannot be transmitted (due to weak signal, interference, or collisions with other packets transmitted at the same time), the Wi-Fi layer will automatically attempt to retransmit it a number of times.  In busy environments, a retransmit rate of 50% or higher is not unusual.",
688                 },
689                 {
690                     field: "dot11.device/dot11.device.datasize",
691                     liveupdate: true,
692                     title: "Data (size)",
693                     draw: kismet_ui.RenderHumanSize,
694                     help: "The amount of data transmitted by this device",
695                 },
696                 {
697                     field: "dot11.device/dot11.device.datasize_retry",
698                     liveupdate: true,
699                     title: "Retried Data",
700                     draw: kismet_ui.RenderHumanSize,
701                     help: "The amount of data re-transmitted by this device, due to lost packets and automatic retry.",
702                 }
703                 ],
704             },
705
706             {
707                 field: "dot11_extras",
708                 id: "dot11_extras",
709                 help: "Some devices advertise additional information about capabilities via additional tag fields when joining a network.",
710                 filter: function(opts) {
711                     try {
712                         if (opts['data']['dot11.device']['dot11.device.min_tx_power'] != 0)
713                             return true;
714
715                         if (opts['data']['dot11.device']['dot11.device.max_tx_power'] != 0)
716                             return true;
717
718                         if (opts['data']['dot11.device']['dot11.device.supported_channels'].length > 0)
719                             return true;
720
721                         return false;
722                     } catch (error) {
723                         return false;
724                     }
725                     
726                 },
727                 groupTitle: "Additional Capabilities",
728                 fields: [
729                 {
730                     field: "dot11.device/dot11.device.min_tx_power",
731                     title: "Minimum TX",
732                     help: "Some devices advertise their minimum transmit power in association requests.  This data is in the IE 33 field.  This data could be manipulated by hostile devices, but can be informational for normal devices.",
733                     filterOnZero: true
734                 },
735                 {
736                     field: "dot11.device/dot11.device.max_tx_power",
737                     title: "Maximum TX",
738                     help: "Some devices advertise their maximum transmit power in association requests.  This data is in the IE 33 field.  This data could be manipulated by hostile devices, but can be informational for normal devices.",
739                     filterOnZero: true
740                 },
741                 {
742                     field: "dot11.device/dot11.device.supported_channels",
743                     title: "Supported Channels",
744                     help: "Some devices advertise the 5GHz channels they support while joining a network.  Supported 2.4GHz channels are not included in this list.  This data is in the IE 36 field.  This data can be manipulated by hostile devices, but can be informational for normal deices.",
745                     filter: function(opts) {
746                         try {
747                             return (opts['data']['dot11.device']['dot11.device.supported_channels'].length);
748                         } catch (error) {
749                             return false;
750                         }
751                     },
752                     draw: function(opts) { 
753                         try {
754                             return opts['data']['dot11.device']['dot11.device.supported_channels'].join(',');
755                         } catch (error) {
756                             return "<i>n/a</i>";
757                         }
758                     }
759                 },
760                 ],
761             },
762
763             {
764                 field: "dot11.device/dot11.device.wpa_handshake_list",
765                 id: "wpa_handshake_title",
766                 filter: function(opts) {
767                     try {
768                         return (Object.keys(opts['data']['dot11.device']['dot11.device.wpa_handshake_list']).length);
769                     } catch (error) {
770                         return false;
771                     }
772                 },
773                 groupTitle: "WPA Handshakes",
774             },
775             {
776                 field: "dot11.device/dot11.device.wpa_handshake_list",
777                 id: "wpa_handshake",
778                 help: "When a client joins a WPA network, it performs a &quot;handshake&quot; of four packets to establish the connection and the unique per-session key.  To decrypt WPA or derive the PSK, at least two specific packets of this handshake are required.  Kismet provides a simplified pcap file of the handshake packets seen, which can be used with other tools to derive the PSK or decrypt the packet stream.",
779                 filter: function(opts) {
780                     try {
781                         return (Object.keys(opts['data']['dot11.device']['dot11.device.wpa_handshake_list']).length);
782                     } catch (error) {
783                         return false;
784                     }
785                 },
786                 groupIterate: true,
787
788                 fields: [
789                 {
790                     field: "device",
791                     id: "device",
792                     title: "Client MAC",
793                     draw: function(opts) {
794                         return kismet.censorMAC(opts['index']);
795                     },
796                 },
797                 {
798                     field: "hsnums",
799                     id: "hsnums",
800                     title: "Packets",
801                     draw: function(opts) {
802                         var hs = 0;
803                         for (const p of kismet.ObjectByString(opts['data'], opts['basekey'])) {
804                             hs = hs | (1 << p['dot11.eapol.message_num']);
805                         }
806
807                         var n = "";
808
809                         for (const p of [1, 2, 3, 4]) {
810                             if (hs & (1 << p)) {
811                                 n = n + p + " ";
812                             }
813                         }
814
815                         return n;
816                     }
817                 },
818                 {
819                     field: "wpa_handshake_download",
820                     id: "handshake_download",
821                     title: "Handshake PCAP",
822                     draw: function(opts) {
823                         var hs = 0;
824                         for (const p of kismet.ObjectByString(opts['data'], opts['basekey'])) {
825                             hs = hs | (1 << p['dot11.eapol.message_num']);
826                         }
827
828                         // We need packets 1&2 or 2&3 to be able to crack the handshake
829                         var warning = "";
830                         if (hs != 30) {
831                             warning = '<br><i style="color: red;">While handshake packets have been seen, a complete 4-way handshake has not been observed.  You may still be able to utilize the partial handshake.</i>';
832                         }
833
834                         var key = opts['data']['kismet.device.base.key'];
835                         var url = `<a href="phy/phy80211/by-key/${key}/device/${opts['index']}/pcap/handshake.pcap">` +
836                             '<i class="fa fa-download"></i> Download Pcap File</a>' +
837                             warning;
838                         return url;
839                     },
840                 }
841                 ]
842             },
843
844             {
845                 field: "dot11.device/dot11.device.pmkid_packet",
846                 id: "wpa_rsn_pmkid",
847                 help: "Some access points disclose the RSN PMKID during the first part of the authentication process.  This can be used to attack the PSK via tools like Aircrack-NG or Hashcat.  If a RSN PMKID packet is seen, Kismet can provide a pcap file.",
848                 filterOnZero: true,
849                 filterOnEmpty: true,
850                 groupTitle: "WPA RSN PMKID",
851
852                 fields: [
853                 {
854                     field: "pmkid_download",
855                     id: "pmkid_download",
856                     title: "WPA PMKID PCAP",
857                     draw: function(opts) {
858                         var key = opts['data']['kismet.device.base.key'];
859                         var url = '<a href="phy/phy80211/by-key/' + key + '/pcap/handshake-pmkid.pcap">' +
860                             '<i class="fa fa-download"></i> Download Pcap File</a>'; 
861                         return url;
862                     },
863                 }
864                 ]
865             },
866
867             {
868                 // Filler title
869                 field: "dot11.device/dot11.device.probed_ssid_map",
870                 id: "probed_ssid_header",
871                 filter: function(opts) {
872                     try {
873                         return (Object.keys(opts['data']['dot11.device']['dot11.device.probed_ssid_map']).length >= 1);
874                     } catch (error) {
875                         return false;
876                     }
877                 },
878                 title: '<b class="k_padding_title">Probed SSIDs</b>',
879                 help: "Wi-Fi clients will send out probe requests for networks they are trying to join.  Probe requests can either be broadcast requests, requesting any network in the area respond, or specific requests, requesting a single SSID the client has used previously.  Different clients may behave differently, and modern clients will typically only send generic broadcast probes.",
880             },
881
882             {
883                 field: "dot11.device/dot11.device.probed_ssid_map",
884                 id: "probed_ssid",
885
886                 filter: function(opts) {
887                     try {
888                         return (Object.keys(opts['data']['dot11.device']['dot11.device.probed_ssid_map']).length >= 1);
889                     } catch (error) {
890                         return false;
891                     }
892                 },
893
894                 groupIterate: true,
895
896                 iterateTitle: function(opts) {
897                     var lastprobe = opts['value'][opts['index']];
898                     var lastpssid = lastprobe['dot11.probedssid.ssid'];
899                     var key = "probessid" + opts['index'];
900
901                     if (lastpssid === '')
902                         lastpssid = "<i>Broadcast</i>";
903
904                     if (lastpssid.replace(/\s/g, '').length == 0) 
905                         lastpssid = '<i>Empty (' + lastpssid.length + ' spaces)</i>'
906
907                     return '<a id="' + key + '" class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Probed SSID ' + lastpssid + '</a>';
908                 },
909
910                 draw: function(opts) {
911                     var tb = $('.expander', opts['cell']).simpleexpand();
912                 },
913
914                 fields: [
915                 {
916                     field: "dot11.probedssid.ssid",
917                     title: "Probed SSID",
918                     empty: "<i>Broadcast</i>",
919                     draw: function(opts) {
920                         if (typeof(opts['value']) === 'undefined')
921                             return '<i>None</i>';
922                         if (opts['value'].replace(/\s/g, '').length == 0) 
923                             return 'Empty (' + opts['value'].length + ' spaces)'
924                         return opts['value'];
925                     },
926                 },
927                 {
928                     field: "dot11.probedssid.wpa_mfp_required",
929                     title: "MFP",
930                     help: "Management Frame Protection (MFP) attempts to mitigate denial of service attacks by authenticating management packets.  It can be part of the 802.11w Wi-Fi standard, or proprietary Cisco extensions.",
931                     draw: function(opts) {
932                         if (opts['value'])
933                             return "Required (802.11w)";
934
935                         if (opts['base']['dot11.probedssid.wpa_mfp_supported'])
936                             return "Supported (802.11w)";
937
938                         return "Unavailable";
939                     }
940                 },
941                 {
942                     field: "dot11.probedssid.first_time",
943                     liveupdate: true,
944                     title: "First Seen",
945                     draw: kismet_ui.RenderTrimmedTime,
946                 },
947                 {
948                     field: "dot11.probedssid.last_time",
949                     liveupdate: true,
950                     title: "Last Seen",
951                     draw: kismet_ui.RenderTrimmedTime,
952                 },
953
954                 {
955                     // Location is its own group
956                     groupTitle: "Avg. Location",
957                     id: "avg.dot11.probedssid.location",
958
959                     // Group field
960                     groupField: "dot11.probedssid.location",
961
962                     liveupdate: true,
963
964                     // Don't show location if we don't know it
965                     filter: function(opts) {
966                         return (kismet.ObjectByString(opts['base'], "dot11.probedssid.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
967                     },
968
969                     fields: [
970                         {
971                             field: "kismet.common.location.avg_loc/kismet.common.location.geopoint",
972                             title: "Location",
973                             liveupdate: true,
974                             draw: function(opts) {
975                                 try {
976                                     if (opts['value'][1] == 0 || opts['value'][0] == 0)
977                                         return "<i>Unknown</i>";
978
979                                     return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
980                                 } catch (error) {
981                                     return "<i>Unknown</i>";
982                                 }
983                             }
984                         },
985                         {
986                             field: "kismet.common.location.avg_loc/kismet.common.location.alt",
987                             title: "Altitude",
988                             liveupdate: true,
989                             filter: function(opts) {
990                                 return (kismet.ObjectByString(opts['base'], "kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
991                             },
992                             draw: function(opts) {
993                                 try {
994                                     return kismet_ui.renderHeightDistance(opts['value']);
995                                 } catch (error) {
996                                     return "<i>Unknown</i>";
997                                 }
998                             },
999                         }
1000                     ],
1001                 },
1002
1003                 {
1004                     field: "dot11.probedssid.dot11r_mobility",
1005                     title: "802.11r Mobility",
1006                     filterOnZero: true,
1007                     help: "The 802.11r standard allows for fast roaming between access points on the same network.  Typically this is found on enterprise-level access points, on a network where multiple APs service the same area.",
1008                     draw: function(opts) { return "Enabled"; }
1009                 },
1010                 {
1011                     field: "dot11.probedssid.dot11r_mobility_domain_id",
1012                     title: "Mobility Domain",
1013                     filterOnZero: true,
1014                     help: "The 802.11r standard allows for fast roaming between access points on the same network."
1015                 },
1016                 {
1017                     field: "dot11.probedssid.wps_manuf",
1018                     title: "WPS Manufacturer",
1019                     filterOnEmpty: true,
1020                     help: "Clients which support Wi-Fi Protected Setup (WPS) may include the device manufacturer in the WPS advertisements.  WPS is not recommended due to security flaws."
1021                 },
1022                 {
1023                     field: "dot11.probedssid.wps_device_name",
1024                     title: "WPS Device",
1025                     filterOnEmpty: true,
1026                     help: "Clients which support Wi-Fi Protected Setup (WPS) may include the device name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1027                 },
1028                 {
1029                     field: "dot11.probedssid.wps_model_name",
1030                     title: "WPS Model",
1031                     filterOnEmpty: true,
1032                     help: "Clients which support Wi-Fi Protected Setup (WPS) may include the specific device model name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1033                 },
1034                 {
1035                     field: "dot11.probedssid.wps_model_number",
1036                     title: "WPS Model #",
1037                     filterOnEmpty: true,
1038                     help: "Clients which support Wi-Fi Protected Setup (WPS) may include the specific model number in the WPS advertisements.  WPS is not recommended due to security flaws.",
1039                 },
1040                 {
1041                     field: "dot11.probedssid.wps_serial_number",
1042                     title: "WPS Serial #",
1043                     filterOnEmpty: true,
1044                     help: "Clients which support Wi-Fi Protected Setup (WPS) may include the device serial number in the WPS advertisements.  This information is not always valid or useful.  WPS is not recommended due to security flaws.",
1045                 },
1046
1047                 ],
1048             },
1049
1050             {
1051                 // Filler title
1052                 field: "dot11.device/dot11.device.advertised_ssid_map",
1053                 id: "advertised_ssid_header",
1054                 filter: function(opts) {
1055                     try {
1056                         return (Object.keys(opts['data']['dot11.device']['dot11.device.advertised_ssid_map']).length >= 1);
1057                     } catch (error) {
1058                         return false;
1059                     }
1060                 },
1061                 title: '<b class="k_padding_title">Advertised SSIDs</b>',
1062                 help: "A single BSSID may advertise multiple SSIDs, either changing its network name over time or combining multiple SSIDs into a single BSSID radio address.  Most modern Wi-Fi access points which support multiple SSIDs will generate a dynamic MAC address for each SSID.  Advertised SSIDs have been seen transmitted from the access point in a beacon.",
1063             },
1064
1065             {
1066                 field: "dot11.device/dot11.device.advertised_ssid_map",
1067                 id: "advertised_ssid",
1068
1069                 filter: function(opts) {
1070                     try {
1071                         return (Object.keys(opts['data']['dot11.device']['dot11.device.advertised_ssid_map']).length >= 1);
1072                     } catch (error) {
1073                         return false;
1074                     }
1075                 },
1076
1077                 groupIterate: true,
1078                 iterateTitle: function(opts) {
1079                     var lastssid = opts['value'][opts['index']]['dot11.advertisedssid.ssid'];
1080                     var lastowessid = opts['value'][opts['index']]['dot11.advertisedssid.owe_ssid'];
1081
1082                     if (lastssid === '') {
1083                         if ('dot11.advertisedssid.owe_ssid' in opts['value'][opts['index']] && lastowessid !== '') {
1084                             return "SSID: " + lastowessid + "  <i>(OWE)</i>";
1085                         }
1086
1087                         return "SSID: <i>Unknown</i>";
1088                     }
1089
1090                     return "SSID: " + lastssid;
1091                 },
1092                 fields: [
1093                 {
1094                     field: "dot11.advertisedssid.ssid",
1095                     title: "SSID",
1096                     draw: function(opts) {
1097                         if (opts['value'].replace(/\s/g, '').length == 0) {
1098                             if ('dot11.advertisedssid.owe_ssid' in opts['base']) {
1099                                 return "<i>SSID advertised as OWE</i>";
1100                             } else {
1101                                 return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
1102                             }
1103                         }
1104
1105                         return opts['value'];
1106                     },
1107                     help: "Advertised SSIDs can be any data, up to 32 characters.  Some access points attempt to cloak the SSID by sending blank spaces or an empty string; these SSIDs can be discovered when a client connects to the network.",
1108                 },
1109                 {
1110                     field: "dot11.advertisedssid.owe_ssid",
1111                     liveupdate: true,
1112                     title: "OWE SSID",
1113                     filterOnEmpty: true,
1114                     help: "Opportunistic Wireless Encryption (OWE) advertises the original SSID on an alternate BSSID.",
1115                 },
1116                 {
1117                     field: "dot11.advertisedssid.owe_bssid",
1118                     liveupdate: true,
1119                     title: "OWE BSSID",
1120                     filterOnEmpty: true,
1121                     help: "Opportunistic Wireless Encryption (OWE) advertises the original SSID with a reference to the linked BSSID.",
1122                     draw: function(opts) {
1123                         $.get(local_uri_prefix + "devices/by-mac/" + opts['value'] + "/devices.json")
1124                         .fail(function() {
1125                             opts['container'].html(opts['value']);
1126                         })
1127                         .done(function(clidata) {
1128                             clidata = kismet.sanitizeObject(clidata);
1129
1130                             for (var cl of clidata) {
1131                                 if (cl['kismet.device.base.phyname'] === 'IEEE802.11') {
1132                                     opts['container'].html(opts['value'] + ' <a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + cl['kismet.device.base.key'] + '\')">View AP Details</a>');
1133                                     return;
1134                                 }
1135
1136                             }
1137                             opts['container'].html(opts['value']);
1138                         });
1139                     },
1140                 },
1141                 {
1142                     field: "dot11.advertisedssid.dot11s.meshid",
1143                     liveupdate: true,
1144                     title: "Mesh ID",
1145                     filterOnEmpty: true,
1146                     help: "802.11s mesh id, present only in meshing networks.",
1147                 },
1148                 {
1149                     field: "dot11.advertisedssid.crypt_set",
1150                     liveupdate: true,
1151                     title: "Encryption",
1152                     draw: function(opts) {
1153                         return CryptToHumanReadable(opts['value']);
1154                     },
1155                     help: "Encryption at the Wi-Fi layer (open, WEP, and WPA) is defined by the beacon sent by the access point advertising the network.  Layer 3 encryption (such as VPNs) is added later and is not advertised as part of the network itself.",
1156                 },
1157                 {
1158                     field: "dot11.advertisedssid.wpa_mfp_required",
1159                     liveupdate: true,
1160                     title: "MFP",
1161                     help: "Management Frame Protection (MFP) attempts to mitigate denial of service attacks by authenticating management packets.  It can be part of the Wi-Fi 802.11w standard or a custom Cisco extension.",
1162                     draw: function(opts) {
1163                         if (opts['value'])
1164                             return "Required (802.11w)";
1165
1166                         if (opts['base']['dot11.advertisedssid.wpa_mfp_supported'])
1167                             return "Supported (802.11w)";
1168
1169                         if (opts['base']['dot11.advertisedssid.cisco_client_mfp'])
1170                             return "Supported (Cisco)";
1171
1172                         return "Unavailable";
1173                     }
1174                 },
1175                 {
1176                     field: "dot11.advertisedssid.channel",
1177                     liveupdate: true,
1178                     title: "Channel",
1179                     help: "Wi-Fi networks on 2.4GHz (channels 1 through 14) are required to include a channel in the advertisement because channel overlap makes it impossible to determine the exact channel the access point is transmitting on.  Networks on 5GHz channels are typically not required to include the channel.",
1180                 },
1181                 {
1182                     field: "dot11.advertisedssid.ht_mode",
1183                     liveupdate: true,
1184                     title: "HT Mode",
1185                     help: "802.11n and 802.11AC networks operate on expanded channels; HT40, HT80 HT160, or HT80+80 (found only on 802.11ac wave2 gear).",
1186                     filterOnEmpty: true
1187                 },
1188                 {
1189                     field: "dot11.advertisedssid.ht_center_1",
1190                     liveupdate: true,
1191                     title: "HT Freq",
1192                     help: "802.11AC networks operate on expanded channels.  This is the frequency of the center of the expanded channel.",
1193                     filterOnZero: true,
1194                     draw: function(opts) {
1195                         return opts['value'] + " (Channel " + (opts['value'] - 5000) / 5 + ")";
1196                     },
1197                 },
1198                 {
1199                     field: "dot11.advertisedssid.ht_center_2",
1200                     liveupdate: true,
1201                     title: "HT Freq2",
1202                     help: "802.11AC networks operate on expanded channels.  This is the frequency of the center of the expanded secondary channel.  Secondary channels are only found on 802.11AC wave-2 80+80 gear.",
1203                     filterOnZero: true,
1204                     draw: function(opts) {
1205                         return opts['value'] + " (Channel " + (opts['value'] - 5000) / 5 + ")";
1206                     },
1207                 },
1208                 {
1209                     field: "dot11.advertisedssid.beacon_info",
1210                     liveupdate: true,
1211                     title: "Beacon Info",
1212                     filterOnEmpty: true,
1213                     help: "Some access points, such as those made by Cisco, can include arbitrary custom info in beacons.  Typically this is used by the network administrators to map where access points are deployed.",
1214                 },
1215                 {
1216                     field: "dot11.advertisedssid.dot11s.gateway",
1217                     liveupdate: true,
1218                     title: "Mesh Gateway",
1219                     help: "An 802.11s mesh device in gateway mode bridges Wi-Fi traffic to another network, such as a wired Ethernet connection.",
1220                     filterOnEmpty: true,
1221                     draw: function(opts) {
1222                         if (opts['value'] == 1)
1223                             return "Enabled";
1224                         return "Disabled (not a gateway)";
1225                     }
1226                 },
1227                 {
1228                     field: "dot11.advertisedssid.dot11s.forwarding",
1229                     liveupdate: true,
1230                     title: "Mesh Forwarding",
1231                     help: "An 802.11s mesh device may forward packets to another mesh device for delivery.",
1232                     filterOnEmpty: true,
1233                     draw: function(opts) {
1234                         if (opts['value'] == 1)
1235                             return "Forwarding";
1236                         return "No";
1237                     }
1238                 },
1239                 {
1240                     field: "dot11.advertisedssid.dot11s.num_peerings",
1241                     liveupdate: true,
1242                     title: "Mesh Peers",
1243                     help: "Number of mesh peers for this device in an 802.11s network.",
1244                     filterOnEmpty: true,
1245                 },
1246                 {
1247                     field: "dot11.advertisedssid.dot11e_qbss_stations",
1248                     liveupdate: true,
1249                     title: "Connected Stations",
1250                     help: "Access points which provide 802.11e / QBSS report the number of stations observed on the channel as part of the channel quality of service.",
1251                     filter: function(opts) {
1252                         try {
1253                             return (opts['base']['dot11.advertisedssid.dot11e_qbss'] == 1);
1254                         } catch (error) {
1255                             return false;
1256                         }
1257                     }
1258                 },
1259                 {
1260                     field: "dot11.advertisedssid.dot11e_channel_utilization_perc",
1261                     liveupdate: true,
1262                     title: "Channel Utilization",
1263                     help: "Access points which provide 802.11e / QBSS calculate the estimated channel saturation as part of the channel quality of service.",
1264                     draw: function(opts) {
1265                         var perc = "n/a";
1266
1267                         if (opts['value'] == 0) {
1268                             perc = "0%";
1269                         } else {
1270                             perc = Number.parseFloat(opts['value']).toPrecision(4) + "%";
1271                         }
1272
1273                         return '<div class="percentage-border"><span class="percentage-text">' + perc + '</span><div class="percentage-fill" style="width:' + opts['value'] + '%"></div></div>';
1274                     },
1275                     filter: function(opts) {
1276                         try {
1277                             return (opts['base']['dot11.advertisedssid.dot11e_qbss'] == 1);
1278                         } catch (error) {
1279                             return false;
1280                         }
1281                     }
1282                 },
1283                 {
1284                     field: "dot11.advertisedssid.ccx_txpower",
1285                     liveupdate: true,
1286                     title: "Cisco CCX TxPower",
1287                     filterOnZero: true,
1288                     help: "Cisco access points may advertise their transmit power in a Cisco CCX IE tag.  Typically this is found on enterprise-level access points, where multiple APs service the same area.",
1289                     draw: function(opts) {
1290                         return opts['value'] + "dBm";
1291                     },
1292                 },
1293                 {
1294                     field: "dot11.advertisedssid.dot11r_mobility",
1295                     liveupdate: true,
1296                     title: "802.11r Mobility",
1297                     filterOnZero: true,
1298                     help: "The 802.11r standard allows for fast roaming between access points on the same network.  Typically this is found on enterprise-level access points, on a network where multiple APs service the same area.",
1299                     draw: function(opts) { return "Enabled"; }
1300                 },
1301                 {
1302                     field: "dot11.advertisedssid.dot11r_mobility_domain_id",
1303                     liveupdate: true,
1304                     title: "Mobility Domain",
1305                     filterOnZero: true,
1306                     help: "The 802.11r standard allows for fast roaming between access points on the same network."
1307                 },
1308                 {
1309                     field: "dot11.advertisedssid.first_time",
1310                     liveupdate: true,
1311                     title: "First Seen",
1312                     draw: kismet_ui.RenderTrimmedTime,
1313                 },
1314                 {
1315                     field: "dot11.advertisedssid.last_time",
1316                     liveupdate: true,
1317                     title: "Last Seen",
1318                     draw: kismet_ui.RenderTrimmedTime,
1319                 },
1320
1321                 {
1322                     // Location is its own group
1323                     groupTitle: "Avg. Location",
1324                     id: "avg.dot11.advertisedssid.location",
1325
1326                     // Group field
1327                     groupField: "dot11.advertisedssid.location",
1328
1329                     liveupdate: true,
1330
1331                     // Don't show location if we don't know it
1332                     filter: function(opts) {
1333                         return (kismet.ObjectByString(opts['base'], "dot11.advertisedssid.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1334                     },
1335
1336                     fields: [
1337                         {
1338                             field: "kismet.common.location.avg_loc/kismet.common.location.geopoint",
1339                             title: "Location",
1340                             liveupdate: true,
1341                             draw: function(opts) {
1342                                 try {
1343                                     if (opts['value'][1] == 0 || opts['value'][0] == 0)
1344                                         return "<i>Unknown</i>";
1345
1346                                     return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1347                                 } catch (error) {
1348                                     return "<i>Unknown</i>";
1349                                 }
1350                             }
1351                         },
1352                         {
1353                             field: "kismet.common.location.avg_loc/kismet.common.location.alt",
1354                             title: "Altitude",
1355                             liveupdate: true,
1356                             filter: function(opts) {
1357                                 return (kismet.ObjectByString(opts['base'], "kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1358                             },
1359                             draw: function(opts) {
1360                                 try {
1361                                     return kismet_ui.renderHeightDistance(opts['value']);
1362                                 } catch (error) {
1363                                     return "<i>Unknown</i>";
1364                                 }
1365                             },
1366                         }
1367                     ],
1368                 },
1369
1370                 {
1371                     field: "dot11.advertisedssid.beaconrate",
1372                     liveupdate: true,
1373                     title: "Beacon Rate",
1374                     draw: function(opts) {
1375                         return opts['value'] + '/sec';
1376                     },
1377                     help: "Wi-Fi typically beacons at 10 packets per second; normally there is no reason for an access point to change this rate, but it may be changed in some situations where a large number of SSIDs are hosted on a single access point.",
1378                 },
1379                 {
1380                     field: "dot11.advertisedssid.maxrate",
1381                     liveupdate: true,
1382                     title: "Max. Rate",
1383                     draw: function(opts) {
1384                         return opts['value'] + ' MBit/s';
1385                     },
1386                     help: "The maximum basic transmission rate supported by this access point",
1387                 },
1388                 {
1389                     field: "dot11.advertisedssid.dot11d_country",
1390                     liveupdate: true,
1391                     title: "802.11d Country",
1392                     filterOnEmpty: true,
1393                     help: "The 802.11d standard required access points to identify their operating country code and signal levels.  This caused clients connecting to those access points to adopt the same regulatory requirements.  802.11d has been phased out and is not found on most modern access points but may still be seen on older hardware.",
1394                 },
1395                 {
1396                     field: "dot11.advertisedssid.wps_manuf",
1397                     liveupdate: true,
1398                     title: "WPS Manufacturer",
1399                     filterOnEmpty: true,
1400                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device manufacturer in the WPS advertisements.  WPS is not recommended due to security flaws."
1401                 },
1402                 {
1403                     field: "dot11.advertisedssid.wps_device_name",
1404                     liveupdate: true,
1405                     title: "WPS Device",
1406                     filterOnEmpty: true,
1407                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1408                 },
1409                 {
1410                     field: "dot11.advertisedssid.wps_model_name",
1411                     liveupdate: true,
1412                     title: "WPS Model",
1413                     filterOnEmpty: true,
1414                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the specific device model name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1415                 },
1416                 {
1417                     field: "dot11.advertisedssid.wps_model_number",
1418                     liveupdate: true,
1419                     title: "WPS Model #",
1420                     filterOnEmpty: true,
1421                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the specific model number in the WPS advertisements.  WPS is not recommended due to security flaws.",
1422                 },
1423                 {
1424                     field: "dot11.advertisedssid.wps_serial_number",
1425                     liveupdate: true,
1426                     title: "WPS Serial #",
1427                     filterOnEmpty: true,
1428                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device serial number in the WPS advertisements.  This information is not always valid or useful.  WPS is not recommended due to security flaws.",
1429                 },
1430
1431                 {
1432                     field: "dot11.advertisedssid.ie_tag_content",
1433                     liveupdate: true,
1434                     filterOnEmpty: true,
1435                     id: "dot11_ssid_ietags",
1436                     title: '<b class="k_padding_title">IE tags</b>',
1437                     help: "IE tags in beacons define the network and the access point attributes; because dot11_keep_ietags is true, Kismet tracks these here.",
1438                 },
1439
1440                 {
1441                     field: "dot11.advertisedssid.ie_tag_content",
1442                     liveupdate: true,
1443                     id: "advertised_ietags",
1444                     filterOnEmpty: true,
1445                     span: true,
1446
1447                     render: function(opts) {
1448                         return '<table id="tagdump" border="0" />';
1449                     },
1450
1451                     draw: function(opts) {
1452                         $('table#tagdump', opts['container']).empty();
1453                         for (var ie in opts['value']) {
1454                             var tag = opts['value'][ie];
1455
1456                             var pretty_tag = 
1457                                 $('<tr>', {
1458                                     class: 'alternating'
1459                                 })
1460                                 .append(
1461                                     $('<td>', {
1462                                         width: "25%",
1463                                         id: "tagno"
1464                                     })
1465                                     .append(
1466                                         $('<div>')
1467                                         .html("<b>" + tag['dot11.ietag.number'] + "</b>")
1468                                     )
1469                                 )
1470                                 .append(
1471                                     $('<td>', {
1472                                         id: "hexdump"
1473                                     })
1474                                 );
1475
1476                             if (tag['dot11.ietag.oui'] != 0) {
1477                                 var oui = ("000000" + tag['dot11.ietag.oui'].toString(16)).substr(-6).replace(/(..)/g, '$1:').slice(0, -1);
1478
1479                                 if (tag['dot11.ietag.oui_manuf'].length != 0)
1480                                     oui = oui + " (" + tag['dot11.ietag.oui_manuf'] + ")";
1481
1482                                 $('#tagno', pretty_tag).append(
1483                                     $('<div>')
1484                                     .text(oui)
1485                                 );
1486                             }
1487
1488                             if (tag['dot11.ietag.subtag'] >= 0) {
1489                                 $('#tagno', pretty_tag).append(
1490                                     $('<div>')
1491                                     .text("Subtag " + tag['dot11.ietag.subtag'])
1492                                 )
1493                             }
1494
1495                             var hexdumps = pretty_hexdump(hexstr_to_bytes(tag['dot11.ietag.data']));
1496
1497                             for (var i in hexdumps) {
1498                                 $('#hexdump', pretty_tag).append(
1499                                     $('<div>')
1500                                     .append(
1501                                         $('<code>')
1502                                         .html(hexdumps[i])
1503                                     )
1504                                 )
1505                             }
1506
1507                             $('table#tagdump', opts['container']).append(pretty_tag);
1508                         }
1509                     },
1510
1511                 },
1512                 ],
1513             },
1514
1515
1516             {
1517                 // Filler title
1518                 field: "dot11.device/dot11.device.responded_ssid_map",
1519                 id: "responded_ssid_header",
1520                 filter: function(opts) {
1521                     try {
1522                         return (Object.keys(opts['data']['dot11.device']['dot11.device.responded_ssid_map']).length >= 1);
1523                     } catch (error) {
1524                         return false;
1525                     }
1526                 },
1527                 title: '<b class="k_padding_title">Responded SSIDs</b>',
1528                 help: "A single BSSID may advertise multiple SSIDs, either changing its network name over time or combining multiple SSIDs into a single BSSID radio address.  Most modern Wi-Fi access points which support multiple SSIDs will generate a dynamic MAC address for each SSID.  SSIDs in the responded categoy have been seen as a probe response from the access point; typically an access point should only respond for SSIDs it also advertises.",
1529             },
1530
1531             {
1532                 field: "dot11.device/dot11.device.responded_ssid_map",
1533                 id: "responded_ssid",
1534
1535                 filter: function(opts) {
1536                     try {
1537                         return (Object.keys(opts['data']['dot11.device']['dot11.device.responded_ssid_map']).length >= 1);
1538                     } catch (error) {
1539                         return false;
1540                     }
1541                 },
1542
1543                 groupIterate: true,
1544                 iterateTitle: function(opts) {
1545                     var lastssid = opts['value'][opts['index']]['dot11.advertisedssid.ssid'];
1546                     var lastowessid = opts['value'][opts['index']]['dot11.advertisedssid.owe_ssid'];
1547
1548                     if (lastssid === '') {
1549                         if ('dot11.advertisedssid.owe_ssid' in opts['value'][opts['index']] && lastowessid !== '') {
1550                             return "SSID: " + lastowessid + "  <i>(OWE)</i>";
1551                         }
1552
1553                         return "SSID: <i>Unknown</i>";
1554                     }
1555
1556                     return "SSID: " + lastssid;
1557                 },
1558                 fields: [
1559                 {
1560                     field: "dot11.advertisedssid.ssid",
1561                     title: "SSID",
1562                     draw: function(opts) {
1563                         if (opts['value'].replace(/\s/g, '').length == 0) {
1564                             if ('dot11.advertisedssid.owe_ssid' in opts['base']) {
1565                                 return "<i>SSID advertised as OWE</i>";
1566                             } else {
1567                                 return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
1568                             }
1569                         }
1570
1571                         return opts['value'];
1572                     },
1573                     help: "Advertised SSIDs can be any data, up to 32 characters.  Some access points attempt to cloak the SSID by sending blank spaces or an empty string; these SSIDs can be discovered when a client connects to the network.",
1574                 },
1575                 {
1576                     field: "dot11.advertisedssid.owe_ssid",
1577                     liveupdate: true,
1578                     title: "OWE SSID",
1579                     filterOnEmpty: true,
1580                     help: "Opportunistic Wireless Encryption (OWE) advertises the original SSID on an alternate BSSID.",
1581                 },
1582                 {
1583                     field: "dot11.advertisedssid.owe_bssid",
1584                     liveupdate: true,
1585                     title: "OWE BSSID",
1586                     filterOnEmpty: true,
1587                     help: "Opportunistic Wireless Encryption (OWE) advertises the original SSID with a reference to the linked BSSID.",
1588                     draw: function(opts) {
1589                         $.get(local_uri_prefix + "devices/by-mac/" + opts['value'] + "/devices.json")
1590                         .fail(function() {
1591                             opts['container'].html(opts['value']);
1592                         })
1593                         .done(function(clidata) {
1594                             clidata = kismet.sanitizeObject(clidata);
1595
1596                             for (var cl of clidata) {
1597                                 if (cl['kismet.device.base.phyname'] === 'IEEE802.11') {
1598                                     opts['container'].html(opts['value'] + ' <a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + cl['kismet.device.base.key'] + '\')">View AP Details</a>');
1599                                     return;
1600                                 }
1601
1602                             }
1603                             opts['container'].html(opts['value']);
1604                         });
1605                     },
1606                 },
1607                 {
1608                     field: "dot11.advertisedssid.crypt_set",
1609                     liveupdate: true,
1610                     title: "Encryption",
1611                     draw: function(opts) {
1612                         return CryptToHumanReadable(opts['value']);
1613                     },
1614                     help: "Encryption at the Wi-Fi layer (open, WEP, and WPA) is defined by the beacon sent by the access point advertising the network.  Layer 3 encryption (such as VPNs) is added later and is not advertised as part of the network itself.",
1615                 },
1616                 {
1617                     field: "dot11.advertisedssid.wpa_mfp_required",
1618                     liveupdate: true,
1619                     title: "MFP",
1620                     help: "Management Frame Protection (MFP) attempts to mitigate denial of service attacks by authenticating management packets.  It can be part of the Wi-Fi 802.11w standard or a custom Cisco extension.",
1621                     draw: function(opts) {
1622                         if (opts['value'])
1623                             return "Required (802.11w)";
1624
1625                         if (opts['base']['dot11.advertisedssid.wpa_mfp_supported'])
1626                             return "Supported (802.11w)";
1627
1628                         if (opts['base']['dot11.advertisedssid.cisco_client_mfp'])
1629                             return "Supported (Cisco)";
1630
1631                         return "Unavailable";
1632                     }
1633                 },
1634                 {
1635                     field: "dot11.advertisedssid.ht_mode",
1636                     liveupdate: true,
1637                     title: "HT Mode",
1638                     help: "802.11n and 802.11AC networks operate on expanded channels; HT40, HT80 HT160, or HT80+80 (found only on 802.11ac wave2 gear).",
1639                     filterOnEmpty: true
1640                 },
1641                 {
1642                     field: "dot11.advertisedssid.ht_center_1",
1643                     liveupdate: true,
1644                     title: "HT Freq",
1645                     help: "802.11AC networks operate on expanded channels.  This is the frequency of the center of the expanded channel.",
1646                     filterOnZero: true,
1647                     draw: function(opts) {
1648                         return opts['value'] + " (Channel " + (opts['value'] - 5000) / 5 + ")";
1649                     },
1650                 },
1651                 {
1652                     field: "dot11.advertisedssid.ht_center_2",
1653                     liveupdate: true,
1654                     title: "HT Freq2",
1655                     help: "802.11AC networks operate on expanded channels.  This is the frequency of the center of the expanded secondary channel.  Secondary channels are only found on 802.11AC wave-2 80+80 gear.",
1656                     filterOnZero: true,
1657                     draw: function(opts) {
1658                         return opts['value'] + " (Channel " + (opts['value'] - 5000) / 5 + ")";
1659                     },
1660                 },
1661                 {
1662                     field: "dot11.advertisedssid.ccx_txpower",
1663                     liveupdate: true,
1664                     title: "Cisco CCX TxPower",
1665                     filterOnZero: true,
1666                     help: "Cisco access points may advertise their transmit power in a Cisco CCX IE tag.  Typically this is found on enterprise-level access points, where multiple APs service the same area.",
1667                     draw: function(opts) {
1668                         return opts['value'] + "dBm";
1669                     },
1670                 },
1671                 {
1672                     field: "dot11.advertisedssid.dot11r_mobility",
1673                     liveupdate: true,
1674                     title: "802.11r Mobility",
1675                     filterOnZero: true,
1676                     help: "The 802.11r standard allows for fast roaming between access points on the same network.  Typically this is found on enterprise-level access points, on a network where multiple APs service the same area.",
1677                     draw: function(opts) { return "Enabled"; }
1678                 },
1679                 {
1680                     field: "dot11.advertisedssid.dot11r_mobility_domain_id",
1681                     liveupdate: true,
1682                     title: "Mobility Domain",
1683                     filterOnZero: true,
1684                     help: "The 802.11r standard allows for fast roaming between access points on the same network."
1685                 },
1686                 {
1687                     field: "dot11.advertisedssid.first_time",
1688                     liveupdate: true,
1689                     title: "First Seen",
1690                     draw: kismet_ui.RenderTrimmedTime,
1691                 },
1692                 {
1693                     field: "dot11.advertisedssid.last_time",
1694                     liveupdate: true,
1695                     title: "Last Seen",
1696                     draw: kismet_ui.RenderTrimmedTime,
1697                 },
1698
1699                 {
1700                     // Location is its own group
1701                     groupTitle: "Avg. Location",
1702                     id: "avg.dot11.advertisedssid.location",
1703
1704                     // Group field
1705                     groupField: "dot11.advertisedssid.location",
1706
1707                     liveupdate: true,
1708
1709                     // Don't show location if we don't know it
1710                     filter: function(opts) {
1711                         return (kismet.ObjectByString(opts['base'], "dot11.advertisedssid.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1712                     },
1713
1714                     fields: [
1715                         {
1716                             field: "kismet.common.location.avg_loc/kismet.common.location.geopoint",
1717                             title: "Location",
1718                             liveupdate: true,
1719                             draw: function(opts) {
1720                                 try {
1721                                     if (opts['value'][1] == 0 || opts['value'][0] == 0)
1722                                         return "<i>Unknown</i>";
1723
1724                                     return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1725                                 } catch (error) {
1726                                     return "<i>Unknown</i>";
1727                                 }
1728                             }
1729                         },
1730                         {
1731                             field: "kismet.common.location.avg_loc/kismet.common.location.alt",
1732                             title: "Altitude",
1733                             liveupdate: true,
1734                             filter: function(opts) {
1735                                 return (kismet.ObjectByString(opts['base'], "kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1736                             },
1737                             draw: function(opts) {
1738                                 try {
1739                                     return kismet_ui.renderHeightDistance(opts['value']);
1740                                 } catch (error) {
1741                                     return "<i>Unknown</i>";
1742                                 }
1743                             },
1744                         }
1745                     ],
1746                 },
1747
1748
1749                 {
1750                     field: "dot11.advertisedssid.maxrate",
1751                     liveupdate: true,
1752                     title: "Max. Rate",
1753                     draw: function(opts) {
1754                         return opts['value'] + ' MBit/s';
1755                     },
1756                     help: "The maximum basic transmission rate supported by this access point",
1757                 },
1758                 {
1759                     field: "dot11.advertisedssid.dot11d_country",
1760                     liveupdate: true,
1761                     title: "802.11d Country",
1762                     filterOnEmpty: true,
1763                     help: "The 802.11d standard required access points to identify their operating country code and signal levels.  This caused clients connecting to those access points to adopt the same regulatory requirements.  802.11d has been phased out and is not found on most modern access points but may still be seen on older hardware.",
1764                 },
1765                 {
1766                     field: "dot11.advertisedssid.wps_manuf",
1767                     liveupdate: true,
1768                     title: "WPS Manufacturer",
1769                     filterOnEmpty: true,
1770                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device manufacturer in the WPS advertisements.  WPS is not recommended due to security flaws."
1771                 },
1772                 {
1773                     field: "dot11.advertisedssid.wps_device_name",
1774                     liveupdate: true,
1775                     title: "WPS Device",
1776                     filterOnEmpty: true,
1777                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1778                 },
1779                 {
1780                     field: "dot11.advertisedssid.wps_model_name",
1781                     liveupdate: true,
1782                     title: "WPS Model",
1783                     filterOnEmpty: true,
1784                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the specific device model name in the WPS advertisements.  WPS is not recommended due to security flaws.",
1785                 },
1786                 {
1787                     field: "dot11.advertisedssid.wps_model_number",
1788                     liveupdate: true,
1789                     title: "WPS Model #",
1790                     filterOnEmpty: true,
1791                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the specific model number in the WPS advertisements.  WPS is not recommended due to security flaws.",
1792                 },
1793                 {
1794                     field: "dot11.advertisedssid.wps_serial_number",
1795                     liveupdate: true,
1796                     title: "WPS Serial #",
1797                     filterOnEmpty: true,
1798                     help: "Access points which advertise Wi-Fi Protected Setup (WPS) may include the device serial number in the WPS advertisements.  This information is not always valid or useful.  WPS is not recommended due to security flaws.",
1799                 },
1800
1801                 {
1802                     field: "dot11.advertisedssid.ie_tag_content",
1803                     liveupdate: true,
1804                     filterOnEmpty: true,
1805                     id: "dot11_ssid_ietags",
1806                     title: '<b class="k_padding_title">IE tags</b>',
1807                     help: "IE tags in beacons define the network and the access point attributes; because dot11_keep_ietags is true, Kismet tracks these here.",
1808                 },
1809
1810                 {
1811                     field: "dot11.advertisedssid.ie_tag_content",
1812                     liveupdate: true,
1813                     id: "advertised_ietags",
1814                     filterOnEmpty: true,
1815                     span: true,
1816
1817                     render: function(opts) {
1818                         return '<table id="tagdump" border="0" />';
1819                     },
1820
1821                     draw: function(opts) {
1822                         $('table#tagdump', opts['container']).empty();
1823                         for (var ie in opts['value']) {
1824                             var tag = opts['value'][ie];
1825
1826                             var pretty_tag = 
1827                                 $('<tr>', {
1828                                     class: 'alternating'
1829                                 })
1830                                 .append(
1831                                     $('<td>', {
1832                                         width: "25%",
1833                                         id: "tagno"
1834                                     })
1835                                     .append(
1836                                         $('<div>')
1837                                         .html("<b>" + tag['dot11.ietag.number'] + "</b>")
1838                                     )
1839                                 )
1840                                 .append(
1841                                     $('<td>', {
1842                                         id: "hexdump"
1843                                     })
1844                                 );
1845
1846                             if (tag['dot11.ietag.oui'] != 0) {
1847                                 var oui = ("000000" + tag['dot11.ietag.oui'].toString(16)).substr(-6).replace(/(..)/g, '$1:').slice(0, -1);
1848
1849                                 if (tag['dot11.ietag.oui_manuf'].length != 0)
1850                                     oui = oui + " (" + tag['dot11.ietag.oui_manuf'] + ")";
1851
1852                                 $('#tagno', pretty_tag).append(
1853                                     $('<div>')
1854                                     .text(oui)
1855                                 );
1856                             }
1857
1858                             if (tag['dot11.ietag.subtag'] >= 0) {
1859                                 $('#tagno', pretty_tag).append(
1860                                     $('<div>')
1861                                     .text("Subtag " + tag['dot11.ietag.subtag'])
1862                                 )
1863                             }
1864
1865                             var hexdumps = pretty_hexdump(hexstr_to_bytes(tag['dot11.ietag.data']));
1866
1867                             for (var i in hexdumps) {
1868                                 $('#hexdump', pretty_tag).append(
1869                                     $('<div>')
1870                                     .append(
1871                                         $('<code>')
1872                                         .html(hexdumps[i])
1873                                     )
1874                                 )
1875                             }
1876
1877                             $('table#tagdump', opts['container']).append(pretty_tag);
1878                         }
1879                     },
1880
1881                 },
1882
1883                 ],
1884             },
1885
1886             {
1887                 field: "dot11_bssts_similar",
1888                 id: "bssts_similar_header",
1889                 help: "Wi-Fi access points advertise a high-precision timestamp in beacons.  Multiple devices with extremely similar timestamps are typically part of the same physical access point advertising multiple BSSIDs.",
1890                 filter: function(opts) {
1891                     try {
1892                         return (Object.keys(opts['data']['kismet.device.base.related_devices']['dot11_bssts_similar']).length >= 1);
1893                     } catch (error) {
1894                         return false;
1895                     }
1896                 },
1897                 title: '<b class="k_padding_title">Shared Hardware (Uptime)</b>'
1898             },
1899
1900             {
1901                 field: "kismet.device.base.related_devices/dot11_bssts_similar",
1902                 id: "bssts_similar",
1903
1904                 filter: function(opts) {
1905                     try {
1906                         return (Object.keys(opts['data']['kismet.device.base.related_devices']['dot11_bssts_similar']).length >= 1);
1907                     } catch (error) {
1908                         return false;
1909                     }
1910                 },
1911
1912                 groupIterate: true,
1913                 iterateTitle: function(opts) {
1914                     var key = kismet.ObjectByString(opts['data'], opts['basekey']);
1915                     if (key != 0) {
1916                         return '<a id="' + key + '" class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Shared with ' + opts['data'] + '</a>';
1917                     }
1918
1919                     return '<a class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Shared with ' + opts['data'] + '</a>';
1920                 },
1921                 draw: function(opts) {
1922                     var tb = $('.expander', opts['cell']).simpleexpand();
1923
1924                     var key = kismet.ObjectByString(opts['data'], opts['basekey']);
1925                     var alink = $('a#' + key, opts['cell']);
1926                     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
1927                     .done(function(data) {
1928                         data = kismet.sanitizeObject(data);
1929
1930                         try {
1931                             var ssid = data['dot11.device']['dot11.device.last_beaconed_ssid_record']['dot11.advertisedssid.ssid'];
1932                             var mac = kismet.censorMAC(data['kismet.device.base.macaddr']);
1933                         } catch (error) {
1934
1935                         }
1936
1937                         if (ssid == "" || typeof(data) === 'undefined')
1938                             ssid = "<i>n/a</i>";
1939
1940                         alink.html("Related to " + mac + " (" + ssid + ")");
1941                     });
1942                 },
1943
1944                 fields: [
1945                 {
1946                     field: "dot11.client.bssid_key",
1947                     title: "Access Point",
1948                     draw: function(opts) {
1949                         if (opts['key'] === '') {
1950                             return "<i>No records for access point</i>";
1951                         } else {
1952                             return '<a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + opts['base'] + '\')">View AP Details</a>';
1953                         }
1954                     }
1955                 },
1956                 ]
1957             },
1958
1959             {
1960                 field: "dot11_wps_uuid_identical",
1961                 id: "wps_uuid_identical_header",
1962                 help: "Some devices change MAC addresses but retain the WPS UUID unique identifier.  These devices have been detected using the same unique ID, which is extremely unlikely to randomly collide.",
1963                 filter: function(opts) {
1964                     try {
1965                         return (Object.keys(opts['data']['kismet.device.base.related_devices']['dot11_uuid_e']).length >= 1);
1966                     } catch (error) {
1967                         return false;
1968                     }
1969                 },
1970                 title: '<b class="k_padding_title">Shared Hardware (WPS UUID)</b>'
1971             },
1972
1973             {
1974                 field: "kismet.device.base.related_devices/dot11_uuid_e",
1975                 id: "wps_uuid_identical",
1976
1977                 filter: function(opts) {
1978                     try {
1979                         return (Object.keys(opts['data']['kismet.device.base.related_devices']['dot11_uuid_e']).length >= 1);
1980                     } catch (error) {
1981                         return false;
1982                     }
1983                 },
1984
1985                 groupIterate: true,
1986                 iterateTitle: function(opts) {
1987                     var key = kismet.ObjectByString(opts['data'], opts['basekey']);
1988                     if (key != 0) {
1989                         return '<a id="' + key + '" class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Same WPS UUID as ' + opts['data'] + '</a>';
1990                     }
1991
1992                     return '<a class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Same WPS UUID as ' + opts['data'] + '</a>';
1993                 },
1994                 draw: function(opts) {
1995                     var tb = $('.expander', opts['cell']).simpleexpand();
1996
1997                     var key = kismet.ObjectByString(opts['data'], opts['basekey']);
1998                     var alink = $('a#' + key, opts['cell']);
1999                     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
2000                     .done(function(data) {
2001                         data = kismet.sanitizeObject(data);
2002
2003                         var mac = "<i>unknown</i>";
2004
2005                         try {
2006                             mac = kismet.censorMAC(data['kismet.device.base.macaddr']);
2007                         } catch (error) {
2008
2009                         }
2010
2011                         alink.html("Related to " + mac);
2012                     });
2013                 },
2014
2015                 fields: [
2016                 {
2017                     field: "dot11.client.bssid_key",
2018                     title: "Access Point",
2019                     draw: function(opts) {
2020                         if (opts['key'] === '') {
2021                             return "<i>No records for access point</i>";
2022                         } else {
2023                             return '<a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + opts['base'] + '\')">View AP Details</a>';
2024                         }
2025                     }
2026                 },
2027                 ]
2028             },
2029
2030             {
2031                 field: "dot11.device/dot11.device.client_map",
2032                 id: "client_behavior_header",
2033                 help: "A Wi-Fi device may be a client of multiple networks over time, but can only be actively associated with a single access point at once.  Clients typically are able to roam between access points with the same name (SSID).",
2034                 filter: function(opts) {
2035                     try {
2036                         return (Object.keys(opts['data']['dot11.device']['dot11.device.client_map']).length >= 1);
2037                     } catch (error) {
2038                         return false;
2039                     }
2040                 },
2041                 title: '<b class="k_padding_title">Wi-Fi Client Behavior</b>'
2042             },
2043
2044             {
2045                 field: "dot11.device/dot11.device.client_map",
2046                 id: "client_behavior",
2047
2048                 filter: function(opts) {
2049                     try {
2050                         return (Object.keys(opts['data']['dot11.device']['dot11.device.client_map']).length >= 1);
2051                     } catch (error) {
2052                         return false;
2053                     }
2054                 },
2055
2056                 groupIterate: true,
2057                 iterateTitle: function(opts) {
2058                     var key = kismet.ObjectByString(opts['data'], opts['basekey'] + 'dot11.client.bssid_key');
2059                     if (key != 0) {
2060                         return '<a id="dot11_bssid_client_' + key + '" class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Client of ' + kismet.censorMAC(opts['index']) + '</a>';
2061                     }
2062
2063                     return '<a class="expander collapsed" data-expander-target="#' + opts['containerid'] + '" href="#">Client of ' + kismet.censorMAC(opts['index']) + '</a>';
2064                 },
2065                 draw: function(opts) {
2066                     var tb = $('.expander', opts['cell']).simpleexpand();
2067                 },
2068
2069                 fields: [
2070                 {
2071                     field: "dot11.client.bssid_key",
2072                     title: "Access Point",
2073                     draw: function(opts) {
2074                         if (opts['key'] === '') {
2075                             return "<i>No records for access point</i>";
2076                         } else {
2077                             return '<a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + opts['value'] + '\')">View AP Details</a>';
2078                         }
2079                     }
2080                 },
2081                 {
2082                     field: "dot11.client.bssid",
2083                     title: "BSSID",
2084                     draw: function(opts) {
2085                         return kismet.censorMAC(opts['value']);
2086                     },
2087                 },
2088                 {
2089                     field: "dot11.client.bssid_key",
2090                     title: "Name",
2091                     draw: function(opts) {
2092                         $.get(local_uri_prefix + "devices/by-key/" + opts['value'] +
2093                                 "/device.json/kismet.device.base.commonname")
2094                         .fail(function() {
2095                             opts['container'].html('<i>None</i>');
2096                         })
2097                         .done(function(clidata) {
2098                             clidata = kismet.sanitizeObject(clidata);
2099
2100                             if (clidata === '' || clidata === '""') {
2101                                 opts['container'].html('<i>None</i>');
2102                             } else {
2103                                 opts['container'].html(kismet.censorMAC(clidata));
2104                             }
2105                         });
2106                     },
2107                 },
2108                 {
2109                     field: "dot11.client.bssid_key",
2110                     title: "Last SSID",
2111                     draw: function(opts) {
2112                         $.get(local_uri_prefix + "devices/by-key/" + opts['value'] +
2113                             'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.ssid')
2114                         .fail(function() {
2115                             opts['container'].html('<i>Unknown</i>');
2116                         })
2117                         .done(function(clidata) {
2118                             clidata = kismet.sanitizeObject(clidata);
2119
2120                             if (clidata === '' || clidata === '""') {
2121                                 opts['container'].html('<i>Unknown</i>');
2122                             } else {
2123                                 opts['container'].html(clidata);
2124                             }
2125                         });
2126                     },
2127                 },
2128                 {
2129                     field: "dot11.client.first_time",
2130                     title: "First Connected",
2131                     draw: kismet_ui.RenderTrimmedTime,
2132                 },
2133                 {
2134                     field: "dot11.client.last_time",
2135                     title: "Last Connected",
2136                     draw: kismet_ui.RenderTrimmedTime,
2137                 },
2138                 {
2139                     field: "dot11.client.datasize",
2140                     title: "Data",
2141                     draw: kismet_ui.RenderHumanSize,
2142                 },
2143                 {
2144                     field: "dot11.client.datasize_retry",
2145                     title: "Retried Data",
2146                     draw: kismet_ui.RenderHumanSize,
2147                 },
2148                 {
2149                     // Set the field to be the host, and filter on it, but also
2150                     // define a group
2151                     field: "dot11.client.dhcp_host",
2152                     groupTitle: "DHCP",
2153                     id: "client_dhcp",
2154                     filterOnEmpty: true,
2155                     help: "If a DHCP data packet is seen, the requested hostname and the operating system / vendor of the DHCP client can be extracted.",
2156                     fields: [
2157                     {
2158                         field: "dot11.client.dhcp_host",
2159                         title: "DHCP Hostname",
2160                         empty: "<i>Unknown</i>"
2161                     },
2162                     {
2163                         field: "dot11.client.dhcp_vendor",
2164                         title: "DHCP Vendor",
2165                         empty: "<i>Unknown</i>"
2166                     }
2167                     ]
2168                 },
2169                 {
2170                     field: "dot11.client.eap_identity",
2171                     title: "EAP Identity",
2172                     filterOnEmpty: true,
2173                     help: "If an EAP handshake (part of joining a WPA-Enterprise protected network) is observed, Kismet may be able to extract the EAP identity of a client; this may represent the users login, or it may be empty or 'anonymouse' when joining a network with a phase-2 authentication, like WPA-PEAP",
2174                 },
2175                 {
2176                     field: "dot11.client.cdp_device",
2177                     groupTitle: "CDP",
2178                     id: "client_cdp",
2179                     filterOnEmpty: true,
2180                     help: "Clients bridged to a wired network may leak CDP (Cisco Discovery Protocol) packets, which can disclose information about the internal wired network.",
2181                     fields: [
2182                     {
2183                         field: "dot11.client.cdp_device",
2184                         title: "CDP Device"
2185                     },
2186                     {
2187                         field: "dot11.client.cdp_port",
2188                         title: "CDP Port",
2189                         empty: "<i>Unknown</i>"
2190                     }
2191                     ]
2192                 },
2193                 {
2194                     field: "dot11.client.ipdata",
2195                     groupTitle: "IP",
2196                     filter: function(opts) {
2197                         if (kismet.ObjectByString(opts['data'], opts['basekey'] + 'dot11.client.ipdata') == 0)
2198                             return false;
2199
2200                         return (kismet.ObjectByString(opts['data'], opts['basekey'] + 'dot11.client.ipdata/kismet.common.ipdata.address') != 0);
2201                     },
2202                     help: "Kismet will attempt to derive the IP ranges in use on a network, either from observed traffic or from DHCP server responses.",
2203                     fields: [
2204                     {
2205                         field: "dot11.client.ipdata/kismet.common.ipdata.address",
2206                         title: "IP Address",
2207                     },
2208                     {
2209                         field: "dot11.client.ipdata/kismet.common.ipdata.netmask",
2210                         title: "Netmask",
2211                         zero: "<i>Unknown</i>"
2212                     },
2213                     {
2214                         field: "dot11.client.ipdata/kismet.common.ipdata.gateway",
2215                         title: "Gateway",
2216                         zero: "<i>Unknown</i>"
2217                     }
2218                     ]
2219                 },
2220                 ]
2221             },
2222
2223             {
2224                 // Filler title
2225                 field: "dot11.device/dot11.device.associated_client_map",
2226                 id: "client_list_header",
2227                 filter: function(opts) {
2228                     try {
2229                         return (Object.keys(opts['data']['dot11.device']['dot11.device.associated_client_map']).length >= 1);
2230                     } catch (error) {
2231                         return false;
2232                     }
2233                 },
2234                 title: '<b class="k_padding_title">Associated Clients</b>',
2235                 help: "An access point typically will have clients associated with it.  These client devices can either be wireless devices connected to the access point, or they can be bridged, wired devices on the network the access point is connected to.",
2236             },
2237
2238             {
2239                 field: "dot11.device/dot11.device.associated_client_map",
2240                 id: "client_list",
2241
2242                 filter: function(opts) {
2243                     try {
2244                         return (Object.keys(opts['data']['dot11.device']['dot11.device.associated_client_map']).length >= 1);
2245                     } catch (error) {
2246                         return false;
2247                     }
2248                 },
2249
2250                 groupIterate: true,
2251                 iterateTitle: function(opts) {
2252                     return '<a id="associated_client_expander_' + opts['base'] + '" class="expander collapsed" href="#" data-expander-target="#' + opts['containerid'] + '">Client ' + kismet.censorMAC(opts['index']) + '</a>';
2253                 },
2254                 draw: function(opts) {
2255                     var tb = $('.expander', opts['cell']).simpleexpand();
2256                 },
2257                 fields: [
2258                 {
2259                     // Dummy field to get us a nested area since we don't have
2260                     // a real field in the client list since it's just a key-val
2261                     // not a nested object
2262                     field: "dummy",
2263                     // Span to fill it
2264                     span: true,
2265                     draw: function(opts) {
2266                         return `<div id="associated_client_content_${opts['base']}">`;
2267                     },
2268                 },
2269                 ]
2270             },
2271             ]
2272         }, storage);
2273     },
2274
2275     finalize: function(data, target, options, storage) {
2276         var apkey = data['kismet.device.base.macaddr'];
2277
2278         var combokeys = {};
2279
2280         try {
2281             Object.values(data['dot11.device']['dot11.device.associated_client_map']).forEach(device => combokeys[device] = 1);
2282         } catch (err) {
2283             ;
2284         }
2285
2286         try {
2287             Object.values(data['dot11.device']['dot11.device.client_map']).forEach(device => combokeys[device['dot11.client.bssid_key']] = 1);
2288         } catch (err) {
2289             ;
2290         }
2291
2292         var param = {
2293             devices: Object.keys(combokeys),
2294             fields: [
2295                 'kismet.device.base.macaddr',
2296                 'kismet.device.base.key',
2297                 'kismet.device.base.type',
2298                 'kismet.device.base.commonname',
2299                 'kismet.device.base.manuf',
2300                 'dot11.device/dot11.device.client_map',
2301                 ['dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.ssid', 'dot11.advertisedssid.lastssid'],
2302             ]
2303         };
2304
2305         var postdata = `json=${encodeURIComponent(JSON.stringify(param))}`;
2306
2307         $.post(`${local_uri_prefix}devices/multikey/as-object/devices.json`, postdata, "json")
2308         .done(function(devs) {
2309             var devs = kismet.sanitizeObject(devs);
2310
2311             var client_devs = [];
2312
2313             try {
2314                 Object.values(data['dot11.device']['dot11.device.client_map']).forEach(device => client_devs.push(device['dot11.client.bssid_key']));
2315             } catch (err) {
2316                 ;
2317             }
2318
2319             client_devs.forEach(function(v) {
2320                 if (!v in devs)
2321                     return;
2322
2323                 var dev = devs[v];
2324
2325                 var lastssid = dev['dot11.advertisedssid.lastssid'];
2326
2327                 if (typeof(lastssid) !== 'string')
2328                     lastssid = `<i>None</i>`;
2329                 else if (lastssid.replace(/\s/g, '').length == 0) 
2330                     lastssid = `<i>Cloaked / Empty (${lastssid.length} characters)</i>`;
2331
2332                 $(`a#dot11_bssid_client_${v}`).html(`Client of ${kismet.censorMAC(dev['kismet.device.base.macaddr'])} (${lastssid})`);
2333             });
2334
2335             client_devs = [];
2336
2337             try {
2338                 client_devs = Object.values(data['dot11.device']['dot11.device.associated_client_map']);
2339             } catch (err) {
2340                 ;
2341             }
2342
2343             client_devs.forEach(function(v) {
2344                 if (!v in devs)
2345                     return;
2346
2347                 var dev = devs[v];
2348
2349                 $(`#associated_client_expander_${v}`).html(`${kismet.censorMAC(dev['kismet.device.base.commonname'])}`);
2350
2351                 $(`#associated_client_content_${v}`).devicedata(dev, {
2352                     id: "clientData",
2353                     fields: [
2354                         {
2355                             field: "kismet.device.base.key",
2356                             title: "Client Info",
2357                             draw: function(opts) {
2358                                 return '<a href="#" onclick="kismet_ui.DeviceDetailWindow(\'' + opts['data']['kismet.device.base.key'] + '\')">View Client Details</a>';
2359                             }
2360                         },
2361                         {
2362                             field: "kismet.device.base.commonname",
2363                             title: "Name",
2364                             filterOnEmpty: "true",
2365                             empty: "<i>None</i>",
2366                             draw: function(opts) {
2367                                 return kismet.censorMAC(opts['value']);
2368                             },
2369                         },
2370                         {
2371                             field: "kismet.device.base.type",
2372                             title: "Type",
2373                             empty: "<i>Unknown</i>"
2374
2375                         },
2376                         {
2377                             field: "kismet.device.base.manuf",
2378                             title: "Manufacturer",
2379                             empty: "<i>Unknown</i>"
2380                         },
2381                         {
2382                             field: "dot11.device.client_map[" + apkey + "]/dot11.client.first_time",
2383                             title: "First Connected",
2384                             draw: kismet_ui.RenderTrimmedTime,
2385                         },
2386                         {
2387                             field: "dot11.device.client_map[" + apkey + "]/dot11.client.last_time",
2388                             title: "Last Connected",
2389                             draw: kismet_ui.RenderTrimmedTime,
2390                         },
2391                         {
2392                             field: "dot11.device.client_map[" + apkey + "]/dot11.client.datasize",
2393                             title: "Data",
2394                             draw: kismet_ui.RenderHumanSize,
2395                         },
2396                         {
2397                             field: "dot11.device.client_map[" + apkey + "]/dot11.client.datasize_retry",
2398                             title: "Retried Data",
2399                             draw: kismet_ui.RenderHumanSize,
2400                         },
2401                     ]
2402                 });
2403             });
2404         });
2405     },
2406 });
2407
2408 var ssid_element;
2409 var ssid_status_element;
2410
2411 var SsidColumns = new Array();
2412
2413 export const AddSsidColumn = (id, options) => {
2414     var coldef = {
2415         kismetId: id,
2416         sTitle: options.sTitle,
2417         field: null,
2418         fields: null,
2419     };
2420
2421     if ('field' in options) {
2422         coldef.field = options.field;
2423     }
2424
2425     if ('fields' in options) {
2426         coldef.fields = options.fields;
2427     }
2428
2429     if ('description' in options) {
2430         coldef.description = options.description;
2431     }
2432
2433     if ('name' in options) {
2434         coldef.name = options.name;
2435     }
2436
2437     if ('orderable' in options) {
2438         coldef.bSortable = options.orderable;
2439     }
2440
2441     if ('visible' in options) {
2442         coldef.bVisible = options.visible;
2443     } else {
2444         coldef.bVisible = true;
2445     }
2446
2447     if ('selectable' in options) {
2448         coldef.user_selectable = options.selectable;
2449     } else {
2450         coldef.user_selectable = true;
2451     }
2452
2453     if ('searchable' in options) {
2454         coldef.bSearchable = options.searchable;
2455     }
2456
2457     if ('width' in options) {
2458         coldef.width = options.width;
2459     }
2460
2461     if ('sClass' in options) {
2462         coldef.sClass = options.sClass;
2463     }
2464
2465     var f;
2466     if (typeof(coldef.field) === 'string') {
2467         var fs = coldef.field.split('/');
2468         f = fs[fs.length - 1];
2469     } else if (Array.isArray(coldef.field)) {
2470         f = coldef.field[1];
2471     }
2472
2473     coldef.mData = function(row, type, set) {
2474         return kismet.ObjectByString(row, f);
2475     }
2476
2477     if ('renderfunc' in options) {
2478         coldef.mRender = options.renderfunc;
2479     }
2480
2481     if ('drawfunc' in options) {
2482         coldef.kismetdrawfunc = options.drawfunc;
2483     }
2484
2485     SsidColumns.push(coldef);
2486 }
2487
2488 export const GetSsidColumns = (showall = false) => {
2489     var ret = new Array();
2490
2491     var order = kismet.getStorage('kismet.ssidtable.columns', []);
2492
2493     if (order.length == 0) {
2494         // sort invisible columns to the end
2495         for (var i in SsidColumns) {
2496             if (!SsidColumns[i].bVisible)
2497                 continue;
2498             ret.push(SsidColumns[i]);
2499         }
2500
2501         for (var i in SsidColumns) {
2502             if (SsidColumns[i].bVisible)
2503                 continue;
2504             ret.push(SsidColumns[i]);
2505         }
2506
2507         return ret;
2508     }
2509
2510     for (var oi in order) {
2511         var o = order[oi];
2512
2513         if (!o.enable)
2514             continue;
2515
2516         var sc = SsidColumns.find(function(e, i, a) {
2517             if (e.kismetId === o.id)
2518                 return true;
2519             return false;
2520         });
2521
2522         if (sc != undefined && sc.user_selectable) {
2523             sc.bVisible = true;
2524             ret.push(sc);
2525         }
2526     }
2527
2528     // Fallback if no columns were selected somehow
2529     if (ret.length == 0) {
2530         // sort invisible columns to the end
2531         for (var i in SsidColumns) {
2532             if (!SsidColumns[i].bVisible)
2533                 continue;
2534             ret.push(SsidColumns[i]);
2535         }
2536
2537         for (var i in SsidColumns) {
2538             if (SsidColumns[i].bVisible)
2539                 continue;
2540             ret.push(SsidColumns[i]);
2541         }
2542
2543         return ret;
2544     }
2545
2546     if (showall) {
2547         for (var sci in SsidColumns) {
2548             var sc = SsidColumns[sci];
2549
2550             var rc = ret.find(function(e, i, a) {
2551                 if (e.kismetId === sc.kismetId)
2552                     return true;
2553                 return false;
2554             });
2555
2556             if (rc == undefined) {
2557                 sc.bVisible = false;
2558                 ret.push(sc);
2559             }
2560         }
2561
2562         return ret;
2563     }
2564
2565     for (var sci in SsidColumns) {
2566         if (!SsidColumns[sci].user_selectable) {
2567             ret.push(SsidColumns[sci]);
2568         }
2569     }
2570
2571     return ret;
2572 }
2573
2574 export const GetSsidColumnMap = (columns) => {
2575     var ret = {};
2576
2577     for (var ci in columns) {
2578         var fields = new Array();
2579
2580         if ('field' in columns[ci])
2581             fields.push(columns[ci]['field']);
2582
2583         if ('fields' in columns[ci])
2584             fields.push.apply(fields, columns[ci]['fields']);
2585
2586         ret[ci] = fields;
2587     }
2588
2589     return ret;
2590 }
2591
2592 export const GetSsidFields = (selected) => {
2593     var rawret = new Array();
2594     var cols = GetSsidColumns();
2595
2596     for (var i in cols) {
2597         if ('field' in cols[i])
2598             rawret.push(cols[i]['field']);
2599
2600         if ('fields' in cols[i])
2601             rawret.push.apply(rawret, cols[i]['fields']);
2602     }
2603
2604     // de-dupe
2605     var ret = rawret.filter(function(item, pos, self) {
2606         return self.indexOf(item) == pos;
2607     });
2608
2609     return ret;
2610 }
2611
2612 var ssidTid = -1;
2613
2614 function ScheduleSsidSummary() {
2615     try {
2616         if (kismet_ui.window_visible && ssid_element.is(":visible")) {
2617             var dt = ssid_element.DataTable();
2618
2619             // Save the state.  We can't use proper state saving because it seems to break
2620             // the table position
2621             kismet.putStorage('kismet.base.ssidtable.order', JSON.stringify(dt.order()));
2622             kismet.putStorage('kismet.base.ssidtable.search', JSON.stringify(dt.search()));
2623
2624             // Snapshot where we are, because the 'don't reset page' in ajax.reload
2625             // DOES still reset the scroll position
2626             var prev_pos = {
2627                 'top': $(dt.settings()[0].nScrollBody).scrollTop(),
2628                 'left': $(dt.settings()[0].nScrollBody).scrollLeft()
2629             };
2630             dt.ajax.reload(function(d) {
2631                 // Restore our scroll position
2632                 $(dt.settings()[0].nScrollBody).scrollTop( prev_pos.top );
2633                 $(dt.settings()[0].nScrollBody).scrollLeft( prev_pos.left );
2634             }, false);
2635         }
2636
2637     } catch (error) {
2638         ;
2639     }
2640     
2641     // Set our timer outside of the datatable callback so that we get called even
2642     // if the ajax load fails
2643     ssidTid = setTimeout(ScheduleSsidSummary, 2000);
2644 }
2645
2646 function InitializeSsidTable() {
2647     var cols = GetSsidColumns();
2648     var colmap = GetSsidColumnMap(cols);
2649     var fields = GetSsidFields();
2650
2651     var json = {
2652         fields: fields,
2653         colmap: colmap,
2654         datatable: true,
2655     };
2656
2657     if ($.fn.dataTable.isDataTable(ssid_element)) {
2658         ssid_element.DataTable().destroy();
2659         ssid_element.empty();
2660     }
2661
2662     ssid_element
2663         .on('xhr.dt', function(e, settings, json, xhr) {
2664             json = kismet.sanitizeObject(json);
2665
2666             try {
2667                 if (json['recordsFiltered'] != json['recordsTotal'])
2668                     ssid_status_element.html(`${json['recordsTotal']} SSIDs (${json['recordsFiltered']} shown after filter)`);
2669                 else
2670                     ssid_status_element.html(`${json['recordsTotal']} SSIDs`);
2671             } catch (error) {
2672                 ;
2673             }
2674         })
2675         .DataTable({
2676             destroy: true,
2677             scrollResize: true,
2678             scrollY: 200,
2679             scrollX: "100%",
2680             serverSide: true,
2681             processing: true,
2682             dom: 'ft',
2683             deferRender: true,
2684             lengthChange: false,
2685             scroller: {
2686                 loadingIndicator: true,
2687             },
2688             ajax: {
2689                 url: local_uri_prefix + "phy/phy80211/ssids/views/ssids.json",
2690                 data: {
2691                     json: JSON.stringify(json)
2692                 },
2693                 method: 'POST',
2694                 timeout: 5000,
2695             },
2696             columns: cols,
2697             columnDefs: [
2698                 { className: "dt_td", targets: "_all" },
2699             ],
2700             order: [ [ 0, "desc" ] ],
2701             createdRow: function(row, data, index) {
2702                 row.id = data['dot11.ssidgroup.hash'];
2703             },
2704             drawCallback: function(settings) {
2705                 var dt = this.api();
2706
2707                 dt.rows({
2708                     page: 'current'
2709                 }).every(function(rowIdx, tableLoop, rowLoop) {
2710                     for (var c in SsidColumns) {
2711                         var col = SsidColumns[c];
2712
2713                         if (!('kismetdrawfunc') in col)
2714                             continue;
2715
2716                         try {
2717                             col.kismetdrawfunc(col, dt, this);
2718                         } catch (error) {
2719                             ;
2720                         }
2721                     }
2722                 });
2723             },
2724         });
2725
2726     var ssid_dt = ssid_element.DataTable();
2727
2728     // Restore the order
2729     var saved_order = kismet.getStorage('kismet.base.ssidtable.order', "");
2730     if (saved_order !== "")
2731         ssid_dt.order(JSON.parse(saved_order));
2732
2733     // Restore the search
2734     var saved_search = kismet.getStorage('kismet.base.ssidtable.search', "");
2735     if (saved_search !== "")
2736         ssid_dt.search(JSON.parse(saved_search));
2737
2738     // Set an onclick handler to spawn the device details dialog
2739     $('tbody', ssid_element).on('click', 'tr', function () {
2740         SsidDetailWindow(this.id);
2741     } );
2742
2743     $('tbody', ssid_element)
2744         .on( 'mouseenter', 'td', function () {
2745             var ssid_dt = ssid_element.DataTable();
2746
2747             if (typeof(ssid_dt.cell(this).index()) === 'Undefined')
2748                 return;
2749
2750             var colIdx = ssid_dt.cell(this).index().column;
2751             var rowIdx = ssid_dt.cell(this).index().row;
2752
2753             // Remove from all cells
2754             $(ssid_dt.cells().nodes()).removeClass('kismet-highlight');
2755             // Highlight the td in this row
2756             $('td', ssid_dt.row(rowIdx).nodes()).addClass('kismet-highlight');
2757         } );
2758
2759     return ssid_dt;
2760 }
2761
2762 kismet_ui_tabpane.AddTab({
2763     id: 'dot11_ssids',
2764     tabTitle: 'SSIDs',
2765     createCallback: function(div) {
2766         div.append(
2767             $('<div>', {
2768                 class: 'resize_wrapper',
2769             })
2770             .append(
2771                 $('<table>', {
2772                     id: 'ssids',
2773                     class: 'stripe hover nowrap',
2774                     'cell-spacing': 0,
2775                     width: '100%',
2776                 })
2777             )
2778         ).append(
2779             $('<div>', {
2780                 id: 'ssids_status',
2781                 style: 'padding-bottom: 10px;',
2782             })
2783         );
2784
2785         ssid_element = $('#ssids', div);
2786         ssid_status_element = $('#ssids_status', div);
2787
2788         InitializeSsidTable();
2789         ScheduleSsidSummary();
2790     },
2791     priority: -1000,
2792 }, 'center');
2793
2794 AddSsidColumn('col_ssid', {
2795     sTitle: 'SSID',
2796     field: 'dot11.ssidgroup.ssid',
2797     name: 'SSID',
2798     // width: '250px',
2799     renderfunc: function(d, t, r, m) {
2800         if (d.length == 0)
2801             return "<i>Cloaked or Empty SSID</i>";
2802         else if (/^ +$/.test(d))
2803             return "<i>Blank SSID</i>";
2804         return d;
2805     },
2806 });
2807
2808 AddSsidColumn('col_ssid_len', {
2809     sTitle: 'Length',
2810     field: 'dot11.ssidgroup.ssid_len',
2811     name: 'SSID Length',
2812     width: '10px',
2813     sClass: "dt-body-right",
2814 });
2815
2816 AddSsidColumn('column_time', {
2817     sTitle: 'Last Seen',
2818     field: 'dot11.ssidgroup.last_time',
2819     description: 'Last-seen time',
2820     width: '200px',
2821     renderfunc: function(d, t, r, m) {
2822         return kismet_ui_base.renderLastTime(d, t, r, m);
2823     },
2824     searchable: true,
2825     visible: true,
2826     orderable: true,
2827 });
2828
2829 AddSsidColumn('column_first_time', {
2830     sTitle: 'First Seen',
2831     field: 'dot11.ssidgroup.first_time',
2832     description: 'First-seen time',
2833     renderfunc: function(d, t, r, m) {
2834         return kismet_ui_base.renderLastTime(d, t, r, m);
2835     },
2836     searchable: true,
2837     visible: false,
2838     orderable: true,
2839 });
2840
2841 AddSsidColumn('column_crypt', {
2842     sTitle: 'Encryption',
2843     field: 'dot11.ssidgroup.crypt_set',
2844     description: 'Encryption',
2845     renderfunc: function(d, t, r, m) {
2846         return CryptToHumanReadable(d);
2847     },
2848     searchable: true,
2849     orderable: true,
2850 });
2851
2852 AddSsidColumn('column_probing', {
2853     sTitle: '# Probing',
2854     field: 'dot11.ssidgroup.probing_devices_len',
2855     description: 'Count of probing devices',
2856     orderable: true,
2857     width: '40px',
2858     sClass: "dt-body-right",
2859 });
2860
2861 AddSsidColumn('column_responding', {
2862     sTitle: '# Responding',
2863     field: 'dot11.ssidgroup.responding_devices_len',
2864     description: 'Count of responding devices',
2865     orderable: true,
2866     width: '40px',
2867     sClass: "dt-body-right",
2868 });
2869
2870 AddSsidColumn('column_advertising', {
2871     sTitle: '# Advertising',
2872     field: 'dot11.ssidgroup.advertising_devices_len',
2873     description: 'Count of advertising devices',
2874     orderable: true,
2875     width: '40px',
2876     sClass: "dt-body-right",
2877 });
2878
2879 AddSsidColumn('column_hash', {
2880     sTitle: 'Hash key',
2881     field: 'dot11.ssidgroup.hash',
2882     description: 'Hash',
2883     searchable: false,
2884     visible: false,
2885     orderable: false,
2886 });
2887
2888
2889 // SSID panel
2890 var SsidDetails = new Array();
2891
2892 const AddSsidDetail = function(id, title, pos, options) {
2893     kismet_ui.AddDetail(SsidDetails, id, title, pos, options);
2894 }
2895
2896 export const SsidDetailWindow = (key) => {
2897     kismet_ui.DetailWindow(key, "SSID Details", 
2898         {
2899             storage: {},
2900         },
2901
2902         function(panel, options) {
2903             var content = panel.content;
2904
2905             panel.active = true;
2906
2907             window['storage_ssid_' + key] = {};
2908             window['storage_ssid_' + key]['foobar'] = 'bar';
2909
2910             panel.updater = function() {
2911                 if (kismet_ui.window_visible) {
2912                     $.get(local_uri_prefix + "phy/phy80211/ssids/by-hash/" + key + "/ssid.json")
2913                         .done(function(fulldata) {
2914                             fulldata = kismet.sanitizeObject(fulldata);
2915
2916                             $('.loadoops', panel.content).hide();
2917
2918                             panel.headerTitle("SSID: " + fulldata['dot11.ssidgroup.ssid']);
2919
2920                             var accordion = $('div#accordion', content);
2921
2922                             if (accordion.length == 0) {
2923                                 accordion = $('<div />', {
2924                                     id: 'accordion'
2925                                 });
2926
2927                                 content.append(accordion);
2928                             }
2929
2930                             var detailslist = SsidDetails;
2931
2932                             for (var dii in detailslist) {
2933                                 var di = detailslist[dii];
2934
2935                                 // Do we skip?
2936                                 if ('filter' in di.options &&
2937                                     typeof(di.options.filter) === 'function') {
2938                                     if (di.options.filter(fulldata) == false) {
2939                                         continue;
2940                                     }
2941                                 }
2942
2943                                 var vheader = $('h3#header_' + di.id, accordion);
2944
2945                                 if (vheader.length == 0) {
2946                                     vheader = $('<h3>', {
2947                                         id: "header_" + di.id,
2948                                     })
2949                                         .html(di.title);
2950
2951                                     accordion.append(vheader);
2952                                 }
2953
2954                                 var vcontent = $('div#' + di.id, accordion);
2955
2956                                 if (vcontent.length == 0) {
2957                                     vcontent = $('<div>', {
2958                                         id: di.id,
2959                                     });
2960                                     accordion.append(vcontent);
2961                                 }
2962
2963                                 // Do we have pre-rendered content?
2964                                 if ('render' in di.options &&
2965                                     typeof(di.options.render) === 'function') {
2966                                     vcontent.html(di.options.render(fulldata));
2967                                 }
2968
2969                                 if ('draw' in di.options && typeof(di.options.draw) === 'function') {
2970                                     di.options.draw(fulldata, vcontent, options, 'storage_ssid_' + key);
2971                                 }
2972
2973                                 if ('finalize' in di.options &&
2974                                     typeof(di.options.finalize) === 'function') {
2975                                     di.options.finalize(fulldata, vcontent, options, 'storage_ssid_' + key);
2976                                 }
2977                             }
2978                             accordion.accordion({ heightStyle: 'fill' });
2979                         })
2980                         .fail(function(jqxhr, texterror) {
2981                             content.html("<div class=\"loadoops\" style=\"padding: 10px;\"><h1>Oops!</h1><p>An error occurred loading ssid details for key <code>" + key + 
2982                                 "</code>: HTTP code <code>" + jqxhr.status + "</code>, " + texterror + "</div>");
2983                         })
2984                         .always(function() {
2985                             panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
2986                         })
2987                 } else {
2988                     panel.timerid = setTimeout(function() { panel.updater(); }, 1000);
2989                 }
2990
2991             };
2992
2993             panel.updater();
2994         },
2995
2996         function(panel, options) {
2997             clearTimeout(panel.timerid);
2998             panel.active = false;
2999             window['storage_ssid_' + key] = {};
3000         });
3001 };
3002
3003 /* Custom device details for dot11 data */
3004 AddSsidDetail("ssid", "Wi-Fi (802.11) SSIDs", 0, {
3005     draw: function(data, target, options, storage) {
3006         target.devicedata(data, {
3007             "id": "ssiddetails",
3008             "fields": [
3009             {
3010                 field: 'dot11.ssidgroup.ssid',
3011                 title: "SSID",
3012                 liveupdate: true,
3013                 draw: function(opts) {
3014                     if (typeof(opts['value']) === 'undefined')
3015                         return '<i>None</i>';
3016                     if (opts['value'].replace(/\s/g, '').length == 0) 
3017                         return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
3018
3019                     return `${opts['value']} <i>(${data['dot11.ssidgroup.ssid_len']} characters)</i>`;
3020                 },
3021                 help: "SSID advertised or probed by one or more devices",
3022             },
3023             {
3024                 field: "dot11.ssidgroup.first_time",
3025                 title: "First Seen",
3026                 liveupdate: true,
3027                 draw: kismet_ui.RenderTrimmedTime,
3028             },
3029             {
3030                 field: "dot11.ssidgroup.last_time",
3031                 liveupdate: true,
3032                 title: "Last Seen",
3033                 draw: kismet_ui.RenderTrimmedTime,
3034             },
3035             {
3036                 field: "dot11.ssidgroup.crypt_set",
3037                 liveupdate: true,
3038                 title: "Encryption",
3039                 draw: function(opts) {
3040                     return CryptToHumanReadable(opts['value']);
3041                 },
3042                 help: "Encryption at the Wi-Fi layer (open, WEP, and WPA) is defined by the beacon sent by the access point advertising the network.  Layer 3 encryption (such as VPNs) is added later and is not advertised as part of the network itself.",
3043             },
3044
3045
3046
3047             {
3048                 field: "advertising_meta",
3049                 id: "advertising header",
3050                 filter: function(opts) {
3051                     try {
3052                         return (Object.keys(opts['data']['dot11.ssidgroup.advertising_devices']).length >= 1);
3053                     } catch (error) {
3054                         return false;
3055                     }
3056                 },
3057                 title: '<b class="k_padding_title">Advertising APs</b>',
3058                 help: "Advertising access points have sent a beacon packet with this SSID.",
3059             },
3060
3061             {
3062                 field: "dot11.ssidgroup.advertising_devices",
3063                 id: "advertising_list",
3064
3065                 filter: function(opts) {
3066                     try {
3067                         return (Object.keys(opts['data']['dot11.ssidgroup.advertising_devices']).length >= 1);
3068                     } catch (error) {
3069                         return false;
3070                     }
3071                 },
3072
3073                 groupIterate: true,
3074                 iterateTitle: function(opts) {
3075                     return '<a id="ssid_expander_advertising_' + opts['base'] + '" class="ssid_expander_advertising expander collapsed" href="#" data-expander-target="#' + opts['containerid'] + '">Access point ' + opts['index'] + '</a>';
3076                 },
3077                 draw: function(opts) {
3078                     var tb = $('.expander', opts['cell']).simpleexpand();
3079                 },
3080                 fields: [
3081                 {
3082                     // Dummy field to get us a nested area since we don't have
3083                     // a real field in the client list since it's just a key-val
3084                     // not a nested object
3085                     field: "dummy",
3086                     // Span to fill it
3087                     span: true,
3088                     draw: function(opts) {
3089                         return `<div class="ssid_content_advertising" id="ssid_content_advertising_${opts['base']}">`;
3090                     },
3091                 },
3092                 ]
3093             },
3094
3095
3096
3097             {
3098                 field: "responding_meta",
3099                 id: "responding header",
3100                 filter: function(opts) {
3101                     try {
3102                         return (Object.keys(opts['data']['dot11.ssidgroup.responding_devices']).length >= 1);
3103                     } catch (error) {
3104                         return false;
3105                     }
3106                 },
3107                 title: '<b class="k_padding_title">Responding APs</b>',
3108                 help: "Responding access points have sent a probe response with this SSID.",
3109             },
3110
3111             {
3112                 field: "dot11.ssidgroup.responding_devices",
3113                 id: "responding_list",
3114
3115                 filter: function(opts) {
3116                     try {
3117                         return (Object.keys(opts['data']['dot11.ssidgroup.responding_devices']).length >= 1);
3118                     } catch (error) {
3119                         return false;
3120                     }
3121                 },
3122
3123                 groupIterate: true,
3124                 iterateTitle: function(opts) {
3125                     return '<a id="ssid_expander_responding_' + opts['base'] + '" class="ssid_expander_responding expander collapsed" href="#" data-expander-target="#' + opts['containerid'] + '">Access point ' + opts['index'] + '</a>';
3126                 },
3127                 draw: function(opts) {
3128                     var tb = $('.expander', opts['cell']).simpleexpand();
3129                 },
3130                 fields: [
3131                 {
3132                     // Dummy field to get us a nested area since we don't have
3133                     // a real field in the client list since it's just a key-val
3134                     // not a nested object
3135                     field: "dummy",
3136                     // Span to fill it
3137                     span: true,
3138                     draw: function(opts) {
3139                         return `<div class="ssid_content_responding" id="ssid_content_responding_${opts['base']}">`;
3140                     },
3141                 },
3142                 ]
3143             },
3144
3145
3146
3147             {
3148                 field: "probing_meta",
3149                 id: "probing header",
3150                 filter: function(opts) {
3151                     try {
3152                         return (Object.keys(opts['data']['dot11.ssidgroup.probing_devices']).length >= 1);
3153                     } catch (error) {
3154                         return false;
3155                     }
3156                 },
3157                 title: '<b class="k_padding_title">Probing devices</b>',
3158                 help: "Probing devices have sent a probe request or association request with this SSID",
3159             },
3160
3161             {
3162                 field: "dot11.ssidgroup.probing_devices",
3163                 id: "probing_list",
3164
3165                 filter: function(opts) {
3166                     try {
3167                         return (Object.keys(opts['data']['dot11.ssidgroup.probing_devices']).length >= 1);
3168                     } catch (error) {
3169                         return false;
3170                     }
3171                 },
3172
3173                 groupIterate: true,
3174                 iterateTitle: function(opts) {
3175                     return '<a id="ssid_expander_probing_' + opts['base'] + '" class="ssid_expander_probing expander collapsed" href="#" data-expander-target="#' + opts['containerid'] + '">Wi-Fi Device ' + opts['index'] + '</a>';
3176                 },
3177                 draw: function(opts) {
3178                     var tb = $('.expander', opts['cell']).simpleexpand();
3179                 },
3180                 fields: [
3181                 {
3182                     // Dummy field to get us a nested area since we don't have
3183                     // a real field in the client list since it's just a key-val
3184                     // not a nested object
3185                     field: "dummy",
3186                     // Span to fill it
3187                     span: true,
3188                     draw: function(opts) {
3189                         return `<div class="ssid_content_probing" id="ssid_content_probing_${opts['base']}">`;
3190                     },
3191                 },
3192                 ]
3193             },
3194
3195             ],
3196         }, storage);
3197     }, 
3198
3199     finalize: function(data, target, options, storage) {
3200         var combokeys = {};
3201
3202         data['dot11.ssidgroup.advertising_devices'].forEach(device => combokeys[device] = 1);
3203         data['dot11.ssidgroup.probing_devices'].forEach(device => combokeys[device] = 1);
3204         data['dot11.ssidgroup.responding_devices'].forEach(device => combokeys[device] = 1);
3205
3206         var param = {
3207             devices: Object.keys(combokeys),
3208             fields: [
3209                 'kismet.device.base.macaddr',
3210                 'kismet.device.base.key',
3211                 'kismet.device.base.type',
3212                 'kismet.device.base.commonname',
3213                 'kismet.device.base.manuf',
3214                 'dot11.device/dot11.device.last_beaconed_ssid_record/dot11.advertisedssid.ssid',
3215                 'dot11.device/dot11.device.advertised_ssid_map',
3216                 'dot11.device/dot11.device.responded_ssid_map',
3217             ]
3218         };
3219
3220         var postdata = `json=${encodeURIComponent(JSON.stringify(param))}`;
3221
3222         $.post(`${local_uri_prefix}devices/multikey/as-object/devices.json`, postdata, "json")
3223         .done(function(aggcli) {
3224             aggcli = kismet.sanitizeObject(aggcli);
3225
3226             data['dot11.ssidgroup.advertising_devices'].forEach(function(v) {
3227                 if (!v in aggcli)
3228                     return;
3229
3230                 var dev = aggcli[v];
3231
3232                 var devssid;
3233
3234                 try {
3235                     dev['dot11.device.advertised_ssid_map'].forEach(function (s) {
3236                         if (s['dot11.advertisedssid.ssid_hash'] == data['dot11.ssidgroup.hash']) {
3237                             devssid = s;
3238                         }
3239                     });
3240                 } catch (e) {
3241                     ;
3242                 }
3243
3244                 var crypttxt;
3245                 try {
3246                     crypttxt = CryptToHumanReadable(devssid['dot11.advertisedssid.crypt_set']);
3247                 } catch (e) {
3248                     ;
3249                 }
3250
3251                 var titlehtml = `${dev['kismet.device.base.commonname']} - ${dev['kismet.device.base.macaddr']}`;
3252
3253                 if (crypttxt != null)
3254                     titlehtml = `${titlehtml} - ${crypttxt}`;
3255
3256                 titlehtml = kismet.censorMAC(titlehtml);
3257
3258                 $(`#ssid_expander_advertising_${v}`).html(titlehtml);
3259
3260                 $(`#ssid_content_advertising_${v}`).devicedata(dev, {
3261                     id: "ssid_adv_data",
3262                     fields: [
3263                         {
3264                             field: 'kismet.device.base.key',
3265                             title: "Advertising Device",
3266                             draw: function(opts) {
3267                                 return `<a href="#" onclick="kismet_ui.DeviceDetailWindow('${opts['data']['kismet.device.base.key']}')">View Device Details</a>`;
3268                             }
3269                         }, 
3270                         {
3271                             field: "kismet.device.base.macaddr",
3272                             title: "MAC",
3273                             draw: function(opts) {
3274                                 return kismet.censorMAC(`${opts['data']['kismet.device.base.macaddr']} (${opts['data']['kismet.device.base.manuf']})`);
3275                             }
3276                         },
3277                         {
3278                             field: 'kismet.device.base.commonname',
3279                             title: "Name",
3280                             liveupdate: true,
3281                             empty: "<i>None</i>",
3282                             draw: function(opts) {
3283                                 return kismet.censorMAC(opts['value']);
3284                             },
3285                         },
3286                         {
3287                             field: 'kismet.device.base.type',
3288                             title: "Type",
3289                             liveupdate: true,
3290                             empty: "<i>Unknown</i>",
3291                         },
3292                         {
3293                             field: 'meta_advertised_crypto',
3294                             title: "Advertised encryption",
3295                             liveupdate: true,
3296                             draw: function(opts) {
3297                                 try {
3298                                     return CryptToHumanReadable(devssid['dot11.advertisedssid.crypt_set']);
3299                                 } catch (e) {
3300                                     return '<i>Unknown</i>';
3301                                 }
3302                             },
3303                             help: "The encryption should be the same across all APs advertising the same SSID in the same area, howevever each AP advertises this information independently.",
3304                         },
3305                         {
3306                             field: 'meta_advertised_firsttime',
3307                             title: "First advertised",
3308                             liveupdate: true,
3309                             draw: function(opts) {
3310                                 try {
3311                                     return kismet_ui.RenderTrimmedTime({value: devssid['dot11.advertisedssid.first_time']});
3312                                 } catch (e) {
3313                                     return '<i>Unknown</i>';
3314                                 }
3315                             }
3316                         },
3317                         {
3318                             field: 'meta_advertised_lasttime',
3319                             title: "Last advertised",
3320                             liveupdate: true,
3321                             draw: function(opts) {
3322                                 try {
3323                                     return kismet_ui.RenderTrimmedTime({value: devssid['dot11.advertisedssid.last_time']});
3324                                 } catch (e) {
3325                                     return '<i>Unknown</i>';
3326                                 }
3327                             }
3328                         },
3329                         {
3330                             field: 'dot11.advertisedssid.ssid',
3331                             title: "Last advertised SSID",
3332                             liveupdate: true,
3333                             draw: function(opts) {
3334                                 if (typeof(opts['value']) === 'undefined')
3335                                     return '<i>None</i>';
3336                                 if (opts['value'].replace(/\s/g, '').length == 0) 
3337                                     return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
3338
3339                                 return opts['value'];
3340                             },
3341                         },
3342                     ]
3343                 });
3344             });
3345
3346
3347             data['dot11.ssidgroup.responding_devices'].forEach(function(v) {
3348                 if (!v in aggcli)
3349                     return;
3350
3351                 var dev = aggcli[v];
3352
3353                 var devssid;
3354
3355                 try {
3356                     dev['dot11.device.responded_ssid_map'].forEach(function (s) {
3357                         if (s['dot11.advertisedssid.ssid_hash'] == data['dot11.ssidgroup.hash']) {
3358                             devssid = s;
3359                         }
3360                     });
3361                 } catch (e) {
3362                     ;
3363                 }
3364
3365                 var crypttxt;
3366                 try {
3367                     crypttxt = CryptToHumanReadable(devssid['dot11.advertisedssid.crypt_set']);
3368                 } catch (e) {
3369                     ;
3370                 }
3371
3372                 var titlehtml = `${dev['kismet.device.base.commonname']} - ${dev['kismet.device.base.macaddr']}`;
3373
3374                 if (crypttxt != null)
3375                     titlehtml = `${titlehtml} - ${crypttxt}`;
3376
3377                 titlehtml = kismet.censorMAC(titlehtml);
3378
3379                 $(`#ssid_expander_responding_${v}`).html(titlehtml);
3380
3381                 $(`#ssid_content_responding_${v}`).devicedata(dev, {
3382                     id: "ssid_adv_data",
3383                     fields: [
3384                         {
3385                             field: 'kismet.device.base.key',
3386                             title: "Responding Device",
3387                             draw: function(opts) {
3388                                 return `<a href="#" onclick="kismet_ui.DeviceDetailWindow('${opts['data']['kismet.device.base.key']}')">View Device Details</a>`;
3389                             }
3390                         }, 
3391                         {
3392                             field: "kismet.device.base.macaddr",
3393                             title: "MAC",
3394                             draw: function(opts) {
3395                                 return kismet.censorMAC(`${opts['data']['kismet.device.base.macaddr']} (${opts['data']['kismet.device.base.manuf']})`);
3396                             }
3397                         },
3398                         {
3399                             liveupdate: true,
3400                             field: 'kismet.device.base.commonname',
3401                             title: "Name",
3402                             empty: "<i>None</i>",
3403                             draw: function(opts) {
3404                                 return kismet.censorMAC(opts['value']);
3405                             },
3406                         },
3407                         {
3408                             liveupdate: true,
3409                             field: 'kismet.device.base.type',
3410                             title: "Type",
3411                             empty: "<i>Unknown</i>",
3412                         },
3413                         {
3414                             field: 'meta_advertised_crypto',
3415                             title: "Advertised encryption",
3416                             liveupdate: true,
3417                             draw: function(opts) {
3418                                 try {
3419                                     return CryptToHumanReadable(devssid['dot11.advertisedssid.crypt_set']);
3420                                 } catch (e) {
3421                                     return '<i>Unknown</i>';
3422                                 }
3423                             },
3424                             help: "The encryption should be the same across all APs advertising the same SSID in the same area, howevever each AP advertises this information independently.",
3425                         },
3426                         {
3427                             field: 'meta_advertised_firsttime',
3428                             title: "First responded",
3429                             liveupdate: true,
3430                             draw: function(opts) {
3431                                 try {
3432                                     return kismet_ui.RenderTrimmedTime({value: devssid['dot11.advertisedssid.first_time']});
3433                                 } catch (e) {
3434                                     return '<i>Unknown</i>';
3435                                 }
3436                             }
3437                         },
3438                         {
3439                             field: 'meta_advertised_lasttime',
3440                             title: "Last responded",
3441                             liveupdate: true,
3442                             draw: function(opts) {
3443                                 try {
3444                                     return kismet_ui.RenderTrimmedTime({value: devssid['dot11.advertisedssid.last_time']});
3445                                 } catch (e) {
3446                                     return '<i>Unknown</i>';
3447                                 }
3448                             }
3449                         },
3450                         {
3451                             liveupdate: true,
3452                             field: 'dot11.advertisedssid.ssid',
3453                             title: "Last advertised SSID",
3454                             draw: function(opts) {
3455                                 if (typeof(opts['value']) === 'undefined')
3456                                     return '<i>None</i>';
3457                                 if (opts['value'].replace(/\s/g, '').length == 0) 
3458                                     return '<i>Cloaked / Empty (' + opts['value'].length + ' spaces)</i>';
3459
3460                                 return opts['value'];
3461                             },
3462                         },
3463                     ]
3464                 });
3465             });
3466
3467
3468             data['dot11.ssidgroup.probing_devices'].forEach(function(v) {
3469                 if (!v in aggcli)
3470                     return;
3471
3472                 var dev = aggcli[v];
3473
3474                 $(`#ssid_expander_probing_${v}`).html(kismet.censorMAC(`${dev['kismet.device.base.commonname']} - ${dev['kismet.device.base.macaddr']}`));
3475
3476                 $(`#ssid_content_probing_${v}`).devicedata(dev, {
3477                     id: "ssid_adv_data",
3478                     fields: [
3479                         {
3480                             field: 'kismet.device.base.key',
3481                             title: "Probing Device",
3482                             draw: function(opts) {
3483                                 return `<a href="#" onclick="kismet_ui.DeviceDetailWindow('${opts['data']['kismet.device.base.key']}')">View Device Details</a>`;
3484                             }
3485                         }, 
3486                         {
3487                             field: "kismet.device.base.macaddr",
3488                             title: "MAC",
3489                             draw: function(opts) {
3490                                 return kismet.censorMAC(`${opts['data']['kismet.device.base.macaddr']} (${opts['data']['kismet.device.base.manuf']})`);
3491                             }
3492                         },
3493                         {
3494                             field: 'kismet.device.base.commonname',
3495                             title: "Name",
3496                             empty: "<i>None</i>",
3497                             draw: function(opts) {
3498                                 return kismet.censorMAC(opts['value']);
3499                             },
3500                         },
3501                         {
3502                             field: 'kismet.device.base.type',
3503                             title: "Type",
3504                             empty: "<i>Unknown</i>",
3505                         },
3506                     ]
3507                 });
3508             });
3509
3510         });
3511     }
3512
3513 });
3514