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 f8eef9dc6..ade1c5480 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@v3 + - uses: actions/stale@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 06d21f0f9..0648fd0ab 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@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/fix-dependabot.yml b/.github/workflows/fix-dependabot.yml deleted file mode 100644 index 045aa84ee..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@v3 - - name: Install Go - uses: actions/setup-go@v3 - with: - go-version: v1.18.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 cf58eed17..fe8027385 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@v2 - - uses: actions/setup-go@v1 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - go-version: '>=1.19.0' + go-version: ">=1.20.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 09a77a785..4aef963eb 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@v3 + - uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: - go-version: v1.19.x + go-version: v1.20.x cache: true - run: go run mage.go lint diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index 07bdd84cc..b9b16f393 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.20.x, 1.21.x, 1.22.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@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@v4 + if: ${{ matrix.go-version == '1.20.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@v4 + if: ${{ matrix.go-version == '1.20.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@v4 + if: ${{ matrix.go-version == '1.20.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@v4 + if: ${{ matrix.go-version == '1.20.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@v4 + if: ${{ matrix.go-version == '1.20.x' }} with: files: build/coverage-tinygo.txt flags: tinygo diff --git a/.github/workflows/tinygo.yml b/.github/workflows/tinygo.yml index b714ce1b2..1bb5f27d3 100644 --- a/.github/workflows/tinygo.yml +++ b/.github/workflows/tinygo.yml @@ -18,23 +18,23 @@ jobs: test: strategy: matrix: - go-version: [1.19.x] + go-version: [1.20.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} cache: true - name: setup tinygo - uses: acifani/setup-tinygo@v1 + uses: acifani/setup-tinygo@v2 with: - tinygo-version: 0.27.0 + tinygo-version: '0.31.2' - name: Cache TinyGo build uses: actions/cache@v3 diff --git a/.gitignore b/.gitignore index 5bd131a25..09d9a69ba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ coraza-waf __debug_bin build/ + +go.work.sum diff --git a/README.md b/README.md index 6bd87a292..288b17b0f 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ The Coraza Project maintains implementations and plugins for the following serve ## Prerequisites -* Go v1.19+ or tinygo compiler +* Go v1.20+ or tinygo compiler * Linux distribution (Debian or Centos recommended), Windows or Mac. ## Coraza Core Usage @@ -58,32 +58,34 @@ Coraza can be used as a library for your Go program to implement a security midd package main import ( - "fmt" - "github.com/corazawaf/coraza/v3" + "fmt" + + "github.com/corazawaf/coraza/v3" ) func main() { - // First we initialize our waf and our seclang parser - waf, err := coraza.NewWAF(coraza.NewWAFConfig(). - WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`)) - // Now we parse our rules - if err != nil { - fmt.Println(err) - } - - // Then we create a transaction and assign some variables - tx := waf.NewTransaction() - defer func() { - tx.ProcessLogging() - tx.Close() - }() - tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345) - - // Finally we process the request headers phase, which may return an interruption - if it := tx.ProcessRequestHeaders(); it != nil { - fmt.Printf("Transaction was interrupted with status %d\n", it.Status) - } + // First we initialize our waf and our seclang parser + waf, err := coraza.NewWAF(coraza.NewWAFConfig(). + WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`)) + // Now we parse our rules + if err != nil { + fmt.Println(err) + } + + // Then we create a transaction and assign some variables + tx := waf.NewTransaction() + defer func() { + tx.ProcessLogging() + tx.Close() + }() + tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345) + + // Finally we process the request headers phase, which may return an interruption + if it := tx.ProcessRequestHeaders(); it != nil { + fmt.Printf("Transaction was interrupted with status %d\n", it.Status) + } } + ``` [Examples/http-server](./examples/http-server/) provides an example to practice with Coraza. @@ -101,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 @@ -132,8 +135,8 @@ Coraza only requires Go for development. You can run `mage.go` to issue developm See the list of commands -```shell -go run mage.go -l +``` +$ go run mage.go -l Targets: check runs lint and tests. coverage runs tests with coverage and race detector enabled. @@ -163,8 +166,8 @@ Our vulnerability management team will respond within 3 working days of your rep ## Thanks -* Modsecurity team for creating ModSecurity * OWASP Coreruleset team for the CRS and their help +* Ivan Ristić for creating ModSecurity ### Coraza on Twitter diff --git a/coraza.conf-recommended b/coraza.conf-recommended index 3b9d8a741..532817ca9 100644 --- a/coraza.conf-recommended +++ b/coraza.conf-recommended @@ -45,7 +45,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 diff --git a/examples/http-server/go.mod b/examples/http-server/go.mod index 08acd1e8c..5ec9b5d18 100644 --- a/examples/http-server/go.mod +++ b/examples/http-server/go.mod @@ -1,12 +1,12 @@ module github.com/corazawaf/coraza/v3/examples/http-server -go 1.18 +go 1.20 require github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24 require ( github.com/magefile/mage v1.15.0 // indirect - github.com/tidwall/gjson v1.17.0 // 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 ) diff --git a/examples/http-server/go.sum b/examples/http-server/go.sum index e91936f74..3dddbd368 100644 --- a/examples/http-server/go.sum +++ b/examples/http-server/go.sum @@ -2,8 +2,8 @@ github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24 h1:dy3992o5ue4 github.com/corazawaf/coraza/v3 v3.0.0-20220914101451-05d352c89b24/go.mod h1:xhc7feR6FUfYgmBmRw3UObvLiyzT3XPQtlJD+huy+Mc= 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/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +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= diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go index ab5c71142..ecbee00b5 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..1318ce303 --- /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/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/experimental/plugins" + "github.com/corazawaf/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/macro/macro.go b/experimental/plugins/macro/macro.go index 6fc10ce91..ec756f0d8 100644 --- a/experimental/plugins/macro/macro.go +++ b/experimental/plugins/macro/macro.go @@ -82,6 +82,8 @@ func expandToken(tx plugintypes.TransactionState, token macroToken) string { } } + // If the variable is known (e.g. TX) but the key is not found, we return the original text + tx.DebugLogger().Warn().Str("variable", token.variable.Name()).Str("key", token.key).Msg("key not found in collection, returning the original text") return token.text } diff --git a/experimental/waf.go b/experimental/waf.go new file mode 100644 index 000000000..bc167a2a9 --- /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/corazawaf/coraza/v3/internal/corazawaf" + "github.com/corazawaf/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..6d97c22fc --- /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/corazawaf/coraza/v3" + "github.com/corazawaf/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 5b4f878f3..712b00ec2 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/corazawaf/coraza/v3 -go 1.19 +go 1.20 // 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.9.0 - github.com/petar-dambovaliev/aho-corasick v0.0.0-20230725210150-fb29fc3c913e - github.com/tidwall/gjson v1.17.0 - golang.org/x/net v0.17.0 - golang.org/x/sync v0.4.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.26.0 + golang.org/x/sync v0.7.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.13.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect ) diff --git a/go.sum b/go.sum index af1b73754..a6fe8c632 100644 --- a/go.sum +++ b/go.sum @@ -1,67 +1,88 @@ 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.9.0 h1:0c8loz/kMEdBmcHJZh0MUgKX84U19AlLk7h6nf2Wkx4= -github.com/mccutchen/go-httpbin/v2 v2.9.0/go.mod h1:+DBHcmg6EOeoizuiOI8iL12VIHXx+9YQNlz+gjB9uxk= -github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -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.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +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.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +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/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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +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.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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..eb7a04e4d 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,4 @@ -go 1.19 +go 1.20 use ( . diff --git a/go.work.sum b/go.work.sum deleted file mode 100644 index 569ff9723..000000000 --- a/go.work.sum +++ /dev/null @@ -1,6 +0,0 @@ -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/http/e2e/cmd/httpe2e/main.go b/http/e2e/cmd/httpe2e/main.go index e60899bf4..47607cbd9 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 0447dec6b..8f427194f 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 6b14aead3..06d765635 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 @@ -108,7 +119,23 @@ func (i *rwInterceptor) Header() http.Header { return i.w.Header() } -var _ http.ResponseWriter = (*rwInterceptor)(nil) +func (i *rwInterceptor) ReadFrom(r io.Reader) (n int64, err error) { + return io.Copy(struct{ io.Writer }{i}, r) +} + +func (i *rwInterceptor) Flush() { + if !i.wroteHeader { + i.WriteHeader(http.StatusOK) + } +} + +type responseWriter interface { + http.ResponseWriter + io.ReaderFrom + http.Flusher +} + +var _ responseWriter = (*rwInterceptor)(nil) // wrap wraps the interceptor into a response writer that also preserves // the http interfaces implemented by the original response writer to avoid @@ -137,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() @@ -168,110 +197,28 @@ func wrap(w http.ResponseWriter, r *http.Request, tx types.Transaction) ( var ( hijacker, isHijacker = i.w.(http.Hijacker) pusher, isPusher = i.w.(http.Pusher) - flusher, isFlusher = i.w.(http.Flusher) - reader, isReader = i.w.(io.ReaderFrom) ) switch { - case !isHijacker && !isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter - }{i}, responseProcessor - case !isHijacker && !isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - io.ReaderFrom - }{i, reader}, responseProcessor - case !isHijacker && !isPusher && isFlusher && !isReader: + case !isHijacker && isPusher: return struct { - http.ResponseWriter - http.Flusher - }{i, flusher}, responseProcessor - case !isHijacker && !isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Flusher - io.ReaderFrom - }{i, flusher, reader}, responseProcessor - case !isHijacker && isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter + responseWriter http.Pusher }{i, pusher}, responseProcessor - case !isHijacker && isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - http.Pusher - io.ReaderFrom - }{i, pusher, reader}, responseProcessor - case !isHijacker && isPusher && isFlusher && !isReader: + case isHijacker && !isPusher: return struct { - http.ResponseWriter - http.Pusher - http.Flusher - }{i, pusher, flusher}, responseProcessor - case !isHijacker && isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Pusher - http.Flusher - io.ReaderFrom - }{i, pusher, flusher, reader}, responseProcessor - case isHijacker && !isPusher && !isFlusher && !isReader: - return struct { - http.ResponseWriter + responseWriter http.Hijacker }{i, hijacker}, responseProcessor - case isHijacker && !isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - io.ReaderFrom - }{i, hijacker, reader}, responseProcessor - case isHijacker && !isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Flusher - }{i, hijacker, flusher}, responseProcessor - case isHijacker && !isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Flusher - io.ReaderFrom - }{i, hijacker, flusher, reader}, responseProcessor - case isHijacker && isPusher && !isFlusher && !isReader: + case isHijacker && isPusher: return struct { - http.ResponseWriter + responseWriter http.Hijacker http.Pusher }{i, hijacker, pusher}, responseProcessor - case isHijacker && isPusher && !isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - io.ReaderFrom - }{i, hijacker, pusher, reader}, responseProcessor - case isHijacker && isPusher && isFlusher && !isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - http.Flusher - }{i, hijacker, pusher, flusher}, responseProcessor - case isHijacker && isPusher && isFlusher && isReader: - return struct { - http.ResponseWriter - http.Hijacker - http.Pusher - http.Flusher - io.ReaderFrom - }{i, hijacker, pusher, flusher, reader}, responseProcessor default: return struct { - http.ResponseWriter + responseWriter }{i}, responseProcessor } } diff --git a/http/interceptor_test.go b/http/interceptor_test.go index e8424705b..d232996bf 100644 --- a/http/interceptor_test.go +++ b/http/interceptor_test.go @@ -8,6 +8,10 @@ package http import ( + "bufio" + "bytes" + "io" + "net" "net/http" "net/http/httptest" "testing" @@ -44,3 +48,286 @@ func TestWriteHeader(t *testing.T) { t.Errorf("unexpected status code, want %d, have %d", want, have) } } + +func TestWrite(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + rw, responseProcessor := wrap(res, req, tx) + _, err = rw.Write([]byte("hello")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = rw.Write([]byte("world")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +func TestWriteWithWriteHeader(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + rw, responseProcessor := wrap(res, req, tx) + rw.WriteHeader(204) + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + _, err = rw.Write([]byte("hello")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = rw.Write([]byte("world")) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +func TestFlush(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + t.Run("WriteHeader before Flush", func(t *testing.T) { + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + rw, responseProcessor := wrap(res, req, tx) + rw.WriteHeader(204) + rw.(http.Flusher).Flush() + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + }) + + t.Run("Flush before WriteHeader", func(t *testing.T) { + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + rw, responseProcessor := wrap(res, req, tx) + rw.(http.Flusher).Flush() + rw.WriteHeader(204) + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 200, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } + }) +} + +type testReaderFrom struct { + io.Writer +} + +func (x *testReaderFrom) ReadFrom(r io.Reader) (n int64, err error) { + return io.Copy(x, r) +} + +func TestReadFrom(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + type responseWriter interface { + http.ResponseWriter + http.Flusher + } + + resWithReaderFrom := struct { + responseWriter + io.ReaderFrom + }{ + res, + &testReaderFrom{res}, + } + + rw, responseProcessor := wrap(resWithReaderFrom, req, tx) + rw.WriteHeader(204) + // although we called WriteHeader, status code should be applied until + // responseProcessor is called. + if unwanted, have := 204, res.Code; unwanted == have { + t.Errorf("unexpected status code %d", have) + } + + _, err = rw.(io.ReaderFrom).ReadFrom(bytes.NewBuffer([]byte("hello world"))) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + _, err = rw.(io.ReaderFrom).ReadFrom(struct{ io.Reader }{bytes.NewBuffer([]byte("hello world"))}) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + err = responseProcessor(tx, req) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if want, have := 204, res.Code; want != have { + t.Errorf("unexpected status code, want %d, have %d", want, have) + } +} + +type testPusher struct{} + +func (x *testPusher) Push(string, *http.PushOptions) error { + return nil +} + +type testHijacker struct{} + +func (x *testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, nil +} + +func TestInterface(t *testing.T) { + waf, err := coraza.NewWAF(coraza.NewWAFConfig()) + if err != nil { + t.Fatal(err) + } + + tx := waf.NewTransaction() + req, _ := http.NewRequest("GET", "", nil) + res := httptest.NewRecorder() + + t.Run("default", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + }{ + res, + }, req, tx) + + _, ok := rw.(http.Pusher) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Pusher") + } + + _, ok = rw.(http.Hijacker) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Hijacker") + } + }) + + t.Run("http.Pusher", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Pusher + }{ + res, + &testPusher{}, + }, req, tx) + + _, ok := rw.(http.Pusher) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Pusher") + } + + _, ok = rw.(http.Hijacker) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Hijacker") + } + }) + + t.Run("http.Hijacker", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Hijacker + }{ + res, + &testHijacker{}, + }, req, tx) + + _, ok := rw.(http.Hijacker) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Hijacker") + } + + _, ok = rw.(http.Pusher) + if ok { + t.Errorf("expected the wrapped ResponseWriter to not implement http.Pusher") + } + }) + + t.Run("http.Hijacker and http.Pusher", func(t *testing.T) { + rw, _ := wrap(struct { + http.ResponseWriter + http.Hijacker + http.Pusher + }{ + res, + &testHijacker{}, + &testPusher{}, + }, req, tx) + + _, ok := rw.(http.Hijacker) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Hijacker") + } + + _, ok = rw.(http.Pusher) + if !ok { + t.Errorf("expected the wrapped ResponseWriter to implement http.Pusher") + } + }) +} diff --git a/http/middleware.go b/http/middleware.go index 666f56399..acb72e16d 100644 --- a/http/middleware.go +++ b/http/middleware.go @@ -15,6 +15,7 @@ import ( "strings" "github.com/corazawaf/coraza/v3" + "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/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 404dfaa2d..249f2f661 100644 --- a/http/middleware_test.go +++ b/http/middleware_test.go @@ -101,8 +101,7 @@ SecRule &REQUEST_HEADERS:Transfer-Encoding "!@eq 0" "id:1,phase:1,deny" } if it == nil { t.Fatal("Expected interruption") - } - if it.RuleID != 1 { + } else if it.RuleID != 1 { t.Fatalf("Expected rule 1 to be triggered, got rule %d", it.RuleID) } if err := tx.Close(); err != nil { @@ -239,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", @@ -284,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, }, } @@ -358,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(). @@ -459,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) @@ -481,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 bb6882a76..252d3879b 100644 --- a/internal/actions/ctl_test.go +++ b/internal/actions/ctl_test.go @@ -237,7 +237,7 @@ func TestCtl(t *testing.T) { } }, }, - "responseBodyLimit successfuly": { + "responseBodyLimit successfully": { input: "responseBodyLimit=12345", prepareTX: func(tx *corazawaf.Transaction) { tx.ProcessRequestHeaders() @@ -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/id.go b/internal/actions/id.go index 4dcd80266..c82a4058a 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/logdata.go b/internal/actions/logdata.go index 03a602d74..24fa05888 100644 --- a/internal/actions/logdata.go +++ b/internal/actions/logdata.go @@ -37,7 +37,7 @@ func (a *logdataFn) Init(r plugintypes.RuleMetadata, data string) error { } func (a *logdataFn) Evaluate(r plugintypes.RuleMetadata, tx plugintypes.TransactionState) { - tx.(*corazawaf.Transaction).Logdata = r.(*corazawaf.Rule).LogData.Expand(tx) + // logdata macro expansion is performed after all other actions have been evaluated (and potentially all the needed variables have been set) } func (a *logdataFn) Type() plugintypes.ActionType { diff --git a/internal/actions/setvar.go b/internal/actions/setvar.go index a372b4410..2c3635206 100644 --- a/internal/actions/setvar.go +++ b/internal/actions/setvar.go @@ -136,20 +136,19 @@ func (a *setvarFn) evaluateTxCollection(r plugintypes.RuleMetadata, tx plugintyp col.Remove(key) return } - res := "" + currentVal := "" if r := col.Get(key); len(r) > 0 { - res = r[0] + currentVal = r[0] } var err error switch { case len(value) == 0: // if nothing to input col.Set(key, []string{""}) - case value[0] == '+': - // if we want to sum - sum := 0 + case value[0] == '+', value[0] == '-': // Increment or decrement, arithmetical operations + val := 0 if len(value) > 1 { - sum, err = strconv.Atoi(value[1:]) + val, err = strconv.Atoi(value[1:]) if err != nil { tx.DebugLogger().Error(). Str("var_value", value). @@ -159,26 +158,23 @@ func (a *setvarFn) evaluateTxCollection(r plugintypes.RuleMetadata, tx plugintyp return } } - val := 0 - if res != "" { - val, err = strconv.Atoi(res) + currentValInt := 0 + if currentVal != "" { + currentValInt, err = strconv.Atoi(currentVal) if err != nil { tx.DebugLogger().Error(). - Str("var_key", res). + Str("var_key", currentVal). Int("rule_id", r.ID()). Err(err). Msg("Invalid value") return } } - col.Set(key, []string{strconv.Itoa(sum + val)}) - case value[0] == '-': - me, _ := strconv.Atoi(value[1:]) - txv, err := strconv.Atoi(res) - if err != nil { - return + if value[0] == '+' { + col.Set(key, []string{strconv.Itoa(currentValInt + val)}) + } else { + col.Set(key, []string{strconv.Itoa(currentValInt - val)}) } - col.Set(key, []string{strconv.Itoa(txv - me)}) default: col.Set(key, []string{value}) } diff --git a/internal/actions/setvar_test.go b/internal/actions/setvar_test.go index c512c83a6..591d5a853 100644 --- a/internal/actions/setvar_test.go +++ b/internal/actions/setvar_test.go @@ -4,19 +4,26 @@ package actions import ( + "bytes" + "strings" "testing" + + "github.com/corazawaf/coraza/v3/collection" + "github.com/corazawaf/coraza/v3/debuglog" + "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" + "github.com/corazawaf/coraza/v3/internal/corazawaf" ) type md struct { } -func (_ md) ID() int { +func (md) ID() int { return 0 } -func (_ md) ParentID() int { +func (md) ParentID() int { return 0 } -func (_ md) Status() int { +func (md) Status() int { return 0 } @@ -46,3 +53,115 @@ func TestSetvarInit(t *testing.T) { } }) } + +var invalidSyntaxAtoiError = "invalid syntax" +var warningKeyNotFoundInCollection = "key not found in collection" + +func TestSetvarEvaluate(t *testing.T) { + tests := []struct { + name string + init string + init2 string + expectInvalidSyntaxError bool + expectNewVarValue string + }{ + { + name: "Numerical operation + with existing variable", + init: "TX.var=5", + init2: "TX.newvar=+%{tx.var}", + expectInvalidSyntaxError: false, + expectNewVarValue: "5", + }, + { + name: "Numerical operation - with existing variable", + init: "TX.var=5", + init2: "TX.newvar=-%{tx.var}", + expectInvalidSyntaxError: false, + expectNewVarValue: "-5", + }, + { + name: "Numerical operation - with existing negative variable", + init: "TX.newvar=-5", + init2: "TX.newvar=+5", + expectInvalidSyntaxError: false, + expectNewVarValue: "0", + }, + { + name: "Numerical operation + with missing (or non-numerical) variable", + init: "TX.newvar=+%{tx.missingvar}", + expectInvalidSyntaxError: true, + }, + { + name: "Numerical operation - with missing (or non-numerical) variable", + init: "TX.newvar=-%{tx.missingvar}", + expectInvalidSyntaxError: true, + }, + } + + for _, tt := range tests { + logsBuf := &bytes.Buffer{} + + logger := debuglog.Default().WithLevel(debuglog.LevelWarn).WithOutput(logsBuf) + + t.Run(tt.name, func(t *testing.T) { + defer logsBuf.Reset() + a := setvar() + metadata := &md{} + if err := a.Init(metadata, tt.init); err != nil { + t.Error("unexpected error during setvar init") + } + waf := corazawaf.NewWAF() + waf.Logger = logger + tx := waf.NewTransaction() + a.Evaluate(metadata, tx) + if tt.expectInvalidSyntaxError { + if logsBuf.Len() == 0 { + t.Fatal("expected error") + } + if !strings.Contains(logsBuf.String(), invalidSyntaxAtoiError) { + t.Errorf("expected error containing %q, got %q", invalidSyntaxAtoiError, logsBuf.String()) + } + if !strings.Contains(logsBuf.String(), warningKeyNotFoundInCollection) { + t.Errorf("expected error containing %q, got %q", warningKeyNotFoundInCollection, logsBuf.String()) + } + } + if logsBuf.Len() != 0 && !tt.expectInvalidSyntaxError { + t.Fatalf("unexpected error: %s", logsBuf.String()) + } + + if tt.init2 != "" { + if err := a.Init(metadata, tt.init2); err != nil { + t.Fatal("unexpected error during setvar init") + } + a.Evaluate(metadata, tx) + if logsBuf.Len() != 0 && !tt.expectInvalidSyntaxError { + t.Fatalf("unexpected error: %s", logsBuf.String()) + } + } + if tt.expectNewVarValue != "" { + checkCollectionValue(t, a.(*setvarFn), tx, "newvar", tt.expectNewVarValue) + } + }) + } +} + +func checkCollectionValue(t *testing.T, a *setvarFn, tx plugintypes.TransactionState, key string, expected string) { + t.Helper() + var col collection.Map + if c, ok := tx.Collection(a.collection).(collection.Map); !ok { + t.Fatal("collection in setvar is not a map") + return + } else { + col = c + } + if col == nil { + t.Fatal("collection in setvar is nil") + return + } + if col == nil { + t.Fatal("collection is nil") + } + if col.Get(key)[0] != expected { + t.Errorf("key %q: expected %q, got %q", key, expected, col.Get(key)) + } +} diff --git a/internal/auditlog/concurrent_writer.go b/internal/auditlog/concurrent_writer.go index fab2382cc..ab6d4bcc0 100644 --- a/internal/auditlog/concurrent_writer.go +++ b/internal/auditlog/concurrent_writer.go @@ -55,6 +55,15 @@ func (cl concurrentWriter) Write(al plugintypes.AuditLog) error { return nil } + formattedAL, err := cl.formatter.Format(al) + if err != nil { + return err + } + + if len(formattedAL) == 0 { + return nil + } + // 192.168.3.130 192.168.3.1 - - [22/Aug/2009:13:24:20 +0100] "GET / HTTP/1.1" 200 56 "-" "-" SojdH8AAQEAAAugAQAAAAAA "-" /20090822/20090822-1324/20090822-132420-SojdH8AAQEAAAugAQAAAAAA 0 1248 t := time.Unix(0, al.Transaction().UnixTimestamp()) @@ -67,11 +76,6 @@ func (cl concurrentWriter) Write(al plugintypes.AuditLog) error { return err } - formattedAL, err := cl.formatter.Format(al) - if err != nil { - return err - } - filepath := path.Join(logdir, filename) if err = os.WriteFile(filepath, formattedAL, cl.logFileMode); err != nil { return err diff --git a/internal/auditlog/concurrent_writer_test.go b/internal/auditlog/concurrent_writer_test.go index 1cbde6613..f3511bec5 100644 --- a/internal/auditlog/concurrent_writer_test.go +++ b/internal/auditlog/concurrent_writer_test.go @@ -8,6 +8,7 @@ package auditlog import ( "encoding/json" + "errors" "fmt" "io/fs" "os" @@ -46,7 +47,51 @@ func TestConcurrentWriterFailsOnInit(t *testing.T) { } } -func TestConcurrentWriterWrites(t *testing.T) { +type mockFormatter struct { + plugintypes.AuditLogFormatter + formatted []byte + err error +} + +func (ef mockFormatter) Format(plugintypes.AuditLog) ([]byte, error) { + return ef.formatted, ef.err +} + +func TestConcurrentWriter(t *testing.T) { + t.Run("empty formatted", func(t *testing.T) { + config := plugintypes.AuditLogConfig{ + Target: os.DevNull, + Formatter: mockFormatter{}, + } + + writer := &concurrentWriter{} + if err := writer.Init(config); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if err := writer.Write(nil); err != nil { + t.Errorf("unexpected error: %v", err) + } + }) + + t.Run("formatting error", func(t *testing.T) { + config := plugintypes.AuditLogConfig{ + Target: os.DevNull, + Formatter: mockFormatter{err: errors.New("formatting error")}, + } + + writer := &concurrentWriter{} + if err := writer.Init(config); err != nil { + t.Errorf("unexpected error: %v", err) + } + + if err := writer.Write(nil); err == nil { + t.Errorf("expected error: %v", err) + } + }) +} + +func TestConcurrentWriterSuccess(t *testing.T) { dir := t.TempDir() file, err := os.Create(filepath.Join(dir, "audit.log")) if err != nil { @@ -59,6 +104,12 @@ func TestConcurrentWriterWrites(t *testing.T) { DirMode: fs.FileMode(0777), Formatter: &jsonFormatter{}, } + + writer := &concurrentWriter{} + if err := writer.Init(config); err != nil { + t.Error("failed to init concurrent logger", err) + } + ts := time.Now() expectedLog := &Log{ Transaction_: Transaction{ @@ -74,10 +125,6 @@ func TestConcurrentWriterWrites(t *testing.T) { }, }, } - writer := &concurrentWriter{} - if err := writer.Init(config); err != nil { - t.Error("failed to init concurrent logger", err) - } if err := writer.Write(expectedLog); err != nil { t.Error("failed to write to logger: ", err) } diff --git a/internal/auditlog/formats.go b/internal/auditlog/formats.go index 82ad71d05..dab2c91c4 100644 --- a/internal/auditlog/formats.go +++ b/internal/auditlog/formats.go @@ -31,6 +31,10 @@ import ( type nativeFormatter struct{} func (nativeFormatter) Format(al plugintypes.AuditLog) ([]byte, error) { + if len(al.Parts()) == 0 { + return nil, nil + } + boundaryPrefix := fmt.Sprintf("--%s-", utils.RandomString(10)) var res strings.Builder @@ -56,31 +60,36 @@ func (nativeFormatter) Format(al plugintypes.AuditLog) ([]byte, error) { // Content-Length: 6 _, _ = fmt.Fprintf( &res, - "%s %s %s\n", + "\n%s %s %s", al.Transaction().Request().Method(), al.Transaction().Request().URI(), al.Transaction().Request().Protocol(), ) for k, vv := range al.Transaction().Request().Headers() { for _, v := range vv { + res.WriteByte('\n') res.WriteString(k) res.WriteString(": ") res.WriteString(v) - res.WriteByte('\n') } } case types.AuditLogPartRequestBody: - // b=test - res.WriteString(al.Transaction().Request().Body()) + if body := al.Transaction().Request().Body(); body != "" { + res.WriteByte('\n') + res.WriteString(body) + } case types.AuditLogPartIntermediaryResponseBody: - res.WriteString(al.Transaction().Response().Body()) + if body := al.Transaction().Response().Body(); body != "" { + res.WriteByte('\n') + res.WriteString(al.Transaction().Response().Body()) + } case types.AuditLogPartResponseHeaders: for k, vv := range al.Transaction().Response().Headers() { for _, v := range vv { + res.WriteByte('\n') res.WriteString(k) res.WriteString(": ") res.WriteString(v) - res.WriteByte('\n') } } case types.AuditLogPartAuditLogTrailer: @@ -91,11 +100,11 @@ func (nativeFormatter) Format(al plugintypes.AuditLog) ([]byte, error) { // Producer: ModSecurity for Apache/2.9.1 (http://www.modsecurity.org/). // Server: Apache // Engine-Mode: "ENABLED" - _, _ = fmt.Fprintf(&res, "Stopwatch: %s\nResponse-Body-Transformed: %s\nProducer: %s\nServer: %s", "", "", "", "") + _, _ = fmt.Fprintf(&res, "\nStopwatch: %s\nResponse-Body-Transformed: %s\nProducer: %s\nServer: %s", "", "", "", "") case types.AuditLogPartRulesMatched: for _, r := range al.Messages() { - res.WriteString(r.Data().Raw()) res.WriteByte('\n') + res.WriteString(r.Data().Raw()) } } res.WriteByte('\n') diff --git a/internal/auditlog/formats_json.go b/internal/auditlog/formats_json.go index 7d96c26ff..f208022a4 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/formats_test.go b/internal/auditlog/formats_test.go index 68712a2d1..91381aad4 100644 --- a/internal/auditlog/formats_test.go +++ b/internal/auditlog/formats_test.go @@ -4,7 +4,9 @@ package auditlog import ( + "bufio" "bytes" + "fmt" "strings" "testing" @@ -12,20 +14,77 @@ import ( "github.com/corazawaf/coraza/v3/types" ) +func checkLine(t *testing.T, lines []string, index int, expected string) { + t.Helper() + if lines[index] != expected { + auditLog := &strings.Builder{} + auditLog.WriteByte('\n') + for i, line := range lines { + auditLog.WriteString(fmt.Sprintf("Line %d: ", i)) + auditLog.WriteString(line) + auditLog.WriteByte('\n') + } + t.Log(auditLog.String()) + t.Fatalf("unexpected line %d, \ngot: %q\nwant: %q\n", index, lines[index], expected) + } +} + +func mutateSeparator(separator string, part byte) string { + return separator[:len(separator)-3] + string(part) + separator[len(separator)-2:] +} + func TestNativeFormatter(t *testing.T) { - al := createAuditLog() f := &nativeFormatter{} - data, err := f.Format(al) - if err != nil { - t.Error(err) - } - if !strings.Contains(f.MIME(), "x-coraza-auditlog-native") { - t.Errorf("failed to match MIME, expected json and got %s", f.MIME()) - } - // Log contains random strings, do a simple sanity check - if !bytes.Contains(data, []byte("[02/Jan/2006:15:04:20 -0700] 123 0 0")) { - t.Errorf("failed to match log, \ngot: %s\n", string(data)) - } + + t.Run("empty parts", func(t *testing.T) { + al := &Log{} + l, err := f.Format(al) + if l != nil { + t.Error("expected nil log") + } + if err != nil { + t.Error("unexpected error") + } + }) + + t.Run("success", func(t *testing.T) { + al := createAuditLog() + data, err := f.Format(al) + if err != nil { + t.Error(err) + } + if !strings.Contains(f.MIME(), "x-coraza-auditlog-native") { + t.Errorf("failed to match MIME, expected json and got %s", f.MIME()) + } + // Log contains random strings, do a simple sanity check + if !bytes.Contains(data, []byte("[02/Jan/2006:15:04:20 -0700] 123 0 0")) { + t.Errorf("failed to match log, \ngot: %s\n", string(data)) + } + + scanner := bufio.NewScanner(bytes.NewReader(data)) + + var lines []string + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + separator := lines[0] + + checkLine(t, lines, 2, "GET /test.php HTTP/1.1") + checkLine(t, lines, 3, "some: request header") + checkLine(t, lines, 4, mutateSeparator(separator, 'C')) + checkLine(t, lines, 6, "some request body") + checkLine(t, lines, 7, mutateSeparator(separator, 'E')) + checkLine(t, lines, 9, "some response body") + checkLine(t, lines, 10, mutateSeparator(separator, 'F')) + checkLine(t, lines, 12, "some: response header") + checkLine(t, lines, 13, mutateSeparator(separator, 'H')) + checkLine(t, lines, 15, "Stopwatch: ") + checkLine(t, lines, 16, "Response-Body-Transformed: ") + checkLine(t, lines, 17, "Producer: ") + checkLine(t, lines, 18, "Server: ") + checkLine(t, lines, 19, mutateSeparator(separator, 'K')) + checkLine(t, lines, 21, `SecAction "id:100"`) + }) } func createAuditLog() *Log { @@ -47,17 +106,20 @@ func createAuditLog() *Log { Method_: "GET", Headers_: map[string][]string{ "some": { - "somedata", + "request header", }, }, + Body_: "some request body", + Protocol_: "HTTP/1.1", }, Response_: &TransactionResponse{ Status_: 200, Headers_: map[string][]string{ "some": { - "somedata", + "response header", }, }, + Body_: "some response body", }, Producer_: &TransactionProducer{ Connector_: "some connector", diff --git a/internal/auditlog/init_tinygo.go b/internal/auditlog/init_tinygo.go index 62ee561b8..ebf5244eb 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 d3926bddb..b60e9d37e 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 054e36f94..78e21df65 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 b9e59e71b..6b648fc6f 100644 --- a/internal/auditlog/noop_writer.go +++ b/internal/auditlog/noop_writer.go @@ -9,7 +9,7 @@ package auditlog import "github.com/corazawaf/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/auditlog/serial_writer.go b/internal/auditlog/serial_writer.go index d752d541a..9626db0ae 100644 --- a/internal/auditlog/serial_writer.go +++ b/internal/auditlog/serial_writer.go @@ -54,6 +54,11 @@ func (sl *serialWriter) Write(al plugintypes.AuditLog) error { if err != nil { return err } + + if len(bts) == 0 { + return nil + } + sl.logger.Println(string(bts)) return nil } diff --git a/internal/bodyprocessors/raw.go b/internal/bodyprocessors/raw.go new file mode 100644 index 000000000..6f7cf7316 --- /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/corazawaf/coraza/v3/experimental/plugins/plugintypes" + "github.com/corazawaf/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..28b9ae749 --- /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/corazawaf/coraza/v3/experimental/plugins/plugintypes" + "github.com/corazawaf/coraza/v3/internal/bodyprocessors" + "github.com/corazawaf/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/bodyprocessors/urlencoded.go b/internal/bodyprocessors/urlencoded.go index 3a4c26e81..fe0176bc4 100644 --- a/internal/bodyprocessors/urlencoded.go +++ b/internal/bodyprocessors/urlencoded.go @@ -10,7 +10,7 @@ import ( "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" "github.com/corazawaf/coraza/v3/internal/collections" - "github.com/corazawaf/coraza/v3/internal/url" + urlutil "github.com/corazawaf/coraza/v3/internal/url" ) type urlencodedBodyProcessor struct { @@ -23,7 +23,7 @@ func (*urlencodedBodyProcessor) ProcessRequest(reader io.Reader, v plugintypes.T } b := buf.String() - values := url.ParseQuery(b, '&') + values := urlutil.ParseQuery(b, '&') argsCol := v.ArgsPost() for k, vs := range values { argsCol.Set(k, vs) diff --git a/internal/collections/map.go b/internal/collections/map.go index eca4dcd38..069c8e6a9 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 c73e09dd5..68bb83c42 100644 --- a/internal/collections/map_test.go +++ b/internal/collections/map_test.go @@ -21,8 +21,53 @@ import ( "github.com/corazawaf/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 8cfb81240..88d9166fd 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 new file mode 100644 index 000000000..4da61f2b4 --- /dev/null +++ b/internal/cookies/cookies.go @@ -0,0 +1,39 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package cookies + +import ( + "net/textproto" + "strings" +) + +// ParseCookies parses cookies and splits in name, value pairs. Won't check for valid names nor values. +// If there are multiple cookies with the same name, it will append to the list with the same name key. +// Loosely based in the stdlib src/net/http/cookie.go +func ParseCookies(rawCookies string) map[string][]string { + cookies := make(map[string][]string) + + rawCookies = textproto.TrimString(rawCookies) + + if rawCookies == "" { + return cookies + } + + var part string + for len(rawCookies) > 0 { // continue since we have rest + part, rawCookies, _ = strings.Cut(rawCookies, ";") + part = textproto.TrimString(part) + if part == "" { + continue + } + name, val, _ := strings.Cut(part, "=") + name = textproto.TrimString(name) + // if name is empty (eg: "Cookie: =foo;") skip it + if name == "" { + continue + } + cookies[name] = append(cookies[name], val) + } + return cookies +} diff --git a/internal/cookies/cookies_test.go b/internal/cookies/cookies_test.go new file mode 100644 index 000000000..d1595a778 --- /dev/null +++ b/internal/cookies/cookies_test.go @@ -0,0 +1,100 @@ +// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package cookies + +import ( + "testing" +) + +func equalMaps(map1 map[string][]string, map2 map[string][]string) bool { + if len(map1) != len(map2) { + return false + } + + // Iterate through the key-value pairs of the first map + for key, slice1 := range map1 { + // Check if the key exists in the second map + slice2, ok := map2[key] + if !ok { + return false + } + + // Compare the values of the corresponding keys + for i, val1 := range slice1 { + val2 := slice2[i] + + // Compare the elements + if val1 != val2 { + return false + } + } + } + + return true +} + +func TestParseCookies(t *testing.T) { + type args struct { + rawCookies string + } + tests := []struct { + name string + args args + want map[string][]string + }{ + { + name: "EmptyString", + args: args{rawCookies: " "}, + want: map[string][]string{}, + }, + { + name: "SimpleCookie", + args: args{rawCookies: "test=test_value"}, + want: map[string][]string{"test": {"test_value"}}, + }, + { + name: "MultipleCookies", + args: args{rawCookies: "test1=test_value1; test2=test_value2"}, + want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}}, + }, + { + name: "SpacesInCookieName", + args: args{rawCookies: " test1 =test_value1; test2 =test_value2"}, + want: map[string][]string{"test1": {"test_value1"}, "test2": {"test_value2"}}, + }, + { + name: "SpacesInCookieValue", + args: args{rawCookies: "test1=test _value1; test2 =test_value2"}, + want: map[string][]string{"test1": {"test _value1"}, "test2": {"test_value2"}}, + }, + { + name: "EmptyCookie", + args: args{rawCookies: ";;foo=bar"}, + want: map[string][]string{"foo": {"bar"}}, + }, + { + name: "EmptyName", + args: args{rawCookies: "=bar;"}, + want: map[string][]string{}, + }, + { + name: "MultipleEqualsInValues", + args: args{rawCookies: "test1=val==ue1;test2=value2"}, + want: map[string][]string{"test1": {"val==ue1"}, "test2": {"value2"}}, + }, + { + name: "RepeatedCookieNameShouldGiveList", + args: args{rawCookies: "test1=value1;test1=value2"}, + want: map[string][]string{"test1": {"value1", "value2"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseCookies(tt.args.rawCookies) + if !equalMaps(got, tt.want) { + t.Errorf("ParseCookies() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/corazarules/rule.go b/internal/corazarules/rule.go index bcf49c8cf..09822481e 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 8810bc2d6..67f8f7dc1 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_ } @@ -100,8 +103,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_ } @@ -142,7 +149,36 @@ func (mr *MatchedRule) Rule() types.RuleMetadata { return mr.Rule_ } -const maxSizeLogMessage = 200 +// 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_ +} + +const maxSizeLogMessage = 280 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 14016b957..a1cbbf2d2 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/corazawaf/coraza/v3/debuglog" "github.com/corazawaf/coraza/v3/experimental/plugins/macro" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" "github.com/corazawaf/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 @@ -169,29 +165,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()) } @@ -202,7 +205,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 @@ -233,26 +236,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{ @@ -264,8 +274,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) } @@ -290,7 +304,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) } } @@ -303,19 +317,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") } } } @@ -332,8 +336,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 } @@ -342,8 +353,8 @@ 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 fist rule. - if !r.MultiMatch { + // matches and to rely on MATCHED_* variables updated by the chain, not just by the first rule. + if r.HasChain || r.operator == nil { if r.Msg != nil { matchedValues[0].(*corazarules.MatchData).Message_ = r.Msg.Expand(tx) } @@ -355,11 +366,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) } } @@ -449,16 +460,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) @@ -468,53 +518,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, @@ -525,9 +551,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) @@ -538,19 +563,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..96e23cdc9 --- /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/corazawaf/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 472dcb27d..06d9c219e 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/corazawaf/coraza/v3/debuglog" "github.com/corazawaf/coraza/v3/experimental/plugins/macro" "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" "github.com/corazawaf/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") + } + }) } } @@ -95,7 +121,8 @@ func (*dummyFlowAction) Init(_ plugintypes.RuleMetadata, _ string) error { } func (*dummyFlowAction) Evaluate(_ plugintypes.RuleMetadata, tx plugintypes.TransactionState) { - tx.(*Transaction).Logdata = "flow action triggered" + // SkipAfter is used in a improper way, just for testing purposes ensuring that the action has been enforced + tx.(*Transaction).SkipAfter = "flow action triggered" } func (*dummyFlowAction) Type() plugintypes.ActionType { @@ -105,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) @@ -112,11 +140,11 @@ 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)) } - if tx.Logdata != "flow action triggered" { + if tx.SkipAfter != "flow action triggered" { t.Errorf("Expected flow action triggered with DetectionOnly engine") } } @@ -128,7 +156,7 @@ func (*dummyNonDisruptiveAction) Init(_ plugintypes.RuleMetadata, _ string) erro } func (*dummyNonDisruptiveAction) Evaluate(_ plugintypes.RuleMetadata, tx plugintypes.TransactionState) { - tx.(*Transaction).Logdata = "action enforced" + tx.(*Transaction).SkipAfter = "action enforced" } func (*dummyNonDisruptiveAction) Type() plugintypes.ActionType { @@ -142,7 +170,7 @@ func TestMatchVariableRunsActionTypeNondisruptive(t *testing.T) { action := &dummyNonDisruptiveAction{} _ = rule.AddAction("dummyNonDisruptiveAction", action) rule.matchVariable(tx, md) - if tx.Logdata != "action enforced" { + if tx.SkipAfter != "action enforced" { t.Errorf("Expected non disruptive action to be enforced during matchVariable") } } @@ -150,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{} @@ -157,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) @@ -164,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)) } @@ -177,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]) } @@ -202,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)) } @@ -255,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 { @@ -531,6 +553,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 @@ -538,12 +561,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.") @@ -553,6 +577,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 @@ -563,6 +588,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) @@ -579,7 +605,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 52e8adfd3..80bce76cf 100644 --- a/internal/corazawaf/rulegroup.go +++ b/internal/corazawaf/rulegroup.go @@ -8,7 +8,7 @@ import ( "time" "github.com/corazawaf/coraza/v3/internal/corazatypes" - "github.com/corazawaf/coraza/v3/internal/strings" + utils "github.com/corazawaf/coraza/v3/internal/strings" "github.com/corazawaf/coraza/v3/types" "github.com/corazawaf/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 9cb69cc82..500f652fe 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" @@ -22,8 +24,10 @@ import ( "github.com/corazawaf/coraza/v3/internal/auditlog" "github.com/corazawaf/coraza/v3/internal/bodyprocessors" "github.com/corazawaf/coraza/v3/internal/collections" + "github.com/corazawaf/coraza/v3/internal/cookies" "github.com/corazawaf/coraza/v3/internal/corazarules" "github.com/corazawaf/coraza/v3/internal/corazatypes" + "github.com/corazawaf/coraza/v3/internal/environment" stringsutil "github.com/corazawaf/coraza/v3/internal/strings" urlutil "github.com/corazawaf/coraza/v3/internal/url" "github.com/corazawaf/coraza/v3/types" @@ -41,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 @@ -48,6 +55,7 @@ type Transaction struct { interruption *types.Interruption // This is used to store log messages + // Deprecated since Coraza 3.0.5: this variable is not used, logdata values are stored in the matched rules Logdata string // Rules will be skipped after a rule with this SecMarker is found @@ -319,8 +327,21 @@ func (tx *Transaction) AddRequestHeader(key string, value string) { tx.variables.reqbodyProcessor.Set("MULTIPART") } case "cookie": - // Cookies use the same syntax as GET params but with semicolon (;) separator - values := urlutil.ParseQuery(value, ';') + // 4.2. Cookie + // + // 4.2.1. Syntax + // + // The user agent sends stored cookies to the origin server in the + // Cookie header. If the server conforms to the requirements in + // Section 4.1 (and the user agent conforms to the requirements in + // Section 5), the user agent will send a Cookie header that conforms to + // the following grammar: + // + // cookie-header = "Cookie:" OWS cookie-string OWS + // cookie-string = cookie-pair *( ";" SP cookie-pair ) + // + // There is no URL Decode performed no the cookies + values := cookies.ParseCookies(value) for k, vr := range values { for _, v := range vr { tx.variables.requestCookies.Add(k, v) @@ -486,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 { @@ -534,7 +556,7 @@ func (tx *Transaction) GetStopWatch() string { } // GetField Retrieve data from collections applying exceptions -// In future releases we may remove de exceptions slice and +// In future releases we may remove the exceptions slice and // make it easier to use func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData { col := tx.Collection(rv.Variable) @@ -561,29 +583,25 @@ func (tx *Transaction) GetField(rv ruleVariableParams) []types.MatchData { matches = col.FindAll() } - var rmi []int - for i, c := range matches { + // in the most common scenario filteredMatches length will be + // the same as matches length, so we avoid allocating per result + filteredMatches := make([]types.MatchData, 0, len(matches)) + + for _, c := range matches { + isException := false + lkey := strings.ToLower(c.Key()) for _, ex := range rv.Exceptions { - lkey := strings.ToLower(c.Key()) - // in case it matches the regex or the keyStr - // Since keys are case sensitive we need to check with lower case if (ex.KeyRx != nil && ex.KeyRx.MatchString(lkey)) || strings.ToLower(ex.KeyStr) == lkey { - // we remove the exception from the list of values - // we tried with standard append, but it fails... let's do some hacking - // m2 := append(matches[:i], matches[i+1:]...) - rmi = append(rmi, i) + isException = true + break } } - } - // we read the list of indexes backwards - // then we remove each one of them because of the exceptions - for i := len(rmi) - 1; i >= 0; i-- { - if len(matches) < rmi[i]+1 { - matches = matches[:rmi[i]-1] - } else { - matches = append(matches[:rmi[i]], matches[rmi[i]+1:]...) + if !isException { + filteredMatches = append(filteredMatches, c) } } + matches = filteredMatches + if rv.Count { count := len(matches) matches = []types.MatchData{ @@ -604,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) } @@ -911,6 +946,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) } @@ -1086,7 +1122,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/corazawaf/coraza/issues/564 tx.variables.outboundDataError.Set("1") if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject { // We interrupt this transaction in case ResponseBodyLimitAction is Reject @@ -1138,7 +1173,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/corazawaf/coraza/issues/564 tx.variables.outboundDataError.Set("1") if tx.WAF.ResponseBodyLimitAction == types.BodyLimitActionReject { return setAndReturnBodyLimitInterruption(tx) @@ -1159,6 +1193,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) } @@ -1274,6 +1309,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(). @@ -1345,22 +1383,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: @@ -1450,13 +1491,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() { @@ -1471,14 +1524,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 @@ -1667,11 +1717,18 @@ func NewTransactionVariables() *TransactionVariables { // 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, diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go index 938aef632..bfff8821a 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,10 +888,39 @@ 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.Fatal("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) + } + if !utils.InSlice("abc", collectionValues(t, tx.variables.requestCookiesNames)) { + t.Fatal("failed to set cookie name") + } + if err := tx.Close(); err != nil { + t.Fatalf("Failed to close transaction: %s", err.Error()) + } +} + +func TestCookiesNotUrldecoded(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + fullCookie := "abc=%7Bd+e+f%7D;hij=%7Bklm%7D" + expectedUrlencodedAbcCookieValue := "%7Bd+e+f%7D" + unexpectedUrldencodedAbcCookieValue := "{d e f}" + tx.AddRequestHeader("cookie", fullCookie) + c := tx.variables.requestCookies.Get("abc")[0] + if c != expectedUrlencodedAbcCookieValue { + if c == unexpectedUrldencodedAbcCookieValue { + t.Errorf("failed to set cookie, unexpected urldecoding. Got: %q, expected: %q", unexpectedUrldencodedAbcCookieValue, expectedUrlencodedAbcCookieValue) + } else { + t.Errorf("failed to set cookie, got %q", c) + } + } + if tx.variables.requestHeaders.Get("cookie")[0] != fullCookie { + t.Errorf("failed to set request header, got: %q, expected: %q", tx.variables.requestHeaders.Get("cookie")[0], fullCookie) } if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) { t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames)) @@ -806,6 +933,28 @@ func TestHeaderSetters(t *testing.T) { } } +func TestMultipleCookiesWithSpaceBetweenThem(t *testing.T) { + waf := NewWAF() + tx := waf.NewTransaction() + multipleCookies := "cookie1=value1; cookie2=value2; cookie1=value2" + tx.AddRequestHeader("cookie", multipleCookies) + v11 := tx.variables.requestCookies.Get("cookie1")[0] + if v11 != "value1" { + t.Errorf("failed to set cookie, got %q", v11) + } + v12 := tx.variables.requestCookies.Get("cookie1")[1] + if v12 != "value2" { + t.Errorf("failed to set cookie, got %q", v12) + } + v2 := tx.variables.requestCookies.Get("cookie2")[0] + if v2 != "value2" { + t.Errorf("failed to set cookie, got %q", v2) + } + if err := tx.Close(); err != nil { + t.Error(err) + } +} + func collectionValues(t *testing.T, col collection.Collection) []string { t.Helper() var values []string @@ -825,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()) } } @@ -876,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()) } } @@ -888,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) } } @@ -928,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{ { @@ -942,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) } } @@ -959,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 } } @@ -981,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") } } @@ -1009,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()) } } @@ -1025,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()) } } @@ -1041,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()) } } @@ -1056,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()) } } @@ -1074,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 { @@ -1084,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) { @@ -1092,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()) } } @@ -1110,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()) } } @@ -1120,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()) } } @@ -1208,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 { @@ -1216,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) } } } @@ -1226,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) { @@ -1336,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) { @@ -1361,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) { @@ -1378,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") } } @@ -1395,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()) + } } } @@ -1411,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()) + } } } @@ -1427,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()) + } } } @@ -1443,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()) + } } } @@ -1467,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) { @@ -1477,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 7af329a10..e6946a3da 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/corazawaf/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,27 +134,43 @@ 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 = "" + tx.Logdata = "" // Deprecated, this variable is not used. Logdata for each matched rule is stored in the MatchData field. tx.SkipAfter = "" tx.AuditEngine = w.AuditEngine tx.AuditLogParts = w.AuditLogParts @@ -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,14 +292,21 @@ 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(), - Logger: logger, - ArgumentLimit: 1000, + AuditLogParts: types.AuditLogParts{ + types.AuditLogPartRequestHeaders, + types.AuditLogPartRequestBody, + types.AuditLogPartResponseHeaders, + types.AuditLogPartAuditLogTrailer, + }, + Logger: logger, + ArgumentLimit: 1000, } if environment.HasAccessToFS { 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..e11d0154a --- /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/corazawaf/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/seclang/directives.go b/internal/seclang/directives.go index 7a158c349..4d2350f2b 100644 --- a/internal/seclang/directives.go +++ b/internal/seclang/directives.go @@ -16,6 +16,7 @@ import ( "github.com/corazawaf/coraza/v3/debuglog" "github.com/corazawaf/coraza/v3/internal/auditlog" "github.com/corazawaf/coraza/v3/internal/corazawaf" + "github.com/corazawaf/coraza/v3/internal/environment" "github.com/corazawaf/coraza/v3/internal/memoize" utils "github.com/corazawaf/coraza/v3/internal/strings" "github.com/corazawaf/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 4b7031b83..654779fc5 100644 --- a/internal/seclang/directives_test.go +++ b/internal/seclang/directives_test.go @@ -4,6 +4,7 @@ package seclang import ( + "regexp" "strings" "testing" @@ -34,7 +35,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,6 +141,7 @@ func TestDirectives(t *testing.T) { }, "SecUploadDir": { {"", expectErrorOnDirective}, + {"/tmp-non-existing", expectErrorOnDirective}, {"/tmp", func(w *corazawaf.WAF) bool { return w.UploadDir == "/tmp" }}, }, "SecSensorId": { @@ -158,6 +160,7 @@ func TestDirectives(t *testing.T) { }, "SecRuleRemoveByTag": { {"", expectErrorOnDirective}, + {"attack-sqli", expectNoErrorOnDirective}, }, "SecRuleRemoveByMsg": { {"", expectErrorOnDirective}, @@ -168,10 +171,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 +296,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 1606f4900..2532d9eaf 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 27761472c..77b61ab99 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 b186d068d..fe30e1230 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..b06e83659 --- /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/corazawaf/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 b9b2b3693..ac6b95f91 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 c4be9897c..8f9961da2 100644 --- a/internal/transformations/base64decode.go +++ b/internal/transformations/base64decode.go @@ -4,17 +4,91 @@ package transformations import ( - "encoding/base64" - - stringsutil "github.com/corazawaf/coraza/v3/internal/strings" + "strings" + "unicode" ) +var base64DecMap = []byte{ + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, + 127, 127, 127, 62, 127, 127, 127, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 127, 127, + 127, 64, 127, 127, 127, 0, 1, 2, 3, 4, + 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 127, 127, 127, 127, 127, 127, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, 127, 127, 127, 127, 127, +} + // base64decode decodes a Base64-encoded string. +// Padding is optional. +// Partial decoding is returned up to the first invalid character (if any). +// New line characters (\r and \n) are ignored. +// Note: a custom base64 decoder is used in order to return partial decoding when an error arises. It +// 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) { - dec, err := base64.StdEncoding.DecodeString(data) - if err != nil { - // Forgiving implementation, which ignores invalid characters - return data, false, nil + res := doBase64decode(data, false) + return res, true, nil +} + +// 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 + } + + var n, x int + var dst strings.Builder + dst.Grow(slen) + + 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 + } + // If invalid character or padding reached, we stop decoding + if currChar == '=' || currChar == ' ' || currChar > 127 { + break + } + decodedChar := base64DecMap[currChar] + // Another condition of invalid character + if decodedChar == 127 { + break + } + + x = (x << 6) | int(decodedChar&0x3F) + n++ + if n == 4 { + dst.WriteByte(byte(x >> 16)) + dst.WriteByte(byte(x >> 8)) + dst.WriteByte(byte(x)) + n = 0 + x = 0 + } } - return stringsutil.WrapUnsafe(dec), true, nil + + // Handle any remaining characters + if n == 2 { + x <<= 12 + dst.WriteByte(byte(x >> 16)) + } else if n == 3 { + x <<= 6 + dst.WriteByte(byte(x >> 16)) + dst.WriteByte(byte(x >> 8)) + } + + return dst.String() } diff --git a/internal/transformations/base64decode_test.go b/internal/transformations/base64decode_test.go index d022b1659..f6842c07d 100644 --- a/internal/transformations/base64decode_test.go +++ b/internal/transformations/base64decode_test.go @@ -10,17 +10,173 @@ import ( "testing" ) -var b64DecodeTests = []string{ - "VGVzdENhc2U=", - "P.HNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==", - "VGVzdABDYXNl", +var b64DecodeTests = []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: "VGVzdENhc2U", + expected: "TestCase", + }, + { + name: "Valid without longer padding", + input: "PA==", + expected: "<", + }, + { + name: "valid ", + input: "PFRFU1Q+", + expected: "", + }, + { + name: "Malformed base64 encoding", + input: "PHNjcmlwd", + expected: "alert(1)", + }, + { + 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/testdata/uppercase.json b/internal/transformations/testdata/uppercase.json new file mode 100644 index 000000000..c8b8f15e9 --- /dev/null +++ b/internal/transformations/testdata/uppercase.json @@ -0,0 +1,38 @@ +[ + { + "input" : "", + "name" : "uppercase", + "type" : "tfn", + "ret" : 0, + "output" : "" + }, + { + "output" : "TESTCASE", + "ret" : 0, + "name" : "uppercase", + "input" : "testcase", + "type" : "tfn" + }, + { + "ret" : 0, + "input" : "test\u0000case", + "type" : "tfn", + "name" : "uppercase", + "output" : "TEST\u0000CASE" + }, + { + "output" : "TESTCASE", + "input" : "testcase", + "name" : "uppercase", + "type" : "tfn", + "ret" : 1 + }, + { + "ret" : 1, + "input" : "Test\u0000Case", + "name" : "uppercase", + "type" : "tfn", + "output" : "TEST\u0000CASE" + } + ] + \ No newline at end of file diff --git a/internal/transformations/transformations.go b/internal/transformations/transformations.go index bcfa533b6..704e04ad1 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) @@ -51,6 +52,7 @@ func init() { Register("replaceComments", replaceComments) Register("replaceNulls", replaceNulls) Register("sha1", sha1T) + Register("uppercase", upperCase) Register("urlDecode", urlDecode) Register("urlDecodeUni", urlDecodeUni) Register("urlEncode", urlEncode) diff --git a/internal/transformations/uppercase.go b/internal/transformations/uppercase.go new file mode 100644 index 000000000..c3b6da8e7 --- /dev/null +++ b/internal/transformations/uppercase.go @@ -0,0 +1,15 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package transformations + +import ( + "strings" +) + +func upperCase(data string) (string, bool, error) { + // TODO: Explicit implementation of ToUpper would allow optimizing away the byte by byte comparison for returning the changed boolean + // See https://github.com/corazawaf/coraza/pull/778#discussion_r1186963422 + transformedData := strings.ToUpper(data) + return transformedData, data != transformedData, nil +} diff --git a/internal/transformations/uppercase_test.go b/internal/transformations/uppercase_test.go new file mode 100644 index 000000000..e2172e09d --- /dev/null +++ b/internal/transformations/uppercase_test.go @@ -0,0 +1,69 @@ +// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors +// SPDX-License-Identifier: Apache-2.0 + +package transformations + +import "testing" + +func TestUpperCase(t *testing.T) { + tests := []struct { + input string + want string + }{ + { + input: "TestCase", + want: "TESTCASE", + }, + { + input: "test\u0000case", + want: "TEST\u0000CASE", + }, + { + input: "TESTCASE", + want: "TESTCASE", + }, + { + input: "", + want: "", + }, + { + input: "ThIs Is A tExT fOr TeStInG uPPerCAse FuNcTiOnAlItY.", + want: "THIS IS A TEXT FOR TESTING UPPERCASE FUNCTIONALITY.", + }, + } + + for _, tc := range tests { + tt := tc + t.Run(tt.input, func(t *testing.T) { + have, changed, err := upperCase(tt.input) + if err != nil { + t.Error(err) + } + if tt.input == tt.want && changed || tt.input != tt.want && !changed { + t.Errorf("input %q, have %q with changed %t", tt.input, have, changed) + } + if have != tt.want { + t.Errorf("have %q, want %q", have, tt.want) + } + }) + } +} + +func BenchmarkUppercase(b *testing.B) { + tests := []string{ + "tesTcase", + "ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY. ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.ThIs Is A tExT fOr TeStInG lOwErCaSe FuNcTiOnAlItY.", + } + for i := 0; i < b.N; i++ { + for _, tt := range tests { + b.Run(tt, func(b *testing.B) { + for j := 0; j < b.N; j++ { + _, _, err := upperCase(tt) + if err != nil { + b.Error(err) + } + } + }) + } + } +} diff --git a/internal/url/url.go b/internal/url/url.go index abc33a440..ea6dca12f 100644 --- a/internal/url/url.go +++ b/internal/url/url.go @@ -9,8 +9,11 @@ import ( // ParseQuery parses the URL-encoded query string and returns the corresponding map. // It takes separators as parameter, for example: & or ; or &; -// It returns error if the query string is malformed. func ParseQuery(query string, separator byte) map[string][]string { + return doParseQuery(query, separator, true) +} + +func doParseQuery(query string, separator byte, urlUnescape bool) map[string][]string { m := make(map[string][]string) for query != "" { key := query @@ -26,15 +29,17 @@ func ParseQuery(query string, separator byte) map[string][]string { if i := strings.IndexByte(key, '='); i >= 0 { key, value = key[:i], key[i+1:] } - key = QueryUnescape(key) - value = QueryUnescape(value) + if urlUnescape { + key = queryUnescape(key) + value = queryUnescape(value) + } m[key] = append(m[key], value) } return m } -// QueryUnescape is a non-strict version of net/url.QueryUnescape. -func QueryUnescape(input string) string { +// queryUnescape is a non-strict version of net/url.QueryUnescape. +func queryUnescape(input string) string { ilen := len(input) res := strings.Builder{} res.Grow(ilen) @@ -49,27 +54,13 @@ func QueryUnescape(input string) string { res.WriteByte(ci) continue } - hi := input[i+1] - lo := input[i+2] - switch { - case hi >= '0' && hi <= '9': - hi -= '0' - case hi >= 'a' && hi <= 'f': - hi -= 'a' - 10 - case hi >= 'A' && hi <= 'F': - hi -= 'A' - 10 - default: + hi, ok := hexDigitToByte(input[i+1]) + if !ok { res.WriteByte(ci) continue } - switch { - case lo >= '0' && lo <= '9': - lo -= '0' - case lo >= 'a' && lo <= 'f': - lo -= 'a' - 10 - case lo >= 'A' && lo <= 'F': - lo -= 'A' - 10 - default: + lo, ok := hexDigitToByte(input[i+2]) + if !ok { res.WriteByte(ci) continue } @@ -81,3 +72,16 @@ func QueryUnescape(input string) string { } return res.String() } + +func hexDigitToByte(digit byte) (byte, bool) { + switch { + case digit >= '0' && digit <= '9': + return digit - '0', true + case digit >= 'a' && digit <= 'f': + return digit - 'a' + 10, true + case digit >= 'A' && digit <= 'F': + return digit - 'A' + 10, true + default: + return 0, false + } +} diff --git a/internal/url/url_test.go b/internal/url/url_test.go index 72c068a4f..7194f4a53 100644 --- a/internal/url/url_test.go +++ b/internal/url/url_test.go @@ -7,27 +7,45 @@ import ( "testing" ) +var parseQueryInput = `var=EmptyValue'||(select extractvalue(xmltype('%awpsd;` + func TestUrlPayloads(t *testing.T) { - out := `var=EmptyValue'||(select extractvalue(xmltype('%awpsd;` - q := ParseQuery(out, '&') + q := ParseQuery(parseQueryInput, '&') if len(q["var"]) == 0 { t.Error("var is empty") } } -func TestQueryUnescape(t *testing.T) { - payloads := map[string]string{ - "sample": "sample", - "s%20ample": "s ample", - "s+ample": "s ample", - "s%2fample": "s/ample", - "s% ample": "s% ample", // non-strict sample - "s%ssample": "s%ssample", // non-strict sample - "s%00ample": "s\x00ample", +func BenchmarkParseQuery(b *testing.B) { + for i := 0; i < b.N; i++ { + ParseQuery(parseQueryInput, '&') } - for k, v := range payloads { - if out := QueryUnescape(k); out != v { +} + +var queryUnescapePayloads = map[string]string{ + "sample": "sample", + "s%20ample": "s ample", + "s+ample": "s ample", + "s%2fample": "s/ample", + "s% ample": "s% ample", // non-strict sample + "s%ssample": "s%ssample", // non-strict sample + "s%00ample": "s\x00ample", + "%7B%%7d": "{%}", + "%7B+%+%7d": "{ % }", +} + +func TestQueryUnescape(t *testing.T) { + for k, v := range queryUnescapePayloads { + if out := queryUnescape(k); out != v { t.Errorf("Error parsing %q, got %q and expected %q", k, out, v) } } } + +func BenchmarkQueryUnescape(b *testing.B) { + for i := 0; i < b.N; i++ { + for k := range queryUnescapePayloads { + queryUnescape(k) + } + } +} diff --git a/magefile.go b/magefile.go index 22f4119a3..905cfb943 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 new file mode 100644 index 000000000..5db72dd6a --- /dev/null +++ b/renovate.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ] +} diff --git a/testing/auditlog_test.go b/testing/auditlog_test.go index 324ef1f7e..fdbfb2c5c 100644 --- a/testing/auditlog_test.go +++ b/testing/auditlog_test.go @@ -13,6 +13,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/corazawaf/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 2f826cbbe..f926bda2c 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/corazawaf/coraza/v3" txhttp "github.com/corazawaf/coraza/v3/http" "github.com/corazawaf/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 e44a834eb..f5d23f9d0 100644 --- a/testing/coreruleset/go.mod +++ b/testing/coreruleset/go.mod @@ -1,44 +1,53 @@ module github.com/corazawaf/coraza/v3/testing/coreruleset -go 1.18 +go 1.20 require ( - github.com/bmatcuk/doublestar/v4 v4.3.0 - github.com/corazawaf/coraza-coreruleset v0.0.0-20230330101229-43b851256042 - github.com/corazawaf/coraza/v3 v3.0.0-20221004054810-060cedcb166d - github.com/coreruleset/go-ftw v0.4.9 - github.com/rs/zerolog v1.28.0 + github.com/bmatcuk/doublestar/v4 v4.6.1 + github.com/corazawaf/coraza-coreruleset/v4 v4.3.0 + github.com/corazawaf/coraza/v3 v3.1.0 + github.com/coreruleset/go-ftw v0.6.4 + 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/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/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.16 // 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.12.0 // indirect - golang.org/x/net v0.14.0 // indirect - golang.org/x/sys v0.11.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.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // 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 ) diff --git a/testing/coreruleset/go.sum b/testing/coreruleset/go.sum index 8d30bf4db..602223a8d 100644 --- a/testing/coreruleset/go.sum +++ b/testing/coreruleset/go.sum @@ -1,298 +1,86 @@ -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.3.0 h1:Ct0GphHCZaXvUh2Gqtk37Mzj1qWvXcW9XnXQs1GL9S0= -github.com/bmatcuk/doublestar/v4 v4.3.0/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.0-20221004054810-060cedcb166d h1:e7nLsrnie6309FYWPZg2kY2yQWhHslmfkzZTPVnpeqg= -github.com/corazawaf/coraza/v3 v3.0.0-20221004054810-060cedcb166d/go.mod h1:+ypLPFkX5j1GwKi+rqRZ57W3lSHReBdeVLh0o8qirI4= -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/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/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.1.0 h1:CB6YxNXdbZjUJS/0FVFoFvS8eOVFbIvlNuHNC5dh88c= +github.com/corazawaf/coraza/v3 v3.1.0/go.mod h1:S0bhYQfTu1Ew3YKdI37X1WWu6t4En4Tvw28aKyQFJaU= +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/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/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.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 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 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= -github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= -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/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +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/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= @@ -300,172 +88,25 @@ 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.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -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.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -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/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/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.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.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.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= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/testing/e2e/e2e_test.go b/testing/e2e/e2e_test.go index a3f596f2f..7fb0cd2fe 100644 --- a/testing/e2e/e2e_test.go +++ b/testing/e2e/e2e_test.go @@ -23,25 +23,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 bd31d7e16..fa74364d3 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..bff4d0f76 --- /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/corazawaf/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/multiphase.go b/testing/engine/multiphase.go index 9ab13f5bc..9f6658e1a 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 8254661e8..a45e332af 100644 --- a/types/rule_match.go +++ b/types/rule_match.go @@ -45,5 +45,6 @@ type MatchedRule interface { Rule() RuleMetadata AuditLog() string + ErrorLog() string } diff --git a/waf.go b/waf.go index 348cafad3..75e8a1327 100644 --- a/waf.go +++ b/waf.go @@ -4,9 +4,13 @@ package coraza import ( + "context" "fmt" + "strings" + "github.com/corazawaf/coraza/v3/experimental" "github.com/corazawaf/coraza/v3/internal/corazawaf" + "github.com/corazawaf/coraza/v3/internal/environment" "github.com/corazawaf/coraza/v3/internal/seclang" "github.com/corazawaf/coraza/v3/types" ) @@ -30,6 +34,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 } @@ -57,19 +67,7 @@ func NewWAF(config WAFConfig) (WAF, error) { } } - if a := c.auditLog; a != nil { - if a.relevantOnly { - waf.AuditEngine = types.AuditEngineRelevantOnly - } else { - waf.AuditEngine = types.AuditEngineOn - } - - waf.AuditLogParts = a.parts - - if a.writer != nil { - waf.SetAuditLogWriter(a.writer) - } - } + populateAuditLog(waf, c) if err := waf.InitAuditLogWriter(); err != nil { return nil, fmt.Errorf("invalid WAF config from audit log: %w", err) @@ -110,6 +108,26 @@ func NewWAF(config WAFConfig) (WAF, error) { return wafWrapper{waf: waf}, nil } +func populateAuditLog(waf *corazawaf.WAF, c *wafConfig) { + if c.auditLog == nil { + return + } + + if c.auditLog.relevantOnly { + waf.AuditEngine = types.AuditEngineRelevantOnly + } else { + waf.AuditEngine = types.AuditEngineOn + } + + if len(c.auditLog.parts) > 0 { + waf.AuditLogParts = c.auditLog.parts + } + + if c.auditLog.writer != nil { + waf.SetAuditLogWriter(c.auditLog.writer) + } +} + type wafWrapper struct { waf *corazawaf.WAF } @@ -121,5 +139,15 @@ 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) } diff --git a/waf_test.go b/waf_test.go index 08d52dfb6..d4ee3e9e6 100644 --- a/waf_test.go +++ b/waf_test.go @@ -5,7 +5,12 @@ package coraza import ( "errors" + "reflect" "testing" + + "github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes" + "github.com/corazawaf/coraza/v3/internal/corazawaf" + "github.com/corazawaf/coraza/v3/types" ) func TestRequestBodyLimit(t *testing.T) { @@ -103,3 +108,73 @@ func TestResponseBodyLimit(t *testing.T) { }) } } + +type testAuditLogWriter struct { + plugintypes.AuditLogWriter +} + +func (*testAuditLogWriter) Init(plugintypes.AuditLogConfig) error { + return nil +} + +func TestPopulateAuditLog(t *testing.T) { + writer := &testAuditLogWriter{} + + testCases := map[string]struct { + config *wafConfig + check func(*testing.T, *corazawaf.WAF) + }{ + "empty config": { + config: &wafConfig{}, + check: func(*testing.T, *corazawaf.WAF) {}, + }, + "with relevant only": { + config: &wafConfig{ + auditLog: &auditLogConfig{ + relevantOnly: true, + }, + }, + check: func(t *testing.T, waf *corazawaf.WAF) { + if waf.AuditEngine != types.AuditEngineRelevantOnly { + t.Fatal("expected AuditLogRelevantOnly to be true") + } + }, + }, + "with parts": { + config: &wafConfig{ + auditLog: &auditLogConfig{ + parts: []types.AuditLogPart{ + types.AuditLogPartRequestHeaders, + types.AuditLogPartResponseBody, + }, + }, + }, + check: func(t *testing.T, waf *corazawaf.WAF) { + if want, have := []types.AuditLogPart{ + types.AuditLogPartRequestHeaders, + types.AuditLogPartResponseBody, + }, waf.AuditLogParts; len(want) != len(have) { + t.Fatalf("unexpected AuditLogParts: want %v, have %v", want, have) + } + }, + }, + "with audit log writer": { + config: &wafConfig{ + auditLog: &auditLogConfig{writer: writer}, + }, + check: func(t *testing.T, waf *corazawaf.WAF) { + if reflect.DeepEqual(waf.AuditLogWriter(), &writer) { + t.Fatal("expected AuditLogWriter to be set") + } + }, + }, + } + + for name, tCase := range testCases { + t.Run(name, func(t *testing.T) { + waf := &corazawaf.WAF{} + populateAuditLog(waf, tCase.config) + tCase.check(t, waf) + }) + } +}