github.com/jonathaningram/gophish@v0.3.1-0.20170829042651-ac3fe6aeae6c/static/js/src/app/campaign_results.js (about) 1 var map = null 2 var doPoll = true; 3 4 // statuses is a helper map to point result statuses to ui classes 5 var statuses = { 6 "Email Sent": { 7 color: "#1abc9c", 8 label: "label-success", 9 icon: "fa-envelope", 10 point: "ct-point-sent" 11 }, 12 "Emails Sent": { 13 color: "#1abc9c", 14 label: "label-success", 15 icon: "fa-envelope", 16 point: "ct-point-sent" 17 }, 18 "In progress": { 19 label: "label-primary" 20 }, 21 "Queued": { 22 label: "label-info" 23 }, 24 "Completed": { 25 label: "label-success" 26 }, 27 "Email Opened": { 28 color: "#f9bf3b", 29 label: "label-warning", 30 icon: "fa-envelope", 31 point: "ct-point-opened" 32 }, 33 "Clicked Link": { 34 color: "#F39C12", 35 label: "label-clicked", 36 icon: "fa-mouse-pointer", 37 point: "ct-point-clicked" 38 }, 39 "Success": { 40 color: "#f05b4f", 41 label: "label-danger", 42 icon: "fa-exclamation", 43 point: "ct-point-clicked" 44 }, 45 "Error": { 46 color: "#6c7a89", 47 label: "label-default", 48 icon: "fa-times", 49 point: "ct-point-error" 50 }, 51 "Error Sending Email": { 52 color: "#6c7a89", 53 label: "label-default", 54 icon: "fa-times", 55 point: "ct-point-error" 56 }, 57 "Submitted Data": { 58 color: "#f05b4f", 59 label: "label-danger", 60 icon: "fa-exclamation", 61 point: "ct-point-clicked" 62 }, 63 "Unknown": { 64 color: "#6c7a89", 65 label: "label-default", 66 icon: "fa-question", 67 point: "ct-point-error" 68 }, 69 "Sending": { 70 color: "#428bca", 71 label: "label-primary", 72 icon: "fa-spinner", 73 point: "ct-point-sending" 74 }, 75 "Campaign Created": { 76 label: "label-success", 77 icon: "fa-rocket" 78 } 79 } 80 81 var statusMapping = { 82 "Email Sent": "sent", 83 "Email Opened": "opened", 84 "Clicked Link": "clicked", 85 "Submitted Data": "submitted_data", 86 } 87 88 // This is an underwhelming attempt at an enum 89 // until I have time to refactor this appropriately. 90 var progressListing = [ 91 "Email Sent", 92 "Email Opened", 93 "Clicked Link", 94 "Submitted Data" 95 ] 96 97 var campaign = {} 98 var bubbles = [] 99 100 function dismiss() { 101 $("#modal\\.flashes").empty() 102 $("#modal").modal('hide') 103 $("#resultsTable").dataTable().DataTable().clear().draw() 104 } 105 106 // Deletes a campaign after prompting the user 107 function deleteCampaign() { 108 swal({ 109 title: "Are you sure?", 110 text: "This will delete the campaign. This can't be undone!", 111 type: "warning", 112 animation: false, 113 showCancelButton: true, 114 confirmButtonText: "Delete Campaign", 115 confirmButtonColor: "#428bca", 116 reverseButtons: true, 117 allowOutsideClick: false, 118 showLoaderOnConfirm: true, 119 preConfirm: function () { 120 return new Promise(function (resolve, reject) { 121 api.campaignId.delete(campaign.id) 122 .success(function (msg) { 123 resolve() 124 }) 125 .error(function (data) { 126 reject(data.responseJSON.message) 127 }) 128 }) 129 } 130 }).then(function () { 131 swal( 132 'Campaign Deleted!', 133 'This campaign has been deleted!', 134 'success' 135 ); 136 $('button:contains("OK")').on('click', function () { 137 location.href = '/campaigns' 138 }) 139 }) 140 } 141 142 // Completes a campaign after prompting the user 143 function completeCampaign() { 144 swal({ 145 title: "Are you sure?", 146 text: "Gophish will stop processing events for this campaign", 147 type: "warning", 148 animation: false, 149 showCancelButton: true, 150 confirmButtonText: "Complete Campaign", 151 confirmButtonColor: "#428bca", 152 reverseButtons: true, 153 allowOutsideClick: false, 154 showLoaderOnConfirm: true, 155 preConfirm: function () { 156 return new Promise(function (resolve, reject) { 157 api.campaignId.complete(campaign.id) 158 .success(function (msg) { 159 resolve() 160 }) 161 .error(function (data) { 162 reject(data.responseJSON.message) 163 }) 164 }) 165 } 166 }).then(function () { 167 swal( 168 'Campaign Completed!', 169 'This campaign has been completed!', 170 'success' 171 ); 172 $('#complete_button')[0].disabled = true; 173 $('#complete_button').text('Completed!') 174 doPoll = false; 175 }) 176 } 177 178 // Exports campaign results as a CSV file 179 function exportAsCSV(scope) { 180 exportHTML = $("#exportButton").html() 181 var csvScope = null 182 switch (scope) { 183 case "results": 184 csvScope = campaign.results 185 break; 186 case "events": 187 csvScope = campaign.timeline 188 break; 189 } 190 if (!csvScope) { 191 return 192 } 193 $("#exportButton").html('<i class="fa fa-spinner fa-spin"></i>') 194 var csvString = Papa.unparse(csvScope, {}) 195 var csvData = new Blob([csvString], { 196 type: 'text/csv;charset=utf-8;' 197 }); 198 if (navigator.msSaveBlob) { 199 navigator.msSaveBlob(csvData, scope + '.csv'); 200 } else { 201 var csvURL = window.URL.createObjectURL(csvData); 202 var dlLink = document.createElement('a'); 203 dlLink.href = csvURL; 204 dlLink.setAttribute('download', scope + '.csv'); 205 document.body.appendChild(dlLink) 206 dlLink.click(); 207 document.body.removeChild(dlLink) 208 } 209 $("#exportButton").html(exportHTML) 210 } 211 212 function replay(event_idx) { 213 request = campaign.timeline[event_idx] 214 details = JSON.parse(request.details) 215 url = null 216 form = $('<form>').attr({ 217 method: 'POST', 218 target: '_blank', 219 }) 220 /* Create a form object and submit it */ 221 $.each(Object.keys(details.payload), function (i, param) { 222 if (param == "rid") { 223 return true; 224 } 225 if (param == "__original_url") { 226 url = details.payload[param]; 227 return true; 228 } 229 $('<input>').attr({ 230 name: param, 231 }).val(details.payload[param]).appendTo(form); 232 }) 233 /* Ensure we know where to send the user */ 234 // Prompt for the URL 235 swal({ 236 title: 'Where do you want the credentials submitted to?', 237 input: 'text', 238 showCancelButton: true, 239 inputPlaceholder: "http://example.com/login", 240 inputValue: url || "", 241 inputValidator: function (value) { 242 return new Promise(function (resolve, reject) { 243 if (value) { 244 resolve(); 245 } else { 246 reject('Invalid URL.'); 247 } 248 }); 249 } 250 }).then(function (result) { 251 url = result 252 submitForm() 253 }) 254 return 255 submitForm() 256 257 function submitForm() { 258 form.attr({ 259 action: url 260 }) 261 form.appendTo('body').submit().remove() 262 } 263 } 264 265 function renderTimeline(data) { 266 record = { 267 "first_name": data[2], 268 "last_name": data[3], 269 "email": data[4], 270 "position": data[5] 271 } 272 results = '<div class="timeline col-sm-12 well well-lg">' + 273 '<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + 274 '</h6><span class="subtitle">Email: ' + escapeHtml(record.email) + '</span>' + 275 '<div class="timeline-graph col-sm-6">' 276 $.each(campaign.timeline, function (i, event) { 277 if (!event.email || event.email == record.email) { 278 // Add the event 279 results += '<div class="timeline-entry">' + 280 ' <div class="timeline-bar"></div>' 281 results += 282 ' <div class="timeline-icon ' + statuses[event.message].label + '">' + 283 ' <i class="fa ' + statuses[event.message].icon + '"></i></div>' + 284 ' <div class="timeline-message">' + escapeHtml(event.message) + 285 ' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm a') + '</span>' 286 if (event.details) { 287 if (event.message == "Submitted Data") { 288 results += '<div class="timeline-replay-button"><button onclick="replay(' + i + ')" class="btn btn-success">' 289 results += '<i class="fa fa-refresh"></i> Replay Credentials</button></div>' 290 results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>' 291 } 292 details = JSON.parse(event.details) 293 if (details.payload) { 294 results += '<div class="timeline-event-results">' 295 results += ' <table class="table table-condensed table-bordered table-striped">' 296 results += ' <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>' 297 $.each(Object.keys(details.payload), function (i, param) { 298 if (param == "rid") { 299 return true; 300 } 301 results += ' <tr>' 302 results += ' <td>' + escapeHtml(param) + '</td>' 303 results += ' <td>' + escapeHtml(details.payload[param]) + '</td>' 304 results += ' </tr>' 305 }) 306 results += ' </tbody></table>' 307 results += '</div>' 308 } 309 if (details.error) { 310 results += '<div class="timeline-event-details"><i class="fa fa-caret-right"></i> View Details</div>' 311 results += '<div class="timeline-event-results">' 312 results += '<span class="label label-default">Error</span> ' + details.error 313 results += '</div>' 314 } 315 } 316 results += '</div></div>' 317 } 318 }) 319 results += '</div></div>' 320 return results 321 } 322 323 var renderTimelineChart = function (chartopts) { 324 return Highcharts.chart('timeline_chart', { 325 chart: { 326 zoomType: 'x', 327 type: 'line', 328 height: "200px" 329 }, 330 title: { 331 text: 'Campaign Timeline' 332 }, 333 xAxis: { 334 type: 'datetime', 335 dateTimeLabelFormats: { 336 second: '%l:%M:%S', 337 minute: '%l:%M', 338 hour: '%l:%M', 339 day: '%b %d, %Y', 340 week: '%b %d, %Y', 341 month: '%b %Y' 342 } 343 }, 344 yAxis: { 345 min: 0, 346 max: 2, 347 visible: false, 348 tickInterval: 1, 349 labels: { 350 enabled: false 351 }, 352 title: { 353 text: "" 354 } 355 }, 356 tooltip: { 357 formatter: function () { 358 return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) + 359 '<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>' 360 } 361 }, 362 legend: { 363 enabled: false 364 }, 365 plotOptions: { 366 series: { 367 marker: { 368 enabled: true, 369 symbol: 'circle', 370 radius: 3 371 }, 372 cursor: 'pointer', 373 }, 374 line: { 375 states: { 376 hover: { 377 lineWidth: 1 378 } 379 } 380 } 381 }, 382 credits: { 383 enabled: false 384 }, 385 series: [{ 386 data: chartopts['data'], 387 dashStyle: "shortdash", 388 color: "#cccccc", 389 lineWidth: 1 390 }] 391 }) 392 } 393 394 /* Renders a pie chart using the provided chartops */ 395 var renderPieChart = function (chartopts) { 396 return Highcharts.chart(chartopts['elemId'], { 397 chart: { 398 type: 'pie', 399 events: { 400 load: function () { 401 var chart = this, 402 rend = chart.renderer, 403 pie = chart.series[0], 404 left = chart.plotLeft + pie.center[0], 405 top = chart.plotTop + pie.center[1]; 406 this.innerText = rend.text(chartopts['data'][0].y, left, top). 407 attr({ 408 'text-anchor': 'middle', 409 'font-size': '24px', 410 'font-weight': 'bold', 411 'fill': chartopts['colors'][0], 412 'font-family': 'Helvetica,Arial,sans-serif' 413 }).add(); 414 }, 415 render: function () { 416 this.innerText.attr({ text: chartopts['data'][0].y }) 417 } 418 } 419 }, 420 title: { 421 text: chartopts['title'] 422 }, 423 plotOptions: { 424 pie: { 425 innerSize: '80%', 426 dataLabels: { 427 enabled: false 428 } 429 } 430 }, 431 credits: { 432 enabled: false 433 }, 434 tooltip: { 435 formatter: function () { 436 if (this.key == undefined) { 437 return false 438 } 439 return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '</b><br/>' 440 } 441 }, 442 series: [{ 443 data: chartopts['data'], 444 colors: chartopts['colors'], 445 }] 446 }) 447 } 448 449 /* poll - Queries the API and updates the UI with the results 450 * 451 * Updates: 452 * * Timeline Chart 453 * * Email (Donut) Chart 454 * * Map Bubbles 455 * * Datatables 456 */ 457 function poll() { 458 api.campaignId.results(campaign.id) 459 .success(function (c) { 460 campaign = c 461 /* Update the timeline */ 462 var timeline_series_data = [] 463 $.each(campaign.timeline, function (i, event) { 464 var event_date = moment.utc(event.time).local() 465 timeline_series_data.push({ 466 email: event.email, 467 x: event_date.valueOf(), 468 y: 1 469 }) 470 }) 471 var timeline_series_data = [] 472 $.each(campaign.timeline, function (i, event) { 473 var event_date = moment.utc(event.time).local() 474 timeline_series_data.push({ 475 email: event.email, 476 message: event.message, 477 x: event_date.valueOf(), 478 y: 1, 479 marker: { 480 fillColor: statuses[event.message].color 481 } 482 }) 483 }) 484 var timeline_chart = $("#timeline_chart").highcharts() 485 timeline_chart.series[0].update({ 486 data: timeline_series_data 487 }) 488 /* Update the results donut chart */ 489 var email_series_data = {} 490 // Load the initial data 491 Object.keys(statusMapping).forEach(function (k) { 492 email_series_data[k] = 0 493 }); 494 $.each(campaign.results, function (i, result) { 495 email_series_data[result.status]++; 496 // Backfill status values 497 var step = progressListing.indexOf(result.status) 498 for (var i = 0; i < step; i++) { 499 email_series_data[progressListing[i]]++ 500 } 501 }) 502 $.each(email_series_data, function (status, count) { 503 var email_data = [] 504 if (!(status in statusMapping)) { 505 return true 506 } 507 email_data.push({ 508 name: status, 509 y: count 510 }) 511 email_data.push({ 512 name: '', 513 y: campaign.results.length - count 514 }) 515 var chart = $("#" + statusMapping[status] + "_chart").highcharts() 516 chart.series[0].update({ 517 data: email_data 518 }) 519 }) 520 /* Update the datatable */ 521 resultsTable = $("#resultsTable").DataTable() 522 resultsTable.rows().every(function (i, tableLoop, rowLoop) { 523 var row = this.row(i) 524 var rowData = row.data() 525 var rid = rowData[0] 526 $.each(campaign.results, function (j, result) { 527 if (result.id == rid) { 528 var label = statuses[result.status].label || "label-default"; 529 rowData[6] = "<span class=\"label " + label + "\">" + result.status + "</span>" 530 resultsTable.row(i).data(rowData).draw(false) 531 if (row.child.isShown()) { 532 row.child(renderTimeline(row.data())) 533 } 534 return false 535 } 536 }) 537 }) 538 /* Update the map information */ 539 bubbles = [] 540 $.each(campaign.results, function (i, result) { 541 // Check that it wasn't an internal IP 542 if (result.latitude == 0 && result.longitude == 0) { 543 return true; 544 } 545 newIP = true 546 $.each(bubbles, function (i, bubble) { 547 if (bubble.ip == result.ip) { 548 bubbles[i].radius += 1 549 newIP = false 550 return false 551 } 552 }) 553 if (newIP) { 554 bubbles.push({ 555 latitude: result.latitude, 556 longitude: result.longitude, 557 name: result.ip, 558 fillKey: "point", 559 radius: 2 560 }) 561 } 562 }) 563 map.bubbles(bubbles) 564 $("#refresh_message").hide() 565 $("#refresh_btn").show() 566 }) 567 } 568 569 function load() { 570 campaign.id = window.location.pathname.split('/').slice(-1)[0] 571 api.campaignId.results(campaign.id) 572 .success(function (c) { 573 campaign = c 574 if (campaign) { 575 $("title").text(c.name + " - Gophish") 576 $("#loading").hide() 577 $("#campaignResults").show() 578 // Set the title 579 $("#page-title").text("Results for " + c.name) 580 if (c.status == "Completed") { 581 $('#complete_button')[0].disabled = true; 582 $('#complete_button').text('Completed!'); 583 doPoll = false; 584 } 585 // Setup tooltips 586 $('[data-toggle="tooltip"]').tooltip() 587 // Setup viewing the details of a result 588 $("#resultsTable").on("click", ".timeline-event-details", function () { 589 // Show the parameters 590 payloadResults = $(this).parent().find(".timeline-event-results") 591 if (payloadResults.is(":visible")) { 592 $(this).find("i").removeClass("fa-caret-down") 593 $(this).find("i").addClass("fa-caret-right") 594 payloadResults.hide() 595 } else { 596 $(this).find("i").removeClass("fa-caret-right") 597 $(this).find("i").addClass("fa-caret-down") 598 payloadResults.show() 599 } 600 }) 601 // Setup the results table 602 resultsTable = $("#resultsTable").DataTable({ 603 destroy: true, 604 "order": [ 605 [2, "asc"] 606 ], 607 columnDefs: [{ 608 orderable: false, 609 targets: "no-sort" 610 }, { 611 className: "details-control", 612 "targets": [1] 613 }, { 614 "visible": false, 615 "targets": [0] 616 }] 617 }); 618 resultsTable.clear(); 619 var email_series_data = {} 620 var timeline_series_data = [] 621 Object.keys(statusMapping).forEach(function (k) { 622 email_series_data[k] = 0 623 }); 624 $.each(campaign.results, function (i, result) { 625 label = statuses[result.status].label || "label-default"; 626 resultsTable.row.add([ 627 result.id, 628 "<i class=\"fa fa-caret-right\"></i>", 629 escapeHtml(result.first_name) || "", 630 escapeHtml(result.last_name) || "", 631 escapeHtml(result.email) || "", 632 escapeHtml(result.position) || "", 633 "<span class=\"label " + label + "\">" + result.status + "</span>" 634 ]).draw() 635 email_series_data[result.status]++; 636 // Backfill status values 637 var step = progressListing.indexOf(result.status) 638 for (var i = 0; i < step; i++) { 639 email_series_data[progressListing[i]]++ 640 } 641 }) 642 // Setup the individual timelines 643 $('#resultsTable tbody').on('click', 'td.details-control', function () { 644 var tr = $(this).closest('tr'); 645 var row = resultsTable.row(tr); 646 if (row.child.isShown()) { 647 // This row is already open - close it 648 row.child.hide(); 649 tr.removeClass('shown'); 650 $(this).find("i").removeClass("fa-caret-down") 651 $(this).find("i").addClass("fa-caret-right") 652 row.invalidate('dom').draw(false) 653 } else { 654 // Open this row 655 $(this).find("i").removeClass("fa-caret-right") 656 $(this).find("i").addClass("fa-caret-down") 657 row.child(renderTimeline(row.data())).show(); 658 tr.addClass('shown'); 659 row.invalidate('dom').draw(false) 660 } 661 }); 662 // Setup the graphs 663 $.each(campaign.timeline, function (i, event) { 664 var event_date = moment.utc(event.time).local() 665 timeline_series_data.push({ 666 email: event.email, 667 message: event.message, 668 x: event_date.valueOf(), 669 y: 1, 670 marker: { 671 fillColor: statuses[event.message].color 672 } 673 }) 674 }) 675 renderTimelineChart({ 676 data: timeline_series_data 677 }) 678 $.each(email_series_data, function (status, count) { 679 var email_data = [] 680 if (!(status in statusMapping)) { 681 return true 682 } 683 email_data.push({ 684 name: status, 685 y: count 686 }) 687 email_data.push({ 688 name: '', 689 y: campaign.results.length - count 690 }) 691 var chart = renderPieChart({ 692 elemId: statusMapping[status] + '_chart', 693 title: status, 694 name: status, 695 data: email_data, 696 colors: [statuses[status].color, '#dddddd'] 697 }) 698 }) 699 if (!map) { 700 map = new Datamap({ 701 element: document.getElementById("resultsMap"), 702 responsive: true, 703 fills: { 704 defaultFill: "#ffffff", 705 point: "#283F50" 706 }, 707 geographyConfig: { 708 highlightFillColor: "#1abc9c", 709 borderColor: "#283F50" 710 }, 711 bubblesConfig: { 712 borderColor: "#283F50" 713 } 714 }); 715 } 716 $.each(campaign.results, function (i, result) { 717 // Check that it wasn't an internal IP 718 if (result.latitude == 0 && result.longitude == 0) { 719 return true; 720 } 721 newIP = true 722 $.each(bubbles, function (i, bubble) { 723 if (bubble.ip == result.ip) { 724 bubbles[i].radius += 1 725 newIP = false 726 return false 727 } 728 }) 729 if (newIP) { 730 bubbles.push({ 731 latitude: result.latitude, 732 longitude: result.longitude, 733 name: result.ip, 734 fillKey: "point", 735 radius: 2 736 }) 737 } 738 }) 739 map.bubbles(bubbles) 740 } 741 // Load up the map data (only once!) 742 $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { 743 if ($(e.target).attr('href') == "#overview") { 744 if (!map) { 745 map = new Datamap({ 746 element: document.getElementById("resultsMap"), 747 responsive: true, 748 fills: { 749 defaultFill: "#ffffff" 750 }, 751 geographyConfig: { 752 highlightFillColor: "#1abc9c", 753 borderColor: "#283F50" 754 } 755 }); 756 } 757 } 758 }) 759 }) 760 .error(function () { 761 $("#loading").hide() 762 errorFlash(" Campaign not found!") 763 }) 764 } 765 766 var setRefresh 767 function refresh() { 768 if (!doPoll) { 769 return; 770 } 771 $("#refresh_message").show() 772 $("#refresh_btn").hide() 773 poll() 774 clearTimeout(setRefresh) 775 setRefresh = setTimeout(refresh, 60000) 776 }; 777 778 779 780 $(document).ready(function () { 781 Highcharts.setOptions({ 782 global: { 783 useUTC: false 784 } 785 }) 786 load(); 787 788 // Start the polling loop 789 setRefresh = setTimeout(refresh, 60000) 790 })