Browse Source

Moved all charts from Chartist to Highcharts. Closes #680.

pull/1902/head
Jordan Wright 4 years ago
parent
commit
75600f5812
20 changed files with 3440 additions and 1258 deletions
  1. +45
    -50
      gulpfile.js
  2. +11
    -4
      models/campaign.go
  3. +2
    -2
      package.json
  4. +3
    -3
      static/css/dist/gophish.css
  5. +2
    -2
      static/css/font-awesome.min.css
  6. +22
    -0
      static/css/main.css
  7. BIN
      static/font/FontAwesome.otf
  8. BIN
      static/font/fontawesome-webfont.eot
  9. +2668
    -652
      static/font/fontawesome-webfont.svg
  10. BIN
      static/font/fontawesome-webfont.ttf
  11. BIN
      static/font/fontawesome-webfont.woff
  12. BIN
      static/font/fontawesome-webfont.woff2
  13. +1
    -1
      static/js/dist/app/campaign_results.min.js
  14. +1
    -1
      static/js/dist/app/dashboard.min.js
  15. +28
    -22
      static/js/dist/vendor.min.js
  16. +352
    -262
      static/js/src/app/campaign_results.js
  17. +205
    -150
      static/js/src/app/dashboard.js
  18. +34
    -34
      static/js/src/app/gophish.js
  19. +50
    -59
      templates/campaign_results.html
  20. +16
    -16
      templates/dashboard.html

+ 45
- 50
gulpfile.js View File

