Refactored result updating to be in result.go.

Added the modified_date field to results so it's easy to keep track of the last results that were modified without having to parse every event. Updated the tests to reflect the changes.
This commit is contained in:
Jordan Wright 2018-05-26 21:26:34 -05:00
parent 222399c5f6
commit 420410b52c
10 changed files with 230 additions and 147 deletions

3
.gitignore vendored
View file

@ -26,4 +26,5 @@ gophish_admin.crt
gophish_admin.key
*.exe
*.db
gophish.db*
gophish

View file

@ -2,7 +2,6 @@ package controllers
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"html/template"
@ -26,13 +25,6 @@ var ErrInvalidRequest = errors.New("Invalid request")
// has already been marked as complete.
var ErrCampaignComplete = errors.New("Event received on completed campaign")
// eventDetails is a struct that wraps common attributes we want to store
// in an event
type eventDetails struct {
Payload url.Values `json:"payload"`
Browser map[string]string `json:"browser"`
}
// CreatePhishingRouter creates the router that handles phishing connections.
func CreatePhishingRouter() http.Handler {
router := mux.NewRouter()
@ -59,16 +51,8 @@ func PhishTracker(w http.ResponseWriter, r *http.Request) {
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_OPENED, Details: string(rj)})
// Don't update the status if the user already clicked the link
// or submitted data to the campaign
if rs.Status == models.EVENT_CLICKED || rs.Status == models.EVENT_DATA_SUBMIT {
http.ServeFile(w, r, "static/images/pixel.png")
return
}
err = rs.UpdateStatus(models.EVENT_OPENED)
d := ctx.Get(r, "details").(models.EventDetails)
err = rs.HandleEmailOpened(d)
if err != nil {
log.Error(err)
}
@ -87,11 +71,9 @@ func PhishReporter(w http.ResponseWriter, r *http.Request) {
return
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_REPORTED, Details: string(rj)})
d := ctx.Get(r, "details").(models.EventDetails)
err = rs.UpdateReported(true)
err = rs.HandleEmailReport(d)
if err != nil {
log.Error(err)
}
@ -112,7 +94,7 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
}
rs := ctx.Get(r, "result").(models.Result)
c := ctx.Get(r, "campaign").(models.Campaign)
rj := ctx.Get(r, "details").([]byte)
d := ctx.Get(r, "details").(models.EventDetails)
p, err := models.GetPage(c.PageId, c.UserId)
if err != nil {
log.Error(err)
@ -121,18 +103,12 @@ func PhishHandler(w http.ResponseWriter, r *http.Request) {
}
switch {
case r.Method == "GET":
if rs.Status != models.EVENT_CLICKED && rs.Status != models.EVENT_DATA_SUBMIT {
rs.UpdateStatus(models.EVENT_CLICKED)
}
err = c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_CLICKED, Details: string(rj)})
err = rs.HandleClickedLink(d)
if err != nil {
log.Error(err)
}
case r.Method == "POST":
// If data was POST'ed, let's record it
rs.UpdateStatus(models.EVENT_DATA_SUBMIT)
// Store the data in an event
c.AddEvent(models.Event{Email: rs.Email, Message: models.EVENT_DATA_SUBMIT, Details: string(rj)})
err = rs.HandleFormSubmit(d)
if err != nil {
log.Error(err)
}
@ -224,16 +200,15 @@ func setupContext(r *http.Request) (error, *http.Request) {
if err != nil {
log.Error(err)
}
d := eventDetails{
d := models.EventDetails{
Payload: r.Form,
Browser: make(map[string]string),
}
d.Browser["address"] = ip
d.Browser["user-agent"] = r.Header.Get("User-Agent")
rj, err := json.Marshal(d)
r = ctx.Set(r, "result", rs)
r = ctx.Set(r, "campaign", c)
r = ctx.Set(r, "details", rj)
r = ctx.Set(r, "details", d)
return nil, r
}

View file

@ -66,7 +66,10 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {
campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_OPENED)
s.Equal(lastEvent.Message, models.EVENT_OPENED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}
func (s *ControllersSuite) TestReportedPhishingEmail() {
@ -78,8 +81,10 @@ func (s *ControllersSuite) TestReportedPhishingEmail() {
campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Reported, true)
s.Equal(campaign.Events[len(campaign.Events)-1].Message, models.EVENT_REPORTED)
s.Equal(lastEvent.Message, models.EVENT_REPORTED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}
func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
@ -92,7 +97,10 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
campaign = s.getFirstCampaign()
result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_CLICKED)
s.Equal(lastEvent.Message, models.EVENT_CLICKED)
s.Equal(result.ModifiedDate, lastEvent.Time)
}
func (s *ControllersSuite) TestNoRecipientID() {

View file

@ -0,0 +1,17 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN modified_date DATETIME;
UPDATE results
SET `modified_date`= (
SELECT max(events.time) FROM events
WHERE events.email=results.email
AND events.campaign_id=results.campaign_id
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View file

@ -0,0 +1,17 @@
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
ALTER TABLE results ADD COLUMN modified_date DATETIME;
UPDATE results
SET `modified_date`= (
SELECT max(events.time) FROM events
WHERE events.email=results.email
AND events.campaign_id=results.campaign_id
);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back

View file

@ -2,6 +2,7 @@ package models
import (
"errors"
"net/url"
"time"
log "github.com/gophish/gophish/logger"
@ -68,6 +69,30 @@ type CampaignStats struct {
Error int64 `json:"error"`
}
// Event contains the fields for an event
// that occurs during the campaign
type Event struct {
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
Email string `json:"email"`
Time time.Time `json:"time"`
Message string `json:"message"`
Details string `json:"details"`
}
// EventDetails is a struct that wraps common attributes we want to store
// in an event
type EventDetails struct {
Payload url.Values `json:"payload"`
Browser map[string]string `json:"browser"`
}
// EventError is a struct that wraps an error that occurs when sending an
// email to a recipient
type EventError struct {
Error string `json:"error"`
}
// ErrCampaignNameNotSpecified indicates there was no template given by the user
var ErrCampaignNameNotSpecified = errors.New("Campaign name not specified")
@ -122,10 +147,10 @@ func (c *Campaign) UpdateStatus(s string) error {
}
// AddEvent creates a new campaign event in the database
func (c *Campaign) AddEvent(e Event) error {
func (c *Campaign) AddEvent(e *Event) error {
e.CampaignId = c.Id
e.Time = time.Now().UTC()
return db.Save(&e).Error
return db.Save(e).Error
}
// getDetails retrieves the related attributes of the campaign
@ -220,17 +245,6 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
return s, err
}
// Event contains the fields for an event
// that occurs during the campaign
type Event struct {
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
Email string `json:"email"`
Time time.Time `json:"time"`
Message string `json:"message"`
Details string `json:"details"`
}
// GetCampaigns returns the campaigns owned by the given user.
func GetCampaigns(uid int64) ([]Campaign, error) {
cs := []Campaign{}
@ -422,7 +436,7 @@ func PostCampaign(c *Campaign, uid int64) error {
log.Error(err)
return err
}
err = c.AddEvent(Event{Message: "Campaign Created"})
err = c.AddEvent(&Event{Message: "Campaign Created"})
if err != nil {
log.Error(err)
}
@ -438,15 +452,16 @@ func PostCampaign(c *Campaign, uid int64) error {
}
resultMap[t.Email] = true
r := &Result{
Email: t.Email,
Position: t.Position,
Status: STATUS_SCHEDULED,
CampaignId: c.Id,
UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
Email: t.Email,
Position: t.Position,
Status: STATUS_SCHEDULED,
CampaignId: c.Id,
UserId: c.UserId,
FirstName: t.FirstName,
LastName: t.LastName,
SendDate: c.LaunchDate,
Reported: false,
ModifiedDate: c.CreatedDate,
}
if c.Status == CAMPAIGN_IN_PROGRESS {
r.Status = STATUS_SENDING

View file

@ -3,7 +3,6 @@ package models
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
@ -58,20 +57,16 @@ func GenerateMailLog(c *Campaign, r *Result) error {
// too many times. Backoff also unlocks the maillog so that it can be processed
// again in the future.
func (m *MailLog) Backoff(reason error) error {
if m.SendAttempt == MaxSendAttempts {
err = m.addError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
r, err := GetResult(m.RId)
if err != nil {
return err
}
if m.SendAttempt == MaxSendAttempts {
r.HandleEmailError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
// Add an error, since we had to backoff because of a
// temporary error of some sort during the SMTP transaction
err = m.addError(reason)
if err != nil {
return err
}
m.SendAttempt++
backoffDuration := math.Pow(2, float64(m.SendAttempt))
m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
@ -79,9 +74,7 @@ func (m *MailLog) Backoff(reason error) error {
if err != nil {
return err
}
r.Status = STATUS_RETRY
r.SendDate = m.SendDate
err = db.Save(r).Error
err = r.HandleEmailBackoff(reason, m.SendDate)
if err != nil {
return err
}
@ -101,32 +94,6 @@ func (m *MailLog) Lock() error {
return db.Save(&m).Error
}
// addError adds an error to the associated campaign
func (m *MailLog) addError(e error) error {
c, err := GetCampaign(m.CampaignId, m.UserId)
if err != nil {
return err
}
// This is redundant in the case of permanent
// errors, but the extra query makes for
// a cleaner API.
r, err := GetResult(m.RId)
if err != nil {
return err
}
es := struct {
Error string `json:"error"`
}{
Error: e.Error(),
}
ej, err := json.Marshal(es)
if err != nil {
log.Warn(err)
}
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENDING_ERROR, Details: string(ej)})
return err
}
// Error sets the error status on the models.Result that the
// maillog refers to. Since MailLog errors are permanent,
// this action also deletes the maillog.
@ -136,14 +103,7 @@ func (m *MailLog) Error(e error) error {
log.Warn(err)
return err
}
// Update the result
err = r.UpdateStatus(ERROR)
if err != nil {
log.Warn(err)
return err
}
// Update the campaign events
err = m.addError(e)
err = r.HandleEmailError(e)
if err != nil {
log.Warn(err)
return err
@ -159,15 +119,7 @@ func (m *MailLog) Success() error {
if err != nil {
return err
}
err = r.UpdateStatus(EVENT_SENT)
if err != nil {
return err
}
c, err := GetCampaign(m.CampaignId, m.UserId)
if err != nil {
return err
}
err = c.AddEvent(Event{Email: r.Email, Message: EVENT_SENT})
err = r.HandleEmailSent()
if err != nil {
return err
}

View file

@ -105,11 +105,7 @@ func (s *ModelsSuite) TestMailLogError(ch *check.C) {
ch.Assert(len(campaign.Events), check.Equals, expectedEventLength)
gotEvent := campaign.Events[1]
es := struct {
Error string `json:"error"`
}{
Error: expectedError.Error(),
}
es := EventError{Error: expectedError.Error()}
ej, _ := json.Marshal(es)
expectedEvent := Event{
Id: gotEvent.Id,

View file

@ -2,6 +2,7 @@ package models
import (
"crypto/rand"
"encoding/json"
"fmt"
"math/big"
"net"
@ -25,30 +26,133 @@ type mmGeoPoint struct {
// Result contains the fields for a result object,
// which is a representation of a target in a campaign.
type Result struct {
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
UserId int64 `json:"-"`
RId string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Status string `json:"status" sql:"not null"`
IP string `json:"ip"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"`
Id int64 `json:"-"`
CampaignId int64 `json:"-"`
UserId int64 `json:"-"`
RId string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Position string `json:"position"`
Status string `json:"status" sql:"not null"`
IP string `json:"ip"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"`
ModifiedDate time.Time `json:"modified_date"`
}
// UpdateStatus updates the status of the result in the database
func (r *Result) UpdateStatus(s string) error {
return db.Table("results").Where("id=?", r.Id).Update("status", s).Error
func (r *Result) createEvent(status string, details interface{}) (*Event, error) {
c, err := GetCampaign(r.CampaignId, r.UserId)
if err != nil {
return nil, err
}
e := &Event{Email: r.Email, Message: status}
if details != nil {
dj, err := json.Marshal(details)
if err != nil {
return nil, err
}
e.Details = string(dj)
}
c.AddEvent(e)
return e, nil
}
// UpdateReported updates when a user reports a campaign
func (r *Result) UpdateReported(s bool) error {
return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error
// HandleEmailSent updates a Result to indicate that the email has been
// successfully sent to the remote SMTP server
func (r *Result) HandleEmailSent() error {
event, err := r.createEvent(EVENT_SENT, nil)
if err != nil {
return err
}
r.Status = EVENT_SENT
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleEmailError updates a Result to indicate that there was an error when
// attempting to send the email to the remote SMTP server.
func (r *Result) HandleEmailError(err error) error {
event, err := r.createEvent(EVENT_SENDING_ERROR, EventError{Error: err.Error()})
if err != nil {
return err
}
r.Status = ERROR
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleEmailBackoff updates a Result to indicate that the email received a
// temporary error and needs to be retried
func (r *Result) HandleEmailBackoff(err error, sendDate time.Time) error {
event, err := r.createEvent(EVENT_SENDING_ERROR, EventError{Error: err.Error()})
if err != nil {
return err
}
r.Status = STATUS_RETRY
r.SendDate = sendDate
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleEmailOpened updates a Result in the case where the recipient opened the
// email.
func (r *Result) HandleEmailOpened(details EventDetails) error {
event, err := r.createEvent(EVENT_OPENED, details)
if err != nil {
return err
}
// Don't update the status if the user already clicked the link
// or submitted data to the campaign
if r.Status == EVENT_CLICKED || r.Status == EVENT_DATA_SUBMIT {
return nil
}
r.Status = EVENT_OPENED
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleClickedLink updates a Result in the case where the recipient clicked
// the link in an email.
func (r *Result) HandleClickedLink(details EventDetails) error {
event, err := r.createEvent(EVENT_CLICKED, details)
if err != nil {
return err
}
// Don't update the status if the user has already submitted data via the
// landing page form.
if r.Status == EVENT_DATA_SUBMIT {
return nil
}
r.Status = EVENT_CLICKED
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleFormSubmit updates a Result in the case where the recipient submitted
// credentials to the form on a Landing Page.
func (r *Result) HandleFormSubmit(details EventDetails) error {
event, err := r.createEvent(EVENT_DATA_SUBMIT, details)
if err != nil {
return err
}
r.Status = EVENT_DATA_SUBMIT
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// HandleEmailReport updates a Result in the case where they report a simulated
// phishing email using the HTTP handler.
func (r *Result) HandleEmailReport(details EventDetails) error {
event, err := r.createEvent(EVENT_REPORTED, details)
if err != nil {
return err
}
r.Reported = true
r.ModifiedDate = event.Time
return db.Save(r).Error
}
// UpdateGeo updates the latitude and longitude of the result in
@ -68,11 +172,10 @@ func (r *Result) UpdateGeo(addr string) error {
return err
}
// Update the database with the record information
return db.Table("results").Where("id=?", r.Id).Updates(map[string]interface{}{
"ip": addr,
"latitude": city.GeoPoint.Latitude,
"longitude": city.GeoPoint.Longitude,
}).Error
r.IP = addr
r.Latitude = city.GeoPoint.Latitude
r.Longitude = city.GeoPoint.Longitude
return db.Save(r).Error
}
// GenerateId generates a unique key to represent the result

View file

@ -1,7 +1,6 @@
package models
import (
"fmt"
"net/mail"
"regexp"
"time"
@ -40,10 +39,9 @@ func (s *ModelsSuite) TestResultSendingStatus(ch *check.C) {
ch.Assert(PostCampaign(&c, c.UserId), check.Equals, nil)
// This campaign wasn't scheduled, so we expect the status to
// be sending
fmt.Println("Campaign STATUS")
fmt.Println(c.Status)
for _, r := range c.Results {
ch.Assert(r.Status, check.Equals, STATUS_SENDING)
ch.Assert(r.ModifiedDate, check.Equals, c.CreatedDate)
}
}
func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) {
@ -54,6 +52,7 @@ func (s *ModelsSuite) TestResultScheduledStatus(ch *check.C) {
// be sending
for _, r := range c.Results {
ch.Assert(r.Status, check.Equals, STATUS_SCHEDULED)
ch.Assert(r.ModifiedDate, check.Equals, c.CreatedDate)
}
}