mirror of https://github.com/gophish/gophish
Added initial functionality to allow arbitrary events
parent
b684fb4ebd
commit
c8abed4896
|
@ -112,6 +112,7 @@ func (ps *PhishingServer) registerRoutes() {
|
||||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", fileServer))
|
||||||
router.HandleFunc("/track", ps.TrackHandler)
|
router.HandleFunc("/track", ps.TrackHandler)
|
||||||
router.HandleFunc("/robots.txt", ps.RobotsHandler)
|
router.HandleFunc("/robots.txt", ps.RobotsHandler)
|
||||||
|
router.HandleFunc("/arbevent", ps.ArbitraryEventHandler)
|
||||||
router.HandleFunc("/{path:.*}/track", ps.TrackHandler)
|
router.HandleFunc("/{path:.*}/track", ps.TrackHandler)
|
||||||
router.HandleFunc("/{path:.*}/report", ps.ReportHandler)
|
router.HandleFunc("/{path:.*}/report", ps.ReportHandler)
|
||||||
router.HandleFunc("/report", ps.ReportHandler)
|
router.HandleFunc("/report", ps.ReportHandler)
|
||||||
|
@ -126,6 +127,32 @@ func (ps *PhishingServer) registerRoutes() {
|
||||||
ps.server.Handler = phishHandler
|
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
|
// TrackHandler tracks emails as they are opened, updating the status for the given Result
|
||||||
func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
|
func (ps *PhishingServer) TrackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
r, err := setupContext(r)
|
r, err := setupContext(r)
|
||||||
|
|
|
@ -51,6 +51,7 @@ const (
|
||||||
EventClicked string = "Clicked Link"
|
EventClicked string = "Clicked Link"
|
||||||
EventDataSubmit string = "Submitted Data"
|
EventDataSubmit string = "Submitted Data"
|
||||||
EventReported string = "Email Reported"
|
EventReported string = "Email Reported"
|
||||||
|
EventArbitraryEvent string = "Arbitrary Event"
|
||||||
EventProxyRequest string = "Proxied request"
|
EventProxyRequest string = "Proxied request"
|
||||||
StatusSuccess string = "Success"
|
StatusSuccess string = "Success"
|
||||||
StatusQueued string = "Queued"
|
StatusQueued string = "Queued"
|
||||||
|
|
|
@ -3,6 +3,7 @@ package models
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
@ -135,6 +136,24 @@ func (r *Result) HandleFormSubmit(details EventDetails) error {
|
||||||
return db.Save(r).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
|
// HandleEmailReport updates a Result in the case where they report a simulated
|
||||||
// phishing email using the HTTP handler.
|
// phishing email using the HTTP handler.
|
||||||
func (r *Result) HandleEmailReport(details EventDetails) error {
|
func (r *Result) HandleEmailReport(details EventDetails) error {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
console.log("Running campaign_results.min.js")
|
||||||
var map = null
|
var map = null
|
||||||
var doPoll = true;
|
var doPoll = true;
|
||||||
|
|
||||||
// statuses is a helper map to point result statuses to ui classes
|
// statuses is a helper map to point result statuses to ui classes
|
||||||
var statuses = {
|
var statuses = {
|
||||||
|
|
||||||
"Email Sent": {
|
"Email Sent": {
|
||||||
color: "#1abc9c",
|
color: "#1abc9c",
|
||||||
label: "label-success",
|
label: "label-success",
|
||||||
|
@ -344,6 +346,7 @@ var renderDevice = function (event_details) {
|
||||||
var browserIcon = 'info-circle'
|
var browserIcon = 'info-circle'
|
||||||
var browserVersion = ''
|
var browserVersion = ''
|
||||||
|
|
||||||
|
|
||||||
if (ua.browser && ua.browser.name) {
|
if (ua.browser && ua.browser.name) {
|
||||||
deviceBrowser = ua.browser.name
|
deviceBrowser = ua.browser.name
|
||||||
// Handle the "mobile safari" case
|
// Handle the "mobile safari" case
|
||||||
|
@ -365,6 +368,7 @@ var renderDevice = function (event_details) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTimeline(data) {
|
function renderTimeline(data) {
|
||||||
|
|
||||||
record = {
|
record = {
|
||||||
"id": data[0],
|
"id": data[0],
|
||||||
"first_name": data[2],
|
"first_name": data[2],
|
||||||
|
@ -383,6 +387,52 @@ function renderTimeline(data) {
|
||||||
$.each(campaign.timeline, function (i, event) {
|
$.each(campaign.timeline, function (i, event) {
|
||||||
if (!event.email || event.email == record.email) {
|
if (!event.email || event.email == record.email) {
|
||||||
// Add the event
|
// 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 += '<div class="timeline-entry">' +
|
||||||
|
' <div class="timeline-bar"></div>'
|
||||||
|
results +=
|
||||||
|
' <div class="timeline-icon ' + statuses[message].label+ '">' +
|
||||||
|
' <i class="fa ' + statuses[message].icon + '"></i></div>' +
|
||||||
|
' <div class="timeline-message">' + 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
|
||||||
|
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
|
||||||
|
|
||||||
|
// 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 += '<div class="timeline-device-details">'
|
||||||
|
details.payload.sub_text.forEach(function (text, index) {
|
||||||
|
results += "<div>"
|
||||||
|
// Check if there's an associated icon
|
||||||
|
if ("sub_icon" in details.payload && details.payload.sub_icon.length >= index){
|
||||||
|
results = results + '<span class="' + details.payload.sub_icon[index] + '"></span> ' //+ text
|
||||||
|
}
|
||||||
|
results += text
|
||||||
|
results += "</div>"
|
||||||
|
})
|
||||||
|
results += '</div>'
|
||||||
|
}
|
||||||
|
results += '</div></div>'
|
||||||
|
} // End arbitrary event processsing
|
||||||
|
|
||||||
|
|
||||||
|
} else { // else, if regular event
|
||||||
|
|
||||||
|
|
||||||
results += '<div class="timeline-entry">' +
|
results += '<div class="timeline-entry">' +
|
||||||
' <div class="timeline-bar"></div>'
|
' <div class="timeline-bar"></div>'
|
||||||
results +=
|
results +=
|
||||||
|
@ -392,7 +442,8 @@ function renderTimeline(data) {
|
||||||
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
|
' <span class="timeline-date">' + moment.utc(event.time).local().format('MMMM Do YYYY h:mm:ss a') + '</span>'
|
||||||
if (event.details) {
|
if (event.details) {
|
||||||
details = JSON.parse(event.details)
|
details = JSON.parse(event.details)
|
||||||
if (event.message == "Clicked Link" || event.message == "Submitted Data") {
|
|
||||||
|
if (event.message == "Clicked Link" || event.message == "Submitted Data" || event.message == "Email Opened") {
|
||||||
deviceView = renderDevice(details)
|
deviceView = renderDevice(details)
|
||||||
if (deviceView) {
|
if (deviceView) {
|
||||||
results += deviceView
|
results += deviceView
|
||||||
|
@ -428,6 +479,8 @@ function renderTimeline(data) {
|
||||||
}
|
}
|
||||||
results += '</div></div>'
|
results += '</div></div>'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// Add the scheduled send event at the bottom
|
// Add the scheduled send event at the bottom
|
||||||
if (record.status == "Scheduled" || record.status == "Retrying") {
|
if (record.status == "Scheduled" || record.status == "Retrying") {
|
||||||
|
@ -612,7 +665,13 @@ var updateMap = function (results) {
|
||||||
* @param {moment(datetime)} send_date
|
* @param {moment(datetime)} send_date
|
||||||
*/
|
*/
|
||||||
function createStatusLabel(status, send_date) {
|
function createStatusLabel(status, send_date) {
|
||||||
|
|
||||||
|
if (status in statuses){
|
||||||
var label = statuses[status].label || "label-default";
|
var label = statuses[status].label || "label-default";
|
||||||
|
} else {
|
||||||
|
var label = "label-default"
|
||||||
|
}
|
||||||
|
|
||||||
var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
|
var statusColumn = "<span class=\"label " + label + "\">" + status + "</span>"
|
||||||
// Add the tooltip if the email is scheduled to be sent
|
// Add the tooltip if the email is scheduled to be sent
|
||||||
if (status == "Scheduled" || status == "Retrying") {
|
if (status == "Scheduled" || status == "Retrying") {
|
||||||
|
@ -637,14 +696,25 @@ function poll() {
|
||||||
/* Update the timeline */
|
/* Update the timeline */
|
||||||
var timeline_series_data = []
|
var timeline_series_data = []
|
||||||
$.each(campaign.timeline, function (i, event) {
|
$.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()
|
var event_date = moment.utc(event.time).local()
|
||||||
timeline_series_data.push({
|
timeline_series_data.push({
|
||||||
email: event.email,
|
email: event.email,
|
||||||
message: event.message,
|
message: message, //event.message,
|
||||||
x: event_date.valueOf(),
|
x: event_date.valueOf(),
|
||||||
y: 1,
|
y: 1,
|
||||||
marker: {
|
marker: {
|
||||||
fillColor: statuses[event.message].color
|
fillColor: statuses[message].color //statuses[event.message].color
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -700,6 +770,7 @@ function poll() {
|
||||||
rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
rowData[8] = moment(result.send_date).format('MMMM Do YYYY, h:mm:ss a')
|
||||||
rowData[7] = result.reported
|
rowData[7] = result.reported
|
||||||
rowData[6] = result.status
|
rowData[6] = result.status
|
||||||
|
|
||||||
resultsTable.row(i).data(rowData)
|
resultsTable.row(i).data(rowData)
|
||||||
if (row.child.isShown()) {
|
if (row.child.isShown()) {
|
||||||
$(row.node()).find("#caret").removeClass("fa-caret-right")
|
$(row.node()).find("#caret").removeClass("fa-caret-right")
|
||||||
|
@ -720,12 +791,59 @@ function poll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
|
|
||||||
campaign.id = window.location.pathname.split('/').slice(-1)[0]
|
campaign.id = window.location.pathname.split('/').slice(-1)[0]
|
||||||
var use_map = JSON.parse(localStorage.getItem('gophish.use_map'))
|
var use_map = JSON.parse(localStorage.getItem('gophish.use_map'))
|
||||||
api.campaignId.results(campaign.id)
|
api.campaignId.results(campaign.id)
|
||||||
.success(function (c) {
|
.success(function (c) {
|
||||||
campaign = c
|
campaign = c
|
||||||
if (campaign) {
|
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")
|
$("title").text(c.name + " - Gophish")
|
||||||
$("#loading").hide()
|
$("#loading").hide()
|
||||||
$("#campaignResults").show()
|
$("#campaignResults").show()
|
||||||
|
@ -794,6 +912,7 @@ function load() {
|
||||||
email_series_data[k] = 0
|
email_series_data[k] = 0
|
||||||
});
|
});
|
||||||
$.each(campaign.results, function (i, result) {
|
$.each(campaign.results, function (i, result) {
|
||||||
|
|
||||||
resultsTable.row.add([
|
resultsTable.row.add([
|
||||||
result.id,
|
result.id,
|
||||||
"<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
|
"<i id=\"caret\" class=\"fa fa-caret-right\"></i>",
|
||||||
|
@ -842,15 +961,25 @@ function load() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var event_date = moment.utc(event.time).local()
|
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({
|
timeline_series_data.push({
|
||||||
email: event.email,
|
email: event.email,
|
||||||
message: event.message,
|
message: message, //event.message,
|
||||||
x: event_date.valueOf(),
|
x: event_date.valueOf(),
|
||||||
y: 1,
|
y: 1,
|
||||||
marker: {
|
marker: {
|
||||||
fillColor: statuses[event.message].color
|
fillColor: statuses[message].color //statuses[event.message].color
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
renderTimelineChart({
|
renderTimelineChart({
|
||||||
data: timeline_series_data
|
data: timeline_series_data
|
||||||
|
@ -869,6 +998,7 @@ function load() {
|
||||||
name: '',
|
name: '',
|
||||||
y: 100 - Math.floor((count / campaign.results.length) * 100)
|
y: 100 - Math.floor((count / campaign.results.length) * 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
var chart = renderPieChart({
|
var chart = renderPieChart({
|
||||||
elemId: statusMapping[status] + '_chart',
|
elemId: statusMapping[status] + '_chart',
|
||||||
title: status,
|
title: status,
|
||||||
|
@ -876,6 +1006,7 @@ function load() {
|
||||||
data: email_data,
|
data: email_data,
|
||||||
colors: [statuses[status].color, '#dddddd']
|
colors: [statuses[status].color, '#dddddd']
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (use_map) {
|
if (use_map) {
|
||||||
|
|
Loading…
Reference in New Issue