@ -5,11 +5,9 @@
*/
var gulp = require('gulp'),
jshint = require('gulp-jshint'),
rename = require('gulp-rename'),
concat = require('gulp-concat'),
uglify = require('gulp-uglify'),
wrap = require('gulp-wrap'),
cleanCSS = require('gulp-clean-css'),
js_directory = 'static/js/src/',
@ -19,72 +17,69 @@ var gulp = require('gulp'),
dest_js_directory = 'static/js/dist/',
dest_css_directory = 'static/css/dist/';
gulp.task('default', ['watch']);
gulp.task('jshint', function() {
return gulp.src(js_directory)
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
gulp.task('build', function() {
gulp.task('vendorjs', function () {
// Vendor minifying / concat
gulp.src([
vendor_directory + 'jquery.js',
vendor_directory + 'bootstrap.min.js',
vendor_directory + 'moment.min.js',
vendor_directory + 'chartist.min.js',
vendor_directory + 'papaparse.min.js',
vendor_directory + 'd3.min.js',
vendor_directory + 'topojson.min.js',
vendor_directory + 'datamaps.min.js',
vendor_directory + 'jquery.dataTables.min.js',
vendor_directory + 'dataTables.bootstrap.js',
vendor_directory + 'datetime-moment.js',
vendor_directory + 'jquery.ui.widget.js',
vendor_directory + 'jquery.fileupload.js',
vendor_directory + 'jquery.iframe-transport.js',
vendor_directory + 'sweetalert2.min.js',
vendor_directory + 'bootstrap-datetime.js',
vendor_directory + 'select2.min.js',
vendor_directory + 'core.min.js'
])
return gulp.src([
vendor_directory + 'jquery.js',
vendor_directory + 'bootstrap.min.js',
vendor_directory + 'moment.min.js',
vendor_directory + 'chartist.min.js',
vendor_directory + 'papaparse.min.js',
vendor_directory + 'd3.min.js',
vendor_directory + 'topojson.min.js',
vendor_directory + 'datamaps.min.js',
vendor_directory + 'jquery.dataTables.min.js',
vendor_directory + 'dataTables.bootstrap.js',
vendor_directory + 'datetime-moment.js',
vendor_directory + 'jquery.ui.widget.js',
vendor_directory + 'jquery.fileupload.js',
vendor_directory + 'jquery.iframe-transport.js',
vendor_directory + 'sweetalert2.min.js',
vendor_directory + 'bootstrap-datetime.js',
vendor_directory + 'select2.min.js',
vendor_directory + 'core.min.js',
vendor_directory + 'highcharts.js'
])
.pipe(concat('vendor.js'))
.pipe(rename({
suffix: '.min'
}))
.pipe(uglify())
.pipe(gulp.dest(dest_js_directory));
})
gulp.task('scripts', function () {
// Gophish app files
gulp.src(app_directory)
.pipe(rename({
suffix: '.min'
}))
.pipe(uglify().on('error', function(e){
.pipe(uglify().on('error', function (e) {
console.log(e);
}))
.pipe(gulp.dest(dest_js_directory + 'app/'));
})
gulp.task('styles', function() {
return gulp.src([
css_directory + 'bootstrap.min.css',
css_directory + 'main.css',
css_directory + 'dashboard.css',
css_directory + 'flat-ui.css',
css_directory + 'dataTables.bootstrap.css',
css_directory + 'font-awesome.min.css',
css_directory + 'chartist.min.css',
css_directory + 'bootstrap-datetime.css',
css_directory + 'checkbox.css',
css_directory + 'sweetalert2.min.css',
css_directory + 'select2.min.css',
css_directory + 'select2-bootstrap.min.css'
])
.pipe(cleanCSS({compatibilty: 'ie9'}))
css_directory + 'bootstrap.min.css',
css_directory + 'main.css',
css_directory + 'dashboard.css',
css_directory + 'flat-ui.css',
css_directory + 'dataTables.bootstrap.css',
css_directory + 'font-awesome.min.css',
css_directory + 'chartist.min.css',
css_directory + 'bootstrap-datetime.css',
css_directory + 'checkbox.css',
css_directory + 'sweetalert2.min.css',
css_directory + 'select2.min.css',
css_directory + 'select2-bootstrap.min.css',
])
.pipe(cleanCSS({ compatibilty: 'ie9' }))
.pipe(concat('gophish.css'))
.pipe(gulp.dest(dest_css_directory));
});
})
gulp.task('build', ['vendorjs', 'scripts', 'styles']);
gulp.task('watch', function() {
gulp.watch('static/js/src/app/**/*.js', ['jshint']);
});
gulp.task('default', ['build']);

+ 11
- 4
models/campaign.go View File

@ -198,6 +198,7 @@ func (c *Campaign) getDetails() error {
}
// getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID.
// It also backfills numbers as appropriate with a running total, so that the values are aggregated.
func getCampaignStats(cid int64) (CampaignStats, error) {
s := CampaignStats{}
query := db.Table("results").Where("campaign_id = ?", cid)
@ -205,22 +206,28 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil {
return s, err
}
err = query.Where("status=?", EVENT_SENT).Count(&s.EmailsSent).Error
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData)
if err != nil {
return s, err
}
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
query.Where("status=?", EVENT_CLICKED).Count(&s.ClickedLink)
if err != nil {
return s, err
}
query.Where("status=?", EVENT_CLICKED).Count(&s.ClickedLink)
// Every submitted data event implies they clicked the link
s.ClickedLink += s.SubmittedData
err = query.Where("status=?", EVENT_OPENED).Count(&s.OpenedEmail).Error
if err != nil {
return s, err
}
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData)
// Every clicked link event implies they opened the email
s.OpenedEmail += s.ClickedLink
err = query.Where("status=?", EVENT_SENT).Count(&s.EmailsSent).Error
if err != nil {
return s, err
}
// Every opened email event implies the email was sent
s.EmailsSent += s.OpenedEmail
err = query.Where("status=?", ERROR).Count(&s.Error).Error
return s, err
}

+ 2
- 2
package.json View File

@ -1,6 +1,6 @@
{
"name": "gophish",
"version": "0.3.0-dev",
"version": "0.4.0-dev",
"repository": {
"type": "git",
"url": "git+https://github.com/gophish/gophish.git"
@ -24,4 +24,4 @@
"jshint": "^2.9.4",
"jshint-stylish": "^2.2.1"
}
}
}

+ 3
- 3
static/css/dist/gophish.css
File diff suppressed because it is too large
View File


+ 2
- 2
static/css/font-awesome.min.css
File diff suppressed because it is too large
View File


+ 22
- 0
static/css/main.css View File

