From 1b8a0073cd9587a2fe38fdaa86224b809722ee15 Mon Sep 17 00:00:00 2001 From: Matteo Pace Date: Mon, 19 Jun 2023 21:32:21 +0200 Subject: [PATCH] feat: Implements Go e2e tests (#802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * adds health checks, CRS e2e tests * adds config check logic and comments around configs * NULLED_BODY, removed references to Envoy, minor optimizations * moves parameters to flags * adds e2e test run, refactors e2e folder * chore: adds improvements to e2e runner. * fix calling e2e tests * httpbin attempt * feat: uses a mux to emulate a proxy. * removes CRS and recommeneded dependencies from e2e test * nits around e2e expected directives and comments * tests: adds content length check. * adds e2e to readme, fix application/x-www-form-urlencoded tests, adds comment about this needed content-type --------- Co-authored-by: José Carlos Chávez --- README.md | 14 ++ go.mod | 1 + go.sum | 2 + go.work.sum | 3 + http/e2e/main.go | 57 ++++++++ http/e2e/pkg/runner.go | 311 ++++++++++++++++++++++++++++++++++++++++ http/interceptor.go | 2 + http/middleware.go | 2 +- http/middleware_test.go | 6 +- testing/e2e/e2e_test.go | 68 +++++++++ 10 files changed, 463 insertions(+), 3 deletions(-) create mode 100644 http/e2e/main.go create mode 100644 http/e2e/pkg/runner.go create mode 100644 testing/e2e/e2e_test.go diff --git a/README.md b/README.md index 0b7fe082c..5448509e0 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,20 @@ the operator with `plugins.RegisterOperator` to reduce binary size / startup ove * `coraza.rule.multiphase_valuation` - enables evaluation of rule variables in the phases that they are ready, not only the phase the rule is defined for. +## E2E Testing + +[`Http/e2e/`](./http/e2e) provides an utility to run e2e tests. +It can be used standalone against your own waf deployment: +```shell +go run github.com/corazawaf/coraza/http/e2e@main --proxy-hostport localhost:8080 --httpbin-hostport localhost:8081 +``` +or as a library by importing: +```go +"github.com/corazawaf/coraza/v3/http/e2e/pkg" +``` +As a reference for library usage, see [`testing/e2e/e2e_test.go`](.testing/e2e/e2e_test.go). +Expected directives that have to be loaded and available flags can be found in [`http/e2e/main.go`](./examples/http/e2e/main.go). + ## Tools * [Go FTW](https://github.com/coreruleset/go-ftw): Rule testing engine diff --git a/go.mod b/go.mod index 68b504f6f..2e2e26839 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/corazawaf/libinjection-go v0.1.2 github.com/foxcpp/go-mockdns v1.0.0 github.com/magefile/mage v1.15.0 + github.com/mccutchen/go-httpbin/v2 v2.8.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20211021192214-5ab2d9280aa9 github.com/tidwall/gjson v1.14.4 golang.org/x/net v0.11.0 diff --git a/go.sum b/go.sum index 029d1bb8f..b7b907ead 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6 github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/mccutchen/go-httpbin/v2 v2.8.0 h1:5Ld/mII532zWV6Dn9UpOUZebfD8chkYmx3UIW3VZET8= +github.com/mccutchen/go-httpbin/v2 v2.8.0/go.mod h1:+DBHcmg6EOeoizuiOI8iL12VIHXx+9YQNlz+gjB9uxk= github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= diff --git a/go.work.sum b/go.work.sum index ba0489e53..96e0c04e2 100644 --- a/go.work.sum +++ b/go.work.sum @@ -96,6 +96,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+ github.com/npillmayer/nestext v0.1.3 h1:2dkbzJ5xMcyJW5b8wwrX+nnRNvf/Nn1KwGhIauGyE2E= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pelletier/go-toml v1.9.1/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= @@ -111,7 +112,9 @@ github.com/ryanuber/columnize v2.1.0+incompatible h1:j1Wcmh8OrK4Q7GXY+V7SVSY8nUW github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= diff --git a/http/e2e/main.go b/http/e2e/main.go new file mode 100644 index 000000000..c3f84c069 --- /dev/null +++ b/http/e2e/main.go @@ -0,0 +1,57 @@ +// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "flag" + "fmt" + "os" + + e2e "github.com/corazawaf/coraza/v3/http/e2e/pkg" +) + +// Flags: +// --nulled-body: Interruptions at response body phase are allowed to return 200 (Instead of 403), but with a body full of null bytes. Defaults to "false". +// --proxy-hostport: Proxy endpoint used to perform requests. Defaults to "localhost:8080". +// --httpbin-hostport: Upstream httpbin endpoint, used for health checking reasons. Defaults to "localhost:8081". + +// Expected Coraza configs: +/* +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess On +SecResponseBodyMimeType application/json +# Custom rule for Coraza config check (ensuring that these configs are used) +SecRule &REQUEST_HEADERS:coraza-e2e "@eq 0" "id:100,phase:1,deny,status:424,log,msg:'Coraza E2E - Missing header'" +# Custom rules for e2e testing +SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,log,deny" +SecRule REQUEST_BODY "@rx maliciouspayload" "id:102,phase:2,t:lowercase,log,deny" +SecRule RESPONSE_HEADERS:pass "@rx leak" "id:103,phase:3,t:lowercase,log,deny" +SecRule RESPONSE_BODY "@contains responsebodycode" "id:104,phase:4,t:lowercase,log,deny" +# Custom rules mimicking the following CRS rules: 941100, 942100, 913100 +SecRule ARGS_NAMES|ARGS "@detectXSS" "id:9411,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls,log,deny" +SecRule ARGS_NAMES|ARGS "@detectSQLi" "id:9421,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatch,log,deny" +SecRule REQUEST_HEADERS:User-Agent "@pm grabber masscan" "id:9131,phase:1,t:none,log,deny" +*/ + +func main() { + // Initialize variables + var ( + nulledBody = flag.Bool("nulled-body", false, "Accept a body filled of empty bytes as an enforced disruptive action. Default: false") + proxyHostport = flag.String("proxy-hostport", "localhost:8080", "Configures the URL in which the proxy is running. Default: \"localhost:8080\"") + httpbinHostport = flag.String("httpbin-hostport", "localhost:8081", "Configures the URL in which httpbin is running. Default: \"localhost:8081\"") + ) + flag.Parse() + + err := e2e.Run(e2e.Config{ + NulledBody: *nulledBody, + ProxiedEntrypoint: *proxyHostport, + HttpbinEntrypoint: *httpbinHostport, + }) + + if err != nil { + fmt.Printf("[Fail] %s\n", err) + os.Exit(1) + } +} diff --git a/http/e2e/pkg/runner.go b/http/e2e/pkg/runner.go new file mode 100644 index 000000000..9532b7efb --- /dev/null +++ b/http/e2e/pkg/runner.go @@ -0,0 +1,311 @@ +// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" +) + +const ( + configCheckStatusCode = 424 + healthCheckTimeout = 15 // Seconds +) + +type Config struct { + NulledBody bool + ProxiedEntrypoint string + HttpbinEntrypoint string +} + +// statusCodeExpectation is a function that checks the status code of a response +// Some connectors (such as coraza-proxy-wasm) might not be able to change anymore the status code at phase:4, +// therefore, if nulledBody parameter is true, we expect a 200, but with a nulled body +type statusCodeExpectation func(int) error + +func expectStatusCode(expectedCode int) statusCodeExpectation { + return func(code int) error { + if code != expectedCode { + return fmt.Errorf("expected status code %d, got %d", expectedCode, code) + } + + return nil + } +} + +func expectNulledBodyStatusCode(nulledBody bool, expectedEmptyBodyCode, expectedNulledBodyCode int) statusCodeExpectation { + return func(code int) error { + if nulledBody { + if code != expectedNulledBodyCode { + return fmt.Errorf("expected status code %d, got %d", expectedNulledBodyCode, code) + } + + return nil + } + + if code != expectedEmptyBodyCode { + return fmt.Errorf("expected status code %d, got %d", expectedEmptyBodyCode, code) + } + + return nil + } +} + +// bodyExpectation sets a function to check the body expectations. +// Some connectors (such as coraza-proxy-wasm) might not be able to change anymore the status code at phase:4, +// therefore, if nulledBody parameter is true, we expect a 200, but with a nulled body +type bodyExpectation func(int, []byte) error + +func expectEmptyOrNulledBody(nulledBody bool) bodyExpectation { + return func(contentLength int, body []byte) error { + if nulledBody { + if contentLength == 0 { + return fmt.Errorf("expected nulled body, got content-length 0") + } + + if len(body) == 0 { + return fmt.Errorf("expected nulled body, got empty body") + } + + for _, b := range body { + if b != 0 { + return fmt.Errorf("expected nulled body, got %q", string(body)) + } + } + + return nil + } + + if contentLength != 0 { + return fmt.Errorf("expected empty body, got content-length %d", contentLength) + } + + if len(body) != 0 { + return fmt.Errorf("expected empty body, got %q", string(body)) + } + + return nil + } +} + +func expectEmptyBody() bodyExpectation { + return func(contentLength int, body []byte) error { + if contentLength != 0 { + return fmt.Errorf("expected empty body, got content-length %d", contentLength) + } + + if len(body) != 0 { + return fmt.Errorf("expected empty body, got %q", string(body)) + } + + return nil + } +} + +func Run(cfg Config) error { + healthURL := setHTTPSchemeIfMissing(cfg.HttpbinEntrypoint) + "/status/200" + baseProxyURL := setHTTPSchemeIfMissing(cfg.ProxiedEntrypoint) + echoProxiedURL := setHTTPSchemeIfMissing(baseProxyURL) + "/anything" + + healthChecks := []struct { + name string + url string + expectedCode int + }{ + { + name: "Health check", + url: healthURL, + expectedCode: 200, + }, + { + name: "Proxy check", + url: baseProxyURL, + expectedCode: 200, + }, + { + name: "Header check", + url: baseProxyURL, + expectedCode: configCheckStatusCode, + }, + } + + tests := []struct { + name string + requestURL string + requestHeaders map[string]string + requestBody string + requestMethod string + expectedStatusCode statusCodeExpectation + expectedBody bodyExpectation + }{ + { + name: "Legit request", + requestURL: baseProxyURL + "?arg=arg_1", + requestMethod: "GET", + expectedStatusCode: expectStatusCode(200), + }, + { + name: "Denied request by URL", + requestURL: baseProxyURL + "/admin", + requestMethod: "GET", + expectedStatusCode: expectStatusCode(403), + expectedBody: expectEmptyBody(), + }, + { + name: "Legit request with legit body", + requestURL: echoProxiedURL, + requestMethod: "POST", + // When sending a POST request, the "application/x-www-form-urlencoded" content-type header is needed + // being the only content-type for which by default Coraza enforces the request body processing. + // See https://github.com/corazawaf/coraza/issues/438 + requestHeaders: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + requestBody: "This is a legit payload", + expectedStatusCode: expectStatusCode(200), + }, + { + name: "Denied request with a malicious request body", + requestURL: echoProxiedURL, + requestMethod: "POST", + requestHeaders: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + requestBody: "maliciouspayload", + expectedStatusCode: expectStatusCode(403), + }, + { + name: "Denied request with a malicious response header", + requestURL: baseProxyURL + "/response-headers?pass=leak", + requestMethod: "GET", + expectedStatusCode: expectStatusCode(403), + }, + { + name: "Denied request with a malicious response body", + requestURL: echoProxiedURL, + requestMethod: "POST", + requestHeaders: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + requestBody: "responsebodycode", + expectedBody: expectEmptyOrNulledBody(cfg.NulledBody), + expectedStatusCode: expectNulledBodyStatusCode(cfg.NulledBody, 403, 200), + }, + { + name: "Denied request with XSS query parameters", + requestURL: echoProxiedURL + "?arg=", + requestMethod: "GET", + expectedStatusCode: expectStatusCode(403), + }, + { + name: "Denied request with SQLi query parameters", + requestURL: echoProxiedURL, + requestMethod: "POST", + requestHeaders: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, + requestBody: "1%27%20ORDER%20BY%203--%2B", + expectedStatusCode: expectStatusCode(403), + }, + { + name: "CRS malicious UA test (913100-6)", + requestURL: echoProxiedURL, + requestHeaders: map[string]string{ + "User-Agent": "Grabber/0.1 (X11; U; Linux i686; en-US; rv:1.7)", + }, + requestMethod: "GET", + expectedStatusCode: expectStatusCode(403), + }, + } + + // Check health endpoint + client := http.DefaultClient + for currentCheckIndex, healthCheck := range healthChecks { + fmt.Printf("[%d/%d] Running health check: %s\n", currentCheckIndex+1, len(healthChecks), healthCheck.name) + timeout := healthCheckTimeout + + ticker := time.NewTicker(time.Second) + defer ticker.Stop() + + req, _ := http.NewRequest(http.MethodGet, healthCheck.url, nil) + for range ticker.C { + if healthCheck.expectedCode != configCheckStatusCode { + // The default e2e header is not added if we are checking that the expected config is loaded + req.Header.Add("coraza-e2e", "ok") + } + resp, err := client.Do(req) + fmt.Printf("[Wait] Waiting for %s. Timeout: %ds\n", healthCheck.url, timeout) + if err == nil { + resp.Body.Close() + if resp.StatusCode == healthCheck.expectedCode { + fmt.Printf("[Ok] Check successful, got status code %d\n", resp.StatusCode) + break + } + if healthCheck.expectedCode == configCheckStatusCode { + return fmt.Errorf("configs check failed, got status code %d, expected %d. Please check configs used", resp.StatusCode, healthCheck.expectedCode) + } + } + timeout-- + if timeout == 0 { + return fmt.Errorf("timeout waiting for response from %s, make sure the server is running. Last request error: %v", healthCheck.url, err) + } + } + } + + // Iterate over tests + for currentTestIndex, test := range tests { + fmt.Printf("[%d/%d] Running test: %s\n", currentTestIndex+1, len(tests), test.name) + var requestBody io.Reader + if test.requestBody != "" { + requestBody = strings.NewReader(test.requestBody) + } + + req, err := http.NewRequest(test.requestMethod, test.requestURL, requestBody) + if err != nil { + return fmt.Errorf("could not make http request: %v", err) + } + for k, v := range test.requestHeaders { + req.Header.Add(k, v) + } + req.Header.Add("coraza-e2e", "ok") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("could not do http request: %v", err) + } + + respBody, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return fmt.Errorf("could not read response body: %v", err) + } + + if test.expectedStatusCode != nil { + if err := test.expectedStatusCode(resp.StatusCode); err != nil { + return err + } + + fmt.Printf("[Ok] Got expected status code %d\n", resp.StatusCode) + } + + if test.expectedBody != nil { + code, err := strconv.Atoi(resp.Header.Get("Content-Length")) + if err != nil { + return fmt.Errorf("could not convert content-length header to int: %v", err) + } + + if err := test.expectedBody(code, respBody); err != nil { + return err + } + + fmt.Print("[Ok] Got expected response body\n") + } + } + return nil +} + +func setHTTPSchemeIfMissing(rawURL string) string { + parsedURL, _ := url.Parse(rawURL) + if parsedURL.Scheme == "" { + parsedURL.Scheme = "http" + } + return parsedURL.String() +} diff --git a/http/interceptor.go b/http/interceptor.go index 0f4f27ee6..6b14aead3 100644 --- a/http/interceptor.go +++ b/http/interceptor.go @@ -43,6 +43,7 @@ func (i *rwInterceptor) WriteHeader(statusCode int) { i.statusCode = statusCode if it := i.tx.ProcessResponseHeaders(statusCode, i.proto); it != nil { + i.Header().Set("Content-Length", "0") i.statusCode = obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode) i.flushWriteHeader() return @@ -136,6 +137,7 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) ( i.flushWriteHeader() return err } else if it != nil { + i.Header().Set("Content-Length", "0") i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)) i.flushWriteHeader() return nil diff --git a/http/middleware.go b/http/middleware.go index 06bc99044..666f56399 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -167,7 +167,7 @@ func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultSt if it.Action == "deny" { statusCode := it.Status if statusCode == 0 { - statusCode = 503 + statusCode = 403 } return statusCode diff --git a/http/middleware_test.go b/http/middleware_test.go index 9ad074408..404dfaa2d 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -446,7 +446,9 @@ func runAgainstWAF(t *testing.T, tCase httpTest, waf coraza.WAF) { reqBody = strings.NewReader(tCase.reqBody) } req, _ := http.NewRequest("POST", ts.URL+tCase.reqURI, reqBody) - // TODO(jcchavezs): Fix it once the discussion in https://github.com/corazawaf/coraza/issues/438 is settled + // When sending a POST request, the "application/x-www-form-urlencoded" content-type header is needed + // being the only content-type for which by default Coraza enforces the request body processing. + // See https://github.com/corazawaf/coraza/issues/438 req.Header.Add("content-type", "application/x-www-form-urlencoded") res, err := ts.Client().Do(req) if err != nil { @@ -488,7 +490,7 @@ func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) { }{ "action deny with no code": { interruptionAction: "deny", - expectedCode: 503, + expectedCode: 403, }, "action deny with code": { interruptionAction: "deny", diff --git a/testing/e2e/e2e_test.go b/testing/e2e/e2e_test.go new file mode 100644 index 000000000..08fbd0999 --- /dev/null +++ b/testing/e2e/e2e_test.go @@ -0,0 +1,68 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +// These benchmarks don't currently compile with TinyGo +//go:build !tinygo +// +build !tinygo + +package e2e_test + +import ( + _ "embed" + "net/http" + "net/http/httptest" + "testing" + + "github.com/corazawaf/coraza/v3" + txhttp "github.com/corazawaf/coraza/v3/http" + e2e "github.com/corazawaf/coraza/v3/http/e2e/pkg" + "github.com/mccutchen/go-httpbin/v2/httpbin" +) + +func TestE2e(t *testing.T) { + conf := coraza.NewWAFConfig() + + customE2eDirectives := ` + SecRuleEngine On + SecRequestBodyAccess On + SecResponseBodyAccess On + SecResponseBodyMimeType application/json + # Custom rule for Coraza config check (ensuring that these configs are used) + SecRule &REQUEST_HEADERS:coraza-e2e "@eq 0" "id:100,phase:1,deny,status:424,log,msg:'Coraza E2E - Missing header'" + # Custom rules for e2e testing + SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,log,deny" + SecRule REQUEST_BODY "@rx maliciouspayload" "id:102,phase:2,t:lowercase,log,deny" + SecRule RESPONSE_HEADERS:pass "@rx leak" "id:103,phase:3,t:lowercase,log,deny" + SecRule RESPONSE_BODY "@contains responsebodycode" "id:104,phase:4,t:lowercase,log,deny" + # Custom rules mimicking the following CRS rules: 941100, 942100, 913100 + SecRule ARGS_NAMES|ARGS "@detectXSS" "id:9411,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:htmlEntityDecode,t:jsDecode,t:cssDecode,t:removeNulls,log,deny" + SecRule ARGS_NAMES|ARGS "@detectSQLi" "id:9421,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatch,log,deny" + SecRule REQUEST_HEADERS:User-Agent "@pm grabber masscan" "id:9131,phase:1,t:none,log,deny" +` + conf = conf. + WithDirectives(customE2eDirectives) + + waf, err := coraza.NewWAF(conf) + if err != nil { + t.Fatal(err) + } + + httpbin := httpbin.New() + + mux := http.NewServeMux() + mux.Handle("/status/200", httpbin) // Health check + mux.Handle("/", txhttp.WrapHandler(waf, httpbin)) + + // Create the server with the WAF and the reverse proxy. + s := httptest.NewServer(mux) + defer s.Close() + + err = e2e.Run(e2e.Config{ + NulledBody: false, + ProxiedEntrypoint: s.URL, + HttpbinEntrypoint: s.URL, + }) + if err != nil { + t.Fatalf("e2e tests failed: %v", err) + } +}