add deny page visit event

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-10 19:04:22 +01:00
parent 245ba8c8e6
commit 8a5de77b28
8 changed files with 141 additions and 2 deletions
+51
View File
@@ -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(&currentNotableEventID, 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 {
+1
View File
@@ -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,
+2
View File
@@ -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,
+60
View File
@@ -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(&currentNotableEventID, 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 {
@@ -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 @@
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.072 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>',
campaign_recipient_message_read:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>',
campaign_recipient_evasion_page_visited:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>',
campaign_recipient_before_page_visited:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>',
campaign_recipient_page_visited:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/></svg>',
campaign_recipient_after_page_visited:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>',
campaign_recipient_deny_page_visited:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>',
campaign_recipient_submitted_data:
'<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>',
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 @@
/>
<span class="text-gray-600 dark:text-gray-300">After Page Visited</span>
</label>
<label class="flex items-center text-xs">
<input
type="checkbox"
bind:checked={eventFilters.campaign_recipient_deny_page_visited}
on:change={() => filterUpdateCounter++}
class="mr-2 rounded border-slate-300 dark:border-gray-700/60"
/>
<span class="text-gray-600 dark:text-gray-300">Deny Page Visited</span>
</label>
<label class="flex items-center text-xs">
<input
type="checkbox"
+5
View File
@@ -15,6 +15,11 @@ const eventNameMap = {
priority: 45,
color: 'bg-evasion-page-visited'
},
campaign_recipient_deny_page_visited: {
name: 'Deny Page Visited',
priority: 47,
color: 'bg-deny-page-visited'
},
campaign_recipient_before_page_visited: {
name: 'Before Page Visited',
priority: 50,
@@ -1121,12 +1121,12 @@
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"
d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244"
/>
</svg>
</StatsCard>
+2
View File
@@ -36,9 +36,11 @@ export default {
cancelled: '#161692',
'message-sent': '#94cae6',
'message-read': '#4cb5b5',
'evasion-page-visited': '#c8a2f0',
'before-page-visited': '#eea5fa',
'page-visited': '#f96dcf',
'after-page-visited': '#f6287b',
'deny-page-visited': '#ff6b35',
'submitted-data': '#f42e41',
reported: '#2c3e50',
'completed-campaign': '#48bb78',