diff --git a/backend/app/server.go b/backend/app/server.go index 02b5167..e3521c3 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -979,11 +979,23 @@ func (s *Server) checkAndServePhishingPage( clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request)) userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED)) submittedData := vo.NewEmptyOptionalString1MB() + + // prepare submitted data for webhook + var webhookData map[string]interface{} if campaign.SaveSubmittedData.MustGet() { submittedData, err = vo.NewOptionalString1MB(c.Request.PostForm.Encode()) if err != nil { return true, fmt.Errorf("user submitted phishing data too large: %s", err) } + // convert form data to map for webhook + webhookData = make(map[string]interface{}) + for key, values := range c.Request.PostForm { + if len(values) == 1 { + webhookData[key] = values[0] + } else { + webhookData[key] = values + } + } } var event *model.CampaignEvent // only save data if red team flag is set @@ -1058,6 +1070,7 @@ func (s *Server) checkAndServePhishingPage( &campaignID, &recipientID, data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA, + webhookData, ) if err != nil { return true, fmt.Errorf("failed to handle webhook: %s", err) @@ -1317,6 +1330,7 @@ func (s *Server) checkAndServePhishingPage( &campaignID, &recipientID, eventName, + nil, ) if err != nil { s.logger.Errorw("failed to handle webhook for Proxy page", @@ -1629,6 +1643,7 @@ func (s *Server) checkAndServePhishingPage( &campaignID, &recipientID, eventName, + nil, ) if err != nil { return true, fmt.Errorf("failed to handle webhook: %s", err) @@ -1836,6 +1851,7 @@ func (s *Server) renderDenyPage( &campaignID, &recipientID, data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED, + nil, ) if err != nil { s.logger.Errorw("failed to handle webhook for deny page visit", diff --git a/backend/database/campaign.go b/backend/database/campaign.go index 1eaaebd..3791706 100644 --- a/backend/database/campaign.go +++ b/backend/database/campaign.go @@ -44,6 +44,7 @@ type Campaign struct { IsAnonymous bool `gorm:"not null;default:false"` IsTest bool `gorm:"not null;default:false"` Obfuscate bool `gorm:"not null;default:false"` + WebhookIncludeData bool `gorm:"not null;default:false"` // has one CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"` diff --git a/backend/model/campaign.go b/backend/model/campaign.go index 2c570b5..cdebd35 100644 --- a/backend/model/campaign.go +++ b/backend/model/campaign.go @@ -38,6 +38,7 @@ type Campaign struct { IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"` IsTest nullable.Nullable[bool] `json:"isTest"` Obfuscate nullable.Nullable[bool] `json:"obfuscate"` + WebhookIncludeData nullable.Nullable[bool] `json:"webhookIncludeData"` TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"` Template *CampaignTemplate `json:"template"` CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` @@ -332,6 +333,12 @@ func (c *Campaign) ToDBMap() map[string]any { m["obfuscate"] = v } } + if c.WebhookIncludeData.IsSpecified() { + m["webhook_include_data"] = false + if v, err := c.WebhookIncludeData.Get(); err == nil { + m["webhook_include_data"] = v + } + } if c.TemplateID.IsSpecified() { m["campaign_template_id"] = nil if v, err := c.TemplateID.Get(); err == nil { diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index c702db9..a09b24d 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -1584,7 +1584,11 @@ func (m *ProxyHandler) handlePathBasedCapture(capture service.ProxyServiceCaptur m.checkCaptureCompletion(session, capture.Name) if session.CampaignRecipientID != nil && session.CampaignID != nil { - m.createCampaignSubmitEvent(session, capturedData, resp.Request) + // convert to map[string]interface{} for webhook + webhookData := map[string]interface{}{ + capture.Name: capturedData, + } + m.createCampaignSubmitEvent(session, webhookData, resp.Request) } // check if cookie bundle should be submitted now that this capture is complete @@ -1682,7 +1686,11 @@ func (m *ProxyHandler) captureFromText(text string, capture service.ProxyService // submit non-cookie captures immediately if capture.From != "cookie" && session.CampaignRecipientID != nil && session.CampaignID != nil { - m.createCampaignSubmitEvent(session, capturedData, req) + // convert to map[string]interface{} for webhook + webhookData := map[string]interface{}{ + capture.Name: capturedData, + } + m.createCampaignSubmitEvent(session, webhookData, req) } // check if we should submit cookie bundle (only when all captures complete) @@ -2643,7 +2651,7 @@ func (m *ProxyHandler) buildCampaignFlowRedirectURL(session *service.ProxySessio return targetURL } -func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession, capturedData interface{}, req *http.Request) { +func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession, capturedData map[string]interface{}, req *http.Request) { if session.CampaignID == nil || session.CampaignRecipientID == nil { return } @@ -2714,6 +2722,7 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession, session.CampaignID, session.RecipientID, data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA, + capturedData, ) if err != nil { m.logger.Errorw("failed to handle webhook for MITM proxy submit", @@ -3442,6 +3451,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic session.CampaignID, session.RecipientID, eventName, + nil, ) if err != nil { m.logger.Errorw("failed to handle webhook for MITM page visit", @@ -3972,6 +3982,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt campaignID, recipientID, data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED, + nil, ) if err != nil { m.logger.Errorw("failed to handle webhook for deny page visit", @@ -4044,6 +4055,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re campaignID, recipientID, data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED, + nil, ) if err != nil { m.logger.Errorw("failed to handle webhook for evasion page visit", diff --git a/backend/repository/campaign.go b/backend/repository/campaign.go index cf24cbe..d7be788 100644 --- a/backend/repository/campaign.go +++ b/backend/repository/campaign.go @@ -1487,6 +1487,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) { isAnonymous := nullable.NewNullableWithValue(row.IsAnonymous) isTest := nullable.NewNullableWithValue(row.IsTest) obfuscate := nullable.NewNullableWithValue(row.Obfuscate) + webhookIncludeData := nullable.NewNullableWithValue(row.WebhookIncludeData) var templateID nullable.Nullable[uuid.UUID] if row.CampaignTemplateID != nil { templateID = nullable.NewNullableWithValue(*row.CampaignTemplateID) @@ -1615,6 +1616,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) { IsAnonymous: isAnonymous, IsTest: isTest, Obfuscate: obfuscate, + WebhookIncludeData: webhookIncludeData, TemplateID: templateID, Template: template, RecipientGroups: recipientGroups, diff --git a/backend/service/campaign.go b/backend/service/campaign.go index f4d7a5a..e74e449 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -1158,6 +1158,7 @@ func (c *Campaign) SaveTrackingPixelLoaded( &campaignID, &recipientID, data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ, + nil, ) if err != nil { return errs.Wrap(err) @@ -1274,6 +1275,9 @@ func (c *Campaign) UpdateByID( if v, err := incoming.Obfuscate.Get(); err == nil { current.Obfuscate.Set(v) } + if v, err := incoming.WebhookIncludeData.Get(); err == nil { + current.WebhookIncludeData.Set(v) + } if v, err := incoming.SortField.Get(); err == nil { current.SortField.Set(v) } @@ -2272,6 +2276,7 @@ func (c *Campaign) saveSendingResult( &campaignID, &recipientID, eventName, + nil, ) if err != nil { return errs.Wrap(err) @@ -2320,6 +2325,7 @@ func (c *Campaign) saveEventCampaignClose( campaignID, nil, data.EVENT_CAMPAIGN_CLOSED, + nil, ) if err != nil { return errs.Wrap(err) @@ -3023,6 +3029,7 @@ func (c *Campaign) SetSentAtByCampaignRecipientID( &campaignID, &recipientID, data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT, + nil, ) if err != nil { return errs.Wrap(err) @@ -3038,6 +3045,7 @@ func (c *Campaign) HandleWebhook( campaignID *uuid.UUID, recipientID *uuid.UUID, eventName string, + capturedData map[string]interface{}, ) error { campaignName, err := c.CampaignRepository.GetNameByID(ctx, campaignID) if err != nil { @@ -3051,11 +3059,25 @@ func (c *Campaign) HandleWebhook( if err != nil { return errs.Wrap(err) } + + // check if campaign has webhookIncludeData enabled + campaign, err := c.CampaignRepository.GetByID(ctx, campaignID, &repository.CampaignOption{}) + if err != nil { + return errs.Wrap(err) + } + + // only include captured data if webhookIncludeData is enabled + var dataToSend map[string]interface{} + if campaign.WebhookIncludeData.MustGet() { + dataToSend = capturedData + } + now := time.Now() webhookReq := WebhookRequest{ Time: &now, CampaignName: campaignName, Event: eventName, + Data: dataToSend, } if email != nil { webhookReq.Email = email.String() diff --git a/backend/service/webhook.go b/backend/service/webhook.go index a172baa..152d6f1 100644 --- a/backend/service/webhook.go +++ b/backend/service/webhook.go @@ -387,8 +387,9 @@ func (w *Webhook) Send( } type WebhookRequest struct { - Time *time.Time `json:"time"` - CampaignName string `json:"campaignName"` - Email string `json:"email"` - Event string `json:"event"` + Time *time.Time `json:"time"` + CampaignName string `json:"campaignName"` + Email string `json:"email"` + Event string `json:"event"` + Data map[string]interface{} `json:"data,omitempty"` } diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index accb05c..f859ef4 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -497,6 +497,7 @@ export class API { * @param {string} campaign.templateID uuid * @param {string} campaign.name * @param {boolean} [campaign.saveSubmittedData] + * @param {boolean} [campaign.saveBrowserMetadata] * @param {boolean} [campaign.isAnonymous] * @param {boolean} [campaign.isTest] * @param {boolean} [campaign.obfuscate] @@ -511,6 +512,7 @@ export class API { * @param {string} campaign.denyPageID uuid * @param {string} campaign.evasionPageID uuid * @param {string} campaign.webhookID uuid + * @param {boolean} [campaign.webhookIncludeData] * @param {Array} [campaign.constraintWeekDays] * @param {string} [campaign.constraintStartTime] * @param {string} [campaign.constraintEndTime] @@ -536,6 +538,7 @@ export class API { denyPageID, evasionPageID, webhookID, + webhookIncludeData, constraintWeekDays, constraintStartTime, constraintEndTime @@ -560,6 +563,7 @@ export class API { denyPageID, evasionPageID, webhookID, + webhookIncludeData, constraintWeekDays, constraintStartTime, constraintEndTime @@ -567,11 +571,11 @@ export class API { }, /** - * - * @param {object} campaign - * @param {string} campaign.id + * @param {Object} campaign + * @param {string} campaign.id uuid * @param {string} campaign.name * @param {boolean} [campaign.saveSubmittedData] + * @param {boolean} [campaign.saveBrowserMetadata] * @param {boolean} [campaign.isAnonymous] * @param {boolean} [campaign.isTest] * @param {boolean} [campaign.obfuscate] @@ -581,12 +585,13 @@ export class API { * @param {string} campaign.sendEndAt * @param {string} [campaign.closeAt] * @param {string} [campaign.anonymizeAt] - * @param {string} campaign.templateID uuid + * @param {string} campaign.templateID uuid * @param {string[]} campaign.recipientGroupIDs []uuid * @param {string[]} campaign.allowDenyIDs []uuid * @param {string} campaign.denyPageID uuid * @param {string} campaign.evasionPageID uuid * @param {string} campaign.webhookID uuid + * @param {boolean} [campaign.webhookIncludeData] * @param {Array} [campaign.constraintWeekDays] * @param {string} [campaign.constraintStartTime] * @param {string} [campaign.constraintEndTime] @@ -594,6 +599,7 @@ export class API { */ update: async ({ id, + templateID, name, saveSubmittedData, saveBrowserMetadata, @@ -606,17 +612,18 @@ export class API { sendEndAt, closeAt, anonymizeAt, - templateID, recipientGroupIDs, allowDenyIDs, denyPageID, evasionPageID, webhookID, + webhookIncludeData, constraintWeekDays, constraintStartTime, constraintEndTime }) => { return await postJSON(this.getPath(`/campaign/${id}`), { + templateID, name, isAnonymous, isTest, @@ -629,12 +636,12 @@ export class API { sendEndAt, closeAt, anonymizeAt, - templateID, recipientGroupIDs, allowDenyIDs, denyPageID, evasionPageID, webhookID, + webhookIncludeData, constraintWeekDays, constraintStartTime, constraintEndTime diff --git a/frontend/src/routes/campaign/+page.svelte b/frontend/src/routes/campaign/+page.svelte index 970d9fc..da16e39 100644 --- a/frontend/src/routes/campaign/+page.svelte +++ b/frontend/src/routes/campaign/+page.svelte @@ -258,7 +258,8 @@ isTest: false, obfuscate: false, selectedCount: 0, - webhookValue: null + webhookValue: null, + webhookIncludeData: false }; let modalError = ''; @@ -636,7 +637,8 @@ constraintWeekDays: weekDaysAvailableToBinary(formValues.constraintWeekDays), constraintStartTime: contraintStartTimeUTC, constraintEndTime: contraintEndTimeUTC, - webhookID: webhookMap.byValueOrNull(formValues.webhookValue) + webhookID: webhookMap.byValueOrNull(formValues.webhookValue), + webhookIncludeData: formValues.webhookIncludeData }); if (!res.success) { @@ -699,7 +701,8 @@ allowDenyIDs: allowDenyIDs, denyPageID: denyPageMap.byValueOrNull(formValues.denyPageValue), evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue), - webhookID: webhookMap.byValueOrNull(formValues.webhookValue) + webhookID: webhookMap.byValueOrNull(formValues.webhookValue), + webhookIncludeData: formValues.webhookIncludeData }); if (!res.success) { @@ -824,7 +827,8 @@ isTest: false, obfuscate: false, selectedCount: 0, - webhookValue: null + webhookValue: null, + webhookIncludeData: false }; scheduleType = 'basic'; allowDenyType = 'none'; @@ -929,7 +933,8 @@ isTest: campaign.isTest, obfuscate: campaign.obfuscate || false, template: templateMap.byKey(campaign.templateID), - webhookValue: webhookMap.byKey(campaign.webhookID) + webhookValue: webhookMap.byKey(campaign.webhookID), + webhookIncludeData: campaign.webhookIncludeData || false }; if (copyMode) { @@ -1595,6 +1600,24 @@ options={Array.from(webhookMap.values())}>Webhook + {#if formValues.webhookValue} +
+ When enabled, captured data (credentials, cookies, etc.) will be included in the + webhook payload +
+