From e0ba3a9dfb0e02ef83b271df984dc33280fe98d1 Mon Sep 17 00:00:00 2001 From: Matteo Pace Date: Mon, 21 Aug 2023 18:02:23 +0200 Subject: [PATCH] moves to official coraza e2e --- .github/workflows/container-image.yaml | 3 +- .gitignore | 5 +- docker-compose.e2e.yaml | 20 +-- docker/e2e/Dockerfile.curl | 16 -- docker/e2e/e2e-rules.conf | 21 ++- docker/e2e/e2e.sh | 199 ------------------------- magefile.go | 29 +++- 7 files changed, 53 insertions(+), 240 deletions(-) delete mode 100644 docker/e2e/Dockerfile.curl delete mode 100755 docker/e2e/e2e.sh diff --git a/.github/workflows/container-image.yaml b/.github/workflows/container-image.yaml index 52e71ef..2964e2e 100644 --- a/.github/workflows/container-image.yaml +++ b/.github/workflows/container-image.yaml @@ -35,7 +35,8 @@ jobs: run: > for image in $HAPROXY_IMAGES; do echo "Running e2e with Haproxy image $image" - HAPROXY_IMAGE=$image docker compose -f docker-compose.e2e.yaml up --abort-on-container-exit tests + HAPROXY_IMAGE=$image + go run mage.go e2e done - name: Set up Docker Buildx diff --git a/.gitignore b/.gitignore index f84664f..16a9611 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ # Test binary, built with `go test -c` *.test +# Binaries generated by make build +coraza-spoa_* + # Output of the go coverage tool, specifically when used with LiteIDE *.out vendor/ @@ -16,4 +19,4 @@ vendor/ # local files config.yaml logs/ -rules/ \ No newline at end of file +rules/ diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index 5d1c5e9..78dc4c6 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -1,10 +1,13 @@ version: "3.9" services: httpbin: - image: mccutchen/go-httpbin:v2.5.0 + restart: unless-stopped + image: mccutchen/go-httpbin:v2.9.0 + command: [ "/bin/go-httpbin", "-port", "8080" ] ports: - 8080:8080 coraza: + restart: unless-stopped build: context: . target: coreruleset @@ -21,7 +24,7 @@ services: "-c", "haproxy -f /usr/local/etc/haproxy/haproxy.cfg | tee /var/lib/haproxy/hap.log" ] - ports: [ "4000:80" ] + ports: [ "4000:80"] links: - "coraza:coraza" - "httpbin:httpbin" @@ -30,17 +33,6 @@ services: source: ./docker/haproxy target: /usr/local/etc/haproxy - hap:/var/lib/haproxy - tests: - depends_on: - - haproxy - - coraza - links: - - "haproxy:haproxy" - - "httpbin:httpbin" - build: - context: docker/e2e - dockerfile: ./Dockerfile.curl - volumes: - - hap:/haproxy + volumes: hap: diff --git a/docker/e2e/Dockerfile.curl b/docker/e2e/Dockerfile.curl deleted file mode 100644 index 26cddc5..0000000 --- a/docker/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.sh /workspace/e2e.sh - -ENV HAPROXY_HOST=haproxy:80 -ENV HTTPBIN_HOST=httpbin:8080 - -CMD ["bash", "/workspace/e2e.sh"] diff --git a/docker/e2e/e2e-rules.conf b/docker/e2e/e2e-rules.conf index aae10b6..e542534 100644 --- a/docker/e2e/e2e-rules.conf +++ b/docker/e2e/e2e-rules.conf @@ -1,9 +1,16 @@ +# See https://github.com/corazawaf/coraza/blob/main/http/e2e/cmd/httpe2e/main.go SecRuleEngine On SecRequestBodyAccess On -SecRule REQUEST_URI "/e2e-deny" "id:101,phase:1,t:lowercase,log,deny" -SecRule REQUEST_URI "/e2e-drop" "id:102,phase:1,t:lowercase,log,drop" -SecRule REQUEST_URI "/e2e-redirect" "id:103,phase:1,t:lowercase,log,redirect:http://www.example.org/denied" -SecRule REQUEST_BODY "@rx maliciouspayload" "id:104,phase:2,t:lowercase,log,deny" -SecRule RESPONSE_STATUS "@streq 406" "id:105,phase:3,t:lowercase,log,deny" -SecRule RESPONSE_HEADERS::e2eblock "true" "id:106,phase:4,t:lowercase,log,deny" -SecRule RESPONSE_BODY "@contains responsebodycode" "id:107,phase:4,t:lowercase,log,deny" +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" diff --git a/docker/e2e/e2e.sh b/docker/e2e/e2e.sh deleted file mode 100755 index 69372c2..0000000 --- a/docker/e2e/e2e.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/bin/bash -# Copyright 2022 The OWASP Coraza contributors -# SPDX-License-Identifier: Apache-2.0 -# -# Script derived from the original in coraza-proxy-wasm & extended for haproxy -# https://github.com/corazawaf/coraza-proxy-wasm/blob/main/e2e/e2e-example.sh - -HAPROXY_HOST=${HAPROXY_HOST:-"localhost:4000"} -HTTPBIN_HOST=${HTTPBIN_HOST:-"localhost:8080"} -HAPROXY_LOGS='/haproxy/hap.log' - -[[ "${DEBUG}" == "true" ]] && set -x - -# if env variables are in place, default values are overridden -health_url="http://${HTTPBIN_HOST}" -url_unfiltered="http://${HAPROXY_HOST}" -url_filtered_deny="${url_unfiltered}/e2e-deny" -url_filtered_drop="${url_unfiltered}/e2e-drop" -url_filtered_redirect="${url_unfiltered}/e2e-redirect" -url_filtered_resp_header="${url_unfiltered}/response-headers?e2eblock=true" -url_echo="${url_unfiltered}/anything" - -trueNegativeBodyPayload="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 "${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 "${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})" -} - -# check_hap_logs checks HAProxy logs for the given regexp. -# $1: The regexp to check logs aginst. -function check_hap_logs() { - local regex=${1} - if [[ $(grep -q -e "$regex" "$HAPROXY_LOGS") ]]; then - echo -e "[Fail] No log lines matches pattern '$regex'" - exit 1 - fi - echo "[Ok] Got logs with an expected pattern '$regex'" -} - -step=1 -total_steps=17 - -## 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 container reachability with an unfiltered GET request -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true negative request" -wait_for_service "${url_echo}?arg=arg_1" 20 - -# Testing filtered request (deny) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - deny" -check_status "${url_filtered_deny}" 403 - -# Testing filtered request (drop) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - drop" -check_status "${url_filtered_drop}" 000 - -# Testing filtered request (redirect) -((step+=1)) -echo "[${step}/${total_steps}] (onRequestheaders) Testing true positive custom rule - redirect" -check_status "${url_filtered_redirect}" 302 - -# Testing body true negative -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true negative request (body)" -check_status "${url_echo}" 200 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${trueNegativeBodyPayload}" - -# Testing body detection -((step+=1)) -echo "[${step}/${total_steps}] (onRequestBody) Testing true positive request (body)" -check_status "${url_echo}" 403 -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${truePositiveBodyPayload}" - -# TODO - Testing response headers detection TODO -#((step+=1)) -#echo "[${step}/${total_steps}] (onResponseHeaders) Testing true positive" -#check_status "${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 "${url_echo}" false -X POST -H 'Content-Type: application/x-www-form-urlencoded' --data "${trueNegativeBodyPayloadForResponseBody}" - -# TODO - Testing response body detection -#((step+=1)) -#echo "[${step}/${total_steps}] (onResponseBody) Testing true positive" -#check_body "${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 "${url_echo}?arg=" 403 - -# Testing SQLI detection during phase 2 -((step+=1)) -echo "[${step}/${total_steps}] Testing SQLi detection at request body" -check_status "${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 "${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 "${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" - -# Find Allow action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: allow)" -check_hap_logs "waf-action: allow" - -# Find Deny action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: deny)" -check_hap_logs "waf-action: deny" - -# Find Drop action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: drop)" -check_hap_logs "waf-action: drop" - -# Find Redirect action -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (waf-action: redirect)" -check_hap_logs "waf-action: redirect" - -# Find no error -((step+=1)) -echo "[${step}/${total_steps}] HAP log format (spoa-error: -)" -check_hap_logs "spoa-error: -" - -echo "[Done] All tests passed" diff --git a/magefile.go b/magefile.go index 857119c..75b8079 100644 --- a/magefile.go +++ b/magefile.go @@ -19,8 +19,8 @@ import ( var addLicenseVersion = "v1.0.0" // https://github.com/google/addlicense // TODO: Use recent version (for example v1.53.2) to run on Go 1.20 (https://github.com/golangci/golangci-lint/pull/3414) -var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases -var gosImportsVer = "v0.1.5" // https://github.com/rinchsan/gosimports/releases/tag/v0.1.5 +var golangCILintVer = "v1.48.0" // https://github.com/golangci/golangci-lint/releases +var gosImportsVer = "v0.1.5" // https://github.com/rinchsan/gosimports/releases/tag/v0.1.5 var errRunGoModTidy = errors.New("go.mod/sum not formatted, commit changes") var errNoGitDir = errors.New("no .git directory found") @@ -73,6 +73,31 @@ func Test() error { return nil } +// E2e runs e2e tests with a built plugin against the e2e deployment. Requires docker-compose. +func E2e() error { + var err error + if err = sh.RunV("docker-compose", "-f", "docker-compose.e2e.yaml", "up", "-d", "haproxy"); err != nil { + return err + } + defer func() { + _ = sh.RunV("docker-compose", "--file", "docker-compose.e2e.yaml", "down", "-v") + }() + + haproxyHost := os.Getenv("HAPROXY_HOST") + if haproxyHost == "" { + haproxyHost = "localhost:4000" + } + httpbinHost := os.Getenv("HTTPBIN_HOST") + if httpbinHost == "" { + httpbinHost = "localhost:8080" + } + + if err = sh.RunV("go", "run", "github.com/corazawaf/coraza/v3/http/e2e/cmd/httpe2e@main", "--proxy-hostport", "http://"+haproxyHost, "--httpbin-hostport", "http://"+httpbinHost); err != nil { + sh.RunV("docker-compose", "-f", "docker-compose.e2e.yaml", "logs", "haproxy") + } + return err +} + // Precommit installs a git hook to run check when committing func Precommit() error { if _, err := os.Stat(filepath.Join(".git", "hooks")); os.IsNotExist(err) {