From 945b5c3e49df1c95941474eb109438ac5d44bbac Mon Sep 17 00:00:00 2001 From: Sebastien Blot Date: Wed, 4 Sep 2024 17:15:06 +0200 Subject: [PATCH] merge from upstream --- .github/dependabot.yml | 11 - .github/workflows/close-issues.yml | 2 +- .github/workflows/codeql-analysis.yml | 8 +- .github/workflows/fix-dependabot.yml | 28 - .github/workflows/fuzz.yml | 8 +- .github/workflows/lint.yml | 6 +- .github/workflows/regression.yml | 26 +- .github/workflows/tinygo.yml | 12 +- README.md | 11 +- coraza.conf-recommended | 110 +--- examples/http-server/go.mod | 16 +- examples/http-server/go.sum | 31 +- examples/http-server/main_test.go | 42 ++ .../plugins/auditlog_formatter_tinygo_test.go | 80 +++ .../plugins/plugintypes/transaction.go | 1 + experimental/waf.go | 17 + experimental/waf_test.go | 32 ++ go.mod | 24 +- go.sum | 111 ++-- go.work | 2 +- http/e2e/cmd/httpe2e/main.go | 19 +- http/e2e/e2e.go | 32 +- http/interceptor.go | 15 +- http/middleware.go | 16 +- http/middleware_test.go | 145 +++-- internal/actions/ctl_test.go | 2 + internal/actions/deny.go | 11 +- internal/actions/id.go | 1 + internal/actions/status.go | 1 + internal/auditlog/concurrent_writer_test.go | 7 +- internal/auditlog/formats_json.go | 4 - internal/auditlog/formats_json_test.go | 3 - internal/auditlog/init_tinygo.go | 9 +- internal/auditlog/logger.go | 4 +- internal/auditlog/logger_test.go | 59 ++ internal/auditlog/noop_writer.go | 2 +- internal/bodyprocessors/multipart.go | 7 + internal/bodyprocessors/multipart_test.go | 116 ++++ internal/bodyprocessors/raw.go | 43 ++ internal/bodyprocessors/raw_test.go | 34 ++ internal/collections/map.go | 80 ++- internal/collections/map_test.go | 47 +- internal/collections/named.go | 6 + internal/cookies/cookies.go | 3 + internal/cookies/cookies_test.go | 3 + internal/corazarules/rule.go | 14 +- internal/corazarules/rule_match.go | 38 +- internal/corazawaf/casesensitive.go | 8 + internal/corazawaf/casesensitive_default.go | 8 + internal/corazawaf/rule.go | 221 ++++---- internal/corazawaf/rule_casesensitive_test.go | 22 + internal/corazawaf/rule_test.go | 105 ++-- internal/corazawaf/rulegroup.go | 4 +- internal/corazawaf/transaction.go | 101 +++- internal/corazawaf/transaction_test.go | 411 ++++++++++---- internal/corazawaf/waf.go | 44 +- internal/corazawaf/waf_test.go | 4 +- internal/environment/default.go | 18 + internal/environment/default_test.go | 45 ++ internal/environment/nofsaccess.go | 6 + internal/operators/eq_test.go | 53 ++ internal/operators/inspect_file_test.go | 11 +- internal/seclang/directives.go | 134 ++++- internal/seclang/directives_test.go | 46 +- internal/seclang/directivesmap.gen.go | 3 +- .../seclang/generator/directivesmap.go.tmpl | 1 - internal/seclang/parser.go | 10 +- internal/seclang/rule_parser.go | 12 +- internal/seclang/rule_parser_test.go | 54 +- internal/seclang/rules_casesensitive_test.go | 133 +++++ internal/seclang/rules_test.go | 219 +++----- internal/transformations/base64decode.go | 16 +- internal/transformations/base64decodeext.go | 10 + .../transformations/base64decodeext_test.go | 100 ++++ internal/transformations/testdata/md5.json | 10 +- internal/transformations/testdata/sha1.json | 6 +- internal/transformations/transformations.go | 1 + magefile.go | 9 +- renovate.json | 7 +- testing/auditlog_test.go | 109 ++++ testing/coreruleset/.ftw.yml | 7 +- testing/coreruleset/coreruleset_test.go | 27 +- testing/coreruleset/go.mod | 59 +- testing/coreruleset/go.sum | 504 +++--------------- testing/e2e/e2e_test.go | 19 +- testing/engine/chains.go | 2 +- testing/engine/directives_ruleexclusions.go | 94 ++++ testing/engine/disruptive_actions.go | 5 +- testing/engine/multiphase.go | 36 ++ types/rule_match.go | 1 + types/variables/variables.go | 2 + waf.go | 22 +- 92 files changed, 2689 insertions(+), 1329 deletions(-) delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/fix-dependabot.yml create mode 100644 experimental/plugins/auditlog_formatter_tinygo_test.go create mode 100644 experimental/waf.go create mode 100644 experimental/waf_test.go create mode 100644 internal/bodyprocessors/raw.go create mode 100644 internal/bodyprocessors/raw_test.go create mode 100644 internal/corazawaf/casesensitive.go create mode 100644 internal/corazawaf/casesensitive_default.go create mode 100644 internal/corazawaf/rule_casesensitive_test.go create mode 100644 internal/environment/default_test.go create mode 100644 internal/operators/eq_test.go create mode 100644 internal/seclang/rules_casesensitive_test.go create mode 100644 internal/transformations/base64decodeext.go create mode 100644 internal/transformations/base64decodeext_test.go create mode 100644 testing/engine/directives_ruleexclusions.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index eb4bfe6e0..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,11 +0,0 @@ -# To get started with Dependabot version updates, you'll need to specify which -# package ecosystems to update and where the package manifests are located. -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "gomod" # See documentation for possible values - directory: "/" # Location of package manifests - schedule: - interval: "daily" diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml index ade1c5480..69351ffee 100644 --- a/.github/workflows/close-issues.yml +++ b/.github/workflows/close-issues.yml @@ -10,7 +10,7 @@ jobs: issues: write pull-requests: write steps: - - uses: actions/stale@v9 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9 with: days-before-issue-stale: 30 days-before-issue-close: 14 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 0648fd0ab..1ef1abff4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -10,15 +10,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3 diff --git a/.github/workflows/fix-dependabot.yml b/.github/workflows/fix-dependabot.yml deleted file mode 100644 index 11f9154b7..000000000 --- a/.github/workflows/fix-dependabot.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Fixes dependabot lint -on: - pull_request: - types: [opened, synchronize] - branches: - - main -jobs: - change-and-push: - runs-on: ubuntu-latest - if: ${{ github.actor == 'dependabot[bot]'}} - steps: - - uses: actions/checkout@v4 - - name: Install Go - uses: actions/setup-go@v5 - with: - go-version: v1.19.x - cache: true - - name: Format code - run: go run mage.go format - - name: Check for uncommitted changes - id: check-uncommited - uses: mskri/check-uncommitted-changes-action@v1.0.1 - - name: Commit and push changes - if: steps.check-uncommited.outputs.outcome == failure() - uses: devops-infra/action-commit-push@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - commit_message: "chore: run mage format" diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 036aa930b..3ff1a966e 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -3,7 +3,7 @@ name: Fuzz tests on: schedule: # https://crontab.guru/#05_14_*_*_* - - cron: '05 14 * * *' + - cron: "05 14 * * *" workflow_dispatch: jobs: @@ -11,10 +11,10 @@ jobs: name: Fuzz tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + - uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5 with: - go-version: '>=1.19.0' + go-version: ">=1.21.0" - run: go run mage.go fuzz - run: | gh issue create --title "$GITHUB_WORKFLOW #$GITHUB_RUN_NUMBER failed" \ diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 96eb95a6c..a3d43e3a4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,10 +18,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5 with: - go-version: v1.19.x + go-version: v1.21.x cache: true - run: go run mage.go lint diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 75f934580..3bb0020e3 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -16,46 +16,46 @@ jobs: test: strategy: matrix: - go-version: [1.19.x, 1.20.x, 1.21.x] + go-version: [1.21.x, 1.22.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5 with: go-version: ${{ matrix.go-version }} cache: true - name: Tests and coverage run: go run mage.go coverage - name: "Codecov: General" - uses: codecov/codecov-action@v3 - if: ${{ matrix.go-version == '1.19.x' }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 + if: ${{ matrix.go-version == '1.21.x' }} with: files: build/coverage.txt flags: default - name: "Codecov: Examples" - uses: codecov/codecov-action@v3 - if: ${{ matrix.go-version == '1.19.x' }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 + if: ${{ matrix.go-version == '1.21.x' }} with: files: build/coverage-examples.txt flags: examples - name: "Codecov: FTW" - uses: codecov/codecov-action@v3 - if: ${{ matrix.go-version == '1.19.x' }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 + if: ${{ matrix.go-version == '1.21.x' }} with: files: build/coverage-ftw.txt flags: ftw - name: "Codecov: FTW Multiphase tag" - uses: codecov/codecov-action@v3 - if: ${{ matrix.go-version == '1.19.x' }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 + if: ${{ matrix.go-version == '1.21.x' }} with: files: build/coverage-ftw-multiphase.txt flags: ftw-multiphase - name: "Codecov: Tinygo" - uses: codecov/codecov-action@v3 - if: ${{ matrix.go-version == '1.19.x' }} + uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4 + if: ${{ matrix.go-version == '1.21.x' }} with: files: build/coverage-tinygo.txt flags: tinygo diff --git a/.github/workflows/tinygo.yml b/.github/workflows/tinygo.yml index 709cc5b8f..8659eb66b 100644 --- a/.github/workflows/tinygo.yml +++ b/.github/workflows/tinygo.yml @@ -18,26 +18,26 @@ jobs: test: strategy: matrix: - go-version: [1.19.x] + go-version: [1.21.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: Install Go - uses: actions/setup-go@v5 + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5 with: go-version: ${{ matrix.go-version }} cache: true - name: setup tinygo - uses: acifani/setup-tinygo@v2 + uses: acifani/setup-tinygo@b2ba42b249c7d3efdfe94166ec0f48b3191404f7 # v2 with: - tinygo-version: 0.27.0 + tinygo-version: '0.31.2' - name: Cache TinyGo build - uses: actions/cache@v3 + uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c # v3 with: path: | ~/.cache/tinygo diff --git a/README.md b/README.md index 33d314993..fe0327988 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ Coraza is an open source, enterprise-grade, high performance Web Application Fir Key Features: -* ⇲ **Drop-in** - Coraza is a drop-in alternative to replace the soon to be abandoned Trustwave ModSecurity Engine and supports industry standard SecLang rule sets. +* ⇲ **Drop-in** - Coraza is an alternative engine that has partial compatibility with ~~Trustwave~~[OWASP ModSecurity Engine](https://github.com/owasp-modsecurity/modsecurity/) and supports industry-standard SecLang rule sets. -* 🔥 **Security** - Coraza runs the [OWASP Core Rule Set (CRS)](https://coreruleset.org) **v4** to protect your web applications from a wide range of attacks, including the OWASP Top Ten, with a minimum of false alerts. CRS protects from many common attack categories including: SQL Injection (SQLi), Cross Site Scripting (XSS), PHP & Java Code Injection, HTTPoxy, Shellshock, Scripting/Scanner/Bot Detection & Metadata & Error Leakages. Note that older versions of the CRS are not compatible. +* 🔥 **Security** - Coraza runs the [OWASP CRS](https://coreruleset.org) **v4** (Formerly known as Core Rule Set) to protect your web applications from a wide range of attacks, including the OWASP Top Ten, with a minimum of false alerts. CRS protects from many common attack categories including: SQL Injection (SQLi), Cross Site Scripting (XSS), PHP & Java Code Injection, HTTPoxy, Shellshock, Scripting/Scanner/Bot Detection & Metadata & Error Leakages. Note that older versions of the CRS are not compatible. * 🔌 **Extensible** - Coraza is a library at its core, with many integrations to deploy on-premise Web Application Firewall instances. Audit Loggers, persistence engines, operators, actions, create your own functionalities to extend Coraza as much as you want. @@ -32,7 +32,7 @@ Key Features: * ﹡ **Simplicity** - Anyone is able to understand and modify the Coraza source code. It is easy to extend Coraza with new functionality. -* 💬 **Community** - Coraza is a community project, contributions are accepted and all ideas will be considered. Find contributor guidance in the [CONTRIBUTION](https://github.com/corazawaf/coraza/blob/v2/master/CONTRIBUTING.md) document. +* 💬 **Community** - Coraza is a community project, contributions are accepted and all ideas will be considered. Find contributor guidance in the [CONTRIBUTION](https://github.com/corazawaf/coraza/blob/main/CONTRIBUTING.md) document.
@@ -47,7 +47,7 @@ The Coraza Project maintains implementations and plugins for the following serve ## Prerequisites -* Go v1.19+ or tinygo compiler +* Go v1.21+ or tinygo compiler * Linux distribution (Debian or Centos recommended), Windows or Mac. ## Coraza Core Usage @@ -103,6 +103,7 @@ only the phase the rule is defined for. dictionaries to reduce memory consumption in deployments that launch several coraza instances. For more context check [this issue](https://github.com/corazawaf/coraza-caddy/issues/76) * `no_fs_access` - indicates that the target environment has no access to FS in order to not leverage OS' filesystem related functionality e.g. file body buffers. +* `coraza.rule.case_sensitive_args_keys` - enables case-sensitive matching for ARGS keys, aligning Coraza behavior with RFC 3986 specification. It will be enabled by default in the next major version. ## E2E Testing @@ -168,7 +169,7 @@ Our vulnerability management team will respond within 3 working days of your rep * OWASP Coreruleset team for the CRS and their help * Ivan Ristić for creating ModSecurity -### Coraza on Twitter +### Coraza on X/Twitter * [@corazaio](https://twitter.com/corazaio) diff --git a/coraza.conf-recommended b/coraza.conf-recommended index 3b9d8a741..ce311ecd1 100644 --- a/coraza.conf-recommended +++ b/coraza.conf-recommended @@ -28,12 +28,11 @@ SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \ SecRule REQUEST_HEADERS:Content-Type "^application/json" \ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" -# Sample rule to enable JSON request body parser for more subtypes. -# Uncomment or adapt this rule if you want to engage the JSON -# Processor for "+json" subtypes +# Enable JSON request body parser for more subtypes. +# Adapt this rule if you want to engage the JSON Processor for "+json" subtypes # -#SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \ -# "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" +SecRule REQUEST_HEADERS:Content-Type "^application/[a-z0-9.-]+[+]json" \ + "id:'200006',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" # Maximum request body size we will accept for buffering. If you support # file uploads then the value given on the first line has to be as large @@ -45,7 +44,8 @@ SecRequestBodyLimit 13107200 SecRequestBodyInMemoryLimit 131072 -SecRequestBodyNoFilesLimit 131072 +# SecRequestBodyNoFilesLimit is currently not supported by Coraza +# SecRequestBodyNoFilesLimit 131072 # What to do if the request body size is above our configured limit. # Keep in mind that this setting will automatically be set to ProcessPartial @@ -60,84 +60,16 @@ SecRequestBodyLimitAction Reject # or log a high-severity alert (when deployed in detection-only mode). # SecRule REQBODY_ERROR "!@eq 0" \ -"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" # By default be strict with what we accept in the multipart/form-data # request body. If the rule below proves to be too strict for your -# environment consider changing it to detection-only. You are encouraged -# _not_ to remove it altogether. +# environment consider changing it to detection-only. +# Do NOT remove it, as it will catch many evasion attempts. # SecRule MULTIPART_STRICT_ERROR "!@eq 0" \ -"id:'200003',phase:2,t:none,log,deny,status:400, \ -msg:'Multipart request body failed strict validation: \ -PE %{REQBODY_PROCESSOR_ERROR}, \ -BQ %{MULTIPART_BOUNDARY_QUOTED}, \ -BW %{MULTIPART_BOUNDARY_WHITESPACE}, \ -DB %{MULTIPART_DATA_BEFORE}, \ -DA %{MULTIPART_DATA_AFTER}, \ -HF %{MULTIPART_HEADER_FOLDING}, \ -LF %{MULTIPART_LF_LINE}, \ -SM %{MULTIPART_MISSING_SEMICOLON}, \ -IQ %{MULTIPART_INVALID_QUOTING}, \ -IP %{MULTIPART_INVALID_PART}, \ -IH %{MULTIPART_INVALID_HEADER_FOLDING}, \ -FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" - -# Did we see anything that might be a boundary? -# -# Here is a short description about the Coraza Multipart parser: the -# parser returns with value 0, if all "boundary-like" line matches with -# the boundary string which given in MIME header. In any other cases it returns -# with different value, eg. 1 or 2. -# -# The RFC 1341 descript the multipart content-type and its syntax must contains -# only three mandatory lines (above the content): -# * Content-Type: multipart/mixed; boundary=BOUNDARY_STRING -# * --BOUNDARY_STRING -# * --BOUNDARY_STRING-- -# -# First line indicates, that this is a multipart content, second shows that -# here starts a part of the multipart content, third shows the end of content. -# -# If there are any other lines, which starts with "--", then it should be -# another boundary id - or not. -# -# After 3.0.3, there are two kinds of types of boundary errors: strict and permissive. -# -# If multipart content contains the three necessary lines with correct order, but -# there are one or more lines with "--", then parser returns with value 2 (non-zero). -# -# If some of the necessary lines (usually the start or end) misses, or the order -# is wrong, then parser returns with value 1 (also a non-zero). -# -# You can choose, which one is what you need. The example below contains the -# 'strict' mode, which means if there are any lines with start of "--", then -# Coraza blocked the content. But the next, commented example contains -# the 'permissive' mode, then you check only if the necessary lines exists in -# correct order. Whit this, you can enable to upload PEM files (eg "----BEGIN.."), -# or other text files, which contains eg. HTTP headers. -# -# The difference is only the operator - in strict mode (first) the content blocked -# in case of any non-zero value. In permissive mode (second, commented) the -# content blocked only if the value is explicit 1. If it 0 or 2, the content will -# allowed. -# - -# -# See #1747 and #1924 for further information on the possible values for -# MULTIPART_UNMATCHED_BOUNDARY. -# -SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \ - "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" - -# Some internal errors will set flags in TX and we will need to look for these. -# All of these are prefixed with "MSC_". The following flags currently exist: -# -# COR_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded. -# -SecRule TX:/^COR_/ "!@streq 0" \ - "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'" - + "id:'200003',phase:2,t:none,log,deny,status:400, \ + msg:'Multipart request body failed strict validation." # -- Response body handling -------------------------------------------------- @@ -229,17 +161,11 @@ SecAuditLogParts ABIJDEFHZ # SecAuditLogType Serial +# The following settings are not supported by Coraza -# -- Miscellaneous ----------------------------------------------------------- - -# Use the most commonly used application/x-www-form-urlencoded parameter -# separator. There's probably only one application somewhere that uses -# something else so don't expect to change this value. -# -SecArgumentSeparator & - -# Settle on version 0 (zero) cookies, as that is what most applications -# use. Using an incorrect cookie version may open your installation to -# evasion attacks (against the rules that examine named cookies). -# -SecCookieFormat 0 +# SecCookieFormat 0 +# SecArgumentSeparator & +# SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \ +# "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" +# SecRule TX:/^COR_/ "!@streq 0" \ +# "id:'200005',phase:2,t:none,deny,msg:'Coraza internal error flagged: %{MATCHED_VAR_NAME}'" diff --git a/examples/http-server/go.mod b/examples/http-server/go.mod index 15a0d65b5..b7369f742 100644 --- a/examples/http-server/go.mod +++ b/examples/http-server/go.mod @@ -1,17 +1,17 @@ -module github.com/corazawaf/coraza/v3/examples/http-server +module github.com/crowdsecurity/coraza/v3/examples/http-server -go 1.19 +go 1.21 -require github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7 +require github.com/crowdsecurity/coraza/v3 v3.0.0-20240108124027-a62b8d8e5607 require ( - github.com/corazawaf/libinjection-go v0.1.2 // indirect + github.com/corazawaf/libinjection-go v0.2.1 // indirect github.com/magefile/mage v1.15.0 // indirect - github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect + github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect rsc.io/binaryregexp v0.2.0 // indirect ) diff --git a/examples/http-server/go.sum b/examples/http-server/go.sum index 12371cc0b..46a14816e 100644 --- a/examples/http-server/go.sum +++ b/examples/http-server/go.sum @@ -1,26 +1,31 @@ -github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM= -github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= -github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7 h1:3rnfZlcD6p5c1Xyc7yvOrqrX+xz4NFbNHftEra4msqg= -github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI= +github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y= +github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= +github.com/crowdsecurity/coraza/v3 v3.0.0-20240108124027-a62b8d8e5607 h1:hyrYw3h8clMcRL2u5ooZ3tmwnmJftmhb9Ws1MKmavvI= +github.com/crowdsecurity/coraza/v3 v3.0.0-20240108124027-a62b8d8e5607/go.mod h1:br36fEqurGYZQGit+iDYsIzW0FF6VufMbDzyyLxEuPA= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= +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/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:POJco99aNgosh92lGqmx7L1ei+kCymivB/419SD15PQ= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= +github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94= +github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go index 9440bdeb9..8dc3746cb 100644 --- a/examples/http-server/main_test.go +++ b/examples/http-server/main_test.go @@ -113,3 +113,45 @@ func TestHttpServer(t *testing.T) { }) } } + +// TestHttpServerConcurrent is meant to be run with the "-race" flag. +// Multiple requests are sent concurrently to the server and race conditions are checked. +// It is especially useful to ensure that rules and their metadata are not edited in an unsafe way +// after parsing time. +func TestHttpServerConcurrent(t *testing.T) { + tests := []struct { + name string + path string + expStatus int + body []byte // if body is populated, POST request is sent + }{ + {"negative", "/", 200, nil}, + {"positive for query parameter 1", "/?id=0", 403, nil}, + {"positive for request body", "/", 403, []byte("password")}, + } + // Spin up the test server with default.conf configuration + testServer := setupTestServer(t) + defer testServer.Close() + // a t.Run wraps all the concurrent tests and permits to close the server only once test is done + // See https://github.com/golang/go/issues/17791 + t.Run("concurrent test", func(t *testing.T) { + for _, tc := range tests { + tt := tc + for i := 0; i < 10; i++ { + // Each test case is added 10 times and then run concurrently + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var statusCode int + if tt.body == nil { + statusCode = doGetRequest(t, testServer.URL+tt.path) + } else { + statusCode = doPostRequest(t, testServer.URL+tt.path, tt.body) + } + if want, have := tt.expStatus, statusCode; want != have { + t.Errorf("Unexpected status code, want: %d, have: %d", want, have) + } + }) + } + } + }) +} diff --git a/experimental/plugins/auditlog_formatter_tinygo_test.go b/experimental/plugins/auditlog_formatter_tinygo_test.go new file mode 100644 index 000000000..d5a4f1396 --- /dev/null +++ b/experimental/plugins/auditlog_formatter_tinygo_test.go @@ -0,0 +1,80 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build tinygo +// +build tinygo + +// Aimed to tinygo, initializing a dedicated serial writer +package plugins_test + +import ( + "fmt" + "io" + + "github.com/crowdsecurity/coraza/v3" + "github.com/crowdsecurity/coraza/v3/experimental/plugins" + "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" +) + +type testFormatter struct{} + +func (testFormatter) Format(al plugintypes.AuditLog) ([]byte, error) { + return []byte(al.Transaction().ID()), nil +} + +func (testFormatter) MIME() string { + return "sample" +} + +// ExampleRegisterAuditLogFormatter shows how to register a custom audit log formatter +// and tests the output of the formatter. +func ExampleRegisterAuditLogFormatter() { + + plugins.RegisterAuditLogWriter("serial", func() plugintypes.AuditLogWriter { + return &serial{} + }) + + plugins.RegisterAuditLogFormatter("txid", &testFormatter{}) + + w, err := coraza.NewWAF( + coraza.NewWAFConfig(). + WithDirectives(` + SecAuditEngine On + SecAuditLogParts ABCFHZ + SecAuditLog /dev/stdout + SecAuditLogType Serial + SecAuditLogFormat txid + `), + ) + if err != nil { + panic(err) + } + + tx := w.NewTransactionWithID("abc123") + tx.ProcessLogging() + tx.Close() + + // Output: abc123 +} + +// serial emulates a custom audit log writer that writes to the log in wasm overriding the default serial writer. +type serial struct { + io.Closer + formatter plugintypes.AuditLogFormatter +} + +func (s *serial) Init(cfg plugintypes.AuditLogConfig) error { + s.formatter = cfg.Formatter + return nil +} + +func (s *serial) Write(al plugintypes.AuditLog) error { + bts, err := s.formatter.Format(al) + if err != nil { + return err + } + fmt.Print(string(bts)) + return nil +} + +func (s *serial) Close() error { return nil } diff --git a/experimental/plugins/plugintypes/transaction.go b/experimental/plugins/plugintypes/transaction.go index a9ebc39b9..d8084eeff 100644 --- a/experimental/plugins/plugintypes/transaction.go +++ b/experimental/plugins/plugintypes/transaction.go @@ -115,4 +115,5 @@ type TransactionVariables interface { ArgsNames() collection.Collection ArgsGetNames() collection.Collection ArgsPostNames() collection.Collection + MultipartStrictError() collection.Single } diff --git a/experimental/waf.go b/experimental/waf.go new file mode 100644 index 000000000..2cebd50d7 --- /dev/null +++ b/experimental/waf.go @@ -0,0 +1,17 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package experimental + +import ( + "github.com/crowdsecurity/coraza/v3/internal/corazawaf" + "github.com/crowdsecurity/coraza/v3/types" +) + +type Options = corazawaf.Options + +// WAFWithOptions is an interface that allows to create transactions +// with options +type WAFWithOptions interface { + NewTransactionWithOptions(Options) types.Transaction +} diff --git a/experimental/waf_test.go b/experimental/waf_test.go new file mode 100644 index 000000000..5f2b600f2 --- /dev/null +++ b/experimental/waf_test.go @@ -0,0 +1,32 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package experimental_test + +import ( + "fmt" + + "github.com/crowdsecurity/coraza/v3" + "github.com/crowdsecurity/coraza/v3/experimental" +) + +func ExampleWAFWithOptions_NewTransactionWithOptions() { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + panic(err) + } + + oWAF, ok := waf.(experimental.WAFWithOptions) + if !ok { + panic("WAF does not implement WAFWithOptions") + } + + tx := oWAF.NewTransactionWithOptions(experimental.Options{ + ID: "abc123", + }) + + fmt.Println("Transaction ID:", tx.ID()) + + // Output: + // Transaction ID: abc123 +} diff --git a/go.mod b/go.mod index 188a209b4..85896cec9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/crowdsecurity/coraza/v3 -go 1.19 +go 1.21 // Testing dependencies: // - go-mockdns @@ -17,22 +17,22 @@ go 1.19 require ( github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df - github.com/corazawaf/libinjection-go v0.1.2 - github.com/foxcpp/go-mockdns v1.0.0 + github.com/corazawaf/libinjection-go v0.2.1 + github.com/foxcpp/go-mockdns v1.1.0 github.com/magefile/mage v1.15.0 - github.com/mccutchen/go-httpbin/v2 v2.13.1 - github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e - github.com/tidwall/gjson v1.17.0 - golang.org/x/net v0.19.0 - golang.org/x/sync v0.5.0 + github.com/mccutchen/go-httpbin/v2 v2.14.0 + github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 + github.com/tidwall/gjson v1.17.1 + golang.org/x/net v0.28.0 + golang.org/x/sync v0.8.0 rsc.io/binaryregexp v0.2.0 ) require ( - github.com/miekg/dns v1.1.50 // indirect + github.com/miekg/dns v1.1.57 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect ) diff --git a/go.sum b/go.sum index a66a41595..4fb4851f9 100644 --- a/go.sum +++ b/go.sum @@ -1,67 +1,94 @@ github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df h1:YWiVl53v0R8Knj/k+4slO0SXPL67Y4dXWiOIWNzrkew= github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df/go.mod h1:7jguE759ADzy2EkxGRXigiC0ER1Yq2IFk2qNtwgzc7U= -github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM= -github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= -github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= -github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y= +github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= 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.13.1 h1:mDTz2RTD3tugs1BKZM7o6YJsXODYWNvjKZko30B/aWk= -github.com/mccutchen/go-httpbin/v2 v2.13.1/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= -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= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:POJco99aNgosh92lGqmx7L1ei+kCymivB/419SD15PQ= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/mccutchen/go-httpbin/v2 v2.14.0 h1:9N7GUf8+JunYMFd+yHPIVYApC6KYgqtF0pHIcTGYcVQ= +github.com/mccutchen/go-httpbin/v2 v2.14.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/go.work b/go.work index a897d181d..b3d2a35e5 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.19 +go 1.21 use ( . diff --git a/http/e2e/cmd/httpe2e/main.go b/http/e2e/cmd/httpe2e/main.go index 1ad77d43f..0cc360e62 100644 --- a/http/e2e/cmd/httpe2e/main.go +++ b/http/e2e/cmd/httpe2e/main.go @@ -19,24 +19,7 @@ import ( // --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" -*/ +// A dedicated set of directives is expected to be loaded for e2e testing. Refer to the `Directives` const in http/e2e/e2e.go. func main() { // Initialize variables diff --git a/http/e2e/e2e.go b/http/e2e/e2e.go index d4882b63c..8f9b35fb7 100644 --- a/http/e2e/e2e.go +++ b/http/e2e/e2e.go @@ -18,6 +18,24 @@ import ( const ( configCheckStatusCode = 424 healthCheckTimeout = 15 // Seconds + + Directives = ` +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" +` ) type Config struct { @@ -236,18 +254,30 @@ func Run(cfg Config) error { resp, err := client.Do(req) fmt.Printf("[Wait] Waiting for %s. Timeout: %ds\n", healthCheck.url, timeout) if err == nil { + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + return err + } 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) } + + fmt.Printf("[Wait] Unexpected status code %d\n", resp.StatusCode) } 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) + if err != nil { + return fmt.Errorf("timeout waiting for response from %s, make sure the server is running. Last request error: %v", healthCheck.url, err) + } + + return fmt.Errorf("timeout waiting for response from %s, unexpected status code", healthCheck.url) } } } diff --git a/http/interceptor.go b/http/interceptor.go index d463b1876..6cc6f71fe 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.cleanHeaders() i.Header().Set("Content-Length", "0") i.statusCode = obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode) i.flushWriteHeader() @@ -65,6 +66,13 @@ func (i *rwInterceptor) flushWriteHeader() { } } +// cleanHeaders removes all headers from the response +func (i *rwInterceptor) cleanHeaders() { + for k := range i.w.Header() { + i.w.Header().Del(k) + } +} + // Write buffers the response body until the request body limit is reach or an // interruption is triggered, this buffer is later used to analyse the body in // the response processor. @@ -88,7 +96,10 @@ func (i *rwInterceptor) Write(b []byte) (int, error) { // to it, otherwise we just send it to the response writer. it, n, err := i.tx.WriteResponseBody(b) if it != nil { - i.overrideWriteHeader(it.Status) + // if there is an interruption we must clean the headers and override the status code + i.cleanHeaders() + i.Header().Set("Content-Length", "0") + i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)) // We only flush the status code after an interruption. i.flushWriteHeader() return 0, nil @@ -153,6 +164,8 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) ( i.flushWriteHeader() return err } else if it != nil { + // if there is an interruption we must clean the headers and override the status code + i.cleanHeaders() i.Header().Set("Content-Length", "0") i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode)) i.flushWriteHeader() diff --git a/http/middleware.go b/http/middleware.go index f56544e5a..5a6ce3a93 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/crowdsecurity/coraza/v3" + "github.com/crowdsecurity/coraza/v3/experimental" "github.com/crowdsecurity/coraza/v3/types" ) @@ -117,8 +118,20 @@ func WrapHandler(waf coraza.WAF, h http.Handler) http.Handler { return h } + newTX := func(*http.Request) types.Transaction { + return waf.NewTransaction() + } + + if ctxwaf, ok := waf.(experimental.WAFWithOptions); ok { + newTX = func(r *http.Request) types.Transaction { + return ctxwaf.NewTransactionWithOptions(experimental.Options{ + Context: r.Context(), + }) + } + } + fn := func(w http.ResponseWriter, r *http.Request) { - tx := waf.NewTransaction() + tx := newTX(r) defer func() { // We run phase 5 rules and create audit logs (if enabled) tx.ProcessLogging() @@ -172,6 +185,5 @@ func obtainStatusCodeFromInterruptionOrDefault(it *types.Interruption, defaultSt return statusCode } - return defaultStatusCode } diff --git a/http/middleware_test.go b/http/middleware_test.go index 379e3fce7..7bc68a7e8 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -238,42 +238,53 @@ type httpTest struct { respBody string expectedProto string expectedStatus int + expectedRespHeadersKeys []string expectedRespBody string } +var expectedNoBlockingHeaders = []string{"Content-Type", "Content-Length", "Coraza-Middleware", "Date"} + +// When an interruption occour, we are expecting that no response headers are sent back to the client. +var expectedBlockingHeaders = []string{"Content-Length", "Date"} + func TestHttpServer(t *testing.T) { tests := map[string]httpTest{ "no blocking": { - reqURI: "/hello", - expectedProto: "HTTP/1.1", - expectedStatus: 201, + reqURI: "/hello", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "no blocking HTTP/2": { - http2: true, - reqURI: "/hello", - expectedProto: "HTTP/2.0", - expectedStatus: 201, + http2: true, + reqURI: "/hello", + expectedProto: "HTTP/2.0", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "args blocking": { - reqURI: "/hello?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/hello?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "request body blocking": { - reqURI: "/hello", - reqBody: "eval('cat /etc/passwd')", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/hello", + reqBody: "eval('cat /etc/passwd')", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "request body larger than limit (process partial)": { reqURI: "/hello", reqBody: "eval('cat /etc/passwd')", echoReqBody: true, // Coraza only sees eva, not eval - reqBodyLimit: 3, - expectedProto: "HTTP/1.1", - expectedStatus: 201, - expectedRespBody: "eval('cat /etc/passwd')", + reqBodyLimit: 3, + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "eval('cat /etc/passwd')", }, "request body larger than limit (reject)": { reqURI: "/hello", @@ -283,37 +294,43 @@ func TestHttpServer(t *testing.T) { shouldRejectOnBodyLimit: true, expectedProto: "HTTP/1.1", expectedStatus: 413, + expectedRespHeadersKeys: expectedBlockingHeaders, expectedRespBody: "", }, "response headers blocking": { - reqURI: "/hello", - respHeaders: map[string]string{"foo": "bar"}, - expectedProto: "HTTP/1.1", - expectedStatus: 401, + reqURI: "/hello", + respHeaders: map[string]string{"foo": "bar"}, + expectedProto: "HTTP/1.1", + expectedStatus: 401, + expectedRespHeadersKeys: expectedBlockingHeaders, }, "response body not blocking": { - reqURI: "/hello", - respBody: "true negative response body", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - expectedRespBody: "true negative response body", + reqURI: "/hello", + respBody: "true negative response body", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "true negative response body", }, "response body blocking": { - reqURI: "/hello", - respBody: "password=xxxx", - expectedProto: "HTTP/1.1", - expectedStatus: 403, - expectedRespBody: "", // blocking at response body phase means returning it empty + reqURI: "/hello", + respBody: "password=xxxx", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespBody: "", // blocking at response body phase means returning it empty + expectedRespHeadersKeys: expectedBlockingHeaders, }, "allow": { - reqURI: "/allow_me", - expectedProto: "HTTP/1.1", - expectedStatus: 201, + reqURI: "/allow_me", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + expectedRespHeadersKeys: expectedNoBlockingHeaders, }, "deny passes over allow due to ordering": { - reqURI: "/allow_me?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 403, + reqURI: "/allow_me?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 403, + expectedRespHeadersKeys: expectedBlockingHeaders, }, } @@ -357,26 +374,29 @@ func TestHttpServer(t *testing.T) { func TestHttpServerWithRuleEngineOff(t *testing.T) { tests := map[string]httpTest{ "no blocking true negative": { - reqURI: "/hello", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Hello!", - expectedRespBody: "Hello!", + reqURI: "/hello", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Hello!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Hello!", }, "no blocking true positive header phase": { - reqURI: "/hello?id=0", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Downstream works!", - expectedRespBody: "Downstream works!", + reqURI: "/hello?id=0", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Downstream works!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Downstream works!", }, "no blocking true positive body phase": { - reqURI: "/hello", - reqBody: "eval('cat /etc/passwd')", - expectedProto: "HTTP/1.1", - expectedStatus: 201, - respBody: "Waf is Off!", - expectedRespBody: "Waf is Off!", + reqURI: "/hello", + reqBody: "eval('cat /etc/passwd')", + expectedProto: "HTTP/1.1", + expectedStatus: 201, + respBody: "Waf is Off!", + expectedRespHeadersKeys: expectedNoBlockingHeaders, + expectedRespBody: "Waf is Off!", }, } logger := debuglog.Default(). @@ -458,6 +478,10 @@ func runAgainstWAF(t *testing.T, tCase httpTest, waf coraza.WAF) { t.Errorf("unexpected status code, want: %d, have: %d", want, have) } + if !keysExistInMap(t, tCase.expectedRespHeadersKeys, res.Header) { + t.Errorf("unexpected response headers, expected keys: %v, headers: %v", tCase.expectedRespHeadersKeys, res.Header) + } + resBody, err := io.ReadAll(res.Body) if err != nil { t.Fatalf("unexpected error when reading the response body: %v", err) @@ -480,6 +504,19 @@ func runAgainstWAF(t *testing.T, tCase httpTest, waf coraza.WAF) { } } +func keysExistInMap(t *testing.T, keys []string, m map[string][]string) bool { + t.Helper() + if len(keys) != len(m) { + return false + } + for _, key := range keys { + if _, ok := m[key]; !ok { + return false + } + } + return true +} + func TestObtainStatusCodeFromInterruptionOrDefault(t *testing.T) { tCases := map[string]struct { interruptionCode int diff --git a/internal/actions/ctl_test.go b/internal/actions/ctl_test.go index 72fc3feb1..9ec5794b7 100644 --- a/internal/actions/ctl_test.go +++ b/internal/actions/ctl_test.go @@ -335,6 +335,8 @@ func TestCtl(t *testing.T) { waf := corazawaf.NewWAF() waf.Logger = logger r := corazawaf.NewRule() + r.ID_ = 1 + r.LogID_ = "1" err := waf.Rules.Add(r) if err != nil { t.Fatalf("failed to add rule: %s", err.Error()) diff --git a/internal/actions/deny.go b/internal/actions/deny.go index a474ce66a..045f706f1 100644 --- a/internal/actions/deny.go +++ b/internal/actions/deny.go @@ -4,6 +4,8 @@ package actions import ( + "net/http" + "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" "github.com/crowdsecurity/coraza/v3/types" ) @@ -12,6 +14,7 @@ import ( // // Description: // Stops rule processing and intercepts transaction. +// If status action is not used, deny action defaults to status 403. // // Example: // ``` @@ -27,14 +30,20 @@ func (a *denyFn) Init(_ plugintypes.RuleMetadata, data string) error { } const noID = 0 +const noStatus = 0 func (a *denyFn) Evaluate(r plugintypes.RuleMetadata, tx plugintypes.TransactionState) { rid := r.ID() if rid == noID { rid = r.ParentID() } + status := r.Status() + // deny action defaults to status 403 + if status == noStatus { + status = http.StatusForbidden + } tx.Interrupt(&types.Interruption{ - Status: r.Status(), + Status: status, RuleID: rid, Action: "deny", Tags: r.Tags(), diff --git a/internal/actions/id.go b/internal/actions/id.go index 1baec7438..43e0f0043 100644 --- a/internal/actions/id.go +++ b/internal/actions/id.go @@ -38,6 +38,7 @@ func (a *idFn) Init(r plugintypes.RuleMetadata, data string) error { cr := r.(*corazawaf.Rule) cr.ID_ = int(i) + cr.LogID_ = strconv.Itoa(i) return nil } diff --git a/internal/actions/status.go b/internal/actions/status.go index d0fe7f483..1bde5a221 100644 --- a/internal/actions/status.go +++ b/internal/actions/status.go @@ -15,6 +15,7 @@ import ( // // Description: // Specifies the response status code to use with actions deny and redirect. +// If status is not set, deny action defaults to status 403. // // Example: // ``` diff --git a/internal/auditlog/concurrent_writer_test.go b/internal/auditlog/concurrent_writer_test.go index 1efa5e7fb..cdadcd7df 100644 --- a/internal/auditlog/concurrent_writer_test.go +++ b/internal/auditlog/concurrent_writer_test.go @@ -12,7 +12,6 @@ import ( "fmt" "io/fs" "os" - "path" "path/filepath" "reflect" "testing" @@ -104,11 +103,15 @@ func TestConcurrentWriterSuccess(t *testing.T) { DirMode: fs.FileMode(0777), Formatter: &jsonFormatter{}, } + if err := file.Close(); err != nil { + t.Error(err) + } writer := &concurrentWriter{} if err := writer.Init(config); err != nil { t.Error("failed to init concurrent logger", err) } + defer writer.Close() ts := time.Now() expectedLog := &Log{ @@ -130,7 +133,7 @@ func TestConcurrentWriterSuccess(t *testing.T) { } fileName := fmt.Sprintf("/%s-%s", ts.Format("20060102-150405"), expectedLog.Transaction().ID()) - logFile := path.Join(dir, ts.Format("20060102"), ts.Format("20060102-1504"), fileName) + logFile := filepath.Join(dir, ts.Format("20060102"), ts.Format("20060102-1504"), fileName) logData, err := os.ReadFile(logFile) if err != nil { diff --git a/internal/auditlog/formats_json.go b/internal/auditlog/formats_json.go index 715dd637d..11ef9fada 100644 --- a/internal/auditlog/formats_json.go +++ b/internal/auditlog/formats_json.go @@ -1,10 +1,6 @@ // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 -// JSON auditlog not supported on TinyGo yet. -//go:build !tinygo -// +build !tinygo - package auditlog import ( diff --git a/internal/auditlog/formats_json_test.go b/internal/auditlog/formats_json_test.go index 7e0ca684f..cc17740df 100644 --- a/internal/auditlog/formats_json_test.go +++ b/internal/auditlog/formats_json_test.go @@ -1,9 +1,6 @@ // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 -//go:build !tinygo -// +build !tinygo - package auditlog import ( diff --git a/internal/auditlog/init_tinygo.go b/internal/auditlog/init_tinygo.go index 7c7a2746e..009451a72 100644 --- a/internal/auditlog/init_tinygo.go +++ b/internal/auditlog/init_tinygo.go @@ -13,14 +13,13 @@ func init() { return noopWriter{} }) RegisterWriter("serial", func() plugintypes.AuditLogWriter { - return noopWriter{} + return &serialWriter{} }) RegisterWriter("https", func() plugintypes.AuditLogWriter { return noopWriter{} }) - // TODO(jcchavezs): check if newest TinyGo supports json.Marshaler for audit log type. - RegisterFormatter("json", &noopFormatter{}) - RegisterFormatter("jsonlegacy", &noopFormatter{}) - RegisterFormatter("native", &noopFormatter{}) + RegisterFormatter("json", &jsonFormatter{}) + RegisterFormatter("jsonlegacy", &legacyJSONFormatter{}) + RegisterFormatter("native", &nativeFormatter{}) } diff --git a/internal/auditlog/logger.go b/internal/auditlog/logger.go index 113952331..7e38c3eff 100644 --- a/internal/auditlog/logger.go +++ b/internal/auditlog/logger.go @@ -27,7 +27,7 @@ var formatters = map[string]plugintypes.AuditLogFormatter{} // RegisterWriter registers a new logger // it can be used for plugins func RegisterWriter(name string, writer func() plugintypes.AuditLogWriter) { - writers[name] = writer + writers[strings.ToLower(name)] = writer } // GetWriter returns a logger by name @@ -43,7 +43,7 @@ func GetWriter(name string) (plugintypes.AuditLogWriter, error) { // RegisterFormatter registers a new logger format // it can be used for plugins func RegisterFormatter(name string, f plugintypes.AuditLogFormatter) { - formatters[name] = f + formatters[strings.ToLower(name)] = f } // GetFormatter returns a formatter by name diff --git a/internal/auditlog/logger_test.go b/internal/auditlog/logger_test.go index ac1826e1b..e2649f7e2 100644 --- a/internal/auditlog/logger_test.go +++ b/internal/auditlog/logger_test.go @@ -55,3 +55,62 @@ func TestGetFormatters(t *testing.T) { } }) } + +type noopWriter struct{} + +func (noopWriter) Init(plugintypes.AuditLogConfig) error { return nil } +func (noopWriter) Write(plugintypes.AuditLog) error { return nil } +func (noopWriter) Close() error { return nil } + +func TestRegisterAndGetWriter(t *testing.T) { + + testCases := []struct { + name string + }{ + {"customwriter"}, + {"CustomWriter"}, + {"CUSTOMWRITER"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + RegisterWriter(tc.name, func() plugintypes.AuditLogWriter { + return noopWriter{} + }) + + writer, err := GetWriter(tc.name) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if writer == nil { + t.Fatalf("expected a writer, got nil") + } + }) + } +} + +func TestRegisterAndGetFormatter(t *testing.T) { + + testCases := []struct { + name string + }{ + {"customFormatter"}, + {"customformatter"}, + {"CUSTOMFORMATTER"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + RegisterFormatter(tc.name, &noopFormatter{}) + retrievedFormatter, err := GetFormatter(tc.name) + if err != nil { + t.Fatalf("expected no error, got: %v", err) + } + + if retrievedFormatter == nil { + t.Fatalf("expected a formatter, got nil") + } + }) + } +} diff --git a/internal/auditlog/noop_writer.go b/internal/auditlog/noop_writer.go index 25f441fad..a148f5834 100644 --- a/internal/auditlog/noop_writer.go +++ b/internal/auditlog/noop_writer.go @@ -9,7 +9,7 @@ package auditlog import "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" -// noopWriter is used to store logs in a single file +// noopWriter is used as a no operations audit log writer. type noopWriter struct{} func (noopWriter) Init(plugintypes.AuditLogConfig) error { return nil } diff --git a/internal/bodyprocessors/multipart.go b/internal/bodyprocessors/multipart.go index 63936289d..e7cd67f0e 100644 --- a/internal/bodyprocessors/multipart.go +++ b/internal/bodyprocessors/multipart.go @@ -24,6 +24,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype storagePath := options.StoragePath mediaType, params, err := mime.ParseMediaType(mimeType) if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } if !strings.HasPrefix(mediaType, "multipart/") { @@ -44,6 +45,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype break } if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } partName := p.FormName() @@ -60,10 +62,13 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype // Only copy file to temp when not running in TinyGo temp, err := os.CreateTemp(storagePath, "crzmp*") if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } + defer temp.Close() sz, err := io.Copy(temp, p) if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } size = sz @@ -71,6 +76,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype } else { sz, err := io.Copy(io.Discard, p) if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } size = sz @@ -83,6 +89,7 @@ func (mbp *multipartBodyProcessor) ProcessRequest(reader io.Reader, v plugintype // if is a field data, err := io.ReadAll(p) if err != nil { + v.MultipartStrictError().(*collections.Single).Set("1") return err } totalSize += int64(len(data)) diff --git a/internal/bodyprocessors/multipart_test.go b/internal/bodyprocessors/multipart_test.go index 74fafa3d7..468881c1f 100644 --- a/internal/bodyprocessors/multipart_test.go +++ b/internal/bodyprocessors/multipart_test.go @@ -94,3 +94,119 @@ text default t.Error("multipart processor should fail for invalid content-type") } } + +func TestMultipartErrorSetsMultipartStrictError(t *testing.T) { + payload := "--a\n" + + "\x0eContent-Disposition\x0e: form-data; name=\"file\";filename=\"1.jsp\"\n" + + "Content-Disposition: form-data; name=\"post\";\n" + + "\n" + + "<%out.print(123)%>\n" + + "--a--" + mp := multipartProcessor(t) + v := corazawaf.NewTransactionVariables() + strictError := v.MultipartStrictError() + if strictError.Get() != "" { + t.Errorf("expected strict error to be empty") + } + if err := mp.ProcessRequest(strings.NewReader(payload), v, plugintypes.BodyProcessorOptions{ + Mime: "multipart/form-data; boundary=a", + }); err != nil { + strictError = v.MultipartStrictError() + if strictError.Get() != "1" { + t.Error("expected strict error") + } + } +} + +// TestMultipartCRLFAndLF tests a multipart payload with mixed CRLF and LF line endings. +// Golang mime/multipart reader uses the first line ending after the boundary and wants to keep it consistent. +// It will fail with NextPart: EOF if the line endings are mixed. +func TestMultipartCRLFAndLF(t *testing.T) { + payload := "----------------------------756b6d74fa1a8ee2" + + "Content-Disposition: form-data; name=\"name\"" + + "" + + "test" + + "----------------------------756b6d74fa1a8ee2" + + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"" + + "Content-Type: text/plain" + + "" + + "This is a very small test file.." + + "----------------------------756b6d74fa1a8ee2" + + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"\r" + + "Content-Type: text/plain\r" + + "\r" + + "This is another very small test file..\r" + + "----------------------------756b6d74fa1a8ee2--\r" + + mp := multipartProcessor(t) + v := corazawaf.NewTransactionVariables() + if err := mp.ProcessRequest(strings.NewReader(payload), v, plugintypes.BodyProcessorOptions{ + Mime: "multipart/form-data; boundary=756b6d74fa1a8ee2", + }); err != nil { + strictError := v.MultipartStrictError() + if strictError.Get() != "1" { + t.Error("expected strict error") + } + if !strings.Contains(err.Error(), "multipart: NextPart: EOF") { + t.Fatal(err) + } + } +} + +// TestMultipartInvalidHeaderFolding tests a multipart payload where headers are folded badly (RFC 2047). +// It will fail with NextPart: EOF. +func TestMultipartInvalidHeaderFolding(t *testing.T) { + payload := "-------------------------------69343412719991675451336310646\n" + + "Content-Disposition: form-data;\n" + + " name=\"a\"\n" + + "\n" + + "\n" + + "-------------------------------69343412719991675451336310646\n" + + "Content-Disposition: form-data;\n" + + " name=\"b\"\n" + + "\n" + + "2\n" + + "-------------------------------69343412719991675451336310646--\n" + mp := multipartProcessor(t) + v := corazawaf.NewTransactionVariables() + if err := mp.ProcessRequest(strings.NewReader(payload), v, plugintypes.BodyProcessorOptions{ + Mime: "multipart/form-data; boundary=69343412719991675451336310646", + }); err != nil { + strictError := v.MultipartStrictError() + if strictError.Get() != "1" { + t.Error("expected strict error") + } + if !strings.Contains(err.Error(), "multipart: NextPart: EOF") { + t.Fatal(err) + } + } +} + +// TestMultipartUnmatchedBoundary tests a multipart payload where there is an unmatched boundary. +func TestMultipartUnmatchedBoundary(t *testing.T) { + payload := "--------------------------756b6d74fa1a8ee2\n" + + "Content-Disposition: form-data; name=\"name\"\n" + + "\n" + + "test\n" + + "--------------------------756b6d74fa1a8ee2\n" + + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"\n" + + "Content-Type: text/plain\n" + + "\n" + + "This is a very small test file..\n" + + "--------------------------756b6d74fa1a8ee2\n" + + "Content-Disposition: form-data; name=\"filedata\"; filename=\"small_text_file.txt\"\n" + + "Content-Type: text/plain\n" + + "\n" + + "This is another very small test file..\n" + + "\n" + mp := multipartProcessor(t) + v := corazawaf.NewTransactionVariables() + if err := mp.ProcessRequest(strings.NewReader(payload), v, plugintypes.BodyProcessorOptions{ + Mime: "multipart/form-data; boundary=756b6d74fa1a8ee2", + }); err != nil { + strictError := v.MultipartStrictError() + if strictError.Get() != "1" { + t.Error("expected strict error") + } + } +} diff --git a/internal/bodyprocessors/raw.go b/internal/bodyprocessors/raw.go new file mode 100644 index 000000000..ebab9c0e1 --- /dev/null +++ b/internal/bodyprocessors/raw.go @@ -0,0 +1,43 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package bodyprocessors + +import ( + "io" + "strconv" + "strings" + + "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" + "github.com/crowdsecurity/coraza/v3/internal/collections" +) + +type rawBodyProcessor struct { +} + +func (*rawBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.TransactionVariables, _ plugintypes.BodyProcessorOptions) error { + var buf strings.Builder + if _, err := io.Copy(&buf, reader); err != nil { + return err + } + + b := buf.String() + + v.RequestBody().(*collections.Single).Set(b) + v.RequestBodyLength().(*collections.Single).Set(strconv.Itoa(len(b))) + return nil +} + +func (*rawBodyProcessor) ProcessResponse(io.Reader, plugintypes.TransactionVariables, plugintypes.BodyProcessorOptions) error { + return nil +} + +var ( + _ plugintypes.BodyProcessor = &rawBodyProcessor{} +) + +func init() { + RegisterBodyProcessor("raw", func() plugintypes.BodyProcessor { + return &rawBodyProcessor{} + }) +} diff --git a/internal/bodyprocessors/raw_test.go b/internal/bodyprocessors/raw_test.go new file mode 100644 index 000000000..3a03e878f --- /dev/null +++ b/internal/bodyprocessors/raw_test.go @@ -0,0 +1,34 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package bodyprocessors_test + +import ( + "strconv" + "strings" + "testing" + + "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" + "github.com/crowdsecurity/coraza/v3/internal/bodyprocessors" + "github.com/crowdsecurity/coraza/v3/internal/corazawaf" +) + +func TestRAW(t *testing.T) { + bp, err := bodyprocessors.GetBodyProcessor("raw") + if err != nil { + t.Fatal(err) + } + v := corazawaf.NewTransactionVariables() + + body := `this is a body +without &any=meaning` + if err := bp.ProcessRequest(strings.NewReader(body), v, plugintypes.BodyProcessorOptions{}); err != nil { + t.Error(err) + } + if v.RequestBody().Get() != body { + t.Errorf("Expected %s, got %s", body, v.RequestBody().Get()) + } + if rbl, _ := strconv.Atoi(v.RequestBodyLength().Get()); rbl != len(body) { + t.Errorf("Expected %d, got %s", len(body), v.RequestBodyLength().Get()) + } +} diff --git a/internal/collections/map.go b/internal/collections/map.go index 3e7579057..9c1a472f5 100644 --- a/internal/collections/map.go +++ b/internal/collections/map.go @@ -15,16 +15,28 @@ import ( // Map is a default collection.Map. type Map struct { - data map[string][]keyValue - variable variables.RuleVariable + isCaseSensitive bool + data map[string][]keyValue + variable variables.RuleVariable } var _ collection.Map = &Map{} +// NewMap creates a new Map. By default, the Map key is case insensitive. func NewMap(variable variables.RuleVariable) *Map { return &Map{ - variable: variable, - data: map[string][]keyValue{}, + isCaseSensitive: false, + variable: variable, + data: map[string][]keyValue{}, + } +} + +// NewCaseSensitiveKeyMap creates a new Map with case sensitive keys. +func NewCaseSensitiveKeyMap(variable variables.RuleVariable) *Map { + return &Map{ + isCaseSensitive: true, + variable: variable, + data: map[string][]keyValue{}, } } @@ -32,14 +44,17 @@ func (c *Map) Get(key string) []string { if len(c.data) == 0 { return nil } - keyL := strings.ToLower(key) + if !c.isCaseSensitive { + key = strings.ToLower(key) + } var values []string - for _, a := range c.data[keyL] { + for _, a := range c.data[key] { values = append(values, a.value) } return values } +// FindRegex returns all map elements whose key matches the regular expression. func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData { var result []types.MatchData for k, data := range c.data { @@ -56,6 +71,7 @@ func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData { return result } +// FindString returns all map elements whose key matches the string. func (c *Map) FindString(key string) []types.MatchData { var result []types.MatchData if key == "" { @@ -64,9 +80,11 @@ func (c *Map) FindString(key string) []types.MatchData { if len(c.data) == 0 { return nil } - keyL := strings.ToLower(key) + if !c.isCaseSensitive { + key = strings.ToLower(key) + } // if key is not empty - if e, ok := c.data[keyL]; ok { + if e, ok := c.data[key]; ok { for _, aVar := range e { result = append(result, &corazarules.MatchData{ Variable_: c.variable, @@ -78,6 +96,7 @@ func (c *Map) FindString(key string) []types.MatchData { return result } +// FindAll returns all map elements. func (c *Map) FindAll() []types.MatchData { var result []types.MatchData for _, data := range c.data { @@ -92,53 +111,70 @@ func (c *Map) FindAll() []types.MatchData { return result } +// Add adds a new key-value pair to the map. func (c *Map) Add(key string, value string) { - keyL := strings.ToLower(key) aVal := keyValue{key: key, value: value} - c.data[keyL] = append(c.data[keyL], aVal) + if !c.isCaseSensitive { + key = strings.ToLower(key) + } + c.data[key] = append(c.data[key], aVal) } +// Set sets the value of a key with the array of strings passed. If the key already exists, it will be overwritten. func (c *Map) Set(key string, values []string) { - keyL := strings.ToLower(key) - c.data[keyL] = make([]keyValue, 0, len(values)) + originalKey := key + if !c.isCaseSensitive { + key = strings.ToLower(key) + } + c.data[key] = make([]keyValue, 0, len(values)) for _, v := range values { - c.data[keyL] = append(c.data[keyL], keyValue{key: key, value: v}) + c.data[key] = append(c.data[key], keyValue{key: originalKey, value: v}) } } +// SetIndex sets the value of a key at the specified index. If the key already exists, it will be overwritten. func (c *Map) SetIndex(key string, index int, value string) { - keyL := strings.ToLower(key) - values := c.data[keyL] - av := keyValue{key: key, value: value} + originalKey := key + if !c.isCaseSensitive { + key = strings.ToLower(key) + } + values := c.data[key] + av := keyValue{key: originalKey, value: value} switch { case len(values) == 0: - c.data[keyL] = []keyValue{av} + c.data[key] = []keyValue{av} case len(values) <= index: - c.data[keyL] = append(c.data[keyL], av) + c.data[key] = append(c.data[key], av) default: - c.data[keyL][index] = av + c.data[key][index] = av } } +// Remove removes a key/value from the map. func (c *Map) Remove(key string) { + if !c.isCaseSensitive { + key = strings.ToLower(key) + } if len(c.data) == 0 { return } - keyL := strings.ToLower(key) - delete(c.data, keyL) + delete(c.data, key) } +// Name returns the name of the map/collection. func (c *Map) Name() string { return c.variable.Name() } +// Reset removes all key/value pairs from the map. func (c *Map) Reset() { for k := range c.data { delete(c.data, k) } } +// Format updates the passed strings.Builder with the formatted map key/values. func (c *Map) Format(res *strings.Builder) { res.WriteString(c.variable.Name()) res.WriteString(":\n") @@ -156,12 +192,14 @@ func (c *Map) Format(res *strings.Builder) { } } +// String returns a string representation of the map key/values. func (c *Map) String() string { res := strings.Builder{} c.Format(&res) return res.String() } +// Len returns the number of key/value pairs in the map. func (c *Map) Len() int { return len(c.data) } diff --git a/internal/collections/map_test.go b/internal/collections/map_test.go index 9ecdf5b99..192411d4d 100644 --- a/internal/collections/map_test.go +++ b/internal/collections/map_test.go @@ -21,8 +21,53 @@ import ( "github.com/crowdsecurity/coraza/v3/types/variables" ) +// Case Insensitive Map +// This is for headers and other collections that are case insensitive func TestMap(t *testing.T) { - c := NewMap(variables.ArgsPost) + c := NewMap(variables.RequestHeaders) + c.SetIndex("user", 1, "value") + c.Set("user-agent", []string{"value2"}) + if c.Get("user")[0] != "value" { + t.Error("Error setting index") + } + if len(c.FindAll()) == 0 { + t.Error("Error finding all") + } + if len(c.FindString("a")) > 0 { + t.Error("Error should not find string") + } + if l := len(c.FindRegex(regexp.MustCompile("user.*"))); l != 2 { + t.Errorf("Error should find regex, got %d", l) + } + + c.Add("user-agent", "value3") + + wantStr := `REQUEST_HEADERS: + user: value + user-agent: value2,value3 +` + + if have := fmt.Sprint(c); have != wantStr { + // Map order is not guaranteed, not pretty but checking twice is the simplest for now. + wantStr = `REQUEST_HEADERS: + user-agent: value2,value3 + user: value +` + if have != wantStr { + t.Errorf("String() = %q, want %q", have, wantStr) + } + } + + if c.Len() != len(c.data) { + t.Fatal("The lengths are not equal.") + } + +} + +// Case Sensitive Map +// This is for ARGS, ARGS_GET, ARGS_POST and other collections that are case sensitive +func TestNewCaseSensitiveKeyMap(t *testing.T) { + c := NewCaseSensitiveKeyMap(variables.ArgsPost) c.SetIndex("key", 1, "value") c.Set("key2", []string{"value2"}) if c.Get("key")[0] != "value" { diff --git a/internal/collections/named.go b/internal/collections/named.go index 035674802..22594e66c 100644 --- a/internal/collections/named.go +++ b/internal/collections/named.go @@ -21,6 +21,12 @@ type NamedCollection struct { var _ collection.Map = &NamedCollection{} +func NewCaseSensitiveNamedCollection(rv variables.RuleVariable) *NamedCollection { + return &NamedCollection{ + Map: NewCaseSensitiveKeyMap(rv), + } +} + func NewNamedCollection(rv variables.RuleVariable) *NamedCollection { return &NamedCollection{ Map: NewMap(rv), diff --git a/internal/cookies/cookies.go b/internal/cookies/cookies.go index 250e45397..4da61f2b4 100644 --- a/internal/cookies/cookies.go +++ b/internal/cookies/cookies.go @@ -1,3 +1,6 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + package cookies import ( diff --git a/internal/cookies/cookies_test.go b/internal/cookies/cookies_test.go index 83ad1466c..d1595a778 100644 --- a/internal/cookies/cookies_test.go +++ b/internal/cookies/cookies_test.go @@ -1,3 +1,6 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + package cookies import ( diff --git a/internal/corazarules/rule.go b/internal/corazarules/rule.go index 38aecebd9..bba4adeaa 100644 --- a/internal/corazarules/rule.go +++ b/internal/corazarules/rule.go @@ -10,7 +10,12 @@ import ( // RuleMetadata is used to store rule metadata // that can be used across packages type RuleMetadata struct { - ID_ int + ID_ int + // Stores the string representation of the rule ID for logging purposes. + // If the rule is part of a chain, the parent ID is used as log ID. + // This approach prevents repeated computations in performance-critical sections, enhancing efficiency. + // It is stored for performance reasons, avoiding to perfrom the computation multiple times in the hot path + LogID_ string File_ string Line_ int Rev_ string @@ -23,6 +28,9 @@ type RuleMetadata struct { Phase_ types.RulePhase Raw_ string SecMark_ string + // Contains the Id of the parent rule if you are inside + // a chain. Otherwise, it will be 0 + ParentID_ int } func (r *RuleMetadata) ID() int { @@ -76,3 +84,7 @@ func (r *RuleMetadata) Raw() string { func (r *RuleMetadata) SecMark() string { return r.SecMark_ } + +func (r *RuleMetadata) LogID() string { + return r.LogID_ +} diff --git a/internal/corazarules/rule_match.go b/internal/corazarules/rule_match.go index 9f2c67cd4..7aa1a99a3 100644 --- a/internal/corazarules/rule_match.go +++ b/internal/corazarules/rule_match.go @@ -4,6 +4,7 @@ package corazarules import ( + "context" "fmt" "strconv" "strings" @@ -30,6 +31,8 @@ type MatchData struct { ChainLevel_ int } +var _ types.MatchData = (*MatchData)(nil) + func (m *MatchData) Variable() variables.RuleVariable { return m.Variable_ } @@ -108,8 +111,12 @@ type MatchedRule struct { MatchedDatas_ []types.MatchData Rule_ types.RuleMetadata + + Context_ context.Context } +var _ types.MatchedRule = (*MatchedRule)(nil) + func (mr *MatchedRule) Message() string { return mr.Message_ } @@ -154,7 +161,36 @@ func (mr *MatchedRule) DisruptiveAction() string { return disruptiveActionMapStr[mr.DisruptiveAction_] } -const maxSizeLogMessage = 200 +const maxSizeLogMessage = 280 + +// Context returns the context associated with the transaction +// This is useful for logging purposes where you want to add +// additional information to the log. +// The context can be easily retrieved in the logger using +// an ancillary interface: +// ``` +// +// type Contexter interface { +// Context() context.Context +// } +// +// ``` +// and then using it like this: +// +// ``` +// +// func errorLogCb(mr types.MatchedRule) { +// ctx := context.Background() +// if ctxer, ok := mr.(Contexter); ok { +// ctx = ctxer.Context() +// } +// logger.Context(ctx).Error().Msg("...") +// } +// +// ``` +func (mr *MatchedRule) Context() context.Context { + return mr.Context_ +} func (mr MatchedRule) writeDetails(log *strings.Builder, matchData types.MatchData) { msg := matchData.Message() diff --git a/internal/corazawaf/casesensitive.go b/internal/corazawaf/casesensitive.go new file mode 100644 index 000000000..fdd68181d --- /dev/null +++ b/internal/corazawaf/casesensitive.go @@ -0,0 +1,8 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build coraza.rule.case_sensitive_args_keys + +package corazawaf + +var shouldUseCaseSensitiveNamedCollection = true diff --git a/internal/corazawaf/casesensitive_default.go b/internal/corazawaf/casesensitive_default.go new file mode 100644 index 000000000..47515ed15 --- /dev/null +++ b/internal/corazawaf/casesensitive_default.go @@ -0,0 +1,8 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build !coraza.rule.case_sensitive_args_keys + +package corazawaf + +var shouldUseCaseSensitiveNamedCollection = false diff --git a/internal/corazawaf/rule.go b/internal/corazawaf/rule.go index 02171d4f2..74dbabeb3 100644 --- a/internal/corazawaf/rule.go +++ b/internal/corazawaf/rule.go @@ -7,11 +7,11 @@ import ( "fmt" "reflect" "regexp" - "strconv" "strings" "sync" "unsafe" + "github.com/crowdsecurity/coraza/v3/debuglog" "github.com/crowdsecurity/coraza/v3/experimental/plugins/macro" "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" "github.com/crowdsecurity/coraza/v3/internal/corazarules" @@ -103,10 +103,6 @@ type Rule struct { // the rule evaluation process actions []ruleActionParams - // Contains the Id of the parent rule if you are inside - // a chain. Otherwise, it will be 0 - ParentID_ int - // Capture is used by the transaction to tell the operator // to capture variables on TX:0-9 Capture bool @@ -173,29 +169,36 @@ const chainLevelZero = 0 func (r *Rule) Evaluate(phase types.RulePhase, tx plugintypes.TransactionState, cache map[transformationKey]*transformationValue) { // collectiveMatchedValues lives across recursive calls of doEvaluate var collectiveMatchedValues []types.MatchData - r.doEvaluate(phase, tx.(*Transaction), &collectiveMatchedValues, chainLevelZero, cache) + + logger := tx.DebugLogger() + + if logger.Debug().IsEnabled() { + if r.ID_ == noID { + logger = logger.With(debuglog.Str("rule_ref", fmt.Sprintf("%s#L%d", r.File_, r.Line_))) + } else { + logger = logger.With(debuglog.Int("rule_id", r.ID_)) + } + } + + r.doEvaluate(logger, phase, tx.(*Transaction), &collectiveMatchedValues, chainLevelZero, cache) } const noID = 0 -func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatchedValues *[]types.MatchData, chainLevel int, cache map[transformationKey]*transformationValue) []types.MatchData { +func (r *Rule) doEvaluate(logger debuglog.Logger, phase types.RulePhase, tx *Transaction, collectiveMatchedValues *[]types.MatchData, chainLevel int, cache map[transformationKey]*transformationValue) []types.MatchData { tx.Capture = r.Capture - rid := r.ID_ - if rid == noID { - rid = r.ParentID_ - } - if multiphaseEvaluation { computeRuleChainMinPhase(r) } var matchedValues []types.MatchData // we log if we are the parent rule - tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Evaluating rule") - defer tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Finish evaluating rule") + logger.Debug().Msg("Evaluating rule") + defer logger.Debug().Msg("Finished rule evaluation") + ruleCol := tx.variables.rule - ruleCol.SetIndex("id", 0, strconv.Itoa(rid)) + ruleCol.SetIndex("id", 0, r.LogID()) if r.Msg != nil { ruleCol.SetIndex("msg", 0, r.Msg.String()) } @@ -206,7 +209,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc ruleCol.SetIndex("severity", 0, r.Severity_.String()) // SecMark and SecAction uses nil operator if r.operator == nil { - tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Forcing rule to match") + logger.Debug().Msg("Forcing rule to match") md := &corazarules.MatchData{} if r.ParentID_ != noID || r.MultiMatch { // In order to support Msg and LogData for inner rules, we need to expand them now @@ -237,26 +240,33 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc } values = tx.GetField(v) - tx.DebugLogger().Debug(). - Int("rule_id", rid). - Str("variable", v.Variable.Name()). - Msg("Expanding arguments for rule") + + vLog := logger + if logger.Debug().IsEnabled() { + vLog = logger.With(debuglog.Str("variable", v.Variable.Name())) + } + vLog.Debug().Msg("Expanding arguments for rule") + for i, arg := range values { - tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Transforming argument for rule") args, errs := r.transformArg(arg, i, cache) if len(errs) > 0 { - log := tx.DebugLogger().Debug().Int("rule_id", rid) - if log.IsEnabled() { - for i, err := range errs { - log = log.Str(fmt.Sprintf("errors[%d]", i), err.Error()) + vWarnLog := vLog.Warn() + if vWarnLog.IsEnabled() { + for _, err := range errs { + vWarnLog = vWarnLog.Err(err) } - log.Msg("Error transforming argument for rule") + vWarnLog.Msg("Error transforming argument for rule") } } - tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Arguments transformed for rule") // args represents the transformed variables for _, carg := range args { + evalLog := vLog. + Debug(). + Str("operator_function", r.operator.Function). + Str("operator_data", r.operator.Data). + Str("arg", carg) + match := r.executeOperator(carg, tx) if match { mr := &corazarules.MatchData{ @@ -268,8 +278,12 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc // Set the txn variables for expansions before usage r.matchVariable(tx, mr) - if r.ParentID_ != noID || r.MultiMatch { - // In order to support Msg and LogData for inner rules, we need to expand them now + // Expansion for parent rule of a chain is postponed in order to rely on updated MATCHED_* variables. + // In all other cases, we want to expand here before continuing the rule evaluation to log the matched data + // just after the match an not just the last one. It is needed to log more than one variable matched by the same rule. + // The same logic applies to support Msg and LogData for inner rules. As soon as the inner rule matches, we want to expand and + // log the matched data. + if r.ParentID_ != noID || !r.HasChain { if r.Msg != nil { mr.Message_ = r.Msg.Expand(tx) } @@ -294,7 +308,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc tx.matchVariable(mr) for _, a := range r.actions { if a.Function.Type() == plugintypes.ActionTypeNondisruptive { - tx.DebugLogger().Debug().Str("action", a.Name).Msg("Evaluating action") + vLog.Debug().Str("action", a.Name).Msg("Evaluating action") a.Function.Evaluate(r, tx) } } @@ -307,19 +321,9 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc } } - tx.DebugLogger().Debug(). - Int("rule_id", rid). - Str("operator_function", r.operator.Function). - Str("operator_data", r.operator.Data). - Str("arg", carg). - Msg("Evaluating operator: MATCH") + evalLog.Msg("Evaluating operator: MATCH") } else { - tx.DebugLogger().Debug(). - Int("rule_id", rid). - Str("operator_function", r.operator.Function). - Str("operator_data", r.operator.Data). - Str("arg", carg). - Msg("Evaluating operator: NO MATCH") + evalLog.Msg("Evaluating operator: NO MATCH") } } } @@ -336,8 +340,15 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc // we only run the chains for the parent rule for nr := r.Chain; nr != nil; { chainLevel++ - tx.DebugLogger().Debug().Int("rule_id", rid).Msg("Evaluating rule chain") - matchedChainValues := nr.doEvaluate(phase, tx, collectiveMatchedValues, chainLevel, cache) + + var nrLogger debuglog.Logger + if nr.ID_ == noID { + nrLogger = logger.With(debuglog.Str("chain_rule_ref", fmt.Sprintf("%s#L%d", nr.File_, nr.Line_))) + } else { + nrLogger = logger.With(debuglog.Int("chain_rule_id", nr.ID_)) + } + + matchedChainValues := nr.doEvaluate(nrLogger, phase, tx, collectiveMatchedValues, chainLevel, cache) if len(matchedChainValues) == 0 { return matchedChainValues } @@ -347,7 +358,7 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc // Expansion of Msg and LogData is postponed here. It allows to run it only if the whole rule/chain // matches and to rely on MATCHED_* variables updated by the chain, not just by the first rule. - if !r.MultiMatch { + if r.HasChain || r.operator == nil { if r.Msg != nil { matchedValues[0].(*corazarules.MatchData).Message_ = r.Msg.Expand(tx) } @@ -359,11 +370,11 @@ func (r *Rule) doEvaluate(phase types.RulePhase, tx *Transaction, collectiveMatc for _, a := range r.actions { if a.Function.Type() == plugintypes.ActionTypeFlow { // Flow actions are evaluated also if the rule engine is set to DetectionOnly - tx.DebugLogger().Debug().Int("rule_id", rid).Str("action", a.Name).Int("phase", int(phase)).Msg("Evaluating flow action for rule") + logger.Debug().Str("action", a.Name).Int("phase", int(phase)).Msg("Evaluating flow action for rule") a.Function.Evaluate(r, tx) } else if a.Function.Type() == plugintypes.ActionTypeDisruptive && tx.RuleEngine == types.RuleEngineOn { // The parser enforces that the disruptive action is just one per rule (if more than one, only the last one is kept) - tx.DebugLogger().Debug().Int("rule_id", rid).Str("action", a.Name).Msg("Executing disruptive action for rule") + logger.Debug().Str("action", a.Name).Msg("Executing disruptive action for rule") a.Function.Evaluate(r, tx) } } @@ -453,16 +464,55 @@ func (r *Rule) AddAction(name string, action plugintypes.Action) error { return nil } +// hasRegex checks the received key to see if it is between forward slashes. +// if it is, it will return true and the content of the regular expression inside the slashes. +// otherwise it will return false and the same key. +func hasRegex(key string) (bool, string) { + if len(key) > 2 && key[0] == '/' && key[len(key)-1] == '/' { + return true, key[1 : len(key)-1] + } + return false, key +} + +// caseSensitiveVariable returns true if the variable is case sensitive +func caseSensitiveVariable(v variables.RuleVariable) bool { + res := false + switch v { + case variables.Args, variables.ArgsNames, + variables.ArgsGet, variables.ArgsPost, + variables.ArgsGetNames, variables.ArgsPostNames: + res = true + } + return res +} + +// newRuleVariableParams creates a new ruleVariableParams +// knows if a key needs to be lowercased. This probably should not be here, +// but the knowledge of the type of the Map it not here also, so let's start with this. +func newRuleVariableParams(v variables.RuleVariable, key string, re *regexp.Regexp, iscount bool) ruleVariableParams { + if !caseSensitiveVariable(v) { + key = strings.ToLower(key) + } + return ruleVariableParams{ + Count: iscount, + Variable: v, + KeyStr: key, + KeyRx: re, + Exceptions: []ruleVariableException{}, + } +} + // AddVariable adds a variable to the rule // The key can be a regexp.Regexp, a string or nil, in case of regexp // it will be used to match the variable, in case of string it will // be a fixed match, in case of nil it will match everything func (r *Rule) AddVariable(v variables.RuleVariable, key string, iscount bool) error { + if r == nil { + return fmt.Errorf("cannot add a variable to an undefined rule") + } var re *regexp.Regexp - if len(key) > 2 && key[0] == '/' && key[len(key)-1] == '/' { - key = key[1 : len(key)-1] - - if vare, err := memoize.Do(key, func() (interface{}, error) { return regexp.Compile(key) }); err != nil { + if isRegex, rx := hasRegex(key); isRegex { + if vare, err := memoize.Do(rx, func() (interface{}, error) { return regexp.Compile(rx) }); err != nil { return err } else { re = vare.(*regexp.Regexp) @@ -472,53 +522,29 @@ func (r *Rule) AddVariable(v variables.RuleVariable, key string, iscount bool) e if multiphaseEvaluation { // Splitting Args variable into ArgsGet and ArgsPost if v == variables.Args { - r.variables = append(r.variables, ruleVariableParams{ - Count: iscount, - Variable: variables.ArgsGet, - KeyStr: strings.ToLower(key), - KeyRx: re, - Exceptions: []ruleVariableException{}, - }) - - r.variables = append(r.variables, ruleVariableParams{ - Count: iscount, - Variable: variables.ArgsPost, - KeyStr: strings.ToLower(key), - KeyRx: re, - Exceptions: []ruleVariableException{}, - }) + r.variables = append(r.variables, newRuleVariableParams(variables.ArgsGet, key, re, iscount)) + r.variables = append(r.variables, newRuleVariableParams(variables.ArgsPost, key, re, iscount)) return nil } // Splitting ArgsNames variable into ArgsGetNames and ArgsPostNames if v == variables.ArgsNames { - r.variables = append(r.variables, ruleVariableParams{ - Count: iscount, - Variable: variables.ArgsGetNames, - KeyStr: strings.ToLower(key), - KeyRx: re, - Exceptions: []ruleVariableException{}, - }) - - r.variables = append(r.variables, ruleVariableParams{ - Count: iscount, - Variable: variables.ArgsPostNames, - KeyStr: strings.ToLower(key), - KeyRx: re, - Exceptions: []ruleVariableException{}, - }) + r.variables = append(r.variables, newRuleVariableParams(variables.ArgsGetNames, key, re, iscount)) + r.variables = append(r.variables, newRuleVariableParams(variables.ArgsPostNames, key, re, iscount)) return nil } } - r.variables = append(r.variables, ruleVariableParams{ - Count: iscount, - Variable: v, - KeyStr: strings.ToLower(key), - KeyRx: re, - Exceptions: []ruleVariableException{}, - }) + r.variables = append(r.variables, newRuleVariableParams(v, key, re, iscount)) return nil } +// needToSplitConcatenatedVariable returns true if the variable v is Args or ArgsNames and the +// variable ve is ArgsGet, ArgsPost, ArgsGetNames or ArgsPostNames +func needToSplitConcatenatedVariable(v variables.RuleVariable, ve variables.RuleVariable) bool { + return (v == variables.Args || v == variables.ArgsNames) && + (ve == variables.ArgsGet || ve == variables.ArgsPost || + ve == variables.ArgsGetNames || ve == variables.ArgsPostNames) +} + // AddVariableNegation adds an exception to a variable // It passes through if the variable is not used // It returns an error if the selector is empty, @@ -529,9 +555,8 @@ func (r *Rule) AddVariable(v variables.RuleVariable, key string, iscount bool) e // ERROR: SecRule !ARGS: "..." func (r *Rule) AddVariableNegation(v variables.RuleVariable, key string) error { var re *regexp.Regexp - if len(key) > 2 && key[0] == '/' && key[len(key)-1] == '/' { - key = key[1 : len(key)-1] - if vare, err := memoize.Do(key, func() (interface{}, error) { return regexp.Compile(key) }); err != nil { + if isRegex, rx := hasRegex(key); isRegex { + if vare, err := memoize.Do(rx, func() (interface{}, error) { return regexp.Compile(rx) }); err != nil { return err } else { re = vare.(*regexp.Regexp) @@ -542,19 +567,15 @@ func (r *Rule) AddVariableNegation(v variables.RuleVariable, key string) error { return fmt.Errorf("cannot create a variable exception for an undefined rule") } for i, rv := range r.variables { - // Splitting Args and ArgsNames variables - if multiphaseEvaluation && v == variables.Args && (rv.Variable == variables.ArgsGet || rv.Variable == variables.ArgsPost) { - rv.Exceptions = append(rv.Exceptions, ruleVariableException{strings.ToLower(key), re}) - r.variables[i] = rv - continue - } - if multiphaseEvaluation && v == variables.ArgsNames && (rv.Variable == variables.ArgsGetNames || rv.Variable == variables.ArgsPostNames) { - rv.Exceptions = append(rv.Exceptions, ruleVariableException{strings.ToLower(key), re}) + // Even when Args and ArgsNames are one map, the exceptions must be created for the individual maps the + // Concat Map contains in order for exceptions to apply in the corresponding phase. + if multiphaseEvaluation && needToSplitConcatenatedVariable(v, rv.Variable) { + rv.Exceptions = append(rv.Exceptions, ruleVariableException{key, re}) r.variables[i] = rv continue } if rv.Variable == v { - rv.Exceptions = append(rv.Exceptions, ruleVariableException{strings.ToLower(key), re}) + rv.Exceptions = append(rv.Exceptions, ruleVariableException{key, re}) r.variables[i] = rv } } diff --git a/internal/corazawaf/rule_casesensitive_test.go b/internal/corazawaf/rule_casesensitive_test.go new file mode 100644 index 000000000..3b3458584 --- /dev/null +++ b/internal/corazawaf/rule_casesensitive_test.go @@ -0,0 +1,22 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build coraza.rule.case_sensitive_args_keys + +package corazawaf + +import ( + "testing" + + "github.com/crowdsecurity/coraza/v3/types/variables" +) + +func TestCaseSensitiveArgsVariableKeys(t *testing.T) { + rule := NewRule() + if err := rule.AddVariable(variables.ArgsGet, "Som3ThinG", false); err != nil { + t.Error(err) + } + if rule.variables[0].KeyStr != "Som3ThinG" { + t.Error("variable key is not case insensitive") + } +} diff --git a/internal/corazawaf/rule_test.go b/internal/corazawaf/rule_test.go index 009ee40b4..f1c3ac0a8 100644 --- a/internal/corazawaf/rule_test.go +++ b/internal/corazawaf/rule_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package corazawaf @@ -8,6 +8,7 @@ import ( "strconv" "testing" + "github.com/crowdsecurity/coraza/v3/debuglog" "github.com/crowdsecurity/coraza/v3/experimental/plugins/macro" "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" "github.com/crowdsecurity/coraza/v3/internal/corazarules" @@ -20,6 +21,7 @@ func TestMatchEvaluate(t *testing.T) { r.Msg, _ = macro.NewMacro("Message") r.LogData, _ = macro.NewMacro("Data Message") r.ID_ = 1 + r.LogID_ = "1" if err := r.AddVariable(variables.ArgsGet, "", false); err != nil { t.Error(err) } @@ -31,7 +33,7 @@ func TestMatchEvaluate(t *testing.T) { tx.AddGetRequestArgument("test", "0") var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 1 { t.Errorf("Expected 1 matchdata from a SecActions rule, got %d", len(matchdata)) } @@ -43,6 +45,7 @@ func TestMatchEvaluate(t *testing.T) { func TestNoMatchEvaluate(t *testing.T) { r := NewRule() r.ID_ = 1 + r.LogID_ = "1" if err := r.AddVariable(variables.ArgsGet, "", false); err != nil { t.Error(err) } @@ -54,7 +57,7 @@ func TestNoMatchEvaluate(t *testing.T) { tx.AddGetRequestArgument("test", "999") var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 0 { t.Errorf("Expected 0 matchdata from a SecActions rule, got %d", len(matchdata)) } @@ -64,27 +67,50 @@ func TestNoMatchEvaluate(t *testing.T) { } func TestNoMatchEvaluateBecauseOfException(t *testing.T) { - r := NewRule() - r.Msg, _ = macro.NewMacro("Message") - r.LogData, _ = macro.NewMacro("Data Message") - r.ID_ = 1 - if err := r.AddVariable(variables.ArgsGet, "", false); err != nil { - t.Error(err) - } - dummyEqOp := &dummyEqOperator{} - r.SetOperator(dummyEqOp, "@eq", "0") - action := &dummyDenyAction{} - _ = r.AddAction("dummyDeny", action) - tx := NewWAF().NewTransaction() - tx.AddGetRequestArgument("test", "0") - tx.RemoveRuleTargetByID(1, variables.ArgsGet, "test") - var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) - if len(matchdata) != 0 { - t.Errorf("Expected 0 matchdata, got %d", len(matchdata)) - } - if tx.interruption != nil { - t.Errorf("Expected interruption not triggered because of RemoveRuleTargetByID") + testCases := []struct { + name string + variable variables.RuleVariable + }{ + { + name: "Test ArgsGet target exception", + variable: variables.ArgsGet, + }, + { + name: "Test Args target exception", + variable: variables.Args, + }, + { + name: "Test ArgsNames target exception", + variable: variables.ArgsNames, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r := NewRule() + r.Msg, _ = macro.NewMacro("Message") + r.LogData, _ = macro.NewMacro("Data Message") + r.ID_ = 1 + r.LogID_ = "1" + if err := r.AddVariable(tc.variable, "", false); err != nil { + t.Error(err) + } + dummyEqOp := &dummyEqOperator{} + r.SetOperator(dummyEqOp, "@eq", "0") + action := &dummyDenyAction{} + _ = r.AddAction("dummyDeny", action) + tx := NewWAF().NewTransaction() + tx.AddGetRequestArgument("test", "0") + tx.RemoveRuleTargetByID(1, tc.variable, "test") + var matchedValues []types.MatchData + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + if len(matchdata) != 0 { + t.Errorf("Expected 0 matchdata, got %d", len(matchdata)) + } + if tx.interruption != nil { + t.Errorf("Expected interruption not triggered because of RemoveRuleTargetByID") + } + }) } } @@ -106,6 +132,7 @@ func (*dummyFlowAction) Type() plugintypes.ActionType { func TestFlowActionIfDetectionOnlyEngine(t *testing.T) { r := NewRule() r.ID_ = 1 + r.LogID_ = "1" r.operator = nil action := &dummyFlowAction{} _ = r.AddAction("dummyFlowAction", action) @@ -113,7 +140,7 @@ func TestFlowActionIfDetectionOnlyEngine(t *testing.T) { tx.RuleEngine = types.RuleEngineDetectionOnly var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 1 { t.Errorf("Expected 1 matchdata, got %d", len(matchdata)) } @@ -151,6 +178,7 @@ func TestMatchVariableRunsActionTypeNondisruptive(t *testing.T) { func TestDisruptiveActionFromChainNotEvaluated(t *testing.T) { r := NewRule() r.ID_ = 1 + r.LogID_ = "1" r.operator = nil r.HasChain = true action := &dummyNonDisruptiveAction{} @@ -158,6 +186,7 @@ func TestDisruptiveActionFromChainNotEvaluated(t *testing.T) { chainedRule := NewRule() chainedRule.ID_ = 0 chainedRule.ParentID_ = 1 + chainedRule.LogID_ = "1" chainedRule.operator = nil chainedAction := &dummyDenyAction{} _ = chainedRule.AddAction("dummyDenyAction", chainedAction) @@ -165,7 +194,7 @@ func TestDisruptiveActionFromChainNotEvaluated(t *testing.T) { tx := NewWAF().NewTransaction() var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 2 { t.Errorf("Expected 2 matchdata from a SecActions chained rule (total 2 rules), got %d", len(matchdata)) } @@ -178,12 +207,13 @@ func TestRuleDetailsTransferredToTransaction(t *testing.T) { r := NewRule() r.ID_ = 0 r.ParentID_ = 1 + r.LogID_ = "1" r.Capture = true r.operator = nil tx := NewWAF().NewTransaction() var matchedValues []types.MatchData - r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if tx.variables.rule.Get("id")[0] != strconv.Itoa(r.ParentID()) { t.Errorf("Expected id: %d (parent id), got %s", r.ParentID(), tx.variables.rule.Get("id")[0]) } @@ -203,11 +233,12 @@ func TestSecActionMessagePropagationInMatchData(t *testing.T) { r.Msg, _ = macro.NewMacro("Message") r.LogData, _ = macro.NewMacro("Data Message") r.ID_ = 1 + r.LogID_ = "1" // SecAction uses nil operator r.operator = nil tx := NewWAF().NewTransaction() var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 1 { t.Errorf("Expected 1 matchdata from a SecActions rule, got %d", len(matchdata)) } @@ -256,16 +287,6 @@ func TestRuleNegativeVariablesEmtpyRule(t *testing.T) { } } -func TestVariableKeysAreCaseInsensitive(t *testing.T) { - rule := NewRule() - if err := rule.AddVariable(variables.RequestURI, "Som3ThinG", false); err != nil { - t.Error(err) - } - if rule.variables[0].KeyStr != "som3thing" { - t.Error("variable key is not case insensitive") - } -} - func TestVariablesRxAreCaseSensitive(t *testing.T) { rule := NewRule() if err := rule.AddVariable(variables.ArgsGet, "/Som3ThinG/", false); err != nil { @@ -533,6 +554,7 @@ func TestTransformArgNoCacheForTXVariable(t *testing.T) { func TestCaptureNotPropagatedToInnerChainRule(t *testing.T) { r := NewRule() r.ID_ = 1 + r.LogID_ = "1" r.operator = nil r.HasChain = true r.Phase_ = 1 @@ -540,12 +562,13 @@ func TestCaptureNotPropagatedToInnerChainRule(t *testing.T) { chainedRule := NewRule() chainedRule.ID_ = 0 chainedRule.ParentID_ = 1 + chainedRule.LogID_ = "1" chainedRule.operator = nil chainedRule.Capture = false r.Chain = chainedRule tx := NewWAF().NewTransaction() var matchedValues []types.MatchData - r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) // We expect that capture is false after doEvaluate. if tx.Capture { t.Errorf("Expected capture to be false. The parent rule enables capture, but inner rule should disable it.") @@ -555,6 +578,7 @@ func TestCaptureNotPropagatedToInnerChainRule(t *testing.T) { func TestExpandMacroAfterWholeRuleEvaluation(t *testing.T) { r := NewRule() r.ID_ = 1 + r.LogID_ = "1" r.operator = nil r.HasChain = true r.Phase_ = 1 @@ -565,6 +589,7 @@ func TestExpandMacroAfterWholeRuleEvaluation(t *testing.T) { chainedRule := NewRule() chainedRule.ID_ = 0 chainedRule.ParentID_ = 1 + chainedRule.LogID_ = "1" chainedRule.operator = nil _ = r.AddVariable(variables.RequestURI, "", false) @@ -581,7 +606,7 @@ func TestExpandMacroAfterWholeRuleEvaluation(t *testing.T) { tx.AddGetRequestArgument("test", "0") var matchedValues []types.MatchData - matchdata := r.doEvaluate(types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) + matchdata := r.doEvaluate(debuglog.Noop(), types.PhaseRequestHeaders, tx, &matchedValues, 0, tx.transformationCache) if len(matchdata) != 2 { t.Errorf("Expected 2 matchdata from a chained rule (total 2 rules), got %d", len(matchdata)) } diff --git a/internal/corazawaf/rulegroup.go b/internal/corazawaf/rulegroup.go index dfbbbaada..f37a625e7 100644 --- a/internal/corazawaf/rulegroup.go +++ b/internal/corazawaf/rulegroup.go @@ -8,7 +8,7 @@ import ( "time" "github.com/crowdsecurity/coraza/v3/internal/corazatypes" - "github.com/crowdsecurity/coraza/v3/internal/strings" + utils "github.com/crowdsecurity/coraza/v3/internal/strings" "github.com/crowdsecurity/coraza/v3/types" "github.com/crowdsecurity/coraza/v3/types/variables" ) @@ -102,7 +102,7 @@ func (rg *RuleGroup) DeleteByMsg(msg string) { func (rg *RuleGroup) DeleteByTag(tag string) { var kept []Rule for _, r := range rg.rules { - if !strings.InSlice(tag, r.Tags_) { + if !utils.InSlice(tag, r.Tags_) { kept = append(kept, r) } } diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go index 1ddb92065..001ceebbf 100644 --- a/internal/corazawaf/transaction.go +++ b/internal/corazawaf/transaction.go @@ -1,16 +1,18 @@ -// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package corazawaf import ( "bufio" + "context" "errors" "fmt" "io" "math" "mime" "net/url" + "os" "path/filepath" "strconv" "strings" @@ -25,6 +27,7 @@ import ( "github.com/crowdsecurity/coraza/v3/internal/cookies" "github.com/crowdsecurity/coraza/v3/internal/corazarules" "github.com/crowdsecurity/coraza/v3/internal/corazatypes" + "github.com/crowdsecurity/coraza/v3/internal/environment" stringsutil "github.com/crowdsecurity/coraza/v3/internal/strings" urlutil "github.com/crowdsecurity/coraza/v3/internal/url" "github.com/crowdsecurity/coraza/v3/types" @@ -42,6 +45,9 @@ type Transaction struct { // Transaction ID id string + // The context associated to the transaction. + context context.Context + // Contains the list of matched rules and associated match information matchedRules []types.MatchedRule @@ -501,6 +507,7 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) { Rule_: &r.RuleMetadata, Log_: r.Log, MatchedDatas_: mds, + Context_: tx.context, } // Populate MatchedRule disruption related fields only if the Engine is capable of performing disruptive actions if tx.RuleEngine == types.RuleEngineOn { @@ -615,6 +622,23 @@ func (tx *Transaction) RemoveRuleTargetByID(id int, variable variables.RuleVaria Variable: variable, KeyStr: key, } + + if multiphaseEvaluation && (variable == variables.Args || variable == variables.ArgsNames) { + // ARGS and ARGS_NAMES have to be splitted into _GET and _POST + switch variable { + case variables.Args: + c.Variable = variables.ArgsGet + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + c.Variable = variables.ArgsPost + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + case variables.ArgsNames: + c.Variable = variables.ArgsGetNames + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + c.Variable = variables.ArgsPostNames + tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) + } + return + } tx.ruleRemoveTargetByID[id] = append(tx.ruleRemoveTargetByID[id], c) } @@ -940,6 +964,7 @@ func (tx *Transaction) ReadRequestBodyFrom(r io.Reader) (*types.Interruption, in } if tx.requestBodyBuffer.length == tx.RequestBodyLimit { + tx.variables.inboundDataError.Set("1") if tx.WAF.RequestBodyLimitAction == types.BodyLimitActionReject { return setAndReturnBodyLimitInterruption(tx) } @@ -1115,7 +1140,6 @@ func (tx *Transaction) WriteResponseBody(b []byte) (*types.Interruption, int, er runProcessResponseBody = false ) if tx.responseBodyBuffer.length+writingBytes >= tx.ResponseBodyLimit { - // TODO: figure out ErrorData vs DataError: https://github.com/crowdsecurity/coraza/issues/564 tx.variables.outboundDataError.Set("1") if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject { // We interrupt this transaction in case ResponseBodyLimitAction is Reject @@ -1167,7 +1191,6 @@ func (tx *Transaction) ReadResponseBodyFrom(r io.Reader) (*types.Interruption, i if l, ok := r.(ByteLenger); ok { writingBytes = int64(l.Len()) if tx.responseBodyBuffer.length+writingBytes >= tx.ResponseBodyLimit { - // TODO: figure out ErrorData vs DataError: https://github.com/crowdsecurity/coraza/issues/564 tx.variables.outboundDataError.Set("1") if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject { return setAndReturnBodyLimitInterruption(tx) @@ -1188,6 +1211,7 @@ func (tx *Transaction) ReadResponseBodyFrom(r io.Reader) (*types.Interruption, i } if tx.responseBodyBuffer.length == tx.ResponseBodyLimit { + tx.variables.outboundDataError.Set("1") if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject { return setAndReturnBodyLimitInterruption(tx) } @@ -1303,6 +1327,9 @@ func (tx *Transaction) ProcessLogging() { if tx.AuditEngine == types.AuditEngineRelevantOnly && tx.audit { re := tx.WAF.AuditLogRelevantStatus status := tx.variables.responseStatus.Get() + if tx.IsInterrupted() { + status = strconv.Itoa(tx.interruption.Status) + } if re != nil && !re.Match([]byte(status)) { // Not relevant status tx.debugLogger.Debug(). @@ -1374,22 +1401,25 @@ func (tx *Transaction) AuditLog() *auditlog.Log { HostIP_: tx.variables.serverAddr.Get(), HostPort_: hostPort, ServerID_: tx.variables.serverName.Get(), // TODO check + Request_: &auditlog.TransactionRequest{ + Method_: tx.variables.requestMethod.Get(), + URI_: tx.variables.requestURI.Get(), + Protocol_: tx.variables.requestProtocol.Get(), + }, } for _, part := range tx.AuditLogParts { switch part { case types.AuditLogPartRequestHeaders: - if al.Transaction_.Request_ == nil { - al.Transaction_.Request_ = &auditlog.TransactionRequest{} - } al.Transaction_.Request_.Headers_ = tx.variables.requestHeaders.Data() case types.AuditLogPartRequestBody: - if al.Transaction_.Request_ == nil { - al.Transaction_.Request_ = &auditlog.TransactionRequest{} + reader, err := tx.requestBodyBuffer.Reader() + if err == nil { + content, err := io.ReadAll(reader) + if err == nil { + al.Transaction_.Request_.Body_ = string(content) + } } - // TODO maybe change to: - // al.Transaction.Request.Body = tx.RequestBodyBuffer.String() - al.Transaction_.Request_.Body_ = tx.variables.requestBody.Get() /* * TODO: @@ -1479,13 +1509,25 @@ func (tx *Transaction) AuditLog() *auditlog.Log { // It also allows caches the transaction back into the sync.Pool func (tx *Transaction) Close() error { defer tx.WAF.txPool.Put(tx) - tx.variables.reset() + var errs []error + if environment.HasAccessToFS { + // TODO(jcchavezs): filesTmpNames should probably be a new kind of collection that + // is aware of the files and then attempt to delete them when the collection + // is resetted or an item is removed. + for _, file := range tx.variables.filesTmpNames.Get("") { + if err := os.Remove(file); err != nil { + errs = append(errs, fmt.Errorf("removing temporary file: %v", err)) + } + } + } + + tx.variables.reset() if err := tx.requestBodyBuffer.Reset(); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("reseting request body buffer: %v", err)) } if err := tx.responseBodyBuffer.Reset(); err != nil { - errs = append(errs, err) + errs = append(errs, fmt.Errorf("reseting response body buffer: %v", err)) } if tx.IsInterrupted() { @@ -1500,14 +1542,11 @@ func (tx *Transaction) Close() error { Msg("Transaction finished") } - switch { - case len(errs) == 0: + if len(errs) == 0 { return nil - case len(errs) == 1: - return fmt.Errorf("transaction close failed: %s", errs[0].Error()) - default: - return fmt.Errorf("transaction close failed:\n- %s\n- %s", errs[0].Error(), errs[1].Error()) } + + return fmt.Errorf("transaction close failed: %v", errors.Join(errs...)) } // String will return a string with the transaction debug information @@ -1574,6 +1613,7 @@ type TransactionVariables struct { multipartFilename *collections.Map multipartName *collections.Map multipartPartHeaders *collections.Map + multipartStrictError *collections.Single outboundDataError *collections.Single queryString *collections.Single remoteAddr *collections.Single @@ -1692,15 +1732,23 @@ func NewTransactionVariables() *TransactionVariables { v.responseXML = collections.NewMap(variables.ResponseXML) v.requestXML = collections.NewMap(variables.RequestXML) v.multipartPartHeaders = collections.NewMap(variables.MultipartPartHeaders) + v.multipartStrictError = collections.NewSingle(variables.MultipartStrictError) // XML is a pointer to RequestXML v.xml = v.requestXML - v.argsGet = collections.NewNamedCollection(variables.ArgsGet) + if shouldUseCaseSensitiveNamedCollection { + v.argsGet = collections.NewCaseSensitiveNamedCollection(variables.ArgsGet) + v.argsPost = collections.NewCaseSensitiveNamedCollection(variables.ArgsPost) + v.argsPath = collections.NewCaseSensitiveNamedCollection(variables.ArgsPath) + } else { + v.argsGet = collections.NewNamedCollection(variables.ArgsGet) + v.argsPost = collections.NewNamedCollection(variables.ArgsPost) + v.argsPath = collections.NewNamedCollection(variables.ArgsPath) + } + v.argsGetNames = v.argsGet.Names(variables.ArgsGetNames) - v.argsPost = collections.NewNamedCollection(variables.ArgsPost) v.argsPostNames = v.argsPost.Names(variables.ArgsPostNames) - v.argsPath = collections.NewNamedCollection(variables.ArgsPath) v.argsCombinedSize = collections.NewSizeCollection(variables.ArgsCombinedSize, v.argsGet, v.argsPost) v.args = collections.NewConcatKeyed( variables.Args, @@ -2018,6 +2066,10 @@ func (v *TransactionVariables) ResBodyProcessorErrorMsg() collection.Single { return v.resBodyProcessorErrorMsg } +func (v *TransactionVariables) MultipartStrictError() collection.Single { + return v.multipartStrictError +} + // All iterates over the variables. We return both variable and its collection, i.e. key/value, to follow // general range iteration in Go which always has a key and value (key is int index for slices). Notably, // this is consistent with discussions for custom iterable types in a future language version @@ -2107,6 +2159,9 @@ func (v *TransactionVariables) All(f func(v variables.RuleVariable, col collecti if !f(variables.MultipartPartHeaders, v.multipartPartHeaders) { return } + if !f(variables.MultipartStrictError, v.multipartStrictError) { + return + } if !f(variables.OutboundDataError, v.outboundDataError) { return } diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go index 7b3e999ec..866a5375c 100644 --- a/internal/corazawaf/transaction_test.go +++ b/internal/corazawaf/transaction_test.go @@ -98,7 +98,7 @@ func TestTxMultipart(t *testing.T) { tx.RequestBodyLimit = 9999999 _, err := tx.ParseRequestReader(strings.NewReader(data)) if err != nil { - t.Error("Failed to parse multipart request: " + err.Error()) + t.Fatal("Failed to parse multipart request: " + err.Error()) } exp := map[string]string{ "%{args_post.text}": "test-value", @@ -108,6 +108,10 @@ func TestTxMultipart(t *testing.T) { } validateMacroExpansion(exp, tx, t) + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxResponse(t *testing.T) { @@ -162,26 +166,40 @@ func TestWriteRequestBody(t *testing.T) { ) testCases := []struct { - name string - requestBodyLimit int - requestBodyLimitAction types.BodyLimitAction - shouldInterrupt bool + name string + requestBodyLimit int + requestBodyLimitAction types.BodyLimitAction + avoidRequestBodyLimitActionInit bool + shouldInterrupt bool + limitReached bool // If the limit is reached, INBOUND_DATA_ERROR should be set }{ { name: "LimitNotReached", requestBodyLimit: urlencodedBodyLen + 2, requestBodyLimitAction: types.BodyLimitAction(-1), + limitReached: false, }, { name: "LimitReachedAndRejects", requestBodyLimit: urlencodedBodyLen - 3, requestBodyLimitAction: types.BodyLimitActionReject, shouldInterrupt: true, + limitReached: true, + }, + { + name: "LimitReachedAndRejectsDefaultValue", + requestBodyLimit: urlencodedBodyLen - 3, + // Omitting requestBodyLimitAction defaults to Reject + // requestBodyLimitAction: types.BodyLimitActionReject, + avoidRequestBodyLimitActionInit: true, + shouldInterrupt: true, + limitReached: true, }, { name: "LimitReachedAndPartialProcessing", requestBodyLimit: urlencodedBodyLen - 3, requestBodyLimitAction: types.BodyLimitActionProcessPartial, + limitReached: true, }, } @@ -201,8 +219,9 @@ func TestWriteRequestBody(t *testing.T) { waf.RuleEngine = types.RuleEngineOn waf.RequestBodyAccess = true waf.RequestBodyLimit = int64(testCase.requestBodyLimit) - waf.RequestBodyLimitAction = testCase.requestBodyLimitAction - + if !testCase.avoidRequestBodyLimitActionInit { + waf.RequestBodyLimitAction = testCase.requestBodyLimitAction + } tx := waf.NewTransaction() tx.AddRequestHeader("content-type", "application/x-www-form-urlencoded") @@ -215,10 +234,12 @@ func TestWriteRequestBody(t *testing.T) { for _, c := range chunks { if it, _, err = writeRequestBody(tx, c); err != nil { - t.Errorf("Failed to write body buffer: %s", err.Error()) + t.Fatalf("Failed to write body buffer: %s", err.Error()) } } - + if testCase.limitReached && tx.variables.inboundDataError.Get() != "1" { + t.Fatalf("Expected INBOUND_DATA_ERROR to be set") + } if testCase.shouldInterrupt { if it == nil { t.Fatal("Expected interruption, got nil") @@ -235,11 +256,13 @@ func TestWriteRequestBody(t *testing.T) { val := tx.variables.argsPost.Get("some") if len(val) != 1 || val[0] != "result" { - t.Errorf("Failed to set urlencoded POST data with arguments: \"%s\"", strings.Join(val, "\", \"")) + t.Fatalf("Failed to set urlencoded POST data with arguments: \"%s\"", strings.Join(val, "\", \"")) } } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } @@ -296,7 +319,9 @@ func TestWriteRequestBodyOnLimitReached(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -343,7 +368,9 @@ func TestWriteRequestBodyIsNopWhenBodyIsNotAccesible(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -354,12 +381,12 @@ func TestResponseHeader(t *testing.T) { tx := makeTransaction(t) tx.AddResponseHeader("content-type", "test") if tx.variables.responseContentType.Get() != "test" { - t.Error("invalid RESPONSE_CONTENT_TYPE after response headers") + t.Fatal("invalid RESPONSE_CONTENT_TYPE after response headers") } interruption := tx.ProcessResponseHeaders(200, "OK") if interruption != nil { - t.Error("unexpected interruption") + t.Fatal("unexpected interruption") } } @@ -368,12 +395,16 @@ func TestProcessRequestHeadersDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff if !tx.IsRuleEngineOff() { - t.Error("expected Engine off") + t.Fatal("expected Engine off") } _ = tx.ProcessRequestHeaders() if tx.lastPhase != 0 { // 0 means no phases have been evaluated - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -381,10 +412,13 @@ func TestProcessRequestBodyDoesNoEvaluationOnEngineOff(t *testing.T) { tx := NewWAF().NewTransaction() tx.RuleEngine = types.RuleEngineOff if _, err := tx.ProcessRequestBody(); err != nil { - t.Error("failed to process request body") + t.Fatal("failed to process request body") } if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -393,7 +427,7 @@ func TestProcessResponseHeadersDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff _ = tx.ProcessResponseHeaders(200, "OK") if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") } } @@ -401,10 +435,10 @@ func TestProcessResponseBodyDoesNoEvaluationOnEngineOff(t *testing.T) { tx := NewWAF().NewTransaction() tx.RuleEngine = types.RuleEngineOff if _, err := tx.ProcessResponseBody(); err != nil { - t.Error("Failed to process response body") + t.Fatal("Failed to process response body") } if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") } } @@ -413,7 +447,10 @@ func TestProcessLoggingDoesNoEvaluationOnEngineOff(t *testing.T) { tx.RuleEngine = types.RuleEngineOff tx.ProcessLogging() if tx.lastPhase != 0 { - t.Error("unexpected rule evaluation") + t.Fatal("unexpected rule evaluation") + } + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -422,11 +459,11 @@ func TestAuditLog(t *testing.T) { tx.AuditLogParts = types.AuditLogParts("ABCDEFGHIJK") al := tx.AuditLog() if al.Transaction().ID() != tx.id { - t.Error("invalid auditlog id") + t.Fatal("invalid auditlog id") } // TODO more checks if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -455,22 +492,33 @@ func TestWriteResponseBody(t *testing.T) { responseBodyLimit int responseBodyLimitAction types.BodyLimitAction shouldInterrupt bool + limitReached bool // If the limit is reached, OUTBOUND_DATA_ERROR should be set }{ { name: "LimitNotReached", responseBodyLimit: urlencodedBodyLen + 2, responseBodyLimitAction: types.BodyLimitAction(-1), + limitReached: false, }, { name: "LimitReachedAndRejects", responseBodyLimit: urlencodedBodyLen - 3, responseBodyLimitAction: types.BodyLimitActionReject, shouldInterrupt: true, + limitReached: true, }, { name: "LimitReachedAndPartialProcessing", responseBodyLimit: urlencodedBodyLen - 3, responseBodyLimitAction: types.BodyLimitActionProcessPartial, + limitReached: true, + }, + { + name: "LimitReachedAndPartialProcessingDefaultValue", + responseBodyLimit: urlencodedBodyLen - 3, + // Omitting requestBodyLimitAction defaults to ProcessPartial + // responseBodyLimitAction: types.BodyLimitActionProcessPartial, + limitReached: true, }, } @@ -509,10 +557,12 @@ func TestWriteResponseBody(t *testing.T) { for _, c := range chunks { if it, _, err = writeResponseBody(tx, c); err != nil { - t.Errorf("Failed to write body buffer: %s", err.Error()) + t.Fatalf("Failed to write body buffer: %s", err.Error()) } } - + if testCase.limitReached && tx.variables.outboundDataError.Get() != "1" { + t.Fatalf("Expected OUTBOUND_DATA_ERROR to be set") + } if testCase.shouldInterrupt { if it == nil { t.Fatal("Expected interruption, got nil") @@ -529,11 +579,13 @@ func TestWriteResponseBody(t *testing.T) { // checking if the body has been populated up to the first POST arg index := strings.Index(urlencodedBody, "&") if tx.variables.responseBody.Get()[:index] != urlencodedBody[:index] { - t.Error("failed to set response body") + t.Fatal("failed to set response body") } } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } @@ -590,7 +642,9 @@ func TestWriteResponseBodyOnLimitReached(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -637,7 +691,9 @@ func TestWriteResponseBodyIsNopWhenBodyIsNotAccesible(t *testing.T) { t.Fatalf("unexpected number of bytes written") } - _ = tx.Close() + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } }) } }) @@ -658,21 +714,21 @@ func TestAuditLogFields(t *testing.T) { }, }) if len(tx.matchedRules) == 0 || tx.matchedRules[0].Rule().ID() != rule.ID_ { - t.Error("failed to match rule for audit") + t.Fatal("failed to match rule for audit") } al := tx.AuditLog() if len(al.Messages()) == 0 || al.Messages()[0].Data().ID() != rule.ID_ { - t.Error("failed to add rules to audit logs") + t.Fatal("failed to add rules to audit logs") } if len(al.Transaction().Request().Headers()) == 0 || al.Transaction().Request().Headers()["test"][0] != "test" { - t.Error("failed to add request header to audit log") + t.Fatal("failed to add request header to audit log") } if len(al.Transaction().Response().Headers()) == 0 || al.Transaction().Response().Headers()["test"][0] != "test" { - t.Error("failed to add Response header to audit log") + t.Fatal("failed to add Response header to audit log") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -681,27 +737,68 @@ func TestResetCapture(t *testing.T) { tx.Capture = true tx.CaptureField(5, "test") if tx.variables.tx.Get("5")[0] != "test" { - t.Error("failed to set capture field from tx") + t.Fatal("failed to set capture field from tx") } tx.resetCaptures() if tx.variables.tx.Get("5")[0] != "" { - t.Error("failed to reset capture field from tx") + t.Fatal("failed to reset capture field from tx") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } func TestRelevantAuditLogging(t *testing.T) { - tx := makeTransaction(t) - tx.WAF.AuditLogRelevantStatus = regexp.MustCompile(`(403)`) - tx.variables.responseStatus.Set("403") - tx.AuditEngine = types.AuditEngineRelevantOnly - // tx.WAF.auditLogger = auditlog.NewAuditLogger() - tx.ProcessLogging() - // TODO how do we check if the log was writen? - if err := tx.Close(); err != nil { - t.Error(err) + tests := []struct { + name string + status string + interruption *types.Interruption + relevantLog bool + }{ + { + name: "TestRelevantAuditLogging", + status: "403", + interruption: nil, + relevantLog: true, + }, + { + name: "TestNotRelevantAuditLogging", + status: "200", + interruption: nil, + relevantLog: false, + }, + { + name: "TestRelevantAuditLoggingWithInterruption", + interruption: &types.Interruption{ + Status: 403, + Action: "deny", + }, + relevantLog: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tx := makeTransaction(t) + debugLog := bytes.Buffer{} + tx.debugLogger = debuglog.Default().WithLevel(debuglog.LevelDebug).WithOutput(&debugLog) + tx.WAF.AuditLogRelevantStatus = regexp.MustCompile(`(403)`) + tx.variables.responseStatus.Set(tt.status) + tx.interruption = tt.interruption + tx.AuditEngine = types.AuditEngineRelevantOnly + tx.audit = true // Mimics that there is something to audit + tx.ProcessLogging() + // TODO how do we check if the log was written? + if err := tx.Close(); err != nil { + t.Error(err) + } + if tt.relevantLog && strings.Contains(debugLog.String(), "Transaction status not marked for audit logging") { + t.Errorf("unexpected debug log: %q. Transaction status should be marked for audit logging", debugLog.String()) + } + if !tt.relevantLog && !strings.Contains(debugLog.String(), "Transaction status not marked for audit logging") { + t.Errorf("missing debug log. Transaction status should be not marked for audit logging not being relevant") + } + }) } } @@ -748,6 +845,7 @@ func TestLogCallback(t *testing.T) { tx := waf.NewTransaction() rule := NewRule() rule.ID_ = 1 + rule.LogID_ = "1" rule.Phase_ = 1 rule.Log = true _ = rule.AddAction("deny", testCase.action) @@ -770,13 +868,13 @@ func TestLogCallback(t *testing.T) { } if buffer == "" || !strings.Contains(buffer, tx.id) { - t.Error("failed to call error log callback") + t.Fatal("failed to call error log callback") } if !strings.Contains(buffer, testCase.expectedLogLine) { - t.Errorf("Expected string \"%s\" with disruptive rule, got %s", testCase.expectedLogLine, buffer) + t.Fatalf("Expected string \"%s\" with disruptive rule, got %s", testCase.expectedLogLine, buffer) if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } }) @@ -790,19 +888,19 @@ func TestHeaderSetters(t *testing.T) { tx.AddRequestHeader("test1", "test2") c := tx.variables.requestCookies.Get("abc")[0] if c != "def" { - t.Errorf("failed to set cookie, got %q", c) + t.Fatalf("failed to set cookie, got %q", c) } if tx.variables.requestHeaders.Get("cookie")[0] != "abc=def;hij=klm" { - t.Error("failed to set request header") + t.Fatal("failed to set request header") } if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) { - t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) + t.Fatal("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) } if !utils.InSlice("abc", collectionValues(t, tx.variables.requestCookiesNames)) { - t.Error("failed to set cookie name") + t.Fatal("failed to set cookie name") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -876,16 +974,16 @@ func TestRequestBodyProcessingAlgorithm(t *testing.T) { tx.AddRequestHeader("content-length", "7") tx.ProcessRequestHeaders() if _, err := tx.requestBodyBuffer.Write([]byte("test123")); err != nil { - t.Error("Failed to write request body buffer") + t.Fatal("Failed to write request body buffer") } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error("failed to process request body") + t.Fatal("failed to process request body") } if tx.variables.requestBody.Get() != "test123" { - t.Error("failed to set request body") + t.Fatal("failed to set request body") } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -927,7 +1025,7 @@ func TestProcessBodiesSkippedIfHeadersPhasesNotReached(t *testing.T) { t.Fatalf("unexpected message, want %q, have %q", want, have) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -939,31 +1037,31 @@ func TestTxVariables(t *testing.T) { KeyRx: regexp.MustCompile("ho.*"), } if len(tx.GetField(rv)) != 1 || tx.GetField(rv)[0].Value() != "www.test.com:80" { - t.Errorf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(tx.GetField(rv)), tx.GetField(rv)) + t.Fatalf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(tx.GetField(rv)), tx.GetField(rv)) } rv.Count = true if len(tx.GetField(rv)) == 0 || tx.GetField(rv)[0].Value() != "1" { - t.Errorf("failed to get count for regexp variable") + t.Fatalf("failed to get count for regexp variable") } // now nil key rv.KeyRx = nil if len(tx.GetField(rv)) == 0 { - t.Error("failed to match rule variable REQUEST_HEADERS with nil key") + t.Fatal("failed to match rule variable REQUEST_HEADERS with nil key") } rv.KeyStr = "" f := tx.GetField(rv) if len(f) == 0 { - t.Error("failed to count variable REQUEST_HEADERS ") + t.Fatal("failed to count variable REQUEST_HEADERS ") } count, err := strconv.Atoi(f[0].Value()) if err != nil { - t.Error(err) + t.Fatal(err) } if count != 5 { - t.Errorf("failed to match rule variable REQUEST_HEADERS with count, %v", rv) + t.Fatalf("failed to match rule variable REQUEST_HEADERS with count, %v", rv) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } @@ -979,12 +1077,12 @@ func TestTxVariablesExceptions(t *testing.T) { } fields := tx.GetField(rv) if len(fields) != 0 { - t.Errorf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) + t.Fatalf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) } rv.Exceptions = nil fields = tx.GetField(rv) if len(fields) != 1 || fields[0].Value() != "www.test.com:80" { - t.Errorf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(fields), fields) + t.Fatalf("failed to match rule variable REQUEST_HEADERS:host, %d matches, %v", len(fields), fields) } rv.Exceptions = []ruleVariableException{ { @@ -993,10 +1091,10 @@ func TestTxVariablesExceptions(t *testing.T) { } fields = tx.GetField(rv) if len(fields) != 0 { - t.Errorf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) + t.Fatalf("REQUEST_HEADERS:host should not match, got %d matches, %v", len(fields), fields) } if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } } @@ -1010,11 +1108,11 @@ func TestTransactionSyncPool(t *testing.T) { }) for i := 0; i < 1000; i++ { if err := tx.Close(); err != nil { - t.Error(err) + t.Fatal(err) } tx = waf.NewTransaction() if len(tx.matchedRules) != 0 { - t.Errorf("failed to sync transaction pool, %d rules found after %d attempts", len(tx.matchedRules), i+1) + t.Fatalf("failed to sync transaction pool, %d rules found after %d attempts", len(tx.matchedRules), i+1) return } } @@ -1032,16 +1130,16 @@ func TestTxPhase4Magic(t *testing.T) { _, _ = tx.ProcessRequestBody() tx.ProcessResponseHeaders(200, "HTTP/1.1") if it, _, err := tx.WriteResponseBody([]byte("more bytes")); it != nil || err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessResponseBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.outboundDataError.Get() != "1" { - t.Error("failed to set outbound data error") + t.Fatal("failed to set outbound data error") } if tx.variables.responseBody.Get() != "mor" { - t.Error("failed to set response body") + t.Fatal("failed to set response body") } } @@ -1060,12 +1158,16 @@ func TestVariablesMatch(t *testing.T) { for k, v := range expect { if m := (tx.Collection(k)).(*collections.Single).Get(); m != v { - t.Errorf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m) + t.Fatalf("failed to match variable %s, Expected: %s, got: %s", k.Name(), v, m) } } if len(tx.variables.matchedVars.Get("ARGS_NAMES:sample")) == 0 { - t.Errorf("failed to match variable %s, got 0", variables.MatchedVars.Name()) + t.Fatalf("failed to match variable %s, got 0", variables.MatchedVars.Name()) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1076,13 +1178,17 @@ func TestTxReqBodyForce(t *testing.T) { tx.RequestBodyAccess = true tx.ForceRequestBodyVariable = true if _, err := tx.requestBodyBuffer.Write([]byte("test")); err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.requestBody.Get() != "test" { - t.Error("failed to set request body") + t.Fatal("failed to set request body") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1092,13 +1198,17 @@ func TestTxReqBodyForceNegative(t *testing.T) { tx.RequestBodyAccess = true tx.ForceRequestBodyVariable = false if _, err := tx.requestBodyBuffer.Write([]byte("test")); err != nil { - t.Error(err) + t.Fatal(err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Error(err) + t.Fatal(err) } if tx.variables.requestBody.Get() == "test" { - t.Error("reqbody should not be there") + t.Fatal("reqbody should not be there") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1107,10 +1217,14 @@ func TestTxProcessConnection(t *testing.T) { tx := waf.NewTransaction() tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080) if tx.variables.remoteAddr.Get() != "127.0.0.1" { - t.Error("failed to set client ip") + t.Fatal("failed to set client ip") } if rp, _ := strconv.Atoi(tx.variables.remotePort.Get()); rp != 80 { - t.Error("failed to set client port") + t.Fatal("failed to set client port") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1125,7 +1239,7 @@ func TestTxSetServerName(t *testing.T) { tx.lastPhase = types.PhaseRequestHeaders tx.SetServerName("coraza.io") if tx.variables.serverName.Get() != "coraza.io" { - t.Error("failed to set server name") + t.Fatal("failed to set server name") } logEntries := strings.Split(strings.TrimSpace(logBuffer.String()), "\n") if want, have := 1, len(logEntries); want != have { @@ -1135,6 +1249,10 @@ func TestTxSetServerName(t *testing.T) { if want, have := "SetServerName has been called after ProcessRequestHeaders", logEntries[0]; !strings.Contains(have, want) { t.Fatalf("unexpected message, want %q, have %q", want, have) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxAddArgument(t *testing.T) { @@ -1143,15 +1261,19 @@ func TestTxAddArgument(t *testing.T) { tx.ProcessConnection("127.0.0.1", 80, "127.0.0.2", 8080) tx.AddGetRequestArgument("test", "testvalue") if tx.variables.argsGet.Get("test")[0] != "testvalue" { - t.Error("failed to set args get") + t.Fatal("failed to set args get") } tx.AddPostRequestArgument("ptest", "ptestvalue") if tx.variables.argsPost.Get("ptest")[0] != "ptestvalue" { - t.Error("failed to set args post") + t.Fatal("failed to set args post") } tx.AddPathRequestArgument("ptest2", "ptestvalue") if tx.variables.argsPath.Get("ptest2")[0] != "ptestvalue" { - t.Error("failed to set args post") + t.Fatal("failed to set args post") + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1161,7 +1283,11 @@ func TestTxGetField(t *testing.T) { Variable: variables.Args, } if f := tx.GetField(rvp); len(f) != 3 { - t.Errorf("failed to get field, expected 2, got %d", len(f)) + t.Fatalf("failed to get field, expected 2, got %d", len(f)) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1171,19 +1297,23 @@ func TestTxProcessURI(t *testing.T) { uri := "http://example.com/path/to/file.html?query=string&other=value" tx.ProcessURI(uri, "GET", "HTTP/1.1") if s := tx.variables.requestURI.Get(); s != uri { - t.Errorf("failed to set request uri, got %s", s) + t.Fatalf("failed to set request uri, got %s", s) } if s := tx.variables.requestBasename.Get(); s != "file.html" { - t.Errorf("failed to set request path, got %s", s) + t.Fatalf("failed to set request path, got %s", s) } if tx.variables.queryString.Get() != "query=string&other=value" { - t.Error("failed to set request query") + t.Fatal("failed to set request query") } if v := tx.variables.args.FindAll(); len(v) != 2 { - t.Errorf("failed to set request args, got %d", len(v)) + t.Fatalf("failed to set request args, got %d", len(v)) } if v := tx.variables.args.FindString("other"); v[0].Value() != "value" { - t.Errorf("failed to set request args, got %v", v) + t.Fatalf("failed to set request args, got %v", v) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) } } @@ -1259,7 +1389,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing for k, v := range tests { m, err := macro.NewMacro(k) if err != nil { - t.Error(err) + t.Fatal(err) } res := m.Expand(tx) if res != v { @@ -1267,7 +1397,7 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing fmt.Println(tx) fmt.Println("===STACK===\n", string(debug.Stack())+"\n===STACK===") } - t.Error("Failed set transaction for " + k + ", expected " + v + ", got " + res) + t.Fatal("Failed set transaction for " + k + ", expected " + v + ", got " + res) } } } @@ -1277,28 +1407,32 @@ func TestMacro(t *testing.T) { tx.variables.tx.Set("some", []string{"secretly"}) m, err := macro.NewMacro("%{unique_id}") if err != nil { - t.Error(err) + t.Fatal(err) } if m.Expand(tx) != tx.id { - t.Errorf("%s != %s", m.Expand(tx), tx.id) + t.Fatalf("%s != %s", m.Expand(tx), tx.id) } m, err = macro.NewMacro("some complex text %{tx.some} wrapped in m") if err != nil { - t.Error(err) + t.Fatal(err) } if m.Expand(tx) != "some complex text secretly wrapped in m" { - t.Errorf("failed to expand m, got %s\n%v", m.Expand(tx), m) + t.Fatalf("failed to expand m, got %s\n%v", m.Expand(tx), m) } _, err = macro.NewMacro("some complex text %{tx.some} wrapped in m %{tx.some}") if err != nil { - t.Error(err) + t.Fatal(err) return } // TODO(anuraaga): Decouple this test from transaction implementation. // if !macro.IsExpandable() || len(macro.tokens) != 4 || macro.Expand(tx) != "some complex text secretly wrapped in m secretly" { - // t.Errorf("failed to parse replacements %v", macro.tokens) + // t.Fatalf("failed to parse replacements %v", macro.tokens) // } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func BenchmarkMacro(b *testing.B) { @@ -1387,6 +1521,10 @@ func TestProcessorsIdempotencyWithAlreadyRaisedInterruption(t *testing.T) { } }) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestIterationStops(t *testing.T) { @@ -1412,16 +1550,20 @@ func TestIterationStops(t *testing.T) { }) if want, have := i+1, len(haveVars); want != have { - t.Errorf("stopped with unexpected number of variables, want %d, have %d", want, have) + t.Fatalf("stopped with unexpected number of variables, want %d, have %d", want, have) } for j, v := range haveVars { if want, have := allVars[j], v; want != have { - t.Errorf("unexpected variable at index %d, want %s, have %s", j, want.Name(), have.Name()) + t.Fatalf("unexpected variable at index %d, want %s, have %s", j, want.Name(), have.Name()) } } }) } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestTxAddResponseArgs(t *testing.T) { @@ -1429,7 +1571,7 @@ func TestTxAddResponseArgs(t *testing.T) { tx := waf.NewTransaction() tx.AddResponseArgument("samplekey", "samplevalue") if tx.variables.responseArgs.Get("samplekey")[0] != "samplevalue" { - t.Errorf("failed to add response argument") + t.Fatalf("failed to add response argument") } } @@ -1446,6 +1588,10 @@ func TestAddGetArgsWithOverlimit(t *testing.T) { if tx.variables.argsGet.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add get args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1462,6 +1608,10 @@ func TestAddPostArgsWithOverlimit(t *testing.T) { if tx.variables.argsPost.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add post args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1478,6 +1628,10 @@ func TestAddPathArgsWithOverlimit(t *testing.T) { if tx.variables.argsPath.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add path args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1494,6 +1648,10 @@ func TestAddResponseArgsWithOverlimit(t *testing.T) { if tx.variables.responseArgs.Len() > waf.ArgumentLimit { t.Fatal("Argument limit is failed while add response args") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } } @@ -1518,6 +1676,10 @@ func TestResponseBodyForceProcessing(t *testing.T) { if len(f) == 0 { t.Fatal("json.key not found") } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } } func TestForceRequestBodyOverride(t *testing.T) { @@ -1528,24 +1690,43 @@ func TestForceRequestBodyOverride(t *testing.T) { tx.variables.RequestBodyProcessor().(*collections.Single).Set("JSON") tx.ProcessRequestHeaders() if _, _, err := tx.WriteRequestBody([]byte("foo=bar&baz=qux")); err != nil { - t.Errorf("Failed to write request body: %v", err) + t.Fatalf("Failed to write request body: %v", err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Errorf("Failed to process request body: %v", err) + t.Fatalf("Failed to process request body: %v", err) } if tx.variables.RequestBodyProcessor().Get() != "JSON" { - t.Errorf("Failed to force request body variable") + t.Fatalf("Failed to force request body variable") } tx = waf.NewTransaction() tx.ForceRequestBodyVariable = true tx.ProcessRequestHeaders() if _, _, err := tx.WriteRequestBody([]byte("foo=bar&baz=qux")); err != nil { - t.Errorf("Failed to write request body: %v", err) + t.Fatalf("Failed to write request body: %v", err) } if _, err := tx.ProcessRequestBody(); err != nil { - t.Errorf("Failed to process request body: %v", err) + t.Fatalf("Failed to process request body: %v", err) } if tx.variables.RequestBodyProcessor().Get() != "URLENCODED" { - t.Errorf("Failed to force request body variable, got RBP: %q", tx.variables.RequestBodyProcessor().Get()) + t.Fatalf("Failed to force request body variable, got RBP: %q", tx.variables.RequestBodyProcessor().Get()) + } + + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } +} + +func TestCloseFails(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + col := tx.Variables().FilesTmpNames().(*collections.Map) + col.Add("", "unexisting") + err := tx.Close() + if err == nil { + t.Fatalf("expected error when closing transaction") + } + + if !strings.Contains(err.Error(), "removing temporary file") { + t.Fatalf("unexpected error message: %s", err.Error()) } } diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go index 582e70cf1..6e76a0588 100644 --- a/internal/corazawaf/waf.go +++ b/internal/corazawaf/waf.go @@ -4,6 +4,7 @@ package corazawaf import ( + "context" "errors" "fmt" "io" @@ -11,7 +12,6 @@ import ( "os" "regexp" "strconv" - "strings" "time" "github.com/crowdsecurity/coraza/v3/debuglog" @@ -91,6 +91,7 @@ type WAF struct { UploadDir string // Request body in memory limit excluding the size of any files being transported in the request. + // TODO: SecRequestBodyNoFilesLimit directive is retrieving the value, but no logic based on it is implemented. See https://github.com/corazawaf/coraza/issues/896 RequestBodyNoFilesLimit int64 RequestBodyLimitAction types.BodyLimitAction @@ -133,24 +134,40 @@ type WAF struct { ArgumentLimit int } +// Options is used to pass options to the WAF instance +type Options struct { + ID string + Context context.Context +} + // NewTransaction Creates a new initialized transaction for this WAF instance func (w *WAF) NewTransaction() *Transaction { - return w.newTransactionWithID(stringutils.RandomString(19)) + return w.newTransaction(Options{ + ID: stringutils.RandomString(19), + Context: context.Background(), + }) } -func (w *WAF) NewTransactionWithID(id string) *Transaction { - if len(strings.TrimSpace(id)) == 0 { - id = stringutils.RandomString(19) - w.Logger.Warn().Msg("Empty ID passed for new transaction") +// NewTransactionWithOptions Creates a new initialized transaction for this WAF +// instance with the provided options +func (w *WAF) NewTransactionWithOptions(opts Options) *Transaction { + if opts.ID == "" { + opts.ID = stringutils.RandomString(19) } - return w.newTransactionWithID(id) + + if opts.Context == nil { + opts.Context = context.Background() + } + + return w.newTransaction(opts) } // NewTransactionWithID Creates a new initialized transaction for this WAF instance // Using the specified ID -func (w *WAF) newTransactionWithID(id string) *Transaction { +func (w *WAF) newTransaction(opts Options) *Transaction { tx := w.txPool.Get().(*Transaction) - tx.id = id + tx.id = opts.ID + tx.context = opts.Context tx.matchedRules = []types.MatchedRule{} tx.interruption = nil tx.Logdata = "" // Deprecated, this variable is not used. Logdata for each matched rule is stored in the MatchData field. @@ -180,7 +197,7 @@ func (w *WAF) newTransactionWithID(id string) *Transaction { // Always non-nil if buffers / collections were already initialized so we don't do any of them // based on the presence of RequestBodyBuffer. if tx.requestBodyBuffer == nil { - // if no requestBodyInMemoryLimit has been set we default to the + // if no requestBodyInMemoryLimit has been set we default to the requestBodyLimit var requestBodyInMemoryLimit int64 = w.RequestBodyLimit if w.requestBodyInMemoryLimit != nil { requestBodyInMemoryLimit = int64(*w.requestBodyInMemoryLimit) @@ -222,7 +239,7 @@ func (w *WAF) newTransactionWithID(id string) *Transaction { tx.variables.highestSeverity.Set("0") tx.variables.uniqueID.Set(tx.id) - w.Logger.Debug().Msg("New transaction created") + tx.debugLogger.Debug().Msg("Transaction started") return tx } @@ -275,9 +292,10 @@ func NewWAF() *WAF { // These defaults are unavoidable as they are zero values for the variables RuleEngine: types.RuleEngineOn, RequestBodyAccess: false, - RequestBodyLimit: _1gb, + RequestBodyLimit: 134217728, // Hard limit equal to _1gb + RequestBodyLimitAction: types.BodyLimitActionReject, ResponseBodyAccess: false, - ResponseBodyLimit: _1gb, + ResponseBodyLimit: 524288, // Hard limit equal to _1gb auditLogWriter: logWriter, auditLogWriterInitialized: false, AuditLogWriterConfig: auditlog.NewConfig(), diff --git a/internal/corazawaf/waf_test.go b/internal/corazawaf/waf_test.go index 883c09337..771667c35 100644 --- a/internal/corazawaf/waf_test.go +++ b/internal/corazawaf/waf_test.go @@ -15,7 +15,7 @@ func TestNewTransaction(t *testing.T) { waf.ResponseBodyAccess = true waf.RequestBodyLimit = 1044 - tx := waf.NewTransactionWithID("test") + tx := waf.NewTransactionWithOptions(Options{ID: "test"}) if !tx.RequestBodyAccess { t.Error("Request body access not enabled") } @@ -28,7 +28,7 @@ func TestNewTransaction(t *testing.T) { if tx.id != "test" { t.Error("ID not set") } - tx = waf.NewTransactionWithID("") + tx = waf.NewTransactionWithOptions(Options{ID: ""}) if tx.id == "" { t.Error("ID not set") } diff --git a/internal/environment/default.go b/internal/environment/default.go index 522045b71..afdc35a69 100644 --- a/internal/environment/default.go +++ b/internal/environment/default.go @@ -6,6 +6,24 @@ package environment +import ( + "fmt" + "os" +) + // HasAccessToFS indicates whether the runtime target environment has access // to OS' filesystem or not. var HasAccessToFS = true + +// IsDirWritable is a helper function to check if the WAF has access to the filesystem +func IsDirWritable(dir string) error { + file, err := os.CreateTemp(dir, "checkfsfile") + if err != nil { + return fmt.Errorf("create file: %w", err) + } + defer func() { + file.Close() + os.Remove(file.Name()) + }() + return nil +} diff --git a/internal/environment/default_test.go b/internal/environment/default_test.go new file mode 100644 index 000000000..c57f1314a --- /dev/null +++ b/internal/environment/default_test.go @@ -0,0 +1,45 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build !no_fs_access +// +build !no_fs_access + +package environment + +import ( + "os" + "testing" +) + +func TestFSCheck(t *testing.T) { + testCases := []struct { + name string + hasAccessToFS bool + tmpDir string + expectError bool + }{ + { + name: "Has access to FS, non-existent dir", + tmpDir: "/non-existent-dir", + expectError: true, + }, + { + name: "Has access to FS, existent dir", + tmpDir: os.TempDir(), + expectError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + os.Setenv("TMPDIR", tc.tmpDir) + err := IsDirWritable(tc.tmpDir) + if tc.expectError && err == nil { + t.Fatal("expected error, got nil") + } + if !tc.expectError && err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + } +} diff --git a/internal/environment/nofsaccess.go b/internal/environment/nofsaccess.go index e1bbaab20..662ae4220 100644 --- a/internal/environment/nofsaccess.go +++ b/internal/environment/nofsaccess.go @@ -7,3 +7,9 @@ package environment var HasAccessToFS = false + +// IsDirWritable is a helper function to check if the WAF has access to the filesystem +// It is unexpected to call this function when no_fs_access build tag is enabled +func IsDirWritable(dir string) error { + panic("Unexpected call to IsDirWritable with no_fs_access build tag") +} diff --git a/internal/operators/eq_test.go b/internal/operators/eq_test.go new file mode 100644 index 000000000..ce4cdbb2c --- /dev/null +++ b/internal/operators/eq_test.go @@ -0,0 +1,53 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package operators + +import ( + "testing" + + "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" +) + +func TestEq(t *testing.T) { + t.Run("test invalid values return 0", func(t *testing.T) { + eq, _ := newEq(plugintypes.OperatorOptions{ + Arguments: "a", + }) + + testCases := map[string]bool{ + "a": true, + "b": true, + "0": true, + "1": false, + } + + for value, want := range testCases { + t.Run(value, func(t *testing.T) { + if have := eq.Evaluate(nil, value); want != have { + t.Errorf("unexpected result: want %v, have %v", want, have) + } + }) + } + }) + + t.Run("test valid values", func(t *testing.T) { + eq, _ := newEq(plugintypes.OperatorOptions{ + Arguments: "1", + }) + + testCases := map[string]bool{ + "1": true, + "01": true, + "1.0": false, + } + + for value, want := range testCases { + t.Run(value, func(t *testing.T) { + if have := eq.Evaluate(nil, value); want != have { + t.Errorf("unexpected result: want %v, have %v", want, have) + } + }) + } + }) +} diff --git a/internal/operators/inspect_file_test.go b/internal/operators/inspect_file_test.go index 0fcd79959..8fc92739a 100644 --- a/internal/operators/inspect_file_test.go +++ b/internal/operators/inspect_file_test.go @@ -9,19 +9,24 @@ package operators import ( _ "fmt" "path/filepath" + "runtime" "testing" "github.com/crowdsecurity/coraza/v3/experimental/plugins/plugintypes" ) func TestInspectFile(t *testing.T) { + existCommand := "/bin/echo" + if runtime.GOOS == "windows" { + existCommand = "C:\\Windows\\system32\\tasklist.exe" + } + tests := []struct { path string exists bool }{ { - // TODO(anuraaga): Don't have this rely on OS details. - path: "/bin/echo", + path: existCommand, exists: true, }, { @@ -37,7 +42,7 @@ func TestInspectFile(t *testing.T) { if err != nil { t.Error("cannot init inspectfile operator") } - if want, have := tt.exists, ipf.Evaluate(nil, ""); want != have { + if want, have := tt.exists, ipf.Evaluate(nil, "/?"); want != have { t.Errorf("inspectfile path %s: want %v, have %v", tt.path, want, have) } }) diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go index cbfe43848..f1fc5f2a4 100644 --- a/internal/seclang/directives.go +++ b/internal/seclang/directives.go @@ -16,6 +16,7 @@ import ( "github.com/crowdsecurity/coraza/v3/debuglog" "github.com/crowdsecurity/coraza/v3/internal/auditlog" "github.com/crowdsecurity/coraza/v3/internal/corazawaf" + "github.com/crowdsecurity/coraza/v3/internal/environment" "github.com/crowdsecurity/coraza/v3/internal/memoize" utils "github.com/crowdsecurity/coraza/v3/internal/strings" "github.com/crowdsecurity/coraza/v3/types" @@ -109,6 +110,7 @@ func directiveSecMarker(options *DirectiveOptions) error { rule.Raw_ = fmt.Sprintf("SecMarker %s", options.Opts) rule.SecMark_ = options.Opts rule.ID_ = 0 + rule.LogID_ = "0" rule.Phase_ = 0 rule.Line_ = options.Parser.LastLine rule.File_ = options.Parser.ConfigFile @@ -229,7 +231,7 @@ func directiveSecResponseBodyAccess(options *DirectiveOptions) error { } // Description: Configures the maximum request body size Coraza will accept for buffering. -// Default: 134217728 (131072 KB) +// Default: 134217728 (128 Mib) // Syntax: SecRequestBodyLimit [LIMIT_IN_BYTES] // --- // Anything over the limit will be rejected with status code 413 (Request Entity Too Large). @@ -306,6 +308,19 @@ func directiveSecServerSignature(options *DirectiveOptions) error { return nil } +// Description: Removes the matching rules from the current configuration context. +// Syntax: SecRuleRemoveByTag [TAG] +// --- +// Normally, you would use `SecRuleRemoveById` to remove rules, but it may occasionally +// be easier to disable an entire group of rules with `SecRuleRemoveByTag`. Matching is +// by case-sensitive string equality. +// +// Example: +// ```apache +// SecRuleRemoveByTag attack-dos +// ``` +// +// Note: OWASP CRS has a list of supported tags https://coreruleset.org/docs/rules/metadata/ func directiveSecRuleRemoveByTag(options *DirectiveOptions) error { if len(options.Opts) == 0 { return errEmptyOptions @@ -341,6 +356,9 @@ func directiveSecRuleRemoveByID(options *DirectiveOptions) error { options.WAF.Rules.DeleteByID(id) } else { + if idx == 0 { + return fmt.Errorf("SecRuleUpdateTargetById: invalid negative id: %s", idOrRange) + } start, err := strconv.Atoi(idOrRange[:idx]) if err != nil { return err @@ -410,7 +428,7 @@ func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error { // Description: Configures the maximum response body size that will be accepted for buffering. // Syntax: SecResponseBodyLimit [LIMIT_IN_BYTES] -// Default: 524288 (512 KB) +// Default: 524288 (512 Kib) // --- // Anything over this limit will be rejected with status code 500 (Internal Server Error). // This setting will not affect the responses with MIME types that are not selected for @@ -448,7 +466,7 @@ func directiveSecRequestBodyLimitAction(options *DirectiveOptions) error { } // Description: Configures the maximum request body size that Coraza will store in memory. -// Default: 131072 (128 KB) +// Default: defaults to RequestBodyLimit // Syntax: SecRequestBodyInMemoryLimit [LIMIT_IN_BYTES] // --- // When a `multipart/form-data` request is being processed, once the in-memory limit is reached, @@ -871,7 +889,13 @@ func directiveSecUploadDir(options *DirectiveOptions) error { return errEmptyOptions } - // TODO validations + if environment.HasAccessToFS { + if err := environment.IsDirWritable(options.Opts); err != nil { + return fmt.Errorf("filesystem access check: %w. Check SecUploadDir provided dir: %s", err, options.Opts) + } + } else { + return fmt.Errorf("SecUploadDir directive is not effective because of no access to the filesystem") + } options.WAF.UploadDir = options.Opts return nil } @@ -890,6 +914,7 @@ func directiveSecUploadDir(options *DirectiveOptions) error { // Generally speaking, the default value is not small enough. For most applications, you // should be able to reduce it down to 128 KB or lower. Anything over the limit will be // rejected with status code 413 (Request Entity Too Large). There is a hard limit of 1 GB. +// Note: not implemented yet func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error { if len(options.Opts) == 0 { return errEmptyOptions @@ -938,22 +963,111 @@ func directiveSecDebugLogLevel(options *DirectiveOptions) error { return options.WAF.SetDebugLogLevel(debuglog.Level(lvl)) } +// Description: Updates the target (variable) list of the specified rule(s). +// Syntax: SecRuleUpdateTargetById ID TARGET1[|TARGET2|TARGET3] +// --- +// This directive will append variables to the specified rule with the targets provided in the second parameter. +// The rule ID can be single IDs or ranges of IDs. The targets are separated by a pipe character. func directiveSecRuleUpdateTargetByID(options *DirectiveOptions) error { - idStr, v, ok := strings.Cut(options.Opts, " ") - if !ok { + if len(options.Opts) == 0 { + return errEmptyOptions + } + + idsOrRanges := strings.Fields(options.Opts) + length := len(idsOrRanges) + if length < 2 { return errors.New("syntax error: SecRuleUpdateTargetById id \"VARIABLES\"") } - id, err := strconv.Atoi(idStr) - if err != nil { - return err + // The last element is expected to be the variable(s) + variables := idsOrRanges[length-1] + for _, idOrRange := range idsOrRanges[:length-1] { + if idx := strings.Index(idOrRange, "-"); idx == -1 { + id, err := strconv.Atoi(idOrRange) + if err != nil { + return err + } + return updateTargetBySingleID(id, variables, options) + } else { + if idx == 0 { + return fmt.Errorf("SecRuleUpdateTargetById: invalid negative id: %s", idOrRange) + } + start, err := strconv.Atoi(idOrRange[:idx]) + if err != nil { + return err + } + + end, err := strconv.Atoi(idOrRange[idx+1:]) + if err != nil { + return err + } + if start == end { + return updateTargetBySingleID(start, variables, options) + } + if start > end { + return fmt.Errorf("invalid range: %s", idOrRange) + } + + for _, rule := range options.WAF.Rules.GetRules() { + if rule.ID_ >= start && rule.ID_ <= end { + rp := RuleParser{ + rule: &rule, + options: RuleOptions{}, + defaultActions: map[types.RulePhase][]ruleAction{}, + } + if err := rp.ParseVariables(strings.Trim(variables, "\"")); err != nil { + return err + } + } + } + } } + return nil +} + +func updateTargetBySingleID(id int, variables string, options *DirectiveOptions) error { + rule := options.WAF.Rules.FindByID(id) + if rule == nil { + return fmt.Errorf("SecRuleUpdateTargetById: rule \"%d\" not found", id) + } rp := RuleParser{ rule: rule, options: RuleOptions{}, defaultActions: map[types.RulePhase][]ruleAction{}, } - return rp.ParseVariables(strings.Trim(v, "\"")) + return rp.ParseVariables(strings.Trim(variables, "\"")) +} + +// Description: Updates the target (variable) list of the specified rule(s) by tag. +// Syntax: SecRuleUpdateTargetByTag TAG TARGET1[|TARGET2|TARGET3] +// --- +// As an alternative to `SecRuleUpdateTargetById`, this directive will append variables to the specified rule +// with the targets provided in the second parameter. It can be handy for updating an entire group of rules. +// Matching is by case-sensitive string equality. +// This directive will append variables to the specified rule with the targets provided in the second parameter. +// The rule ID can be single IDs or ranges of IDs. The targets are separated by a pipe character. +// Note: OWASP CRS has a list of supported tags https://coreruleset.org/docs/rules/metadata/ +func directiveSecRuleUpdateTargetByTag(options *DirectiveOptions) error { + tagAndvars := strings.Fields(options.Opts) + if len(tagAndvars) != 2 { + return errors.New("syntax error: SecRuleUpdateTargetByTag tag \"VARIABLES\"") + } + + for _, rule := range options.WAF.Rules.GetRules() { + inputTag := strings.Trim(tagAndvars[0], "\"") + if utils.InSlice(inputTag, rule.Tags_) { + rp := RuleParser{ + rule: &rule, + options: RuleOptions{}, + defaultActions: map[types.RulePhase][]ruleAction{}, + } + inputVars := strings.Trim(tagAndvars[1], "\"") + if err := rp.ParseVariables(inputVars); err != nil { + return err + } + } + } + return nil } func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error { diff --git a/internal/seclang/directives_test.go b/internal/seclang/directives_test.go index ef182c431..2fa7af6dd 100644 --- a/internal/seclang/directives_test.go +++ b/internal/seclang/directives_test.go @@ -4,6 +4,8 @@ package seclang import ( + "os" + "regexp" "strings" "testing" @@ -34,7 +36,7 @@ func Test_NonImplementedDirective(t *testing.T) { } } -func TestSecRuleUpdateTargetBy(t *testing.T) { +func TestSecRuleUpdateTargetByID(t *testing.T) { waf := corazawaf.NewWAF() rule, err := ParseRule(RuleOptions{ Data: "REQUEST_URI \"^/test\" \"id:181,tag:test\"", @@ -140,7 +142,8 @@ func TestDirectives(t *testing.T) { }, "SecUploadDir": { {"", expectErrorOnDirective}, - {"/tmp", func(w *corazawaf.WAF) bool { return w.UploadDir == "/tmp" }}, + {"/tmp-non-existing", expectErrorOnDirective}, + {os.TempDir(), func(w *corazawaf.WAF) bool { return w.UploadDir == os.TempDir() }}, }, "SecSensorId": { {"", expectErrorOnDirective}, @@ -158,6 +161,7 @@ func TestDirectives(t *testing.T) { }, "SecRuleRemoveByTag": { {"", expectErrorOnDirective}, + {"attack-sqli", expectNoErrorOnDirective}, }, "SecRuleRemoveByMsg": { {"", expectErrorOnDirective}, @@ -168,10 +172,41 @@ func TestDirectives(t *testing.T) { {"1-a", expectErrorOnDirective}, {"a-2", expectErrorOnDirective}, {"2-1", expectErrorOnDirective}, + {"-1", expectErrorOnDirective}, + {"-5--1", expectErrorOnDirective}, + {"5--1", expectErrorOnDirective}, {"1", expectNoErrorOnDirective}, {"1 2", expectNoErrorOnDirective}, {"1 2 3-4", expectNoErrorOnDirective}, }, + "SecRuleUpdateTargetById": { + {"", expectErrorOnDirective}, + {"a", expectErrorOnDirective}, + {"1-a", expectErrorOnDirective}, + {"a-2", expectErrorOnDirective}, + {"2-1", expectErrorOnDirective}, + {"1-a \"ARGS:wp_post\"", expectErrorOnDirective}, + {"a-2 \"ARGS:wp_post\"", expectErrorOnDirective}, + {"2-1 \"ARGS:wp_post\"", expectErrorOnDirective}, + {"-1 \"ARGS:wp_post\"", expectErrorOnDirective}, + {"-5--1 \"ARGS:wp_post\"", expectErrorOnDirective}, + {"5--1 \"ARGS:wp_post\"", expectErrorOnDirective}, + // Variables has also to be provided to the directive + {"1", expectErrorOnDirective}, + {"1 \"ARGS:wp_post\"", expectNoErrorOnDirective}, + {"7-7 \"ARGS:wp_post\"", expectNoErrorOnDirective}, + {"1 2 \"ARGS:wp_post\"", expectNoErrorOnDirective}, + {"1 2 3-4 \"ARGS:wp_post\"", expectNoErrorOnDirective}, + {"1 \"REQUEST_BODY|ARGS:wp_post\"", expectNoErrorOnDirective}, + {"1 2 3-4 \"ARGS:wp_post|RESPONSE_HEADERS\"", expectNoErrorOnDirective}, + }, + "SecRuleUpdateTargetByTag": { + {"", expectErrorOnDirective}, + {"a", expectErrorOnDirective}, + {"tag-1 \"ARGS:wp_post\"", expectNoErrorOnDirective}, + {"tag-1 tag-2 \"ARGS:wp_post\"", expectErrorOnDirective}, // Multiple tags in line is not supported + {"tag-2 \"ARGS:wp_post|RESPONSE_HEADERS|!REQUEST_BODY\"", expectNoErrorOnDirective}, + }, "SecResponseBodyMimeTypesClear": { {"", func(w *corazawaf.WAF) bool { return len(w.ResponseBodyMimeTypes) == 0 }}, {"x", expectErrorOnDirective}, @@ -262,7 +297,12 @@ func TestDirectives(t *testing.T) { } } else { if err != nil { - t.Errorf("unexpected error: %s", err.Error()) + match, _ := regexp.MatchString(`rule "\d+" not found`, err.Error()) + // Logical errors are not checked by this test, therefore this specific pattern is allowed here + if !match { + // Syntax errors are checked + t.Errorf("unexpected error: %s", err.Error()) + } } if !tCase.check(waf) { diff --git a/internal/seclang/directivesmap.gen.go b/internal/seclang/directivesmap.gen.go index 2114ef287..9e6fc118d 100644 --- a/internal/seclang/directivesmap.gen.go +++ b/internal/seclang/directivesmap.gen.go @@ -60,6 +60,7 @@ var ( _ directive = directiveSecDebugLog _ directive = directiveSecDebugLogLevel _ directive = directiveSecRuleUpdateTargetByID + _ directive = directiveSecRuleUpdateTargetByTag _ directive = directiveSecIgnoreRuleCompilationErrors _ directive = directiveSecDataset _ directive = directiveSecArgumentsLimit @@ -120,6 +121,7 @@ var directivesMap = map[string]directive{ "secdebuglog": directiveSecDebugLog, "secdebugloglevel": directiveSecDebugLogLevel, "secruleupdatetargetbyid": directiveSecRuleUpdateTargetByID, + "secruleupdatetargetbytag": directiveSecRuleUpdateTargetByTag, "secignorerulecompilationerrors": directiveSecIgnoreRuleCompilationErrors, "secdataset": directiveSecDataset, "secargumentslimit": directiveSecArgumentsLimit, @@ -127,7 +129,6 @@ var directivesMap = map[string]directive{ // Unsupported directives "secargumentseparator": directiveUnsupported, "seccookieformat": directiveUnsupported, - "secruleupdatetargetbytag": directiveUnsupported, "secruleupdatetargetbymsg": directiveUnsupported, "secruleupdateactionbyid": directiveUnsupported, "secrulescript": directiveUnsupported, diff --git a/internal/seclang/generator/directivesmap.go.tmpl b/internal/seclang/generator/directivesmap.go.tmpl index 6724515d6..0e2956acf 100644 --- a/internal/seclang/generator/directivesmap.go.tmpl +++ b/internal/seclang/generator/directivesmap.go.tmpl @@ -16,7 +16,6 @@ var directivesMap = map[string]directive{ // Unsupported directives "secargumentseparator": directiveUnsupported, "seccookieformat": directiveUnsupported, - "secruleupdatetargetbytag": directiveUnsupported, "secruleupdatetargetbymsg": directiveUnsupported, "secruleupdateactionbyid": directiveUnsupported, "secrulescript": directiveUnsupported, diff --git a/internal/seclang/parser.go b/internal/seclang/parser.go index 8388fe541..09c16de2e 100644 --- a/internal/seclang/parser.go +++ b/internal/seclang/parser.go @@ -64,7 +64,7 @@ func (p *Parser) FromFile(profilePath string) error { return fmt.Errorf("failed to readfile: %s", err.Error()) } - err = p.FromString(string(file)) + err = p.parseString(string(file)) if err != nil { // we don't use defer for this as tinygo does not seem to like it p.currentDir = originalDir @@ -85,6 +85,14 @@ func (p *Parser) FromFile(profilePath string) error { // It will return error if any directive fails to parse // or arguments are invalid func (p *Parser) FromString(data string) error { + oldCurrentFile := p.currentFile + p.currentFile = "_inline_" + err := p.parseString(data) + p.currentFile = oldCurrentFile + return err +} + +func (p *Parser) parseString(data string) error { scanner := bufio.NewScanner(strings.NewReader(data)) var linebuffer strings.Builder inBackticks := false diff --git a/internal/seclang/rule_parser.go b/internal/seclang/rule_parser.go index b9441edf8..829d32c76 100644 --- a/internal/seclang/rule_parser.go +++ b/internal/seclang/rule_parser.go @@ -145,6 +145,9 @@ func (rp *RuleParser) ParseVariables(vars string) error { isEscaped = !isEscaped default: curKey = append(curKey, c) + if isEscaped { + isEscaped = false + } } case 3: // XPATH @@ -379,12 +382,14 @@ func ParseRule(options RuleOptions) (*corazawaf.Rule, error) { } } rule := rp.Rule() - rule.Raw_ = options.Raw rule.File_ = options.ParserConfig.ConfigFile rule.Line_ = options.ParserConfig.LastLine if parent := getLastRuleExpectingChain(options.WAF); parent != nil { rule.ParentID_ = parent.ID_ + // While the ID_ will be kept to 0 being a chain rule, the LogID_ is meant to be + // the printable ID that represents the chain rule, therefore the parent's ID is inherited. + rule.LogID_ = parent.LogID_ lastChain := parent for lastChain.Chain != nil { lastChain = lastChain.Chain @@ -392,7 +397,12 @@ func ParseRule(options RuleOptions) (*corazawaf.Rule, error) { // TODO we must remove defaultactions from chains rule.Phase_ = 0 lastChain.Chain = rule + // This way we store the raw rule in the parent + parent.Raw_ += " \n" + options.Raw return nil, nil + } else { + // we only want Raw for the parent + rule.Raw_ = options.Raw } return rule, nil } diff --git a/internal/seclang/rule_parser_test.go b/internal/seclang/rule_parser_test.go index 7334f75b4..31526dc38 100644 --- a/internal/seclang/rule_parser_test.go +++ b/internal/seclang/rule_parser_test.go @@ -5,6 +5,7 @@ package seclang import ( "errors" + "reflect" "strings" "testing" @@ -120,7 +121,7 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) { SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:9,phase:2" SecRuleUpdateTargetById 99 "!REQUEST_HEADERS:xyz" `) - expectedErr = errors.New("cannot create a variable exception for an undefined rule") + expectedErr = errors.New("SecRuleUpdateTargetById: rule \"99\" not found") if errors.Unwrap(err).Error() != expectedErr.Error() { t.Fatalf("unexpected error, want %q, have %q", expectedErr, errors.Unwrap(err).Error()) } @@ -251,6 +252,57 @@ func TestInvalidOperatorRuleData(t *testing.T) { } } +func TestRawChainedRules(t *testing.T) { + waf := corazawaf.NewWAF() + p := NewParser(waf) + if err := p.FromString(` + SecRule REQUEST_URI "abc" "id:7,phase:2,chain" + SecRule REQUEST_URI "def" "chain" + SecRule REQUEST_URI "ghi" "" + `); err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + raw := waf.Rules.GetRules()[0].Raw() + spl := strings.Split(raw, "\n") + if len(spl) != 3 { + t.Errorf("unexpected number of chained rules, want 3, have %d", len(spl)) + } + for i, r := range spl { + // we test that all lines begin with SecRule REQUEST_URI " + if !strings.HasPrefix(r, "SecRule REQUEST_URI ") { + t.Errorf("unexpected rule at line %d: %s", i, r) + } + } +} + +func TestParseRule(t *testing.T) { + tests := []struct { + name string + vars string + want int + }{ + {"Does not contain escape characters", `ARGS_GET:/(test)/|REQUEST_XML`, 2}, + {"The last variable contains escape characters", `ARGS_GET|REQUEST_XML:/(test)\b/`, 2}, + {"Contains escape characters", `ARGS_GET:/(test\b)/|REQUEST_XML`, 2}, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.name, func(t *testing.T) { + rp := RuleParser{ + rule: corazawaf.NewRule(), + } + if err := rp.ParseVariables(tt.vars); err != nil { + t.Error(err) + } + got := reflect.ValueOf(rp.rule).Elem().FieldByName("variables").Len() + if got != tt.want { + t.Error("variables parse error want", tt.want, "got", got) + } + }) + } +} + func BenchmarkParseActions(b *testing.B) { actionsToBeParsed := "id:980170,phase:5,pass,t:none,noauditlog,msg:'Anomaly Scores:Inbound Scores - Outbound Scores',tag:test" for i := 0; i < b.N; i++ { diff --git a/internal/seclang/rules_casesensitive_test.go b/internal/seclang/rules_casesensitive_test.go new file mode 100644 index 000000000..2046f66d8 --- /dev/null +++ b/internal/seclang/rules_casesensitive_test.go @@ -0,0 +1,133 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build coraza.rule.case_sensitive_args_keys + +package seclang + +import ( + "testing" + + "github.com/crowdsecurity/coraza/v3/internal/corazawaf" +) + +func TestCaseSensitiveRuleMatchRegex(t *testing.T) { + waf := corazawaf.NewWAF() + parser := NewParser(waf) + err := parser.FromString(` + SecRuleEngine On + SecRule ARGS:/^Key/ "@streq my-value" "id:1028,phase:1,deny,status:403,msg:'ARGS:key matched.'" + `) + if err != nil { + t.Error(err.Error()) + } + tx := waf.NewTransaction() + tx.ProcessURI("https://asdf.com/index.php?t1=aaa&T1=zzz&t2=bbb&t3=ccc&Keyless=my-value&a=test&jsessionid=74B0CB414BD77D17B5680A6386EF1666", "GET", "HTTP/1.1") + tx.ProcessConnection("127.0.0.1", 0, "", 0) + tx.ProcessRequestHeaders() + if len(tx.MatchedRules()) != 1 { + t.Errorf("failed to match rules with %d", len(tx.MatchedRules())) + } + if tx.Interruption() == nil { + t.Fatal("failed to interrupt transaction") + } +} + +func TestCaseSensitiveArguments(t *testing.T) { + waf := corazawaf.NewWAF() + rules := `SecRule ARGS:Test1 "Xyz" "id:3, phase:2, log, deny"` + parser := NewParser(waf) + + err := parser.FromString(rules) + if err != nil { + t.Error() + return + } + + tx := waf.NewTransaction() + tx.ProcessRequestHeaders() + tx.AddPostRequestArgument("Test1", "Xyz") + it, err := tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + if it == nil { + t.Errorf("failed to test arguments value match: Same case argument name, %+v\n", tx.MatchedRules()) + } + + tx = waf.NewTransaction() + tx.ProcessRequestHeaders() + tx.AddPostRequestArgument("TEST1", "Xyz") + it, err = tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + if it != nil { + t.Errorf("failed to test arguments value match: argument is matching a different case, %+v\n", tx.MatchedRules()) + } + + tx = waf.NewTransaction() + tx.ProcessRequestHeaders() + tx.AddPostRequestArgument("Test1", "XYZ") + it, err = tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + if it != nil { + t.Errorf("failed to test arguments value match: argument is matching a different case, %+v\n", tx.MatchedRules()) + } +} + +func TestCaseSensitiveURIQueryParam(t *testing.T) { + waf := corazawaf.NewWAF() + rules := `SecRule ARGS:Test1 "@contains SQLI" "id:3, phase:2, log, pass"` + parser := NewParser(waf) + + err := parser.FromString(rules) + if err != nil { + t.Error() + return + } + + tx := waf.NewTransaction() + tx.ProcessURI("/url?Test1='SQLI", "POST", "HTTP/1.1") + tx.ProcessRequestHeaders() + _, err = tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + + if len(tx.MatchedRules()) == 1 { + if len(tx.MatchedRules()[0].MatchedDatas()) != 1 { + t.Errorf("failed to test uri query param. Found matches: %d, %+v\n", + len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) + } + if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { + t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) + } + } else { + t.Errorf("failed to test uri query param: Same case arg name: %d, %+v\n", + len(tx.MatchedRules()), tx.MatchedRules()) + } + + tx = waf.NewTransaction() + tx.ProcessURI("/test?test1='SQLI&Test1='SQLI&TEST1='SQLI", "POST", "HTTP/1.1") + tx.ProcessRequestHeaders() + _, err = tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + + if len(tx.MatchedRules()) == 1 { + if len(tx.MatchedRules()[0].MatchedDatas()) != 1 { + t.Errorf("failed to test uri query param. Found matches: %d, %+v\n", + len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) + } + if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { + t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) + } + } else { + t.Errorf("failed to test qparam pollution: Multiple arg different case: %d, %+v\n", + len(tx.MatchedRules()), tx.MatchedRules()) + } +} diff --git a/internal/seclang/rules_test.go b/internal/seclang/rules_test.go index 05305ca3a..a6873685c 100644 --- a/internal/seclang/rules_test.go +++ b/internal/seclang/rules_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors // SPDX-License-Identifier: Apache-2.0 package seclang @@ -245,6 +245,33 @@ func TestTagsAreNotPrintedTwice(t *testing.T) { } } +func TestPrintedExtraMsgAndDataFromRuleWithMultipleMatches(t *testing.T) { + waf := corazawaf.NewWAF() + var logs []string + waf.SetErrorCallback(func(mr types.MatchedRule) { + logs = append(logs, mr.ErrorLog()) + }) + parser := NewParser(waf) + err := parser.FromString(` + SecRule ARGS_GET "@rx .*" "id:1, phase:1, log, pass, logdata:'%{MATCHED_VAR} in %{MATCHED_VAR_NAME}" + `) + if err != nil { + t.Error(err.Error()) + } + tx := waf.NewTransaction() + tx.AddGetRequestArgument("test", "1") + tx.AddGetRequestArgument("test2", "2") + tx.ProcessRequestHeaders() + if len(logs) != 1 { + t.Errorf("failed to log. Expected 1 entry, got %d", len(logs)) + } + if count := strings.Count(logs[0], "2 in ARGS_GET:test2"); count != 1 { + t.Errorf("failed to log logdata, expected %q occurence, got %v", "2 in ARGS_GET:test2", logs[0]) + } + if count := strings.Count(logs[0], "1 in ARGS_GET:test"); count != 1 { + t.Errorf("failed to log second logdata, expected %q occurence, got %v", "1 in ARGS_GET:test", logs[0]) + } +} func TestPrintedExtraMsgAndDataFromChainedRules(t *testing.T) { waf := corazawaf.NewWAF() var logs []string @@ -269,7 +296,7 @@ func TestPrintedExtraMsgAndDataFromChainedRules(t *testing.T) { t.Errorf("failed to set status, got %d", it.Status) } if len(logs) != 1 { - t.Errorf("failed to log with %d", len(logs)) + t.Errorf("failed to log. Expected 1 entry, got %d", len(logs)) } if count := strings.Count(logs[0], "1 in ARGS_GET:test"); count != 3 { t.Errorf("failed to log logdata, expected 3 repetitions, got %d", count) @@ -290,7 +317,7 @@ func TestPrintedMultipleMsgAndDataWithMultiMatch(t *testing.T) { }) parser := NewParser(waf) err := parser.FromString(` - SecRule ARGS_GET "@rx .*" "id:9696, phase:1, log, chain, deny, t:lowercase, status:403, msg:'msg', logdata:'%{MATCHED_VAR} in %{MATCHED_VAR_NAME}',multiMatch" + SecRule ARGS_GET "@rx .*" "id:9696, phase:1, log, deny, t:lowercase, status:403, msg:'msg', logdata:'%{MATCHED_VAR} in %{MATCHED_VAR_NAME}',multiMatch" `) if err != nil { t.Error(err.Error()) @@ -654,73 +681,6 @@ func TestArgumentNamesCaseSensitive(t *testing.T) { */ } -func TestArgumentsCaseSensitive(t *testing.T) { - waf := corazawaf.NewWAF() - rules := `SecRule ARGS:Test1 "Xyz" "id:3, phase:2, log, deny"` - parser := NewParser(waf) - - err := parser.FromString(rules) - if err != nil { - t.Error() - return - } - - tx := waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("Test1", "Xyz") - it, err := tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if it == nil { - t.Errorf("failed to test arguments value match: Same case argument name, %+v\n", tx.MatchedRules()) - } - - tx = waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("TEST1", "Xyz") - it, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if it == nil { - t.Errorf("failed to test arguments value match: Upper case argument name, %+v\n", tx.MatchedRules()) - } - - tx = waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("test1", "Xyz") - it, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if it == nil { - t.Errorf("failed to test arguments value match: Lower case argument name, %+v\n", tx.MatchedRules()) - } - - tx = waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("test1", "xyz") - it, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if it != nil { - t.Error("failed to test arguments value: different value case") - } - - tx = waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("test1", "XYZ") - it, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if it != nil { - t.Error("failed to test arguments value: different value case") - } -} - func TestCookiesCaseSensitive(t *testing.T) { waf := corazawaf.NewWAF() rules := `SecRule REQUEST_COOKIES:Test1 "Xyz" "id:3, phase:2, log, deny"` @@ -855,9 +815,19 @@ func TestHeadersCaseSensitive(t *testing.T) { } } -func TestParameterPollution(t *testing.T) { +// HPP - Detect HTTP Parameter Pollution Attacks +// Parameter pollution attacks are a type of attack where the attacker tries to manipulate the parameters of a request +// to bypass security controls, or to cause unexpected behavior. This rule is designed to detect parameter pollution +// The following test will test the parameter pollution with the following rule: +// SecRule ARGS:test1 "xyz" "id:3, phase:2, log, pass" +// Attack: +// POST /test?test1=xyz +// test1=abc&test1=ZZZZ +// In this case, the attacker tries to send three different values for the same parameter, and the rule should still match. +// Coraza should add the matched parameter to an array and iterate over it to check for matches. +func TestSingleParameterPollution(t *testing.T) { waf := corazawaf.NewWAF() - rules := `SecRule Args:TESt1 "Xyz" "id:3, phase:2, log, pass"` + rules := `SecRule ARGS:test1 "xyz" "id:3, phase:2, log, pass"` parser := NewParser(waf) err := parser.FromString(rules) @@ -869,8 +839,8 @@ func TestParameterPollution(t *testing.T) { tx := waf.NewTransaction() tx.ProcessRequestHeaders() tx.AddPostRequestArgument("test1", "xyz") - tx.AddPostRequestArgument("Test1", "Xyz") - tx.AddPostRequestArgument("TEST1", "XYZ") + tx.AddPostRequestArgument("test1", "abc") + tx.AddPostRequestArgument("test1", "ZZZZ") _, err = tx.ProcessRequestBody() if err != nil { t.Error(err) @@ -885,94 +855,58 @@ func TestParameterPollution(t *testing.T) { t.Errorf("failed to test arguments pollution: Single match fixed case: %d, %+v\n", len(tx.MatchedRules()), tx.MatchedRules()) } - - tx = waf.NewTransaction() - tx.ProcessRequestHeaders() - tx.AddPostRequestArgument("test1", "xyz") - tx.AddPostRequestArgument("Test1", "Xyz") - tx.AddPostRequestArgument("tesT1", "Xyz") - tx.AddPostRequestArgument("TEST1", "XYZ") - _, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - if len(tx.MatchedRules()) == 1 { - if len(tx.MatchedRules()[0].MatchedDatas()) != 2 { - t.Errorf("failed to test arguments pollution. Found matches: %d, %+v\n", - len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) - } - } else { - t.Errorf("failed to test arguments pollution: Multiple match mixed case: %d, %+v\n", - len(tx.MatchedRules()), tx.MatchedRules()) - } - } -func TestURIQueryParamCaseSensitive(t *testing.T) { +// HPP - Detect HTTP Parameter Pollution Attacks +// This test case uses two rules instead of one to test the parameter pollution. The rules are: +// 1. SecRule ARGS:test1 "xyz" "id:3, phase:2, log, pass" +// 2. SecRule ARGS:test1 "ZZZZ" "id:4, phase:2, log, pass" +// Attack: +// POST /test?test1=xyz&test1=ABCD +// test1=abc&test1=ZZZZ +// In this case, the attacker tries to send multiple different values for the same parameter, and the rule should match in +// both cases. Coraza should add the matched parameter to an array and iterate over it to check for matches.` +// For the above case, the rule should match twice. +func TestMultipleParameterPollution(t *testing.T) { + rules := `SecRule ARGS:test1 "xyz" "id:3, phase:2, log, pass" +SecRule ARGS:test1 "ZZZZ" "id:4, phase:2, log, pass"` waf := corazawaf.NewWAF() - rules := `SecRule ARGS:Test1 "@contains SQLI" "id:3, phase:2, log, pass"` parser := NewParser(waf) - err := parser.FromString(rules) if err != nil { t.Error() return } - tx := waf.NewTransaction() - tx.ProcessURI("/url?Test1='SQLI", "POST", "HTTP/1.1") + tx.AddGetRequestArgument("test1", "xyz") + tx.AddGetRequestArgument("test1", "ABCD") tx.ProcessRequestHeaders() + tx.AddPostRequestArgument("test1", "abc") + tx.AddPostRequestArgument("test1", "ZZZZ") _, err = tx.ProcessRequestBody() if err != nil { t.Error(err) } - - if len(tx.MatchedRules()) == 1 { + if len(tx.MatchedRules()) == 2 { if len(tx.MatchedRules()[0].MatchedDatas()) != 1 { - t.Errorf("failed to test uri query param. Found matches: %d, %+v\n", + t.Errorf("failed to test first argument pollution. Found matches: %d, %+v\n", len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) } - if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { - t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) - } - } else { - t.Errorf("failed to test uri query param: Same case arg name: %d, %+v\n", - len(tx.MatchedRules()), tx.MatchedRules()) - } - - tx = waf.NewTransaction() - tx.ProcessURI("/test?test1='SQLI&Test1='SQLI&TEST1='SQLI", "POST", "HTTP/1.1") - tx.ProcessRequestHeaders() - _, err = tx.ProcessRequestBody() - if err != nil { - t.Error(err) - } - - if len(tx.MatchedRules()) == 1 { - if len(tx.MatchedRules()[0].MatchedDatas()) != 3 { - t.Errorf("failed to test uri query param. Found matches: %d, %+v\n", + if len(tx.MatchedRules()[1].MatchedDatas()) != 1 { + t.Errorf("failed to test second match pollution. Found matches: %d, %+v\n", len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) } - if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { - t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) - } } else { - t.Errorf("failed to test qparam pollution: Multiple arg different case: %d, %+v\n", - len(tx.MatchedRules()), tx.MatchedRules()) + t.Errorf("failed to test arguments pollution, less matches than expected: %d", len(tx.MatchedRules())) } } -/* func TestURIQueryParamNameCaseSensitive(t *testing.T) { - waf := coraza.NewWAF() + waf := corazawaf.NewWAF() rules := `SecRule ARGS_NAMES "Test1" "id:3, phase:2, log, pass"` - parser, err := NewParser(waf) - if err != nil { - t.Error(err) - return - } + parser := NewParser(waf) - err = parser.FromString(rules) + err := parser.FromString(rules) if err != nil { t.Error() return @@ -980,17 +914,18 @@ func TestURIQueryParamNameCaseSensitive(t *testing.T) { tx := waf.NewTransaction() tx.ProcessURI("/url?Test1='SQLI", "POST", "HTTP/1.1") + tx.ProcessRequestHeaders() _, err = tx.ProcessRequestBody() if err != nil { t.Error(err) } if len(tx.MatchedRules()) == 1 { - if len(tx.MatchedRules()[0].MatchedDatas) != 1 { + if len(tx.MatchedRules()[0].MatchedDatas()) != 1 { t.Errorf("failed to test uri query param. Expected: 1, Found matches: %d, %+v\n", - len(tx.MatchedRules()[0].MatchedDatas), tx.MatchedRules()) + len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) } - if !isMatchData(tx.MatchedRules()[0].MatchedDatas, "Test1") { + if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) } } else { @@ -1000,17 +935,18 @@ func TestURIQueryParamNameCaseSensitive(t *testing.T) { tx = waf.NewTransaction() tx.ProcessURI("/test?test1='SQLI&Test1='SQLI&TEST1='SQLI", "POST", "HTTP/1.1") + tx.ProcessRequestHeaders() _, err = tx.ProcessRequestBody() if err != nil { t.Error(err) } if len(tx.MatchedRules()) == 1 { - if len(tx.MatchedRules()[0].MatchedDatas) != 1 { + if len(tx.MatchedRules()[0].MatchedDatas()) != 1 { t.Errorf("Failed to test uri query param. Expected: 1, Found matches: %d, %+v\n", - len(tx.MatchedRules()[0].MatchedDatas), tx.MatchedRules()) + len(tx.MatchedRules()[0].MatchedDatas()), tx.MatchedRules()) } - if !isMatchData(tx.MatchedRules()[0].MatchedDatas, "Test1") { + if !isMatchData(tx.MatchedRules()[0].MatchedDatas(), "Test1") { t.Error("Key did not match: Test1 !=", tx.MatchedRules()[0]) } } else { @@ -1018,7 +954,6 @@ func TestURIQueryParamNameCaseSensitive(t *testing.T) { len(tx.MatchedRules())) } } -*/ func isMatchData(mds []types.MatchData, key string) (result bool) { result = false diff --git a/internal/transformations/base64decode.go b/internal/transformations/base64decode.go index 76345599d..8f9961da2 100644 --- a/internal/transformations/base64decode.go +++ b/internal/transformations/base64decode.go @@ -3,7 +3,10 @@ package transformations -import "strings" +import ( + "strings" + "unicode" +) var base64DecMap = []byte{ 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, @@ -29,11 +32,12 @@ var base64DecMap = []byte{ // would be possible to use the standard library only relying on undocumented behaviors of the decoder. // For more context, see https://github.com/corazawaf/coraza/pull/940 func base64decode(data string) (string, bool, error) { - res := doBase64decode(data) + res := doBase64decode(data, false) return res, true, nil } -func doBase64decode(src string) string { +// The 'ext' flag indicates whether the function should conduct a lenient decoding, primarily utilized in the 'base64decodeext' transformation. +func doBase64decode(src string, ext bool) string { slen := len(src) if slen == 0 { return src @@ -45,6 +49,12 @@ func doBase64decode(src string) string { for i := 0; i < slen; i++ { currChar := src[i] + + // Skip whitespaces and '.' if ext is set + if ext && (unicode.IsSpace(rune(currChar)) || currChar == '.') { + continue + } + // new line characters are ignored. if currChar == '\r' || currChar == '\n' { continue diff --git a/internal/transformations/base64decodeext.go b/internal/transformations/base64decodeext.go new file mode 100644 index 000000000..d5c530f6e --- /dev/null +++ b/internal/transformations/base64decodeext.go @@ -0,0 +1,10 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 +package transformations + +// Decodes a Base64-encoded string. Unlike base64Decode, +// this version uses a forgiving implementation, which ignores invalid characters such as whitespace and ".", +func base64decodeext(data string) (string, bool, error) { + res := doBase64decode(data, true) + return res, true, nil +} diff --git a/internal/transformations/base64decodeext_test.go b/internal/transformations/base64decodeext_test.go new file mode 100644 index 000000000..a64c9d433 --- /dev/null +++ b/internal/transformations/base64decodeext_test.go @@ -0,0 +1,100 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package transformations + +import ( + "bytes" + "encoding/base64" + "strings" + "testing" +) + +var b64DecodeExtTests = []struct { + name string + input string + expected string +}{ + { + name: "Valid", + input: "VGVzdENhc2U=", + expected: "TestCase", + }, + { + name: "Valid with \u0000", + input: "VGVzdABDYXNl", + expected: "Test\x00Case", + }, + { + name: "Valid without padding", + input: "P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", + expected: "", + }, + { + name: "Decode with the space (invalid character)", + input: "PFR FU1Q+", + expected: "", + }, + { + name: "Decoded upto a .", + input: "P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", + expected: "", // No matter where the invalid character is, it is omitted + }, + { + name: "Decoded upto a . (In different position)", + input: "PHNjcmlwd.D5hbGVydCgxKTwvc2NyaXB0Pg==", + expected: "", + }, + { + name: "Decoded upto a . (In different position)", + input: "PHNjcmlwdD.5hbGVydCgxKTwvc2NyaXB0Pg==", + expected: "", + }, +} + +func TestBase64DecodeExt(t *testing.T) { + for _, tt := range b64DecodeExtTests { + t.Run(tt.name, func(t *testing.T) { + actual, _, err := base64decodeext(tt.input) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if actual != tt.expected { + t.Errorf("Expected %q, but got %q", tt.expected, actual) + } + }) + } +} +func BenchmarkB64DecodeExt(b *testing.B) { + for _, tt := range b64DecodeExtTests { + b.Run(tt.input, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _, _, err := base64decodeext(tt.input) + if err != nil { + b.Error(err) + } + } + }) + } +} + +func FuzzB64DecodeExt(f *testing.F) { + for _, tc := range b64DecodeExtTests { + f.Add(tc.input) + } + f.Fuzz(func(t *testing.T, tc string) { + data, _, err := base64decodeext(tc) + // We decode base64 within non-base64 so there is no + // error case. + if err != nil { + t.Error(err) + } + + refData, err := base64.StdEncoding.DecodeString(tc) + // The standard library decoder will fail on many inputs ours succeeds on, but when + // it doesn't and there are no newlines in the input, they should match. + if err == nil && !strings.ContainsAny(tc, "\n\r") && !bytes.Equal([]byte(data), refData) { + t.Errorf("mismatch with stdlib for input %s", tc) + } + }) +} diff --git a/internal/transformations/testdata/md5.json b/internal/transformations/testdata/md5.json index 86db034a5..f8e96bb37 100644 --- a/internal/transformations/testdata/md5.json +++ b/internal/transformations/testdata/md5.json @@ -4,33 +4,33 @@ "ret" : 1, "input" : "zimmerle", "type" : "tfn", - "name" : "md5" + "name" : "md5-test1" }, { "output" : "\\xd4\\x1d\\x8c\\xd9\\x8f\\x00\\xb2\\x04\\xe9\\x80\\x09\\x98\\xec\\xf8B~", "ret" : 1, "input" : "", "type" : "tfn", - "name" : "md5" + "name" : "md5-test2" }, { "output" : "\\xc16\\x83Y\\xaa)(\\xa50\\xb5\\x00\\x07\\xd1\\xde\\xeaw", "ret" : 1, "input" : "진 마 리", - "name" : "md5", + "name" : "md5-test3", "type" : "tfn" }, { "output" : "\\xa6\\xe7\\xd3\\xb4o\\xdf\\xaf\\x0b\\xde*\\x1f\\x83*\\x00\\xd2\\xde", "ret" : 1, "input" : "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08", - "name" : "md5", + "name" : "md5-test4", "type" : "tfn" }, { "input" : "TestCase", "type" : "tfn", - "name" : "md5", + "name" : "md5-test5", "ret" : 1, "output" : "\\xc9\\xab\\xa2\\xc3\\xe6\\x01&\\x16\\x9e\\x80\\xe9\\xa2k\\xa2s\\xc1" } diff --git a/internal/transformations/testdata/sha1.json b/internal/transformations/testdata/sha1.json index 8588c18d6..fcc0ac28f 100644 --- a/internal/transformations/testdata/sha1.json +++ b/internal/transformations/testdata/sha1.json @@ -2,21 +2,21 @@ { "type" : "tfn", "input" : "", - "name" : "sha1", + "name" : "sha1-test1", "ret" : 1, "output" : "\\xda\\x39\\xa3\\xee\\x5e\\x6b\\x4b\\x0d\\x32\\x55\\xbf\\xef\\x95\\x60\\x18\\x90\\xaf\\xd8\\x07\\x09" }, { "type" : "tfn", "input" : "TestCase", - "name" : "sha1", + "name" : "sha1-test2", "ret" : 1, "output" : "\\xa7\\x0c\\xe3\\x83\\x89\\xe3\\x18\\xbd\\x2b\\xe1\\x8a\\x01\\x11\\xc6\\xdc\\x76\\xbd\\x2c\\xd9\\xed" }, { "type" : "tfn", "input" : "\\x00\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08", - "name" : "sha1", + "name" : "sha1-test3", "ret" : 1, "output" : "\\x63\\xbf\\x60\\xc7\\x10\\x5a\\x07\\xa2\\xb1\\x25\\xbb\\xf8\\x9e\\x61\\xab\\xda\\xbc\\x69\\x78\\xc2" } diff --git a/internal/transformations/transformations.go b/internal/transformations/transformations.go index e4caa4b19..ce14ac62a 100644 --- a/internal/transformations/transformations.go +++ b/internal/transformations/transformations.go @@ -29,6 +29,7 @@ func GetTransformation(name string) (plugintypes.Transformation, error) { func init() { Register("base64Decode", base64decode) + Register("base64DecodeExt", base64decodeext) Register("cmdLine", cmdLine) Register("compressWhitespace", compressWhitespace) Register("cssDecode", cssDecode) diff --git a/magefile.go b/magefile.go index da4e38f7e..c948c08d6 100644 --- a/magefile.go +++ b/magefile.go @@ -110,7 +110,7 @@ func Test() error { return err } - if err := sh.RunV("go", "test", "./examples/http-server"); err != nil { + if err := sh.RunV("go", "test", "./examples/http-server", "-race"); err != nil { return err } @@ -127,6 +127,10 @@ func Test() error { return err } + if err := sh.RunV("go", "test", "-tags=coraza.rule.case_sensitive_args_keys", "-run=^TestCaseSensitive", "./..."); err != nil { + return err + } + return nil } @@ -182,7 +186,8 @@ func Fuzz() error { { pkg: "./internal/transformations", tests: []string{ - "FuzzB64Decode", + "FuzzB64Decode$", + "FuzzB64DecodeExt", "FuzzCMDLine", }, }, diff --git a/renovate.json b/renovate.json index 5db72dd6a..599cfc543 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,9 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ - "config:recommended" - ] + "config:best-practices" + ], + "constraints": { + "go": "1.21" + } } diff --git a/testing/auditlog_test.go b/testing/auditlog_test.go index 466c8e0db..6d9164a32 100644 --- a/testing/auditlog_test.go +++ b/testing/auditlog_test.go @@ -13,6 +13,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/crowdsecurity/coraza/v3/internal/auditlog" @@ -224,3 +225,111 @@ func TestAuditLogOnNoLog(t *testing.T) { t.Error(err) } } + +func TestAuditLogRequestMethodURIProtocol(t *testing.T) { + waf := corazawaf.NewWAF() + parser := seclang.NewParser(waf) + if err := parser.FromString(` + SecRuleEngine DetectionOnly + SecAuditEngine On + SecAuditLogFormat json + SecAuditLogType serial + `); err != nil { + t.Fatal(err) + } + // generate a random tmp file + file, err := os.Create(filepath.Join(t.TempDir(), "tmp.log")) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file.Name()) + if err := parser.FromString(fmt.Sprintf("SecAuditLog %s", file.Name())); err != nil { + t.Fatal(err) + } + tx := waf.NewTransaction() + + uri := "/some-url" + method := "POST" + proto := "HTTP/1.1" + + tx.ProcessURI(uri, method, proto) + // now we read file + if _, err := file.Seek(0, 0); err != nil { + t.Error(err) + } + tx.ProcessLogging() + var al2 auditlog.Log + if err := json.NewDecoder(file).Decode(&al2); err != nil { + t.Error(err) + } + trans := al2.Transaction() + if trans == nil { + t.Fatalf("Expected 1 transaction, got nil") + } + req := trans.Request() + if req == nil { + t.Fatalf("Expected 1 request, got nil") + } + if req.URI() != uri { + t.Fatalf("Expected %s uri, got %s", uri, req.URI()) + } + if req.Method() != method { + t.Fatalf("Expected %s method, got %s", method, req.Method()) + } + if req.Protocol() != proto { + t.Fatalf("Expected %s protocol, got %s", proto, req.Protocol()) + } +} + +func TestAuditLogRequestBody(t *testing.T) { + waf := corazawaf.NewWAF() + parser := seclang.NewParser(waf) + if err := parser.FromString(` + SecRuleEngine DetectionOnly + SecAuditEngine On + SecAuditLogFormat json + SecAuditLogType serial + SecRequestBodyAccess On + `); err != nil { + t.Fatal(err) + } + // generate a random tmp file + file, err := os.Create(filepath.Join(t.TempDir(), "tmp.log")) + if err != nil { + t.Fatal(err) + } + defer os.Remove(file.Name()) + if err := parser.FromString(fmt.Sprintf("SecAuditLog %s", file.Name())); err != nil { + t.Fatal(err) + } + tx := waf.NewTransaction() + params := "somepost=data" + _, _, err = tx.ReadRequestBodyFrom(strings.NewReader(params)) + if err != nil { + t.Error(err) + } + _, err = tx.ProcessRequestBody() + if err != nil { + t.Error(err) + } + // now we read file + if _, err := file.Seek(0, 0); err != nil { + t.Error(err) + } + tx.ProcessLogging() + var al2 auditlog.Log + if err := json.NewDecoder(file).Decode(&al2); err != nil { + t.Error(err) + } + trans := al2.Transaction() + if trans == nil { + t.Fatalf("Expected 1 transaction, got nil") + } + req := trans.Request() + if req == nil { + t.Fatalf("Expected 1 request, got nil") + } + if req.Body() != params { + t.Fatalf("Expected %s uri, got %s", params, req.Body()) + } +} diff --git a/testing/coreruleset/.ftw.yml b/testing/coreruleset/.ftw.yml index f2875a90e..0cc93009b 100644 --- a/testing/coreruleset/.ftw.yml +++ b/testing/coreruleset/.ftw.yml @@ -4,11 +4,10 @@ testoverride: 920100-4: 'Invalid uri, Coraza not reached - 404 page not found' 920100-5: 'Invalid uri, Coraza not reached - 404 page not found' 920100-8: 'Go/http allows a colon in the path. Test expects status 400 or 403 (Apache behaviour)' - 920170-3: 'HEAD request with data. Go/http does not allow it - 400 Bad Request' 920270-4: 'Rule works, log contains 920270. Test expects status 400 (Apache behaviour)' 920272-5: 'Rule works, log contains 920272. Test expects status 400 (Apache behaviour)' 920290-1: 'Rule works, log contains 920290. Test expects status 400 (Apache behaviour)' - 920420-8: 'HEAD request with data. Go/http does not allow it - 400 Bad Request' - 920430-5: 'Test has expect_error, Go/http and Envoy return 400' - 920430-8: 'Go/http does no allow HTTP/3.0 - 505 HTTP Version Not Supported' + 920290-4: 'Go/http returns 400 Bad Request: missing required Host header' + 920430-8: 'Go/http does not allow HTTP/3.0 - 505 HTTP Version Not Supported' 932200-13: 'wip' + 930110-7: 'CRS issue: https://github.com/coreruleset/coreruleset/issues/3736' diff --git a/testing/coreruleset/coreruleset_test.go b/testing/coreruleset/coreruleset_test.go index 14b8d634f..a0d1bf3fc 100644 --- a/testing/coreruleset/coreruleset_test.go +++ b/testing/coreruleset/coreruleset_test.go @@ -29,8 +29,8 @@ import ( "github.com/coreruleset/go-ftw/test" "github.com/rs/zerolog" - coreruleset "github.com/corazawaf/coraza-coreruleset" - crstests "github.com/corazawaf/coraza-coreruleset/tests" + coreruleset "github.com/corazawaf/coraza-coreruleset/v4" + crstests "github.com/corazawaf/coraza-coreruleset/v4/tests" "github.com/crowdsecurity/coraza/v3" txhttp "github.com/crowdsecurity/coraza/v3/http" "github.com/crowdsecurity/coraza/v3/types" @@ -224,9 +224,9 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ defer r.Body.Close() w.Header().Set("Content-Type", "text/plain") switch { - case r.URL.Path == "/anything": + case r.URL.Path == "/anything", r.URL.Path == "/post": body, err := io.ReadAll(r.Body) - // Emulated httpbin behaviour: /anything endpoint acts as an echo server, writing back the request body + // Emulated httpbin behaviour: /anything and /post endpoints act as an echo server, writing back the request body if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" { // Tests 954120-1 and 954120-2 are the only two calling /anything with a POST and payload is urlencoded if err != nil { @@ -234,11 +234,16 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ } urldecodedBody, err := url.QueryUnescape(string(body)) if err != nil { - t.Fatalf("handler can not unescape urlencoded request body: %v", err) + t.Logf("[warning] handler can not unescape urlencoded request body: %v", err) + // If the body can't be unescaped, we will keep going with the received body + urldecodedBody = string(body) } - fmt.Fprintf(w, urldecodedBody) + fmt.Fprint(w, urldecodedBody) } else { _, err = w.Write(body) + if err != nil { + t.Fatalf("handler can not write request body: %v", err) + } } case strings.HasPrefix(r.URL.Path, "/base64/"): @@ -247,25 +252,25 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \ if err != nil { t.Fatalf("handler can not decode base64: %v", err) } - fmt.Fprintf(w, string(b64Decoded)) + fmt.Fprint(w, string(b64Decoded)) default: // Common path "/status/200" defaults here - fmt.Fprintf(w, "Hello!") + fmt.Fprint(w, "Hello!") } }))) defer s.Close() - var tests []test.FTWTest + var tests []*test.FTWTest err = doublestar.GlobWalk(crstests.FS, "**/*.yaml", func(path string, d os.DirEntry) error { yaml, err := fs.ReadFile(crstests.FS, path) if err != nil { return err } - t, err := test.GetTestFromYaml(yaml) + ftwt, err := test.GetTestFromYaml(yaml) if err != nil { return err } - tests = append(tests, t) + tests = append(tests, ftwt) return nil }) if err != nil { diff --git a/testing/coreruleset/go.mod b/testing/coreruleset/go.mod index f447bcbf0..1df3aeff2 100644 --- a/testing/coreruleset/go.mod +++ b/testing/coreruleset/go.mod @@ -1,46 +1,55 @@ module github.com/crowdsecurity/coraza/v3/testing/coreruleset -go 1.18 +go 1.21 require ( github.com/bmatcuk/doublestar/v4 v4.6.1 - github.com/corazawaf/coraza-coreruleset v0.0.0-20230330101229-43b851256042 - github.com/coreruleset/go-ftw v0.4.9 - github.com/rs/zerolog v1.31.0 + github.com/corazawaf/coraza-coreruleset/v4 v4.3.0 + github.com/coreruleset/go-ftw v0.6.4 + github.com/crowdsecurity/coraza/v3 v3.0.0-00010101000000-000000000000 + github.com/rs/zerolog v1.33.0 ) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible // indirect - github.com/corazawaf/libinjection-go v0.1.2 // indirect - github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/goccy/go-yaml v1.8.10 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/huandu/xstrings v1.3.3 // indirect - github.com/icza/backscanner v0.0.0-20220812133752-2e60bffed4a2 // indirect - github.com/imdario/mergo v0.3.13 // indirect - github.com/knadh/koanf v1.4.4 // indirect - github.com/kyokomi/emoji v2.2.4+incompatible // indirect + github.com/corazawaf/coraza/v3 v3.2.1 // indirect + github.com/corazawaf/libinjection-go v0.2.1 // indirect + github.com/coreruleset/ftw-tests-schema v1.1.0 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/goccy/go-yaml v1.11.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/huandu/xstrings v1.4.0 // indirect + github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect + github.com/knadh/koanf/providers/env v0.1.0 // indirect + github.com/knadh/koanf/providers/file v0.1.0 // indirect + github.com/knadh/koanf/providers/rawbytes v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.1 // indirect + github.com/kyokomi/emoji/v2 v2.2.13 // indirect github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e // indirect - github.com/tidwall/gjson v1.17.0 // indirect + github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect + github.com/tidwall/gjson v1.17.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/yargevad/filepathx v1.0.0 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/tools v0.6.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/binaryregexp v0.2.0 // indirect ) + +replace github.com/crowdsecurity/coraza/v3 => ../../ diff --git a/testing/coreruleset/go.sum b/testing/coreruleset/go.sum index 9edd6c09a..2a610f6c9 100644 --- a/testing/coreruleset/go.sum +++ b/testing/coreruleset/go.sum @@ -1,300 +1,95 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= -github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= -github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= -github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= -github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= -github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/corazawaf/coraza-coreruleset v0.0.0-20230330101229-43b851256042 h1:WMAVBbS+u1zopf0gT1UBTtmmxVRCj9gY1dmnT14PsZM= -github.com/corazawaf/coraza-coreruleset v0.0.0-20230330101229-43b851256042/go.mod h1:h7fBXlh00atH/uVC9Lpjawg/RlJCsHjvyVk+bP3ylq8= -github.com/corazawaf/coraza/v3 v3.0.4 h1:Llemgoh0hp2NggCwcWN8lNiV4Pfe+AWzf1oEcasT234= -github.com/corazawaf/coraza/v3 v3.0.4/go.mod h1:3fTYjY5BZv3nezLpH6NAap0gr3jZfbQWUAu2GF17ET4= -github.com/corazawaf/libinjection-go v0.1.2 h1:oeiV9pc5rvJ+2oqOqXEAMJousPpGiup6f7Y3nZj5GoM= -github.com/corazawaf/libinjection-go v0.1.2/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/corazawaf/coraza-coreruleset/v4 v4.3.0 h1:izzVRUxfvVf1OXhRQXpFm1jj3g/cIlLu9SiNzXOW7XU= +github.com/corazawaf/coraza-coreruleset/v4 v4.3.0/go.mod h1:RQMGurig+irQq7v21yq7rM/9SAEf1bT6hCSplJ0ByKY= +github.com/corazawaf/coraza/v3 v3.2.1 h1:zBIji4ut9FtFe8lXdqFwXMAkUoDJZ7HsOlEUYWERLI8= +github.com/corazawaf/coraza/v3 v3.2.1/go.mod h1:fVndCGdUHJWl9c26VZPcORQRzUYwMPnRkC6TyTkhbUg= +github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yfxBma30Dy/Y= +github.com/corazawaf/libinjection-go v0.2.1/go.mod h1:OP4TM7xdJ2skyXqNX1AN1wN5nNZEmJNuWbNPOItn7aw= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreruleset/go-ftw v0.4.9 h1:4s4GPnn07d1S58QhoZlWy6UTdpjCTzhnhsfmh1cJ7E8= -github.com/coreruleset/go-ftw v0.4.9/go.mod h1:VLRHyrid8L2gB5AsnHpQnHfizBkZpKZuy3OUq0s8rCc= -github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7 h1:3rnfZlcD6p5c1Xyc7yvOrqrX+xz4NFbNHftEra4msqg= -github.com/crowdsecurity/coraza/v3 v3.0.0-20231206165912-778ce24a85e7/go.mod h1:jNww1Y9SujXQc89zDR+XOb70bkC7mZ6ep7iKhUBBsiI= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/coreruleset/ftw-tests-schema v1.1.0 h1:3+NYrdLE3HVmOc3nGrisRBBvY9lGjePUrV+YkT5Ay3s= +github.com/coreruleset/ftw-tests-schema v1.1.0/go.mod h1:gRd9wBxjUI85HypWRDxJzbk1JqHC4KTxl0l/Y2p9QK4= +github.com/coreruleset/go-ftw v0.6.4 h1:EdDNld38Jv4lxqHS+csGOJuHu1/8rpp4TlrFyoijTPk= +github.com/coreruleset/go-ftw v0.6.4/go.mod h1:IayMjfOmmNNBcqTcZU92e6UZTy79/eFdmJEmRu8tLs4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/goccy/go-yaml v1.8.10 h1:XpBOLD8cmOZlLYjUFPqSZZ+Ubi4/UKxO2eXyhg5WuAA= -github.com/goccy/go-yaml v1.8.10/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= -github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= -github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= -github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= -github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= -github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= -github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= -github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/icza/backscanner v0.0.0-20220812133752-2e60bffed4a2 h1:Cra36ePFgA2F/pnhd1qq++SB/VL5RyDkPOIH2a3RlDc= -github.com/icza/backscanner v0.0.0-20220812133752-2e60bffed4a2/go.mod h1:GYeBD1CF7AqnKZK+UCytLcY3G+UKo0ByXX/3xfdNyqQ= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5 h1:FcxwOojw6pUiPpsf7Q6Fw/pI+7cR6FlapLBEGV/902A= +github.com/icza/backscanner v0.0.0-20240328210400-b40c3a86dec5/go.mod h1:GYeBD1CF7AqnKZK+UCytLcY3G+UKo0ByXX/3xfdNyqQ= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/knadh/koanf v1.4.4 h1:d2jY5nCCeoaiqvEKSBW9rEc93EfNy/XWgWsSB3j7JEA= -github.com/knadh/koanf v1.4.4/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kyokomi/emoji v2.2.4+incompatible h1:np0woGKwx9LiHAQmwZx79Oc0rHpNw3o+3evou4BEPv4= -github.com/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= +github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= +github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg= +github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= +github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= +github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 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/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= -github.com/pelletier/go-toml v1.9.1 h1:a6qW1EVNZWH9WGI6CsYdD8WAylkoXBS5yv0XHlh17Tc= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e h1:POJco99aNgosh92lGqmx7L1ei+kCymivB/419SD15PQ= -github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 h1:1Kw2vDBXmjop+LclnzCb/fFy+sgb3gYARwfmoUcQe6o= +github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= @@ -302,183 +97,26 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= -go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/testing/e2e/e2e_test.go b/testing/e2e/e2e_test.go index 7dc19997a..55f2c4c86 100644 --- a/testing/e2e/e2e_test.go +++ b/testing/e2e/e2e_test.go @@ -22,25 +22,8 @@ import ( 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) + WithDirectives(e2e.Directives) waf, err := coraza.NewWAF(conf) if err != nil { diff --git a/testing/engine/chains.go b/testing/engine/chains.go index e57e79e20..afe779378 100644 --- a/testing/engine/chains.go +++ b/testing/engine/chains.go @@ -119,7 +119,7 @@ SecRule ARGS_GET "@rx prepayloadpost" "id:200, phase:2, log, msg:'Rule Parent 2 SecRule MATCHED_VAR "@rx pre" "chain" SecRule MATCHED_VAR "@rx post" -SecRule ARGS_GET:var3 "@rx pre3payloadpost" "id:300, phase:2, log, msg:'Rule Parent 300', \ +SecRule ARGS_GET:Var3 "@rx pre3payloadpost" "id:300, phase:2, log, msg:'Rule Parent 300', \ logdata:'Matched Data: %{TX.0} found within %{TX.300_MATCHED_VAR_NAME}: %{MATCHED_VAR}',\ setvar:'tx.300_matched_var_name=%{MATCHED_VAR_NAME}',\ chain" diff --git a/testing/engine/directives_ruleexclusions.go b/testing/engine/directives_ruleexclusions.go new file mode 100644 index 000000000..c9b71bad1 --- /dev/null +++ b/testing/engine/directives_ruleexclusions.go @@ -0,0 +1,94 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package engine + +import ( + "github.com/crowdsecurity/coraza/v3/testing/profile" +) + +var _ = profile.RegisterProfile(profile.Profile{ + Meta: profile.Meta{ + Author: "M4tteoP", + Description: "Test SecRuleUpdateTarget directives", + Enabled: true, + Name: "SecRuleUpdateTarget.yaml", + }, + Tests: []profile.Test{ + { + Title: "SecRuleUpdateTarget", + Stages: []profile.Stage{ + { + Stage: profile.SubStage{ + Input: profile.StageInput{ + URI: "/index.php?t1=aaa&t2=bbb", + Method: "POST", + Headers: map[string]string{ + "content-type": "application/x-www-form-urlencoded", + "Cookie": "cookie=aaa", + }, + }, + Output: profile.ExpectedOutput{ + TriggeredRules: []int{ + 20, + 30, + 50, + 62, + 51, + }, + NonTriggeredRules: []int{ + 10, + 12, + 16, + 40, + 60, + 61, + }, + }, + }, + }, + }, + }, + }, + Rules: ` + # ARGS:t1 is removed by SecRuleUpdateTargetById, rule 10 should not be triggered + SecRule ARGS:t1 "@rx aaa" "id:10,phase:1,log" + SecRuleUpdateTargetById 10 "!ARGS:t1" + + # ARGS:t2 is removed by SecRuleUpdateTargetById, rule 12 (in range ids 11-13) should not be triggered + SecRule ARGS:t2 "@rx bbb" "id:12,phase:1,log" + SecRuleUpdateTargetById 11-13 "!ARGS:t2" + + # ARGS:t2 is removed by SecRuleUpdateTargetById, rule 16 (in range) should not be triggered + SecRule ARGS:t2 "@rx bbb" "id:16,phase:1,log" + SecRuleUpdateTargetById 13-15 16 18 "!ARGS:t2" + + # ARGS:t1 is removed by SecRuleUpdateTargetById, but REQUEST_COOKIES should still trigger rule 20 + SecRule ARGS:t1|REQUEST_COOKIES "@rx aaa" "id:20,phase:1,log" + SecRuleUpdateTargetById 20 "!ARGS:t1" + + # ARGS:t1 is added by SecRuleUpdateTargetById, it should trigger rule 30 + SecRule REQUEST_BODY "@rx aaa" "id:30,phase:1,log" + SecRuleUpdateTargetById 30 "ARGS:t1" + + # ARGS:t19999 is added by SecRuleUpdateTargetById, it should not trigger rule 40 + SecRule REQUEST_BODY "@rx aaa" "id:40,phase:1,log" + SecRuleUpdateTargetById 40 "ARGS:t19999" + + # ARGS:t1 is NOT removed by SecRuleUpdateTargetByTag, rule 50 should be triggered + SecRule ARGS:t1 "@rx aaa" "id:50,phase:1,log,tag:tag-1" + SecRuleUpdateTargetByTag tag-1111 "!ARGS:t1" + + # ARGS:t1 is NOT removed by SecRuleUpdateTargetByTag. Because case sensitive matching, rule 51 should be triggered + SecRule ARGS:t1 "@rx aaa" "id:51,phase:1,log,tag:tag-1b" + SecRuleUpdateTargetByTag tAg-1b "!ARGS:t1" + + # ARGS:t1 is removed by SecRuleUpdateTargetByTag, rule 60,61 should not be triggered. + SecRule ARGS "@rx aaa" "id:60,phase:1,log,tag:tag-2" + SecRule ARGS:t2 "@rx bbb" "id:61,phase:1,log,tag:tag-2" + SecRule ARGS:t1|REQUEST_COOKIES "@rx aaa" "id:62,phase:1,log,tag:tag-2" + # The tag might also be wrapped in double quotes + SecRuleUpdateTargetByTag "tag-2" "!ARGS:t1|!ARGS:t2" + + `, +}) diff --git a/testing/engine/disruptive_actions.go b/testing/engine/disruptive_actions.go index f8fa2ce13..e9ab466fd 100644 --- a/testing/engine/disruptive_actions.go +++ b/testing/engine/disruptive_actions.go @@ -43,7 +43,7 @@ var _ = profile.RegisterProfile(profile.Profile{ Output: profile.ExpectedOutput{ TriggeredRules: []int{2}, Interruption: &profile.ExpectedInterruption{ - Status: 500, + Status: 403, Data: "", RuleID: 2, Action: "deny", @@ -285,7 +285,8 @@ var _ = profile.RegisterProfile(profile.Profile{ }, Rules: ` SecRule REQUEST_URI "/redirect1$" "phase:1,id:1,log,status:302,redirect:https://www.example.com" -SecRule REQUEST_URI "/deny1$" "phase:1,id:2,log,status:500,deny" +# deny action defaults to status 403 +SecRule REQUEST_URI "/deny1$" "phase:1,id:2,log,deny" SecRule REQUEST_URI "/drop1$" "phase:1,id:3,log,drop" SecRule REQUEST_URI "/redirect2$" "phase:2,id:21,log,status:302,redirect:https://www.example.com" diff --git a/testing/engine/multiphase.go b/testing/engine/multiphase.go index 59123e057..77d771f85 100644 --- a/testing/engine/multiphase.go +++ b/testing/engine/multiphase.go @@ -186,3 +186,39 @@ SecRule REQUEST_URI|REQUEST_BODY "@rx test" "id:3, phase:2, deny, log, status:50 SecRule REQUEST_URI "@unconditionalMatch" "id:4, phase:1, pass, log" `, }) + +var _ = profile.RegisterProfile(profile.Profile{ + Meta: profile.Meta{ + Author: "M4tteoP", + Description: "Tests CRS ruleRemoveTargetById usage with multiphase and ARGS/ARGS_NAMES", + Enabled: true, + Name: "multiphase_ruleRemoveTargetById_args.yaml", + }, + Tests: []profile.Test{ + { + Title: "ruleRemoveTargetByIdWithARGS", + Stages: []profile.Stage{ + { + Stage: profile.SubStage{ + Input: profile.StageInput{ + URI: "/test/?fbclid=justanid", + Method: "GET", + }, + Output: profile.ExpectedOutput{ + TriggeredRules: []int{942441}, + NonTriggeredRules: []int{942440}, + }, + }, + }, + }, + }, + }, + // Rule 942441 should exclude ARGS:fbclid splitting it into excluding ARGS_GET:fbclidand ARGS_POST:fbclid, + // therefore rule 942440 should not be triggered. + Rules: ` +SecDebugLogLevel 9 + +SecRule ARGS_GET:fbclid "@unconditionalMatch" "id:942441, phase:2,pass,t:none,t:urlDecodeUni,ctl:ruleRemoveTargetById=942440;ARGS:fbclid" +SecRule ARGS_NAMES|ARGS "@rx justanid" "id:942440,phase:2,status:503,log,t:none,t:urlDecodeUni" +`, +}) diff --git a/types/rule_match.go b/types/rule_match.go index 4c85f46a4..a64d4f945 100644 --- a/types/rule_match.go +++ b/types/rule_match.go @@ -45,6 +45,7 @@ type MatchedRule interface { Rule() RuleMetadata AuditLog() string + ErrorLog() string DisruptiveAction() string diff --git a/types/variables/variables.go b/types/variables/variables.go index 64d5cafe2..989988396 100644 --- a/types/variables/variables.go +++ b/types/variables/variables.go @@ -194,6 +194,8 @@ const ( ResBodyProcessorError = variables.ResBodyProcessorError // ResBodyProcessorErrorMsg contains the error message if the response body processor failed ResBodyProcessorErrorMsg = variables.ResBodyProcessorErrorMsg + // MultipartStrictError will be set to 1 when there is an error parsing multipart + MultipartStrictError = variables.MultipartStrictError ) // Parse returns the byte interpretation diff --git a/waf.go b/waf.go index cbed97f9e..e500491fb 100644 --- a/waf.go +++ b/waf.go @@ -4,9 +4,13 @@ package coraza import ( + "context" "fmt" + "strings" + "github.com/crowdsecurity/coraza/v3/experimental" "github.com/crowdsecurity/coraza/v3/internal/corazawaf" + "github.com/crowdsecurity/coraza/v3/internal/environment" "github.com/crowdsecurity/coraza/v3/internal/seclang" "github.com/crowdsecurity/coraza/v3/types" ) @@ -31,6 +35,12 @@ func NewWAF(config WAFConfig) (WAF, error) { waf := corazawaf.NewWAF() + if environment.HasAccessToFS { + if err := environment.IsDirWritable(waf.TmpDir); err != nil { + return nil, fmt.Errorf("filesystem access check: %w. Use 'no_fs_access' build tag, if not available", err) + } + } + if c.debugLogger != nil { waf.Logger = c.debugLogger } @@ -130,7 +140,17 @@ func (w wafWrapper) NewTransaction() types.Transaction { // NewTransactionWithID implements the same method on WAF. func (w wafWrapper) NewTransactionWithID(id string) types.Transaction { - return w.waf.NewTransactionWithID(id) + id = strings.TrimSpace(id) + if len(id) == 0 { + w.waf.Logger.Warn().Msg("Empty ID passed for new transaction") + } + + return w.waf.NewTransactionWithOptions(corazawaf.Options{Context: context.Background(), ID: id}) +} + +// NewTransaction implements the same method on WAF. +func (w wafWrapper) NewTransactionWithOptions(opts experimental.Options) types.Transaction { + return w.waf.NewTransactionWithOptions(opts) } func (w wafWrapper) GetRuleGroup() *corazawaf.RuleGroup {