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

703-submit-credentials
Jordan Wright 2017-08-05 21:11:50 -05:00
parent 972c40fd87
commit 75600f5812
20 changed files with 3444 additions and 1262 deletions

View File

@ -5,11 +5,9 @@
*/ */
var gulp = require('gulp'), var gulp = require('gulp'),
jshint = require('gulp-jshint'),
rename = require('gulp-rename'), rename = require('gulp-rename'),
concat = require('gulp-concat'), concat = require('gulp-concat'),
uglify = require('gulp-uglify'), uglify = require('gulp-uglify'),
wrap = require('gulp-wrap'),
cleanCSS = require('gulp-clean-css'), cleanCSS = require('gulp-clean-css'),
js_directory = 'static/js/src/', js_directory = 'static/js/src/',
@ -19,17 +17,9 @@ var gulp = require('gulp'),
dest_js_directory = 'static/js/dist/', dest_js_directory = 'static/js/dist/',
dest_css_directory = 'static/css/dist/'; dest_css_directory = 'static/css/dist/';
gulp.task('default', ['watch']); gulp.task('vendorjs', function () {
gulp.task('jshint', function() {
return gulp.src(js_directory)
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'));
});
gulp.task('build', function() {
// Vendor minifying / concat // Vendor minifying / concat
gulp.src([ return gulp.src([
vendor_directory + 'jquery.js', vendor_directory + 'jquery.js',
vendor_directory + 'bootstrap.min.js', vendor_directory + 'bootstrap.min.js',
vendor_directory + 'moment.min.js', vendor_directory + 'moment.min.js',
@ -47,7 +37,8 @@ gulp.task('build', function() {
vendor_directory + 'sweetalert2.min.js', vendor_directory + 'sweetalert2.min.js',
vendor_directory + 'bootstrap-datetime.js', vendor_directory + 'bootstrap-datetime.js',
vendor_directory + 'select2.min.js', vendor_directory + 'select2.min.js',
vendor_directory + 'core.min.js' vendor_directory + 'core.min.js',
vendor_directory + 'highcharts.js'
]) ])
.pipe(concat('vendor.js')) .pipe(concat('vendor.js'))
.pipe(rename({ .pipe(rename({
@ -55,7 +46,9 @@ gulp.task('build', function() {
})) }))
.pipe(uglify()) .pipe(uglify())
.pipe(gulp.dest(dest_js_directory)); .pipe(gulp.dest(dest_js_directory));
})
gulp.task('scripts', function () {
// Gophish app files // Gophish app files
gulp.src(app_directory) gulp.src(app_directory)
.pipe(rename({ .pipe(rename({
@ -65,7 +58,9 @@ gulp.task('build', function() {
console.log(e); console.log(e);
})) }))
.pipe(gulp.dest(dest_js_directory + 'app/')); .pipe(gulp.dest(dest_js_directory + 'app/'));
})
gulp.task('styles', function() {
return gulp.src([ return gulp.src([
css_directory + 'bootstrap.min.css', css_directory + 'bootstrap.min.css',
css_directory + 'main.css', css_directory + 'main.css',
@ -78,13 +73,13 @@ gulp.task('build', function() {
css_directory + 'checkbox.css', css_directory + 'checkbox.css',
css_directory + 'sweetalert2.min.css', css_directory + 'sweetalert2.min.css',
css_directory + 'select2.min.css', css_directory + 'select2.min.css',
css_directory + 'select2-bootstrap.min.css' css_directory + 'select2-bootstrap.min.css',
]) ])
.pipe(cleanCSS({ compatibilty: 'ie9' })) .pipe(cleanCSS({ compatibilty: 'ie9' }))
.pipe(concat('gophish.css')) .pipe(concat('gophish.css'))
.pipe(gulp.dest(dest_css_directory)); .pipe(gulp.dest(dest_css_directory));
}); })
gulp.task('watch', function() { gulp.task('build', ['vendorjs', 'scripts', 'styles']);
gulp.watch('static/js/src/app/**/*.js', ['jshint']);
}); gulp.task('default', ['build']);

View File

@ -198,6 +198,7 @@ func (c *Campaign) getDetails() error {
} }
// getCampaignStats returns a CampaignStats object for the campaign with the given campaign ID. // 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) { func getCampaignStats(cid int64) (CampaignStats, error) {
s := CampaignStats{} s := CampaignStats{}
query := db.Table("results").Where("campaign_id = ?", cid) query := db.Table("results").Where("campaign_id = ?", cid)
@ -205,11 +206,7 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil { if err != nil {
return s, err 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
if err != nil { if err != nil {
return s, err return s, err
} }
@ -217,10 +214,20 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
if err != nil { if err != nil {
return s, err return s, err
} }
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData) // 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 { if err != nil {
return s, err return s, err
} }
// 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 err = query.Where("status=?", ERROR).Count(&s.Error).Error
return s, err return s, err
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "gophish", "name": "gophish",
"version": "0.3.0-dev", "version": "0.4.0-dev",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/gophish/gophish.git" "url": "git+https://github.com/gophish/gophish.git"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

22
static/css/main.css vendored
View File

@ -541,3 +541,25 @@ table.dataTable{
.input-group-btn .btn { .input-group-btn .btn {
line-height:20px !important; 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;
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 357 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,64 +4,70 @@ 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": {
slice: "ct-slice-donut-sent", color: "#1abc9c",
legend: "ct-legend-sent",
label: "label-success", label: "label-success",
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-sent" 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": { "Email Opened": {
slice: "ct-slice-donut-opened", color: "#f9bf3b",
legend: "ct-legend-opened",
label: "label-warning", label: "label-warning",
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-opened" point: "ct-point-opened"
}, },
"Clicked Link": { "Clicked Link": {
slice: "ct-slice-donut-clicked", color: "#F39C12",
legend: "ct-legend-clicked",
label: "label-clicked", label: "label-clicked",
icon: "fa-mouse-pointer", icon: "fa-mouse-pointer",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Success": { "Success": {
slice: "ct-slice-donut-success", color: "#f05b4f",
legend: "ct-legend-success",
label: "label-danger", label: "label-danger",
icon: "fa-exclamation", icon: "fa-exclamation",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Error": { "Error": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-times", icon: "fa-times",
point: "ct-point-error" point: "ct-point-error"
}, },
"Error Sending Email": { "Error Sending Email": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-times", icon: "fa-times",
point: "ct-point-error" point: "ct-point-error"
}, },
"Submitted Data": { "Submitted Data": {
slice: "ct-slice-donut-success", color: "#f05b4f",
legend: "ct-legend-success",
label: "label-danger", label: "label-danger",
icon: "fa-exclamation", icon: "fa-exclamation",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Unknown": { "Unknown": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-question", icon: "fa-question",
point: "ct-point-error" point: "ct-point-error"
}, },
"Sending": { "Sending": {
slice: "ct-slice-donut-sending", color: "#428bca",
legend: "ct-legend-sending",
label: "label-primary", label: "label-primary",
icon: "fa-spinner", icon: "fa-spinner",
point: "ct-point-sending" 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 campaign = {}
var bubbles = [] var bubbles = []
@ -93,6 +115,7 @@ function deleteCampaign() {
confirmButtonColor: "#428bca", confirmButtonColor: "#428bca",
reverseButtons: true, reverseButtons: true,
allowOutsideClick: false, allowOutsideClick: false,
showLoaderOnConfirm: true,
preConfirm: function () { preConfirm: function () {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
api.campaignId.delete(campaign.id) api.campaignId.delete(campaign.id)
@ -128,6 +151,7 @@ function completeCampaign() {
confirmButtonColor: "#428bca", confirmButtonColor: "#428bca",
reverseButtons: true, reverseButtons: true,
allowOutsideClick: false, allowOutsideClick: false,
showLoaderOnConfirm: true,
preConfirm: function () { preConfirm: function () {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
api.campaignId.complete(campaign.id) api.campaignId.complete(campaign.id)
@ -296,6 +320,131 @@ function renderTimeline(data) {
return 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)) +
'<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 /* poll - Queries the API and updates the UI with the results
* *
@ -310,51 +459,64 @@ function poll() {
.success(function (c) { .success(function (c) {
campaign = c campaign = c
/* Update the timeline */ /* Update the timeline */
var timeline_data = { var timeline_series_data = []
series: [{
name: "Events",
data: []
}]
}
$.each(campaign.timeline, function (i, event) { $.each(campaign.timeline, function (i, event) {
timeline_data.series[0].data.push({ var event_date = moment(event.time)
meta: i, timeline_series_data.push({
x: new Date(event.time), email: event.email,
x: event_date.valueOf(),
y: 1 y: 1
}) })
}) })
var timeline_chart = $("#timeline_chart") var timeline_series_data = []
if (timeline_chart.get(0).__chartist__) { $.each(campaign.timeline, function (i, event) {
timeline_chart.get(0).__chartist__.update(timeline_data) 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 */ /* Update the results donut chart */
var email_data = {
series: []
}
var email_series_data = {} 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) { $.each(campaign.results, function (i, result) {
if (!email_series_data[result.status]) {
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++; 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) { $.each(email_series_data, function (status, count) {
email_data.series.push({ var email_data = []
meta: status, if (!(status in statusMapping)) {
value: count return true
})
$("#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)
} }
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
})
})
/* Update the datatable */ /* Update the datatable */
resultsTable = $("#resultsTable").DataTable() resultsTable = $("#resultsTable").DataTable()
resultsTable.rows().every(function (i, tableLoop, rowLoop) { resultsTable.rows().every(function (i, tableLoop, rowLoop) {
@ -436,42 +598,6 @@ function load() {
payloadResults.show() 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 the results table // Setup the results table
resultsTable = $("#resultsTable").DataTable({ resultsTable = $("#resultsTable").DataTable({
destroy: true, destroy: true,
@ -490,6 +616,11 @@ function load() {
}] }]
}); });
resultsTable.clear(); 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) { $.each(campaign.results, function (i, result) {
label = statuses[result.status].label || "label-default"; label = statuses[result.status].label || "label-default";
resultsTable.row.add([ resultsTable.row.add([
@ -501,10 +632,11 @@ function load() {
escapeHtml(result.position) || "", escapeHtml(result.position) || "",
"<span class=\"label " + label + "\">" + result.status + "</span>" "<span class=\"label " + label + "\">" + result.status + "</span>"
]).draw() ]).draw()
if (!email_series_data[result.status]) {
email_series_data[result.status] = 1
} else {
email_series_data[result.status]++; 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 // Setup the individual timelines
@ -529,87 +661,41 @@ function load() {
}); });
// Setup the graphs // Setup the graphs
$.each(campaign.timeline, function (i, event) { $.each(campaign.timeline, function (i, event) {
timeline_data.series[0].data.push({ var event_date = moment(event.time)
meta: i, timeline_series_data.push({
x: new Date(event.time), email: event.email,
y: 1 message: event.message,
x: event_date.valueOf(),
y: 1,
marker: {
fillColor: statuses[event.message].color
}
}) })
}) })
$("#email_chart_legend").html("") renderTimelineChart({
data: timeline_series_data
})
$.each(email_series_data, function (status, count) { $.each(email_series_data, function (status, count) {
email_data.series.push({ var email_data = []
meta: status, if (!(status in statusMapping)) {
value: count return true
})
$("#email_chart_legend").append('<li><span class="' + statuses[status].legend + '"></span>' + status + '</li>')
})
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)
} }
email_data.push({
name: status,
y: count
})
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']
}) })
// 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)
}
$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)
}) })
// 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
});
});
if (!map) { if (!map) {
map = new Datamap({ map = new Datamap({
element: document.getElementById("resultsMap"), element: document.getElementById("resultsMap"),
@ -692,8 +778,12 @@ function refresh() {
$(document).ready(function () { $(document).ready(function () {
Highcharts.setOptions({
global: {
useUTC: false
}
})
load(); load();
// Start the polling loop
// Start the polling loop // Start the polling loop
setRefresh = setTimeout(refresh, 60000) setRefresh = setTimeout(refresh, 60000)

View File

@ -3,15 +3,13 @@ var campaigns = []
// 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": {
slice: "ct-slice-donut-sent", color: "#1abc9c",
legend: "ct-legend-sent",
label: "label-success", label: "label-success",
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-sent" point: "ct-point-sent"
}, },
"Emails Sent": { "Emails Sent": {
slice: "ct-slice-donut-sent", color: "#1abc9c",
legend: "ct-legend-sent",
label: "label-success", label: "label-success",
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-sent" point: "ct-point-sent"
@ -26,57 +24,49 @@ var statuses = {
label: "label-success" label: "label-success"
}, },
"Email Opened": { "Email Opened": {
slice: "ct-slice-donut-opened", color: "#f9bf3b",
legend: "ct-legend-opened",
label: "label-warning", label: "label-warning",
icon: "fa-envelope", icon: "fa-envelope",
point: "ct-point-opened" point: "ct-point-opened"
}, },
"Clicked Link": { "Clicked Link": {
slice: "ct-slice-donut-clicked", color: "#F39C12",
legend: "ct-legend-clicked",
label: "label-clicked", label: "label-clicked",
icon: "fa-mouse-pointer", icon: "fa-mouse-pointer",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Success": { "Success": {
slice: "ct-slice-donut-success", color: "#f05b4f",
legend: "ct-legend-success",
label: "label-danger", label: "label-danger",
icon: "fa-exclamation", icon: "fa-exclamation",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Error": { "Error": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-times", icon: "fa-times",
point: "ct-point-error" point: "ct-point-error"
}, },
"Error Sending Email": { "Error Sending Email": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-times", icon: "fa-times",
point: "ct-point-error" point: "ct-point-error"
}, },
"Submitted Data": { "Submitted Data": {
slice: "ct-slice-donut-success", color: "#f05b4f",
legend: "ct-legend-success",
label: "label-danger", label: "label-danger",
icon: "fa-exclamation", icon: "fa-exclamation",
point: "ct-point-clicked" point: "ct-point-clicked"
}, },
"Unknown": { "Unknown": {
slice: "ct-slice-donut-error", color: "#6c7a89",
legend: "ct-legend-error",
label: "label-default", label: "label-default",
icon: "fa-question", icon: "fa-question",
point: "ct-point-error" point: "ct-point-error"
}, },
"Sending": { "Sending": {
slice: "ct-slice-donut-sending", color: "#428bca",
legend: "ct-legend-sending",
label: "label-primary", label: "label-primary",
icon: "fa-spinner", icon: "fa-spinner",
point: "ct-point-sending" point: "ct-point-sending"
@ -92,7 +82,6 @@ var statsMapping = {
"opened": "Email Opened", "opened": "Email Opened",
"clicked": "Clicked Link", "clicked": "Clicked Link",
"submitted_data": "Submitted Data", "submitted_data": "Submitted Data",
"error": "Error"
} }
function deleteCampaign(idx) { function deleteCampaign(idx) {
@ -105,17 +94,64 @@ function deleteCampaign(idx) {
} }
} }
function generateStatsPieChart(campaigns) { /* Renders a pie chart using the provided chartops */
var stats_opts = { function renderPieChart(chartopts) {
donut: true, return Highcharts.chart(chartopts['elemId'], {
donutWidth: 40, chart: {
chartPadding: 0, type: 'pie',
showLabel: false 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_series_data = {}
var stats_data = {
series: []
}
var total = 0 var total = 0
$.each(campaigns, function (i, campaign) { $.each(campaigns, function (i, campaign) {
@ -134,44 +170,108 @@ 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. // I don't like this, but I guess it'll have to work.
// Turns submitted_data into Submitted Data // Turns submitted_data into Submitted Data
if (!(status in statsMapping)) {
return true
}
status_label = statsMapping[status] status_label = statsMapping[status]
stats_data.series.push({ stats_data.push({
meta: status_label, name: status_label,
value: Math.floor((count / total) * 100) y: Math.floor((count / total) * 100),
count: count
}) })
$("#stats_chart_legend").append('<li><span class="' + statuses[status_label].legend + '"></span>' + status_label + '</li>') stats_data.push({
name: '',
y: 100 - Math.floor((count / total) * 100)
}) })
var stats_chart = renderPieChart({
var stats_chart = new Chartist.Pie("#stats_chart", stats_data, stats_opts) elemId: status + '_chart',
title: status_label,
$piechart = $("#stats_chart") name: status,
var $pietoolTip = $piechart data: stats_data,
.append('<div class="chartist-tooltip"></div>') colors: [statuses[status_label].color, "#dddddd"]
.find('.chartist-tooltip')
.hide();
$piechart.get(0).__chartist__.on('draw', function(data) {
data.element.addClass(statuses[data.meta].slice)
}) })
// Update with the latest data stats_data = []
$piechart.get(0).__chartist__.update(stats_data) });
}
$piechart.on('mouseenter', '.ct-slice-donut', function() { function generateTimelineChart(campaigns) {
var $point = $(this) var overview_data = []
value = $point.attr('ct:value') $.each(campaigns, function (i, campaign) {
label = $point.attr('ct:meta') var campaign_date = moment(campaign.created_date)
$pietoolTip.html(label + ': ' + value.toString() + "%").show(); // Add it to the chart data
}); campaign.y = 0
// Clicked events also contain our data submitted events
$piechart.on('mouseleave', '.ct-slice-donut', function() { campaign.y += campaign.stats.clicked
$pietoolTip.hide(); campaign.y = Math.floor((campaign.y / campaign.stats.total) * 100)
}); // Add the data to the overview chart
$piechart.on('mousemove', function(event) { overview_data.push({
$pietoolTip.css({ campaign_id: campaign.id,
left: (event.offsetX || event.originalEvent.layerX) - $pietoolTip.width() / 2 - 10, name: campaign.name,
top: (event.offsetY + 40 || event.originalEvent.layerY) - $pietoolTip.height() - 80 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 () {
@ -182,26 +282,15 @@ $(document).ready(function() {
if (campaigns.length > 0) { if (campaigns.length > 0) {
$("#dashboard").show() $("#dashboard").show()
// Create the overview chart data // Create the overview chart data
var overview_data = {
labels: [],
series: [
[]
]
}
var overview_opts = {
axisX: {
showGrid: false
},
showArea: true,
plugins: [],
low: 0,
high: 100
}
campaignTable = $("#campaignTable").DataTable({ campaignTable = $("#campaignTable").DataTable({
columnDefs: [{ columnDefs: [{
orderable: false, orderable: false,
targets: "no-sort" targets: "no-sort"
}], },
{ className: "color-sent", targets: [2] },
{ className: "color-opened", targets: [3] },
{ className: "color-clicked", targets: [4] },
{ className: "color-success", targets: [5] }],
order: [ order: [
[1, "desc"] [1, "desc"]
] ]
@ -222,6 +311,10 @@ $(document).ready(function() {
campaignTable.row.add([ campaignTable.row.add([
escapeHtml(campaign.name), escapeHtml(campaign.name),
campaign_date, 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>", "<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'>\ "<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>\ <i class='fa fa-bar-chart'></i>\
@ -231,48 +324,10 @@ $(document).ready(function() {
</button></div>" </button></div>"
]).draw() ]).draw()
$('[data-toggle="tooltip"]').tooltip() $('[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 // Build the charts
generateStatsPieChart(campaigns) generateStatsPieCharts(campaigns)
var overview_chart = new Chartist.Line('#overview_chart', overview_data, overview_opts) generateTimelineChart(campaigns)
// 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
});
} else { } else {
$("#emptyMessage").show() $("#emptyMessage").show()
} }

View File

@ -17,7 +17,9 @@
</li> </li>
<li><a href="/settings">Settings</a> <li><a href="/settings">Settings</a>
</li> </li>
<li><hr></li> <li>
<hr>
</li>
<li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a> <li><a href="https://gophish.gitbooks.io/user-guide/content/">User Guide</a>
</li> </li>
<li><a href="/api/">API Documentation</a> <li><a href="/api/">API Documentation</a>
@ -39,7 +41,8 @@
<i class="fa fa-arrow-circle-o-left fa-lg"></i> Back <i class="fa fa-arrow-circle-o-left fa-lg"></i> Back
</a> </a>
<div class="btn-group"> <div class="btn-group">
<button type="button" id="exportButton" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button type="button" id="exportButton" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<i class="fa fa-file-excel-o"></i> Export CSV <i class="fa fa-file-excel-o"></i> Export CSV
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</button> </button>
@ -65,25 +68,20 @@
<div class="row"> <div class="row">
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li class="active"><a href="#overview" aria-controls="home" role="tab" data-toggle="tab">Overview</a></li> <li class="active"><a href="#overview" aria-controls="home" role="tab" data-toggle="tab">Overview</a></li>
<!--<li><a href="#plugins" aria-controls="profile" role="tab" data-toggle="tab">Plugins</a></li>
<li><a href="#demographics" aria-controls="settings" role="tab" data-toggle="tab">Demographics</a></li>-->
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="overview"> <div role="tabpanel" class="tab-pane active" id="overview">
<br/> <br/>
<div class="row"> <div class="row">
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<p style="text-align:center;">Campaign Timeline</p>
<br/>
<div id="timeline_chart"></div> <div id="timeline_chart"></div>
</div> </div>
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<p style="text-align:center;">Email Status</p>
<div id="email_chart" class="col-lg-7 col-md-7"></div>
<div class="col-lg-5 col-md-5">
<ul id="email_chart_legend" class="chartist-legend">
</ul>
</div> </div>
<div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -93,12 +91,6 @@
</div> </div>
</div> </div>
</div> </div>
<!--
<div role="tabpanel" class="tab-pane" id="plugins">
</div>
<div role="tabpanel" class="tab-pane" id="demographics">
Demographics here
</div> -->
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@ -122,7 +114,6 @@
</div> </div>
<div id="flashes" class="row"></div> <div id="flashes" class="row"></div>
</div> </div>
{{end}} {{end}} {{define "scripts"}}
{{define "scripts"}}
<script src="/js/dist/app/campaign_results.min.js"></script> <script src="/js/dist/app/campaign_results.min.js"></script>
{{end}} {{end}}

View File

@ -38,19 +38,15 @@
No campaigns created yet. Let's create one! No campaigns created yet. Let's create one!
</div> </div>
</div> </div>
<div id="dashboard" style="display:none;"> <div id="dashboard">
<div class="row"> <div class="row">
<div id="overview_chart" class="col-lg-6 col-md-6 col-sm-12 col-xs-12"> <div id="overview_chart" style="height:200px;" class="col-lg-12 col-md-12 col-sm-12 col-xs-12"></div>
<p style="text-align:center;">Phishing Success Overview</p>
</div>
<div class="col-lg-6 col-md-6 col-sm-12 col-xs-12">
<p style="text-align:center;">Average Phishing Results</p>
<div id="stats_chart" class="col-lg-7 col-md-7"></div>
<div class="col-lg-5 col-md-5">
<ul id="stats_chart_legend" class="chartist-legend">
</ul>
</div>
</div> </div>
<div class="row">
<div id="sent_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="opened_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="clicked_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
<div id="submitted_data_chart" style="height:200px;" class="col-lg-3 col-md-3"></div>
</div> </div>
<div class="row"> <div class="row">
<h2>Recent Campaigns</h2> <h2>Recent Campaigns</h2>
@ -63,10 +59,14 @@
<table id="campaignTable" class="table"> <table id="campaignTable" class="table">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th class="col-md-2 col-sm-2">Name</th>
<th>Created Date</th> <th class="col-md-2 col-sm-2">Created Date</th>
<th>Status</th> <th class="col-md-1 col-sm-1"><i class="fa fa-envelope-o"></i></th>
<th class="col-md-2 col-sm-2 no-sort"></th> <th class="col-md-1 col-sm-1"><i class="fa fa-envelope-open-o"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-mouse-pointer"></i></th>
<th class="col-md-1 col-sm-1"><i class="fa fa-exclamation-circle"></i></th>
<th class="col-md-1 col-sm-1">Status</th>
<th class="col-md-2 col-sm-2 no-sort"></i></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>