diff --git a/README.md b/README.md index 42709e6..f9d58cb 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,14 @@ base_url: https://10.0.0.15:3333 profiles: - slack - email +# Enables notifications for each of the webhook events https://docs.getgophish.com/user-guide/documentation/webhooks. Options are `email_error`, `email_sent`, `email_opened`, `clicked_link`, `submitted_data` and `email_reported`. +events: + - email_error + - email_sent + - email_opened + - clicked_link + - submitted_data + - email_reported # Slack Profile slack: diff --git a/config.go b/config.go index ea7eb9c..bfbd596 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,10 @@ import ( "github.com/spf13/viper" ) +var defaultErrorTemplate = `Email ID - {{ .ID }}` + +var defaultSentTemplate = `Email ID - {{ .ID }}` + var defaultClickedTemplate = `Email ID - {{ .ID }} Email Address - {{ .Email }} IP Address - {{ .Address }} @@ -22,6 +26,8 @@ Email Address - {{ .Email }} IP Address - {{ .Address }} User Agent - {{ .UserAgent }}` +var defaultReportedTemplate = `Email ID - {{ .ID }}` + var defaultgraphqlTemplate = `mutation InsertGophishLog ($oplog: bigint!, $sourceIp: String, $tool: String, $userContext: String, $description: String, $output: String, $comments: String, $extraFields: jsonb!) { insert_oplogEntry(objects: {oplog: $oplog, sourceIp: $sourceIp, tool: $tool, userContext: $userContext, description: $description, comments: $comments, output: $output, extraFields: $extraFields}) { returning { @@ -53,11 +59,15 @@ func setDefaults() { viper.SetDefault("ip_query_base", "https://whatismyipaddress.com/ip/") viper.SetDefault("listen_port", "9999") viper.SetDefault("webhook_path", "/webhook") + viper.SetDefault("email_error_sending_template", defaultErrorTemplate) + viper.SetDefault("email_sent_template", defaultSentTemplate) viper.SetDefault("email_send_click_template", defaultClickedTemplate) viper.SetDefault("email_submitted_credentials_template", defaultSubmittedCredentailsTemplate) viper.SetDefault("email_default_email_open_template", defaultEmailOpenedTemplate) + viper.SetDefault("email_reported_template", defaultReportedTemplate) viper.SetDefault("graphql_default_query", defaultgraphqlTemplate) viper.SetDefault("profiles", []string{"slack"}) + viper.SetDefault("events", []string{"email_opened", "clicked_link", "submitted_data"}) } func setLogLevel() { @@ -77,7 +87,7 @@ func validateConfig() { } } - globalConfigs := []string{"secret", "profiles"} + globalConfigs := []string{"secret", "profiles", "events"} checkKeysExist(globalConfigs...) profiles := viper.GetStringSlice("profiles") diff --git a/main.go b/main.go index 3cd49a6..6cd7c6d 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,12 @@ func handler(w http.ResponseWriter, r *http.Request) { return } - sender, err := senderDispatch(response.Message, response, []byte(response.Details)) + var details []byte + if response.Details != nil { + details = []byte(*response.Details) + } + + sender, err := senderDispatch(response.Message, response, details) if err != nil { log.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/messages.go b/messages.go index 66518cd..5e747bc 100644 --- a/messages.go +++ b/messages.go @@ -14,9 +14,12 @@ import ( // Can't be const because need reference to variable for Slack webhook title var ( + EmailError string = "Error Sending Email" + EmailSent string = "Email Sent" + EmailOpened string = "Email Opened" ClickedLink string = "Clicked Link" SubmittedData string = "Submitted Data" - EmailOpened string = "Email Opened" + EmailReported string = "Email Reported" ) type Sender interface { @@ -25,16 +28,35 @@ type Sender interface { SendGraphql() error } +func contains(slice []string, str string) bool { + for _, a := range slice { + if a == str { + return true + } + } + return false +} + func senderDispatch(status string, webhookResponse WebhookResponse, response []byte) (Sender, error) { - if status == ClickedLink { - return NewClickDetails(webhookResponse, response) + enabled_events := viper.GetStringSlice("events") + if status == EmailError && contains(enabled_events, "email_error") { + return NewErrorDetails(webhookResponse) } - if status == EmailOpened { + if status == EmailSent && contains(enabled_events, "email_sent") { + return NewSentDetails(webhookResponse) + } + if status == EmailOpened && contains(enabled_events, "email_opened") { return NewOpenedDetails(webhookResponse, response) } - if status == SubmittedData { + if status == ClickedLink && contains(enabled_events, "clicked_link") { + return NewClickDetails(webhookResponse, response) + } + if status == SubmittedData && contains(enabled_events, "submitted_data") { return NewSubmittedDetails(webhookResponse, response) } + if status == EmailReported && contains(enabled_events, "email_reported") { + return NewReportedDetails(webhookResponse) + } log.Warn("unknown status:", status) return nil, nil } @@ -42,11 +64,11 @@ func senderDispatch(status string, webhookResponse WebhookResponse, response []b // More information about events can be found here: // https://github.com/gophish/gophish/blob/db63ee978dcd678caee0db71e5e1b91f9f293880/models/result.go#L50 type WebhookResponse struct { - Success bool `json:"success"` - CampaignID uint `json:"campaign_id"` - Message string `json:"message"` - Details string `json:"details"` - Email string `json:"email"` + Success bool `json:"success"` + CampaignID uint `json:"campaign_id"` + Message string `json:"message"` + Details *string `json:"details"` + Email string `json:"email"` } func NewWebhookResponse(body []byte) (WebhookResponse, error) { @@ -81,68 +103,140 @@ func (e EventDetails) Address() string { return e.Browser["address"] } -type SubmittedDetails struct { +type ErrorDetails struct { + CampaignID uint + Email string +} + +func NewErrorDetails(response WebhookResponse) (ErrorDetails, error) { + errorDetails := ErrorDetails{ + CampaignID: response.CampaignID, + Email: response.Email, + } + return errorDetails, nil +} + +func (w ErrorDetails) SendSlack() error { + orange := "#ffa500" + attachment := slack.Attachment{Title: &EmailError, Color: &orange} + if !viper.GetBool("slack.disable_credentials") { + attachment.AddField(slack.Field{Title: "Email", Value: w.Email}) + } + attachment = addCampaignButton(attachment, w.CampaignID) + return sendSlackAttachment(attachment) +} + +func (w ErrorDetails) SendEmail() error { + templateString := viper.GetString("email_error_sending_template") + body, err := getEmailBody(templateString, w) + if err != nil { + return err + } + return sendEmail("PhishBot - Error Sending Email", body) +} + +func (w ErrorDetails) SendGraphql() error { + oplog_entry := ghostwriterOplogEntry{ + UserContext: w.Email, + Description: "Campaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Comments: EmailError, + } + return sendGraphql(oplog_entry) +} + +type SentDetails struct { + CampaignID uint + Email string +} + +func NewSentDetails(response WebhookResponse) (SentDetails, error) { + sentDetails := SentDetails{ + CampaignID: response.CampaignID, + Email: response.Email, + } + return sentDetails, nil +} + +func (w SentDetails) SendSlack() error { + orange := "#ffa500" + attachment := slack.Attachment{Title: &EmailSent, Color: &orange} + if !viper.GetBool("slack.disable_credentials") { + attachment.AddField(slack.Field{Title: "Email", Value: w.Email}) + } + attachment = addCampaignButton(attachment, w.CampaignID) + return sendSlackAttachment(attachment) +} + +func (w SentDetails) SendEmail() error { + templateString := viper.GetString("email_sent_template") + body, err := getEmailBody(templateString, w) + if err != nil { + return err + } + return sendEmail("PhishBot - Email Sent", body) +} + +func (w SentDetails) SendGraphql() error { + oplog_entry := ghostwriterOplogEntry{ + UserContext: w.Email, + Description: "Campaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Comments: EmailSent, + } + return sendGraphql(oplog_entry) +} + +type OpenedDetails struct { CampaignID uint ID string Email string Address string UserAgent string - Username string - Password string } -func NewSubmittedDetails(response WebhookResponse, detailsRaw []byte) (SubmittedDetails, error) { +func NewOpenedDetails(response WebhookResponse, detailsRaw []byte) (OpenedDetails, error) { details, err := NewEventDetails(detailsRaw) if err != nil { - return SubmittedDetails{}, err + return OpenedDetails{}, err } - submittedDetails := SubmittedDetails{ + clickDetails := OpenedDetails{ CampaignID: response.CampaignID, ID: details.ID(), + Email: response.Email, Address: details.Address(), UserAgent: details.UserAgent(), - Email: response.Email, - Username: details.Payload.Get("username"), - Password: details.Payload.Get("password"), } - return submittedDetails, nil + return clickDetails, nil } -func (w SubmittedDetails) SendSlack() error { - red := "#f05b4f" - attachment := slack.Attachment{Title: &SubmittedData, Color: &red} +func (w OpenedDetails) SendSlack() error { + yellow := "#ffff00" + attachment := slack.Attachment{Title: &EmailOpened, Color: &yellow} attachment.AddField(slack.Field{Title: "ID", Value: w.ID}) attachment.AddField(slack.Field{Title: "Address", Value: slackFormatIP(w.Address)}) attachment.AddField(slack.Field{Title: "User Agent", Value: w.UserAgent}) if !viper.GetBool("slack.disable_credentials") { attachment.AddField(slack.Field{Title: "Email", Value: w.Email}) - attachment.AddField(slack.Field{Title: "Username", Value: w.Username}) - attachment.AddField(slack.Field{Title: "Password", Value: w.Password}) } attachment = addCampaignButton(attachment, w.CampaignID) return sendSlackAttachment(attachment) } -func (w SubmittedDetails) SendEmail() error { - templateString := viper.GetString("email_submitted_credentials_template") +func (w OpenedDetails) SendEmail() error { + templateString := viper.GetString("email_send_click_template") body, err := getEmailBody(templateString, w) if err != nil { return err } - return sendEmail("PhishBot - Credentials Submitted", body) + return sendEmail("PhishBot - Email Opened", body) } -func (w SubmittedDetails) SendGraphql() error { - var output string - if !viper.GetBool("ghostwriter.disable_credentials") { - output = "\nUsername: " + w.Username + "\nPassword: " + w.Password - } +func (w OpenedDetails) SendGraphql() error { oplog_entry := ghostwriterOplogEntry{ SourceIp: w.Address, UserContext: w.Email, Description: "User ID: " + w.ID + "\nCampaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), - Output: output, - Comments: SubmittedData, + Output: "UserAgent: " + w.UserAgent, + Comments: EmailOpened, } return sendGraphql(oplog_entry) } @@ -215,58 +309,109 @@ func getEmailBody(templateValue string, obj interface{}) (string, error) { return out.String(), nil } -type OpenedDetails struct { +type SubmittedDetails struct { CampaignID uint ID string Email string Address string UserAgent string + Username string + Password string } -func NewOpenedDetails(response WebhookResponse, detailsRaw []byte) (OpenedDetails, error) { +func NewSubmittedDetails(response WebhookResponse, detailsRaw []byte) (SubmittedDetails, error) { details, err := NewEventDetails(detailsRaw) if err != nil { - return OpenedDetails{}, err + return SubmittedDetails{}, err } - clickDetails := OpenedDetails{ + submittedDetails := SubmittedDetails{ CampaignID: response.CampaignID, ID: details.ID(), - Email: response.Email, Address: details.Address(), UserAgent: details.UserAgent(), + Email: response.Email, + Username: details.Payload.Get("username"), + Password: details.Payload.Get("password"), } - return clickDetails, nil + return submittedDetails, nil } -func (w OpenedDetails) SendSlack() error { - yellow := "#ffff00" - attachment := slack.Attachment{Title: &EmailOpened, Color: &yellow} +func (w SubmittedDetails) SendSlack() error { + red := "#f05b4f" + attachment := slack.Attachment{Title: &SubmittedData, Color: &red} attachment.AddField(slack.Field{Title: "ID", Value: w.ID}) attachment.AddField(slack.Field{Title: "Address", Value: slackFormatIP(w.Address)}) attachment.AddField(slack.Field{Title: "User Agent", Value: w.UserAgent}) if !viper.GetBool("slack.disable_credentials") { attachment.AddField(slack.Field{Title: "Email", Value: w.Email}) + attachment.AddField(slack.Field{Title: "Username", Value: w.Username}) + attachment.AddField(slack.Field{Title: "Password", Value: w.Password}) } attachment = addCampaignButton(attachment, w.CampaignID) return sendSlackAttachment(attachment) } -func (w OpenedDetails) SendEmail() error { - templateString := viper.GetString("email_send_click_template") +func (w SubmittedDetails) SendEmail() error { + templateString := viper.GetString("email_submitted_credentials_template") body, err := getEmailBody(templateString, w) if err != nil { return err } - return sendEmail("PhishBot - Email Opened", body) + return sendEmail("PhishBot - Credentials Submitted", body) } -func (w OpenedDetails) SendGraphql() error { +func (w SubmittedDetails) SendGraphql() error { + var output string + if !viper.GetBool("ghostwriter.disable_credentials") { + output = "\nUsername: " + w.Username + "\nPassword: " + w.Password + } oplog_entry := ghostwriterOplogEntry{ SourceIp: w.Address, UserContext: w.Email, Description: "User ID: " + w.ID + "\nCampaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), - Output: "UserAgent: " + w.UserAgent, - Comments: EmailOpened, + Output: output, + Comments: SubmittedData, + } + return sendGraphql(oplog_entry) +} + +type ReportedDetails struct { + CampaignID uint + Email string +} + +func NewReportedDetails(response WebhookResponse) (ReportedDetails, error) { + reportedDetails := ReportedDetails{ + CampaignID: response.CampaignID, + Email: response.Email, + } + return reportedDetails, nil +} + +func (w ReportedDetails) SendSlack() error { + orange := "#ffa500" + attachment := slack.Attachment{Title: &EmailReported, Color: &orange} + if !viper.GetBool("slack.disable_credentials") { + attachment.AddField(slack.Field{Title: "Email", Value: w.Email}) + } + attachment = addCampaignButton(attachment, w.CampaignID) + return sendSlackAttachment(attachment) +} + +func (w ReportedDetails) SendEmail() error { + templateString := viper.GetString("email_reported_template") + body, err := getEmailBody(templateString, w) + if err != nil { + return err + } + return sendEmail("PhishBot - Email Reported", body) +} + +func (w ReportedDetails) SendGraphql() error { + oplog_entry := ghostwriterOplogEntry{ + UserContext: w.Email, + Description: "Campaign ID: " + strconv.FormatUint(uint64(w.CampaignID), 10), + Comments: EmailReported, } return sendGraphql(oplog_entry) } diff --git a/sending_helpers.go b/sending_helpers.go index b7a93bd..e698ca8 100644 --- a/sending_helpers.go +++ b/sending_helpers.go @@ -79,12 +79,22 @@ func sendGraphql(data ghostwriterOplogEntry) error { req := graphql.NewRequest(query) req.Header.Set("Authorization", "Bearer "+apiKey) req.Var("oplog", oplogId) - req.Var("sourceIp", data.SourceIp) + if data.SourceIp != "" { + req.Var("sourceIp", data.SourceIp) + } req.Var("tool", "gophish") - req.Var("userContext", data.UserContext) - req.Var("description", data.Description) - req.Var("output", data.Output) - req.Var("comments", data.Comments) + if data.UserContext != "" { + req.Var("userContext", data.UserContext) + } + if data.Description != "" { + req.Var("description", data.Description) + } + if data.Output != "" { + req.Var("output", data.Output) + } + if data.Comments != "" { + req.Var("comments", data.Comments) + } req.Var("extraFields", "") ctx := context.Background()