From c8abed489660a27f93fd0cbad3ee0a465db50fb9 Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Fri, 7 Aug 2020 20:51:25 +0100 Subject: [PATCH 1/5] Added initial functionality to allow arbitrary events --- controllers/phish.go | 27 + models/models.go | 39 +- models/result.go | 19 + static/js/src/app/campaign_results.js | 2053 +++++++++++++------------ 4 files changed, 1158 insertions(+), 980 deletions(-) diff --git a/controllers/phish.go b/controllers/phish.go index 68701dda..117562f3 100644 --- a/controllers/phish.go +++ b/controllers/phish.go @@ -112,6 +112,7 @@ func (ps *PhishingServer) registerRoutes() { router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer)) router.HandleFunc("/track", ps.TrackHandler) router.HandleFunc("/robots.txt", ps.RobotsHandler) + router.HandleFunc("/arbevent", ps.ArbitraryEventHandler) router.HandleFunc("/{path:.*}/track", ps.TrackHandler) router.HandleFunc("/{path:.*}/report", ps.ReportHandler) router.HandleFunc("/report", ps.ReportHandler) @@ -126,6 +127,32 @@ func (ps *PhishingServer) registerRoutes() { ps.server.Handler = phishHandler } +// ArbitraryEventHandler deals with arbitrary events - for example opening Word documents, secondary links, etc +func (ps *PhishingServer) ArbitraryEventHandler(w http.ResponseWriter, r *http.Request) { + + r, err := setupContext(r) + if err != nil { + // Log the error if it wasn't something we can safely ignore + if err != ErrInvalidRequest && err != ErrCampaignComplete { + log.Error(err) + } + http.NotFound(w, r) + return + } + + rs := ctx.Get(r, "result").(models.Result) + d := ctx.Get(r, "details").(models.EventDetails) + + err = rs.HandleArbitraryEvent(d) + if err != nil { + log.Error(err) + w.Write([]byte(err.Error())) + } else { + + w.Write([]byte("Event received")) + } +} + // TrackHandler tracks emails as they are opened, updating the status for the given Result func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) { r, err := setupContext(r) diff --git a/models/models.go b/models/models.go index 29a68151..ebca7d3d 100644 --- a/models/models.go +++ b/models/models.go @@ -40,25 +40,26 @@ const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD" const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN" const ( - CampaignInProgress string = "In progress" - CampaignQueued string = "Queued" - CampaignCreated string = "Created" - CampaignEmailsSent string = "Emails Sent" - CampaignComplete string = "Completed" - EventSent string = "Email Sent" - EventSendingError string = "Error Sending Email" - EventOpened string = "Email Opened" - EventClicked string = "Clicked Link" - EventDataSubmit string = "Submitted Data" - EventReported string = "Email Reported" - EventProxyRequest string = "Proxied request" - StatusSuccess string = "Success" - StatusQueued string = "Queued" - StatusSending string = "Sending" - StatusUnknown string = "Unknown" - StatusScheduled string = "Scheduled" - StatusRetry string = "Retrying" - Error string = "Error" + CampaignInProgress string = "In progress" + CampaignQueued string = "Queued" + CampaignCreated string = "Created" + CampaignEmailsSent string = "Emails Sent" + CampaignComplete string = "Completed" + EventSent string = "Email Sent" + EventSendingError string = "Error Sending Email" + EventOpened string = "Email Opened" + EventClicked string = "Clicked Link" + EventDataSubmit string = "Submitted Data" + EventReported string = "Email Reported" + EventArbitraryEvent string = "Arbitrary Event" + EventProxyRequest string = "Proxied request" + StatusSuccess string = "Success" + StatusQueued string = "Queued" + StatusSending string = "Sending" + StatusUnknown string = "Unknown" + StatusScheduled string = "Scheduled" + StatusRetry string = "Retrying" + Error string = "Error" ) // Flash is used to hold flash information for use in templates. diff --git a/models/result.go b/models/result.go index 6ad5812f..35305f34 100644 --- a/models/result.go +++ b/models/result.go @@ -3,6 +3,7 @@ package models import ( "crypto/rand" "encoding/json" + "errors" "math/big" "net" "time" @@ -135,6 +136,24 @@ func (r *Result) HandleFormSubmit(details EventDetails) error { return db.Save(r).Error } +// HandleArbitraryEvent updates a Result with an arbitrary event (e.g Word document opened, secondary link clicked) +func (r *Result) HandleArbitraryEvent(details EventDetails) error { + + EventTitle := details.Payload.Get("title") + + if EventTitle == "" { + return errors.New("No title supplied for arbitrary event") + } + + event, err := r.createEvent(EventArbitraryEvent, details) + if err != nil { + return err + } + r.Status = EventTitle + r.ModifiedDate = event.Time + return db.Save(r).Error +} + // HandleEmailReport updates a Result in the case where they report a simulated // phishing email using the HTTP handler. func (r *Result) HandleEmailReport(details EventDetails) error { diff --git a/static/js/src/app/campaign_results.js b/static/js/src/app/campaign_results.js index 767d53eb..14a2cefd 100644 --- a/static/js/src/app/campaign_results.js +++ b/static/js/src/app/campaign_results.js @@ -1,961 +1,1092 @@ -var map = null -var doPoll = true; - -// statuses is a helper map to point result statuses to ui classes -var statuses = { - "Email Sent": { - color: "#1abc9c", - label: "label-success", - icon: "fa-envelope", - point: "ct-point-sent" - }, - "Emails Sent": { - color: "#1abc9c", - label: "label-success", - icon: "fa-envelope", - point: "ct-point-sent" - }, - "In progress": { - label: "label-primary" - }, - "Queued": { - label: "label-info" - }, - "Completed": { - label: "label-success" - }, - "Email Opened": { - color: "#f9bf3b", - label: "label-warning", - icon: "fa-envelope-open", - point: "ct-point-opened" - }, - "Clicked Link": { - color: "#F39C12", - label: "label-clicked", - icon: "fa-mouse-pointer", - point: "ct-point-clicked" - }, - "Success": { - color: "#f05b4f", - label: "label-danger", - icon: "fa-exclamation", - point: "ct-point-clicked" - }, - //not a status, but is used for the campaign timeline and user timeline - "Email Reported": { - color: "#45d6ef", - label: "label-info", - icon: "fa-bullhorn", - point: "ct-point-reported" - }, - "Error": { - color: "#6c7a89", - label: "label-default", - icon: "fa-times", - point: "ct-point-error" - }, - "Error Sending Email": { - color: "#6c7a89", - label: "label-default", - icon: "fa-times", - point: "ct-point-error" - }, - "Submitted Data": { - color: "#f05b4f", - label: "label-danger", - icon: "fa-exclamation", - point: "ct-point-clicked" - }, - "Unknown": { - color: "#6c7a89", - label: "label-default", - icon: "fa-question", - point: "ct-point-error" - }, - "Sending": { - color: "#428bca", - label: "label-primary", - icon: "fa-spinner", - point: "ct-point-sending" - }, - "Retrying": { - color: "#6c7a89", - label: "label-default", - icon: "fa-clock-o", - point: "ct-point-error" - }, - "Scheduled": { - color: "#428bca", - label: "label-primary", - icon: "fa-clock-o", - point: "ct-point-sending" - }, - "Campaign Created": { - label: "label-success", - icon: "fa-rocket" - } -} - -var statusMapping = { - "Email Sent": "sent", - "Email Opened": "opened", - "Clicked Link": "clicked", - "Submitted Data": "submitted_data", - "Email Reported": "reported", -} - -// This is an underwhelming attempt at an enum -// until I have time to refactor this appropriately. -var progressListing = [ - "Email Sent", - "Email Opened", - "Clicked Link", - "Submitted Data" -] - -var campaign = {} -var bubbles = [] - -function dismiss() { - $("#modal\\.flashes").empty() - $("#modal").modal('hide') - $("#resultsTable").dataTable().DataTable().clear().draw() -} - -// Deletes a campaign after prompting the user -function deleteCampaign() { - Swal.fire({ - title: "Are you sure?", - text: "This will delete the campaign. This can't be undone!", - type: "warning", - animation: false, - showCancelButton: true, - confirmButtonText: "Delete Campaign", - confirmButtonColor: "#428bca", - reverseButtons: true, - allowOutsideClick: false, - showLoaderOnConfirm: true, - preConfirm: function () { - return new Promise(function (resolve, reject) { - api.campaignId.delete(campaign.id) - .success(function (msg) { - resolve() - }) - .error(function (data) { - reject(data.responseJSON.message) - }) - }) - } - }).then(function (result) { - if(result.value){ - Swal.fire( - 'Campaign Deleted!', - 'This campaign has been deleted!', - 'success' - ); - } - $('button:contains("OK")').on('click', function () { - location.href = '/campaigns' - }) - }) -} - -// Completes a campaign after prompting the user -function completeCampaign() { - Swal.fire({ - title: "Are you sure?", - text: "Gophish will stop processing events for this campaign", - type: "warning", - animation: false, - showCancelButton: true, - confirmButtonText: "Complete Campaign", - confirmButtonColor: "#428bca", - reverseButtons: true, - allowOutsideClick: false, - showLoaderOnConfirm: true, - preConfirm: function () { - return new Promise(function (resolve, reject) { - api.campaignId.complete(campaign.id) - .success(function (msg) { - resolve() - }) - .error(function (data) { - reject(data.responseJSON.message) - }) - }) - } - }).then(function (result) { - if (result.value){ - Swal.fire( - 'Campaign Completed!', - 'This campaign has been completed!', - 'success' - ); - $('#complete_button')[0].disabled = true; - $('#complete_button').text('Completed!') - doPoll = false; - } - }) -} - -// Exports campaign results as a CSV file -function exportAsCSV(scope) { - exportHTML = $("#exportButton").html() - var csvScope = null - var filename = campaign.name + ' - ' + capitalize(scope) + '.csv' - switch (scope) { - case "results": - csvScope = campaign.results - break; - case "events": - csvScope = campaign.timeline - break; - } - if (!csvScope) { - return - } - $("#exportButton").html('') - var csvString = Papa.unparse(csvScope, { - 'escapeFormulae': true - }) - var csvData = new Blob([csvString], { - type: 'text/csv;charset=utf-8;' - }); - if (navigator.msSaveBlob) { - navigator.msSaveBlob(csvData, filename); - } else { - var csvURL = window.URL.createObjectURL(csvData); - var dlLink = document.createElement('a'); - dlLink.href = csvURL; - dlLink.setAttribute('download', filename) - document.body.appendChild(dlLink) - dlLink.click(); - document.body.removeChild(dlLink) - } - $("#exportButton").html(exportHTML) -} - -function replay(event_idx) { - request = campaign.timeline[event_idx] - details = JSON.parse(request.details) - url = null - form = $('
').attr({ - method: 'POST', - target: '_blank', - }) - /* Create a form object and submit it */ - $.each(Object.keys(details.payload), function (i, param) { - if (param == "rid") { - return true; - } - if (param == "__original_url") { - url = details.payload[param]; - return true; - } - $('').attr({ - name: param, - }).val(details.payload[param]).appendTo(form); - }) - /* Ensure we know where to send the user */ - // Prompt for the URL - Swal.fire({ - title: 'Where do you want the credentials submitted to?', - input: 'text', - showCancelButton: true, - inputPlaceholder: "http://example.com/login", - inputValue: url || "", - inputValidator: function (value) { - return new Promise(function (resolve, reject) { - if (value) { - resolve(); - } else { - reject('Invalid URL.'); - } - }); - } - }).then(function (result) { - if (result.value){ - url = result.value - submitForm() - } - }) - return - submitForm() - - function submitForm() { - form.attr({ - action: url - }) - form.appendTo('body').submit().remove() - } -} - -/** - * Returns an HTML string that displays the OS and browser that clicked the link - * or submitted credentials. - * - * @param {object} event_details - The "details" parameter for a campaign - * timeline event - * - */ -var renderDevice = function (event_details) { - var ua = UAParser(details.browser['user-agent']) - var detailsString = '
' - - var deviceIcon = 'laptop' - if (ua.device.type) { - if (ua.device.type == 'tablet' || ua.device.type == 'mobile') { - deviceIcon = ua.device.type - } - } - - var deviceVendor = '' - if (ua.device.vendor) { - deviceVendor = ua.device.vendor.toLowerCase() - if (deviceVendor == 'microsoft') deviceVendor = 'windows' - } - - var deviceName = 'Unknown' - if (ua.os.name) { - deviceName = ua.os.name - if (deviceName == "Mac OS") { - deviceVendor = 'apple' - } else if (deviceName == "Windows") { - deviceVendor = 'windows' - } - if (ua.device.vendor && ua.device.model) { - deviceName = ua.device.vendor + ' ' + ua.device.model - } - } - - if (ua.os.version) { - deviceName = deviceName + ' (OS Version: ' + ua.os.version + ')' - } - - deviceString = '
' + - '' + - '' + - ' ' + escapeHtml(deviceName) + '
' - - detailsString += deviceString - - var deviceBrowser = 'Unknown' - var browserIcon = 'info-circle' - var browserVersion = '' - - if (ua.browser && ua.browser.name) { - deviceBrowser = ua.browser.name - // Handle the "mobile safari" case - deviceBrowser = deviceBrowser.replace('Mobile ', '') - if (deviceBrowser) { - browserIcon = deviceBrowser.toLowerCase() - if (browserIcon == 'ie') browserIcon = 'internet-explorer' - } - browserVersion = '(Version: ' + ua.browser.version + ')' - } - - var browserString = '
' + - ' ' + - deviceBrowser + ' ' + browserVersion + '
' - - detailsString += browserString - detailsString += '
' - return detailsString -} - -function renderTimeline(data) { - record = { - "id": data[0], - "first_name": data[2], - "last_name": data[3], - "email": data[4], - "position": data[5], - "status": data[6], - "reported": data[7], - "send_date": data[8] - } - results = '
' + - '
Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + - '
Email: ' + escapeHtml(record.email) + - '
Result ID: ' + escapeHtml(record.id) + '
' + - '
' - $.each(campaign.timeline, function (i, event) { - if (!event.email || event.email == record.email) { - // Add the event - results += '
' + - '
' - results += - '
' + - '
' + - '
' + escapeHtml(event.message) + - ' ' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '' - if (event.details) { - details = JSON.parse(event.details) - if (event.message == "Clicked Link" || event.message == "Submitted Data") { - deviceView = renderDevice(details) - if (deviceView) { - results += deviceView - } - } - if (event.message == "Submitted Data") { - results += '
' - results += '
View Details
' - } - if (details.payload) { - results += '
' - results += ' ' - results += ' ' - $.each(Object.keys(details.payload), function (i, param) { - if (param == "rid") { - return true; - } - results += ' ' - results += ' ' - results += ' ' - results += ' ' - }) - results += '
ParameterValue(s)
' + escapeHtml(param) + '' + escapeHtml(details.payload[param]) + '
' - results += '
' - } - if (details.error) { - results += '
View Details
' - results += '
' - results += 'Error ' + details.error - results += '
' - } - } - results += '
' - } - }) - // Add the scheduled send event at the bottom - if (record.status == "Scheduled" || record.status == "Retrying") { - results += '
' + - '
' - results += - '
' + - '
' + - '
' + "Scheduled to send at " + record.send_date + '' - } - results += '
' - return results -} - -var renderTimelineChart = function (chartopts) { - return Highcharts.chart('timeline_chart', { - chart: { - zoomType: 'x', - type: 'line', - height: "200px" - }, - title: { - text: 'Campaign Timeline' - }, - xAxis: { - type: 'datetime', - dateTimeLabelFormats: { - second: '%l:%M:%S', - minute: '%l:%M', - hour: '%l:%M', - day: '%b %d, %Y', - week: '%b %d, %Y', - month: '%b %Y' - } - }, - yAxis: { - min: 0, - max: 2, - visible: false, - tickInterval: 1, - labels: { - enabled: false - }, - title: { - text: "" - } - }, - tooltip: { - formatter: function () { - return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) + - '
Event: ' + this.point.message + '
Email: ' + this.point.email + '' - } - }, - legend: { - enabled: false - }, - plotOptions: { - series: { - marker: { - enabled: true, - symbol: 'circle', - radius: 3 - }, - cursor: 'pointer', - }, - line: { - states: { - hover: { - lineWidth: 1 - } - } - } - }, - credits: { - enabled: false - }, - series: [{ - data: chartopts['data'], - dashStyle: "shortdash", - color: "#cccccc", - lineWidth: 1, - turboThreshold: 0 - }] - }) -} - -/* Renders a pie chart using the provided chartops */ -var renderPieChart = function (chartopts) { - return Highcharts.chart(chartopts['elemId'], { - chart: { - type: 'pie', - events: { - load: function () { - var chart = this, - rend = chart.renderer, - pie = chart.series[0], - left = chart.plotLeft + pie.center[0], - top = chart.plotTop + pie.center[1]; - this.innerText = rend.text(chartopts['data'][0].count, left, top). - attr({ - 'text-anchor': 'middle', - 'font-size': '24px', - 'font-weight': 'bold', - 'fill': chartopts['colors'][0], - 'font-family': 'Helvetica,Arial,sans-serif' - }).add(); - }, - render: function () { - this.innerText.attr({ - text: chartopts['data'][0].count - }) - } - } - }, - title: { - text: chartopts['title'] - }, - plotOptions: { - pie: { - innerSize: '80%', - dataLabels: { - enabled: false - } - } - }, - credits: { - enabled: false - }, - tooltip: { - formatter: function () { - if (this.key == undefined) { - return false - } - return '\u25CF' + this.point.name + ': ' + this.y + '%
' - } - }, - series: [{ - data: chartopts['data'], - colors: chartopts['colors'], - }] - }) -} - -/* Updates the bubbles on the map - -@param {campaign.result[]} results - The campaign results to process -*/ -var updateMap = function (results) { - if (!map) { - return - } - bubbles = [] - $.each(campaign.results, function (i, result) { - // Check that it wasn't an internal IP - if (result.latitude == 0 && result.longitude == 0) { - return true; - } - newIP = true - $.each(bubbles, function (i, bubble) { - if (bubble.ip == result.ip) { - bubbles[i].radius += 1 - newIP = false - return false - } - }) - if (newIP) { - bubbles.push({ - latitude: result.latitude, - longitude: result.longitude, - name: result.ip, - fillKey: "point", - radius: 2 - }) - } - }) - map.bubbles(bubbles) -} - -/** - * Creates a status label for use in the results datatable - * @param {string} status - * @param {moment(datetime)} send_date - */ -function createStatusLabel(status, send_date) { - var label = statuses[status].label || "label-default"; - var statusColumn = "" + status + "" - // Add the tooltip if the email is scheduled to be sent - if (status == "Scheduled" || status == "Retrying") { - var sendDateMessage = "Scheduled to send at " + send_date - statusColumn = "" + status + "" - } - return statusColumn -} - -/* poll - Queries the API and updates the UI with the results - * - * Updates: - * * Timeline Chart - * * Email (Donut) Chart - * * Map Bubbles - * * Datatables - */ -function poll() { - api.campaignId.results(campaign.id) - .success(function (c) { - campaign = c - /* Update the timeline */ - var timeline_series_data = [] - $.each(campaign.timeline, function (i, event) { - var event_date = moment.utc(event.time).local() - timeline_series_data.push({ - email: event.email, - message: event.message, - x: event_date.valueOf(), - y: 1, - marker: { - fillColor: statuses[event.message].color - } - }) - }) - var timeline_chart = $("#timeline_chart").highcharts() - timeline_chart.series[0].update({ - data: timeline_series_data - }) - /* Update the results donut chart */ - var email_series_data = {} - // Load the initial data - Object.keys(statusMapping).forEach(function (k) { - email_series_data[k] = 0 - }); - $.each(campaign.results, function (i, result) { - email_series_data[result.status]++; - if (result.reported) { - email_series_data['Email Reported']++ - } - // Backfill status values - var step = progressListing.indexOf(result.status) - for (var i = 0; i < step; i++) { - email_series_data[progressListing[i]]++ - } - }) - $.each(email_series_data, function (status, count) { - var email_data = [] - if (!(status in statusMapping)) { - return true - } - email_data.push({ - name: status, - y: Math.floor((count / campaign.results.length) * 100), - count: count - }) - email_data.push({ - name: '', - y: 100 - Math.floor((count / campaign.results.length) * 100) - }) - var chart = $("#" + statusMapping[status] + "_chart").highcharts() - chart.series[0].update({ - data: email_data - }) - }) - - /* Update the datatable */ - resultsTable = $("#resultsTable").DataTable() - resultsTable.rows().every(function (i, tableLoop, rowLoop) { - var row = this.row(i) - var rowData = row.data() - var rid = rowData[0] - $.each(campaign.results, function (j, result) { - if (result.id == rid) { - rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') - rowData[7] = result.reported - rowData[6] = result.status - resultsTable.row(i).data(rowData) - if (row.child.isShown()) { - $(row.node()).find("#caret").removeClass("fa-caret-right") - $(row.node()).find("#caret").addClass("fa-caret-down") - row.child(renderTimeline(row.data())) - } - return false - } - }) - }) - resultsTable.draw(false) - /* Update the map information */ - updateMap(campaign.results) - $('[data-toggle="tooltip"]').tooltip() - $("#refresh_message").hide() - $("#refresh_btn").show() - }) -} - -function load() { - campaign.id = window.location.pathname.split('/').slice(-1)[0] - var use_map = JSON.parse(localStorage.getItem('gophish.use_map')) - api.campaignId.results(campaign.id) - .success(function (c) { - campaign = c - if (campaign) { - $("title").text(c.name + " - Gophish") - $("#loading").hide() - $("#campaignResults").show() - // Set the title - $("#page-title").text("Results for " + c.name) - if (c.status == "Completed") { - $('#complete_button')[0].disabled = true; - $('#complete_button').text('Completed!'); - doPoll = false; - } - // Setup viewing the details of a result - $("#resultsTable").on("click", ".timeline-event-details", function () { - // Show the parameters - payloadResults = $(this).parent().find(".timeline-event-results") - if (payloadResults.is(":visible")) { - $(this).find("i").removeClass("fa-caret-down") - $(this).find("i").addClass("fa-caret-right") - payloadResults.hide() - } else { - $(this).find("i").removeClass("fa-caret-right") - $(this).find("i").addClass("fa-caret-down") - payloadResults.show() - } - }) - // Setup the results table - resultsTable = $("#resultsTable").DataTable({ - destroy: true, - "order": [ - [2, "asc"] - ], - columnDefs: [{ - orderable: false, - targets: "no-sort" - }, { - className: "details-control", - "targets": [1] - }, { - "visible": false, - "targets": [0, 8] - }, - { - "render": function (data, type, row) { - return createStatusLabel(data, row[8]) - }, - "targets": [6] - }, - { - className: "text-center", - "render": function (reported, type, row) { - if (type == "display") { - if (reported) { - return "" - } - return "" - } - return reported - }, - "targets": [7] - } - ] - }); - resultsTable.clear(); - var email_series_data = {} - var timeline_series_data = [] - Object.keys(statusMapping).forEach(function (k) { - email_series_data[k] = 0 - }); - $.each(campaign.results, function (i, result) { - resultsTable.row.add([ - result.id, - "", - escapeHtml(result.first_name) || "", - escapeHtml(result.last_name) || "", - escapeHtml(result.email) || "", - escapeHtml(result.position) || "", - result.status, - result.reported, - moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') - ]) - email_series_data[result.status]++; - if (result.reported) { - email_series_data['Email Reported']++ - } - // Backfill status values - var step = progressListing.indexOf(result.status) - for (var i = 0; i < step; i++) { - email_series_data[progressListing[i]]++ - } - }) - resultsTable.draw(); - // Setup tooltips - $('[data-toggle="tooltip"]').tooltip() - // Setup the individual timelines - $('#resultsTable tbody').on('click', 'td.details-control', function () { - var tr = $(this).closest('tr'); - var row = resultsTable.row(tr); - if (row.child.isShown()) { - // This row is already open - close it - row.child.hide(); - tr.removeClass('shown'); - $(this).find("i").removeClass("fa-caret-down") - $(this).find("i").addClass("fa-caret-right") - } else { - // Open this row - $(this).find("i").removeClass("fa-caret-right") - $(this).find("i").addClass("fa-caret-down") - row.child(renderTimeline(row.data())).show(); - tr.addClass('shown'); - } - }); - // Setup the graphs - $.each(campaign.timeline, function (i, event) { - if (event.message == "Campaign Created") { - return true - } - var event_date = moment.utc(event.time).local() - timeline_series_data.push({ - email: event.email, - message: event.message, - x: event_date.valueOf(), - y: 1, - marker: { - fillColor: statuses[event.message].color - } - }) - }) - renderTimelineChart({ - data: timeline_series_data - }) - $.each(email_series_data, function (status, count) { - var email_data = [] - if (!(status in statusMapping)) { - return true - } - email_data.push({ - name: status, - y: Math.floor((count / campaign.results.length) * 100), - count: count - }) - email_data.push({ - name: '', - y: 100 - Math.floor((count / campaign.results.length) * 100) - }) - var chart = renderPieChart({ - elemId: statusMapping[status] + '_chart', - title: status, - name: status, - data: email_data, - colors: [statuses[status].color, '#dddddd'] - }) - }) - - if (use_map) { - $("#resultsMapContainer").show() - map = new Datamap({ - element: document.getElementById("resultsMap"), - responsive: true, - fills: { - defaultFill: "#ffffff", - point: "#283F50" - }, - geographyConfig: { - highlightFillColor: "#1abc9c", - borderColor: "#283F50" - }, - bubblesConfig: { - borderColor: "#283F50" - } - }); - } - updateMap(campaign.results) - } - }) - .error(function () { - $("#loading").hide() - errorFlash(" Campaign not found!") - }) -} - -var setRefresh - -function refresh() { - if (!doPoll) { - return; - } - $("#refresh_message").show() - $("#refresh_btn").hide() - poll() - clearTimeout(setRefresh) - setRefresh = setTimeout(refresh, 60000) -}; - -function report_mail(rid, cid) { - Swal.fire({ - title: "Are you sure?", - text: "This result will be flagged as reported (RID: " + rid + ")", - type: "question", - animation: false, - showCancelButton: true, - confirmButtonText: "Continue", - confirmButtonColor: "#428bca", - reverseButtons: true, - allowOutsideClick: false, - showLoaderOnConfirm: true - }).then(function (result) { - if (result.value){ - api.campaignId.get(cid).success((function(c) { - report_url = c.url; - report_url += report_url.endsWith("/") ? "" : "/"; - report_url += "report?rid=" + rid; - $.ajax({ - url: report_url, - method: "GET", - success: function(data) { - refresh(); - } - }); - })); - } - }) -} - -$(document).ready(function () { - Highcharts.setOptions({ - global: { - useUTC: false - } - }) - load(); - - // Start the polling loop - setRefresh = setTimeout(refresh, 60000) -}) +console.log("Running campaign_results.min.js") +var map = null +var doPoll = true; + +// statuses is a helper map to point result statuses to ui classes +var statuses = { + + "Email Sent": { + color: "#1abc9c", + label: "label-success", + icon: "fa-envelope", + point: "ct-point-sent" + }, + "Emails Sent": { + color: "#1abc9c", + label: "label-success", + icon: "fa-envelope", + point: "ct-point-sent" + }, + "In progress": { + label: "label-primary" + }, + "Queued": { + label: "label-info" + }, + "Completed": { + label: "label-success" + }, + "Email Opened": { + color: "#f9bf3b", + label: "label-warning", + icon: "fa-envelope-open", + point: "ct-point-opened" + }, + "Clicked Link": { + color: "#F39C12", + label: "label-clicked", + icon: "fa-mouse-pointer", + point: "ct-point-clicked" + }, + "Success": { + color: "#f05b4f", + label: "label-danger", + icon: "fa-exclamation", + point: "ct-point-clicked" + }, + //not a status, but is used for the campaign timeline and user timeline + "Email Reported": { + color: "#45d6ef", + label: "label-info", + icon: "fa-bullhorn", + point: "ct-point-reported" + }, + "Error": { + color: "#6c7a89", + label: "label-default", + icon: "fa-times", + point: "ct-point-error" + }, + "Error Sending Email": { + color: "#6c7a89", + label: "label-default", + icon: "fa-times", + point: "ct-point-error" + }, + "Submitted Data": { + color: "#f05b4f", + label: "label-danger", + icon: "fa-exclamation", + point: "ct-point-clicked" + }, + "Unknown": { + color: "#6c7a89", + label: "label-default", + icon: "fa-question", + point: "ct-point-error" + }, + "Sending": { + color: "#428bca", + label: "label-primary", + icon: "fa-spinner", + point: "ct-point-sending" + }, + "Retrying": { + color: "#6c7a89", + label: "label-default", + icon: "fa-clock-o", + point: "ct-point-error" + }, + "Scheduled": { + color: "#428bca", + label: "label-primary", + icon: "fa-clock-o", + point: "ct-point-sending" + }, + "Campaign Created": { + label: "label-success", + icon: "fa-rocket" + } +} + +var statusMapping = { + "Email Sent": "sent", + "Email Opened": "opened", + "Clicked Link": "clicked", + "Submitted Data": "submitted_data", + "Email Reported": "reported", +} + +// This is an underwhelming attempt at an enum +// until I have time to refactor this appropriately. +var progressListing = [ + "Email Sent", + "Email Opened", + "Clicked Link", + "Submitted Data" +] + +var campaign = {} +var bubbles = [] + +function dismiss() { + $("#modal\\.flashes").empty() + $("#modal").modal('hide') + $("#resultsTable").dataTable().DataTable().clear().draw() +} + +// Deletes a campaign after prompting the user +function deleteCampaign() { + Swal.fire({ + title: "Are you sure?", + text: "This will delete the campaign. This can't be undone!", + type: "warning", + animation: false, + showCancelButton: true, + confirmButtonText: "Delete Campaign", + confirmButtonColor: "#428bca", + reverseButtons: true, + allowOutsideClick: false, + showLoaderOnConfirm: true, + preConfirm: function () { + return new Promise(function (resolve, reject) { + api.campaignId.delete(campaign.id) + .success(function (msg) { + resolve() + }) + .error(function (data) { + reject(data.responseJSON.message) + }) + }) + } + }).then(function (result) { + if(result.value){ + Swal.fire( + 'Campaign Deleted!', + 'This campaign has been deleted!', + 'success' + ); + } + $('button:contains("OK")').on('click', function () { + location.href = '/campaigns' + }) + }) +} + +// Completes a campaign after prompting the user +function completeCampaign() { + Swal.fire({ + title: "Are you sure?", + text: "Gophish will stop processing events for this campaign", + type: "warning", + animation: false, + showCancelButton: true, + confirmButtonText: "Complete Campaign", + confirmButtonColor: "#428bca", + reverseButtons: true, + allowOutsideClick: false, + showLoaderOnConfirm: true, + preConfirm: function () { + return new Promise(function (resolve, reject) { + api.campaignId.complete(campaign.id) + .success(function (msg) { + resolve() + }) + .error(function (data) { + reject(data.responseJSON.message) + }) + }) + } + }).then(function (result) { + if (result.value){ + Swal.fire( + 'Campaign Completed!', + 'This campaign has been completed!', + 'success' + ); + $('#complete_button')[0].disabled = true; + $('#complete_button').text('Completed!') + doPoll = false; + } + }) +} + +// Exports campaign results as a CSV file +function exportAsCSV(scope) { + exportHTML = $("#exportButton").html() + var csvScope = null + var filename = campaign.name + ' - ' + capitalize(scope) + '.csv' + switch (scope) { + case "results": + csvScope = campaign.results + break; + case "events": + csvScope = campaign.timeline + break; + } + if (!csvScope) { + return + } + $("#exportButton").html('') + var csvString = Papa.unparse(csvScope, { + 'escapeFormulae': true + }) + var csvData = new Blob([csvString], { + type: 'text/csv;charset=utf-8;' + }); + if (navigator.msSaveBlob) { + navigator.msSaveBlob(csvData, filename); + } else { + var csvURL = window.URL.createObjectURL(csvData); + var dlLink = document.createElement('a'); + dlLink.href = csvURL; + dlLink.setAttribute('download', filename) + document.body.appendChild(dlLink) + dlLink.click(); + document.body.removeChild(dlLink) + } + $("#exportButton").html(exportHTML) +} + +function replay(event_idx) { + request = campaign.timeline[event_idx] + details = JSON.parse(request.details) + url = null + form = $('').attr({ + method: 'POST', + target: '_blank', + }) + /* Create a form object and submit it */ + $.each(Object.keys(details.payload), function (i, param) { + if (param == "rid") { + return true; + } + if (param == "__original_url") { + url = details.payload[param]; + return true; + } + $('').attr({ + name: param, + }).val(details.payload[param]).appendTo(form); + }) + /* Ensure we know where to send the user */ + // Prompt for the URL + Swal.fire({ + title: 'Where do you want the credentials submitted to?', + input: 'text', + showCancelButton: true, + inputPlaceholder: "http://example.com/login", + inputValue: url || "", + inputValidator: function (value) { + return new Promise(function (resolve, reject) { + if (value) { + resolve(); + } else { + reject('Invalid URL.'); + } + }); + } + }).then(function (result) { + if (result.value){ + url = result.value + submitForm() + } + }) + return + submitForm() + + function submitForm() { + form.attr({ + action: url + }) + form.appendTo('body').submit().remove() + } +} + +/** + * Returns an HTML string that displays the OS and browser that clicked the link + * or submitted credentials. + * + * @param {object} event_details - The "details" parameter for a campaign + * timeline event + * + */ +var renderDevice = function (event_details) { + var ua = UAParser(details.browser['user-agent']) + var detailsString = '
' + + var deviceIcon = 'laptop' + if (ua.device.type) { + if (ua.device.type == 'tablet' || ua.device.type == 'mobile') { + deviceIcon = ua.device.type + } + } + + var deviceVendor = '' + if (ua.device.vendor) { + deviceVendor = ua.device.vendor.toLowerCase() + if (deviceVendor == 'microsoft') deviceVendor = 'windows' + } + + var deviceName = 'Unknown' + if (ua.os.name) { + deviceName = ua.os.name + if (deviceName == "Mac OS") { + deviceVendor = 'apple' + } else if (deviceName == "Windows") { + deviceVendor = 'windows' + } + if (ua.device.vendor && ua.device.model) { + deviceName = ua.device.vendor + ' ' + ua.device.model + } + } + + if (ua.os.version) { + deviceName = deviceName + ' (OS Version: ' + ua.os.version + ')' + } + + deviceString = '
' + + '' + + '' + + ' ' + escapeHtml(deviceName) + '
' + + detailsString += deviceString + + var deviceBrowser = 'Unknown' + var browserIcon = 'info-circle' + var browserVersion = '' + + + if (ua.browser && ua.browser.name) { + deviceBrowser = ua.browser.name + // Handle the "mobile safari" case + deviceBrowser = deviceBrowser.replace('Mobile ', '') + if (deviceBrowser) { + browserIcon = deviceBrowser.toLowerCase() + if (browserIcon == 'ie') browserIcon = 'internet-explorer' + } + browserVersion = '(Version: ' + ua.browser.version + ')' + } + + var browserString = '
' + + ' ' + + deviceBrowser + ' ' + browserVersion + '
' + + detailsString += browserString + detailsString += '
' + return detailsString +} + +function renderTimeline(data) { + + record = { + "id": data[0], + "first_name": data[2], + "last_name": data[3], + "email": data[4], + "position": data[5], + "status": data[6], + "reported": data[7], + "send_date": data[8] + } + results = '
' + + '
Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) + + '
Email: ' + escapeHtml(record.email) + + '
Result ID: ' + escapeHtml(record.id) + '
' + + '
' + $.each(campaign.timeline, function (i, event) { + if (!event.email || event.email == record.email) { + // Add the event + + // Handle arbitrary event as a special case + // We could collapse the first half into the regular code, but for now it feels neater to keep it separate and live with the code re-use (*waves @ jordan*) + if (event.message == "Arbitrary Event"){ + + if (event.details) { // Should always be data, otherwise we can ignore the event + details = JSON.parse(event.details) + message = details.payload.title + + results += '
' + + '
' + results += + '
' + + '
' + + '
' + escapeHtml(message) + // This is the case that makes code reuse tricky, as we want the title of the arbitrary event from the payload. TODO Give some more thought. Perhaps we scrap 'Arbitrary Event' and put the title in the message, and have some other indicator of the requirement to uniquely parse the contents of 'payload' e.g. payload['ae'] = 1 + ' ' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '' + + // Check if user agent present && requested to display it + if ("browser" in details && details.payload.ua == 1){ + deviceView = renderDevice(details) + if (deviceView) { + results += deviceView + } + } + + //Iterate over sub-items + if ("sub_text" in details.payload) { + results += '
' + details.payload.sub_text.forEach(function (text, index) { + results += "
" + // Check if there's an associated icon + if ("sub_icon" in details.payload && details.payload.sub_icon.length >= index){ + results = results + ' ' //+ text + } + results += text + results += "
" + }) + results += '
' + } + results += '
' + } // End arbitrary event processsing + + + } else { // else, if regular event + + + results += '
' + + '
' + results += + '
' + + '
' + + '
' + escapeHtml(event.message) + + ' ' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '' + if (event.details) { + details = JSON.parse(event.details) + + if (event.message == "Clicked Link" || event.message == "Submitted Data" || event.message == "Email Opened") { + deviceView = renderDevice(details) + if (deviceView) { + results += deviceView + } + } + if (event.message == "Submitted Data") { + results += '
' + results += '
View Details
' + } + if (details.payload) { + results += '
' + results += ' ' + results += ' ' + $.each(Object.keys(details.payload), function (i, param) { + if (param == "rid") { + return true; + } + results += ' ' + results += ' ' + results += ' ' + results += ' ' + }) + results += '
ParameterValue(s)
' + escapeHtml(param) + '' + escapeHtml(details.payload[param]) + '
' + results += '
' + } + if (details.error) { + results += '
View Details
' + results += '
' + results += 'Error ' + details.error + results += '
' + } + } + results += '
' + } + + } + }) + // Add the scheduled send event at the bottom + if (record.status == "Scheduled" || record.status == "Retrying") { + results += '
' + + '
' + results += + '
' + + '
' + + '
' + "Scheduled to send at " + record.send_date + '' + } + results += '
' + return results +} + +var renderTimelineChart = function (chartopts) { + return Highcharts.chart('timeline_chart', { + chart: { + zoomType: 'x', + type: 'line', + height: "200px" + }, + title: { + text: 'Campaign Timeline' + }, + xAxis: { + type: 'datetime', + dateTimeLabelFormats: { + second: '%l:%M:%S', + minute: '%l:%M', + hour: '%l:%M', + day: '%b %d, %Y', + week: '%b %d, %Y', + month: '%b %Y' + } + }, + yAxis: { + min: 0, + max: 2, + visible: false, + tickInterval: 1, + labels: { + enabled: false + }, + title: { + text: "" + } + }, + tooltip: { + formatter: function () { + return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) + + '
Event: ' + this.point.message + '
Email: ' + this.point.email + '' + } + }, + legend: { + enabled: false + }, + plotOptions: { + series: { + marker: { + enabled: true, + symbol: 'circle', + radius: 3 + }, + cursor: 'pointer', + }, + line: { + states: { + hover: { + lineWidth: 1 + } + } + } + }, + credits: { + enabled: false + }, + series: [{ + data: chartopts['data'], + dashStyle: "shortdash", + color: "#cccccc", + lineWidth: 1, + turboThreshold: 0 + }] + }) +} + +/* Renders a pie chart using the provided chartops */ +var renderPieChart = function (chartopts) { + return Highcharts.chart(chartopts['elemId'], { + chart: { + type: 'pie', + events: { + load: function () { + var chart = this, + rend = chart.renderer, + pie = chart.series[0], + left = chart.plotLeft + pie.center[0], + top = chart.plotTop + pie.center[1]; + this.innerText = rend.text(chartopts['data'][0].count, left, top). + attr({ + 'text-anchor': 'middle', + 'font-size': '24px', + 'font-weight': 'bold', + 'fill': chartopts['colors'][0], + 'font-family': 'Helvetica,Arial,sans-serif' + }).add(); + }, + render: function () { + this.innerText.attr({ + text: chartopts['data'][0].count + }) + } + } + }, + title: { + text: chartopts['title'] + }, + plotOptions: { + pie: { + innerSize: '80%', + dataLabels: { + enabled: false + } + } + }, + credits: { + enabled: false + }, + tooltip: { + formatter: function () { + if (this.key == undefined) { + return false + } + return '\u25CF' + this.point.name + ': ' + this.y + '%
' + } + }, + series: [{ + data: chartopts['data'], + colors: chartopts['colors'], + }] + }) +} + +/* Updates the bubbles on the map + +@param {campaign.result[]} results - The campaign results to process +*/ +var updateMap = function (results) { + if (!map) { + return + } + bubbles = [] + $.each(campaign.results, function (i, result) { + // Check that it wasn't an internal IP + if (result.latitude == 0 && result.longitude == 0) { + return true; + } + newIP = true + $.each(bubbles, function (i, bubble) { + if (bubble.ip == result.ip) { + bubbles[i].radius += 1 + newIP = false + return false + } + }) + if (newIP) { + bubbles.push({ + latitude: result.latitude, + longitude: result.longitude, + name: result.ip, + fillKey: "point", + radius: 2 + }) + } + }) + map.bubbles(bubbles) +} + +/** + * Creates a status label for use in the results datatable + * @param {string} status + * @param {moment(datetime)} send_date + */ +function createStatusLabel(status, send_date) { + + if (status in statuses){ + var label = statuses[status].label || "label-default"; + } else { + var label = "label-default" + } + + var statusColumn = "" + status + "" + // Add the tooltip if the email is scheduled to be sent + if (status == "Scheduled" || status == "Retrying") { + var sendDateMessage = "Scheduled to send at " + send_date + statusColumn = "" + status + "" + } + return statusColumn +} + +/* poll - Queries the API and updates the UI with the results + * + * Updates: + * * Timeline Chart + * * Email (Donut) Chart + * * Map Bubbles + * * Datatables + */ +function poll() { + api.campaignId.results(campaign.id) + .success(function (c) { + campaign = c + /* Update the timeline */ + var timeline_series_data = [] + $.each(campaign.timeline, function (i, event) { + + // Handle arbitary event + if (event.message == "Arbitrary Event") { + details = JSON.parse(event.details) + message = details.payload.title + + } else { + message = event.message + //color = statuses[event.message].color + } + + var event_date = moment.utc(event.time).local() + timeline_series_data.push({ + email: event.email, + message: message, //event.message, + x: event_date.valueOf(), + y: 1, + marker: { + fillColor: statuses[message].color //statuses[event.message].color + } + }) + }) + var timeline_chart = $("#timeline_chart").highcharts() + timeline_chart.series[0].update({ + data: timeline_series_data + }) + /* Update the results donut chart */ + var email_series_data = {} + // Load the initial data + Object.keys(statusMapping).forEach(function (k) { + email_series_data[k] = 0 + }); + $.each(campaign.results, function (i, result) { + email_series_data[result.status]++; + if (result.reported) { + email_series_data['Email Reported']++ + } + // Backfill status values + var step = progressListing.indexOf(result.status) + for (var i = 0; i < step; i++) { + email_series_data[progressListing[i]]++ + } + }) + $.each(email_series_data, function (status, count) { + var email_data = [] + if (!(status in statusMapping)) { + return true + } + email_data.push({ + name: status, + y: Math.floor((count / campaign.results.length) * 100), + count: count + }) + email_data.push({ + name: '', + y: 100 - Math.floor((count / campaign.results.length) * 100) + }) + var chart = $("#" + statusMapping[status] + "_chart").highcharts() + chart.series[0].update({ + data: email_data + }) + }) + + /* Update the datatable */ + resultsTable = $("#resultsTable").DataTable() + resultsTable.rows().every(function (i, tableLoop, rowLoop) { + var row = this.row(i) + var rowData = row.data() + var rid = rowData[0] + $.each(campaign.results, function (j, result) { + if (result.id == rid) { + rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') + rowData[7] = result.reported + rowData[6] = result.status + + resultsTable.row(i).data(rowData) + if (row.child.isShown()) { + $(row.node()).find("#caret").removeClass("fa-caret-right") + $(row.node()).find("#caret").addClass("fa-caret-down") + row.child(renderTimeline(row.data())) + } + return false + } + }) + }) + resultsTable.draw(false) + /* Update the map information */ + updateMap(campaign.results) + $('[data-toggle="tooltip"]').tooltip() + $("#refresh_message").hide() + $("#refresh_btn").show() + }) +} + +function load() { + + campaign.id = window.location.pathname.split('/').slice(-1)[0] + var use_map = JSON.parse(localStorage.getItem('gophish.use_map')) + api.campaignId.results(campaign.id) + .success(function (c) { + campaign = c + if (campaign) { + + // We add arbitrary events to the statuses dict and arbitrary event names the progressListing array. + tmpTitles = {} + campaign.timeline.forEach(function(event) { + if (event.message == "Arbitrary Event") { + details = JSON.parse(event.details) // TODO Validate this exists + + title = "Arbitrary Event" + if ("title" in details.payload){ + title = String(details.payload.title) + + } + statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to + + + statuses[title]["color"] = "#00FFFF" // Default + if ("color" in details.payload ){ + color = String(details.payload.color) + if (!(/^#[0-9A-F]{6}$/i.test(color))) { + color = "#00FFFF" // Default to Cyan if the color is invalid + } + statuses[title]["color"] = color + } + + statuses[title]["icon"] = "fa fa-info" // Default + if ("icon" in details.payload ){ + icon = String(details.payload.icon) + statuses[title]["icon"] = icon + } + + statuses[title]["label"] = "label-info" // Default + if ("label" in details.payload ){ + label = String(details.payload.label) + statuses[title]["label"] = label + } + + // Store unique event titles to be added to the progressListing once we exit the loop + tmpTitles[title] = 1 + } + }) + + // We add the arbitary event titles to the progress listing + // Small problem here is that the ordering will assume anything appended is more serious than 'data submitted' + progressListing = progressListing.concat(Object.keys(tmpTitles)) + + + $("title").text(c.name + " - Gophish") + $("#loading").hide() + $("#campaignResults").show() + // Set the title + $("#page-title").text("Results for " + c.name) + if (c.status == "Completed") { + $('#complete_button')[0].disabled = true; + $('#complete_button').text('Completed!'); + doPoll = false; + } + // Setup viewing the details of a result + $("#resultsTable").on("click", ".timeline-event-details", function () { + // Show the parameters + payloadResults = $(this).parent().find(".timeline-event-results") + if (payloadResults.is(":visible")) { + $(this).find("i").removeClass("fa-caret-down") + $(this).find("i").addClass("fa-caret-right") + payloadResults.hide() + } else { + $(this).find("i").removeClass("fa-caret-right") + $(this).find("i").addClass("fa-caret-down") + payloadResults.show() + } + }) + // Setup the results table + resultsTable = $("#resultsTable").DataTable({ + destroy: true, + "order": [ + [2, "asc"] + ], + columnDefs: [{ + orderable: false, + targets: "no-sort" + }, { + className: "details-control", + "targets": [1] + }, { + "visible": false, + "targets": [0, 8] + }, + { + "render": function (data, type, row) { + return createStatusLabel(data, row[8]) + }, + "targets": [6] + }, + { + className: "text-center", + "render": function (reported, type, row) { + if (type == "display") { + if (reported) { + return "" + } + return "" + } + return reported + }, + "targets": [7] + } + ] + }); + resultsTable.clear(); + var email_series_data = {} + var timeline_series_data = [] + Object.keys(statusMapping).forEach(function (k) { + email_series_data[k] = 0 + }); + $.each(campaign.results, function (i, result) { + + resultsTable.row.add([ + result.id, + "", + escapeHtml(result.first_name) || "", + escapeHtml(result.last_name) || "", + escapeHtml(result.email) || "", + escapeHtml(result.position) || "", + result.status, + result.reported, + moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') + ]) + email_series_data[result.status]++; + if (result.reported) { + email_series_data['Email Reported']++ + } + // Backfill status values + var step = progressListing.indexOf(result.status) + for (var i = 0; i < step; i++) { + email_series_data[progressListing[i]]++ + } + }) + resultsTable.draw(); + // Setup tooltips + $('[data-toggle="tooltip"]').tooltip() + // Setup the individual timelines + $('#resultsTable tbody').on('click', 'td.details-control', function () { + var tr = $(this).closest('tr'); + var row = resultsTable.row(tr); + if (row.child.isShown()) { + // This row is already open - close it + row.child.hide(); + tr.removeClass('shown'); + $(this).find("i").removeClass("fa-caret-down") + $(this).find("i").addClass("fa-caret-right") + } else { + // Open this row + $(this).find("i").removeClass("fa-caret-right") + $(this).find("i").addClass("fa-caret-down") + row.child(renderTimeline(row.data())).show(); + tr.addClass('shown'); + } + }); + // Setup the graphs + $.each(campaign.timeline, function (i, event) { + if (event.message == "Campaign Created") { + return true + } + var event_date = moment.utc(event.time).local() + + // Handle arbitary event + if (event.message == "Arbitrary Event") { + details = JSON.parse(event.details) + message = details.payload.title + } else { + message = event.message + } + + timeline_series_data.push({ + email: event.email, + message: message, //event.message, + x: event_date.valueOf(), + y: 1, + marker: { + fillColor: statuses[message].color //statuses[event.message].color + } + }) + + }) + renderTimelineChart({ + data: timeline_series_data + }) + $.each(email_series_data, function (status, count) { + var email_data = [] + if (!(status in statusMapping)) { + return true + } + email_data.push({ + name: status, + y: Math.floor((count / campaign.results.length) * 100), + count: count + }) + email_data.push({ + name: '', + y: 100 - Math.floor((count / campaign.results.length) * 100) + }) + + var chart = renderPieChart({ + elemId: statusMapping[status] + '_chart', + title: status, + name: status, + data: email_data, + colors: [statuses[status].color, '#dddddd'] + }) + + }) + + if (use_map) { + $("#resultsMapContainer").show() + map = new Datamap({ + element: document.getElementById("resultsMap"), + responsive: true, + fills: { + defaultFill: "#ffffff", + point: "#283F50" + }, + geographyConfig: { + highlightFillColor: "#1abc9c", + borderColor: "#283F50" + }, + bubblesConfig: { + borderColor: "#283F50" + } + }); + } + updateMap(campaign.results) + } + }) + .error(function () { + $("#loading").hide() + errorFlash(" Campaign not found!") + }) +} + +var setRefresh + +function refresh() { + if (!doPoll) { + return; + } + $("#refresh_message").show() + $("#refresh_btn").hide() + poll() + clearTimeout(setRefresh) + setRefresh = setTimeout(refresh, 60000) +}; + +function report_mail(rid, cid) { + Swal.fire({ + title: "Are you sure?", + text: "This result will be flagged as reported (RID: " + rid + ")", + type: "question", + animation: false, + showCancelButton: true, + confirmButtonText: "Continue", + confirmButtonColor: "#428bca", + reverseButtons: true, + allowOutsideClick: false, + showLoaderOnConfirm: true + }).then(function (result) { + if (result.value){ + api.campaignId.get(cid).success((function(c) { + report_url = c.url; + report_url += report_url.endsWith("/") ? "" : "/"; + report_url += "report?rid=" + rid; + $.ajax({ + url: report_url, + method: "GET", + success: function(data) { + refresh(); + } + }); + })); + } + }) +} + +$(document).ready(function () { + Highcharts.setOptions({ + global: { + useUTC: false + } + }) + load(); + + // Start the polling loop + setRefresh = setTimeout(refresh, 60000) +}) \ No newline at end of file From f212c13375f8e27cefafa8aae4423fc34197e38f Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Fri, 7 Aug 2020 22:27:02 +0100 Subject: [PATCH 2/5] Fixed refresh bug whereby new arbitary event properties weren't being imported --- static/js/src/app/campaign_results.js | 106 +++++++++++++++----------- 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/static/js/src/app/campaign_results.js b/static/js/src/app/campaign_results.js index 14a2cefd..6a62fde6 100644 --- a/static/js/src/app/campaign_results.js +++ b/static/js/src/app/campaign_results.js @@ -1,4 +1,3 @@ -console.log("Running campaign_results.min.js") var map = null var doPoll = true; @@ -693,6 +692,9 @@ function poll() { api.campaignId.results(campaign.id) .success(function (c) { campaign = c + + updateArbitraryEventData(campaign) // Update data structures with new arbitrary event specifications + /* Update the timeline */ var timeline_series_data = [] $.each(campaign.timeline, function (i, event) { @@ -799,50 +801,7 @@ function load() { campaign = c if (campaign) { - // We add arbitrary events to the statuses dict and arbitrary event names the progressListing array. - tmpTitles = {} - campaign.timeline.forEach(function(event) { - if (event.message == "Arbitrary Event") { - details = JSON.parse(event.details) // TODO Validate this exists - - title = "Arbitrary Event" - if ("title" in details.payload){ - title = String(details.payload.title) - - } - statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to - - - statuses[title]["color"] = "#00FFFF" // Default - if ("color" in details.payload ){ - color = String(details.payload.color) - if (!(/^#[0-9A-F]{6}$/i.test(color))) { - color = "#00FFFF" // Default to Cyan if the color is invalid - } - statuses[title]["color"] = color - } - - statuses[title]["icon"] = "fa fa-info" // Default - if ("icon" in details.payload ){ - icon = String(details.payload.icon) - statuses[title]["icon"] = icon - } - - statuses[title]["label"] = "label-info" // Default - if ("label" in details.payload ){ - label = String(details.payload.label) - statuses[title]["label"] = label - } - - // Store unique event titles to be added to the progressListing once we exit the loop - tmpTitles[title] = 1 - } - }) - - // We add the arbitary event titles to the progress listing - // Small problem here is that the ordering will assume anything appended is more serious than 'data submitted' - progressListing = progressListing.concat(Object.keys(tmpTitles)) - + updateArbitraryEventData(campaign) // Update data structures with new arbitrary event specifications $("title").text(c.name + " - Gophish") $("#loading").hide() @@ -1079,6 +1038,63 @@ function report_mail(rid, cid) { }) } + +/* updateArbitraryData will go through the supplied campaign and add arbitrary event data to three data structure: + statuses + statusMapping //TODO + progressListing +*/ +function updateArbitraryEventData(campaign){ + + // We add arbitrary events to the statuses dict and arbitrary event names the progressListing array. + campaign.timeline.forEach(function(event) { + if (event.message == "Arbitrary Event") { + details = JSON.parse(event.details) // TODO Validate this exists + + title = "Arbitrary Event" + if ("title" in details.payload){ + title = String(details.payload.title) + + } + statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to + + + statuses[title]["color"] = "#00FFFF" // Default + if ("color" in details.payload ){ + color = String(details.payload.color) + if (!(/^#[0-9A-F]{6}$/i.test(color))) { + color = "#00FFFF" // Default to Cyan if the color is invalid + } + statuses[title]["color"] = color + } + + statuses[title]["icon"] = "fa fa-info" // Default + if ("icon" in details.payload ){ + icon = String(details.payload.icon) + statuses[title]["icon"] = icon + } + + statuses[title]["label"] = "label-info" // Default + if ("label" in details.payload ){ + label = String(details.payload.label) + statuses[title]["label"] = label + } + + // Add the title to the progressListing array (if it's not already in there) + if (!progressListing.includes(title)) { + progressListing.push(title) + } + + + } + }) + + // We add the arbitary event titles to the progress listing + // Small problem here is that the ordering will assume anything appended is more serious than 'data submitted' + //progressListing = progressListing.concat(Object.keys(tmpTitles)) + +} + $(document).ready(function () { Highcharts.setOptions({ global: { From f5509a56c28b834f07ea61d10dee816aa0f34d94 Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Sat, 8 Aug 2020 20:34:38 +0100 Subject: [PATCH 3/5] Added support for pie charts for arbitrary events --- static/js/src/app/campaign_results.js | 164 ++++++++++++++++++++++---- templates/campaign_results.html | 6 + 2 files changed, 144 insertions(+), 26 deletions(-) diff --git a/static/js/src/app/campaign_results.js b/static/js/src/app/campaign_results.js index 6a62fde6..b13345ef 100644 --- a/static/js/src/app/campaign_results.js +++ b/static/js/src/app/campaign_results.js @@ -1,5 +1,7 @@ var map = null var doPoll = true; +var arbEventsPieCharts = true; // Include pie charts for arbitary events or not + // Setting to true will add the arb events to statusMapping as wel as adding HTML chart elements. // statuses is a helper map to point result statuses to ui classes var statuses = { @@ -104,6 +106,7 @@ var statusMapping = { "Clicked Link": "clicked", "Submitted Data": "submitted_data", "Email Reported": "reported", + //"Opened Word Document" : "opened_word_document" } // This is an underwhelming attempt at an enum @@ -693,7 +696,7 @@ function poll() { .success(function (c) { campaign = c - updateArbitraryEventData(campaign) // Update data structures with new arbitrary event specifications + updateArbitraryEventData(campaign, false) // Update data structures with new arbitrary event specifications /* Update the timeline */ var timeline_series_data = [] @@ -725,13 +728,20 @@ function poll() { data: timeline_series_data }) /* Update the results donut chart */ - var email_series_data = {} + //var email_series_data = {} // Load the initial data - Object.keys(statusMapping).forEach(function (k) { - email_series_data[k] = 0 - }); + //Object.keys(statusMapping).forEach(function (k) { + // email_series_data[k] = 0 + //}); + + /* $.each(campaign.results, function (i, result) { - email_series_data[result.status]++; + + // Don't count arbitrary events, we do this independently to avoid backfill logic. + if (progressListing.includes(result.status)) { + email_series_data[result.status]++; + } + if (result.reported) { email_series_data['Email Reported']++ } @@ -740,7 +750,12 @@ function poll() { for (var i = 0; i < step; i++) { email_series_data[progressListing[i]]++ } - }) + })*/ + + // New function for counting events. Doesn't handle backfill, yet. + email_series_data = countCampaignEvents(campaign) + + $.each(email_series_data, function (status, count) { var email_data = [] if (!(status in statusMapping)) { @@ -801,7 +816,7 @@ function load() { campaign = c if (campaign) { - updateArbitraryEventData(campaign) // Update data structures with new arbitrary event specifications + updateArbitraryEventData(campaign, true) // Update data structures with new arbitrary event specifications $("title").text(c.name + " - Gophish") $("#loading").hide() @@ -865,11 +880,12 @@ function load() { ] }); resultsTable.clear(); - var email_series_data = {} + //var email_series_data = {} var timeline_series_data = [] - Object.keys(statusMapping).forEach(function (k) { - email_series_data[k] = 0 - }); + //Object.keys(statusMapping).forEach(function (k) { + // email_series_data[k] = 0 + //}); + $.each(campaign.results, function (i, result) { resultsTable.row.add([ @@ -883,16 +899,34 @@ function load() { result.reported, moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a') ]) - email_series_data[result.status]++; + + + /* + // Don't count arbitrary events, we do this independently to avoid backfill logic. + if (progressListing.includes(result.status)) { + email_series_data[result.status]++; + } + if (result.reported) { email_series_data['Email Reported']++ } + + //TODO: At some point need to figure out backfilling with arbitrary events + // Possibly just backfill Email sent and Email Opened before getting into more complex + // data structures + // Backfill status values var step = progressListing.indexOf(result.status) for (var i = 0; i < step; i++) { email_series_data[progressListing[i]]++ } + */ + }) + + // New function for counting events. Doesn't handle backfill, yet. + email_series_data = countCampaignEvents(campaign) + resultsTable.draw(); // Setup tooltips $('[data-toggle="tooltip"]').tooltip() @@ -1041,16 +1075,27 @@ function report_mail(rid, cid) { /* updateArbitraryData will go through the supplied campaign and add arbitrary event data to three data structure: statuses - statusMapping //TODO - progressListing -*/ -function updateArbitraryEventData(campaign){ + statusMapping + progressListing // Todo, needs more consideration on backfill - // We add arbitrary events to the statuses dict and arbitrary event names the progressListing array. - campaign.timeline.forEach(function(event) { + The createPies boolean is used to allow us to create the pies on load() but not re-create them from calling poll(), as the + highchart info gets overwritten. The problem with this is that if a new arbitrary event comes in while the page is loaded + the poll() won't add the pie. Need to investigate this. TODO + +*/ +function updateArbitraryEventData(campaign, createPies){ + + + var arbEventNames = [] // Hold unique arb event names. Used to create HTML pie charts if arbEventPieCharts set to true + + campaign.timeline.forEach(function(event) { // Step over each event + + if (event.message == "Arbitrary Event") { + details = JSON.parse(event.details) // TODO Validate this exists + // 1. Add title, color, icon, and label properties to statuses dict title = "Arbitrary Event" if ("title" in details.payload){ title = String(details.payload.title) @@ -1058,7 +1103,6 @@ function updateArbitraryEventData(campaign){ } statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to - statuses[title]["color"] = "#00FFFF" // Default if ("color" in details.payload ){ color = String(details.payload.color) @@ -1080,19 +1124,87 @@ function updateArbitraryEventData(campaign){ statuses[title]["label"] = label } - // Add the title to the progressListing array (if it's not already in there) - if (!progressListing.includes(title)) { - progressListing.push(title) + if (!arbEventNames.includes(title)){ + arbEventNames.push(title) } + /* How to handle progressListing needs more thought, and probably */ + // Add the title to the progressListing array (if it's not already in there) + //if (!progressListing.includes(title)) { + // progressListing.push(title) + //} + } }) - // We add the arbitary event titles to the progress listing - // Small problem here is that the ordering will assume anything appended is more serious than 'data submitted' - //progressListing = progressListing.concat(Object.keys(tmpTitles)) + // 2.0 If arbEventsPieChart is enabled we add to statusMapping and add HTML charts for the event + if (arbEventsPieCharts == true && createPies == true) { + //2.1 Create HTML elements + + // Split the array into multiple arrays, each of size 5. This let's us create pie chart rows of five + arbEventNames.sort() + chunkedArbEvents = Array.from({ length: Math.ceil(arbEventNames.length / 5) }, (v, i) => arbEventNames.slice(i * 5, i * 5 + 5) ); + + $("#arbpie").html('') // i. Clear the div class + html = '' + chunkedArbEvents.forEach(function(chunk){ + + rowhtml = '
\n\t
\n' + chunk.forEach(function(title){ + sanitizedEventName = title.toLowerCase().replace(/ /g, "_") // Convert Opened Word Document to opened_word_document. + sanitizedEventName = escapeHtml(sanitizedEventName) // Should maybe do more tests on this. Or even use a short random string rather than the name. e.g {"Opened Word Document" : "7a2f87"} + //i. Add the HTML element + rowhtml += '\t
\n' + + //ii. Add to statusMapping + statusMapping[title] = sanitizedEventName + + + }) + rowhtml += '\t
\n
\n' + + html += rowhtml + + }) + $("#arbpie").html(html) + + } + +} + + +// countCampaignEvents will return a dict of title:count of arbitrary and regular events from a campaign +// Todo: Need to implement backfill logic +function countCampaignEvents(campaign) { + + // Add all the default events to a counter dict + eventsCounter = {} + Object.keys(statusMapping).forEach(function (k) { + eventsCounter[k] = 0 + }); + + + campaign.timeline.forEach(function(event){ + if (event.message == "Arbitrary Event"){ + details = JSON.parse(event.details) + title = details.payload.title[0] + } else { + title = event.message + // Backfill logic for non arbitrary events. Todo + + } + if (title in eventsCounter) { + eventsCounter[title] += 1 + } else { + eventsCounter[title] = 1 + } + + // Backfill logic here for arb? + + }) + return eventsCounter } $(document).ready(function () { diff --git a/templates/campaign_results.html b/templates/campaign_results.html index fa3106c2..86ee5d1e 100644 --- a/templates/campaign_results.html +++ b/templates/campaign_results.html @@ -54,8 +54,14 @@
+
+ + +
+ +

Targets Map

From ee2d7398a7f95a9d2ef76d04c587ff4441991f8e Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Wed, 12 Aug 2020 12:02:13 +0100 Subject: [PATCH 4/5] Updated naming scheme from Arbitrary Events to Custom Events --- controllers/phish.go | 8 ++-- models/models.go | 40 ++++++++-------- models/result.go | 8 ++-- static/js/src/app/campaign_results.js | 68 +++++++++++++-------------- templates/campaign_results.html | 2 +- 5 files changed, 63 insertions(+), 63 deletions(-) diff --git a/controllers/phish.go b/controllers/phish.go index 117562f3..1472cd49 100644 --- a/controllers/phish.go +++ b/controllers/phish.go @@ -112,7 +112,7 @@ func (ps *PhishingServer) registerRoutes() { router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer)) router.HandleFunc("/track", ps.TrackHandler) router.HandleFunc("/robots.txt", ps.RobotsHandler) - router.HandleFunc("/arbevent", ps.ArbitraryEventHandler) + router.HandleFunc("/event", ps.CustomEventHandler) router.HandleFunc("/{path:.*}/track", ps.TrackHandler) router.HandleFunc("/{path:.*}/report", ps.ReportHandler) router.HandleFunc("/report", ps.ReportHandler) @@ -127,8 +127,8 @@ func (ps *PhishingServer) registerRoutes() { ps.server.Handler = phishHandler } -// ArbitraryEventHandler deals with arbitrary events - for example opening Word documents, secondary links, etc -func (ps *PhishingServer) ArbitraryEventHandler(w http.ResponseWriter, r *http.Request) { +// CustomEventHandler deals with Custom events - for example opening Word documents, secondary links, etc +func (ps *PhishingServer) CustomEventHandler(w http.ResponseWriter, r *http.Request) { r, err := setupContext(r) if err != nil { @@ -143,7 +143,7 @@ func (ps *PhishingServer) ArbitraryEventHandler(w http.ResponseWriter, r *http.R rs := ctx.Get(r, "result").(models.Result) d := ctx.Get(r, "details").(models.EventDetails) - err = rs.HandleArbitraryEvent(d) + err = rs.HandleCustomEvent(d) if err != nil { log.Error(err) w.Write([]byte(err.Error())) diff --git a/models/models.go b/models/models.go index ebca7d3d..73e78443 100644 --- a/models/models.go +++ b/models/models.go @@ -40,26 +40,26 @@ const InitialAdminPassword = "GOPHISH_INITIAL_ADMIN_PASSWORD" const InitialAdminApiToken = "GOPHISH_INITIAL_ADMIN_API_TOKEN" const ( - CampaignInProgress string = "In progress" - CampaignQueued string = "Queued" - CampaignCreated string = "Created" - CampaignEmailsSent string = "Emails Sent" - CampaignComplete string = "Completed" - EventSent string = "Email Sent" - EventSendingError string = "Error Sending Email" - EventOpened string = "Email Opened" - EventClicked string = "Clicked Link" - EventDataSubmit string = "Submitted Data" - EventReported string = "Email Reported" - EventArbitraryEvent string = "Arbitrary Event" - EventProxyRequest string = "Proxied request" - StatusSuccess string = "Success" - StatusQueued string = "Queued" - StatusSending string = "Sending" - StatusUnknown string = "Unknown" - StatusScheduled string = "Scheduled" - StatusRetry string = "Retrying" - Error string = "Error" + CampaignInProgress string = "In progress" + CampaignQueued string = "Queued" + CampaignCreated string = "Created" + CampaignEmailsSent string = "Emails Sent" + CampaignComplete string = "Completed" + EventSent string = "Email Sent" + EventSendingError string = "Error Sending Email" + EventOpened string = "Email Opened" + EventClicked string = "Clicked Link" + EventDataSubmit string = "Submitted Data" + EventReported string = "Email Reported" + EventCustomEvent string = "Custom Event" + EventProxyRequest string = "Proxied request" + StatusSuccess string = "Success" + StatusQueued string = "Queued" + StatusSending string = "Sending" + StatusUnknown string = "Unknown" + StatusScheduled string = "Scheduled" + StatusRetry string = "Retrying" + Error string = "Error" ) // Flash is used to hold flash information for use in templates. diff --git a/models/result.go b/models/result.go index 35305f34..0df75238 100644 --- a/models/result.go +++ b/models/result.go @@ -136,16 +136,16 @@ func (r *Result) HandleFormSubmit(details EventDetails) error { return db.Save(r).Error } -// HandleArbitraryEvent updates a Result with an arbitrary event (e.g Word document opened, secondary link clicked) -func (r *Result) HandleArbitraryEvent(details EventDetails) error { +// HandleCustomEvent updates a Result with an custom event (e.g Word document opened, secondary link clicked) +func (r *Result) HandleCustomEvent(details EventDetails) error { EventTitle := details.Payload.Get("title") if EventTitle == "" { - return errors.New("No title supplied for arbitrary event") + return errors.New("No title supplied for custom event") } - event, err := r.createEvent(EventArbitraryEvent, details) + event, err := r.createEvent(EventCustomEvent, details) if err != nil { return err } diff --git a/static/js/src/app/campaign_results.js b/static/js/src/app/campaign_results.js index b13345ef..4fc998c5 100644 --- a/static/js/src/app/campaign_results.js +++ b/static/js/src/app/campaign_results.js @@ -1,7 +1,7 @@ var map = null var doPoll = true; -var arbEventsPieCharts = true; // Include pie charts for arbitary events or not - // Setting to true will add the arb events to statusMapping as wel as adding HTML chart elements. +var customEventsPieCharts = true; // Include pie charts for custom events or not + // Setting to true will add the custom events to statusMapping as wel as adding HTML chart elements. // statuses is a helper map to point result statuses to ui classes var statuses = { @@ -390,9 +390,9 @@ function renderTimeline(data) { if (!event.email || event.email == record.email) { // Add the event - // Handle arbitrary event as a special case + // Handle custom event as a special case // We could collapse the first half into the regular code, but for now it feels neater to keep it separate and live with the code re-use (*waves @ jordan*) - if (event.message == "Arbitrary Event"){ + if (event.message == "Custom Event"){ if (event.details) { // Should always be data, otherwise we can ignore the event details = JSON.parse(event.details) @@ -403,7 +403,7 @@ function renderTimeline(data) { results += '
' + '
' + - '
' + escapeHtml(message) + // This is the case that makes code reuse tricky, as we want the title of the arbitrary event from the payload. TODO Give some more thought. Perhaps we scrap 'Arbitrary Event' and put the title in the message, and have some other indicator of the requirement to uniquely parse the contents of 'payload' e.g. payload['ae'] = 1 + '
' + escapeHtml(message) + // This is the case that makes code reuse tricky, as we want the title of the custom event from the payload. TODO Give some more thought. Perhaps we scrap 'Custom Event' and put the title in the message, and have some other indicator of the requirement to uniquely parse the contents of 'payload' e.g. payload['ae'] = 1 ' ' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '' // Check if user agent present && requested to display it @@ -429,7 +429,7 @@ function renderTimeline(data) { results += '
' } results += '
' - } // End arbitrary event processsing + } // End custom event processsing } else { // else, if regular event @@ -696,14 +696,14 @@ function poll() { .success(function (c) { campaign = c - updateArbitraryEventData(campaign, false) // Update data structures with new arbitrary event specifications + updateCustomEventData(campaign, false) // Update data structures with new custom event specifications /* Update the timeline */ var timeline_series_data = [] $.each(campaign.timeline, function (i, event) { - // Handle arbitary event - if (event.message == "Arbitrary Event") { + // Handle custom event + if (event.message == "Custom Event") { details = JSON.parse(event.details) message = details.payload.title @@ -737,7 +737,7 @@ function poll() { /* $.each(campaign.results, function (i, result) { - // Don't count arbitrary events, we do this independently to avoid backfill logic. + // Don't count custom events, we do this independently to avoid backfill logic. if (progressListing.includes(result.status)) { email_series_data[result.status]++; } @@ -816,7 +816,7 @@ function load() { campaign = c if (campaign) { - updateArbitraryEventData(campaign, true) // Update data structures with new arbitrary event specifications + updateCustomEventData(campaign, true) // Update data structures with new custom event specifications $("title").text(c.name + " - Gophish") $("#loading").hide() @@ -902,7 +902,7 @@ function load() { /* - // Don't count arbitrary events, we do this independently to avoid backfill logic. + // Don't count custom events, we do this independently to avoid backfill logic. if (progressListing.includes(result.status)) { email_series_data[result.status]++; } @@ -911,7 +911,7 @@ function load() { email_series_data['Email Reported']++ } - //TODO: At some point need to figure out backfilling with arbitrary events + //TODO: At some point need to figure out backfilling with custom events // Possibly just backfill Email sent and Email Opened before getting into more complex // data structures @@ -955,8 +955,8 @@ function load() { } var event_date = moment.utc(event.time).local() - // Handle arbitary event - if (event.message == "Arbitrary Event") { + // Handle custom event + if (event.message == "Custom Event") { details = JSON.parse(event.details) message = details.payload.title } else { @@ -1073,35 +1073,35 @@ function report_mail(rid, cid) { } -/* updateArbitraryData will go through the supplied campaign and add arbitrary event data to three data structure: +/* updateCustomData will go through the supplied campaign and add custom event data to three data structure: statuses statusMapping progressListing // Todo, needs more consideration on backfill The createPies boolean is used to allow us to create the pies on load() but not re-create them from calling poll(), as the - highchart info gets overwritten. The problem with this is that if a new arbitrary event comes in while the page is loaded + highchart info gets overwritten. The problem with this is that if a new custom event comes in while the page is loaded the poll() won't add the pie. Need to investigate this. TODO */ -function updateArbitraryEventData(campaign, createPies){ +function updateCustomEventData(campaign, createPies){ - var arbEventNames = [] // Hold unique arb event names. Used to create HTML pie charts if arbEventPieCharts set to true + var customEventNames = [] // Hold unique custom event names. Used to create HTML pie charts if customEventPieCharts set to true campaign.timeline.forEach(function(event) { // Step over each event - if (event.message == "Arbitrary Event") { + if (event.message == "Custom Event") { details = JSON.parse(event.details) // TODO Validate this exists // 1. Add title, color, icon, and label properties to statuses dict - title = "Arbitrary Event" + title = "Custom Event" if ("title" in details.payload){ title = String(details.payload.title) } - statuses[title] = {"arbitrary event" : 1} // Set true to be arbitrary event, just so we can discern if we need to + statuses[title] = {"custom event" : 1} // Set true to be custom event, just so we can discern if we need to statuses[title]["color"] = "#00FFFF" // Default if ("color" in details.payload ){ @@ -1124,8 +1124,8 @@ function updateArbitraryEventData(campaign, createPies){ statuses[title]["label"] = label } - if (!arbEventNames.includes(title)){ - arbEventNames.push(title) + if (!customEventNames.includes(title)){ + customEventNames.push(title) } /* How to handle progressListing needs more thought, and probably */ @@ -1138,16 +1138,16 @@ function updateArbitraryEventData(campaign, createPies){ } }) - // 2.0 If arbEventsPieChart is enabled we add to statusMapping and add HTML charts for the event - if (arbEventsPieCharts == true && createPies == true) { + // 2.0 If customEventsPieChart is enabled we add to statusMapping and add HTML charts for the event + if (customEventsPieCharts == true && createPies == true) { //2.1 Create HTML elements // Split the array into multiple arrays, each of size 5. This let's us create pie chart rows of five - arbEventNames.sort() - chunkedArbEvents = Array.from({ length: Math.ceil(arbEventNames.length / 5) }, (v, i) => arbEventNames.slice(i * 5, i * 5 + 5) ); + customEventNames.sort() + chunkedArbEvents = Array.from({ length: Math.ceil(customEventNames.length / 5) }, (v, i) => customEventNames.slice(i * 5, i * 5 + 5) ); - $("#arbpie").html('') // i. Clear the div class + $("#custompie").html('') // i. Clear the div class html = '' chunkedArbEvents.forEach(function(chunk){ @@ -1168,14 +1168,14 @@ function updateArbitraryEventData(campaign, createPies){ html += rowhtml }) - $("#arbpie").html(html) + $("#custompie").html(html) } } -// countCampaignEvents will return a dict of title:count of arbitrary and regular events from a campaign +// countCampaignEvents will return a dict of title:count of custom and regular events from a campaign // Todo: Need to implement backfill logic function countCampaignEvents(campaign) { @@ -1187,12 +1187,12 @@ function countCampaignEvents(campaign) { campaign.timeline.forEach(function(event){ - if (event.message == "Arbitrary Event"){ + if (event.message == "Custom Event"){ details = JSON.parse(event.details) title = details.payload.title[0] } else { title = event.message - // Backfill logic for non arbitrary events. Todo + // Backfill logic for non custom events. Todo } if (title in eventsCounter) { @@ -1201,7 +1201,7 @@ function countCampaignEvents(campaign) { eventsCounter[title] = 1 } - // Backfill logic here for arb? + // Backfill logic here for custom? }) return eventsCounter diff --git a/templates/campaign_results.html b/templates/campaign_results.html index 86ee5d1e..bb6231e9 100644 --- a/templates/campaign_results.html +++ b/templates/campaign_results.html @@ -59,7 +59,7 @@
-
+
From 1adfc323b4f91927c0d4c81258aff333e1512ecf Mon Sep 17 00:00:00 2001 From: Glenn Wilkinson Date: Wed, 12 Aug 2020 12:30:41 +0100 Subject: [PATCH 5/5] Small fixes --- controllers/phish.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/controllers/phish.go b/controllers/phish.go index 1472cd49..dc9a4a2e 100644 --- a/controllers/phish.go +++ b/controllers/phish.go @@ -146,10 +146,9 @@ func (ps *PhishingServer) CustomEventHandler(w http.ResponseWriter, r *http.Requ err = rs.HandleCustomEvent(d) if err != nil { log.Error(err) - w.Write([]byte(err.Error())) + http.NotFound(w, r) } else { - - w.Write([]byte("Event received")) + w.WriteHeader(http.StatusNoContent) } }