add option to add data to webhook events

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-14 20:50:50 +01:00
parent 68d3466efb
commit 70c7b6203d
9 changed files with 113 additions and 18 deletions
+16
View File
@@ -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",
+1
View File
@@ -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;"`
+7
View File
@@ -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 {
+15 -3
View File
@@ -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",
+2
View File
@@ -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,
+22
View File
@@ -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()
+5 -4
View File
@@ -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"`
}
+13 -6
View File
@@ -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
+32 -5
View File
@@ -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</TextFieldSelect
>
</div>
{#if formValues.webhookValue}
<div class="mb-6">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
bind:checked={formValues.webhookIncludeData}
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
Include captured data in webhook
</span>
</label>
<p class="mt-1 ml-6 text-xs text-gray-500 dark:text-gray-400">
When enabled, captured data (credentials, cookies, etc.) will be included in the
webhook payload
</p>
</div>
{/if}
<div class="mb-6">
<SelectSquare
@@ -1948,6 +1971,10 @@
<span class="text-grayblue-dark font-medium">Webhook:</span>
<span class="text-pc-darkblue dark:text-white">{formValues.webhookValue}</span
>
<span class="text-grayblue-dark font-medium">Include Data:</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.webhookIncludeData ? 'Yes' : 'No'}</span
>
{/if}
{#if formValues.denyPageValue}