diff --git a/backend/go.mod b/backend/go.mod index 65cf887..a964e4e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,6 +4,7 @@ go 1.25.1 require ( github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 + github.com/PuerkitoBio/goquery v1.8.1 github.com/boombuler/barcode v1.0.1 github.com/brianvoe/gofakeit/v7 v7.0.4 github.com/caddyserver/certmagic v0.19.2 @@ -31,6 +32,7 @@ require ( ) require ( + github.com/andybalholm/cascadia v1.3.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bytedance/sonic v1.12.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index bac5a87..a88f5bd 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,5 +1,9 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= +github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -149,6 +153,7 @@ github.com/yeqown/go-qrcode/v2 v2.2.4 h1:cXdYlrhzHzVAnJHiwr/T6lAUmS9MtEStjEZBjAr github.com/yeqown/go-qrcode/v2 v2.2.4/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw= github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0= github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= @@ -163,28 +168,56 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k= golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index 969991a..7ea2b20 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -20,6 +20,7 @@ import ( "sync/atomic" "time" + "github.com/PuerkitoBio/goquery" "github.com/go-errors/errors" "github.com/google/uuid" "github.com/phishingclub/phishingclub/cache" @@ -1479,9 +1480,28 @@ func (m *ProxyHandler) applyCustomReplacementsWithoutSession(body []byte, config } func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string) []byte { + // default to regex engine if not specified + engine := replacement.Engine + if engine == "" { + engine = "regex" + } + + switch engine { + case "regex": + return m.applyRegexReplacement(body, replacement, sessionID) + case "dom": + return m.applyDomReplacement(body, replacement, sessionID) + default: + m.logger.Errorw("unsupported replacement engine", "engine", engine, "sessionID", sessionID) + return body + } +} + +// applyRegexReplacement applies regex-based replacement +func (m *ProxyHandler) applyRegexReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string) []byte { re, err := regexp.Compile(replacement.Find) if err != nil { - m.logger.Errorw("invalid replacement regex", "error", err) + m.logger.Errorw("invalid replacement regex", "error", err, "sessionID", sessionID) return body } @@ -1493,6 +1513,130 @@ func (m *ProxyHandler) applyReplacement(body []byte, replacement service.ProxySe return body } +// applyDomReplacement applies DOM-based replacement +func (m *ProxyHandler) applyDomReplacement(body []byte, replacement service.ProxyServiceReplaceRule, sessionID string) []byte { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(string(body))) + if err != nil { + m.logger.Errorw("failed to parse html for dom manipulation", "error", err, "sessionID", sessionID) + return body + } + + // find elements using the selector + selection := doc.Find(replacement.Find) + if selection.Length() == 0 { + // no elements found, return original body + return body + } + + // apply target filtering + selection = m.applyTargetFilter(selection, replacement.Target) + if selection.Length() == 0 { + return body + } + + switch replacement.Action { + case "setText": + selection.SetText(replacement.Replace) + case "setHtml": + selection.SetHtml(replacement.Replace) + case "setAttr": + // for setAttr, replace should be in format "attribute:value" + parts := strings.SplitN(replacement.Replace, ":", 2) + if len(parts) == 2 { + selection.SetAttr(parts[0], parts[1]) + } else { + m.logger.Errorw("invalid setAttr replace format, expected 'attribute:value'", "replace", replacement.Replace, "sessionID", sessionID) + return body + } + case "removeAttr": + selection.RemoveAttr(replacement.Replace) + case "addClass": + selection.AddClass(replacement.Replace) + case "removeClass": + selection.RemoveClass(replacement.Replace) + case "remove": + selection.Remove() + default: + m.logger.Errorw("unsupported dom action", "action", replacement.Action, "sessionID", sessionID) + return body + } + + // get the modified html + html, err := doc.Html() + if err != nil { + m.logger.Errorw("failed to generate html from dom document", "error", err, "sessionID", sessionID) + return body + } + + return []byte(html) +} + +// applyTargetFilter filters the selection based on target specification +func (m *ProxyHandler) applyTargetFilter(selection *goquery.Selection, target string) *goquery.Selection { + if target == "" || target == "all" { + return selection + } + + length := selection.Length() + if length == 0 { + return selection + } + + switch target { + case "first": + return selection.First() + case "last": + return selection.Last() + default: + // handle numeric patterns like "1,3,5" or "2-4" + if matched, _ := regexp.MatchString(`^(\d+,)*\d+$`, target); matched { + // comma-separated list like "1,3,5" + indices := strings.Split(target, ",") + var filteredSelection *goquery.Selection + for _, indexStr := range indices { + if index, err := strconv.Atoi(strings.TrimSpace(indexStr)); err == nil { + // convert to 0-based index + if index > 0 && index <= length { + element := selection.Eq(index - 1) + if filteredSelection == nil { + filteredSelection = element + } else { + filteredSelection = filteredSelection.AddSelection(element) + } + } + } + } + if filteredSelection != nil { + return filteredSelection + } + } else if matched, _ := regexp.MatchString(`^\d+-\d+$`, target); matched { + // range like "2-4" + parts := strings.Split(target, "-") + if len(parts) == 2 { + start, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) + end, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) + if err1 == nil && err2 == nil && start > 0 && end >= start { + var filteredSelection *goquery.Selection + for i := start; i <= end && i <= length; i++ { + element := selection.Eq(i - 1) + if filteredSelection == nil { + filteredSelection = element + } else { + filteredSelection = filteredSelection.AddSelection(element) + } + } + if filteredSelection != nil { + return filteredSelection + } + } + } + } + } + + // fallback to all if target is invalid + return selection +} + func (m *ProxyHandler) processCookiesForPhishingDomain(resp *http.Response, ps *ProxySession) { cookies := resp.Cookies() if len(cookies) == 0 { diff --git a/backend/service/proxy.go b/backend/service/proxy.go index 0f73814..a7c37f1 100644 --- a/backend/service/proxy.go +++ b/backend/service/proxy.go @@ -117,8 +117,11 @@ type ProxyServiceCaptureRule struct { // ProxyServiceReplaceRule represents a replacement rule type ProxyServiceReplaceRule struct { Name string `yaml:"name,omitempty"` - Find string `yaml:"find"` - Replace string `yaml:"replace"` + Engine string `yaml:"engine,omitempty"` // "regex" (default) or "dom" + Find string `yaml:"find,omitempty"` // regex pattern (regex engine) or css selector (dom engine) + Replace string `yaml:"replace,omitempty"` // replacement value for both engines + Action string `yaml:"action,omitempty"` // dom action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove + Target string `yaml:"target,omitempty"` // target matching: "first", "last", "all" (default), "1,3,5", "2-4" From string `yaml:"from,omitempty"` } @@ -713,6 +716,13 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) { } } } + + // clean up rewrite rules based on engine type + if domainConfig.Rewrite != nil { + for i := range domainConfig.Rewrite { + m.cleanupRewriteRule(&domainConfig.Rewrite[i]) + } + } config.Hosts[domain] = domainConfig } @@ -730,20 +740,134 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) { } } } + + // clean up global rewrite rules based on engine type + if config.Global != nil && config.Global.Rewrite != nil { + for i := range config.Global.Rewrite { + m.cleanupRewriteRule(&config.Global.Rewrite[i]) + } + } +} + +// cleanupRewriteRule ensures only relevant fields are set based on engine type +func (m *Proxy) cleanupRewriteRule(rule *ProxyServiceReplaceRule) { + // set default engine to regex if not specified + if rule.Engine == "" { + rule.Engine = "regex" + } + + // clean up fields based on engine type + if rule.Engine == "regex" { + // for regex engine, completely remove dom-specific fields + rule.Action = "" + rule.Target = "" + // set default 'from' to 'response_body' for regex if not specified + if rule.From == "" { + rule.From = "response_body" + } + } else if rule.Engine == "dom" { + // for dom engine, set default target if not specified + if rule.Target == "" { + rule.Target = "all" + } + // dom engine always uses response_body, force it + rule.From = "response_body" + } } // validateReplaceRules validates a slice of replace rules func (m *Proxy) validateReplaceRules(replaceRules []ProxyServiceReplaceRule) error { for _, replace := range replaceRules { - if replace.Find == "" { - return validate.WrapErrorWithField(errors.New("replace rule 'find' is required"), "proxyConfig") + // set default engine to regex if not specified + engine := replace.Engine + if engine == "" { + engine = "regex" } - if _, err := regexp.Compile(replace.Find); err != nil { + + // validate engine type + if engine != "regex" && engine != "dom" { return validate.WrapErrorWithField( - errors.New("invalid regex pattern in replace rule 'find': "+err.Error()), + errors.New("invalid 'engine' value in replace rule, must be 'regex' or 'dom'"), "proxyConfig", ) } + + // validate based on engine type + if engine == "regex" { + if replace.Find == "" { + return validate.WrapErrorWithField(errors.New("replace rule 'find' is required for regex engine"), "proxyConfig") + } + if _, err := regexp.Compile(replace.Find); err != nil { + return validate.WrapErrorWithField( + errors.New("invalid regex pattern in replace rule 'find': "+err.Error()), + "proxyConfig", + ) + } + if replace.Replace == "" { + return validate.WrapErrorWithField(errors.New("replace rule 'replace' is required for regex engine"), "proxyConfig") + } + } else if engine == "dom" { + if replace.Find == "" { + return validate.WrapErrorWithField(errors.New("replace rule 'find' is required for dom engine"), "proxyConfig") + } + if replace.Action == "" { + return validate.WrapErrorWithField(errors.New("replace rule 'action' is required for dom engine"), "proxyConfig") + } + + // validate dom actions + validActions := []string{"setText", "setHtml", "setAttr", "removeAttr", "addClass", "removeClass", "remove"} + validAction := false + for _, action := range validActions { + if replace.Action == action { + validAction = true + break + } + } + if !validAction { + return validate.WrapErrorWithField( + errors.New("invalid 'action' value in replace rule, must be one of: "+strings.Join(validActions, ", ")), + "proxyConfig", + ) + } + + // validate that actions requiring a replace value have one + if (replace.Action == "setText" || replace.Action == "setHtml" || replace.Action == "setAttr" || replace.Action == "addClass" || replace.Action == "removeClass") && replace.Replace == "" { + return validate.WrapErrorWithField( + errors.New("replace rule 'replace' is required for action '"+replace.Action+"'"), + "proxyConfig", + ) + } + + // validate target field if specified + if replace.Target != "" { + validTargets := []string{"first", "last", "all"} + isValidTarget := false + for _, target := range validTargets { + if replace.Target == target { + isValidTarget = true + break + } + } + // also check for numeric patterns like "1,3,5" or "2-4" + if !isValidTarget { + if matched, _ := regexp.MatchString(`^(\d+,)*\d+$`, replace.Target); matched { + isValidTarget = true + } else if matched, _ := regexp.MatchString(`^\d+-\d+$`, replace.Target); matched { + isValidTarget = true + } + } + if !isValidTarget { + return validate.WrapErrorWithField( + errors.New("invalid 'target' value in replace rule, must be 'first', 'last', 'all', numeric list (1,3,5), or range (2-4)"), + "proxyConfig", + ) + } + } + + // dom engine doesn't use 'from' field - it always works on response_body + // if 'from' is specified for dom engine, it's ignored (will be forced to response_body) + } + if replace.From != "" { validFromValues := []string{"request_body", "request_header", "response_body", "response_header", "any"} valid := false diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt index 55d7fd3..446d2c8 100644 --- a/backend/vendor/modules.txt +++ b/backend/vendor/modules.txt @@ -19,6 +19,12 @@ github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/o github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/options github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/shared github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/version +# github.com/PuerkitoBio/goquery v1.8.1 +## explicit; go 1.13 +github.com/PuerkitoBio/goquery +# github.com/andybalholm/cascadia v1.3.1 +## explicit; go 1.16 +github.com/andybalholm/cascadia # github.com/atotto/clipboard v0.1.4 ## explicit github.com/atotto/clipboard diff --git a/frontend/src/lib/utils/proxyYamlCompletion.js b/frontend/src/lib/utils/proxyYamlCompletion.js index 5407fc2..0e1bc12 100644 --- a/frontend/src/lib/utils/proxyYamlCompletion.js +++ b/frontend/src/lib/utils/proxyYamlCompletion.js @@ -14,6 +14,7 @@ export class ProxyYamlCompletionProvider { try { if (!this.completionProvider) { this.completionProvider = this.monaco.languages.registerCompletionItemProvider('yaml', { + triggerCharacters: [' ', ':', '-', '"', "'"], provideCompletionItems: (model, position) => { try { return this.provideCompletionItems(model, position); @@ -77,16 +78,25 @@ export class ProxyYamlCompletionProvider { const currentIndent = this.getIndent(linePrefix); // Handle specific field value completions - if (linePrefix.match(/mode:\s*$/)) { + if (linePrefix.match(/\s*engine:\s*$/)) { + return this.getEngineSuggestions(range); + } + if (linePrefix.match(/\s*action:\s*$/)) { + return this.getDomActionSuggestions(range); + } + if (linePrefix.match(/\s*target:\s*$/)) { + return this.getTargetSuggestions(range); + } + if (linePrefix.match(/\s*mode:\s*$/)) { return this.getModeSuggestions(range); } - if (linePrefix.match(/from:\s*$/)) { + if (linePrefix.match(/\bfrom:\s*["']?/)) { return this.getFromSuggestions(range); } - if (linePrefix.match(/method:\s*$/)) { + if (linePrefix.match(/\s*method:\s*$/)) { return this.getMethodSuggestions(range); } - if (linePrefix.match(/(with_session|without_session):\s*$/)) { + if (linePrefix.match(/\s*(with_session|without_session):\s*$/)) { return this.getActionSuggestions(range); } @@ -342,7 +352,7 @@ export class ProxyYamlCompletionProvider { ]; } - getRewriteSuggestions(range) { + getRewriteSuggestions(range, linesAbove, current) { return [ { label: 'name', @@ -351,25 +361,49 @@ export class ProxyYamlCompletionProvider { documentation: 'Optional rewrite rule name', range }, + { + label: 'engine', + kind: this.monaco.languages.CompletionItemKind.Property, + insertText: 'engine: "regex"', + documentation: 'Rewrite engine: "regex" (default) or "dom"', + range + }, { label: 'find', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'find: "pattern"', - documentation: 'Regex pattern to find (required)', + documentation: + 'Pattern/selector to find (regex pattern for regex engine, CSS selector for dom engine)', range }, { label: 'replace', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'replace: "replacement"', - documentation: 'Replacement text (required)', + documentation: 'Replacement value (replacement text for regex, value for dom actions)', + range + }, + { + label: 'action', + kind: this.monaco.languages.CompletionItemKind.Property, + insertText: 'action: "setText"', + documentation: + 'DOM action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove', + range + }, + { + label: 'target', + kind: this.monaco.languages.CompletionItemKind.Property, + insertText: 'target: "all"', + documentation: 'Target matching: "first", "last", "all" (default), "1,3,5", "2-4"', range }, { label: 'from', kind: this.monaco.languages.CompletionItemKind.Property, insertText: 'from: "response_body"', - documentation: 'Where to apply replacement', + documentation: + 'Where to apply replacement (regex engine only - dom engine always uses response_body)', range } ]; @@ -391,11 +425,59 @@ export class ProxyYamlCompletionProvider { getNewRewriteSuggestions(range) { return [ { - label: 'rewrite rule', + label: 'regex rewrite rule', kind: this.monaco.languages.CompletionItemKind.Snippet, insertText: - 'name: "rewrite_name"\n find: "pattern"\n replace: "replacement"\n from: "response_body"', - documentation: 'New rewrite rule template', + 'name: "regex_rule"\n find: "pattern"\n replace: "replacement"\n from: "response_body"', + documentation: 'New regex-based rewrite rule template', + range + }, + { + label: 'dom rewrite rule', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "dom_rule"\n engine: "dom"\n find: "css-selector"\n action: "setText"\n replace: "new-value"\n target: "all"', + documentation: 'New DOM-based rewrite rule template', + range + }, + { + label: 'dom change title', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "change_title"\n engine: "dom"\n find: "title"\n action: "setText"\n replace: "Secure Login Portal"\n target: "first"', + documentation: 'Change page title using DOM', + range + }, + { + label: 'dom modify form action', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "modify_form"\n engine: "dom"\n find: "form[action]"\n action: "setAttr"\n replace: "action:/evil/submit"\n target: "all"', + documentation: 'Modify form action attribute using DOM', + range + }, + { + label: 'dom add CSS class', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "add_class"\n engine: "dom"\n find: ".login-form"\n action: "addClass"\n replace: "enhanced-security"\n target: "all"', + documentation: 'Add CSS class to elements using DOM', + range + }, + { + label: 'dom remove elements', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "remove_warnings"\n engine: "dom"\n find: ".security-warning"\n action: "remove"\n target: "all"', + documentation: 'Remove security warnings using DOM', + range + }, + { + label: 'dom remove attribute', + kind: this.monaco.languages.CompletionItemKind.Snippet, + insertText: + 'name: "remove_csrf"\n engine: "dom"\n find: "input[name=\'_token\']"\n action: "removeAttr"\n replace: "name"\n target: "all"', + documentation: 'Remove attributes using DOM', range } ]; @@ -500,6 +582,119 @@ export class ProxyYamlCompletionProvider { ]; } + getEngineSuggestions(range) { + return [ + { + label: '"regex"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"regex"', + documentation: 'Regex-based replacement engine (default)', + range + }, + { + label: '"dom"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"dom"', + documentation: 'DOM manipulation engine for HTML elements', + range + } + ]; + } + + getTargetSuggestions(range) { + return [ + { + label: '"all"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"all"', + documentation: 'Target all matching elements (default)', + range + }, + { + label: '"first"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"first"', + documentation: 'Target only the first matching element', + range + }, + { + label: '"last"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"last"', + documentation: 'Target only the last matching element', + range + }, + { + label: '"1,3,5"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"1,3,5"', + documentation: 'Target specific elements by index (comma-separated)', + range + }, + { + label: '"2-4"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"2-4"', + documentation: 'Target a range of elements (start-end)', + range + } + ]; + } + + getDomActionSuggestions(range) { + return [ + { + label: '"setText"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"setText"', + documentation: 'Set the text content of selected elements (preserves HTML structure)', + range + }, + { + label: '"setHtml"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"setHtml"', + documentation: 'Set the HTML content of selected elements (replaces inner HTML)', + range + }, + { + label: '"setAttr"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"setAttr"', + documentation: 'Set attribute value (use value format: "attribute:value")', + range + }, + { + label: '"removeAttr"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"removeAttr"', + documentation: 'Remove attribute from selected elements', + range + }, + { + label: '"addClass"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"addClass"', + documentation: 'Add CSS class to selected elements', + range + }, + { + label: '"removeClass"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"removeClass"', + documentation: 'Remove CSS class from selected elements', + range + }, + { + label: '"remove"', + kind: this.monaco.languages.CompletionItemKind.Value, + insertText: '"remove"', + documentation: 'Remove selected elements from DOM', + range + } + ]; + } + getActionSuggestions(range) { return [ { @@ -612,11 +807,16 @@ export class ProxyYamlCompletionProvider { name: 'Unique identifier for the rule', method: 'HTTP method to match (GET, POST, PUT, DELETE, etc.)', path: 'URL path pattern to match (regex)', - find: 'Regex pattern to capture data, or cookie name if from=cookie', - from: 'Location to search: request_body, request_header, response_body, response_header, cookie, any', + find: 'Pattern to find: regex pattern (regex engine) or CSS selector (dom engine)', + from: 'Location to search (regex engine only): request_body, request_header, response_body, response_header, cookie, any', required: 'Whether this capture is required for page and capture completion', - rewrite: 'Rules for modifying request/response content', - replace: 'Replacement text for the find pattern', + rewrite: 'Rules for modifying request/response content using regex or dom engines', + replace: 'Replacement value: replacement text (regex engine) or value for dom actions', + engine: + 'Rewrite engine: "regex" (default) for pattern replacement or "dom" for HTML manipulation', + action: 'DOM action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove', + target: + 'Target matching: "first", "last", "all" (default), "1,3,5" (specific), "2-4" (range)', to: 'Target phishing domain for this original domain' }; diff --git a/frontend/src/routes/proxy/+page.svelte b/frontend/src/routes/proxy/+page.svelte index 9cb3066..c44b968 100644 --- a/frontend/src/routes/proxy/+page.svelte +++ b/frontend/src/routes/proxy/+page.svelte @@ -74,7 +74,49 @@ portal.example.com: path: "/login" find: "username=([^&]+).*password=([^&]+)" from: "request_body" - required: true`; + required: true + rewrite: + # regex-based replacement (default engine) + - name: "replace_logo" + find: "logo\\.png" + replace: "evil-logo.png" + from: "response_body" + # dom-based manipulations + - name: "change_title" + engine: "dom" + find: "title" + action: "setText" + replace: "Secure Login Portal" + target: "first" + - name: "inject_meta" + engine: "dom" + find: "head" + action: "setHtml" + replace: "" + target: "first" + - name: "modify_form_action" + engine: "dom" + find: "form[action='/login']" + action: "setAttr" + replace: "action:/auth/submit" + target: "all" + - name: "add_style_class" + engine: "dom" + find: ".login-form" + action: "addClass" + replace: "enhanced-security" + target: "all" + - name: "remove_csrf_tokens" + engine: "dom" + find: "input[name='_token']" + action: "removeAttr" + replace: "name" + target: "all" + - name: "hide_warnings" + engine: "dom" + find: ".security-warning" + action: "remove" + target: "all"`; $: { modalText = getModalText('Proxy', modalMode);