diff --git a/backend/app/server.go b/backend/app/server.go index cd5775b..af49b18 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -1577,6 +1577,57 @@ func (s *Server) renderDenyPage( c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes()) c.Abort() + + // log deny page visited event + denyPageVisitEventID := uuid.New() + eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED] + clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request)) + userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED)) + + var denyPageVisitEvent *model.CampaignEvent + if !campaign.IsAnonymous.MustGet() { + denyPageVisitEvent = &model.CampaignEvent{ + ID: &denyPageVisitEventID, + CampaignID: &campaignID, + RecipientID: &recipientID, + IP: clientIP, + UserAgent: userAgent, + EventID: eventID, + Data: vo.NewEmptyOptionalString1MB(), + } + } else { + denyPageVisitEvent = &model.CampaignEvent{ + ID: &denyPageVisitEventID, + CampaignID: &campaignID, + RecipientID: nil, + IP: vo.NewEmptyOptionalString64(), + UserAgent: vo.NewEmptyOptionalString255(), + EventID: eventID, + Data: vo.NewEmptyOptionalString1MB(), + } + } + + err = s.repositories.Campaign.SaveEvent(c, denyPageVisitEvent) + if err != nil { + s.logger.Errorw("failed to save deny page visit event", + "error", err, + "campaignID", campaignID.String(), + ) + } + + // check and update if most notable event for recipient + currentNotableEventID, _ := campaignRecipient.NotableEventID.Get() + if cache.IsMoreNotableCampaignRecipientEventID(¤tNotableEventID, eventID) { + campaignRecipient.NotableEventID.Set(*eventID) + err := s.repositories.CampaignRecipient.UpdateByID(c, &campaignRecipientID, campaignRecipient) + if err != nil { + s.logger.Errorw("failed to update campaign recipient notable event", + "error", err, + "campaignRecipientID", campaignRecipientID.String(), + ) + } + } + // safely log with nil checks pageName := "unknown" if pageNameVal, err := page.Name.Get(); err == nil { diff --git a/backend/cache/local.go b/backend/cache/local.go index ce75cc2..24c4775 100644 --- a/backend/cache/local.go +++ b/backend/cache/local.go @@ -43,6 +43,7 @@ var CampaignEventPriority = map[string]int{ data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED: 60, data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED: 40, data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED: 50, + data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED: 38, data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED: 35, data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: 30, data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: 20, diff --git a/backend/data/events.go b/backend/data/events.go index 579bc8d..b1e288d 100644 --- a/backend/data/events.go +++ b/backend/data/events.go @@ -14,6 +14,7 @@ const ( EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED = "campaign_recipient_before_page_visited" EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED = "campaign_recipient_page_visited" EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED = "campaign_recipient_after_page_visited" + EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED = "campaign_recipient_deny_page_visited" EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA = "campaign_recipient_submitted_data" EVENT_CAMPAIGN_RECIPIENT_REPORTED = "campaign_recipient_reported" EVENT_CAMPAIGN_RECIPIENT_CANCELLED = "campaign_recipient_cancelled" @@ -34,6 +35,7 @@ var Events = []string{ EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED, EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED, EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED, + EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED, EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA, EVENT_CAMPAIGN_RECIPIENT_REPORTED, EVENT_CAMPAIGN_RECIPIENT_CANCELLED, diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index 2ccf15f..ce9a5bd 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -3457,6 +3457,9 @@ func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *Re resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(htmlContent))) resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + // log deny page visit event + m.registerDenyPageVisitEventDirect(req, reqCtx) + return resp } @@ -3670,6 +3673,63 @@ func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *Requ return buf.String(), nil } +func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCtx *RequestContext) { + // use cached recipient data + if reqCtx.CampaignRecipient == nil || reqCtx.RecipientID == nil || reqCtx.CampaignID == nil || reqCtx.Campaign == nil { + return + } + + recipientID := reqCtx.RecipientID + campaignID := reqCtx.CampaignID + campaign := reqCtx.Campaign + + eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED] + newEventID := uuid.New() + clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req)) + userAgent := vo.NewOptionalString255Must(utils.Substring(req.UserAgent(), 0, 1000)) // MAX_USER_AGENT_SAVED equivalent + + var event *model.CampaignEvent + if !campaign.IsAnonymous.MustGet() { + event = &model.CampaignEvent{ + ID: &newEventID, + CampaignID: campaignID, + RecipientID: recipientID, + IP: clientIP, + UserAgent: userAgent, + EventID: eventID, + Data: vo.NewEmptyOptionalString1MB(), + } + } else { + ua := vo.NewEmptyOptionalString255() + event = &model.CampaignEvent{ + ID: &newEventID, + CampaignID: campaignID, + RecipientID: nil, + IP: vo.NewEmptyOptionalString64(), + UserAgent: ua, + EventID: eventID, + Data: vo.NewEmptyOptionalString1MB(), + } + } + + err := m.CampaignRepository.SaveEvent(req.Context(), event) + if err != nil { + m.logger.Errorw("failed to save deny page visit event", "error", err) + } + + // check and update if most notable event for recipient + cRecipient := reqCtx.CampaignRecipient + currentNotableEventID, _ := cRecipient.NotableEventID.Get() + if cache.IsMoreNotableCampaignRecipientEventID(¤tNotableEventID, eventID) { + cRecipient.NotableEventID.Set(*eventID) + campaignRecipientID := reqCtx.CampaignRecipientID + err := m.CampaignRecipientRepository.UpdateByID(req.Context(), campaignRecipientID, cRecipient) + if err != nil { + m.logger.Errorw("failed to update campaign recipient notable event for deny page", "error", err) + } + } +} + func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, reqCtx *RequestContext) { // use cached recipient data if reqCtx.CampaignRecipient == nil || reqCtx.RecipientID == nil || reqCtx.CampaignID == nil || reqCtx.Campaign == nil { diff --git a/frontend/src/lib/components/EventTimeline.svelte b/frontend/src/lib/components/EventTimeline.svelte index 17c21cb..9a9223f 100644 --- a/frontend/src/lib/components/EventTimeline.svelte +++ b/frontend/src/lib/components/EventTimeline.svelte @@ -45,6 +45,7 @@ campaign_recipient_before_page_visited: true, campaign_recipient_page_visited: true, campaign_recipient_after_page_visited: true, + campaign_recipient_deny_page_visited: true, campaign_recipient_submitted_data: true, campaign_recipient_reported: true }; @@ -741,9 +742,11 @@ campaign_recipient_message_sent: '#94cae6', campaign_recipient_message_failed: '#f2bb58', campaign_recipient_message_read: '#4cb5b5', + campaign_recipient_evasion_page_visited: '#c8a2f0', campaign_recipient_before_page_visited: '#eea5fa', campaign_recipient_page_visited: '#f96dcf', campaign_recipient_after_page_visited: '#f6287b', + campaign_recipient_deny_page_visited: '#ff6b35', campaign_recipient_submitted_data: '#f42e41', campaign_recipient_reported: '#2c3e50' }; @@ -770,12 +773,16 @@ '', campaign_recipient_message_read: '', + campaign_recipient_evasion_page_visited: + '', campaign_recipient_before_page_visited: '', campaign_recipient_page_visited: '', campaign_recipient_after_page_visited: '', + campaign_recipient_deny_page_visited: + '', campaign_recipient_submitted_data: '', campaign_recipient_reported: @@ -798,9 +805,11 @@ campaign_recipient_message_sent: 'Email successfully delivered', campaign_recipient_message_failed: 'Email delivery failed', campaign_recipient_message_read: 'Recipient opened the email', + campaign_recipient_evasion_page_visited: 'Recipient encountered evasion page', campaign_recipient_before_page_visited: 'Recipient browsing before target page', campaign_recipient_page_visited: 'Recipient visited the target page', campaign_recipient_after_page_visited: 'Recipient continued browsing after target', + campaign_recipient_deny_page_visited: 'Recipient was denied access', campaign_recipient_submitted_data: 'Recipient submitted form data', campaign_recipient_reported: 'Email was reported as spam' }; @@ -1001,6 +1010,15 @@ /> After Page Visited +