@ -541,3 +541,25 @@ table.dataTable{
.input-group-btn .btn {
line-height:20px !important;
}
.highcharts-title {
font-family: "Source Sans Pro",Helvetica,Arial,sans-serif;
}
.color-success {
font-weight: bold;
color: #f05b4f;
}
.color-sent {
font-weight: bold;
color: #1abc9c;
}
.color-opened {
font-weight: bold;
color: #f9bf3b;
}
.color-clicked {
font-weight: bold;
color: #f39c12;
}
.color-success {
color: #f05b4f;
}

BIN
static/font/FontAwesome.otf View File


BIN
static/font/fontawesome-webfont.eot View File


+ 2668
- 652
static/font/fontawesome-webfont.svg
File diff suppressed because it is too large
View File


BIN
static/font/fontawesome-webfont.ttf View File


BIN
static/font/fontawesome-webfont.woff View File


BIN
static/font/fontawesome-webfont.woff2 View File


+ 1
- 1
static/js/dist/app/campaign_results.min.js
File diff suppressed because it is too large
View File


+ 1
- 1
static/js/dist/app/dashboard.min.js
File diff suppressed because it is too large
View File


+ 28
- 22
static/js/dist/vendor.min.js
File diff suppressed because it is too large
View File


+ 352
- 262
static/js/src/app/campaign_results.js View File

@ -4,64 +4,70 @@ var doPoll = true;
// statuses is a helper map to point result statuses to ui classes
var statuses = {
"Email Sent": {
slice: "ct-slice-donut-sent",
legend: "ct-legend-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": {
slice: "ct-slice-donut-opened",
legend: "ct-legend-opened",
color: "#f9bf3b",
label: "label-warning",
icon: "fa-envelope",
point: "ct-point-opened"
},
"Clicked Link": {
slice: "ct-slice-donut-clicked",
legend: "ct-legend-clicked",
color: "#F39C12",
label: "label-clicked",
icon: "fa-mouse-pointer",
point: "ct-point-clicked"
},
"Success": {
slice: "ct-slice-donut-success",
legend: "ct-legend-success",
color: "#f05b4f",
label: "label-danger",
icon: "fa-exclamation",
point: "ct-point-clicked"
},
"Error": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-times",
point: "ct-point-error"
},
"Error Sending Email": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-times",
point: "ct-point-error"
},
"Submitted Data": {
slice: "ct-slice-donut-success",
legend: "ct-legend-success",
color: "#f05b4f",
label: "label-danger",
icon: "fa-exclamation",
point: "ct-point-clicked"
},
"Unknown": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-question",
point: "ct-point-error"
},
"Sending": {
slice: "ct-slice-donut-sending",
legend: "ct-legend-sending",
color: "#428bca",
label: "label-primary",
icon: "fa-spinner",
point: "ct-point-sending"
@ -72,6 +78,22 @@ var statuses = {
}
}
var statusMapping = {
"Email Sent": "sent",
"Email Opened": "opened",
"Clicked Link": "clicked",
"Submitted Data": "submitted_data",
}
// 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 = []
@ -93,24 +115,25 @@ function deleteCampaign() {
confirmButtonColor: "#428bca",
reverseButtons: true,
allowOutsideClick: false,
preConfirm: function() {
return new Promise(function(resolve, reject) {
showLoaderOnConfirm: true,
preConfirm: function () {
return new Promise(function (resolve, reject) {
api.campaignId.delete(campaign.id)
.success(function(msg) {
.success(function (msg) {
resolve()
})
.error(function(data) {
.error(function (data) {
reject(data.responseJSON.message)
})
})
}
}).then(function() {
}).then(function () {
swal(
'Campaign Deleted!',
'This campaign has been deleted!',
'success'
);
$('button:contains("OK")').on('click', function() {
$('button:contains("OK")').on('click', function () {
location.href = '/campaigns'
})
})
@ -128,18 +151,19 @@ function completeCampaign() {
confirmButtonColor: "#428bca",
reverseButtons: true,
allowOutsideClick: false,
preConfirm: function() {
return new Promise(function(resolve, reject) {
showLoaderOnConfirm: true,
preConfirm: function () {
return new Promise(function (resolve, reject) {
api.campaignId.complete(campaign.id)
.success(function(msg) {
.success(function (msg) {
resolve()
})
.error(function(data) {
.error(function (data) {
reject(data.responseJSON.message)
})
})
}
}).then(function() {
}).then(function () {
swal(
'Campaign Completed!',
'This campaign has been completed!',
@ -190,32 +214,32 @@ function replay(event_idx) {
details = JSON.parse(request.details)
url = null
form = $('<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;
}
$('<input>').attr({
name: param,
}).val(details.payload[param]).appendTo(form);
})
/* Ensure we know where to send the user */
// Prompt for the URL
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;
}
$('<input>').attr({
name: param,
}).val(details.payload[param]).appendTo(form);
})
/* Ensure we know where to send the user */
// Prompt for the URL
swal({
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) {
inputValidator: function (value) {
return new Promise(function (resolve, reject) {
if (value) {
resolve();
} else {
@ -223,7 +247,7 @@ function replay(event_idx) {
}
});
}
}).then(function(result) {
}).then(function (result) {
url = result
submitForm()
})
@ -249,7 +273,7 @@ function renderTimeline(data) {
'<h6>Timeline for ' + escapeHtml(record.first_name) + ' ' + escapeHtml(record.last_name) +
'</h6><span class="subtitle">Email: ' + escapeHtml(record.email) + '</span>' +
'<div class="timeline-graph col-sm-6">'
$.each(campaign.timeline, function(i, event) {
$.each(campaign.timeline, function (i, event) {
if (!event.email || event.email == record.email) {
// Add the event
results += '<div class="timeline-entry">' +
@ -270,7 +294,7 @@ function renderTimeline(data) {
results += '<div class="timeline-event-results">'
results += ' <table class="table table-condensed table-bordered table-striped">'
results += ' <thead><tr><th>Parameter</th><th>Value(s)</tr></thead><tbody>'
$.each(Object.keys(details.payload), function(i, param) {
$.each(Object.keys(details.payload), function (i, param) {
if (param == "rid") {
return true;
}
@ -296,6 +320,131 @@ function renderTimeline(data) {
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)) +
'<br>Event: ' + this.point.message + '<br>Email: <b>' + this.point.email + '</b>'
}
},
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
}]
})
}
/* 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].y, 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].y })
}
}
},
title: {
text: chartopts['title']
},
plotOptions: {
pie: {
innerSize: '80%',
dataLabels: {
enabled: false
}
}
},
credits: {
enabled: false
},
tooltip: {
formatter: function () {
if (this.key == undefined) {
return false
}
return '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '</b><br/>'
}
},
series: [{
data: chartopts['data'],
colors: chartopts['colors'],
}]
})
}
/* poll - Queries the API and updates the UI with the results
*
@ -307,81 +456,94 @@ function renderTimeline(data) {
*/
function poll() {
api.campaignId.results(campaign.id)
.success(function(c) {
.success(function (c) {
campaign = c
/* Update the timeline */
var timeline_data = {
series: [{
name: "Events",
data: []
}]
}
$.each(campaign.timeline, function(i, event) {
timeline_data.series[0].data.push({
meta: i,
x: new Date(event.time),
/* Update the timeline */
var timeline_series_data = []
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
timeline_series_data.push({
email: event.email,
x: event_date.valueOf(),
y: 1
})
})
var timeline_chart = $("#timeline_chart")
if (timeline_chart.get(0).__chartist__) {
timeline_chart.get(0).__chartist__.update(timeline_data)
}
var timeline_series_data = []
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
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_data = {
series: []
}
var email_series_data = {}
$.each(campaign.results, function(i, result) {
if (!email_series_data[result.status]) {
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++;
// 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]++;
// Backfill status values
var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) {
email_series_data[progressListing[i]]++
}
})
$("#email_chart_legend").html("")
$.each(email_series_data, function(status, count) {
email_data.series.push({
meta: status,
value: count
$.each(email_series_data, function (status, count) {
var email_data = []
if (!(status in statusMapping)) {
return true
}
email_data.push({
name: status,
y: count
})
email_data.push({
name: '',
y: campaign.results.length - count
})
var chart = $("#" + statusMapping[status] + "_chart").highcharts()
chart.series[0].update({
data: email_data
})
$("#email_chart_legend").append('<li><span class="' + statuses[status].legend + '"></span>' + status + '</li>')
})
var email_chart = $("#email_chart")
if (email_chart.get(0).__chartist__) {
email_chart.get(0).__chartist__.on('draw', function(data) {
data.element.addClass(statuses[data.meta].slice)
})
// Update with the latest data
email_chart.get(0).__chartist__.update(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) {
var label = statuses[result.status].label || "label-default";
rowData[6] = "<span class=\"label " + label + "\">" + result.status + "</span>"
resultsTable.row(i).data(rowData).draw(false)
if (row.child.isShown()) {
row.child(renderTimeline(row.data()))
}
return false
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) {
var label = statuses[result.status].label || "label-default";
rowData[6] = "<span class=\"label " + label + "\">" + result.status + "</span>"
resultsTable.row(i).data(rowData).draw(false)
if (row.child.isShown()) {
row.child(renderTimeline(row.data()))
}
})
return false
}
})
/* Update the map information */
})
/* Update the map information */
bubbles = []
$.each(campaign.results, function(i, result) {
$.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) {
$.each(bubbles, function (i, bubble) {
if (bubble.ip == result.ip) {
bubbles[i].radius += 1
newIP = false
@ -407,13 +569,13 @@ function poll() {
function load() {
campaign.id = window.location.pathname.split('/').slice(-1)[0]
api.campaignId.results(campaign.id)
.success(function(c) {
.success(function (c) {
campaign = c
if (campaign) {
$("title").text(c.name + " - Gophish")
$("#loading").hide()
$("#campaignResults").show()
// Set the title
// Set the title
$("#page-title").text("Results for " + c.name)
if (c.status == "Completed") {
$('#complete_button')[0].disabled = true;
@ -422,57 +584,21 @@ function load() {
}
// Setup tooltips
$('[data-toggle="tooltip"]').tooltip()
// 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 our graphs
var timeline_data = {
series: [{
name: "Events",
data: []
}]
}
var email_data = {
series: []
}
var email_legend = {}
var email_series_data = {}
var timeline_opts = {
axisX: {
showGrid: false,
type: Chartist.FixedScaleAxis,
divisor: 5,
labelInterpolationFnc: function(value) {
return moment(value).format('MMMM Do YYYY h:mm a')
}
},
axisY: {
type: Chartist.FixedScaleAxis,
ticks: [0, 1, 2],
low: 0,
showLabel: false
},
showArea: false,
plugins: []
}
var email_opts = {
donut: true,
donutWidth: 40,
chartPadding: 0,
showLabel: 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
})
// Setup the results table
resultsTable = $("#resultsTable").DataTable({
destroy: true,
"order": [
@ -490,25 +616,31 @@ function load() {
}]
});
resultsTable.clear();
$.each(campaign.results, function(i, result) {
label = statuses[result.status].label || "label-default";
resultsTable.row.add([
result.id,
"<i class=\"fa fa-caret-right\"></i>",
escapeHtml(result.first_name) || "",
escapeHtml(result.last_name) || "",
escapeHtml(result.email) || "",
escapeHtml(result.position) || "",
"<span class=\"label " + label + "\">" + result.status + "</span>"
]).draw()
if (!email_series_data[result.status]) {
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++;
}
})
// Setup the individual timelines
$('#resultsTable tbody').on('click', 'td.details-control', function() {
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) {
label = statuses[result.status].label || "label-default";
resultsTable.row.add([
result.id,
"<i class=\"fa fa-caret-right\"></i>",
escapeHtml(result.first_name) || "",
escapeHtml(result.last_name) || "",
escapeHtml(result.email) || "",
escapeHtml(result.position) || "",
"<span class=\"label " + label + "\">" + result.status + "</span>"
]).draw()
email_series_data[result.status]++;
// Backfill status values
var step = progressListing.indexOf(result.status)
for (var i = 0; i < step; i++) {
email_series_data[progressListing[i]]++
}
})
// 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()) {
@ -528,88 +660,42 @@ function load() {
}
});
// Setup the graphs
$.each(campaign.timeline, function(i, event) {
timeline_data.series[0].data.push({
meta: i,
x: new Date(event.time),
y: 1
$.each(campaign.timeline, function (i, event) {
var event_date = moment(event.time)
timeline_series_data.push({
email: event.email,
message: event.message,
x: event_date.valueOf(),
y: 1,
marker: {
fillColor: statuses[event.message].color
}
})
})
$("#email_chart_legend").html("")
$.each(email_series_data, function(status, count) {
email_data.series.push({
meta: status,
value: count
})
$("#email_chart_legend").append('<li><span class="' + statuses[status].legend + '"></span>' + status + '</li>')
renderTimelineChart({
data: timeline_series_data
})
var timeline_chart = new Chartist.Line('#timeline_chart', timeline_data, timeline_opts)
timeline_chart.on('draw', function(data) {
if (data.type === "point") {
var point_style = statuses[campaign.timeline[data.meta].message].point
var circle = new Chartist.Svg("circle", {
cx: [data.x],
cy: [data.y],
r: 5,
fill: "#283F50",
meta: data.meta,
value: 1,
}, point_style + ' ct-timeline-point')
data.element.replace(circle)
}
})
// Setup the overview chart listeners
$chart = $("#timeline_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-timeline-point', function() {
var $point = $(this)
cidx = $point.attr('meta')
html = "Event: " + campaign.timeline[cidx].message
if (campaign.timeline[cidx].email) {
html += '<br>' + "Email: " + escapeHtml(campaign.timeline[cidx].email)
$.each(email_series_data, function (status, count) {
var email_data = []
if (!(status in statusMapping)) {
return true
}
$toolTip.html(html).show()
});
$chart.on('mouseleave', '.ct-timeline-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 70 || event.originalEvent.layerY) - $toolTip.height() - 40
});
});
var email_chart = new Chartist.Pie("#email_chart", email_data, email_opts)
email_chart.on('draw', function(data) {
data.element.addClass(statuses[data.meta].slice)
email_data.push({
name: status,
y: count
})
// Setup the average chart listeners
$piechart = $("#email_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString()).show();
});
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
});
});
email_data.push({
name: '',
y: campaign.results.length - count
})
var chart = renderPieChart({
elemId: statusMapping[status] + '_chart',
title: status,
name: status,
data: email_data,
colors: [statuses[status].color, '#dddddd']
})
})
if (!map) {
map = new Datamap({
element: document.getElementById("resultsMap"),
@ -627,13 +713,13 @@ function load() {
}
});
}
$.each(campaign.results, function(i, result) {
$.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) {
$.each(bubbles, function (i, bubble) {
if (bubble.ip == result.ip) {
bubbles[i].radius += 1
newIP = false
@ -653,7 +739,7 @@ function load() {
map.bubbles(bubbles)
}
// Load up the map data (only once!)
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e) {
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if ($(e.target).attr('href') == "#overview") {
if (!map) {
map = new Datamap({
@ -671,7 +757,7 @@ function load() {
}
})
})
.error(function() {
.error(function () {
$("#loading").hide()
errorFlash(" Campaign not found!")
})
@ -691,10 +777,14 @@ function refresh() {
$(document).ready(function() {
$(document).ready(function () {
Highcharts.setOptions({
global: {
useUTC: false
}
})
load();
// Start the polling loop
// Start the polling loop
setRefresh = setTimeout(refresh, 60000)
})

+ 205
- 150
static/js/src/app/dashboard.js View File

@ -3,15 +3,13 @@ var campaigns = []
// statuses is a helper map to point result statuses to ui classes
var statuses = {
"Email Sent": {
slice: "ct-slice-donut-sent",
legend: "ct-legend-sent",
color: "#1abc9c",
label: "label-success",
icon: "fa-envelope",
point: "ct-point-sent"
},
"Emails Sent": {
slice: "ct-slice-donut-sent",
legend: "ct-legend-sent",
color: "#1abc9c",
label: "label-success",
icon: "fa-envelope",
point: "ct-point-sent"
@ -26,57 +24,49 @@ var statuses = {
label: "label-success"
},
"Email Opened": {
slice: "ct-slice-donut-opened",
legend: "ct-legend-opened",
color: "#f9bf3b",
label: "label-warning",
icon: "fa-envelope",
point: "ct-point-opened"
},
"Clicked Link": {
slice: "ct-slice-donut-clicked",
legend: "ct-legend-clicked",
color: "#F39C12",
label: "label-clicked",
icon: "fa-mouse-pointer",
point: "ct-point-clicked"
},
"Success": {
slice: "ct-slice-donut-success",
legend: "ct-legend-success",
color: "#f05b4f",
label: "label-danger",
icon: "fa-exclamation",
point: "ct-point-clicked"
},
"Error": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-times",
point: "ct-point-error"
},
"Error Sending Email": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-times",
point: "ct-point-error"
},
"Submitted Data": {
slice: "ct-slice-donut-success",
legend: "ct-legend-success",
color: "#f05b4f",
label: "label-danger",
icon: "fa-exclamation",
point: "ct-point-clicked"
},
"Unknown": {
slice: "ct-slice-donut-error",
legend: "ct-legend-error",
color: "#6c7a89",
label: "label-default",
icon: "fa-question",
point: "ct-point-error"
},
"Sending": {
slice: "ct-slice-donut-sending",
legend: "ct-legend-sending",
color: "#428bca",
label: "label-primary",
icon: "fa-spinner",
point: "ct-point-sending"
@ -92,34 +82,80 @@ var statsMapping = {
"opened": "Email Opened",
"clicked": "Clicked Link",
"submitted_data": "Submitted Data",
"error": "Error"
}
function deleteCampaign(idx) {
if (confirm("Delete " + campaigns[idx].name + "?")) {
api.campaignId.delete(campaigns[idx].id)
.success(function(data) {
.success(function (data) {
successFlash(data.message)
location.reload()
})
}
}
function generateStatsPieChart(campaigns) {
var stats_opts = {
donut: true,
donutWidth: 40,
chartPadding: 0,
showLabel: false
}
/* Renders a pie chart using the provided chartops */
function renderPieChart(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 '<span style="color:' + this.color + '">\u25CF</span>' + this.point.name + ': <b>' + this.y + '%</b><br/>'
}
},
series: [{
data: chartopts['data'],
colors: chartopts['colors'],
}]
})
}
function generateStatsPieCharts(campaigns) {
var stats_data = []
var stats_series_data = {}
var stats_data = {
series: []
}
var total = 0
$.each(campaigns, function(i, campaign) {
$.each(campaign.stats, function(status, count) {
$.each(campaigns, function (i, campaign) {
$.each(campaign.stats, function (status, count) {
if (status == "total") {
total += count
return true
@ -131,153 +167,172 @@ function generateStatsPieChart(campaigns) {
}
})
})
$.each(stats_series_data, function(status, count) {
$.each(stats_series_data, function (status, count) {
// I don't like this, but I guess it'll have to work.
// Turns submitted_data into Submitted Data
if (!(status in statsMapping)) {
return true
}
status_label = statsMapping[status]
stats_data.series.push({
meta: status_label,
value: Math.floor((count / total) * 100)
stats_data.push({
name: status_label,
y: Math.floor((count / total) * 100),
count: count
})
$("#stats_chart_legend").append('<li><span class="' + statuses[status_label].legend + '"></span>' + status_label + '</li>')
})
var stats_chart = new Chartist.Pie("#stats_chart", stats_data, stats_opts)
$piechart = $("#stats_chart")
var $pietoolTip = $piechart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$piechart.get(0).__chartist__.on('draw', function(data) {
data.element.addClass(statuses[data.meta].slice)
stats_data.push({
name: '',
y: 100 - Math.floor((count / total) * 100)
})
// Update with the latest data
$piechart.get(0).__chartist__.update(stats_data)
$piechart.on('mouseenter', '.ct-slice-donut', function() {
var $point = $(this)
value = $point.attr('ct:value')
label = $point.attr('ct:meta')
$pietoolTip.html(label + ': ' + value.toString() + "%").show();
var stats_chart = renderPieChart({
elemId: status + '_chart',
title: status_label,
name: status,
data: stats_data,
colors: [statuses[status_label].color, "#dddddd"]
})
stats_data = []
});
}
$piechart.on('mouseleave', '.ct-slice-donut', function() {
$pietoolTip.hide();
});
$piechart.on('mousemove', function(event) {
$pietoolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80
});
});
function generateTimelineChart(campaigns) {
var overview_data = []
$.each(campaigns, function (i, campaign) {
var campaign_date = moment(campaign.created_date)
// Add it to the chart data
campaign.y = 0
// Clicked events also contain our data submitted events
campaign.y += campaign.stats.clicked
campaign.y = Math.floor((campaign.y / campaign.stats.total) * 100)
// Add the data to the overview chart
overview_data.push({
campaign_id: campaign.id,
name: campaign.name,
x: campaign_date.valueOf(),
y: campaign.y
})
})
Highcharts.chart('overview_chart', {
chart: {
zoomType: 'x',
type: 'areaspline'
},
title: {
text: 'Phishing Success Overview'
},
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: 100,
title: {
text: "% of Success"
}
},
tooltip: {
formatter: function () {
return Highcharts.dateFormat('%A, %b %d %l:%M:%S %P', new Date(this.x)) +
'<br>' + this.point.name + '<br>% Success: <b>' + this.y + '%</b>'
}
},
legend: {
enabled: false
},
plotOptions: {
series: {
marker: {
enabled: true,
symbol: 'circle',
radius: 3
},
cursor: 'pointer',
point: {
events: {
click: function (e) {
window.location.href = "/campaigns/" + this.campaign_id
}
}
}
}
},
credits: {
enabled: false
},
series: [{
data: overview_data,
color: "#f05b4f",
fillOpacity: 0.5
}]
})
}
$(document).ready(function() {
$(document).ready(function () {
api.campaigns.summary()
.success(function(data) {
.success(function (data) {
$("#loading").hide()
campaigns = data.campaigns
if (campaigns.length > 0) {
$("#dashboard").show()
// Create the overview chart data
var overview_data = {
labels: [],
series: [
[]
]
}
var overview_opts = {
axisX: {
showGrid: false
},
showArea: true,
plugins: [],
low: 0,
high: 100
}
// Create the overview chart data
campaignTable = $("#campaignTable").DataTable({
columnDefs: [{
orderable: false,
targets: "no-sort"
}],
},
{ className: "color-sent", targets: [2] },
{ className: "color-opened", targets: [3] },
{ className: "color-clicked", targets: [4] },
{ className: "color-success", targets: [5] }],
order: [
[1, "desc"]
]
});
$.each(campaigns, function(i, campaign) {
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a')
var label = statuses[campaign.status].label || "label-default";
//section for tooltips on the status of a campaign to show some quick stats
var launchDate;
if (moment(campaign.launch_date).isAfter(moment())) {
launchDate = "Scheduled to start: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
}
// Add it to the table
campaignTable.row.add([
escapeHtml(campaign.name),
campaign_date,
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
$.each(campaigns, function (i, campaign) {
var campaign_date = moment(campaign.created_date).format('MMMM Do YYYY, h:mm:ss a')
var label = statuses[campaign.status].label || "label-default";
//section for tooltips on the status of a campaign to show some quick stats
var launchDate;
if (moment(campaign.launch_date).isAfter(moment())) {
launchDate = "Scheduled to start: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total
} else {
launchDate = "Launch Date: " + moment(campaign.launch_date).format('MMMM Do YYYY, h:mm:ss a')
var quickStats = launchDate + "<br><br>" + "Number of recipients: " + campaign.stats.total + "<br><br>" + "Emails opened: " + campaign.stats.opened + "<br><br>" + "Emails clicked: " + campaign.stats.clicked + "<br><br>" + "Submitted Credentials: " + campaign.stats.submitted_data + "<br><br>" + "Errors : " + campaign.stats.error
}
// Add it to the table
campaignTable.row.add([
escapeHtml(campaign.name),
campaign_date,
campaign.stats.sent,
campaign.stats.opened,
campaign.stats.clicked,
campaign.stats.submitted_data,
"<span class=\"label " + label + "\" data-toggle=\"tooltip\" data-placement=\"right\" data-html=\"true\" title=\"" + quickStats + "\">" + campaign.status + "</span>",
"<div class='pull-right'><a class='btn btn-primary' href='/campaigns/" + campaign.id + "' data-toggle='tooltip' data-placement='left' title='View Results'>\
<i class='fa fa-bar-chart'></i>\
</a>\
<button class='btn btn-danger' onclick='deleteCampaign(" + i + ")' data-toggle='tooltip' data-placement='left' title='Delete Campaign'>\
<i class='fa fa-trash-o'></i>\
</button></div>"
]).draw()
$('[data-toggle="tooltip"]').tooltip()
// Add it to the chart data
campaign.y = 0
campaign.y += campaign.stats.clicked + campaign.stats.submitted_data
campaign.y = Math.floor((campaign.y / campaign.stats.total) * 100)
// Add the data to the overview chart
overview_data.labels.push(campaign_date)
overview_data.series[0].push({
meta: i,
value: campaign.y
})
})
// Build the charts
generateStatsPieChart(campaigns)
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts)
// Setup the overview chart listeners
$chart = $("#overview_chart")
var $toolTip = $chart
.append('<div class="chartist-tooltip"></div>')
.find('.chartist-tooltip')
.hide();
$chart.on('mouseenter', '.ct-point', function() {
var $point = $(this)
value = $point.attr('ct:value') || 0
cidx = $point.attr('ct:meta')
$toolTip.html(campaigns[cidx].name + '<br>' + "Successes: " + value.toString() + "%").show();
});
$chart.on('mouseleave', '.ct-point', function() {
$toolTip.hide();
});
$chart.on('mousemove', function(event) {
$toolTip.css({
left: (event.offsetX || event.originalEvent.layerX) - $toolTip.width() / 2 - 10,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $toolTip.height() - 40
});
});
$("#overview_chart").on("click", ".ct-point", function(e) {
var $cidx = $(this).attr('ct:meta');
window.location.href = "/campaigns/" + campaigns[cidx].id
});