Evasion page implementation.

Fix ip filtering.
Support for ip filter in proxies
This commit is contained in:
Ronni Skansing
2025-10-18 23:44:33 +02:00
parent 94bd6e28bd
commit 26880e36cf
17 changed files with 855 additions and 86 deletions
+107 -13
View File
@@ -84,6 +84,7 @@ func NewServer(
repositories.Proxy,
repositories.Identifier,
services.Campaign,
services.Template,
cookieName,
)
@@ -697,6 +698,12 @@ func (s *Server) checkAndServePhishingPage(
if err != nil {
return false, fmt.Errorf("failed to get recipient: %s", err)
}
// check for evasion page first
var evasionPageID *uuid.UUID
if v, err := campaign.EvasionPageID.Get(); err == nil {
evasionPageID = &v
}
// figure out which page types this template has
var beforePageID *uuid.UUID
var beforeProxyID *uuid.UUID
@@ -737,19 +744,34 @@ func (s *Server) checkAndServePhishingPage(
nextPageType := ""
currentPageType := ""
s.logger.Debugw("determining page flow",
"pageTypeQuery", pageTypeQuery,
"hasBeforePage", beforePageID != nil,
"hasBeforeProxy", beforeProxyID != nil,
"hasLandingPage", landingPageID != nil,
"hasLandingProxy", landingProxyID != nil,
"hasAfterPage", afterPageID != nil,
"hasAfterProxy", afterProxyID != nil,
"campaignRecipientID", campaignRecipientID.String(),
)
/*
s.logger.Debugw("determining page flow",
"pageTypeQuery", pageTypeQuery,
"hasBeforePage", beforePageID != nil,
"hasBeforeProxy", beforeProxyID != nil,
"hasLandingPage", landingPageID != nil,
"hasLandingProxy", landingProxyID != nil,
"hasAfterPage", afterPageID != nil,
"hasAfterProxy", afterProxyID != nil,
"campaignRecipientID", campaignRecipientID.String(),
)
*/
if len(pageTypeQuery) == 0 {
if beforePageID != nil || beforeProxyID != nil {
// check if there's an evasion page to serve first
if evasionPageID != nil {
pageID = evasionPageID
s.logger.Debugw("initial request - serving evasion page",
"pageID", pageID.String(),
)
currentPageType = data.PAGE_TYPE_EVASION
// determine next page type based on template structure
if beforePageID != nil || beforeProxyID != nil {
nextPageType = data.PAGE_TYPE_BEFORE
} else {
nextPageType = data.PAGE_TYPE_LANDING
}
} else if beforePageID != nil || beforeProxyID != nil {
if beforePageID != nil {
pageID = beforePageID
s.logger.Debugw("initial request - serving before landing page",
@@ -785,8 +807,72 @@ func (s *Server) checkAndServePhishingPage(
// if there is a page type, then we use that
} else {
switch pageTypeQuery {
// this is not relevant - already taken care of, ignore it
// this is set if the previous page was an evasion page
case data.PAGE_TYPE_EVASION:
// after evasion page, go to before page or landing page
if beforePageID != nil || beforeProxyID != nil {
if beforePageID != nil {
pageID = beforePageID
s.logger.Debugw("serving before landing page from evasion state",
"pageID", pageID.String(),
)
} else {
proxyID = beforeProxyID
s.logger.Debugw("serving before landing Proxy from evasion state",
"proxyID", proxyID.String(),
)
}
currentPageType = data.PAGE_TYPE_BEFORE
nextPageType = data.PAGE_TYPE_LANDING
} else {
if landingPageID != nil {
pageID = landingPageID
s.logger.Debugw("serving landing page from evasion state",
"pageID", pageID.String(),
)
} else {
proxyID = landingProxyID
s.logger.Debugw("serving landing Proxy from evasion state",
"proxyID", proxyID.String(),
)
}
currentPageType = data.PAGE_TYPE_LANDING
if afterPageID != nil || afterProxyID != nil {
nextPageType = data.PAGE_TYPE_AFTER
} else {
nextPageType = data.PAGE_TYPE_DONE
}
}
// special case for deny page access from evasion page
case "deny":
// serve the deny page if one is configured
if denyPageID, err := campaign.DenyPageID.Get(); err == nil {
err = s.renderDenyPage(c, domain, &denyPageID)
if err != nil {
return true, fmt.Errorf("failed to render deny page from evasion: %s", err)
}
return true, nil
} else {
// if no deny page configured, return 403
c.String(http.StatusForbidden, "Access denied")
c.Abort()
return true, nil
}
// this is set when transitioning to the before page
case data.PAGE_TYPE_BEFORE:
if beforePageID != nil {
pageID = beforePageID
s.logger.Debugw("serving before landing page from state",
"pageID", pageID.String(),
)
} else {
proxyID = beforeProxyID
s.logger.Debugw("serving before landing Proxy from state",
"proxyID", proxyID.String(),
)
}
currentPageType = data.PAGE_TYPE_BEFORE
nextPageType = data.PAGE_TYPE_LANDING
// this is set if the previous page was a before page
case data.PAGE_TYPE_LANDING:
if landingPageID != nil {
@@ -1030,6 +1116,8 @@ func (s *Server) checkAndServePhishingPage(
visitEventID := uuid.New()
eventName := ""
switch currentPageType {
case data.PAGE_TYPE_EVASION:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
case data.PAGE_TYPE_BEFORE:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
case data.PAGE_TYPE_LANDING:
@@ -1236,6 +1324,7 @@ func (s *Server) checkAndServePhishingPage(
return true, fmt.Errorf("failed to encrypt next page type: %s", err)
}
urlPath := cTemplate.URLPath.MustGet().String()
err = s.renderPageTemplate(
c,
domain,
@@ -1246,6 +1335,7 @@ func (s *Server) checkAndServePhishingPage(
cTemplate,
encryptedParam,
urlPath,
campaign,
)
if err != nil {
return true, fmt.Errorf("failed to render phishing page: %s", err)
@@ -1254,6 +1344,8 @@ func (s *Server) checkAndServePhishingPage(
visitEventID := uuid.New()
eventName := ""
switch currentPageType {
case data.PAGE_TYPE_EVASION:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
case data.PAGE_TYPE_BEFORE:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
case data.PAGE_TYPE_LANDING:
@@ -1543,12 +1635,13 @@ func (s *Server) renderPageTemplate(
campaignTemplate *model.CampaignTemplate,
stateParam string,
urlPath string,
campaign *model.Campaign,
) error {
content, err := page.Content.Get()
if err != nil {
return fmt.Errorf("no page content set to render: %s", err)
}
phishingPage, err := s.services.Template.CreatePhishingPage(
phishingPage, err := s.services.Template.CreatePhishingPageWithCampaign(
domain,
email,
campaignRecipientID,
@@ -1557,6 +1650,7 @@ func (s *Server) renderPageTemplate(
campaignTemplate,
stateParam,
urlPath,
campaign,
)
if err != nil {
return fmt.Errorf("failed to create phishing page: %s", err)
+11 -10
View File
@@ -37,16 +37,17 @@ func IsUpdateAvailable() bool {
// readonly
var CampaignEventPriority = map[string]int{
// campaign recipient events
data.EVENT_CAMPAIGN_RECIPIENT_REPORTED: 90,
data.EVENT_CAMPAIGN_RECIPIENT_CANCELLED: 80,
data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: 70,
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_MESSAGE_READ: 30,
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: 20,
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: 20,
data.EVENT_CAMPAIGN_RECIPIENT_SCHEDULED: 10,
data.EVENT_CAMPAIGN_RECIPIENT_REPORTED: 90,
data.EVENT_CAMPAIGN_RECIPIENT_CANCELLED: 80,
data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: 70,
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_EVASION_PAGE_VISITED: 35,
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: 30,
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: 20,
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: 20,
data.EVENT_CAMPAIGN_RECIPIENT_SCHEDULED: 10,
// campaign events
data.EVENT_CAMPAIGN_CLOSED: 30,
data.EVENT_CAMPAIGN_ACTIVE: 20,
+2
View File
@@ -178,6 +178,7 @@ func (c *Campaign) GetByID(g *gin.Context) {
WithRecipientGroups: true,
WithAllowDeny: true,
WithDenyPage: true,
WithEvasionPage: true,
},
)
// handle responses
@@ -210,6 +211,7 @@ func (c *Campaign) GetByName(g *gin.Context) {
WithRecipientGroups: true,
WithAllowDeny: true,
WithDenyPage: true,
WithEvasionPage: true,
},
)
// handle responses
+12 -10
View File
@@ -6,16 +6,17 @@ const (
EVENT_CAMPAIGN_SELF_MANAGED = "campaign_self_managed"
EVENT_CAMPAIGN_CLOSED = "campaign_closed"
EVENT_CAMPAIGN_RECIPIENT_SCHEDULED = "campaign_recipient_scheduled"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = "campaign_recipient_message_sent"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = "campaign_recipient_message_failed"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ = "campaign_recipient_message_read"
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_SUBMITTED_DATA = "campaign_recipient_submitted_data"
EVENT_CAMPAIGN_RECIPIENT_REPORTED = "campaign_recipient_reported"
EVENT_CAMPAIGN_RECIPIENT_CANCELLED = "campaign_recipient_cancelled"
EVENT_CAMPAIGN_RECIPIENT_SCHEDULED = "campaign_recipient_scheduled"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = "campaign_recipient_message_sent"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = "campaign_recipient_message_failed"
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ = "campaign_recipient_message_read"
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED = "campaign_recipient_evasion_page_visited"
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_SUBMITTED_DATA = "campaign_recipient_submitted_data"
EVENT_CAMPAIGN_RECIPIENT_REPORTED = "campaign_recipient_reported"
EVENT_CAMPAIGN_RECIPIENT_CANCELLED = "campaign_recipient_cancelled"
)
var Events = []string{
@@ -29,6 +30,7 @@ var Events = []string{
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT,
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED,
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ,
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED,
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED,
+1
View File
@@ -1,6 +1,7 @@
package data
const (
PAGE_TYPE_EVASION = "evasion"
PAGE_TYPE_BEFORE = "before"
PAGE_TYPE_LANDING = "landing"
PAGE_TYPE_AFTER = "after"
+6 -4
View File
@@ -48,10 +48,12 @@ type Campaign struct {
CampaignTemplate *CampaignTemplate
// can has one
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;uniqueIndex:idx_campaigns_unique_name_and_company_id;"`
Company *Company
DenyPageID *uuid.UUID `gorm:"type:uuid;index;"`
DenyPage *Page `gorm:"foreignKey:DenyPageID;references:ID"`
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;uniqueIndex:idx_campaigns_unique_name_and_company_id;"`
Company *Company
DenyPageID *uuid.UUID `gorm:"type:uuid;index;"`
DenyPage *Page `gorm:"foreignKey:DenyPageID;references:ID"`
EvasionPageID *uuid.UUID `gorm:"type:uuid;index;"`
EvasionPage *Page `gorm:"foreignKey:EvasionPageID;references:ID"`
// NotableEventID notable event for this campaign
NotableEvent *Event `gorm:"foreignKey:NotableEventID;references:ID"`
NotableEventID *uuid.UUID `gorm:"type:uuid;index"`
+25 -12
View File
@@ -46,6 +46,8 @@ type Campaign struct {
AllowDenyIDs nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"`
DenyPageID nullable.Nullable[uuid.UUID] `json:"denyPageID,omitempty"`
DenyPage *Page `json:"denyPage"`
EvasionPageID nullable.Nullable[uuid.UUID] `json:"evasionPageID,omitempty"`
EvasionPage *Page `json:"evasionPage"`
WebhookID nullable.Nullable[uuid.UUID] `json:"webhookID"`
// must not be set by a user
@@ -85,7 +87,7 @@ func (c *Campaign) Validate() error {
if len(c.RecipientGroupIDs.MustGet()) == 0 {
return validate.WrapErrorWithField(errors.New("must have at least one recipient group"), "RecipientGroupIDs")
}
if err := c.ValidateDenyPage(); err != nil {
if err := c.ValidateEvasionAndDenyPages(); err != nil {
return err
}
@@ -208,13 +210,22 @@ func (c *Campaign) ValidateNoSendTimesSet() error {
return nil
}
// ValidateDenyPage checks that a deny page is set
func (c *Campaign) ValidateDenyPage() error {
if c.DenyPageID.IsSpecified() && !c.DenyPageID.IsNull() {
if c.AllowDenyIDs.IsSpecified() && (c.AllowDenyIDs.IsNull() || len(c.AllowDenyIDs.MustGet()) == 0) {
return validate.WrapErrorWithField(errors.New("requires a allow deny IDs to be set"), "denyPage")
// ValidateEvasionAndDenyPages checks evasion page and deny page requirements
func (c *Campaign) ValidateEvasionAndDenyPages() error {
// if evasion page is set, deny page must also be set
if c.EvasionPageID.IsSpecified() && !c.EvasionPageID.IsNull() {
if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
return validate.WrapErrorWithField(errors.New("evasion page requires a deny page to be set"), "evasionPage")
}
}
// if allow/deny IDs are set (IP filtering), deny page must be set
if c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0 {
if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
return validate.WrapErrorWithField(errors.New("IP filtering requires a deny page to be set"), "denyPage")
}
}
return nil
}
@@ -320,15 +331,17 @@ func (c *Campaign) ToDBMap() map[string]any {
m["company_id"] = c.CompanyID.MustGet()
}
}
allowDenyIsSet := c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0
if allowDenyIsSet {
if c.DenyPageID.IsSpecified() {
m["deny_page_id"] = nil
if v, err := c.DenyPageID.Get(); err == nil {
m["deny_page_id"] = v.String()
} else {
m["deny_page_id"] = nil
}
} else {
m["deny_page_id"] = nil
}
if c.EvasionPageID.IsSpecified() {
m["evasion_page_id"] = nil
if v, err := c.EvasionPageID.Get(); err == nil {
m["evasion_page_id"] = v.String()
}
}
if c.WebhookID.IsSpecified() {
m["webhook_id"] = nil
+432 -2
View File
@@ -9,6 +9,7 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
@@ -113,6 +114,7 @@ type ProxyHandler struct {
ProxyRepository *repository.Proxy
IdentifierRepository *repository.Identifier
CampaignService *service.Campaign
TemplateService *service.Template
cookieName string
}
@@ -126,6 +128,7 @@ func NewProxyHandler(
proxyRepository *repository.Proxy,
identifierRepository *repository.Identifier,
campaignService *service.Campaign,
templateService *service.Template,
cookieName string,
) *ProxyHandler {
return &ProxyHandler{
@@ -139,6 +142,7 @@ func NewProxyHandler(
ProxyRepository: proxyRepository,
IdentifierRepository: identifierRepository,
CampaignService: campaignService,
TemplateService: templateService,
cookieName: cookieName,
}
}
@@ -153,6 +157,22 @@ func (m *ProxyHandler) HandleHTTPRequest(w http.ResponseWriter, req *http.Reques
return err
}
// check ip filtering if we have a campaign recipient
if reqCtx.CampaignRecipientID != nil {
blocked, resp := m.checkIPFilter(req, reqCtx)
if blocked {
if resp != nil {
return m.writeResponse(w, resp)
}
// if no deny page configured, return 404
return m.writeResponse(w, &http.Response{
StatusCode: http.StatusNotFound,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("")),
})
}
}
// create http client
client, err := m.createHTTPClient(req, reqCtx.ProxyConfig)
if err != nil {
@@ -259,9 +279,20 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
reqURL := req.URL.String()
createSession := reqCtx.CampaignRecipientID != nil
// handle existing session cleanup if this is an initial request
// check for evasion/deny pages BEFORE session resolution for initial requests
if createSession {
// cleanup any existing session first
m.cleanupExistingSession(reqCtx.CampaignRecipientID, reqURL)
// check if this is a deny request from evasion page
if resp := m.checkAndServeDenyPage(req, reqCtx); resp != nil {
return req, resp
}
// check if we should serve an evasion page first
if resp := m.checkAndServeEvasionPage(req, reqCtx); resp != nil {
return req, resp
}
} else {
// check for existing session
sessionCookie, err := req.Cookie(m.cookieName)
@@ -1951,6 +1982,14 @@ func (m *ProxyHandler) getCurrentPageType(req *http.Request, template *model.Cam
func (m *ProxyHandler) getNextPageType(currentPageType string, template *model.CampaignTemplate) string {
switch currentPageType {
case data.PAGE_TYPE_EVASION:
if _, errPage := template.BeforeLandingPageID.Get(); errPage == nil {
return data.PAGE_TYPE_BEFORE
}
if _, errProxy := template.BeforeLandingProxyID.Get(); errProxy == nil {
return data.PAGE_TYPE_BEFORE
}
return data.PAGE_TYPE_LANDING
case data.PAGE_TYPE_BEFORE:
return data.PAGE_TYPE_LANDING
case data.PAGE_TYPE_LANDING:
@@ -2417,7 +2456,7 @@ func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response)
// check for nil response
if resp == nil {
m.logger.Errorw("response is nil in writeResponse")
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
return errors.New("response is nil")
}
@@ -2435,6 +2474,7 @@ func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response)
// copy body
if resp.Body != nil {
defer resp.Body.Close()
_, err := io.Copy(w, resp.Body)
return err
}
@@ -2606,6 +2646,8 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxyS
// determine event name based on page type
var eventName string
switch currentPageType {
case data.PAGE_TYPE_EVASION:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
case data.PAGE_TYPE_BEFORE:
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
case data.PAGE_TYPE_LANDING:
@@ -2714,3 +2756,391 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxyS
"eventName", eventName,
)
}
// checkAndServeEvasionPage checks if an evasion page should be served and returns the response if so
func (m *ProxyHandler) checkAndServeEvasionPage(req *http.Request, reqCtx *RequestContext) *http.Response {
// get campaign info first
campaign, _, _, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
if err != nil {
return nil
}
// check if there's an evasion page configured
evasionPageID, err := campaign.EvasionPageID.Get()
if err != nil {
return nil
}
// get the campaign template
templateID, err := campaign.TemplateID.Get()
if err != nil {
return nil
}
ctx := req.Context()
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
WithIdentifier: true,
})
if err != nil {
return nil
}
// check if this is an initial request (no state parameter)
// we already know we have a campaign recipient ID from reqCtx
if cTemplate.StateIdentifier != nil {
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
encryptedParam := req.URL.Query().Get(stateParamKey)
// if there is a state parameter, this is not initial request
if encryptedParam != "" {
return nil
}
}
// this is initial request with campaign recipient ID and no state parameter, serve evasion page
return m.serveEvasionPageResponseDirect(req, reqCtx, &evasionPageID, campaign, cTemplate)
}
// checkAndServeDenyPage checks if a deny page should be served and returns the response if so
func (m *ProxyHandler) checkAndServeDenyPage(req *http.Request, reqCtx *RequestContext) *http.Response {
// get campaign info first
campaign, _, _, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
if err != nil {
return nil
}
// get the campaign template
templateID, err := campaign.TemplateID.Get()
if err != nil {
return nil
}
ctx := req.Context()
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
WithIdentifier: true,
})
if err != nil {
return nil
}
// check if state parameter indicates deny
if cTemplate.StateIdentifier != nil {
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
encryptedParam := req.URL.Query().Get(stateParamKey)
if encryptedParam != "" {
campaignID, err := campaign.ID.Get()
if err != nil {
return nil
}
secret := utils.UUIDToSecret(&campaignID)
if decrypted, err := utils.Decrypt(encryptedParam, secret); err == nil {
if decrypted == "deny" {
return m.serveDenyPageResponseDirect(req, reqCtx, campaign)
}
}
}
}
return nil
}
func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx *RequestContext, evasionPageID *uuid.UUID, campaign *model.Campaign, cTemplate *model.CampaignTemplate) *http.Response {
ctx := req.Context()
evasionPage, err := m.PageRepository.GetByID(ctx, evasionPageID, &repository.PageOption{})
if err != nil {
m.logger.Errorw("failed to get evasion page", "error", err, "pageID", evasionPageID)
return nil
}
campaignID, err := campaign.ID.Get()
if err != nil {
return nil
}
// determine next page type after evasion
var nextPageType string
if _, err := cTemplate.BeforeLandingPageID.Get(); err == nil {
nextPageType = data.PAGE_TYPE_BEFORE
} else if _, err := cTemplate.BeforeLandingProxyID.Get(); err == nil {
nextPageType = data.PAGE_TYPE_BEFORE
} else {
nextPageType = data.PAGE_TYPE_LANDING
}
htmlContent, err := m.renderEvasionPageTemplate(req, reqCtx, evasionPage, campaign, cTemplate, nextPageType)
if err != nil {
m.logger.Errorw("failed to render evasion page template", "error", err)
return nil
}
// create HTTP response
resp := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(htmlContent)),
}
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(htmlContent)))
resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
// register evasion page visit event
m.registerEvasionPageVisitEventDirect(req, reqCtx.CampaignRecipientID, &campaignID, campaign)
return resp
}
func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *RequestContext, campaign *model.Campaign) *http.Response {
denyPageID, err := campaign.DenyPageID.Get()
if err != nil {
// if no deny page configured, return 403
resp := &http.Response{
StatusCode: 403,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("Access denied")),
}
resp.Header.Set("Content-Type", "text/plain")
resp.Header.Set("Content-Length", "13")
return resp
}
ctx := req.Context()
denyPage, err := m.PageRepository.GetByID(ctx, &denyPageID, &repository.PageOption{})
if err != nil {
m.logger.Errorw("failed to get deny page", "error", err, "pageID", denyPageID)
return nil
}
// render the deny page
htmlContent, err := denyPage.Content.Get()
if err != nil {
return nil
}
content := htmlContent.String()
// create HTTP response
resp := &http.Response{
StatusCode: 200,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(content)),
}
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
return resp
}
func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *RequestContext, page *model.Page, campaign *model.Campaign, cTemplate *model.CampaignTemplate, nextPageType string) (string, error) {
// get recipient
ctx := req.Context()
cRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, reqCtx.CampaignRecipientID, &repository.CampaignRecipientOption{})
if err != nil {
return "", fmt.Errorf("failed to get campaign recipient: %w", err)
}
recipientID, err := cRecipient.RecipientID.Get()
if err != nil {
return "", fmt.Errorf("failed to get recipient ID: %w", err)
}
// get recipient details
recipientRepo := repository.Recipient{DB: m.CampaignRecipientRepository.DB}
recipient, err := recipientRepo.GetByID(ctx, &recipientID, &repository.RecipientOption{})
if err != nil {
return "", fmt.Errorf("failed to get recipient: %w", err)
}
// get email for template
templateID, err := campaign.TemplateID.Get()
if err != nil {
return "", fmt.Errorf("failed to get template ID: %w", err)
}
cTemplateWithEmail, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
WithEmail: true,
})
if err != nil {
return "", fmt.Errorf("failed to get campaign template with email: %w", err)
}
emailID := cTemplateWithEmail.EmailID.MustGet()
emailRepo := repository.Email{DB: m.CampaignRepository.DB}
email, err := emailRepo.GetByID(ctx, &emailID, &repository.EmailOption{})
if err != nil {
return "", fmt.Errorf("failed to get email: %w", err)
}
// get domain
hostVO, err := vo.NewString255(req.Host)
if err != nil {
return "", fmt.Errorf("failed to create host VO: %w", err)
}
domain, err := m.DomainRepository.GetByName(ctx, hostVO, &repository.DomainOption{})
if err != nil {
return "", fmt.Errorf("failed to get domain: %w", err)
}
// create encrypted state parameter for next page
campaignID := campaign.ID.MustGet()
encryptedNextState, err := utils.Encrypt(nextPageType, utils.UUIDToSecret(&campaignID))
if err != nil {
return "", fmt.Errorf("failed to encrypt next state: %w", err)
}
// get page content
htmlContent, err := page.Content.Get()
if err != nil {
return "", fmt.Errorf("failed to get evasion page HTML content: %w", err)
}
// convert model.Domain to database.Domain
var proxyID *uuid.UUID
if id, err := domain.ProxyID.Get(); err == nil {
proxyID = &id
}
dbDomain := &database.Domain{
ID: domain.ID.MustGet(),
Name: domain.Name.MustGet().String(),
Type: domain.Type.MustGet().String(),
ProxyID: proxyID,
ProxyTargetDomain: domain.ProxyTargetDomain.MustGet().String(),
HostWebsite: domain.HostWebsite.MustGet(),
RedirectURL: domain.RedirectURL.MustGet().String(),
}
// use template service to render the page
urlPath := req.URL.Path
buf, err := m.TemplateService.CreatePhishingPageWithCampaign(
dbDomain,
email,
reqCtx.CampaignRecipientID,
recipient,
htmlContent.String(),
cTemplate,
encryptedNextState,
urlPath,
campaign,
)
if err != nil {
return "", fmt.Errorf("failed to render evasion page template: %w", err)
}
return buf.String(), nil
}
func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, campaignRecipientID *uuid.UUID, campaignID *uuid.UUID, campaign *model.Campaign) {
// get recipient from campaign recipient
ctx := req.Context()
cRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, campaignRecipientID, &repository.CampaignRecipientOption{})
if err != nil {
return
}
recipientID, err := cRecipient.RecipientID.Get()
if err != nil {
return
}
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_EVASION_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 evasion page visit event", "error", err)
}
}
// checkIPFilter checks if the IP is allowed for proxy requests
// returns (blocked, response) where blocked=true means the IP should be blocked
func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext) (bool, *http.Response) {
// get campaign info
campaign, _, campaignID, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
if err != nil {
return false, nil // allow if we can't get campaign info
}
// extract client IP and strip port if present using net.SplitHostPort for IPv6 safety
ip := utils.ExtractClientIP(req)
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
// get allow/deny list entries
allowDenyEntries, err := m.CampaignRepository.GetAllDenyByCampaignID(req.Context(), campaignID)
if err != nil && err != gorm.ErrRecordNotFound {
return false, nil // allow if we can't get the list
}
// if there are no entries, allow access
if len(allowDenyEntries) == 0 {
return false, nil
}
// check IP against allow/deny lists
isAllowListing := false
allowed := false // for allow lists, default is deny
for i, allowDeny := range allowDenyEntries {
if i == 0 {
isAllowListing = allowDeny.Allowed.MustGet()
if !isAllowListing {
// if deny listing, then by default the IP is allowed until proven otherwise
allowed = true
}
}
ok, err := allowDeny.IsIPAllowed(ip)
if err != nil {
continue
}
if isAllowListing && ok {
allowed = true
break
} else if !isAllowListing && !ok {
allowed = false
break
}
}
if !allowed {
// try to serve deny page
if _, err := campaign.DenyPageID.Get(); err == nil {
resp := m.serveDenyPageResponseDirect(req, reqCtx, campaign)
return true, resp
}
// if no deny page, block with 404
return true, nil
}
return false, nil
}
+21
View File
@@ -59,6 +59,7 @@ type CampaignOption struct {
WithRecipientGroupCount bool
WithAllowDeny bool
WithDenyPage bool
WithEvasionPage bool
IncludeTestCampaigns bool
}
@@ -105,6 +106,9 @@ func (r *Campaign) load(db *gorm.DB, options *CampaignOption) *gorm.DB {
if options.WithDenyPage {
db = db.Preload("DenyPage")
}
if options.WithEvasionPage {
db = db.Preload("EvasionPage")
}
return db
}
@@ -1500,6 +1504,21 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
denyPageID.SetNull()
}
var evasionPage *model.Page
if row.EvasionPage != nil {
ep, err := ToPage(row.EvasionPage)
if err != nil {
return nil, errs.Wrap(err)
}
evasionPage = ep
}
evasionPageID := nullable.NewNullNullable[uuid.UUID]()
if row.EvasionPageID != nil {
evasionPageID.Set(*row.EvasionPageID)
} else {
evasionPageID.SetNull()
}
constraintWeekDays := nullable.NewNullNullable[vo.CampaignWeekDays]()
if row.ConstraintWeekDays != nil {
weekDays, err := vo.NewCampaignWeekDays(*row.ConstraintWeekDays)
@@ -1573,6 +1592,8 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
AllowDeny: allowDeny,
DenyPage: denyPage,
DenyPageID: denyPageID,
EvasionPage: evasionPage,
EvasionPageID: evasionPageID,
WebhookID: webhookID,
NotableEventID: notableEventID,
NotableEventName: notableEventName,
+9
View File
@@ -1336,6 +1336,15 @@ func (c *Campaign) UpdateByID(
}
}
// handle evasion page ID
if incoming.EvasionPageID.IsSpecified() {
if incoming.EvasionPageID.IsNull() {
current.EvasionPageID.SetNull()
} else {
current.EvasionPageID.Set(incoming.EvasionPageID.MustGet())
}
}
// validate and update
if err := current.Validate(); err != nil {
return errs.Wrap(err)
+54 -2
View File
@@ -188,7 +188,7 @@ func (t *Template) ApplyPageMock(content string) (*bytes.Buffer, error) {
URLIdentifier: urlIdentifier,
StateIdentifier: stateIdentifier,
}
return t.CreatePhishingPage(
return t.CreatePhishingPageWithCampaign(
domain,
email,
&campaignRecipientID,
@@ -197,6 +197,7 @@ func (t *Template) ApplyPageMock(content string) (*bytes.Buffer, error) {
campaignTemplate,
"stateParam",
"urlPath",
nil, // no campaign for mock, so DenyURL will be empty string
)
}
@@ -279,6 +280,21 @@ func (t *Template) CreatePhishingPage(
campaignTemplate *model.CampaignTemplate,
stateParam string,
urlPath string,
) (*bytes.Buffer, error) {
return t.CreatePhishingPageWithCampaign(domain, email, campaignRecipientID, recipient, contentToRender, campaignTemplate, stateParam, urlPath, nil)
}
// CreatePhishingPageWithCampaign creates a new phishing page with optional campaign for deny URL support
func (t *Template) CreatePhishingPageWithCampaign(
domain *database.Domain,
email *model.Email,
campaignRecipientID *uuid.UUID,
recipient *model.Recipient,
contentToRender string,
campaignTemplate *model.CampaignTemplate,
stateParam string,
urlPath string,
campaign *model.Campaign,
) (*bytes.Buffer, error) {
w := bytes.NewBuffer([]byte{})
id := campaignRecipientID.String()
@@ -289,6 +305,20 @@ func (t *Template) CreatePhishingPage(
urlIdentifier := campaignTemplate.URLIdentifier.Name.MustGet()
stateIdentifier := campaignTemplate.StateIdentifier.Name.MustGet()
url := fmt.Sprintf("%s?%s=%s&%s=%s", baseURL, urlIdentifier, id, stateIdentifier, stateParam)
// create deny URL if campaign and deny page exist
denyURL := ""
if campaign != nil {
if _, err := campaign.DenyPageID.Get(); err == nil {
// create a special state param that indicates deny page should be served
campaignID := campaign.ID.MustGet()
denyStateParam, encErr := utils.Encrypt("deny", utils.UUIDToSecret(&campaignID))
if encErr == nil {
denyURL = fmt.Sprintf("%s?%s=%s&%s=%s", baseURL, urlIdentifier, id, stateIdentifier, denyStateParam)
}
}
}
tmpl, err := template.New("page").
Funcs(TemplateFuncs()).
Parse(contentToRender)
@@ -296,10 +326,11 @@ func (t *Template) CreatePhishingPage(
if err != nil {
return w, fmt.Errorf("failed to parse page template: %s", err)
}
data := t.newTemplateDataMap(
data := t.newTemplateDataMapWithDenyURL(
id,
baseURL,
url,
denyURL,
recipient,
"", // trackingPixelPath
"", // trackingPixelMarkup
@@ -406,6 +437,27 @@ func (t *Template) newTemplateDataMap(
return &m
}
// newTemplateDataMapWithDenyURL creates a new data map for templates with deny URL for evasion pages
func (t *Template) newTemplateDataMapWithDenyURL(
id string,
baseURL string,
url string,
denyURL string,
recipient *model.Recipient,
trackingPixelPath string,
trackingPixelMarkup string,
email *model.Email,
apiSender *model.APISender,
) *map[string]any {
// get the standard template data
data := t.newTemplateDataMap(id, baseURL, url, recipient, trackingPixelPath, trackingPixelMarkup, email, apiSender)
// add the deny URL for evasion pages
(*data)["DenyURL"] = denyURL
return data
}
// TemplateFuncs returns template functions for templates
func TemplateFuncs() template.FuncMap {
return template.FuncMap{
+5
View File
@@ -1,6 +1,7 @@
package utils
import (
"net"
"net/http"
"strings"
)
@@ -36,6 +37,10 @@ func ExtractClientIP(req *http.Request) string {
}
}
}
// strip port
if host, _, err := net.SplitHostPort(clientIP); err == nil {
clientIP = host
}
return clientIP
}
+6
View File
@@ -508,6 +508,7 @@ export class API {
* @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 {Array} [campaign.constraintWeekDays]
* @param {string} [campaign.constraintStartTime]
@@ -530,6 +531,7 @@ export class API {
recipientGroupIDs,
allowDenyIDs,
denyPageID,
evasionPageID,
webhookID,
constraintWeekDays,
constraintStartTime,
@@ -551,6 +553,7 @@ export class API {
recipientGroupIDs,
allowDenyIDs,
denyPageID,
evasionPageID,
webhookID,
constraintWeekDays,
constraintStartTime,
@@ -787,6 +790,7 @@ export class API {
* @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
* @returns {Promise<ApiResponse>}
*/
@@ -810,6 +814,7 @@ export class API {
recipientGroupIDs,
allowDenyIDs,
denyPageID,
evasionPageID,
webhookID
}) => {
return await postJSON(this.getPath(`/campaign/${id}`), {
@@ -831,6 +836,7 @@ export class API {
recipientGroupIDs,
allowDenyIDs,
denyPageID,
evasionPageID,
webhookID
});
},
@@ -41,6 +41,7 @@
campaign_recipient_message_sent: true,
campaign_recipient_message_failed: true,
campaign_recipient_message_read: true,
campaign_recipient_evasion_page_visited: true,
campaign_recipient_before_page_visited: true,
campaign_recipient_page_visited: true,
campaign_recipient_after_page_visited: true,
+5
View File
@@ -10,6 +10,11 @@ const eventNameMap = {
color: 'bg-failed-sending'
},
campaign_recipient_message_read: { name: 'Message Read', priority: 40, color: 'bg-message-read' },
campaign_recipient_evasion_page_visited: {
name: 'Evasion Page Visited',
priority: 45,
color: 'bg-evasion-page-visited'
},
campaign_recipient_before_page_visited: {
name: 'Before Page Visited',
priority: 50,
+138 -33
View File
@@ -94,13 +94,13 @@
icon: ''
},
{
label: 'Add allow-list',
label: 'Allow',
value: 'allow',
icon: ''
},
{
label: 'Add deny-list',
label: 'Deny',
value: 'deny',
icon: ''
}
@@ -148,6 +148,23 @@
let scheduleType = 'basic';
let allowDenyType = 'none';
let allAllowDeny = [];
let showSecurityOptions = false;
// reactive statement to enable security options when deny page is set
$: if (formValues.denyPageValue && formValues.denyPageValue.trim() !== '') {
showSecurityOptions = true;
}
// reactive statement to clear evasion page and IP filtering when deny page is cleared
$: if (!formValues.denyPageValue) {
if (formValues.evasionPageValue) {
formValues.evasionPageValue = null;
}
if (allowDenyType !== 'none') {
allowDenyType = 'none';
formValues.allowDeny = [];
}
}
const defaultSendField = 'Email';
const defaultSendOrder = 'Random';
@@ -228,6 +245,7 @@
recipientGroups: [],
allowDeny: [],
denyPageValue: null,
evasionPageValue: null,
constraintWeekDays: [],
contraintStartTime: null,
contraintEndTime: null,
@@ -406,6 +424,15 @@
};
const validateMisc = () => {
// validate that deny page is selected if evasion page or IP filtering is used
if (formValues.evasionPageValue && !formValues.denyPageValue) {
modalError = 'Deny page is required when using an evasion page';
return false;
}
if (allowDenyType !== 'none' && !formValues.denyPageValue) {
modalError = 'Deny page is required when using IP filtering';
return false;
}
return checkCurrentStepValidity();
};
@@ -573,6 +600,7 @@
recipientGroupIDs: recipientGroupIDs,
allowDenyIDs: allowDenyIDs,
denyPageID: denyPageMap.byValueOrNull(formValues.denyPageValue),
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
constraintWeekDays: weekDaysAvailableToBinary(formValues.constraintWeekDays),
constraintStartTime: contraintStartTimeUTC,
constraintEndTime: contraintEndTimeUTC,
@@ -636,6 +664,7 @@
recipientGroupIDs: recipientGroupIDs,
allowDenyIDs: allowDenyIDs,
denyPageID: denyPageMap.byValueOrNull(formValues.denyPageValue),
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
webhookID: webhookMap.byValueOrNull(formValues.webhookValue)
});
@@ -752,6 +781,7 @@
recipientGroups: [],
allowDeny: [],
denyPageValue: null,
evasionPageValue: null,
constraintWeekDays: [],
contraintStartTime: null,
contraintEndTime: null,
@@ -779,7 +809,6 @@
const onChangeAllowDenyType = () => {
formValues.allowDeny = [];
formValues.denyPageValue = null;
setAllowDenyType(allAllowDeny);
};
@@ -881,6 +910,10 @@
if (campaign.denyPage) {
formValues.denyPageValue = campaign.denyPage.name;
}
if (campaign.evasionPage) {
formValues.evasionPageValue = campaign.evasionPage.name;
}
};
/*
@@ -1462,36 +1495,6 @@
</div>
<div class="mb-6">
<SelectSquare
label="IP filtering"
options={ipFilterOptions}
bind:value={allowDenyType}
onChange={() => {
onChangeAllowDenyType();
}}
/>
{#if allowDenyType !== 'none'}
<TextFieldMultiSelect
id="allowDenyIDs"
toolTipText="Select the IP groups to allow or block"
bind:value={formValues.allowDeny}
options={Array.from(allowDenyMap.values())}>Lists</TextFieldMultiSelect
>
<TextFieldSelect
id="deny-page"
bind:value={formValues.denyPageValue}
optional
onSelect={(page) => {
formValues.denyPageValue = page;
}}
options={Array.from(denyPageMap.values())}>Blocked Access Page</TextFieldSelect
>
{/if}
</div>
<div>
<TextFieldSelect
id="webhook"
bind:value={formValues.webhookValue}
@@ -1499,6 +1502,101 @@
options={Array.from(webhookMap.values())}>Webhook</TextFieldSelect
>
</div>
<div class="mb-6">
<SelectSquare
optional
label="Security Configuration"
options={[
{ value: false, label: 'Disabled' },
{ value: true, label: 'Enabled' }
]}
bind:value={showSecurityOptions}
onChange={() => {
if (!showSecurityOptions) {
formValues.denyPageValue = '';
formValues.evasionPageValue = '';
allowDenyType = 'none';
formValues.allowDeny = [];
}
}}
/>
</div>
{#if showSecurityOptions}
<div class="mb-6">
<TextFieldSelect
id="deny-page"
bind:value={formValues.denyPageValue}
optional
toolTipText="Page to show when access is denied. Required for evasion pages and IP filtering."
onSelect={(page) => {
formValues.denyPageValue = page;
}}
options={Array.from(denyPageMap.values())}>Deny Page</TextFieldSelect
>
</div>
<div class="mb-6">
{#if formValues.denyPageValue}
<TextFieldSelect
id="evasion-page"
bind:value={formValues.evasionPageValue}
optional
toolTipText="Select an anti-bot/evasion page to be served before the first real page. If evasion fails, the deny page will be shown instead."
onSelect={(page) => {
formValues.evasionPageValue = page;
}}
options={Array.from(denyPageMap.values())}
>Anti-bot / Evasion Page</TextFieldSelect
>
{:else}
<div
class="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600"
>
<p class="text-gray-600 dark:text-gray-400 text-sm">
<strong>Anti-bot / Evasion Page</strong><br />
You must select a deny page first to use evasion pages.
</p>
</div>
{/if}
</div>
<div class="mb-6">
{#if formValues.denyPageValue}
<SelectSquare
label="IP filtering"
toolTipText="Filter access based on IP address lists"
options={ipFilterOptions}
width="small"
bind:value={allowDenyType}
onChange={() => {
onChangeAllowDenyType();
}}
/>
{#if allowDenyType !== 'none'}
<div class="mt-4">
<TextFieldMultiSelect
id="allowDenyIDs"
toolTipText="Select the IP groups to allow or block"
bind:value={formValues.allowDeny}
options={Array.from(allowDenyMap.values())}>Lists</TextFieldMultiSelect
>
</div>
{/if}
{:else}
<div
class="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600"
>
<p class="text-gray-600 dark:text-gray-400 text-sm">
<strong>IP filtering mode</strong><br />
You must select a deny page first to use IP filtering.
</p>
</div>
{/if}
</div>
{/if}
</FormColumn>
</FormColumns>
{:else if currentStep === 5}
@@ -1748,6 +1846,13 @@
>
{/if}
{#if formValues.evasionPageValue}
<span class="text-grayblue-dark font-medium">Evasion Page:</span>
<span class="text-pc-darkblue dark:text-white"
>{formValues.evasionPageValue}</span
>
{/if}
{#if formValues.anonymizeAt}
<span class="text-grayblue-dark font-medium">Anonymize at:</span>
<span class="text-pc-darkblue dark:text-white">
@@ -216,6 +216,8 @@
campaign.saveSubmittedData = t.saveSubmittedData;
campaign.isAnonymous = t.isAnonymous;
campaign.allowDeny = t.allowDeny;
campaign.denyPage = t.denyPage;
campaign.evasionPage = t.evasionPage;
campaign.webhookID = t.webhookID;
campaign.template = templateMap.byKey(t.templateID);
campaign.recipientGroups = t.recipientGroupIDs.map((id) => recipientGroupMap.byKey(id));
@@ -1286,6 +1288,24 @@
{/if}
</span>
<span class="text-grayblue-dark font-medium">Deny Page:</span>
<span class="text-pc-darkblue dark:text-white">
{#if campaign.denyPage}
{campaign.denyPage.name}
{:else}
None
{/if}
</span>
<span class="text-grayblue-dark font-medium">Evasion Page:</span>
<span class="text-pc-darkblue dark:text-white">
{#if campaign.evasionPage}
{campaign.evasionPage.name}
{:else}
None
{/if}
</span>
<span class="text-grayblue-dark font-medium">Webhook:</span>
<span class="text-pc-darkblue dark:text-white">
{#if campaign.webhookID}