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 gophish_admin.key
*.exe *.exe
*.db gophish.db*
gophish

View file

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

View file

@ -66,7 +66,10 @@ func (s *ControllersSuite) TestOpenedPhishingEmail() {
campaign = s.getFirstCampaign() campaign = s.getFirstCampaign()
result = campaign.Results[0] result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_OPENED) s.Equal(result.Status, models.EVENT_OPENED)
s.Equal(lastEvent.Message, models.EVENT_OPENED)
s.Equal(result.ModifiedDate, lastEvent.Time)
} }
func (s *ControllersSuite) TestReportedPhishingEmail() { func (s *ControllersSuite) TestReportedPhishingEmail() {
@ -78,8 +81,10 @@ func (s *ControllersSuite) TestReportedPhishingEmail() {
campaign = s.getFirstCampaign() campaign = s.getFirstCampaign()
result = campaign.Results[0] result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Reported, true) 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() { func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
@ -92,7 +97,10 @@ func (s *ControllersSuite) TestClickedPhishingLinkAfterOpen() {
campaign = s.getFirstCampaign() campaign = s.getFirstCampaign()
result = campaign.Results[0] result = campaign.Results[0]
lastEvent := campaign.Events[len(campaign.Events)-1]
s.Equal(result.Status, models.EVENT_CLICKED) s.Equal(result.Status, models.EVENT_CLICKED)
s.Equal(lastEvent.Message, models.EVENT_CLICKED)
s.Equal(result.ModifiedDate, lastEvent.Time)
} }
func (s *ControllersSuite) TestNoRecipientID() { 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 ( import (
"errors" "errors"
"net/url"
"time" "time"
log "github.com/gophish/gophish/logger" log "github.com/gophish/gophish/logger"
@ -68,6 +69,30 @@ type CampaignStats struct {
Error int64 `json:"error"` 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 // ErrCampaignNameNotSpecified indicates there was no template given by the user
var ErrCampaignNameNotSpecified = errors.New("Campaign name not specified") 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 // 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.CampaignId = c.Id
e.Time = time.Now().UTC() e.Time = time.Now().UTC()
return db.Save(&e).Error return db.Save(e).Error
} }
// getDetails retrieves the related attributes of the campaign // getDetails retrieves the related attributes of the campaign
@ -220,17 +245,6 @@ func getCampaignStats(cid int64) (CampaignStats, error) {
return s, err 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. // GetCampaigns returns the campaigns owned by the given user.
func GetCampaigns(uid int64) ([]Campaign, error) { func GetCampaigns(uid int64) ([]Campaign, error) {
cs := []Campaign{} cs := []Campaign{}
@ -422,7 +436,7 @@ func PostCampaign(c *Campaign, uid int64) error {
log.Error(err) log.Error(err)
return err return err
} }
err = c.AddEvent(Event{Message: "Campaign Created"}) err = c.AddEvent(&Event{Message: "Campaign Created"})
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
@ -438,15 +452,16 @@ func PostCampaign(c *Campaign, uid int64) error {
} }
resultMap[t.Email] = true resultMap[t.Email] = true
r := &Result{ r := &Result{
Email: t.Email, Email: t.Email,
Position: t.Position, Position: t.Position,
Status: STATUS_SCHEDULED, Status: STATUS_SCHEDULED,
CampaignId: c.Id, CampaignId: c.Id,
UserId: c.UserId, UserId: c.UserId,
FirstName: t.FirstName, FirstName: t.FirstName,
LastName: t.LastName, LastName: t.LastName,
SendDate: c.LaunchDate, SendDate: c.LaunchDate,
Reported: false, Reported: false,
ModifiedDate: c.CreatedDate,
} }
if c.Status == CAMPAIGN_IN_PROGRESS { if c.Status == CAMPAIGN_IN_PROGRESS {
r.Status = STATUS_SENDING r.Status = STATUS_SENDING

View file

@ -3,7 +3,6 @@ package models
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "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 // too many times. Backoff also unlocks the maillog so that it can be processed
// again in the future. // again in the future.
func (m *MailLog) Backoff(reason error) error { func (m *MailLog) Backoff(reason error) error {
if m.SendAttempt == MaxSendAttempts {
err = m.addError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
r, err := GetResult(m.RId) r, err := GetResult(m.RId)
if err != nil { if err != nil {
return err return err
} }
if m.SendAttempt == MaxSendAttempts {
r.HandleEmailError(ErrMaxSendAttempts)
return ErrMaxSendAttempts
}
// Add an error, since we had to backoff because of a // Add an error, since we had to backoff because of a
// temporary error of some sort during the SMTP transaction // temporary error of some sort during the SMTP transaction
err = m.addError(reason)
if err != nil {
return err
}
m.SendAttempt++ m.SendAttempt++
backoffDuration := math.Pow(2, float64(m.SendAttempt)) backoffDuration := math.Pow(2, float64(m.SendAttempt))
m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration)) m.SendDate = m.SendDate.Add(time.Minute * time.Duration(backoffDuration))
@ -79,9 +74,7 @@ func (m *MailLog) Backoff(reason error) error {
if err != nil { if err != nil {
return err return err
} }
r.Status = STATUS_RETRY err = r.HandleEmailBackoff(reason, m.SendDate)
r.SendDate = m.SendDate
err = db.Save(r).Error
if err != nil { if err != nil {
return err return err
} }
@ -101,32 +94,6 @@ func (m *MailLog) Lock() error {
return db.Save(&m).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 // Error sets the error status on the models.Result that the
// maillog refers to. Since MailLog errors are permanent, // maillog refers to. Since MailLog errors are permanent,
// this action also deletes the maillog. // this action also deletes the maillog.
@ -136,14 +103,7 @@ func (m *MailLog) Error(e error) error {
log.Warn(err) log.Warn(err)
return err return err
} }
// Update the result err = r.HandleEmailError(e)
err = r.UpdateStatus(ERROR)
if err != nil {
log.Warn(err)
return err
}
// Update the campaign events
err = m.addError(e)
if err != nil { if err != nil {
log.Warn(err) log.Warn(err)
return err return err
@ -159,15 +119,7 @@ func (m *MailLog) Success() error {
if err != nil { if err != nil {
return err return err
} }
err = r.UpdateStatus(EVENT_SENT) err = r.HandleEmailSent()
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})
if err != nil { if err != nil {
return err return err
} }

View file

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

View file

@ -2,6 +2,7 @@ package models
import ( import (
"crypto/rand" "crypto/rand"
"encoding/json"
"fmt" "fmt"
"math/big" "math/big"
"net" "net"
@ -25,30 +26,133 @@ type mmGeoPoint struct {
// Result contains the fields for a result object, // Result contains the fields for a result object,
// which is a representation of a target in a campaign. // which is a representation of a target in a campaign.
type Result struct { type Result struct {
Id int64 `json:"-"` Id int64 `json:"-"`
CampaignId int64 `json:"-"` CampaignId int64 `json:"-"`
UserId int64 `json:"-"` UserId int64 `json:"-"`
RId string `json:"id"` RId string `json:"id"`
Email string `json:"email"` Email string `json:"email"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
Position string `json:"position"` Position string `json:"position"`
Status string `json:"status" sql:"not null"` Status string `json:"status" sql:"not null"`
IP string `json:"ip"` IP string `json:"ip"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
SendDate time.Time `json:"send_date"` SendDate time.Time `json:"send_date"`
Reported bool `json:"reported" sql:"not null"` 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) createEvent(status string, details interface{}) (*Event, error) {
func (r *Result) UpdateStatus(s string) error { c, err := GetCampaign(r.CampaignId, r.UserId)
return db.Table("results").Where("id=?", r.Id).Update("status", s).Error 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 // HandleEmailSent updates a Result to indicate that the email has been
func (r *Result) UpdateReported(s bool) error { // successfully sent to the remote SMTP server
return db.Table("results").Where("id=?", r.Id).Update("reported", s).Error 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 // UpdateGeo updates the latitude and longitude of the result in
@ -68,11 +172,10 @@ func (r *Result) UpdateGeo(addr string) error {
return err return err
} }
// Update the database with the record information // Update the database with the record information
return db.Table("results").Where("id=?", r.Id).Updates(map[string]interface{}{ r.IP = addr
"ip": addr, r.Latitude = city.GeoPoint.Latitude
"latitude": city.GeoPoint.Latitude, r.Longitude = city.GeoPoint.Longitude
"longitude": city.GeoPoint.Longitude, return db.Save(r).Error
}).Error
} }
// GenerateId generates a unique key to represent the result // GenerateId generates a unique key to represent the result

View file

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