├── LICENSE ├── README.md ├── requirements.txt ├── synopsis.py └── web ├── css ├── chart_components.css ├── file_reader.css └── synopsis.css ├── index.html ├── js ├── activity_by_domain.js ├── all_activity_bar_chart.js ├── all_websites_visited.js ├── autofill_data_table.js ├── devices_data.js ├── extracted_accounts.js ├── file_reader.js ├── grid_sorting.js ├── heatmap.js ├── helpers.js ├── page_functions.js ├── preferences.js ├── search_queries.js ├── syn_geo.js └── word_cloud_searches.js ├── synopsis_screenshot.png └── vendor ├── bootstrap ├── css │ ├── bootstrap-grid.css │ ├── bootstrap-grid.min.css │ ├── bootstrap-reboot.css │ ├── bootstrap-reboot.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ ├── bootstrap.min.css │ └── bootstrap.min.css.map └── js │ ├── bootstrap.bundle.js │ ├── bootstrap.bundle.js.map │ ├── bootstrap.bundle.min.js │ ├── bootstrap.bundle.min.js.map │ ├── bootstrap.js │ ├── bootstrap.js.map │ ├── bootstrap.min.js │ └── bootstrap.min.js.map ├── chart.js ├── Chart.bundle.js ├── Chart.bundle.min.js ├── Chart.js └── Chart.min.js ├── d3 ├── d3.js └── d3.v4.js ├── d3cloud └── d3.layout.cloud.js ├── datatables ├── dataTables.bootstrap4.css ├── dataTables.bootstrap4.js └── jquery.dataTables.js ├── font-awesome ├── css │ ├── font-awesome.css │ ├── font-awesome.css.map │ └── font-awesome.min.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── less │ ├── animated.less │ ├── bordered-pulled.less │ ├── core.less │ ├── fixed-width.less │ ├── font-awesome.less │ ├── icons.less │ ├── larger.less │ ├── list.less │ ├── mixins.less │ ├── path.less │ ├── rotated-flipped.less │ ├── screen-reader.less │ ├── stacked.less │ └── variables.less └── scss │ ├── _animated.scss │ ├── _bordered-pulled.scss │ ├── _core.scss │ ├── _fixed-width.scss │ ├── _icons.scss │ ├── _larger.scss │ ├── _list.scss │ ├── _mixins.scss │ ├── _path.scss │ ├── _rotated-flipped.scss │ ├── _screen-reader.scss │ ├── _stacked.scss │ ├── _variables.scss │ └── font-awesome.scss ├── images ├── close.png ├── loading.gif ├── next.png └── prev.png ├── jquery-easing ├── jquery.easing.compatibility.js ├── jquery.easing.js └── jquery.easing.min.js ├── jquery ├── jquery.js ├── jquery.min.js ├── jquery.min.map ├── jquery.slim.js ├── jquery.slim.min.js └── jquery.slim.min.map ├── leaflet ├── images │ ├── marker-icon-2x-black.png │ ├── marker-icon-2x-blue.png │ ├── marker-icon-2x-green.png │ ├── marker-icon-2x-grey.png │ ├── marker-icon-2x-orange.png │ ├── marker-icon-2x-red.png │ ├── marker-icon-2x-violet.png │ ├── marker-icon-2x-yellow.png │ ├── marker-icon-2x.png │ ├── marker-icon-black.png │ ├── marker-icon-blue.png │ ├── marker-icon-green.png │ ├── marker-icon-grey.png │ ├── marker-icon-orange.png │ ├── marker-icon-red.png │ ├── marker-icon-violet.png │ ├── marker-icon-yellow.png │ ├── marker-icon.png │ └── marker-shadow.png ├── leaflet.css └── leaflet.js ├── lightbox ├── lightbox.css └── lightbox.js ├── moment └── moment.js ├── muuri ├── hammer.js ├── muuri.js └── web-animations.min.js ├── shuffle └── shuffle.js └── typeahead └── typeahead.bundle.js /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Synopsis 2 | 3 | Synopsis is a tool to aid analysts reviewing browser history files by providing 4 | a high-level “synopsis” of key information. Many questions are common across 5 | different types of investigations, and Synopsis aims to help investigators get 6 | answers quickly. This tool provides a dashboard-type view that can be used to 7 | assess whether the browsing history from the device in question is relevant to 8 | the case at hand. 9 | 10 | 11 | 12 | Synopsis extracts the following types of information: 13 | - search engine queries 14 | - discovered accounts on websites 15 | - autofill data (information typed into web forms) 16 | - historical geolocation 17 | - activity per domain 18 | - overall activity trends based on time and day of week 19 | 20 | Each of the above items is displayed in one or more “cards” in a dashboard. 21 | Synopsis also provides search functionality, so investigators can filter down 22 | the collected data to terms of particular interest for their investigation. 23 | 24 | Synopsis is built using open source technologies, including Python and several 25 | Python libraries, Chart.js, and more. No code from any Exabeam products is used 26 | in Synopsis. 27 | 28 | 29 | ## Install 30 | 31 | ``` 32 | git clone git://github.com/ExabeamLabs/Synopsis 33 | cd Synopsis 34 | pip install -r requirements.txt 35 | ``` 36 | 37 | ## Usage 38 | 39 | Run the `synopsis.py` Python script against the browser profile you want to 40 | investigate. At this time, only Chrome is supported. If you are running this 41 | script against your local Chrome profile, please make sure that Chrome is not 42 | running. 43 | 44 | ``` 45 | python2.7 synopsis.py 46 | ``` 47 | 48 | This will generate `synopsis_YYYY-MM-DDTHH-MM-SS.json`. In the `web` sub-directory, 49 | there is an `index.html` file. Open `web\index.html` in the web browser 50 | of your choice. In the 'File Upload' pane that appears, click 'Choose File' and select 51 | the generated Synopsis JSON file to review the results (the page may take a while to 52 | load, depending on the size of the browser profile). 53 | 54 | ## Contribute 55 | 56 | PRs accepted. 57 | 58 | ## License 59 | 60 | Apache License 2.0 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | keyring==10.4.0 2 | leveldb==0.194 3 | matplotlib==2.0.0 4 | pandas==0.22.0 5 | requests==2.20.0 6 | wordcloud==1.2.1 7 | -------------------------------------------------------------------------------- /web/css/chart_components.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | .chart-legend li span{ 15 | display: inline-block; 16 | width: 12px; 17 | height: 12px; 18 | margin-right: 5px; 19 | } 20 | 21 | .chart-legend{ 22 | height:250px; 23 | overflow:auto; 24 | } 25 | 26 | .chart-legend li{ 27 | cursor:pointer; 28 | } -------------------------------------------------------------------------------- /web/css/file_reader.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | #byte_content { 14 | margin: 5px 0; 15 | max-height: 100px; 16 | overflow-y: auto; 17 | overflow-x: hidden; 18 | } 19 | #byte_range { margin-top: 5px; } 20 | 21 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Synopsis 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 50 | 51 |
52 |
53 |
54 |
55 |
56 |
57 | File Upload 58 |
59 | 60 |
61 | 62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | 290 |
291 | 292 | 293 | 294 |
295 |
296 |
297 | Synopsis 298 |
299 |
300 |
301 | 302 | 303 | 304 | 305 | 306 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 |
361 |
362 | 363 | 364 | 365 | 366 | 367 | -------------------------------------------------------------------------------- /web/js/activity_by_domain.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | // Constructor initial graph 15 | bar_builder = [] 16 | var list_of_bars = document.getElementById("list_of_bars") 17 | 18 | function remove_chart(name_of_website) { 19 | 20 | button_string = name_of_website 21 | 22 | 23 | var chart_cursor = document.getElementById(name_of_website.replace('button-','')) 24 | var button_cursor = document.getElementById(button_string) 25 | chart_cursor.parentNode.removeChild(chart_cursor) 26 | 27 | button_cursor.parentNode.removeChild(button_cursor) 28 | 29 | 30 | } 31 | 32 | function create_new_bar_chart(name_of_website,domains_keymap){ 33 | data_arr_1 = [] 34 | website_keymapper = [] 35 | 36 | if (!domains_keymap){ 37 | domains_keymap = window.domains_keymap 38 | } 39 | for(var property in domains_keymap[name_of_website]){ 40 | if (!domains_keymap[name_of_website].hasOwnProperty(property)){ 41 | continue; 42 | } 43 | data_arr_1.push(property) 44 | website_keymapper.push(domains_keymap[name_of_website][property]) 45 | } 46 | 47 | 48 | 49 | 50 | 51 | max_num = Math.max.apply(null,website_keymapper) 52 | var chart_div = document.createElement("div"); 53 | chart_div.style.display = "inline-block" 54 | var new_canvas = document.createElement("canvas"); 55 | var close_button = document.createElement("button") 56 | 57 | random_num_id = Math.floor(Math.random() * 999); 58 | 59 | close_button.innerHTML = "✖" 60 | close_button.style.cssFloat = "right" 61 | close_button.style.marginTop = "50px" 62 | close_button.id = "button-"+name_of_website+'-'+random_num_id 63 | 64 | close_button.onclick = function(){ 65 | remove_chart(this.id) 66 | }; 67 | new_canvas.width = "675" 68 | 69 | new_canvas.height = "225" 70 | new_canvas.id = name_of_website +'-'+random_num_id 71 | new_canvas.style.cssFloat = "left" 72 | var line_chart = new Chart(new_canvas, { 73 | type: 'bar', 74 | data: { 75 | labels: data_arr_1, 76 | datasets: [{ 77 | label: name_of_website, 78 | backgroundColor: getRandomColor(), 79 | borderColor: "rgba(2,117,216,1)", 80 | data: website_keymapper, 81 | } 82 | ], 83 | }, 84 | options: { 85 | responsive: false, 86 | maintainAspectRatio: true, 87 | scales: { 88 | xAxes: [{ 89 | display:true, 90 | time: { 91 | unit: 'month' 92 | }, 93 | gridLines: { 94 | display: false 95 | }, 96 | ticks: { 97 | maxTicksLimit: 4, 98 | maxRotation: 0, 99 | minRotation: 0 100 | } 101 | }], 102 | yAxes: [{ 103 | ticks: { 104 | min: 0, 105 | max: max_num, 106 | maxTicksLimit: 5 107 | }, 108 | gridLines: { 109 | display: true 110 | } 111 | }], 112 | }, 113 | legend: { 114 | display: true, 115 | position: 'bottom', 116 | labels: { 117 | fontColor: "#000080", 118 | } 119 | } 120 | } 121 | }); 122 | 123 | chart_div.appendChild(new_canvas) 124 | chart_div.appendChild(close_button) 125 | list_of_bars.insertBefore(chart_div,list_of_bars.firstChild) 126 | data_arr_1 = [] 127 | website_keymapper = [] 128 | } 129 | 130 | function add_chart_to_card(){ 131 | var cur_selection = document.getElementById('website_selection') 132 | 133 | create_new_bar_chart(cur_selection.value) 134 | } 135 | 136 | function initialize_bar_Charts(items,domains_keymap,dropdown_list_domains){ 137 | var internal_domains_keymap = domains_keymap 138 | for (var i = 0; i < 4; i++){ 139 | create_new_bar_chart(items[i][0],domains_keymap) 140 | } 141 | window.domains_keymap = domains_keymap 142 | var substringMatcher = function(strs) { 143 | return function findMatches(q, cb) { 144 | var matches, substringRegex; 145 | 146 | // an array that will be populated with substring matches 147 | matches = []; 148 | 149 | // regex used to determine if a string contains the substring `q` 150 | substrRegex = new RegExp(q, 'i'); 151 | 152 | // iterate through the pool of strings and for any string that 153 | // contains the substring `q`, add it to the `matches` array 154 | $.each(strs, function(i, str) { 155 | if (substrRegex.test(str)) { 156 | matches.push(str); 157 | } 158 | }); 159 | 160 | cb(matches); 161 | }; 162 | }; 163 | 164 | var states = dropdown_list_domains 165 | 166 | $('#scrollable-dropdown-menu .typeahead').typeahead({ 167 | hint: true, 168 | highlight: true, 169 | minLength: 1 170 | }, 171 | { 172 | name: 'states', 173 | source: substringMatcher(states) 174 | }); 175 | var activity_domain_shuffle = document.getElementById('activity_by_domain') 176 | activity_domain_shuffle.setAttribute('data-groups',"['" + dropdown_list_domains.join("','") + "']") 177 | } 178 | -------------------------------------------------------------------------------- /web/js/all_activity_bar_chart.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function initialize_all_activity_bar_chart(all_domains_data_arr_area_chart,all_domains_max_num){ 14 | 15 | var area_chart = document.getElementById("web_activity_area_chart"); 16 | web_activity_area_chart.height = 300 17 | var web_activity_chart = new Chart(area_chart, { 18 | type: 'line', 19 | data: { 20 | labels: all_domains_data_arr_1, 21 | datasets: [{ 22 | label: "Sessions", 23 | lineTension: 0.3, 24 | backgroundColor: "rgba(2,117,216,1)", 25 | borderColor: "rgba(2,117,216,1)", 26 | pointRadius: 5, 27 | 28 | pointBackgroundColor: "rgba(2,117,216,1)", 29 | pointBorderColor: "rgba(255,255,255,0.8)", 30 | pointHoverRadius: 5, 31 | pointHoverBackgroundColor: "rgba(2,117,216,1)", 32 | pointHitRadius: 20, 33 | pointBorderWidth: 2, 34 | data: all_domains_data_arr_area_chart 35 | }], 36 | }, 37 | options: { 38 | events: ["click"], 39 | scales: { 40 | xAxes: [{ 41 | time: { 42 | unit: 'date' 43 | }, 44 | gridLines: { 45 | display: false 46 | }, 47 | ticks: { 48 | maxTicksLimit: 7, 49 | maxRotation: 0, 50 | minRotation: 0 51 | } 52 | }], 53 | yAxes: [{ 54 | ticks: { 55 | min: 0, 56 | max: all_domains_max_num, 57 | maxTicksLimit: 5 58 | }, 59 | gridLines: { 60 | color: "rgba(0, 0, 0, .125)", 61 | } 62 | }], 63 | }, 64 | legend: { 65 | display: false 66 | } 67 | } 68 | }); 69 | 70 | } -------------------------------------------------------------------------------- /web/js/all_websites_visited.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright 2018 Exabeam, Inc. 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | function initialize_all_websites_visited(visits_dict){ 15 | // Creates the websites table 16 | var web_visited_var = document.getElementById("mytab5") 17 | var websites_data_group = document.getElementById('websites_data_group') 18 | for (const [key, value] of Object.entries(visits)) { 19 | if (!value.domain.includes("localhost")) { 20 | visits_dict[value.domain] = (visits_dict[value.domain] || 0) + 1; 21 | var table_row = document.createElement('tr') 22 | 23 | var websites_domain = document.createElement('td') 24 | var websites_url = document.createElement('td') 25 | var websites_visited = document.createElement('td') 26 | websites_domain.innerHTML = value.domain 27 | websites_url.innerHTML = value.url 28 | websites_visited.innerHTML = value.visit_time 29 | 30 | 31 | table_row.appendChild(websites_visited) 32 | table_row.appendChild(websites_domain) 33 | table_row.appendChild(websites_url) 34 | 35 | 36 | web_visited_var.appendChild(table_row) 37 | } 38 | 39 | var entry_date = new Date(Date.parse(value.visit_time)) 40 | entry_date.setHours(entry_date.getHours()-7); 41 | day = entry_date.getDay() 42 | hour_minute_second = (entry_date.getHours() + ':' + entry_date.getMinutes()+ ':'+ entry_date.getSeconds()) 43 | 44 | } 45 | 46 | $('#websites_visited_table').DataTable({ 47 | paging: true, 48 | scrollY: 370, 49 | responsive: true, 50 | fixedColumns: true, 51 | columnDefs: [ 52 | { width: 200, targets: 0 }, 53 | { width: 150, targets: 0 }, 54 | { width: 150, targets: 0 } 55 | ], 56 | bInfo : false 57 | }); 58 | 59 | websites_data_group.setAttribute('data-groups',"['" + dropdown_list_domains.join("','") + "']") 60 | } -------------------------------------------------------------------------------- /web/js/autofill_data_table.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function init_autofill_data_table(autofill){ 14 | var autofill_data_group = document.getElementById('autofill_data_group') 15 | var autofill_data_arr = ['autofill'] 16 | // Creates the autofill table 17 | try { 18 | var autofill_data_var = document.getElementById("mytab3") 19 | for ( var i = 0, len = autofill.length; i< len; i++){ 20 | var table_row = document.createElement('tr') 21 | var autofill_name = document.createElement('td') 22 | var autofill_value = document.createElement('td') 23 | var autofill_count = document.createElement('td') 24 | autofill_value.setAttribute("style", "word-wrap:break-word"); 25 | autofill_name.innerHTML = autofill[i].name 26 | autofill_value.innerHTML = autofill[i].value 27 | autofill_count.innerHTML = autofill[i].count 28 | autofill_data_arr.push(autofill[i].name) 29 | autofill_data_arr.push(autofill[i].value) 30 | 31 | table_row.appendChild(autofill_name) 32 | table_row.appendChild(autofill_value) 33 | table_row.appendChild(autofill_count) 34 | 35 | autofill_data_var.appendChild(table_row) 36 | } 37 | } 38 | catch(err) { 39 | console.log("Cannot find autofill") 40 | } 41 | autofill_data_group.setAttribute('data-groups',"['" + autofill_data_arr.join("','") + "']") 42 | 43 | $('#autofill_data_table_declaration').DataTable({ 44 | paging: false, 45 | scrollY: 173, 46 | responsive: true, 47 | columnDefs: [ 48 | { width: 50, targets: 0 }, 49 | { width: 50, targets: 0 }, 50 | { width: 50, targets: 0 } 51 | ], 52 | fixedColumns: true, 53 | bInfo : false, 54 | order: [[ 2, "desc" ]] 55 | }); 56 | } -------------------------------------------------------------------------------- /web/js/devices_data.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function init_devices_data(devices){ 14 | var devices_data_group = document.getElementById('devices_data_group') 15 | var no_data = document.getElementsByClassName("dataTables_empty") 16 | removeElementsByClass("dataTables_empty") 17 | var devices_data_arr = [] 18 | // Creates the autofill table 19 | var datatables_devices_var = document.getElementById("mytab4") 20 | for ( var i = 0, len = devices.length; i< len; i++){ 21 | var table_row = document.createElement('tr') 22 | var autofill_name = document.createElement('td') 23 | var autofill_ip = document.createElement('td') 24 | var autofill_model = document.createElement('td') 25 | var autofill_network = document.createElement('td') 26 | // autofill_value.setAttribute("style", "word-wrap:break-word"); 27 | autofill_name.innerHTML = devices[i].friendlyName 28 | autofill_ip.innerHTML = devices[i].ip 29 | autofill_model.innerHTML = devices[i].modelName 30 | autofill_network.innerHTML = devices[i].networks 31 | 32 | devices_data_arr.push(devices[i].friendlyName) 33 | devices_data_arr.push(devices[i].ip) 34 | devices_data_arr.push(devices[i].networks) 35 | devices_data_arr.push(devices[i].modelName) 36 | 37 | table_row.appendChild(autofill_name) 38 | table_row.appendChild(autofill_ip) 39 | table_row.appendChild(autofill_model) 40 | table_row.appendChild(autofill_network) 41 | 42 | datatables_devices_var.appendChild(table_row) 43 | } 44 | devices_data_group.setAttribute('data-groups',"['" + devices_data_arr.join("','") + "']") 45 | $('#devices_table_declaration').DataTable({ 46 | paging: false, 47 | scrollY: 445, 48 | searching: false, 49 | bSort : false, 50 | bInfo : false, 51 | }); 52 | } -------------------------------------------------------------------------------- /web/js/extracted_accounts.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | function init_extracted_accounts(accounts){ 15 | var table_body = document.getElementById("extracted_accounts_collection") 16 | var data_groups_extracted_table = document.getElementById('extracted_accounts') 17 | data_groups_extracted_array = [] 18 | 19 | for ( var i = 0, len = accounts.length; i< len; i++){ 20 | var table_row = document.createElement('tr') 21 | var account_cell = document.createElement('td') 22 | var account_domain = document.createElement('td') 23 | account_domain.setAttribute("style", "word-wrap:break-word"); 24 | account_cell.innerHTML = accounts[i].account 25 | account_domain.innerHTML = accounts[i].domain 26 | data_groups_extracted_array.push(accounts[i].account) 27 | data_groups_extracted_array.push(accounts[i].domain) 28 | table_row.appendChild(account_cell) 29 | table_row.appendChild(account_domain) 30 | table_body.appendChild(table_row) 31 | } 32 | 33 | 34 | data_groups_extracted_table.setAttribute('data-groups',"['" + data_groups_extracted_array.join("','") + "']") 35 | $('#extracted_accounts_datatable').DataTable({ 36 | paging: false, 37 | scrollY: 190, 38 | searching: false, 39 | bSort : false, 40 | bInfo : false, 41 | }); 42 | 43 | } -------------------------------------------------------------------------------- /web/js/file_reader.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function post_process(data_sample){ 14 | geolocation = data_sample.geolocation 15 | devices = data_sample.media_router.devices 16 | networks = data_sample.media_router.networks 17 | heatmap = data_sample.heatmap 18 | accounts = data_sample.accounts 19 | preferences = data_sample.preferences 20 | search_word_cloud = data_sample.search_word_cloud 21 | searches = data_sample.searches 22 | visits = data_sample.visits 23 | autofill = data_sample.autofill 24 | autofill_word_cloud = data_sample.autofill_word_cloud 25 | daymap = { 26 | "Mon": 1, 27 | "Tues":2, 28 | "Wed": 3, 29 | "Thur":4, 30 | "Fri":5, 31 | "Sat":6, 32 | "Sun":7 33 | } 34 | 35 | tsv_array = [] 36 | 37 | for (hour = 0; hour < heatmap.length; hour++){ 38 | for (var key in heatmap[hour]) { 39 | if (heatmap[hour].hasOwnProperty(key)) { 40 | tsv_data = { 41 | "day": daymap[key], 42 | "hour": hour, 43 | "value": heatmap[hour][key] 44 | } 45 | tsv_array.push(tsv_data) 46 | } 47 | } 48 | } 49 | 50 | 51 | visits_dict = {} 52 | 53 | dropdown_list_domains = [] 54 | // Iterate through huge visits list 55 | domains_keymap = {} 56 | one_run_through = 0 57 | all_domains = {} 58 | 59 | all_domains_data_arr_1 = [] 60 | all_domains_data_arr_area_chart = [] 61 | all_domains_max_num =0 62 | 63 | 64 | for (const [key, value] of Object.entries(visits)) { 65 | if (!value.domain.includes("localhost")) { 66 | visits_dict[value.domain] = (visits_dict[value.domain] || 0) + 1; 67 | } 68 | } 69 | 70 | var items = Object.keys(visits_dict).map(function(key) { 71 | return [key, visits_dict[key]]; 72 | }) 73 | 74 | items.sort(function(first,second) { 75 | return second[1] - first[1]; 76 | }) 77 | 78 | searches_array = [] 79 | query_table = [] 80 | for (const [key, value] of Object.entries(searches)) { 81 | for (var i = 0; i < value.query_terms.split(' ').length; i++){ 82 | searches_array.push(value.query_terms.split(' ')[i]) 83 | } 84 | query_table.push({ 85 | 'time': value.visit_time, 86 | 'query': value.query_terms 87 | }) 88 | 89 | } 90 | 91 | 92 | for (const [key, value] of Object.entries(visits)) { 93 | // For each top level domain check: 94 | for (var i=0; i < items.length; i++){ 95 | // Make sure our key is in json 96 | if(domains_keymap.hasOwnProperty(items[i][0])) { 97 | 98 | if (value.domain.includes(items[i][0])) { 99 | time_was_visited = value.visit_time.slice(0,10) 100 | domains_keymap[items[i][0]][time_was_visited] = (domains_keymap[items[i][0]][time_was_visited] ||0)+1 101 | all_domains[time_was_visited]=(domains_keymap[items[i][0]][time_was_visited] ||0)+1 102 | 103 | for (var j = 0; j < i; j++){ 104 | if (!domains_keymap[items[j][0]].hasOwnProperty(time_was_visited)){ 105 | domains_keymap[items[j][0]][time_was_visited] = 0 106 | } 107 | 108 | } 109 | } 110 | } else { 111 | domains_keymap[items[i][0]]= {} 112 | } 113 | 114 | dropdown_list_domains.indexOf(items[i][0]) === -1 ? dropdown_list_domains.push(items[i][0]) : 0; 115 | } 116 | } 117 | 118 | 119 | 120 | for(var property in all_domains){ 121 | if (!all_domains.hasOwnProperty(property)){ 122 | continue; 123 | } 124 | all_domains_data_arr_1.push(property) 125 | all_domains_data_arr_area_chart.push(all_domains[property]) 126 | if ( all_domains[property] > all_domains_max_num ){ 127 | all_domains_max_num = all_domains[property] 128 | 129 | } 130 | } 131 | 132 | try{ 133 | initialize_bar_Charts(items,domains_keymap,dropdown_list_domains) 134 | }catch (err){ 135 | console.log("Error: Cannot process function initialize_bar_Charts(items,domains_keymap,dropdown_list_domains)") 136 | } 137 | autofill_array = [""] 138 | 139 | try{ 140 | initialize_preferences_chart(preferences,dropdown_list_domains,geolocation,accounts,devices,searches_array) 141 | }catch (err){ 142 | console.log("Error: Cannot process function initialize_preferences_chart(preferences,dropdown_list_domains,geolocation,accounts,devices,searches_array)") 143 | } 144 | try{ 145 | initialize_all_activity_bar_chart(all_domains_data_arr_area_chart,all_domains_max_num) 146 | }catch (err){ 147 | console.log("Error: Cannot process function initialize_all_activity_bar_chart(all_domains_data_arr_area_chart,all_domains_max_num)") 148 | } 149 | try{ 150 | initialize_searches_wordcloud(search_word_cloud) 151 | }catch (err){ 152 | console.log("Error: Cannot process function initialize_searches_wordcloud(search_word_cloud)") 153 | } 154 | try{ 155 | initialize_autofill_wordcloud(autofill_word_cloud,autofill_array) 156 | }catch (err){ 157 | console.log("Error: Cannot process function initialize_autofill_wordcloud(autofill_word_cloud,autofill_array)") 158 | } 159 | try{ 160 | initmap(geolocation); 161 | }catch (err){ 162 | console.log("Error: Cannot process function initmap(geolocation);") 163 | } 164 | try{ 165 | init_extracted_accounts(accounts) 166 | }catch (err){ 167 | console.log("Error: Cannot process function init_extracted_accounts(accounts)") 168 | } 169 | try{ 170 | init_autofill_data_table(autofill) 171 | }catch (err){ 172 | console.log("Error: Cannot process function init_autofill_data_table(autofill)") 173 | } 174 | try{ 175 | init_devices_data(devices) 176 | }catch (err){ 177 | console.log("Error: Cannot process function init_devices_data(devices)") 178 | } 179 | try{ 180 | initialize_search_queries(query_table,searches_array) 181 | }catch (err){ 182 | console.log("Error: Cannot process function initialize_search_queries(query_table,searches_array)") 183 | } 184 | 185 | try{ 186 | initialize_heatmap(tsv_array) 187 | }catch (err){ 188 | console.log("Error: Cannot process function initialize_heatmap(tsv_array)") 189 | } 190 | try{ 191 | initialize_all_websites_visited(visits_dict) 192 | }catch (err){ 193 | console.log("Error: Cannot process function initialize_all_websites_visited(visits_dict)") 194 | } 195 | window.visits_dict = visits_dict 196 | grid_display = document.getElementById("post_upload") 197 | grid_display.style.visibility = "visible" 198 | 199 | 200 | window.demo = new Demo(document.getElementById('grid')); 201 | 202 | var loading_bar = document.getElementById("loading_bar") 203 | loading_bar.style.visibility = "hidden" 204 | 205 | var file_uploader = document.getElementById("file_upload") 206 | file_uploader.style.visibility = "hidden" 207 | } 208 | 209 | 210 | 211 | jQuery('#file').change(function(){ 212 | 213 | var file = document.getElementById('file').files[0]; 214 | var progress = jQuery('#progress'); 215 | var loading_bar = document.getElementById("loading_bar") 216 | loading_bar.style.visibility = "visible" 217 | // Read File In Chunks 218 | if(file){ 219 | var reader = new FileReader(); 220 | var size = file.size; 221 | 222 | 223 | var chunk_size = Math.pow(2, 13); 224 | var chunks = []; 225 | 226 | var offset = 0; 227 | var bytes = 0; 228 | 229 | 230 | reader.onloadend = function(e){ 231 | if (e.target.readyState == FileReader.DONE){ 232 | var chunk = e.target.result; 233 | bytes += chunk.length; 234 | 235 | chunks.push(chunk); 236 | 237 | // Displays progress bar as to how many chunks processed 238 | 239 | 240 | if((offset < size)){ 241 | offset += chunk_size; 242 | var blob = file.slice(offset, offset + chunk_size); 243 | 244 | reader.readAsText(blob); 245 | 246 | } else { 247 | progress.html("processing JSON file.."); 248 | var content = chunks.join(""); 249 | 250 | // When done, perform postprocessing 251 | 252 | post_process(JSON.parse(content)) 253 | var elem = document.querySelector('#file_upload'); 254 | //elem.parentNode.removeChild(elem); 255 | 256 | 257 | 258 | }; 259 | } 260 | 261 | 262 | 263 | }; 264 | 265 | var blob = file.slice(offset, offset + chunk_size); 266 | reader.readAsText(blob); 267 | } 268 | }); -------------------------------------------------------------------------------- /web/js/grid_sorting.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | var Demo = function (element) { 14 | this.element = element; 15 | 16 | this.shuffle = new Shuffle(element, { 17 | itemSelector: '.grid__brick', 18 | sizer: element.querySelector('.my-sizer-element'), 19 | }); 20 | 21 | // Log events. 22 | this.addShuffleEventListeners(); 23 | 24 | this._activeFilters = []; 25 | 26 | this.addFilterButtons(); 27 | this.addSorting(); 28 | this.addSearchFilter(); 29 | 30 | this.mode = 'exclusive'; 31 | }; 32 | Demo.prototype.addSorting = function () { 33 | var buttonGroup = document.querySelector('.sort-options'); 34 | 35 | if (!buttonGroup) { 36 | return; 37 | } 38 | 39 | buttonGroup.addEventListener('change', this._handleSortChange.bind(this)); 40 | }; 41 | 42 | // Advanced filtering 43 | Demo.prototype.addSearchFilter = function () { 44 | var searchInput = document.querySelector('.js-shuffle-search'); 45 | 46 | if (!searchInput) { 47 | return; 48 | } 49 | 50 | searchInput.addEventListener('keyup', this._handleSearchKeyup.bind(this)); 51 | }; 52 | 53 | /** 54 | * Filter the shuffle instance by items with a title that matches the search input. 55 | * @param {Event} evt Event object. 56 | */ 57 | Demo.prototype.addShuffleEventListeners = function () { 58 | this.shuffle.on(Shuffle.EventType.LAYOUT, function (data) { 59 | 60 | }); 61 | 62 | this.shuffle.on(Shuffle.EventType.REMOVED, function (data) { 63 | 64 | }); 65 | }; 66 | Demo.prototype.addFilterButtons = function () { 67 | var options = document.querySelector('.filter-options'); 68 | 69 | if (!options) { 70 | return; 71 | } 72 | 73 | var filterButtons = Array.from(options.children); 74 | 75 | filterButtons.forEach(function (button) { 76 | button.addEventListener('click', this._handleFilterClick.bind(this), false); 77 | }, this); 78 | }; 79 | 80 | Demo.prototype._handleSearchKeyup = function (evt) { 81 | var searchText = evt.target.value.toLowerCase(); 82 | 83 | this.shuffle.filter(function (element, shuffle) { 84 | 85 | // If there is a current filter applied, ignore elements that don't match it. 86 | if (shuffle.group !== Shuffle.ALL_ITEMS) { 87 | // Get the item's groups. 88 | var groups = JSON.parse(element.getAttribute('data-groups')); 89 | var isElementInCurrentGroup = groups.indexOf(shuffle.group) !== -1; 90 | 91 | // Only search elements in the current group 92 | if (!isElementInCurrentGroup) { 93 | return false; 94 | } 95 | } 96 | 97 | var titleElement = element.getAttribute('data-groups').toString() 98 | var titleText = titleElement.toLowerCase().trim(); 99 | 100 | return titleText.indexOf(searchText) !== -1; 101 | }); 102 | }; 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /web/js/heatmap.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function initialize_heatmap(data){ 14 | const margin = { top: 75, right: 0, bottom: 100, left: 110 }, 15 | width = 960 - margin.left - margin.right, 16 | height = 430 - margin.top - margin.bottom, 17 | gridSize = Math.floor(width / 24), 18 | legendElementWidth = gridSize*2, 19 | buckets = 9, 20 | colors = ["#ffffd9","#edf8b1","#c7e9b4","#7fcdbb","#41b6c4","#1d91c0","#225ea8","#253494","#081d58"], // alternatively colorbrewer.YlGnBu[9] 21 | days = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"], 22 | times = ["1a", "2a", "3a", "4a", "5a", "6a", "7a", "8a", "9a", "10a", "11a", "12a", "1p", "2p", "3p", "4p", "5p", "6p", "7p", "8p", "9p", "10p", "11p", "12p"]; 23 | datasets = ["data.tsv", "data2.tsv"]; 24 | 25 | const svg = d3.select("#chart").append("svg") 26 | .attr("width", width + margin.left + margin.right) 27 | .attr("height", height + margin.top + margin.bottom) 28 | .append("g") 29 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 30 | 31 | const dayLabels = svg.selectAll(".dayLabel") 32 | .data(days) 33 | .enter().append("text") 34 | .text(function (d) { return d; }) 35 | .attr("x", -50) 36 | .attr("y", (d, i) => i * gridSize) 37 | .style("text-anchor", "end") 38 | .attr("transform", "translate(-6," + gridSize / 1.5 + ")") 39 | .attr("class", (d, i) => ((i >= 0 && i <= 4) ? "dayLabel mono axis axis-workweek" : "dayLabel mono axis")); 40 | 41 | const timeLabels = svg.selectAll(".timeLabel") 42 | .data(times) 43 | .enter().append("text") 44 | .text((d) => d) 45 | .attr("x", (d, i) => i * gridSize - gridSize) 46 | .attr("y", 0) 47 | .style("text-anchor", "middle") 48 | .attr("transform", "translate(" + gridSize / 2 + ", -6)") 49 | .attr("class", (d, i) => ((i >= 7 && i <= 16) ? "timeLabel mono axis axis-worktime" : "timeLabel mono axis")); 50 | 51 | const type = (d) => { 52 | return { 53 | day: +d.day, 54 | hour: +d.hour, 55 | value: +d.value 56 | }; 57 | }; 58 | 59 | const heatmapChart = function(data) { 60 | console.log(data) 61 | 62 | const colorScale = d3.scaleQuantile() 63 | .domain([0, buckets - 1, d3.max(data, (d) => d.value)]) 64 | .range(colors); 65 | 66 | const cards = svg.selectAll(".hour") 67 | .data(data, (d) => d.day+':'+d.hour); 68 | 69 | cards.append("title"); 70 | 71 | cards.enter().append("rect") 72 | .attr("x", (d) => (d.hour - 1) * gridSize) 73 | .attr("y", (d) => (d.day - 1) * gridSize) 74 | .attr("rx", 4) 75 | .attr("ry", 4) 76 | .attr("class", "hour bordered") 77 | .attr("width", gridSize) 78 | .attr("height", gridSize) 79 | .style("fill", colors[0]) 80 | .merge(cards) 81 | .transition() 82 | .duration(1000) 83 | .style("fill", (d) => colorScale(d.value)); 84 | 85 | cards.select("title").text((d) => d.value); 86 | 87 | cards.exit().remove(); 88 | 89 | const legend = svg.selectAll(".legend") 90 | .data([0].concat(colorScale.quantiles()), (d) => d); 91 | 92 | const legend_g = legend.enter().append("g") 93 | .attr("class", "legend"); 94 | 95 | legend_g.append("rect") 96 | .attr("x", (d, i) => legendElementWidth * i) 97 | .attr("y", height) 98 | .attr("width", legendElementWidth) 99 | .attr("height", gridSize / 2) 100 | .style("fill", (d, i) => colors[i]); 101 | 102 | legend_g.append("text") 103 | .attr("class", "mono") 104 | .text((d) => "≥ " + Math.round(d)) 105 | .attr("x", (d, i) => legendElementWidth * i) 106 | .attr("y", height + gridSize); 107 | 108 | legend.exit().remove(); 109 | 110 | }; 111 | 112 | heatmapChart(data); 113 | 114 | 115 | } -------------------------------------------------------------------------------- /web/js/helpers.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function getRandomColor() { 14 | var letters = '0123456789ABCDEF'; 15 | var color = '#'; 16 | for (var i = 0; i < 6; i++) { 17 | color += letters[Math.floor(Math.random() * 16)]; 18 | } 19 | return color; 20 | } 21 | 22 | function removeElementsByClass(className){ 23 | var elements = document.getElementsByClassName(className); 24 | while(elements.length > 0){ 25 | elements[0].parentNode.removeChild(elements[0]); 26 | } 27 | } -------------------------------------------------------------------------------- /web/js/page_functions.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | (function($) { 14 | "use strict"; // Start of use strict 15 | 16 | $('.searchbox-input').on('keyup',function () { 17 | console.log(filter); 18 | //$('.card').show(); 19 | var filter = $(this).val(); // get the value of the input, which we filter on 20 | console.log(filter); 21 | $('.container').find(".card-title:not(:contains(" + filter + "))").parent().css('display','none'); 22 | }); 23 | 24 | // Configure tooltips for collapsed side navigation 25 | $('.navbar-sidenav [data-toggle="tooltip"]').tooltip({ 26 | template: '' 27 | }) 28 | // Toggle the side navigation 29 | $("#sidenavToggler").click(function(e) { 30 | e.preventDefault(); 31 | $("body").toggleClass("sidenav-toggled"); 32 | $(".navbar-sidenav .nav-link-collapse").addClass("collapsed"); 33 | $(".navbar-sidenav .sidenav-second-level, .navbar-sidenav .sidenav-third-level").removeClass("show"); 34 | }); 35 | // Force the toggled class to be removed when a collapsible nav link is clicked 36 | $(".navbar-sidenav .nav-link-collapse").click(function(e) { 37 | e.preventDefault(); 38 | $("body").removeClass("sidenav-toggled"); 39 | }); 40 | // Prevent the content wrapper from scrolling when the fixed side navigation hovered over 41 | $('body.fixed-nav .navbar-sidenav, body.fixed-nav .sidenav-toggler, body.fixed-nav .navbar-collapse').on('mousewheel DOMMouseScroll', function(e) { 42 | var e0 = e.originalEvent, 43 | delta = e0.wheelDelta || -e0.detail; 44 | this.scrollTop += (delta < 0 ? 1 : -1) * 30; 45 | e.preventDefault(); 46 | }); 47 | // Scroll to top button appear 48 | $(document).scroll(function() { 49 | var scrollDistance = $(this).scrollTop(); 50 | if (scrollDistance > 100) { 51 | $('.scroll-to-top').fadeIn(); 52 | } else { 53 | $('.scroll-to-top').fadeOut(); 54 | } 55 | }); 56 | // Configure tooltips globally 57 | $('[data-toggle="tooltip"]').tooltip() 58 | // Smooth scrolling using jQuery easing 59 | $(document).on('click', 'a.scroll-to-top', function(event) { 60 | var $anchor = $(this); 61 | $('html, body').stop().animate({ 62 | scrollTop: ($($anchor.attr('href')).offset().top) 63 | }, 1000, 'easeInOutExpo'); 64 | event.preventDefault(); 65 | }); 66 | })(jQuery); // End of use strict 67 | -------------------------------------------------------------------------------- /web/js/preferences.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function initialize_preferences_chart(preferences,dropdown_list_domains,geolocation,accounts,devices,searches_array){ 14 | // Creates the preferences table 15 | var data_groups_preferences_table = document.getElementById('preferences_data_group') 16 | data_groups_preferences_array = [] 17 | for ( var i = 0, len = preferences.length; i< len; i++){ 18 | data_groups_preferences_array.push(preferences[i].setting) 19 | data_groups_preferences_array.push(preferences[i].value) 20 | 21 | } 22 | 23 | data_groups_preferences_table.setAttribute('data-groups',"['" + data_groups_preferences_array.join("','") + "']") 24 | 25 | 26 | var prof_pic = document.getElementById("profile_picture") 27 | var full_name = document.getElementById("full_name") 28 | var email = document.getElementById("email") 29 | 30 | var u_web = document.getElementById("u_web") 31 | var u_words = document.getElementById("u_words") 32 | var u_geo = document.getElementById("u_geo") 33 | var u_ext = document.getElementById("u_ext") 34 | var u_dev = document.getElementById("u_dev") 35 | 36 | full_name.innerHTML = preferences[0].value 37 | email.innerHTML = preferences[1].value 38 | prof_pic.src = preferences[2].value 39 | 40 | 41 | u_web.innerHTML = 'Analyzed ' + dropdown_list_domains.length + ' visited websites.' 42 | u_words.innerHTML = 'Mined ' + searches_array.length + ' search queries.' 43 | u_geo.innerHTML = 'Found ' + geolocation.length + ' places.' 44 | u_ext.innerHTML = 'Extracted ' + accounts.length + ' accounts.' 45 | u_dev.innerHTML = 'Observed ' + devices.length + ' connected devices.' 46 | } 47 | -------------------------------------------------------------------------------- /web/js/search_queries.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | function initialize_search_queries(query_table,searches_array){ 15 | 16 | var query_data_group = document.getElementById('query_data_group') 17 | var query_data_arr = [] 18 | // Creates the autofill table 19 | 20 | var tab_query_table = document.getElementById("tab_query") 21 | for ( var i = 0, len = query_table.length; i< len; i++){ 22 | var table_row = document.createElement('tr') 23 | var query_time = document.createElement('td') 24 | var query_term_td = document.createElement('td') 25 | 26 | query_time.innerHTML = query_table[i].time 27 | query_term_td.innerHTML = query_table[i].query 28 | 29 | query_data_arr.push(query_table[i].time) 30 | query_data_arr.push(query_table[i].query) 31 | 32 | 33 | table_row.appendChild(query_time) 34 | table_row.appendChild(query_term_td) 35 | 36 | tab_query_table.appendChild(table_row) 37 | } 38 | query_data_group.setAttribute('data-groups',"['" + query_data_arr.join("','") + "']") 39 | $('#dataTable_query').DataTable({ 40 | paging: false, 41 | scrollY: 170, 42 | responsive: true, 43 | fixedColumns: true, 44 | searching: true, 45 | columnDefs: [ 46 | { width: 150, targets: 0 }, 47 | { width: 250, targets: 0 } 48 | ], 49 | bInfo : false 50 | }); 51 | var search_engine_queries_shuffle = document.getElementById('search_engine_queries_shuffle') 52 | search_engine_queries_shuffle.setAttribute('data-groups',"['" + searches_array.join("','") + "']") 53 | } -------------------------------------------------------------------------------- /web/js/syn_geo.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | // Logic to create Maps 14 | 15 | 16 | function initmap(geolocation) { 17 | var map; 18 | var ajaxRequest; 19 | var plotlist; 20 | var plotlayers=[]; 21 | 22 | coord_map = [] 23 | for (var i = 0; i < geolocation.length; i++){ 24 | // latlng_string = geolocation[i].geo.split(';') 25 | // alng = parseFloat(latlng_string[0]) 26 | alng = parseFloat(geolocation[i].lat) 27 | // alat = parseFloat(latlng_string[1]) 28 | alat = parseFloat(geolocation[i].lon) 29 | atimestamp = geolocation[i].timestamp 30 | coord_map.push({ 31 | "lng":alng, 32 | "lat":alat, 33 | "time":atimestamp 34 | }) 35 | 36 | } 37 | markers = [] 38 | // set up the map 39 | var geolocation_shuffle = document.getElementById('geolocation_shuffle') 40 | geolocation_shuffle.setAttribute('data-groups',"['" + coord_map.join("','") + "']") 41 | map = new L.Map('mapid'); 42 | // create the tile layer with correct attribution 43 | var osmUrl='http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; 44 | var osmAttrib='Map data © OpenStreetMap contributors'; 45 | var osm = new L.TileLayer(osmUrl, {minZoom: 2, maxZoom: 16, attribution: osmAttrib}); 46 | var bounds = new L.LatLngBounds(); 47 | map.setView(new L.LatLng(coord_map[0].lng, coord_map[0].lat),13); 48 | map.addLayer(osm); 49 | map.scrollWheelZoom.disable(); 50 | for( var i = 0; i < coord_map.length;i++){ 51 | lng = coord_map[i].lng 52 | lat = coord_map[i].lat 53 | timestamp = coord_map[i].time 54 | 55 | marker = L.marker([lng,lat]).addTo(map) 56 | .openPopup(); 57 | markers.push([lng,lat]) 58 | } 59 | var abounds = L.latLngBounds(markers); 60 | map.fitBounds(abounds);//works! 61 | 62 | 63 | //var popup = L.popup() 64 | //.setLatLng([lng, lat]) 65 | //.setContent("Last known location retrieved on : " + timestamp) 66 | //.openOn(map); 67 | } -------------------------------------------------------------------------------- /web/js/word_cloud_searches.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Exabeam, Inc. 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | function initialize_searches_wordcloud(search_word_cloud){ 14 | document.getElementById('word-cloud-img') 15 | .setAttribute( 16 | 'href', 'data:image/png;base64,'+search_word_cloud 17 | ); 18 | document.getElementById('word-cloud-img2') 19 | .setAttribute( 20 | 'src', 'data:image/png;base64,'+search_word_cloud 21 | ); 22 | 23 | } 24 | 25 | function initialize_autofill_wordcloud(autofill_word_cloud,autofill_array){ 26 | try{ 27 | document.getElementById('word-cloud-img-autofill') 28 | .setAttribute( 29 | 'href', 'data:image/png;base64,'+autofill_word_cloud 30 | ); 31 | document.getElementById('word-cloud-img-autofill2') 32 | .setAttribute( 33 | 'src', 'data:image/png;base64,'+autofill_word_cloud 34 | ); 35 | }catch(err) { 36 | console.log("Cannot find autofill word cloud") 37 | } 38 | var auto_fill_queries_shuffle = document.getElementById('auto_fill_queries_shuffle') 39 | auto_fill_queries_shuffle.setAttribute('data-groups',"['" + autofill_array.join("','") + "']") 40 | } -------------------------------------------------------------------------------- /web/synopsis_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExabeamLabs/Synopsis/0b45d8a19ff55847cff3a4c5ffa9b6e95c255673/web/synopsis_screenshot.png -------------------------------------------------------------------------------- /web/vendor/bootstrap/css/bootstrap-reboot.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */ 8 | *, 9 | *::before, 10 | *::after { 11 | box-sizing: border-box; 12 | } 13 | 14 | html { 15 | font-family: sans-serif; 16 | line-height: 1.15; 17 | -webkit-text-size-adjust: 100%; 18 | -ms-text-size-adjust: 100%; 19 | -ms-overflow-style: scrollbar; 20 | -webkit-tap-highlight-color: transparent; 21 | } 22 | 23 | @-ms-viewport { 24 | width: device-width; 25 | } 26 | 27 | article, aside, dialog, figcaption, figure, footer, header, hgroup, main, nav, section { 28 | display: block; 29 | } 30 | 31 | body { 32 | margin: 0; 33 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | line-height: 1.5; 37 | color: #212529; 38 | text-align: left; 39 | background-color: #fff; 40 | } 41 | 42 | [tabindex="-1"]:focus { 43 | outline: none !important; 44 | } 45 | 46 | hr { 47 | box-sizing: content-box; 48 | height: 0; 49 | overflow: visible; 50 | } 51 | 52 | h1, h2, h3, h4, h5, h6 { 53 | margin-top: 0; 54 | margin-bottom: 0.5rem; 55 | } 56 | 57 | p { 58 | margin-top: 0; 59 | margin-bottom: 1rem; 60 | } 61 | 62 | abbr[title], 63 | abbr[data-original-title] { 64 | text-decoration: underline; 65 | -webkit-text-decoration: underline dotted; 66 | text-decoration: underline dotted; 67 | cursor: help; 68 | border-bottom: 0; 69 | } 70 | 71 | address { 72 | margin-bottom: 1rem; 73 | font-style: normal; 74 | line-height: inherit; 75 | } 76 | 77 | ol, 78 | ul, 79 | dl { 80 | margin-top: 0; 81 | margin-bottom: 1rem; 82 | } 83 | 84 | ol ol, 85 | ul ul, 86 | ol ul, 87 | ul ol { 88 | margin-bottom: 0; 89 | } 90 | 91 | dt { 92 | font-weight: 700; 93 | } 94 | 95 | dd { 96 | margin-bottom: .5rem; 97 | margin-left: 0; 98 | } 99 | 100 | blockquote { 101 | margin: 0 0 1rem; 102 | } 103 | 104 | dfn { 105 | font-style: italic; 106 | } 107 | 108 | b, 109 | strong { 110 | font-weight: bolder; 111 | } 112 | 113 | small { 114 | font-size: 80%; 115 | } 116 | 117 | sub, 118 | sup { 119 | position: relative; 120 | font-size: 75%; 121 | line-height: 0; 122 | vertical-align: baseline; 123 | } 124 | 125 | sub { 126 | bottom: -.25em; 127 | } 128 | 129 | sup { 130 | top: -.5em; 131 | } 132 | 133 | a { 134 | color: #007bff; 135 | text-decoration: none; 136 | background-color: transparent; 137 | -webkit-text-decoration-skip: objects; 138 | } 139 | 140 | a:hover { 141 | color: #0056b3; 142 | text-decoration: underline; 143 | } 144 | 145 | a:not([href]):not([tabindex]) { 146 | color: inherit; 147 | text-decoration: none; 148 | } 149 | 150 | a:not([href]):not([tabindex]):focus, a:not([href]):not([tabindex]):hover { 151 | color: inherit; 152 | text-decoration: none; 153 | } 154 | 155 | a:not([href]):not([tabindex]):focus { 156 | outline: 0; 157 | } 158 | 159 | pre, 160 | code, 161 | kbd, 162 | samp { 163 | font-family: monospace, monospace; 164 | font-size: 1em; 165 | } 166 | 167 | pre { 168 | margin-top: 0; 169 | margin-bottom: 1rem; 170 | overflow: auto; 171 | -ms-overflow-style: scrollbar; 172 | } 173 | 174 | figure { 175 | margin: 0 0 1rem; 176 | } 177 | 178 | img { 179 | vertical-align: middle; 180 | border-style: none; 181 | } 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | a, 188 | area, 189 | button, 190 | [role="button"], 191 | input:not([type="range"]), 192 | label, 193 | select, 194 | summary, 195 | textarea { 196 | -ms-touch-action: manipulation; 197 | touch-action: manipulation; 198 | } 199 | 200 | table { 201 | border-collapse: collapse; 202 | } 203 | 204 | caption { 205 | padding-top: 0.75rem; 206 | padding-bottom: 0.75rem; 207 | color: #868e96; 208 | text-align: left; 209 | caption-side: bottom; 210 | } 211 | 212 | th { 213 | text-align: inherit; 214 | } 215 | 216 | label { 217 | display: inline-block; 218 | margin-bottom: .5rem; 219 | } 220 | 221 | button { 222 | border-radius: 0; 223 | } 224 | 225 | button:focus { 226 | outline: 1px dotted; 227 | outline: 5px auto -webkit-focus-ring-color; 228 | } 229 | 230 | input, 231 | button, 232 | select, 233 | optgroup, 234 | textarea { 235 | margin: 0; 236 | font-family: inherit; 237 | font-size: inherit; 238 | line-height: inherit; 239 | } 240 | 241 | button, 242 | input { 243 | overflow: visible; 244 | } 245 | 246 | button, 247 | select { 248 | text-transform: none; 249 | } 250 | 251 | button, 252 | html [type="button"], 253 | [type="reset"], 254 | [type="submit"] { 255 | -webkit-appearance: button; 256 | } 257 | 258 | button::-moz-focus-inner, 259 | [type="button"]::-moz-focus-inner, 260 | [type="reset"]::-moz-focus-inner, 261 | [type="submit"]::-moz-focus-inner { 262 | padding: 0; 263 | border-style: none; 264 | } 265 | 266 | input[type="radio"], 267 | input[type="checkbox"] { 268 | box-sizing: border-box; 269 | padding: 0; 270 | } 271 | 272 | input[type="date"], 273 | input[type="time"], 274 | input[type="datetime-local"], 275 | input[type="month"] { 276 | -webkit-appearance: listbox; 277 | } 278 | 279 | textarea { 280 | overflow: auto; 281 | resize: vertical; 282 | } 283 | 284 | fieldset { 285 | min-width: 0; 286 | padding: 0; 287 | margin: 0; 288 | border: 0; 289 | } 290 | 291 | legend { 292 | display: block; 293 | width: 100%; 294 | max-width: 100%; 295 | padding: 0; 296 | margin-bottom: .5rem; 297 | font-size: 1.5rem; 298 | line-height: inherit; 299 | color: inherit; 300 | white-space: normal; 301 | } 302 | 303 | progress { 304 | vertical-align: baseline; 305 | } 306 | 307 | [type="number"]::-webkit-inner-spin-button, 308 | [type="number"]::-webkit-outer-spin-button { 309 | height: auto; 310 | } 311 | 312 | [type="search"] { 313 | outline-offset: -2px; 314 | -webkit-appearance: none; 315 | } 316 | 317 | [type="search"]::-webkit-search-cancel-button, 318 | [type="search"]::-webkit-search-decoration { 319 | -webkit-appearance: none; 320 | } 321 | 322 | ::-webkit-file-upload-button { 323 | font: inherit; 324 | -webkit-appearance: button; 325 | } 326 | 327 | output { 328 | display: inline-block; 329 | } 330 | 331 | summary { 332 | display: list-item; 333 | } 334 | 335 | template { 336 | display: none; 337 | } 338 | 339 | [hidden] { 340 | display: none !important; 341 | } 342 | /*# sourceMappingURL=bootstrap-reboot.css.map */ -------------------------------------------------------------------------------- /web/vendor/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v4.0.0-beta.2 (https://getbootstrap.com) 3 | * Copyright 2011-2017 The Bootstrap Authors 4 | * Copyright 2011-2017 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#007bff;text-decoration:none;background-color:transparent;-webkit-text-decoration-skip:objects}a:hover{color:#0056b3;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#868e96;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important} 8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */ -------------------------------------------------------------------------------- /web/vendor/d3cloud/d3.layout.cloud.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.d3||(g.d3 = {}));g=(g.layout||(g.layout = {}));g.cloud = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o> 5, 9 | ch = 1 << 11; 10 | 11 | module.exports = function() { 12 | var size = [256, 256], 13 | text = cloudText, 14 | font = cloudFont, 15 | fontSize = cloudFontSize, 16 | fontStyle = cloudFontNormal, 17 | fontWeight = cloudFontNormal, 18 | rotate = cloudRotate, 19 | padding = cloudPadding, 20 | spiral = archimedeanSpiral, 21 | words = [], 22 | timeInterval = Infinity, 23 | event = dispatch("word", "end"), 24 | timer = null, 25 | random = Math.random, 26 | cloud = {}, 27 | canvas = cloudCanvas; 28 | 29 | cloud.canvas = function(_) { 30 | return arguments.length ? (canvas = functor(_), cloud) : canvas; 31 | }; 32 | 33 | cloud.start = function() { 34 | var contextAndRatio = getContext(canvas()), 35 | board = zeroArray((size[0] >> 5) * size[1]), 36 | bounds = null, 37 | n = words.length, 38 | i = -1, 39 | tags = [], 40 | data = words.map(function(d, i) { 41 | d.text = text.call(this, d, i); 42 | d.font = font.call(this, d, i); 43 | d.style = fontStyle.call(this, d, i); 44 | d.weight = fontWeight.call(this, d, i); 45 | d.rotate = rotate.call(this, d, i); 46 | d.size = ~~fontSize.call(this, d, i); 47 | d.padding = padding.call(this, d, i); 48 | return d; 49 | }).sort(function(a, b) { return b.size - a.size; }); 50 | 51 | if (timer) clearInterval(timer); 52 | timer = setInterval(step, 0); 53 | step(); 54 | 55 | return cloud; 56 | 57 | function step() { 58 | var start = Date.now(); 59 | while (Date.now() - start < timeInterval && ++i < n && timer) { 60 | var d = data[i]; 61 | d.x = (size[0] * (random() + .5)) >> 1; 62 | d.y = (size[1] * (random() + .5)) >> 1; 63 | cloudSprite(contextAndRatio, d, data, i); 64 | if (d.hasText && place(board, d, bounds)) { 65 | tags.push(d); 66 | event.call("word", cloud, d); 67 | if (bounds) cloudBounds(bounds, d); 68 | else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}]; 69 | // Temporary hack 70 | d.x -= size[0] >> 1; 71 | d.y -= size[1] >> 1; 72 | } 73 | } 74 | if (i >= n) { 75 | cloud.stop(); 76 | event.call("end", cloud, tags, bounds); 77 | } 78 | } 79 | } 80 | 81 | cloud.stop = function() { 82 | if (timer) { 83 | clearInterval(timer); 84 | timer = null; 85 | } 86 | return cloud; 87 | }; 88 | 89 | function getContext(canvas) { 90 | canvas.width = canvas.height = 1; 91 | var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2); 92 | canvas.width = (cw << 5) / ratio; 93 | canvas.height = ch / ratio; 94 | 95 | var context = canvas.getContext("2d"); 96 | context.fillStyle = context.strokeStyle = "red"; 97 | context.textAlign = "center"; 98 | 99 | return {context: context, ratio: ratio}; 100 | } 101 | 102 | function place(board, tag, bounds) { 103 | var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}], 104 | startX = tag.x, 105 | startY = tag.y, 106 | maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]), 107 | s = spiral(size), 108 | dt = random() < .5 ? 1 : -1, 109 | t = -dt, 110 | dxdy, 111 | dx, 112 | dy; 113 | 114 | while (dxdy = s(t += dt)) { 115 | dx = ~~dxdy[0]; 116 | dy = ~~dxdy[1]; 117 | 118 | if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break; 119 | 120 | tag.x = startX + dx; 121 | tag.y = startY + dy; 122 | 123 | if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 || 124 | tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue; 125 | // TODO only check for collisions within current bounds. 126 | if (!bounds || !cloudCollide(tag, board, size[0])) { 127 | if (!bounds || collideRects(tag, bounds)) { 128 | var sprite = tag.sprite, 129 | w = tag.width >> 5, 130 | sw = size[0] >> 5, 131 | lx = tag.x - (w << 4), 132 | sx = lx & 0x7f, 133 | msx = 32 - sx, 134 | h = tag.y1 - tag.y0, 135 | x = (tag.y + tag.y0) * sw + (lx >> 5), 136 | last; 137 | for (var j = 0; j < h; j++) { 138 | last = 0; 139 | for (var i = 0; i <= w; i++) { 140 | board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0); 141 | } 142 | x += sw; 143 | } 144 | delete tag.sprite; 145 | return true; 146 | } 147 | } 148 | } 149 | return false; 150 | } 151 | 152 | cloud.timeInterval = function(_) { 153 | return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval; 154 | }; 155 | 156 | cloud.words = function(_) { 157 | return arguments.length ? (words = _, cloud) : words; 158 | }; 159 | 160 | cloud.size = function(_) { 161 | return arguments.length ? (size = [+_[0], +_[1]], cloud) : size; 162 | }; 163 | 164 | cloud.font = function(_) { 165 | return arguments.length ? (font = functor(_), cloud) : font; 166 | }; 167 | 168 | cloud.fontStyle = function(_) { 169 | return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle; 170 | }; 171 | 172 | cloud.fontWeight = function(_) { 173 | return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight; 174 | }; 175 | 176 | cloud.rotate = function(_) { 177 | return arguments.length ? (rotate = functor(_), cloud) : rotate; 178 | }; 179 | 180 | cloud.text = function(_) { 181 | return arguments.length ? (text = functor(_), cloud) : text; 182 | }; 183 | 184 | cloud.spiral = function(_) { 185 | return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral; 186 | }; 187 | 188 | cloud.fontSize = function(_) { 189 | return arguments.length ? (fontSize = functor(_), cloud) : fontSize; 190 | }; 191 | 192 | cloud.padding = function(_) { 193 | return arguments.length ? (padding = functor(_), cloud) : padding; 194 | }; 195 | 196 | cloud.random = function(_) { 197 | return arguments.length ? (random = _, cloud) : random; 198 | }; 199 | 200 | cloud.on = function() { 201 | var value = event.on.apply(event, arguments); 202 | return value === event ? cloud : value; 203 | }; 204 | 205 | return cloud; 206 | }; 207 | 208 | function cloudText(d) { 209 | return d.text; 210 | } 211 | 212 | function cloudFont() { 213 | return "serif"; 214 | } 215 | 216 | function cloudFontNormal() { 217 | return "normal"; 218 | } 219 | 220 | function cloudFontSize(d) { 221 | return Math.sqrt(d.value); 222 | } 223 | 224 | function cloudRotate() { 225 | return (~~(Math.random() * 6) - 3) * 30; 226 | } 227 | 228 | function cloudPadding() { 229 | return 1; 230 | } 231 | 232 | // Fetches a monochrome sprite bitmap for the specified text. 233 | // Load in batches for speed. 234 | function cloudSprite(contextAndRatio, d, data, di) { 235 | if (d.sprite) return; 236 | var c = contextAndRatio.context, 237 | ratio = contextAndRatio.ratio; 238 | 239 | c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio); 240 | var x = 0, 241 | y = 0, 242 | maxh = 0, 243 | n = data.length; 244 | --di; 245 | while (++di < n) { 246 | d = data[di]; 247 | c.save(); 248 | c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font; 249 | var w = c.measureText(d.text + "m").width * ratio, 250 | h = d.size << 1; 251 | if (d.rotate) { 252 | var sr = Math.sin(d.rotate * cloudRadians), 253 | cr = Math.cos(d.rotate * cloudRadians), 254 | wcr = w * cr, 255 | wsr = w * sr, 256 | hcr = h * cr, 257 | hsr = h * sr; 258 | w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5; 259 | h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr)); 260 | } else { 261 | w = (w + 0x1f) >> 5 << 5; 262 | } 263 | if (h > maxh) maxh = h; 264 | if (x + w >= (cw << 5)) { 265 | x = 0; 266 | y += maxh; 267 | maxh = 0; 268 | } 269 | if (y + h >= ch) break; 270 | c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio); 271 | if (d.rotate) c.rotate(d.rotate * cloudRadians); 272 | c.fillText(d.text, 0, 0); 273 | if (d.padding) c.lineWidth = 2 * d.padding, c.strokeText(d.text, 0, 0); 274 | c.restore(); 275 | d.width = w; 276 | d.height = h; 277 | d.xoff = x; 278 | d.yoff = y; 279 | d.x1 = w >> 1; 280 | d.y1 = h >> 1; 281 | d.x0 = -d.x1; 282 | d.y0 = -d.y1; 283 | d.hasText = true; 284 | x += w; 285 | } 286 | var pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data, 287 | sprite = []; 288 | while (--di >= 0) { 289 | d = data[di]; 290 | if (!d.hasText) continue; 291 | var w = d.width, 292 | w32 = w >> 5, 293 | h = d.y1 - d.y0; 294 | // Zero the buffer 295 | for (var i = 0; i < h * w32; i++) sprite[i] = 0; 296 | x = d.xoff; 297 | if (x == null) return; 298 | y = d.yoff; 299 | var seen = 0, 300 | seenRow = -1; 301 | for (var j = 0; j < h; j++) { 302 | for (var i = 0; i < w; i++) { 303 | var k = w32 * j + (i >> 5), 304 | m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0; 305 | sprite[k] |= m; 306 | seen |= m; 307 | } 308 | if (seen) seenRow = j; 309 | else { 310 | d.y0++; 311 | h--; 312 | j--; 313 | y++; 314 | } 315 | } 316 | d.y1 = d.y0 + seenRow; 317 | d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32); 318 | } 319 | } 320 | 321 | // Use mask-based collision detection. 322 | function cloudCollide(tag, board, sw) { 323 | sw >>= 5; 324 | var sprite = tag.sprite, 325 | w = tag.width >> 5, 326 | lx = tag.x - (w << 4), 327 | sx = lx & 0x7f, 328 | msx = 32 - sx, 329 | h = tag.y1 - tag.y0, 330 | x = (tag.y + tag.y0) * sw + (lx >> 5), 331 | last; 332 | for (var j = 0; j < h; j++) { 333 | last = 0; 334 | for (var i = 0; i <= w; i++) { 335 | if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0)) 336 | & board[x + i]) return true; 337 | } 338 | x += sw; 339 | } 340 | return false; 341 | } 342 | 343 | function cloudBounds(bounds, d) { 344 | var b0 = bounds[0], 345 | b1 = bounds[1]; 346 | if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0; 347 | if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0; 348 | if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1; 349 | if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1; 350 | } 351 | 352 | function collideRects(a, b) { 353 | return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y; 354 | } 355 | 356 | function archimedeanSpiral(size) { 357 | var e = size[0] / size[1]; 358 | return function(t) { 359 | return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)]; 360 | }; 361 | } 362 | 363 | function rectangularSpiral(size) { 364 | var dy = 4, 365 | dx = dy * size[0] / size[1], 366 | x = 0, 367 | y = 0; 368 | return function(t) { 369 | var sign = t < 0 ? -1 : 1; 370 | // See triangular numbers: T_n = n * (n + 1) / 2. 371 | switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) { 372 | case 0: x += dx; break; 373 | case 1: y += dy; break; 374 | case 2: x -= dx; break; 375 | default: y -= dy; break; 376 | } 377 | return [x, y]; 378 | }; 379 | } 380 | 381 | // TODO reuse arrays? 382 | function zeroArray(n) { 383 | var a = [], 384 | i = -1; 385 | while (++i < n) a[i] = 0; 386 | return a; 387 | } 388 | 389 | function cloudCanvas() { 390 | return document.createElement("canvas"); 391 | } 392 | 393 | function functor(d) { 394 | return typeof d === "function" ? d : function() { return d; }; 395 | } 396 | 397 | var spirals = { 398 | archimedean: archimedeanSpiral, 399 | rectangular: rectangularSpiral 400 | }; 401 | 402 | },{"d3-dispatch":2}],2:[function(require,module,exports){ 403 | // https://d3js.org/d3-dispatch/ Version 1.0.3. Copyright 2017 Mike Bostock. 404 | (function (global, factory) { 405 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 406 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 407 | (factory((global.d3 = global.d3 || {}))); 408 | }(this, (function (exports) { 'use strict'; 409 | 410 | var noop = {value: function() {}}; 411 | 412 | function dispatch() { 413 | for (var i = 0, n = arguments.length, _ = {}, t; i < n; ++i) { 414 | if (!(t = arguments[i] + "") || (t in _)) throw new Error("illegal type: " + t); 415 | _[t] = []; 416 | } 417 | return new Dispatch(_); 418 | } 419 | 420 | function Dispatch(_) { 421 | this._ = _; 422 | } 423 | 424 | function parseTypenames(typenames, types) { 425 | return typenames.trim().split(/^|\s+/).map(function(t) { 426 | var name = "", i = t.indexOf("."); 427 | if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i); 428 | if (t && !types.hasOwnProperty(t)) throw new Error("unknown type: " + t); 429 | return {type: t, name: name}; 430 | }); 431 | } 432 | 433 | Dispatch.prototype = dispatch.prototype = { 434 | constructor: Dispatch, 435 | on: function(typename, callback) { 436 | var _ = this._, 437 | T = parseTypenames(typename + "", _), 438 | t, 439 | i = -1, 440 | n = T.length; 441 | 442 | // If no callback was specified, return the callback of the given type and name. 443 | if (arguments.length < 2) { 444 | while (++i < n) if ((t = (typename = T[i]).type) && (t = get(_[t], typename.name))) return t; 445 | return; 446 | } 447 | 448 | // If a type was specified, set the callback for the given type and name. 449 | // Otherwise, if a null callback was specified, remove callbacks of the given name. 450 | if (callback != null && typeof callback !== "function") throw new Error("invalid callback: " + callback); 451 | while (++i < n) { 452 | if (t = (typename = T[i]).type) _[t] = set(_[t], typename.name, callback); 453 | else if (callback == null) for (t in _) _[t] = set(_[t], typename.name, null); 454 | } 455 | 456 | return this; 457 | }, 458 | copy: function() { 459 | var copy = {}, _ = this._; 460 | for (var t in _) copy[t] = _[t].slice(); 461 | return new Dispatch(copy); 462 | }, 463 | call: function(type, that) { 464 | if ((n = arguments.length - 2) > 0) for (var args = new Array(n), i = 0, n, t; i < n; ++i) args[i] = arguments[i + 2]; 465 | if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); 466 | for (t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); 467 | }, 468 | apply: function(type, that, args) { 469 | if (!this._.hasOwnProperty(type)) throw new Error("unknown type: " + type); 470 | for (var t = this._[type], i = 0, n = t.length; i < n; ++i) t[i].value.apply(that, args); 471 | } 472 | }; 473 | 474 | function get(type, name) { 475 | for (var i = 0, n = type.length, c; i < n; ++i) { 476 | if ((c = type[i]).name === name) { 477 | return c.value; 478 | } 479 | } 480 | } 481 | 482 | function set(type, name, callback) { 483 | for (var i = 0, n = type.length; i < n; ++i) { 484 | if (type[i].name === name) { 485 | type[i] = noop, type = type.slice(0, i).concat(type.slice(i + 1)); 486 | break; 487 | } 488 | } 489 | if (callback != null) type.push({name: name, value: callback}); 490 | return type; 491 | } 492 | 493 | exports.dispatch = dispatch; 494 | 495 | Object.defineProperty(exports, '__esModule', { value: true }); 496 | 497 | }))); 498 | 499 | },{}]},{},[1])(1) 500 | }); -------------------------------------------------------------------------------- /web/vendor/datatables/dataTables.bootstrap4.css: -------------------------------------------------------------------------------- 1 | table.dataTable { 2 | clear: both; 3 | margin-top: 6px !important; 4 | margin-bottom: 6px !important; 5 | max-width: none !important; 6 | border-collapse: separate !important; 7 | } 8 | table.dataTable td, 9 | table.dataTable th { 10 | -webkit-box-sizing: content-box; 11 | box-sizing: content-box; 12 | } 13 | table.dataTable td.dataTables_empty, 14 | table.dataTable th.dataTables_empty { 15 | text-align: center; 16 | } 17 | table.dataTable.nowrap th, 18 | table.dataTable.nowrap td { 19 | white-space: nowrap; 20 | } 21 | 22 | div.dataTables_wrapper div.dataTables_length label { 23 | font-weight: normal; 24 | text-align: left; 25 | white-space: nowrap; 26 | } 27 | div.dataTables_wrapper div.dataTables_length select { 28 | width: 75px; 29 | display: inline-block; 30 | } 31 | div.dataTables_wrapper div.dataTables_filter { 32 | text-align: right; 33 | } 34 | div.dataTables_wrapper div.dataTables_filter label { 35 | font-weight: normal; 36 | white-space: nowrap; 37 | text-align: left; 38 | } 39 | div.dataTables_wrapper div.dataTables_filter input { 40 | margin-left: 0.5em; 41 | display: inline-block; 42 | width: auto; 43 | } 44 | div.dataTables_wrapper div.dataTables_info { 45 | padding-top: 0.85em; 46 | white-space: nowrap; 47 | } 48 | div.dataTables_wrapper div.dataTables_paginate { 49 | margin: 0; 50 | white-space: nowrap; 51 | text-align: right; 52 | } 53 | div.dataTables_wrapper div.dataTables_paginate ul.pagination { 54 | margin: 2px 0; 55 | white-space: nowrap; 56 | justify-content: flex-end; 57 | } 58 | div.dataTables_wrapper div.dataTables_processing { 59 | position: absolute; 60 | top: 50%; 61 | left: 50%; 62 | width: 200px; 63 | margin-left: -100px; 64 | margin-top: -26px; 65 | text-align: center; 66 | padding: 1em 0; 67 | } 68 | 69 | table.dataTable thead > tr > th.sorting_asc, table.dataTable thead > tr > th.sorting_desc, table.dataTable thead > tr > th.sorting, 70 | table.dataTable thead > tr > td.sorting_asc, 71 | table.dataTable thead > tr > td.sorting_desc, 72 | table.dataTable thead > tr > td.sorting { 73 | padding-right: 30px; 74 | } 75 | table.dataTable thead > tr > th:active, 76 | table.dataTable thead > tr > td:active { 77 | outline: none; 78 | } 79 | table.dataTable thead .sorting, 80 | table.dataTable thead .sorting_asc, 81 | table.dataTable thead .sorting_desc, 82 | table.dataTable thead .sorting_asc_disabled, 83 | table.dataTable thead .sorting_desc_disabled { 84 | cursor: pointer; 85 | position: relative; 86 | } 87 | table.dataTable thead .sorting:before, table.dataTable thead .sorting:after, 88 | table.dataTable thead .sorting_asc:before, 89 | table.dataTable thead .sorting_asc:after, 90 | table.dataTable thead .sorting_desc:before, 91 | table.dataTable thead .sorting_desc:after, 92 | table.dataTable thead .sorting_asc_disabled:before, 93 | table.dataTable thead .sorting_asc_disabled:after, 94 | table.dataTable thead .sorting_desc_disabled:before, 95 | table.dataTable thead .sorting_desc_disabled:after { 96 | position: absolute; 97 | bottom: 0.9em; 98 | display: block; 99 | opacity: 0.3; 100 | } 101 | table.dataTable thead .sorting:before, 102 | table.dataTable thead .sorting_asc:before, 103 | table.dataTable thead .sorting_desc:before, 104 | table.dataTable thead .sorting_asc_disabled:before, 105 | table.dataTable thead .sorting_desc_disabled:before { 106 | right: 1em; 107 | content: "\2191"; 108 | } 109 | table.dataTable thead .sorting:after, 110 | table.dataTable thead .sorting_asc:after, 111 | table.dataTable thead .sorting_desc:after, 112 | table.dataTable thead .sorting_asc_disabled:after, 113 | table.dataTable thead .sorting_desc_disabled:after { 114 | right: 0.5em; 115 | content: "\2193"; 116 | } 117 | table.dataTable thead .sorting_asc:before, 118 | table.dataTable thead .sorting_desc:after { 119 | opacity: 1; 120 | } 121 | table.dataTable thead .sorting_asc_disabled:before, 122 | table.dataTable thead .sorting_desc_disabled:after { 123 | opacity: 0; 124 | } 125 | 126 | div.dataTables_scrollHead table.dataTable { 127 | margin-bottom: 0 !important; 128 | } 129 | 130 | div.dataTables_scrollBody table { 131 | border-top: none; 132 | margin-top: 0 !important; 133 | margin-bottom: 0 !important; 134 | } 135 | div.dataTables_scrollBody table thead .sorting:after, 136 | div.dataTables_scrollBody table thead .sorting_asc:after, 137 | div.dataTables_scrollBody table thead .sorting_desc:after { 138 | display: none; 139 | } 140 | div.dataTables_scrollBody table tbody tr:first-child th, 141 | div.dataTables_scrollBody table tbody tr:first-child td { 142 | border-top: none; 143 | } 144 | 145 | div.dataTables_scrollFoot > .dataTables_scrollFootInner { 146 | box-sizing: content-box; 147 | } 148 | div.dataTables_scrollFoot > .dataTables_scrollFootInner > table { 149 | margin-top: 0 !important; 150 | border-top: none; 151 | } 152 | 153 | @media screen and (max-width: 767px) { 154 | div.dataTables_wrapper div.dataTables_length, 155 | div.dataTables_wrapper div.dataTables_filter, 156 | div.dataTables_wrapper div.dataTables_info, 157 | div.dataTables_wrapper div.dataTables_paginate { 158 | text-align: center; 159 | } 160 | } 161 | table.dataTable.table-sm > thead > tr > th { 162 | padding-right: 20px; 163 | } 164 | table.dataTable.table-sm .sorting:before, 165 | table.dataTable.table-sm .sorting_asc:before, 166 | table.dataTable.table-sm .sorting_desc:before { 167 | top: 5px; 168 | right: 0.85em; 169 | } 170 | table.dataTable.table-sm .sorting:after, 171 | table.dataTable.table-sm .sorting_asc:after, 172 | table.dataTable.table-sm .sorting_desc:after { 173 | top: 5px; 174 | } 175 | 176 | table.table-bordered.dataTable th, 177 | table.table-bordered.dataTable td { 178 | border-left-width: 0; 179 | } 180 | table.table-bordered.dataTable th:last-child, table.table-bordered.dataTable th:last-child, 181 | table.table-bordered.dataTable td:last-child, 182 | table.table-bordered.dataTable td:last-child { 183 | border-right-width: 0; 184 | } 185 | table.table-bordered.dataTable tbody th, 186 | table.table-bordered.dataTable tbody td { 187 | border-bottom-width: 0; 188 | } 189 | 190 | div.dataTables_scrollHead table.table-bordered { 191 | border-bottom-width: 0; 192 | } 193 | 194 | div.table-responsive > div.dataTables_wrapper > div.row { 195 | margin: 0; 196 | } 197 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:first-child { 198 | padding-left: 0; 199 | } 200 | div.table-responsive > div.dataTables_wrapper > div.row > div[class^="col-"]:last-child { 201 | padding-right: 0; 202 | } 203 | -------------------------------------------------------------------------------- /web/vendor/datatables/dataTables.bootstrap4.js: -------------------------------------------------------------------------------- 1 | /*! DataTables Bootstrap 3 integration 2 | * ©2011-2015 SpryMedia Ltd - datatables.net/license 3 | */ 4 | 5 | /** 6 | * DataTables integration for Bootstrap 3. This requires Bootstrap 3 and 7 | * DataTables 1.10 or newer. 8 | * 9 | * This file sets the defaults and adds options to DataTables to style its 10 | * controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap 11 | * for further information. 12 | */ 13 | (function( factory ){ 14 | if ( typeof define === 'function' && define.amd ) { 15 | // AMD 16 | define( ['jquery', 'datatables.net'], function ( $ ) { 17 | return factory( $, window, document ); 18 | } ); 19 | } 20 | else if ( typeof exports === 'object' ) { 21 | // CommonJS 22 | module.exports = function (root, $) { 23 | if ( ! root ) { 24 | root = window; 25 | } 26 | 27 | if ( ! $ || ! $.fn.dataTable ) { 28 | // Require DataTables, which attaches to jQuery, including 29 | // jQuery if needed and have a $ property so we can access the 30 | // jQuery object that is used 31 | $ = require('datatables.net')(root, $).$; 32 | } 33 | 34 | return factory( $, root, root.document ); 35 | }; 36 | } 37 | else { 38 | // Browser 39 | factory( jQuery, window, document ); 40 | } 41 | }(function( $, window, document, undefined ) { 42 | 'use strict'; 43 | var DataTable = $.fn.dataTable; 44 | 45 | 46 | /* Set the defaults for DataTables initialisation */ 47 | $.extend( true, DataTable.defaults, { 48 | dom: 49 | "<'row'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" + 50 | "<'row'<'col-sm-12'tr>>" + 51 | "<'row'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>", 52 | renderer: 'bootstrap' 53 | } ); 54 | 55 | 56 | /* Default class modification */ 57 | $.extend( DataTable.ext.classes, { 58 | sWrapper: "dataTables_wrapper container-fluid dt-bootstrap4", 59 | sFilterInput: "form-control form-control-sm", 60 | sLengthSelect: "form-control form-control-sm", 61 | sProcessing: "dataTables_processing card", 62 | sPageButton: "paginate_button page-item" 63 | } ); 64 | 65 | 66 | /* Bootstrap paging button renderer */ 67 | DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) { 68 | var api = new DataTable.Api( settings ); 69 | var classes = settings.oClasses; 70 | var lang = settings.oLanguage.oPaginate; 71 | var aria = settings.oLanguage.oAria.paginate || {}; 72 | var btnDisplay, btnClass, counter=0; 73 | 74 | var attach = function( container, buttons ) { 75 | var i, ien, node, button; 76 | var clickHandler = function ( e ) { 77 | e.preventDefault(); 78 | if ( !$(e.currentTarget).hasClass('disabled') && api.page() != e.data.action ) { 79 | api.page( e.data.action ).draw( 'page' ); 80 | } 81 | }; 82 | 83 | for ( i=0, ien=buttons.length ; i 0 ? 102 | '' : ' disabled'); 103 | break; 104 | 105 | case 'previous': 106 | btnDisplay = lang.sPrevious; 107 | btnClass = button + (page > 0 ? 108 | '' : ' disabled'); 109 | break; 110 | 111 | case 'next': 112 | btnDisplay = lang.sNext; 113 | btnClass = button + (page < pages-1 ? 114 | '' : ' disabled'); 115 | break; 116 | 117 | case 'last': 118 | btnDisplay = lang.sLast; 119 | btnClass = button + (page < pages-1 ? 120 | '' : ' disabled'); 121 | break; 122 | 123 | default: 124 | btnDisplay = button + 1; 125 | btnClass = page === button ? 126 | 'active' : ''; 127 | break; 128 | } 129 | 130 | if ( btnDisplay ) { 131 | node = $('
  • ', { 132 | 'class': classes.sPageButton+' '+btnClass, 133 | 'id': idx === 0 && typeof button === 'string' ? 134 | settings.sTableId +'_'+ button : 135 | null 136 | } ) 137 | .append( $('', { 138 | 'href': '#', 139 | 'aria-controls': settings.sTableId, 140 | 'aria-label': aria[ button ], 141 | 'data-dt-idx': counter, 142 | 'tabindex': settings.iTabIndex, 143 | 'class': 'page-link' 144 | } ) 145 | .html( btnDisplay ) 146 | ) 147 | .appendTo( container ); 148 | 149 | settings.oApi._fnBindAction( 150 | node, {action: button}, clickHandler 151 | ); 152 | 153 | counter++; 154 | } 155 | } 156 | } 157 | }; 158 | 159 | // IE9 throws an 'unknown error' if document.activeElement is used 160 | // inside an iframe or frame. 161 | var activeEl; 162 | 163 | try { 164 | // Because this approach is destroying and recreating the paging 165 | // elements, focus is lost on the select button which is bad for 166 | // accessibility. So we want to restore focus once the draw has 167 | // completed 168 | activeEl = $(host).find(document.activeElement).data('dt-idx'); 169 | } 170 | catch (e) {} 171 | 172 | attach( 173 | $(host).empty().html('