From 61f7926017314d9a893f73f9c60dd98589f9bd31 Mon Sep 17 00:00:00 2001 From: Matteo Pace Date: Wed, 6 Sep 2023 11:37:53 +0200 Subject: [PATCH] feat(e2e): swaps e2e with the official Coraza ones, updates Go to 1.20 (#224) --- .github/workflows/ci.yaml | 13 +- .github/workflows/nightly-coraza-check.yaml | 2 +- README.md | 2 +- e2e/Dockerfile.curl | 16 --- e2e/docker-compose.yml | 17 ++- e2e/e2e-example.sh | 151 -------------------- e2e/envoy-config.yaml | 109 ++++++++++++++ example/docker-compose.yml | 4 +- ftw/docker-compose.yml | 4 +- go.mod | 2 +- magefiles/go.mod | 2 +- magefiles/magefile.go | 28 +++- 12 files changed, 152 insertions(+), 198 deletions(-) delete mode 100644 e2e/Dockerfile.curl delete mode 100755 e2e/e2e-example.sh create mode 100644 e2e/envoy-config.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6032aec..b338f65 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,12 +12,12 @@ on: workflow_dispatch: env: - GO_VERSION: 1.19 + GO_VERSION: '1.20' TINYGO_VERSION: 0.28.1 # Run e2e tests against latest two releases and latest dev ENVOY_IMAGES: > + envoyproxy/envoy:v1.27-latest envoyproxy/envoy:v1.26-latest - envoyproxy/envoy:v1.25-latest envoyproxy/envoy-dev:latest jobs: @@ -70,13 +70,8 @@ jobs: - name: Run unit tests run: go run mage.go coverage - - name: Run e2e tests against the example - shell: bash - run: > - for image in $ENVOY_IMAGES; do - echo "Running e2e with Envoy image $image" - ENVOY_IMAGE=$image go run mage.go e2e - done + - name: Run e2e tests + run: go run mage.go e2e - name: Run regression tests (ftw) run: go run mage.go ftw diff --git a/.github/workflows/nightly-coraza-check.yaml b/.github/workflows/nightly-coraza-check.yaml index 84a8f02..30c034c 100644 --- a/.github/workflows/nightly-coraza-check.yaml +++ b/.github/workflows/nightly-coraza-check.yaml @@ -8,7 +8,7 @@ on: - cron: "0 4 * * *" env: - GO_VERSION: 1.19 + GO_VERSION: '1.20' TINYGO_VERSION: 0.28.1 jobs: diff --git a/README.md b/README.md index 2a6dcc1..bf6b3b7 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ In order to monitor envoy logs while performing requests you can run: ### Manual requests -Run `./e2e/e2e-example.sh` in order to run the following requests against the just set up test environment, otherwise manually execute and tweak them to grasp the behaviour of the filter: +List of requests that can be manually executed and tweaked to grasp the behaviour of the filter: ```bash # True positive requests: diff --git a/e2e/Dockerfile.curl b/e2e/Dockerfile.curl deleted file mode 100644 index 4a0e98b..0000000 --- a/e2e/Dockerfile.curl +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2022 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 - -FROM curlimages/curl -USER root - -WORKDIR /workspace - -RUN apk add --no-cache bash - -COPY ./e2e-example.sh /workspace/e2e-example.sh - -ENV ENVOY_HOST=envoy:8080 -ENV HTTPBIN_HOST=httpbin:8080 - -CMD ["bash","-c", "/workspace/e2e-example.sh"] diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index d16ffd9..fd15e27 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -1,19 +1,18 @@ services: httpbin: - image: mccutchen/go-httpbin:v2.5.0 + image: mccutchen/go-httpbin:v2.9.0 + command: [ "/bin/go-httpbin", "-port", "8081" ] + ports: + - 8081:8081 envoy: depends_on: - httpbin - image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.23-latest} + image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.27-latest} command: - -c - /conf/envoy-config.yaml volumes: - ../build:/build - - ../example:/conf # relying on envoy-config file from /example/ - tests: - depends_on: - - envoy - build: - context: . - dockerfile: ./Dockerfile.curl + - .:/conf + ports: + - 8080:8080 diff --git a/e2e/e2e-example.sh b/e2e/e2e-example.sh deleted file mode 100755 index 86a498f..0000000 --- a/e2e/e2e-example.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash -# Copyright 2022 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 -ENVOY_HOST=${ENVOY_HOST:-"localhost:8080"} -HTTPBIN_HOST=${HTTPBIN_HOST:-"localhost:8081"} -TIMEOUT_SECS=${TIMEOUT_SECS:-5} - -[[ "${DEBUG}" == "true" ]] && set -x - -# if env variables are in place, default values are overridden -health_url="http://${HTTPBIN_HOST}" -envoy_url_unfiltered="http://${ENVOY_HOST}" -envoy_url_filtered="${envoy_url_unfiltered}/admin" -envoy_url_filtered_resp_header="${envoy_url_unfiltered}/status/406" -envoy_url_echo="${envoy_url_unfiltered}/anything" - -tueNegativeBodyPayload="This is a payload" -truePositiveBodyPayload="maliciouspayload" -trueNegativeBodyPayloadForResponseBody="Hello world" -truePositiveBodyPayloadForResponseBody="responsebodycode" - -# wait_for_service waits until the given URL returns a 200 status code. -# $1: The URL to send requests to. -# $2: The max number of requests to send before giving up. -function wait_for_service() { - local status_code="000" - local url=${1} - local max=${2} - while [[ "${status_code}" -ne "200" ]]; do - status_code=$(curl --write-out "%{http_code}" --silent --output /dev/null "${url}") - sleep 1 - echo -ne "[Wait] Waiting for response from ${url}. Timeout: ${max}s \r" - ((max-=1)) - if [[ "${max}" -eq 0 ]]; then - echo "[Fail] Timeout waiting for response from ${url}, make sure the server is running." - exit 1 - fi - done - echo -e "\n[Ok] Got status code ${status_code}" -} - -# check_status sends HTTP requests to the given URL and expects a given response code. -# $1: The URL to send requests to. -# $2: The expected status code. -# $3-N: The rest of the arguments will be passed to the curl command as additional arguments -# to customize the HTTP call. -function check_status() { - local url=${1} - local status=${2} - local args=("${@:3}" --write-out '%{http_code}' --silent --output /dev/null) - status_code=$(curl --max-time ${TIMEOUT_SECS} "${args[@]}" "${url}") - if [[ "${status_code}" -ne ${status} ]] ; then - echo "[Fail] Unexpected response with code ${status_code} from ${url}" - exit 1 - fi - echo "[Ok] Got status code ${status_code}, expected ${status}" -} - -# check_body sends the given HTTP request and checks the response body. -# $1: The URL to send requests to. -# $2: true/false indicating if an empty, or null body is expected or not. -# $3-N: The rest of the arguments will be passed to the curl command as additional arguments -# to customize the HTTP call. -function check_body() { - local url=${1} - local empty=${2} - local args=("${@:3}" --silent) - response_body=$(curl --max-time ${TIMEOUT_SECS} "${args[@]}" "${url}") - if [[ "${empty}" == "true" ]] && [[ -n "${response_body}" ]]; then - echo -e "[Fail] Unexpected response with a body. Body dump:\n${response_body}" - exit 1 - fi - if [[ "${empty}" != "true" ]] && [[ -z "${response_body}" ]]; then - echo -e "[Fail] Unexpected response with a body. Body dump:\n${response_body}" - exit 1 - fi - echo "[Ok] Got response with an expected body (empty=${empty})" -} - -step=1 -total_steps=12 - -## Testing that basic coraza phases are working - -# Testing if the server is up -echo "[${step}/${total_steps}] Testing application reachability" -wait_for_service "${health_url}" 15 - -# Testing envoy container reachability with an unfiltered GET request -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true negative request" -wait_for_service "${envoy_url_echo}?arg=arg_1" 20 - -# Testing filtered request -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule" -check_status "${envoy_url_filtered}" 403 -# This test ensures the response body is empty on interruption. Specifically this makes -# sure no body is returned although actionContinue is passed in phase 3 & 4. -# See https://github.com/corazawaf/coraza-proxy-wasm/pull/126 -check_body "${envoy_url_filtered}" true - -# Testing body true negative -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true negative request (body)" -check_status "${envoy_url_echo}" 200 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${tueNegativeBodyPayload}" - -# Testing body detection -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true positive request (body)" -check_status "${envoy_url_unfiltered}" 403 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${truePositiveBodyPayload}" - -# Testing response headers detection -((step+=1)) -echo "[${step}/${total_steps}] (onResponseHeaders) Testing true positive" -check_status "${envoy_url_filtered_resp_header}" 403 - -# TODO(M4tteoP): Update response body e2e after https://github.com/corazawaf/coraza-proxy-wasm/issues/26 -# Testing response body true negative -((step+=1)) -echo "[${step}/${total_steps}] (onResponseBody) Testing true negative" -check_body "${envoy_url_unfiltered}" false -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${trueNegativeBodyPayloadForResponseBody}" - -# Testing response body detection -((step+=1)) -echo "[${step}/${total_steps}] (onResponseBody) Testing true positive" -check_body "${envoy_url_echo}" true -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${truePositiveBodyPayloadForResponseBody}" - -## Testing extra requests examples from the readme and some CRS rules in anomaly score mode. - -# Testing XSS detection during phase 1 -((step+=1)) -echo "[${step}/${total_steps}] Testing XSS detefction at request headers" -check_status "${envoy_url_echo}?arg=" 403 - -# Testing SQLI detection during phase 2 -((step+=1)) -echo "[${step}/${total_steps}] Testing SQLi detection at request body" -check_status "${envoy_url_echo}" 403 -X POST --data "1%27%20ORDER%20BY%203--%2B" - -# Triggers a CRS scanner detection rule (913100) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing CRS rule 913100" -check_status "${envoy_url_echo}" 403 --user-agent "Grabber/0.1 (X11; U; Linux i686; en-US; rv:1.7)" -H "Host: localhost" -H "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5" - -# True negative GET request with an usual user-agent -((step+=1)) -echo "[${step}/${total_steps}] True negative GET request with user-agent" -check_status "${envoy_url_echo}" 200 --user-agent "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" - -echo "[Done] All tests passed" diff --git a/e2e/envoy-config.yaml b/e2e/envoy-config.yaml new file mode 100644 index 0000000..f044c38 --- /dev/null +++ b/e2e/envoy-config.yaml @@ -0,0 +1,109 @@ +stats_config: + stats_tags: + # Envoy extracts the first matching group as a value. + # See https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/metrics/v3/stats.proto#config-metrics-v3-statsconfig. + - tag_name: phase + regex: "(_phase=([a-z_]+))" + - tag_name: rule_id + regex: "(_ruleid=([0-9]+))" + - tag_name: identifier + regex: "(_identifier=([0-9a-z.:]+))" + - tag_name: owner + regex: "(_owner=([0-9a-z.:]+))" + - tag_name: authority + regex: "(_authority=([0-9a-z.:]+))" + +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 8080 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + stat_prefix: ingress_http + codec_type: auto + route_config: + # A custom response header is added for e2e testing purposes. A local response, triggered by an interruption, + # has to allow custom added headers like this. See https://github.com/corazawaf/coraza-proxy-wasm/pull/172 + response_headers_to_add: + - header: + key: "custom_header" + value: "custom_value" + virtual_hosts: + - name: local_route + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: local_server + http_filters: + - name: envoy.filters.http.wasm + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm + config: + name: "coraza-filter" + root_id: "" + configuration: + "@type": "type.googleapis.com/google.protobuf.StringValue" + # See https://github.com/corazawaf/coraza/blob/main/http/e2e/cmd/httpe2e/main.go#L22 for e2e Coraza directives + value: | + { + "directives_map": { + "rs1": [ + "SecRuleEngine On", + "SecRequestBodyAccess On", + "SecResponseBodyAccess On", + "SecResponseBodyMimeType application/json", + "SecRule &REQUEST_HEADERS:coraza-e2e \"@eq 0\" \"id:100,phase:1,deny,status:424,log,msg:'Coraza E2E - Missing header'\"", + "SecRule REQUEST_URI \"@streq /admin\" \"id:101,phase:1,t:lowercase,log,deny,status:403\"", + "SecRule REQUEST_BODY \"@rx maliciouspayload\" \"id:102,phase:2,t:lowercase,log,deny,status:403\"", + "SecRule RESPONSE_HEADERS:pass \"@rx leak\" \"id:103,phase:3,t:lowercase,log,deny,status:403\"", + "SecRule RESPONSE_BODY \"@contains responsebodycode\" \"id:104,phase:4,t:lowercase,log,deny,status:403\"", + "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,status:403\"", + "SecRule ARGS_NAMES|ARGS \"@detectSQLi\" \"id:9421,phase:2,t:none,t:utf8toUnicode,t:urlDecodeUni,t:removeNulls,multiMatch,log,deny,status:403\"", + "SecRule REQUEST_HEADERS:User-Agent \"@pm grabber masscan\" \"id:9131,phase:1,t:none,log,deny,status:403\"" + ] + }, + "default_directives": "rs1", + "metric_labels": { + "owner": "coraza", + "identifier": "global" + } + } + vm_config: + runtime: "envoy.wasm.runtime.v8" + vm_id: "my_vm_id" + code: + local: + filename: "build/main.wasm" + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + + clusters: + - name: local_server + connect_timeout: 6000s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + load_assignment: + cluster_name: local_server + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: httpbin + port_value: 8081 + +admin: + access_log_path: "/dev/null" + address: + socket_address: + address: 0.0.0.0 + port_value: 8082 diff --git a/example/docker-compose.yml b/example/docker-compose.yml index 76a7906..952ec71 100644 --- a/example/docker-compose.yml +++ b/example/docker-compose.yml @@ -1,6 +1,6 @@ services: httpbin: - image: mccutchen/go-httpbin:v2.5.0 + image: mccutchen/go-httpbin:v2.9.0 environment: - MAX_BODY_SIZE=15728640 # 15 MiB ports: @@ -19,7 +19,7 @@ services: depends_on: - chown - httpbin - image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.23-latest} + image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.27-latest} command: - -c - /conf/envoy-config.yaml diff --git a/ftw/docker-compose.yml b/ftw/docker-compose.yml index 49049fb..a7c6407 100644 --- a/ftw/docker-compose.yml +++ b/ftw/docker-compose.yml @@ -1,6 +1,6 @@ services: httpbin: - image: mccutchen/go-httpbin:v2.5.0 + image: mccutchen/go-httpbin:v2.9.0 chown: image: alpine:3.16 command: @@ -14,7 +14,7 @@ services: depends_on: - chown - httpbin - image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.23-latest} + image: ${ENVOY_IMAGE:-envoyproxy/envoy:v1.27-latest} command: - -c - ${ENVOY_CONFIG:-/conf/envoy-config.yaml} diff --git a/go.mod b/go.mod index 41d0bcb..ad8594c 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/corazawaf/coraza-proxy-wasm -go 1.19 +go 1.20 require ( github.com/corazawaf/coraza-wasilibs v0.0.0-20230620081031-05a5097dbea3 diff --git a/magefiles/go.mod b/magefiles/go.mod index 5cd743b..99b43c4 100644 --- a/magefiles/go.mod +++ b/magefiles/go.mod @@ -1,6 +1,6 @@ module github.com/corazawaf/coraza-proxy-wasm/magefiles -go 1.19 +go 1.20 require ( fortio.org/fortio v1.38.4 diff --git a/magefiles/magefile.go b/magefiles/magefile.go index 5a4e96d..3995ce5 100644 --- a/magefiles/magefile.go +++ b/magefiles/magefile.go @@ -19,14 +19,13 @@ import ( "github.com/tetratelabs/wabin/wasm" ) -var minGoVersion = "1.19" +var minGoVersion = "1.20" var tinygoMinorVersion = "0.28" var addLicenseVersion = "04bfe4ee9ca5764577b029acc6a1957fd1997153" // https://github.com/google/addlicense -var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases +var golangCILintVer = "v1.54.2" // https://github.com/golangci/golangci-lint/releases var gosImportsVer = "v0.3.1" // https://github.com/rinchsan/gosimports/releases/tag/v0.3.1 var errCommitFormatting = errors.New("files not formatted, please commit formatting changes") -var errNoGitDir = errors.New("no .git directory found") func init() { for _, check := range []func() error{ @@ -216,10 +215,29 @@ func Build() error { // E2e runs e2e tests with a built plugin against the example deployment. Requires docker-compose. func E2e() error { - if err := sh.RunV("docker-compose", "--file", "e2e/docker-compose.yml", "build", "--pull"); err != nil { + var err error + if err = sh.RunV("docker-compose", "--file", "e2e/docker-compose.yml", "up", "-d", "envoy"); err != nil { return err } - return sh.RunV("docker-compose", "-f", "e2e/docker-compose.yml", "up", "--abort-on-container-exit", "tests") + defer func() { + _ = sh.RunV("docker-compose", "--file", "e2e/docker-compose.yml", "down", "-v") + }() + + envoyHost := os.Getenv("ENVOY_HOST") + if envoyHost == "" { + envoyHost = "localhost:8080" + } + httpbinHost := os.Getenv("HTTPBIN_HOST") + if httpbinHost == "" { + httpbinHost = "localhost:8081" + } + + // --nulled-body is needed because coraza-proxy-wasm returns a 200 OK with a nulled body when if the interruption happens after phase 3 + if err = sh.RunV("go", "run", "github.com/corazawaf/coraza/v3/http/e2e/cmd/httpe2e@main", "--proxy-hostport", + "http://"+envoyHost, "--httpbin-hostport", "http://"+httpbinHost, "--nulled-body"); err != nil { + sh.RunV("docker-compose", "-f", "e2e/docker-compose.yml", "logs", "envoy") + } + return err } // Ftw runs ftw tests with a built plugin and Envoy. Requires docker-compose.