mirror of https://github.com/gophish/gophish
Merge pull request #322 from gophish/290-stop-campaign
Implement the ability to complete a campaign. Fixes #290pull/324/head
commit
8cfdd07663
|
@ -12,3 +12,4 @@ static/js/moment.min.js linguist-vendored
|
||||||
static/js/jquery* linguist-vendored
|
static/js/jquery* linguist-vendored
|
||||||
static/js/ui-bootstrap-* linguist-vendored
|
static/js/ui-bootstrap-* linguist-vendored
|
||||||
static/js/datatables* linguist-vendored
|
static/js/datatables* linguist-vendored
|
||||||
|
static/js/sweetalert2.min.js linguist-vendored
|
||||||
|
|
|
@ -128,6 +128,22 @@ func API_Campaigns_Id_Results(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API_Campaigns_Id_Complete effectively "ends" a campaign.
|
||||||
|
// Future phishing emails clicked will return a simple "404" page.
|
||||||
|
func API_Campaigns_Id_Complete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
id, _ := strconv.ParseInt(vars["id"], 0, 64)
|
||||||
|
switch {
|
||||||
|
case r.Method == "GET":
|
||||||
|
err := models.CompleteCampaign(id, ctx.Get(r, "user_id").(int64))
|
||||||
|
if err != nil {
|
||||||
|
JSONResponse(w, models.Response{Success: false, Message: "Error completing campaign"}, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
JSONResponse(w, models.Response{Success: true, Message: "Campaign completed successfully!"}, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// API_Groups returns a list of groups if requested via GET.
|
// API_Groups returns a list of groups if requested via GET.
|
||||||
// If requested via POST, API_Groups creates a new group and returns a reference to it.
|
// If requested via POST, API_Groups creates a new group and returns a reference to it.
|
||||||
func API_Groups(w http.ResponseWriter, r *http.Request) {
|
func API_Groups(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ func CreateAdminRouter() http.Handler {
|
||||||
api.HandleFunc("/campaigns/", Use(API_Campaigns, mid.RequireAPIKey))
|
api.HandleFunc("/campaigns/", Use(API_Campaigns, mid.RequireAPIKey))
|
||||||
api.HandleFunc("/campaigns/{id:[0-9]+}", Use(API_Campaigns_Id, mid.RequireAPIKey))
|
api.HandleFunc("/campaigns/{id:[0-9]+}", Use(API_Campaigns_Id, mid.RequireAPIKey))
|
||||||
api.HandleFunc("/campaigns/{id:[0-9]+}/results", Use(API_Campaigns_Id_Results, mid.RequireAPIKey))
|
api.HandleFunc("/campaigns/{id:[0-9]+}/results", Use(API_Campaigns_Id_Results, mid.RequireAPIKey))
|
||||||
|
api.HandleFunc("/campaigns/{id:[0-9]+}/complete", Use(API_Campaigns_Id_Complete, mid.RequireAPIKey))
|
||||||
api.HandleFunc("/groups/", Use(API_Groups, mid.RequireAPIKey))
|
api.HandleFunc("/groups/", Use(API_Groups, mid.RequireAPIKey))
|
||||||
api.HandleFunc("/groups/{id:[0-9]+}", Use(API_Groups_Id, mid.RequireAPIKey))
|
api.HandleFunc("/groups/{id:[0-9]+}", Use(API_Groups_Id, mid.RequireAPIKey))
|
||||||
api.HandleFunc("/templates/", Use(API_Templates, mid.RequireAPIKey))
|
api.HandleFunc("/templates/", Use(API_Templates, mid.RequireAPIKey))
|
||||||
|
@ -110,6 +111,11 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
|
// Don't process events for completed campaigns
|
||||||
|
if c.Status == models.CAMPAIGN_COMPLETE {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_OPENED})
|
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_OPENED})
|
||||||
// Don't update the status if the user already clicked the link
|
// Don't update the status if the user already clicked the link
|
||||||
// or submitted data to the campaign
|
// or submitted data to the campaign
|
||||||
|
@ -157,11 +163,16 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rs.UpdateStatus(models.STATUS_SUCCESS)
|
|
||||||
c, err := models.GetCampaign(rs.CampaignId, rs.UserId)
|
c, err := models.GetCampaign(rs.CampaignId, rs.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
|
// Don't process events for completed campaigns
|
||||||
|
if c.Status == models.CAMPAIGN_COMPLETE {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rs.UpdateStatus(models.STATUS_SUCCESS)
|
||||||
p, err := models.GetPage(c.PageId, c.UserId)
|
p, err := models.GetPage(c.PageId, c.UserId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Println(err)
|
Logger.Println(err)
|
||||||
|
|
|
@ -338,8 +338,29 @@ func DeleteCampaign(id int64) error {
|
||||||
// Delete the campaign
|
// Delete the campaign
|
||||||
err = db.Delete(&Campaign{Id: id}).Error
|
err = db.Delete(&Campaign{Id: id}).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Logger.Panicln(err)
|
Logger.Println(err)
|
||||||
return err
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteCampaign effectively "ends" a campaign.
|
||||||
|
// Any future emails clicked will return a simple "404" page.
|
||||||
|
func CompleteCampaign(id int64, uid int64) error {
|
||||||
|
Logger.Printf("Marking campaign %d as complete\n", id)
|
||||||
|
c, err := GetCampaign(id, uid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Don't overwrite original completed time
|
||||||
|
if c.Status == CAMPAIGN_COMPLETE {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Mark the campaign as complete
|
||||||
|
c.CompletedDate = time.Now()
|
||||||
|
c.Status = CAMPAIGN_COMPLETE
|
||||||
|
err = db.Where("id=? and user_id=?", id, uid).Save(&c).Error
|
||||||
|
if err != nil {
|
||||||
|
Logger.Println(err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,7 +513,14 @@ td.details-control{
|
||||||
margin-left:10px !important;
|
margin-left:10px !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable{
|
table.dataTable{
|
||||||
width:100% !important;
|
width:100% !important;
|
||||||
}
|
}
|
||||||
|
.btn-blue {
|
||||||
|
color:#fff;
|
||||||
|
background-color:#428bca;
|
||||||
|
border-color:#428bca;
|
||||||
|
}
|
||||||
|
.btn-blue:hover{
|
||||||
|
background-color:#64a1d6;
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,5 @@
|
||||||
var map = null
|
var map = null
|
||||||
|
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 = {
|
||||||
|
@ -94,6 +95,52 @@ function deleteCampaign() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Completes a campaign after prompting the user
|
||||||
|
function completeCampaign() {
|
||||||
|
swal({
|
||||||
|
title: "Are you sure?",
|
||||||
|
text: "Gophish will stop processing events for this campaign",
|
||||||
|
type: "warning",
|
||||||
|
animation: false,
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: "Complete Campaign",
|
||||||
|
confirmButtonColor: "#428bca",
|
||||||
|
reverseButtons: true,
|
||||||
|
allowOutsideClick: false,
|
||||||
|
preConfirm: function() {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
api.campaignId.complete(campaign.id)
|
||||||
|
.success(function(msg) {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.error(function(data) {
|
||||||
|
reject(data.responseJSON.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).then(function() {
|
||||||
|
swal(
|
||||||
|
'Campaign Completed!',
|
||||||
|
'This campaign has been completed!',
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
$('#complete_button')[0].disabled = true;
|
||||||
|
$('#complete_button').text('Completed!')
|
||||||
|
doPoll = false;
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
if (confirm("Are you sure you want to delete: " + campaign.name + "?")) {
|
||||||
|
api.campaignId.delete(campaign.id)
|
||||||
|
.success(function(msg) {
|
||||||
|
location.href = '/campaigns'
|
||||||
|
})
|
||||||
|
.error(function(e) {
|
||||||
|
$("#modal\\.flashes").empty().append("<div style=\"text-align:center\" class=\"alert alert-danger\">\
|
||||||
|
<i class=\"fa fa-exclamation-circle\"></i> " + data.responseJSON.message + "</div>")
|
||||||
|
})
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
// Exports campaign results as a CSV file
|
// Exports campaign results as a CSV file
|
||||||
function exportAsCSV(scope) {
|
function exportAsCSV(scope) {
|
||||||
exportHTML = $("#exportButton").html()
|
exportHTML = $("#exportButton").html()
|
||||||
|
@ -298,7 +345,12 @@ function load() {
|
||||||
$("#campaignResults").show()
|
$("#campaignResults").show()
|
||||||
// Set the title
|
// Set the title
|
||||||
$("#page-title").text("Results for " + c.name)
|
$("#page-title").text("Results for " + c.name)
|
||||||
// Setup tooltips
|
if (c.status == "Completed") {
|
||||||
|
$('#complete_button')[0].disabled = true;
|
||||||
|
$('#complete_button').text('Completed!');
|
||||||
|
doPoll = false;
|
||||||
|
}
|
||||||
|
// Setup tooltips
|
||||||
$('[data-toggle="tooltip"]').tooltip()
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
// Setup viewing the details of a result
|
// Setup viewing the details of a result
|
||||||
$("#resultsTable").on("click", ".timeline-event-details", function() {
|
$("#resultsTable").on("click", ".timeline-event-details", function() {
|
||||||
|
@ -554,11 +606,13 @@ function load() {
|
||||||
errorFlash(" Campaign not found!")
|
errorFlash(" Campaign not found!")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
load();
|
load();
|
||||||
// Start the polling loop
|
// Start the polling loop
|
||||||
function refresh() {
|
function refresh() {
|
||||||
|
if (!doPoll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$("#refresh_message").show()
|
$("#refresh_message").show()
|
||||||
poll()
|
poll()
|
||||||
$("#refresh_message").hide()
|
$("#refresh_message").hide()
|
||||||
|
|
|
@ -62,6 +62,10 @@ var api = {
|
||||||
// results() - Queries the API for GET /campaigns/:id/results
|
// results() - Queries the API for GET /campaigns/:id/results
|
||||||
results: function(id) {
|
results: function(id) {
|
||||||
return query("/campaigns/" + id + "/results", "GET", {}, true)
|
return query("/campaigns/" + id + "/results", "GET", {}, true)
|
||||||
|
},
|
||||||
|
// complete() - Completes a campaign at POST /campaigns/:id/complete
|
||||||
|
complete: function(id) {
|
||||||
|
return query("/campaigns/" + id + "/complete", "GET", {}, true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// groups contains the endpoints for /groups
|
// groups contains the endpoints for /groups
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -25,6 +25,7 @@
|
||||||
<link href='https://fonts.googleapis.com/css?family=Roboto:700,500' rel='stylesheet' type='text/css'>
|
<link href='https://fonts.googleapis.com/css?family=Roboto:700,500' rel='stylesheet' type='text/css'>
|
||||||
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
|
<link href='https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,300,600,700' rel='stylesheet' type='text/css'>
|
||||||
<link href="/css/checkbox.css" rel="stylesheet">
|
<link href="/css/checkbox.css" rel="stylesheet">
|
||||||
|
<link href="/css/sweetalert2.min.css" rel="stylesheet">
|
||||||
<script>
|
<script>
|
||||||
{{if .User}}
|
{{if .User}}
|
||||||
var user = {
|
var user = {
|
||||||
|
|
|
@ -48,6 +48,9 @@
|
||||||
<li><a href="#" onclick="exportAsCSV('events')">Raw Events</a></li>
|
<li><a href="#" onclick="exportAsCSV('events')">Raw Events</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<button id="complete_button" type="button" class="btn btn-blue" data-toggle="tooltip" onclick="completeCampaign()">
|
||||||
|
<i class="fa fa-flag-checkered"></i> Complete
|
||||||
|
</button>
|
||||||
<button type="button" class="btn btn-danger" data-toggle="tooltip" onclick="deleteCampaign()">
|
<button type="button" class="btn btn-danger" data-toggle="tooltip" onclick="deleteCampaign()">
|
||||||
<i class="fa fa-trash-o fa-lg"></i> Delete
|
<i class="fa fa-trash-o fa-lg"></i> Delete
|
||||||
</button>
|
</button>
|
||||||
|
@ -100,7 +103,7 @@
|
||||||
<table id="resultsTable" class="table">
|
<table id="resultsTable" class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Result ID</th>
|
<th>Result ID</th>
|
||||||
<th class="no-sort"></th>
|
<th class="no-sort"></th>
|
||||||
<th>First Name</th>
|
<th>First Name</th>
|
||||||
<th>Last Name</th>
|
<th>Last Name</th>
|
||||||
|
@ -124,5 +127,6 @@
|
||||||
<script src="/js/topojson.min.js"></script>
|
<script src="/js/topojson.min.js"></script>
|
||||||
<script src="/js/datamaps.min.js"></script>
|
<script src="/js/datamaps.min.js"></script>
|
||||||
<script src="/js/papaparse.min.js"></script>
|
<script src="/js/papaparse.min.js"></script>
|
||||||
|
<script src="/js/sweetalert2.min.js"></script>
|
||||||
<script src="/js/app/campaign_results.js"></script>
|
<script src="/js/app/campaign_results.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -49,10 +49,10 @@
|
||||||
<table id="campaignTable" class="table" style="display:none;">
|
<table id="campaignTable" class="table" style="display:none;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th class="col-md-3">Name</th>
|
||||||
<th>Created Date</th>
|
<th class="col-md-4">Created Date</th>
|
||||||
<th>Status</th>
|
<th class="col-md-2">Status</th>
|
||||||
<th class="col-md-2 no-sort"></th>
|
<th class="col-md-3 no-sort"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
Loading…
Reference in New Issue