mirror of https://github.com/gophish/gophish
553 lines
16 KiB
Go
553 lines
16 KiB
Go
package models
|
|
|
|
import (
|
|
"errors"
|
|
"net/url"
|
|
"time"
|
|
|
|
log "github.com/gophish/gophish/logger"
|
|
"github.com/jinzhu/gorm"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// Campaign is a struct representing a created campaign
|
|
type Campaign struct {
|
|
Id int64 `json:"id"`
|
|
UserId int64 `json:"-"`
|
|
Name string `json:"name" sql:"not null"`
|
|
CreatedDate time.Time `json:"created_date"`
|
|
LaunchDate time.Time `json:"launch_date"`
|
|
CompletedDate time.Time `json:"completed_date"`
|
|
TemplateId int64 `json:"-"`
|
|
Template Template `json:"template"`
|
|
PageId int64 `json:"-"`
|
|
Page Page `json:"page"`
|
|
Status string `json:"status"`
|
|
Results []Result `json:"results,omitempty"`
|
|
Groups []Group `json:"groups,omitempty"`
|
|
Events []Event `json:"timeline,omitemtpy"`
|
|
SMTPId int64 `json:"-"`
|
|
SMTP SMTP `json:"smtp"`
|
|
URL string `json:"url"`
|
|
}
|
|
|
|
// CampaignResults is a struct representing the results from a campaign
|
|
type CampaignResults struct {
|
|
Id int64 `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Reported string `json:"reported"`
|
|
Results []Result `json:"results, omitempty"`
|
|
Events []Event `json:"timeline,omitempty"`
|
|
}
|
|
|
|
// CampaignSummaries is a struct representing the overview of campaigns
|
|
type CampaignSummaries struct {
|
|
Total int64 `json:"total"`
|
|
Campaigns []CampaignSummary `json:"campaigns"`
|
|
}
|
|
|
|
// CampaignSummary is a struct representing the overview of a single camaign
|
|
type CampaignSummary struct {
|
|
Id int64 `json:"id"`
|
|
CreatedDate time.Time `json:"created_date"`
|
|
LaunchDate time.Time `json:"launch_date"`
|
|
CompletedDate time.Time `json:"completed_date"`
|
|
Status string `json:"status"`
|
|
Name string `json:"name"`
|
|
Stats CampaignStats `json:"stats"`
|
|
}
|
|
|
|
// CampaignStats is a struct representing the statistics for a single campaign
|
|
type CampaignStats struct {
|
|
Total int64 `json:"total"`
|
|
EmailsSent int64 `json:"sent"`
|
|
OpenedEmail int64 `json:"opened"`
|
|
ClickedLink int64 `json:"clicked"`
|
|
SubmittedData int64 `json:"submitted_data"`
|
|
EmailReported int64 `json:"email_reported"`
|
|
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")
|
|
|
|
// ErrGroupNotSpecified indicates there was no template given by the user
|
|
var ErrGroupNotSpecified = errors.New("No groups specified")
|
|
|
|
// ErrTemplateNotSpecified indicates there was no template given by the user
|
|
var ErrTemplateNotSpecified = errors.New("No email template specified")
|
|
|
|
// ErrPageNotSpecified indicates a landing page was not provided for the campaign
|
|
var ErrPageNotSpecified = errors.New("No landing page specified")
|
|
|
|
// ErrSMTPNotSpecified indicates a sending profile was not provided for the campaign
|
|
var ErrSMTPNotSpecified = errors.New("No sending profile specified")
|
|
|
|
// ErrTemplateNotFound indicates the template specified does not exist in the database
|
|
var ErrTemplateNotFound = errors.New("Template not found")
|
|
|
|
// ErrGroupNotFound indicates a group specified by the user does not exist in the database
|
|
var ErrGroupNotFound = errors.New("Group not found")
|
|
|
|
// ErrPageNotFound indicates a page specified by the user does not exist in the database
|
|
var ErrPageNotFound = errors.New("Page not found")
|
|
|
|
// ErrSMTPNotFound indicates a sending profile specified by the user does not exist in the database
|
|
var ErrSMTPNotFound = errors.New("Sending profile not found")
|
|
|
|
// RecipientParameter is the URL parameter that points to the result ID for a recipient.
|
|
const RecipientParameter = "rid"
|
|
|
|
// Validate checks to make sure there are no invalid fields in a submitted campaign
|
|
func (c *Campaign) Validate() error {
|
|
switch {
|
|
case c.Name == "":
|
|
return ErrCampaignNameNotSpecified
|
|
case len(c.Groups) == 0:
|
|
return ErrGroupNotSpecified
|
|
case c.Template.Name == "":
|
|
return ErrTemplateNotSpecified
|
|
case c.Page.Name == "":
|
|
return ErrPageNotSpecified
|
|
case c.SMTP.Name == "":
|
|
return ErrSMTPNotSpecified
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateStatus changes the campaign status appropriately
|
|
func (c *Campaign) UpdateStatus(s string) error {
|
|
// This could be made simpler, but I think there's a bug in gorm
|
|
return db.Table("campaigns").Where("id=?", c.Id).Update("status", s).Error
|
|
}
|
|
|
|
// AddEvent creates a new campaign event in the database
|
|
func (c *Campaign) AddEvent(e *Event) error {
|
|
e.CampaignId = c.Id
|
|
e.Time = time.Now().UTC()
|
|
return db.Save(e).Error
|
|
}
|
|
|
|
// getDetails retrieves the related attributes of the campaign
|
|
// from the database. If the Events and the Results are not available,
|
|
// an error is returned. Otherwise, the attribute name is set to [Deleted],
|
|
// indicating the user deleted the attribute (template, smtp, etc.)
|
|
func (c *Campaign) getDetails() error {
|
|
err = db.Model(c).Related(&c.Results).Error
|
|
if err != nil {
|
|
log.Warnf("%s: results not found for campaign", err)
|
|
return err
|
|
}
|
|
err = db.Model(c).Related(&c.Events).Error
|
|
if err != nil {
|
|
log.Warnf("%s: events not found for campaign", err)
|
|
return err
|
|
}
|
|
err = db.Table("templates").Where("id=?", c.TemplateId).Find(&c.Template).Error
|
|
if err != nil {
|
|
if err != gorm.ErrRecordNotFound {
|
|
return err
|
|
}
|
|
c.Template = Template{Name: "[Deleted]"}
|
|
log.Warnf("%s: template not found for campaign", err)
|
|
}
|
|
err = db.Where("template_id=?", c.Template.Id).Find(&c.Template.Attachments).Error
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
log.Warn(err)
|
|
return err
|
|
}
|
|
err = db.Table("pages").Where("id=?", c.PageId).Find(&c.Page).Error
|
|
if err != nil {
|
|
if err != gorm.ErrRecordNotFound {
|
|
return err
|
|
}
|
|
c.Page = Page{Name: "[Deleted]"}
|
|
log.Warnf("%s: page not found for campaign", err)
|
|
}
|
|
err = db.Table("smtp").Where("id=?", c.SMTPId).Find(&c.SMTP).Error
|
|
if err != nil {
|
|
// Check if the SMTP was deleted
|
|
if err != gorm.ErrRecordNotFound {
|
|
return err
|
|
}
|
|
c.SMTP = SMTP{Name: "[Deleted]"}
|
|
log.Warnf("%s: sending profile not found for campaign", err)
|
|
}
|
|
err = db.Where("smtp_id=?", c.SMTP.Id).Find(&c.SMTP.Headers).Error
|
|
if err != nil && err != gorm.ErrRecordNotFound {
|
|
log.Warn(err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getBaseURL returns the Campaign's configured URL.
|
|
// This is used to implement the TemplateContext interface.
|
|
func (c *Campaign) getBaseURL() string {
|
|
return c.URL
|
|
}
|
|
|
|
// getFromAddress returns the Campaign's configured SMTP "From" address.
|
|
// This is used to implement the TemplateContext interface.
|
|
func (c *Campaign) getFromAddress() string {
|
|
return c.SMTP.FromAddress
|
|
}
|
|
|
|
// 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)
|
|
err := query.Count(&s.Total).Error
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
query.Where("status=?", EVENT_DATA_SUBMIT).Count(&s.SubmittedData)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
query.Where("status=?", EVENT_CLICKED).Count(&s.ClickedLink)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
query.Where("reported=?", true).Count(&s.EmailReported)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
// 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
|
|
}
|
|
// 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
|
|
}
|
|
|
|
// GetCampaigns returns the campaigns owned by the given user.
|
|
func GetCampaigns(uid int64) ([]Campaign, error) {
|
|
cs := []Campaign{}
|
|
err := db.Model(&User{Id: uid}).Related(&cs).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
for i := range cs {
|
|
err = cs[i].getDetails()
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
}
|
|
return cs, err
|
|
}
|
|
|
|
// GetCampaignSummaries gets the summary objects for all the campaigns
|
|
// owned by the current user
|
|
func GetCampaignSummaries(uid int64) (CampaignSummaries, error) {
|
|
overview := CampaignSummaries{}
|
|
cs := []CampaignSummary{}
|
|
// Get the basic campaign information
|
|
query := db.Table("campaigns").Where("user_id = ?", uid)
|
|
query = query.Select("id, name, created_date, launch_date, completed_date, status")
|
|
err := query.Scan(&cs).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
return overview, err
|
|
}
|
|
for i := range cs {
|
|
s, err := getCampaignStats(cs[i].Id)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return overview, err
|
|
}
|
|
cs[i].Stats = s
|
|
}
|
|
overview.Total = int64(len(cs))
|
|
overview.Campaigns = cs
|
|
return overview, nil
|
|
}
|
|
|
|
// GetCampaignSummary gets the summary object for a campaign specified by the campaign ID
|
|
func GetCampaignSummary(id int64, uid int64) (CampaignSummary, error) {
|
|
cs := CampaignSummary{}
|
|
query := db.Table("campaigns").Where("user_id = ? AND id = ?", uid, id)
|
|
query = query.Select("id, name, created_date, launch_date, completed_date, status")
|
|
err := query.Scan(&cs).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
return cs, err
|
|
}
|
|
s, err := getCampaignStats(cs.Id)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return cs, err
|
|
}
|
|
cs.Stats = s
|
|
return cs, nil
|
|
}
|
|
|
|
// GetCampaign returns the campaign, if it exists, specified by the given id and user_id.
|
|
func GetCampaign(id int64, uid int64) (Campaign, error) {
|
|
c := Campaign{}
|
|
err := db.Where("id = ?", id).Where("user_id = ?", uid).Find(&c).Error
|
|
if err != nil {
|
|
log.Errorf("%s: campaign not found", err)
|
|
return c, err
|
|
}
|
|
err = c.getDetails()
|
|
return c, err
|
|
}
|
|
|
|
// GetCampaignResults returns just the campaign results for the given campaign
|
|
func GetCampaignResults(id int64, uid int64) (CampaignResults, error) {
|
|
cr := CampaignResults{}
|
|
err := db.Table("campaigns").Where("id=? and user_id=?", id, uid).Find(&cr).Error
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"campaign_id": id,
|
|
"error": err,
|
|
}).Error(err)
|
|
return cr, err
|
|
}
|
|
err = db.Table("results").Where("campaign_id=? and user_id=?", cr.Id, uid).Find(&cr.Results).Error
|
|
if err != nil {
|
|
log.Errorf("%s: results not found for campaign", err)
|
|
return cr, err
|
|
}
|
|
err = db.Table("events").Where("campaign_id=?", cr.Id).Find(&cr.Events).Error
|
|
if err != nil {
|
|
log.Errorf("%s: events not found for campaign", err)
|
|
return cr, err
|
|
}
|
|
return cr, err
|
|
}
|
|
|
|
// GetQueuedCampaigns returns the campaigns that are queued up for this given minute
|
|
func GetQueuedCampaigns(t time.Time) ([]Campaign, error) {
|
|
cs := []Campaign{}
|
|
err := db.Where("launch_date <= ?", t).
|
|
Where("status = ?", CAMPAIGN_QUEUED).Find(&cs).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Infof("Found %d Campaigns to run\n", len(cs))
|
|
for i := range cs {
|
|
err = cs[i].getDetails()
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
}
|
|
return cs, err
|
|
}
|
|
|
|
// PostCampaign inserts a campaign and all associated records into the database.
|
|
func PostCampaign(c *Campaign, uid int64) error {
|
|
if err := c.Validate(); err != nil {
|
|
return err
|
|
}
|
|
// Fill in the details
|
|
c.UserId = uid
|
|
c.CreatedDate = time.Now().UTC()
|
|
c.CompletedDate = time.Time{}
|
|
c.Status = CAMPAIGN_QUEUED
|
|
if c.LaunchDate.IsZero() {
|
|
c.LaunchDate = c.CreatedDate
|
|
} else {
|
|
c.LaunchDate = c.LaunchDate.UTC()
|
|
}
|
|
if c.LaunchDate.Before(c.CreatedDate) || c.LaunchDate.Equal(c.CreatedDate) {
|
|
c.Status = CAMPAIGN_IN_PROGRESS
|
|
}
|
|
// Check to make sure all the groups already exist
|
|
for i, g := range c.Groups {
|
|
c.Groups[i], err = GetGroupByName(g.Name, uid)
|
|
if err == gorm.ErrRecordNotFound {
|
|
log.WithFields(logrus.Fields{
|
|
"group": g.Name,
|
|
}).Error("Group does not exist")
|
|
return ErrGroupNotFound
|
|
} else if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
}
|
|
// Check to make sure the template exists
|
|
t, err := GetTemplateByName(c.Template.Name, uid)
|
|
if err == gorm.ErrRecordNotFound {
|
|
log.WithFields(logrus.Fields{
|
|
"template": t.Name,
|
|
}).Error("Template does not exist")
|
|
return ErrTemplateNotFound
|
|
} else if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
c.Template = t
|
|
c.TemplateId = t.Id
|
|
// Check to make sure the page exists
|
|
p, err := GetPageByName(c.Page.Name, uid)
|
|
if err == gorm.ErrRecordNotFound {
|
|
log.WithFields(logrus.Fields{
|
|
"page": p.Name,
|
|
}).Error("Page does not exist")
|
|
return ErrPageNotFound
|
|
} else if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
c.Page = p
|
|
c.PageId = p.Id
|
|
// Check to make sure the sending profile exists
|
|
s, err := GetSMTPByName(c.SMTP.Name, uid)
|
|
if err == gorm.ErrRecordNotFound {
|
|
log.WithFields(logrus.Fields{
|
|
"smtp": s.Name,
|
|
}).Error("Sending profile does not exist")
|
|
return ErrSMTPNotFound
|
|
} else if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
c.SMTP = s
|
|
c.SMTPId = s.Id
|
|
// Insert into the DB
|
|
err = db.Save(c).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
err = c.AddEvent(&Event{Message: "Campaign Created"})
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
// Insert all the results
|
|
resultMap := make(map[string]bool)
|
|
for _, g := range c.Groups {
|
|
// Insert a result for each target in the group
|
|
for _, t := range g.Targets {
|
|
// Remove duplicate results - we should only
|
|
// send emails to unique email addresses.
|
|
if _, ok := resultMap[t.Email]; ok {
|
|
continue
|
|
}
|
|
resultMap[t.Email] = true
|
|
r := &Result{
|
|
BaseRecipient: BaseRecipient{
|
|
Email: t.Email,
|
|
Position: t.Position,
|
|
FirstName: t.FirstName,
|
|
LastName: t.LastName,
|
|
},
|
|
Status: STATUS_SCHEDULED,
|
|
CampaignId: c.Id,
|
|
UserId: c.UserId,
|
|
SendDate: c.LaunchDate,
|
|
Reported: false,
|
|
ModifiedDate: c.CreatedDate,
|
|
}
|
|
if c.Status == CAMPAIGN_IN_PROGRESS {
|
|
r.Status = STATUS_SENDING
|
|
}
|
|
err = r.GenerateId()
|
|
if err != nil {
|
|
log.Error(err)
|
|
continue
|
|
}
|
|
err = db.Save(r).Error
|
|
if err != nil {
|
|
log.WithFields(logrus.Fields{
|
|
"email": t.Email,
|
|
}).Error(err)
|
|
}
|
|
c.Results = append(c.Results, *r)
|
|
err = GenerateMailLog(c, r)
|
|
if err != nil {
|
|
log.Error(err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
err = db.Save(c).Error
|
|
return err
|
|
}
|
|
|
|
//DeleteCampaign deletes the specified campaign
|
|
func DeleteCampaign(id int64) error {
|
|
log.WithFields(logrus.Fields{
|
|
"campaign_id": id,
|
|
}).Info("Deleting campaign")
|
|
// Delete all the campaign results
|
|
err := db.Where("campaign_id=?", id).Delete(&Result{}).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
err = db.Where("campaign_id=?", id).Delete(&Event{}).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
// Delete the campaign
|
|
err = db.Delete(&Campaign{Id: id}).Error
|
|
if err != nil {
|
|
log.Error(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 {
|
|
log.WithFields(logrus.Fields{
|
|
"campaign_id": id,
|
|
}).Info("Marking campaign as complete")
|
|
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().UTC()
|
|
c.Status = CAMPAIGN_COMPLETE
|
|
err = db.Where("id=? and user_id=?", id, uid).Save(&c).Error
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
return err
|
|
}
|