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