Repository: corazawaf/coraza
Pull request #1131 (2024-08-19 18:37:12)

chore(deps): update github/codeql-action digest to 2c779ab
[![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [github/codeql-action](https://togithub.com/github/codeql-action) | action | digest | `429e197` -> `2c779ab` |

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR was generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View the [repository job log](https://developer.mend.io/github/corazawaf/coraza).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4yNi4xIiwidXBkYXRlZEluVmVyIjoiMzguMjYuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->


renovate[bot]: ### Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

 ⚠️ **Warning**: custom changes will be lost.

Patch:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 1ef1abf..b711e25 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -13,12 +13,12 @@ jobs:
       uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3
+      uses: github/codeql-action/init@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3
       with:
         languages: go
 
     - name: Autobuild
-      uses: github/codeql-action/autobuild@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3
+      uses: github/codeql-action/autobuild@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@429e1977040da7a23b6822b13c129cd1ba93dbb2 # v3
+      uses: github/codeql-action/analyze@2c779ab0d087cd7fe7b826087247c2c81f27bfa6 # v3
diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml
index bc703b1..6f06c35 100644
--- a/.github/workflows/regression.yml
+++ b/.github/workflows/regression.yml
@@ -30,31 +30,31 @@ jobs:
       - name: Tests and coverage
         run: go run mage.go coverage
       - name: "Codecov: General"
-        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
+        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
         if: ${{ matrix.go-version == '1.22.x' }}
         with:
           files: build/coverage.txt
           flags: default
       - name: "Codecov: Examples"
-        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
+        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
         if: ${{ matrix.go-version == '1.22.x' }}
         with:
           files: build/coverage-examples.txt
           flags: examples
       - name: "Codecov: FTW"
-        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
+        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
         if: ${{ matrix.go-version == '1.22.x' }}
         with:
           files: build/coverage-ftw.txt
           flags: ftw
       - name: "Codecov: FTW Multiphase tag"
-        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
+        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
         if: ${{ matrix.go-version == '1.22.x' }}
         with:
           files: build/coverage-ftw-multiphase.txt
           flags: ftw-multiphase
       - name: "Codecov: Tinygo"
-        uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4
+        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
         if: ${{ matrix.go-version == '1.22.x' }}
         with:
           files: build/coverage-tinygo.txt
diff --git a/.golangci.yml b/.golangci.yml
index 6edd2ac..e7c44cf 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -19,4 +19,4 @@ issues:
   exclude-rules:
     - path: magefile\.go
       linters:
-        - deadcode
+        - deadcode
\ No newline at end of file
diff --git a/coraza.conf-recommended b/coraza.conf-recommended
index ce311ec..ccc5884 100644
--- a/coraza.conf-recommended
+++ b/coraza.conf-recommended
@@ -161,8 +161,11 @@ SecAuditLogParts ABIJDEFHZ
 #
 SecAuditLogType Serial
 
-# The following settings are not supported by Coraza
+# The format used to write the audit log.
+# Can be one of JSON|JsonLegacy|Native|OCSF
+SecAuditLogFormat Native
 
+# The following settings are not supported by Coraza
 # SecCookieFormat 0
 # SecArgumentSeparator &
 # SecRule MULTIPART_UNMATCHED_BOUNDARY "@eq 1" \
diff --git a/examples/http-server/go.mod b/examples/http-server/go.mod
index fde9666..11ab0d9 100644
--- a/examples/http-server/go.mod
+++ b/examples/http-server/go.mod
@@ -8,10 +8,11 @@ require (
 	github.com/corazawaf/libinjection-go v0.2.1 // indirect
 	github.com/magefile/mage v1.15.0 // indirect
 	github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
-	github.com/tidwall/gjson v1.17.1 // indirect
+	github.com/tidwall/gjson v1.17.3 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.1 // indirect
-	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/net v0.29.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
+	golang.org/x/tools v0.22.0 // indirect
 	rsc.io/binaryregexp v0.2.0 // indirect
 )
diff --git a/examples/http-server/go.sum b/examples/http-server/go.sum
index 9be2d9c..bde8489 100644
--- a/examples/http-server/go.sum
+++ b/examples/http-server/go.sum
@@ -10,22 +10,22 @@ 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/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
-golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/tools v0.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/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/experimental/plugins/plugintypes/auditlog.go b/experimental/plugins/plugintypes/auditlog.go
index b64911d..4742953 100644
--- a/experimental/plugins/plugintypes/auditlog.go
+++ b/experimental/plugins/plugintypes/auditlog.go
@@ -1,4 +1,4 @@
-// Copyright 2023 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 plugintypes
@@ -6,6 +6,7 @@ package plugintypes
 import (
 	"io/fs"
 
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -31,6 +32,8 @@ type AuditLogTransaction interface {
 	Response() AuditLogTransactionResponse
 	HasResponse() bool
 	Producer() AuditLogTransactionProducer
+	HighestSeverity() string // The highest severity of the matched rules for the transaction
+	IsInterrupted() bool     // True if the transaction was interrupted
 }
 
 // AuditLogTransactionResponse contains response specific information
@@ -61,6 +64,8 @@ type AuditLogTransactionRequest interface {
 	Headers() map[string][]string
 	Body() string
 	Files() []AuditLogTransactionRequestFiles
+	Args() *collections.ConcatKeyed // A string representation of all request agruments in the format 'k=v,'
+	Length() int32                  // The total size of the request in bytes
 }
 
 // AuditLogTransactionRequestFiles contains information for the
diff --git a/go.mod b/go.mod
index 18a10b9..470dbef 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/corazawaf/coraza/v3
 
-go 1.22
+go 1.22.3
 
 // Testing dependencies:
 // - go-mockdns
@@ -14,16 +14,18 @@ go 1.22
 // - aho-corasick
 // - gjson
 // - binaryregexp
+// - ocsf-schema-golang
 
 require (
 	github.com/anuraaga/go-modsecurity v0.0.0-20220824035035-b9a4099778df
 	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.14.0
+	github.com/mccutchen/go-httpbin/v2 v2.15.0
 	github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4
-	github.com/tidwall/gjson v1.17.1
-	golang.org/x/net v0.28.0
+	github.com/tidwall/gjson v1.18.0
+	github.com/valllabh/ocsf-schema-golang v1.0.3
+	golang.org/x/net v0.29.0
 	golang.org/x/sync v0.8.0
 	rsc.io/binaryregexp v0.2.0
 )
@@ -32,7 +34,8 @@ require (
 	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.17.0 // indirect
-	golang.org/x/sys v0.23.0 // indirect
-	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
+	golang.org/x/mod v0.18.0 // indirect
+	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/tools v0.22.0 // indirect
+	google.golang.org/protobuf v1.34.1 // indirect
 )
diff --git a/go.sum b/go.sum
index e0c1da6..fde1e3e 100644
--- a/go.sum
+++ b/go.sum
@@ -4,21 +4,29 @@ github.com/corazawaf/libinjection-go v0.2.1 h1:vNJ7L6c4xkhRgYU6sIO0Tl54TmeCQv/yf
 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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 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.14.0 h1:9N7GUf8+JunYMFd+yHPIVYApC6KYgqtF0pHIcTGYcVQ=
 github.com/mccutchen/go-httpbin/v2 v2.14.0/go.mod h1:f4DUXYlU6yH0V81O4lJIwqpmYdTXXmYwzxMnYEimFPk=
+github.com/mccutchen/go-httpbin/v2 v2.15.0 h1:3b2s8LMRR2aFd+8U+1Bx2kdgHNQ5ZQkQOiW8e52Jj9A=
+github.com/mccutchen/go-httpbin/v2 v2.15.0/go.mod h1:GBy5I7XwZ4ZLhT3hcq39I4ikwN9x4QUt6EAxNiR8Jus=
 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/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/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/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
+github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
 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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -29,8 +37,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91
 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -39,8 +47,8 @@ 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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
@@ -59,8 +67,8 @@ 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.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
-golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.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=
@@ -81,8 +89,10 @@ 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/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 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 6d1cff1..6907c6b 100644
--- a/go.work
+++ b/go.work
@@ -1,4 +1,4 @@
-go 1.22
+go 1.22.3
 
 use (
 	.
diff --git a/http/interceptor.go b/http/interceptor.go
index 06d7656..0fa9feb 100644
--- a/http/interceptor.go
+++ b/http/interceptor.go
@@ -83,7 +83,11 @@ func (i *rwInterceptor) Write(b []byte) (int, error) {
 		// if there is an interruption it must be from at least phase 4 and hence
 		// WriteHeader or Write should have been called and hence the status code
 		// has been flushed to the delegated response writer.
-		return 0, nil
+		//
+		// We return the number of bytes as according to the interface io.Writer
+		// if we don't return an error, the number of bytes written is len(p).
+		// See https://pkg.go.dev/io#Writer
+		return len(b), nil
 	}
 
 	if !i.wroteHeader {
@@ -102,7 +106,10 @@ func (i *rwInterceptor) Write(b []byte) (int, error) {
 			i.overrideWriteHeader(obtainStatusCodeFromInterruptionOrDefault(it, i.statusCode))
 			// We only flush the status code after an interruption.
 			i.flushWriteHeader()
-			return 0, nil
+			// We return the number of bytes as according to the interface io.Writer
+			// if we don't return an error, the number of bytes written is len(p).
+			// See https://pkg.go.dev/io#Writer
+			return len(b), nil
 		}
 		return n, err
 	}
diff --git a/internal/actions/setvar.go b/internal/actions/setvar.go
index 2c36352..3999217 100644
--- a/internal/actions/setvar.go
+++ b/internal/actions/setvar.go
@@ -145,16 +145,23 @@ func (a *setvarFn) evaluateTxCollection(r plugintypes.RuleMetadata, tx plugintyp
 	case len(value) == 0:
 		// if nothing to input
 		col.Set(key, []string{""})
-	case value[0] == '+', value[0] == '-': // Increment or decrement, arithmetical operations
+	// Check if this could be an arithemetic operation. If it is followed by a number, it will be treated as an arithmetic operation. Otherwise, it will be treated as a string.
+	case value[0] == '+', value[0] == '-':
 		val := 0
 		if len(value) > 1 {
 			val, err = strconv.Atoi(value[1:])
 			if err != nil {
-				tx.DebugLogger().Error().
-					Str("var_value", value).
-					Int("rule_id", r.ID()).
-					Err(err).
-					Msg("Invalid value")
+				// If the variable doesn't exist, we would need to raise an error. Otherwise, it should be the same value.
+				if strings.HasPrefix(value[1:], "tx.") {
+					tx.DebugLogger().Error().
+						Str("var_value", value).
+						Int("rule_id", r.ID()).
+						Err(err).
+						Msg(value)
+					return
+				}
+
+				col.Set(key, []string{value})
 				return
 			}
 		}
diff --git a/internal/auditlog/auditlog.go b/internal/auditlog/auditlog.go
index 9ff0e35..7a0383b 100644
--- a/internal/auditlog/auditlog.go
+++ b/internal/auditlog/auditlog.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 auditlog
@@ -7,6 +7,7 @@ import (
 	"encoding/json"
 
 	"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -77,13 +78,15 @@ type Transaction struct {
 	// Client IP Address string representation
 	ClientIP_ string `json:"client_ip"`
 
-	ClientPort_ int                  `json:"client_port"`
-	HostIP_     string               `json:"host_ip"`
-	HostPort_   int                  `json:"host_port"`
-	ServerID_   string               `json:"server_id"`
-	Request_    *TransactionRequest  `json:"request,omitempty"`
-	Response_   *TransactionResponse `json:"response,omitempty"`
-	Producer_   *TransactionProducer `json:"producer,omitempty"`
+	ClientPort_      int                  `json:"client_port"`
+	HostIP_          string               `json:"host_ip"`
+	HostPort_        int                  `json:"host_port"`
+	ServerID_        string               `json:"server_id"`
+	Request_         *TransactionRequest  `json:"request,omitempty"`
+	Response_        *TransactionResponse `json:"response,omitempty"`
+	Producer_        *TransactionProducer `json:"producer,omitempty"`
+	HighestSeverity_ string               `json:"highest_severity"`
+	IsInterrupted_   bool                 `json:"is_interrupted"`
 }
 
 var _ plugintypes.AuditLogTransaction = Transaction{}
@@ -140,6 +143,14 @@ func (t Transaction) Producer() plugintypes.AuditLogTransactionProducer {
 	return t.Producer_
 }
 
+func (t Transaction) HighestSeverity() string {
+	return t.HighestSeverity_
+}
+
+func (t Transaction) IsInterrupted() bool {
+	return t.IsInterrupted_
+}
+
 // TransactionResponse contains response specific
 // information
 type TransactionResponse struct {
@@ -197,26 +208,50 @@ type TransactionProducer struct {
 var _ plugintypes.AuditLogTransactionProducer = (*TransactionProducer)(nil)
 
 func (tp *TransactionProducer) Connector() string {
+	if tp == nil {
+		return ""
+	}
+
 	return tp.Connector_
 }
 
 func (tp *TransactionProducer) Version() string {
+	if tp == nil {
+		return ""
+	}
+
 	return tp.Version_
 }
 
 func (tp *TransactionProducer) Server() string {
+	if tp == nil {
+		return ""
+	}
+
 	return tp.Server_
 }
 
 func (tp *TransactionProducer) RuleEngine() string {
+	if tp == nil {
+		return ""
+	}
+
 	return tp.RuleEngine_
 }
 
 func (tp *TransactionProducer) Stopwatch() string {
+	if tp == nil {
+		return ""
+	}
+
 	return tp.Stopwatch_
 }
 
 func (tp *TransactionProducer) Rulesets() []string {
+	if tp == nil {
+		return nil
+	}
+
 	return tp.Rulesets_
 }
 
@@ -230,6 +265,8 @@ type TransactionRequest struct {
 	Headers_     map[string][]string                           `json:"headers"`
 	Body_        string                                        `json:"body"`
 	Files_       []plugintypes.AuditLogTransactionRequestFiles `json:"files"`
+	Args_        *collections.ConcatKeyed                      `json:"args"`
+	Length_      int32                                         `json:"length"`
 }
 
 var _ plugintypes.AuditLogTransactionRequest = (*TransactionRequest)(nil)
@@ -286,6 +323,21 @@ func (tr *TransactionRequest) Files() []plugintypes.AuditLogTransactionRequestFi
 	return tr.Files_
 }
 
+func (tr *TransactionRequest) Args() *collections.ConcatKeyed {
+	if tr == nil {
+		return &collections.ConcatKeyed{}
+	}
+
+	return tr.Args_
+}
+
+func (tr *TransactionRequest) Length() int32 {
+	if tr == nil {
+		return 0
+	}
+	return tr.Length_
+}
+
 // TransactionRequestFiles contains information
 // for the uploaded files using multipart forms
 type TransactionRequestFiles struct {
diff --git a/internal/auditlog/formats_ocsf.go b/internal/auditlog/formats_ocsf.go
new file mode 100644
index 0000000..ca3c695
--- /dev/null
+++ b/internal/auditlog/formats_ocsf.go
@@ -0,0 +1,257 @@
+// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+// OCSF log format
+// OCSF (Open Cybersecurity Schema Framework) (https://github.com/ocsf) is an open-source framework with the goal of providing an open standard for logging security events.
+// This log format will produce a JSON log which adheres to the OCSF schema (https://schema.ocsf.io/)
+
+package auditlog
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/events/application"
+	"github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/events/application/enums"
+	"github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/objects"
+	ocsf_object_enums "github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/objects/enums"
+
+	"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
+	"github.com/corazawaf/coraza/v3/types"
+)
+
+type ocsfFormatter struct{}
+
+func (f ocsfFormatter) getRequestArguments(al plugintypes.AuditLog) string {
+	argString := &strings.Builder{}
+	if al.Transaction().Request().Args() != nil {
+		args := al.Transaction().Request().Args().FindAll()
+
+		argCount := len(args)
+		for i, arg := range al.Transaction().Request().Args().FindAll() {
+			argString.WriteString(fmt.Sprintf("%s=%s", arg.Key(), arg.Value()))
+			if i < argCount {
+				argString.WriteString(",")
+			}
+		}
+	}
+	return argString.String()
+}
+
+func (f ocsfFormatter) getRequestHeaders(al plugintypes.AuditLog) []*objects.HttpHeader {
+	requestHeaders := []*objects.HttpHeader{}
+	for key, values := range al.Transaction().Request().Headers() {
+		for _, value := range values {
+			requestHeaders = append(requestHeaders, &objects.HttpHeader{
+				Name:  key,
+				Value: value,
+			})
+		}
+	}
+	return requestHeaders
+}
+
+func (f ocsfFormatter) getResponseHeaders(al plugintypes.AuditLog) []*objects.HttpHeader {
+	responseHeaders := []*objects.HttpHeader{}
+	for key, values := range al.Transaction().Response().Headers() {
+		for _, value := range values {
+			responseHeaders = append(responseHeaders, &objects.HttpHeader{
+				Name:  key,
+				Value: value,
+			})
+		}
+	}
+	return responseHeaders
+}
+
+func (f ocsfFormatter) getAffectedWebResources(al plugintypes.AuditLog) []*objects.WebResource {
+	// Create an array of web Resources affected by this activity
+	webResources := []*objects.WebResource{}
+	webResources = append(webResources, &objects.WebResource{
+		UrlString: al.Transaction().Request().URI(),
+	})
+
+	return webResources
+}
+
+// Returns an array of Enrichment objects containing the details of each message in AuditLog.Messages
+func (f ocsfFormatter) getMatchDetails(al plugintypes.AuditLog) []*objects.Enrichment {
+	matchDetails := []*objects.Enrichment{}
+
+	for _, match := range al.Messages() {
+		matchData, _ := json.Marshal(match.Data())
+		matchDetails = append(matchDetails, &objects.Enrichment{
+			Data:  string(matchData),
+			Name:  match.Data().Msg(),
+			Value: match.Data().Data(),
+		})
+	}
+
+	return matchDetails
+}
+
+// Returns an array of Observable objects
+func (f ocsfFormatter) getObservables(al plugintypes.AuditLog) []*objects.Observable {
+	observables := []*objects.Observable{}
+
+	if al.Transaction().ServerID() != "" {
+		observables = append(observables, &objects.Observable{
+			Name:   "ServerID",
+			Type:   "ServerID",
+			TypeId: ocsf_object_enums.OBSERVABLE_TYPE_ID_OBSERVABLE_TYPE_ID_OTHER,
+			Value:  al.Transaction().ServerID(),
+		})
+	}
+
+	for _, file := range al.Transaction().Request().Files() {
+		observables = append(observables, &objects.Observable{
+			Name:   file.Name(),
+			Type:   "File Name",
+			TypeId: 7,
+			Value:  file.Name(),
+		})
+		observables = append(observables, &objects.Observable{
+			Name:   file.Name(),
+			Type:   "Size",
+			TypeId: ocsf_object_enums.OBSERVABLE_TYPE_ID_OBSERVABLE_TYPE_ID_OTHER,
+			Value:  fmt.Sprint(file.Size()),
+		})
+		observables = append(observables, &objects.Observable{
+			Name:   file.Name(),
+			Type:   "Mime",
+			TypeId: ocsf_object_enums.OBSERVABLE_TYPE_ID_OBSERVABLE_TYPE_ID_OTHER,
+			Value:  file.Mime(),
+		})
+	}
+
+	return observables
+}
+
+func (f ocsfFormatter) Format(al plugintypes.AuditLog) ([]byte, error) {
+
+	// Determine the Action/ActionID based on whether the transaction was interrutped
+	ActionID := enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_ALLOWED
+	Action := "Allowed"
+	if al.Transaction().IsInterrupted() {
+		ActionID = enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_DENIED
+		Action = "Denied"
+
+	}
+
+	// Populate the required fields for the WebRecourcesActivity
+	webResourcesActivity := application.WebResourcesActivity{
+		ActivityId:   enums.WEB_RESOURCES_ACTIVITY_ACTIVITY_ID_WEB_RESOURCES_ACTIVITY_ACTIVITY_ID_READ,
+		ActivityName: "Read",
+		CategoryName: "Application Activity",
+		ClassName:    "Web Resources Activity",
+		CategoryUid:  enums.WEB_RESOURCES_ACTIVITY_CATEGORY_UID_WEB_RESOURCES_ACTIVITY_CATEGORY_UID_APPLICATION_ACTIVITY,
+		ClassUid:     enums.WEB_RESOURCES_ACTIVITY_CLASS_UID_WEB_RESOURCES_ACTIVITY_CLASS_UID_WEB_RESOURCES_ACTIVITY,
+		Time:         al.Transaction().UnixTimestamp(),
+		ActionId:     ActionID,
+		Action:       Action,
+		Metadata: &objects.Metadata{
+			CorrelationUid: "",
+			EventCode:      "",
+			//Labels:         [2]string{"", ""},
+			LogLevel: "",
+			LogName:  "",
+			//LogProvider: "OWASP Coraza Web Application Firewall",
+			LogProvider: al.Transaction().Producer().Connector(),
+			LogVersion:  al.Transaction().Producer().Version(),
+			LoggedTime:  time.Now().UnixMicro(),
+			Product: &objects.Product{
+				VendorName: "OWASP Coraza Web Application Firewall",
+			},
+			Version: "1.2.0",
+		},
+		TypeUid:     enums.WEB_RESOURCES_ACTIVITY_TYPE_UID_WEB_RESOURCES_ACTIVITY_TYPE_UID_WEB_RESOURCES_ACTIVITY_READ,
+		Enrichments: f.getMatchDetails(al),
+		HttpRequest: &objects.HttpRequest{
+			Version:     al.Transaction().Request().Protocol(),
+			Args:        f.getRequestArguments(al),
+			HttpMethod:  al.Transaction().Request().Method(),
+			Uid:         al.Transaction().ID(),
+			Url:         &objects.Url{UrlString: al.Transaction().Request().URI()},
+			HttpHeaders: f.getRequestHeaders(al),
+			Length:      al.Transaction().Request().Length(),
+		},
+		HttpResponse: &objects.HttpResponse{
+			Code:        int32(al.Transaction().Response().Status()),
+			HttpHeaders: f.getResponseHeaders(al),
+		},
+		SrcEndpoint: &objects.NetworkEndpoint{
+			Ip:   al.Transaction().ClientIP(),
+			Port: int32(al.Transaction().ClientPort()),
+		},
+		DstEndpoint: &objects.NetworkEndpoint{
+			Ip:   al.Transaction().HostIP(),
+			Port: int32(al.Transaction().HostPort()),
+		},
+		WebResources: f.getAffectedWebResources(al),
+	}
+
+	userAgent := al.Transaction().Request().Headers()["user-agent"]
+	if len(userAgent) > 0 {
+		webResourcesActivity.HttpRequest.UserAgent = userAgent[0]
+	}
+
+	// Note: 'referer' is a misspelling of 'referrer' but was incorporated into the HTTP specification with this misspelling
+	// see https://en.wikipedia.org/wiki/HTTP_referer
+	referrer := al.Transaction().Request().Headers()["referer"]
+	if len(referrer) > 0 {
+		webResourcesActivity.HttpRequest.Referrer = referrer[0]
+	}
+
+	xForwardedFor := al.Transaction().Request().Headers()["x-forwarded-for"]
+	if len(xForwardedFor) > 0 {
+		webResourcesActivity.HttpRequest.XForwardedFor = xForwardedFor
+	}
+
+	if len(al.Messages()) > 0 {
+		message := al.Messages()[0]
+		webResourcesActivity.Message = message.Message()
+	}
+
+	_, offset := time.Now().Zone()
+	webResourcesActivity.TimezoneOffset = int32(offset)
+
+	// The WebResource Activity Severity ID is not to be confused by the Transaction severity.  The Transaction severity has to do with Coraza error/debug severity,
+	// while WebResource Activity Severity is defined by OCSF to represent the severity of the security event.
+	// For now, we're setting severityID to 'Other' and setting Severity to the Highest severity of the matched rules.
+	// A future update should map/translate rule severity to OCSF severity if possible.
+	highestSeverity, _ := types.ParseRuleSeverity(al.Transaction().HighestSeverity())
+	webResourcesActivity.Severity = highestSeverity.String()
+	webResourcesActivity.SeverityId = enums.WEB_RESOURCES_ACTIVITY_SEVERITY_ID_WEB_RESOURCES_ACTIVITY_SEVERITY_ID_OTHER
+
+	webResourcesActivity.StartTime = al.Transaction().UnixTimestamp()
+	webResourcesActivity.TypeName = "Read"
+
+	webResourcesActivity.Observables = f.getObservables(al)
+
+	// Not implemented
+	// webResourcesActivity.Count = 0
+	// webResourcesActivity.Duration = 0
+	// webResourcesActivity.EndTime = 0
+	// webResourcesActivity.RawData = ""
+	// webResourcesActivity.Status = ""
+	// webResourcesActivity.StatusCode = ""
+	// webResourcesActivity.StatusDetail = ""
+	// webResourcesActivity.StatusId = enums.WEB_RESOURCES_ACTIVITY_STATUS_ID_WEB_RESOURCES_ACTIVITY_STATUS_ID_UNKNOWN
+	// webResourcesActivity.Tls = &objects.Tls{}
+	// webResourcesActivity.WebResourcesResult =
+	// webResourcesActivity.Unmapped = nil
+
+	logJson, _ := json.Marshal(&webResourcesActivity)
+
+	return logJson, nil
+}
+
+func (ocsfFormatter) MIME() string {
+	return "application/json"
+}
+
+var (
+	_ plugintypes.AuditLogFormatter = (*ocsfFormatter)(nil)
+)
diff --git a/internal/auditlog/init.go b/internal/auditlog/init.go
index 6984da4..41a16e6 100644
--- a/internal/auditlog/init.go
+++ b/internal/auditlog/init.go
@@ -22,4 +22,5 @@ func init() {
 	RegisterFormatter("json", &jsonFormatter{})
 	RegisterFormatter("jsonlegacy", &legacyJSONFormatter{})
 	RegisterFormatter("native", &nativeFormatter{})
+	RegisterFormatter("ocsf", &ocsfFormatter{})
 }
diff --git a/internal/auditlog/init_tinygo.go b/internal/auditlog/init_tinygo.go
index ebf5244..b10fd79 100644
--- a/internal/auditlog/init_tinygo.go
+++ b/internal/auditlog/init_tinygo.go
@@ -22,4 +22,5 @@ func init() {
 	RegisterFormatter("json", &jsonFormatter{})
 	RegisterFormatter("jsonlegacy", &legacyJSONFormatter{})
 	RegisterFormatter("native", &nativeFormatter{})
+	RegisterFormatter("ocsf", &ocsfFormatter{})
 }
diff --git a/internal/corazawaf/rule.go b/internal/corazawaf/rule.go
index bfff25e..cd6cd62 100644
--- a/internal/corazawaf/rule.go
+++ b/internal/corazawaf/rule.go
@@ -5,7 +5,6 @@ package corazawaf
 
 import (
 	"fmt"
-	"reflect"
 	"regexp"
 	"strings"
 	"sync"
@@ -399,7 +398,7 @@ func (r *Rule) transformArg(arg types.MatchData, argIdx int, cache map[transform
 		default:
 			// NOTE: See comment on transformationKey struct to understand this hacky code
 			argKey := arg.Key()
-			argKeyPtr := (*reflect.StringHeader)(unsafe.Pointer(&argKey)).Data // nolint:staticcheck
+			argKeyPtr := unsafe.StringData(argKey)
 			key := transformationKey{
 				argKey:            argKeyPtr,
 				argIndex:          argIdx,
diff --git a/internal/corazawaf/rulegroup.go b/internal/corazawaf/rulegroup.go
index 80bce76..81b3f0c 100644
--- a/internal/corazawaf/rulegroup.go
+++ b/internal/corazawaf/rulegroup.go
@@ -253,7 +253,7 @@ type transformationKey struct {
 	// transaction phase and we would never have different string pointers with the same
 	// content, or more problematically same pointer for different content, as the strings
 	// will be alive throughout the phase.
-	argKey            uintptr
+	argKey            *byte
 	argIndex          int
 	argVariable       variables.RuleVariable
 	transformationsID int
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 880c622..36159c6 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -67,6 +67,7 @@ type Transaction struct {
 	// Copies from the WAF instance that may be overwritten by the ctl action
 	AuditEngine               types.AuditEngineStatus
 	AuditLogParts             types.AuditLogParts
+	AuditLogFormat            string
 	ForceRequestBodyVariable  bool
 	RequestBodyAccess         bool
 	RequestBodyLimit          int64
@@ -1372,6 +1373,18 @@ func (tx *Transaction) AuditLog() *auditlog.Log {
 
 	clientPort, _ := strconv.Atoi(tx.variables.remotePort.Get())
 	hostPort, _ := strconv.Atoi(tx.variables.serverPort.Get())
+
+	// Convert the transaction fullRequestLength to Int32
+	requestLength, err := strconv.ParseInt(tx.variables.fullRequestLength.Get(), 10, 32)
+	if err != nil {
+		requestLength = 0
+		tx.DebugLogger().Error().
+			Str("transaction", "AuditLog").
+			Str("value", tx.variables.fullRequestLength.Get()).
+			Err(err).
+			Msg("Error converting request length to integer")
+	}
+
 	// YYYY/MM/DD HH:mm:ss
 	ts := time.Unix(0, tx.Timestamp).Format("2006/01/02 15:04:05")
 	al.Transaction_ = auditlog.Transaction{
@@ -1387,7 +1400,10 @@ func (tx *Transaction) AuditLog() *auditlog.Log {
 			Method_:   tx.variables.requestMethod.Get(),
 			URI_:      tx.variables.requestURI.Get(),
 			Protocol_: tx.variables.requestProtocol.Get(),
+			Args_:     tx.variables.args,
+			Length_:   int32(requestLength),
 		},
+		IsInterrupted_: tx.IsInterrupted(),
 	}
 
 	for _, part := range tx.AuditLogParts {
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index e6946a3..e4430a8 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -119,6 +119,9 @@ type WAF struct {
 	// Array of logging parts to be used
 	AuditLogParts types.AuditLogParts
 
+	// Audit log format
+	AuditLogFormat string
+
 	// Contains the regular expression for relevant status audit logging
 	AuditLogRelevantStatus *regexp.Regexp
 
@@ -174,6 +177,7 @@ func (w *WAF) newTransaction(opts Options) *Transaction {
 	tx.SkipAfter = ""
 	tx.AuditEngine = w.AuditEngine
 	tx.AuditLogParts = w.AuditLogParts
+	tx.AuditLogFormat = w.AuditLogFormat
 	tx.ForceRequestBodyVariable = false
 	tx.RequestBodyAccess = w.RequestBodyAccess
 	tx.RequestBodyLimit = int64(w.RequestBodyLimit)
@@ -305,8 +309,9 @@ func NewWAF() *WAF {
 			types.AuditLogPartResponseHeaders,
 			types.AuditLogPartAuditLogTrailer,
 		},
-		Logger:        logger,
-		ArgumentLimit: 1000,
+		AuditLogFormat: "Native",
+		Logger:         logger,
+		ArgumentLimit:  1000,
 	}
 
 	if environment.HasAccessToFS {
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index 4d2350f..b6670c2 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -646,9 +646,9 @@ func directiveSecAuditLogType(options *DirectiveOptions) error {
 	return nil
 }
 
-// Description: Select the output format of the AuditLogs. The format can be either
-// the native AuditLogs format or JSON.
-// Syntax: SecAuditLogFormat JSON|Native
+// Description: Select the output format of the AuditLogs. The format can be
+// the native AuditLogs format, JSON, or OCSF (Open CyberSecurity Schema Framework).
+// Syntax: SecAuditLogFormat JSON|JsonLegacy|Native|OCSF
 // Default: Native
 func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {


Test patch:
diff --git a/experimental/plugins/macro/macro_test.go b/experimental/plugins/macro/macro_test.go
index 2f7a2ba..94e9579 100644
--- a/experimental/plugins/macro/macro_test.go
+++ b/experimental/plugins/macro/macro_test.go
@@ -67,6 +67,24 @@ func TestCompile(t *testing.T) {
 		}
 	})
 
+	t.Run("unknown key", func(t *testing.T) {
+		m := &macro{}
+
+		err := m.compile("%{tx.missing_key}")
+		if err != nil {
+			t.Fatalf("unexpected error")
+		}
+
+		if want, have := 1, len(m.tokens); want != have {
+			t.Fatalf("unexpected number of tokens: want %d, have %d", want, have)
+		}
+
+		expectedMacro := macroToken{"tx.missing_key", variables.TX, "missing_key"}
+		if want, have := m.tokens[0], expectedMacro; want != have {
+			t.Errorf("unexpected token: wanted %v, got %v", want, have)
+		}
+	})
+
 	t.Run("valid macro", func(t *testing.T) {
 		m := &macro{}
 		err := m.compile("%{tx.count}")
@@ -123,7 +141,7 @@ func TestCompile(t *testing.T) {
 }
 
 func TestExpand(t *testing.T) {
-	t.Run("no expansion", func(t *testing.T) {
+	t.Run("unknown variable", func(t *testing.T) {
 		m := &macro{
 			tokens: []macroToken{
 				{"text", variables.Unknown, ""},
diff --git a/internal/actions/setvar_test.go b/internal/actions/setvar_test.go
index 591d5a8..ff73a41 100644
--- a/internal/actions/setvar_test.go
+++ b/internal/actions/setvar_test.go
@@ -96,6 +96,18 @@ func TestSetvarEvaluate(t *testing.T) {
 			init:                     "TX.newvar=-%{tx.missingvar}",
 			expectInvalidSyntaxError: true,
 		},
+		{
+			name:                     "Non Numerical Operation - If the value starts with -",
+			init:                     "TX.newvar=----expected_value",
+			expectInvalidSyntaxError: false,
+			expectNewVarValue:        "----expected_value",
+		},
+		{
+			name:                     "Non Numerical Operation - If the value starts with +",
+			init:                     "TX.newvar=+++expected_value",
+			expectInvalidSyntaxError: false,
+			expectNewVarValue:        "+++expected_value",
+		},
 	}
 
 	for _, tt := range tests {
@@ -108,24 +120,29 @@ func TestSetvarEvaluate(t *testing.T) {
 			a := setvar()
 			metadata := &md{}
 			if err := a.Init(metadata, tt.init); err != nil {
-				t.Error("unexpected error during setvar init")
+				t.Fatal("unexpected error during setvar init")
 			}
+
 			waf := corazawaf.NewWAF()
 			waf.Logger = logger
+
 			tx := waf.NewTransaction()
 			a.Evaluate(metadata, tx)
+
 			if tt.expectInvalidSyntaxError {
+				t.Log(logsBuf.String())
 				if logsBuf.Len() == 0 {
-					t.Fatal("expected error")
+					t.Fatal("expected logs")
 				}
+
 				if !strings.Contains(logsBuf.String(), invalidSyntaxAtoiError) {
-					t.Errorf("expected error containing %q, got %q", invalidSyntaxAtoiError, logsBuf.String())
+					t.Errorf("expected error log containing %q, got %q", invalidSyntaxAtoiError, logsBuf.String())
 				}
+
 				if !strings.Contains(logsBuf.String(), warningKeyNotFoundInCollection) {
-					t.Errorf("expected error containing %q, got %q", warningKeyNotFoundInCollection, logsBuf.String())
+					t.Errorf("expected error log containing %q, got %q", warningKeyNotFoundInCollection, logsBuf.String())
 				}
-			}
-			if logsBuf.Len() != 0 && !tt.expectInvalidSyntaxError {
+			} else if logsBuf.Len() != 0 {
 				t.Fatalf("unexpected error: %s", logsBuf.String())
 			}
 
diff --git a/internal/auditlog/auditlog_test.go b/internal/auditlog/auditlog_test.go
index d7b1950..089640c 100644
--- a/internal/auditlog/auditlog_test.go
+++ b/internal/auditlog/auditlog_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
 //go:build !tinygo
@@ -6,20 +6,188 @@
 
 package auditlog
 
-import "testing"
+import (
+	"slices"
+	"testing"
+)
+
+func TestAuditLogUnmarshalInvalidJSON(t *testing.T) {
+	// Improper JSON data (missing a closing '}')
+	invalidSerializedLog := []byte(`{
+		"transaction": {
+			"id":
+	}`)
+
+	log := &Log{}
+
+	// Verify a error is returned for invalidly formatted JSON data
+	if err := log.UnmarshalJSON(invalidSerializedLog); err != nil {
+		if err.Error() != "invalid character '}' looking for beginning of value" {
+			t.Errorf("failed to match error message, \ngot: %s, \nexpected: %s", err, "invalid character '}' looking for beginning of value")
+		}
+
+	}
+}
+
+func TestAuditLogUnmarshalEmptyJSON(t *testing.T) {
+	serializedLog := []byte(`{
+		"transaction": {
+		}
+	}`)
+
+	log := &Log{}
+
+	// Validate proper results for nil values
+	if err := log.UnmarshalJSON(serializedLog); err != nil {
+		t.Error(err)
+	}
+
+	if want, have := "", log.Transaction().ID(); want != have {
+		t.Errorf("failed to match transaction id, got: %s, expected: %s", have, want)
+	}
+
+	// Validate Transaction Request parameters
+	if want, have := false, log.Transaction().HasRequest(); want != have {
+		t.Errorf("failed to match transaction has request, got: %t, expected: %t", have, want)
+	}
+
+	if want, have := "", log.Transaction().Request().Method(); want != have {
+		t.Errorf("failed to match transaction request method, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "", log.Transaction().Request().HTTPVersion(); want != have {
+		t.Errorf("failed to match transaction request method, got: %s, expected: %s", have, want)
+	}
+
+	// Validate Transaction Response parameters
+	if want, have := false, log.Transaction().HasResponse(); want != have {
+		t.Errorf("failed to match transaction has response, got: %t, expected: %t", have, want)
+	}
+
+	if want, have := 0, log.Transaction().Response().Status(); want != have {
+		t.Errorf("failed to match transaction has response, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := "", log.Transaction().Response().Protocol(); want != have {
+		t.Errorf("failed to match transaction has response, got: %s, expected: %s", have, want)
+	}
+
+	// Validate log messages
+	if want, have := 0, len(log.Messages()); want != have {
+		t.Errorf("failed to match messages length, got: %d, expected: %d", have, want)
+	}
+
+	// Validaate Transaction Producer parameters
+	if want, have := "", log.Transaction().Producer().Connector(); want != have {
+		t.Errorf("failed to match producer connector, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "", log.Transaction().Producer().Version(); want != have {
+		t.Errorf("failed to match producer version, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "", log.Transaction().Producer().Server(); want != have {
+		t.Errorf("failed to match producer server, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "", log.Transaction().Producer().RuleEngine(); want != have {
+		t.Errorf("failed to match producer rule engine, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "", log.Transaction().Producer().Stopwatch(); want != have {
+		t.Errorf("failed to match producer stopwatch, got: %s, expected: %s", have, want)
+	}
+
+	if have := log.Transaction().Producer().Rulesets(); nil != have {
+		t.Errorf("failed to match producer ruleset, got: %s, expected: nil", have)
+	}
+
+	if want, have := false, log.Transaction_.HasResponse(); want != have {
+		t.Errorf("failed to match transaction has response, got: %t, expected: %t", have, want)
+	}
+
+	if want, have := 0, log.Transaction().Response().Status(); want != have {
+		t.Errorf("failed to match producer server, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := false, log.Transaction_.HasRequest(); want != have {
+		t.Errorf("failed to match transaction has request, got: %t, expected: %t", have, want)
+	}
+}
 
 func TestAuditLogUnmarshalJSON(t *testing.T) {
+
 	serializedLog := []byte(`{
 		"transaction": {
-			"id": "abc123"
+			"id": "abc123",
+			"producer": {
+				"connector": "c",
+				"version": "d",
+				"server": "e",
+				"rule_engine": "f",
+				"stopwatch": "g",
+				"rulesets": [
+					"h",
+					"i"
+				]
+			},
+			"request": {
+				"method": "",
+				"protocol": "",
+				"uri": "",
+				"http_version": "",
+				"body": "",
+				"length": 123,
+				"uid": "",
+				"headers":{
+					"request_header_key": [
+						"request_header_value"
+					]
+				}
+					
+			},
+			"response": {
+				"status": 200,
+				"protocol": "p",
+				"body": "b",
+				"headers":{
+					"response_header_key": [
+						"response_header_value"
+					]
+				}
+			}
 		},
 		"messages": [
 			{
 				"actionset": "a",
-				"message": "b"
+				"message": "b",
+				"data": {
+					"file": "/etc/coraza-spoa/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf",
+					"line": 4468,
+					"id": 930130,
+					"rev": "1.3",
+					"msg": "Restricted File Access Attempt",
+					"data": "Matched Data: /.git/ found within REQUEST_FILENAME: /.git/config",
+					"severity": 2,
+					"ver": "OWASP_CRS/4.4.0-dev",
+					"maturity": 3,
+					"accuracy": 8,
+					"tags": [
+						"application-multi",
+						"language-multi",
+						"platform-multi",
+						"attack-lfi",
+						"paranoia-level/1",
+						"OWASP_CRS",
+						"capec/1000/255/153/126",
+						"PCI/6.5.4"
+					],
+					"raw": "SecRule REQUEST_FILENAME \"@pmFromFile restricted-files.data\" \"id: 930130,phase: 1,block,capture,t:none,t:utf8toUnicode,t:urlDecodeUni,t:normalizePathWin,msg:\"Restricted File Access Attempt\",logdata:\"Matched Data: %{TX.0} found within %{MATCHED_VAR_NAME}: %{MATCHED_VAR}\",tag:\"application-multi\",tag:\"language-multi\",tag:\"platform-multi\",tag:\"attack-lfi\",tag:\"paranoia-level/1\",tag:\"OWASP_CRS\",tag:\"capec/1000/255/153/126\",tag:\"PCI/6.5.4\",ver:\"OWASP_CRS/4.4.0-dev\",severity:\"CRITICAL\",setvar:\"tx.lfi_score=+%{tx.critical_anomaly_score}\",setvar:\"tx.inbound_anomaly_score_pl1=+%{tx.critical_anomaly_score}\"\""
+				}
 			}
 		]
 	}`)
+
 	log := &Log{}
 	if err := log.UnmarshalJSON(serializedLog); err != nil {
 		t.Error(err)
@@ -29,18 +197,41 @@ func TestAuditLogUnmarshalJSON(t *testing.T) {
 		t.Errorf("failed to match transaction id, got: %s, expected: %s", have, want)
 	}
 
-	if want, have := false, log.Transaction_.HasRequest(); want != have {
+	// Validate Transaction Request parameters
+	if want, have := true, log.Transaction().HasRequest(); want != have {
 		t.Errorf("failed to match transaction has request, got: %t, expected: %t", have, want)
 	}
 
-	if want, have := "", log.Transaction_.Request().Method(); want != have {
+	if want, have := "", log.Transaction().Request().Method(); want != have {
 		t.Errorf("failed to match transaction request method, got: %s, expected: %s", have, want)
 	}
 
-	if want, have := false, log.Transaction_.HasResponse(); want != have {
+	if want, have := "request_header_value", log.Transaction().Request().Headers()["request_header_key"]; !slices.Contains(have, want) {
+		t.Errorf("failed to match message data tags, expected tag: %s not found in array", want)
+	}
+
+	// Validate Transaction Response parameters
+	if want, have := true, log.Transaction().HasResponse(); want != have {
 		t.Errorf("failed to match transaction has response, got: %t, expected: %t", have, want)
 	}
 
+	if want, have := 200, log.Transaction().Response().Status(); want != have {
+		t.Errorf("failed to match transaction response status, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := "p", log.Transaction().Response().Protocol(); want != have {
+		t.Errorf("failed to match transaction response protocol, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "b", log.Transaction().Response().Body(); want != have {
+		t.Errorf("failed to match transaction response body, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "response_header_value", log.Transaction().Response().Headers()["response_header_key"]; !slices.Contains(have, want) {
+		t.Errorf("failed to match transaction response header response_header_key, expected value: %s not found in array", want)
+	}
+
+	// Validate log messages
 	if want, have := 1, len(log.Messages()); want != have {
 		t.Errorf("failed to match messages length, got: %d, expected: %d", have, want)
 	}
@@ -52,4 +243,90 @@ func TestAuditLogUnmarshalJSON(t *testing.T) {
 	if want, have := "b", log.Messages()[0].Message(); want != have {
 		t.Errorf("failed to match message, got: %s, expected: %s", have, want)
 	}
+
+	if want, have := "/etc/coraza-spoa/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf", log.Messages()[0].Data().File(); want != have {
+		t.Errorf("failed to match message data file, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := 4468, log.Messages()[0].Data().Line(); want != have {
+		t.Errorf("failed to match message data line, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := 930130, log.Messages()[0].Data().ID(); want != have {
+		t.Errorf("failed to match message data id, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := "1.3", log.Messages()[0].Data().Rev(); want != have {
+		t.Errorf("failed to match message data rev, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "Restricted File Access Attempt", log.Messages()[0].Data().Msg(); want != have {
+		t.Errorf("failed to match message data msg, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "Matched Data: /.git/ found within REQUEST_FILENAME: /.git/config", log.Messages()[0].Data().Data(); want != have {
+		t.Errorf("failed to match message data data, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := 2, log.Messages()[0].Data().Severity().Int(); want != have {
+		t.Errorf("failed to match message data severity, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := "OWASP_CRS/4.4.0-dev", log.Messages()[0].Data().Ver(); want != have {
+		t.Errorf("failed to match message data ver, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := 3, log.Messages()[0].Data().Maturity(); want != have {
+		t.Errorf("failed to match message data maturity, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := 8, log.Messages()[0].Data().Accuracy(); want != have {
+		t.Errorf("failed to match message data accuracy, got: %d, expected: %d", have, want)
+	}
+
+	if want, have := "application-multi", log.Messages()[0].Data().Tags(); !slices.Contains(have, want) {
+		t.Errorf("failed to match message data tags, expected tag: %s not found in array", want)
+	}
+
+	if want, have := "paranoia-level/1", log.Messages()[0].Data().Tags(); !slices.Contains(have, want) {
+		t.Errorf("failed to match message data tags, expected tag: %s not found in array", want)
+	}
+
+	if want, have := "capec/1000/255/153/126", log.Messages()[0].Data().Tags(); !slices.Contains(have, want) {
+		t.Errorf("failed to match message data tags, expected tag: %s not found in array", want)
+	}
+
+	if want, have := "PCI/6.5.4", log.Messages()[0].Data().Tags(); !slices.Contains(have, want) {
+		t.Errorf("failed to match message data tags, expected tag: %s not found in array", want)
+	}
+
+	// Validate Transaction Producer parameters
+	if want, have := "c", log.Transaction().Producer().Connector(); want != have {
+		t.Errorf("failed to match producer connector, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "d", log.Transaction().Producer().Version(); want != have {
+		t.Errorf("failed to match producer version, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "e", log.Transaction().Producer().Server(); want != have {
+		t.Errorf("failed to match producer server, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "f", log.Transaction().Producer().RuleEngine(); want != have {
+		t.Errorf("failed to match producer rule engine, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "g", log.Transaction().Producer().Stopwatch(); want != have {
+		t.Errorf("failed to match producer stopwatch, got: %s, expected: %s", have, want)
+	}
+
+	if want, have := "h", log.Transaction().Producer().Rulesets(); !slices.Contains(have, want) {
+		t.Errorf("failed to match transaction producer rulesets, expected tag: %s not found in array", want)
+	}
+
+	if want, have := "i", log.Transaction().Producer().Rulesets(); !slices.Contains(have, want) {
+		t.Errorf("failed to match transaction producer rulesets, expected tag: %s not found in array", want)
+	}
+
 }
diff --git a/internal/auditlog/formats_ocsf_test.go b/internal/auditlog/formats_ocsf_test.go
new file mode 100644
index 0000000..2f04099
--- /dev/null
+++ b/internal/auditlog/formats_ocsf_test.go
@@ -0,0 +1,411 @@
+// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package auditlog
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
+	"github.com/corazawaf/coraza/v3/internal/collections"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+	"github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/events/application"
+	"github.com/valllabh/ocsf-schema-golang/ocsf/v1_2_0/events/application/enums"
+)
+
+func TestOCSFFormatter(t *testing.T) {
+	for _, al := range createAuditLogs() {
+		f := &ocsfFormatter{}
+		data, err := f.Format(al)
+		if err != nil {
+			t.Error(err)
+		}
+		if !strings.Contains(f.MIME(), "json") {
+			t.Errorf("failed to match MIME, expected json and got %s", f.MIME())
+		}
+
+		var wra application.WebResourcesActivity
+		if err := json.Unmarshal(data, &wra); err != nil {
+			t.Error(err)
+		}
+
+		// validate MIME type
+		if f.MIME() != "application/json" {
+			t.Errorf("failed to match ocsfFormatter MIME type, \ngot: %s\nexpected: %s", fmt.Sprint(f.MIME()), "application/json")
+		}
+
+		// validate Unix Timestamp
+		if wra.Time != al.Transaction().UnixTimestamp() {
+			t.Errorf("failed to match audit log Unix Timestamp, \ngot: %s\nexpected: %s", fmt.Sprint(wra.Time), fmt.Sprint(al.Transaction().UnixTimestamp()))
+		}
+
+		// validate transation interruption
+		if al.Transaction().IsInterrupted() {
+			if wra.Action != "Denied" {
+				t.Errorf("failed to match audit log Action, \ngot: %s\nexpected: %s", wra.Action, "Denied")
+			}
+
+			if wra.ActionId != enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_DENIED {
+				t.Errorf("failed to match audit log Action ID, \ngot: %s\nexpected: %s", wra.ActionId, enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_DENIED)
+			}
+		} else {
+			if wra.Action != "Allowed" {
+				t.Errorf("failed to match audit log Action, \ngot: %s\nexpected: %s", wra.Action, "Allowed")
+			}
+
+			if wra.ActionId != enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_ALLOWED {
+				t.Errorf("failed to match audit log Action ID, \ngot: %s\nexpected: %s", wra.ActionId, enums.WEB_RESOURCES_ACTIVITY_ACTION_ID_WEB_RESOURCES_ACTIVITY_ACTION_ID_ALLOWED)
+			}
+		}
+
+		// validate Server ID
+		for _, observable := range wra.Observables {
+			if observable.Name == "ServerID" {
+				if observable.Value != al.Transaction().ServerID() {
+					t.Errorf("failed to match audit log Server ID, \ngot: %s\nexpected: %s", observable.Value, al.Transaction().ServerID())
+				}
+			}
+		}
+
+		// validate transaction requests
+		if al.Transaction().HasRequest() {
+			// validate Request URI
+			if wra.HttpRequest.Url.UrlString != al.Transaction().Request().URI() {
+				t.Errorf("failed to match audit log URI, \ngot: %s\nexpected: %s", wra.HttpRequest.Url.UrlString, al.Transaction().Request().URI())
+			}
+			// validate Request Method
+			if wra.HttpRequest.HttpMethod != al.Transaction().Request().Method() {
+				t.Errorf("failed to match audit log HTTP Request Method, \ngot: %s\nexpected: %s", wra.HttpRequest.HttpMethod, al.Transaction().Request().Method())
+			}
+			// validate Request Headers
+			for _, header := range wra.HttpRequest.HttpHeaders {
+				if header.Value != al.Transaction().Request().Headers()[header.Name][0] {
+					t.Errorf("failed to match audit log Request Header, \ngot: %s\nexpected: %s", header.Value, al.Transaction().Request().Headers()[header.Name][0])
+				}
+			}
+			// validate Request Files
+			for _, file := range al.Transaction().Request().Files() {
+				for _, observable := range wra.Observables {
+					if observable.Name == file.Name() {
+						if observable.Type == "File Name" {
+							if file.Name() != observable.Value {
+								t.Errorf("failed to match audit log Request File Name, \ngot: %s\nexpected: %s", observable.Value, file.Name())
+							}
+						}
+						if observable.Type == "Mime" {
+							if file.Mime() != observable.Value {
+								t.Errorf("failed to match audit log Request File Mime, \ngot: %s\nexpected: %s", observable.Value, file.Mime())
+							}
+						}
+						if observable.Type == "Size" {
+							if fmt.Sprint(file.Size()) != observable.Value {
+								t.Errorf("failed to match audit log Request File Size, \ngot: %s\nexpected: %s", observable.Value, fmt.Sprint(file.Size()))
+							}
+						}
+					}
+				}
+			}
+
+			// validate Request Protocol
+			if wra.HttpRequest.Version != al.Transaction().Request().Protocol() {
+				t.Errorf("failed to match audit log HTTP Request Protocol, \ngot: %s\nexpected: %s", wra.HttpRequest.Version, al.Transaction().Request().Protocol())
+			}
+
+			// validate Request Arguments
+			if al.Transaction().Request().Args() != nil {
+				for _, arg := range al.Transaction().Request().Args().FindAll() {
+					if strings.Contains(wra.HttpRequest.Args, fmt.Sprintf("%s=%s", arg.Key(), arg.Value())) == false {
+						t.Errorf("failed to match audit log Request arguments, \n%s not found in: %s", fmt.Sprintf("%s=%s", arg.Key(), arg.Value()), wra.HttpRequest.Args)
+					}
+				}
+			}
+
+			// validate Request UID
+			if wra.HttpRequest.Uid != al.Transaction().ID() {
+				t.Errorf("failed to match audit log HTTP Request UID, \ngot: %s\nexpected: %s", wra.HttpRequest.Uid, al.Transaction().ID())
+			}
+
+			// validate Request Length
+			if wra.HttpRequest.Length != al.Transaction().Request().Length() {
+				t.Errorf("failed to match audit log HTTP Request Length, \ngot: %d\nexpected: %d", wra.HttpRequest.Length, al.Transaction().Request().Length())
+			}
+		}
+
+		if al.Transaction().HasResponse() {
+			// validate Response Status
+			if int(wra.HttpResponse.Code) != al.Transaction().Response().Status() {
+				t.Errorf("failed to match audit log HTTP Response Status, \ngot: %s\nexpected: %s", fmt.Sprint(wra.HttpResponse.Code), fmt.Sprint(al.Transaction().Response().Status()))
+			}
+
+			// validate Response Headers
+			for _, header := range wra.HttpResponse.HttpHeaders {
+				if header.Value != al.Transaction().Response().Headers()[header.Name][0] {
+					t.Errorf("failed to match audit log Response Header, \ngot: %s\nexpected: %s", header.Value, al.Transaction().Response().Headers()[header.Name][0])
+				}
+			}
+		}
+
+		// validate Enrichments (Rule Matches)
+		if wra.Enrichments[0].Name != al.Messages()[0].Data().Msg() {
+			t.Errorf("failed to match audit log data, \ngot: %s\nexpected: %s", wra.Enrichments[0].Name, al.Messages()[0].Data().Msg())
+		}
+
+		// validate Schema
+		// ocsf-schema-golang appears to have a bug and is not validating against the OCSF 1.2 Schema.
+		// It would be nice to include this validation as part of the test suite, but for now it must be disabled until this bug is fixed.
+		// if ocsfvalidate_1_2.Validate("web_resources_activity", data) != nil {
+		// 	t.Errorf("failed to validate audit log schema, \ngot: %s\nexpected: %s", ocsfvalidate_1_2.Validate("web_resources_activity", data), "")
+		// }
+	}
+}
+
+func createAuditLogs() []*Log {
+
+	transactionLogs := []*Log{}
+
+	// Test case for "normal" / "typical" transaction
+	getArgs := collections.NewMap(variables.ArgsGet)
+	postArgs := collections.NewMap(variables.ArgsPost)
+	pathArgs := collections.NewMap(variables.ArgsPath)
+	args := collections.NewConcatKeyed(variables.Args, getArgs, postArgs, pathArgs)
+	getArgs.Add("qkey", "qvalue")
+	postArgs.Add("pkey", "pvalue")
+	transactionLogs = append(transactionLogs, &Log{
+		Parts_: []types.AuditLogPart{
+			types.AuditLogPartRequestHeaders,
+			types.AuditLogPartRequestBody,
+			types.AuditLogPartIntermediaryResponseBody,
+			types.AuditLogPartResponseHeaders,
+			types.AuditLogPartAuditLogTrailer,
+			types.AuditLogPartRulesMatched,
+		},
+		Transaction_: Transaction{
+			Timestamp_:     "02/Jan/2006:15:04:20 -0700",
+			UnixTimestamp_: 1136239460,
+			ID_:            "123",
+			IsInterrupted_: true,
+			Request_: &TransactionRequest{
+				URI_:    "/test.php?qkey=qvalue",
+				Method_: "GET",
+				Headers_: map[string][]string{
+					"host": {
+						"test.coraza.null",
+					},
+				},
+				Body_:     "pkey=pvalue",
+				Protocol_: "HTTP/1.1",
+				Args_:     args,
+				Length_:   112345,
+				Files_: []plugintypes.AuditLogTransactionRequestFiles{
+					&TransactionRequestFiles{
+						Name_: "dummyfile.txt",
+						Mime_: "text/plain",
+						Size_: 12345,
+					},
+				},
+			},
+			Response_: &TransactionResponse{
+				Status_: 200,
+				Headers_: map[string][]string{
+					"connection": {
+						"close",
+					},
+				},
+				Body_: "some response body",
+			},
+			Producer_: &TransactionProducer{
+				Connector_: "some connector",
+				Version_:   "1.2.3",
+			},
+		},
+		Messages_: []plugintypes.AuditLogMessage{
+			&Message{
+				Message_: "some message",
+				Data_: &MessageData{
+					Msg_: "some message",
+					Raw_: "SecAction \"id:100\"",
+				},
+			},
+		},
+	})
+
+	// Test case for abnormal transaction (all empty values, no arguments)
+	getArgs = collections.NewMap(variables.ArgsGet)
+	postArgs = collections.NewMap(variables.ArgsPost)
+	pathArgs = collections.NewMap(variables.ArgsPath)
+	args = collections.NewConcatKeyed(variables.Args, getArgs, postArgs, pathArgs)
+	transactionLogs = append(transactionLogs, &Log{
+		Parts_: []types.AuditLogPart{
+			types.AuditLogPartRequestHeaders,
+			types.AuditLogPartRequestBody,
+			types.AuditLogPartIntermediaryResponseBody,
+			types.AuditLogPartResponseHeaders,
+			types.AuditLogPartAuditLogTrailer,
+			types.AuditLogPartRulesMatched,
+		},
+		Transaction_: Transaction{
+			Timestamp_:     "",
+			UnixTimestamp_: 0,
+			ID_:            "",
+			IsInterrupted_: true,
+			ServerID_:      "someServer",
+			Request_: &TransactionRequest{
+				URI_:    "",
+				Method_: "",
+				Headers_: map[string][]string{
+					"host": {
+						"",
+					},
+					"accept": {
+						"",
+					},
+					"accept-encoding": {
+						"",
+					},
+					"accept-language": {
+						"",
+					},
+					"cache-control": {
+						"",
+					},
+					"connection": {
+						"",
+					},
+					"upgrade-insecure-requests": {
+						"",
+					},
+					"user-agent": {
+						"",
+					},
+					"x-forwarded-for": {
+						"",
+					},
+					"referer": {
+						"",
+					},
+				},
+				Body_:     "",
+				Protocol_: "",
+				Args_:     args,
+				Length_:   0,
+			},
+			Response_: &TransactionResponse{
+				Status_: 200,
+				Headers_: map[string][]string{
+					"connection": {
+						"",
+					},
+					"content-type": {
+						"",
+					},
+					"referrer-policy": {
+						"",
+					},
+					"strict-transport-security": {
+						"",
+					},
+					"transfer-encoding": {
+						"",
+					},
+					"referer": {
+						"",
+					},
+				},
+				Body_: "",
+			},
+			Producer_: &TransactionProducer{
+				Connector_: "",
+				Version_:   "",
+			},
+		},
+		Messages_: []plugintypes.AuditLogMessage{
+			&Message{
+				Message_: "some message",
+				Data_: &MessageData{
+					Msg_: "some message",
+					Raw_: "SecAction \"id:100\"",
+				},
+			},
+		},
+	})
+
+	// Test case for abnormal transaction (all empty values, no arguments, no reponse)
+	getArgs = collections.NewMap(variables.ArgsGet)
+	postArgs = collections.NewMap(variables.ArgsPost)
+	pathArgs = collections.NewMap(variables.ArgsPath)
+	args = collections.NewConcatKeyed(variables.Args, getArgs, postArgs, pathArgs)
+	transactionLogs = append(transactionLogs, &Log{
+		Parts_: []types.AuditLogPart{
+			types.AuditLogPartRequestHeaders,
+			types.AuditLogPartRequestBody,
+			types.AuditLogPartIntermediaryResponseBody,
+			types.AuditLogPartResponseHeaders,
+			types.AuditLogPartAuditLogTrailer,
+			types.AuditLogPartRulesMatched,
+		},
+		Transaction_: Transaction{
+			Timestamp_:     "",
+			UnixTimestamp_: 0,
+			ID_:            "",
+			IsInterrupted_: true,
+			Request_: &TransactionRequest{
+				URI_:    "",
+				Method_: "",
+				Headers_: map[string][]string{
+					"host": {
+						"",
+					},
+					"accept": {
+						"",
+					},
+					"accept-encoding": {
+						"",
+					},
+					"accept-language": {
+						"",
+					},
+					"cache-control": {
+						"",
+					},
+					"connection": {
+						"",
+					},
+					"upgrade-insecure-requests": {
+						"",
+					},
+					"user-agent": {
+						"",
+					},
+					"x-forwarded-for": {
+						"",
+					},
+					"referer": {
+						"",
+					},
+				},
+				Body_:     "",
+				Protocol_: "",
+				Args_:     args,
+			},
+			Producer_: &TransactionProducer{
+				Connector_: "",
+				Version_:   "",
+			},
+		},
+		Messages_: []plugintypes.AuditLogMessage{
+			&Message{
+				Message_: "some message",
+				Data_: &MessageData{
+					Msg_: "some message",
+					Raw_: "SecAction \"id:100\"",
+				},
+			},
+		},
+	})
+
+	return transactionLogs
+}
diff --git a/internal/auditlog/https_writer_test.go b/internal/auditlog/https_writer_test.go
index 21c478d..f0713e0 100644
--- a/internal/auditlog/https_writer_test.go
+++ b/internal/auditlog/https_writer_test.go
@@ -1,4 +1,4 @@
-// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
 //go:build !tinygo
@@ -19,7 +19,6 @@ import (
 )
 
 var sampleHttpsAuditLog = &Log{
-
 	Transaction_: Transaction{
 		ID_: "test123",
 	},
@@ -33,7 +32,7 @@ var sampleHttpsAuditLog = &Log{
 	},
 }
 
-func TestHTTPSAuditLog(t *testing.T) {
+func TestHTTPAuditLog(t *testing.T) {
 	writer := &httpsWriter{}
 	formatter := &nativeFormatter{}
 	pts, err := types.ParseAuditLogParts("ABCDEZ")
@@ -74,7 +73,7 @@ func TestHTTPSAuditLog(t *testing.T) {
 	}
 }
 
-func TestJSONAuditHTTPS(t *testing.T) {
+func TestJSONAuditHTTP(t *testing.T) {
 	writer := &httpsWriter{}
 	formatter := &jsonFormatter{}
 	// we create a test http server
@@ -98,3 +97,28 @@ func TestJSONAuditHTTPS(t *testing.T) {
 		t.Fatal(err)
 	}
 }
+
+func TestOCSFAuditHTTP(t *testing.T) {
+	writer := &httpsWriter{}
+	formatter := &ocsfFormatter{}
+	// we create a test http server
+	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		if r.ContentLength == 0 {
+			t.Fatal("ContentLength is 0")
+		}
+		if ct := r.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
+			t.Fatalf("Content-Type is not application/json, got %s", ct)
+		}
+	}))
+	defer server.Close()
+	if err := writer.Init(plugintypes.AuditLogConfig{
+		Target:    server.URL,
+		Formatter: formatter,
+	}); err != nil {
+		t.Fatal(err)
+	}
+	if err := writer.Write(sampleHttpsAuditLog); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/internal/seclang/parser_test.go b/internal/seclang/parser_test.go
index c5000cc..b03ce1f 100644
--- a/internal/seclang/parser_test.go
+++ b/internal/seclang/parser_test.go
@@ -169,6 +169,10 @@ func TestHardcodedIncludeDirectiveDDOS2(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
+	err = tmpFile2.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
 
 	w := bufio.NewWriter(tmpFile)
 	for i := 0; i < maxIncludeRecursion+1; i++ {
diff --git a/testing/coreruleset/.ftw-overrides.yml b/testing/coreruleset/.ftw-overrides.yml
new file mode 100644
index 0000000..0d32f24
--- /dev/null
+++ b/testing/coreruleset/.ftw-overrides.yml
@@ -0,0 +1,52 @@
+version: "v0.0.0"
+meta:
+  engine: "coraza"
+  platform: "go"
+  annotations:
+    - purpose: "Overrides for CRS test suite running against Coraza deployed as a Go middleware"
+test_overrides:
+  # - rule_id: 920100
+  #   test_ids: [5]
+  #   reason: "Invalid uri, Coraza not reached - 301 returned"
+  #   output:
+  #     status: 301
+  - rule_id: 920100
+    test_ids: [8]
+    reason: |
+      On Apache is not allowed to put a colon in the path. Go/http allows it.
+      Note that the colon is a legal character in a regular path segment, according to the RFC.
+    output:
+      status: 200
+      log:
+        no_expect_ids: [920100]
+  - rule_id: 920270
+    test_ids: [4]
+    reason: "Rule works, Apache test expects status 400"
+    output:
+      log:
+        expect_ids: [920270]
+  - rule_id: 920274
+    test_ids: [1]
+    reason: "Host validation. Apache expects status 400. Coraza correctly triggers the rule 920274"
+    output:
+      log:
+        expect_ids: [920274]
+  - rule_id: 920290
+    test_ids: [1]
+    reason: "TODO"
+    output:
+      log:
+        expect_ids: [920280] # TODO: understand why 920280 (Missing Host Header) is triggered and not 920290 (Empty Host header). See what go-ftw sends.
+  - rule_id: 920290
+    test_ids: [4]
+    reason: "TODO"
+    output:
+      log:
+        expect_ids: [920280] # TODO: understand why 920280 (Missing Host Header) is triggered and not 920290 (Empty Host header). See what go-ftw sends.
+  - rule_id: 920430
+    test_ids: [8]
+    reason: "Go/http does not allow HTTP/3.0 - 505 HTTP Version Not Supported"
+    output:
+      status: 505
+      log:
+        no_expect_ids: [920430]
diff --git a/testing/coreruleset/.ftw.yml b/testing/coreruleset/.ftw.yml
index 0cc9300..23077a4 100644
--- a/testing/coreruleset/.ftw.yml
+++ b/testing/coreruleset/.ftw.yml
@@ -1,13 +1,19 @@
+# Tests should not just be ignored via .ftw.yml, but new expectations for each test should be set.
+# Avoid as much as possible adding new entries here, in favor of .ftw-overrides.yml
 ---
 testoverride:
   ignore:
     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)'
-    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)'
-    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'
+
+    # TODO: investigate
+    932200-13: 'Failing only in multiphase evalution'
+    932300-10: 'Failing only in multiphase evalution'
+    933120-2: 'Failing only in multiphase evalution'
+    920280-3: ''
+    920430-3: ''
+    920430-5: ''
+    920430-9: ''
+    920610-2: 'fragments, Coraza might just happly accept them. Run and check it.'
+    920620-1: 'Rule checks if multiple Content-Type headers are kepts. Go/http might keep them and trigger the rule. Run and check it.'
diff --git a/testing/coreruleset/coreruleset_test.go b/testing/coreruleset/coreruleset_test.go
index f926bda..6fe6ead 100644
--- a/testing/coreruleset/coreruleset_test.go
+++ b/testing/coreruleset/coreruleset_test.go
@@ -9,7 +9,6 @@ package coreruleset
 
 import (
 	"bufio"
-	b64 "encoding/base64"
 	"fmt"
 	"io"
 	"io/fs"
@@ -21,6 +20,7 @@ import (
 	"strconv"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/bmatcuk/doublestar/v4"
 	"github.com/coreruleset/go-ftw/config"
@@ -34,6 +34,7 @@ import (
 	"github.com/corazawaf/coraza/v3"
 	txhttp "github.com/corazawaf/coraza/v3/http"
 	"github.com/corazawaf/coraza/v3/types"
+	albedo "github.com/coreruleset/albedo/server"
 )
 
 func BenchmarkCRSCompilation(b *testing.B) {
@@ -220,43 +221,13 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 		t.Fatal(err)
 	}
 
+	// CRS regression tests are expected to be run with https://github.com/coreruleset/albedo as backend server
 	s := httptest.NewServer(txhttp.WrapHandler(waf, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		defer r.Body.Close()
+		// TODO: Investigate why we need to enforce text/plain to have response body tests working.
+		// Check the Content-Type set by albed and SecResponseBodyMimeType
 		w.Header().Set("Content-Type", "text/plain")
-		switch {
-		case r.URL.Path == "/anything", r.URL.Path == "/post":
-			body, err := io.ReadAll(r.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 {
-					t.Fatalf("handler can not read request body: %v", err)
-				}
-				urldecodedBody, err := url.QueryUnescape(string(body))
-				if err != nil {
-					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.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/"):
-			// Emulated httpbin behaviour: /base64 endpoint write the decoded base64 into the response body
-			b64Decoded, err := b64.StdEncoding.DecodeString(strings.TrimPrefix(r.URL.Path, "/base64/"))
-			if err != nil {
-				t.Fatalf("handler can not decode base64: %v", err)
-			}
-			fmt.Fprint(w, string(b64Decoded))
-		default:
-			// Common path "/status/200" defaults here
-			fmt.Fprint(w, "Hello!")
-		}
+		albedo.Handler().ServeHTTP(w, r)
 	})))
 	defer s.Close()
 
@@ -266,7 +237,7 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 		if err != nil {
 			return err
 		}
-		ftwt, err := test.GetTestFromYaml(yaml)
+		ftwt, err := test.GetTestFromYaml(yaml, path)
 		if err != nil {
 			return err
 		}
@@ -293,15 +264,21 @@ SecRule REQUEST_HEADERS:X-CRS-Test "@rx ^.*$" \
 	cfg.TestOverride.Overrides.DestAddr = &host
 	cfg.TestOverride.Overrides.Port = &port
 
+	cfg.LoadPlatformOverrides(".ftw-overrides.yml")
 	res, err := runner.Run(cfg, tests, runner.RunnerConfig{
-		ShowTime: false,
+		ShowTime:    false,
+		ReadTimeout: 3 * time.Second, // Defaults to 1s but looks to be not enough in the CI
 	}, output.NewOutput("quiet", os.Stdout))
 	if err != nil {
 		t.Fatal(err)
 	}
-
-	if len(res.Stats.Failed) > 0 {
-		t.Errorf("failed tests: %v", res.Stats.Failed)
+	totalIgnored := len(res.Stats.Ignored)
+	if totalIgnored > 0 {
+		t.Logf("[info] %d ignored tests: %v", totalIgnored, res.Stats.Ignored)
+	}
+	totalFailed := len(res.Stats.Failed)
+	if totalFailed > 0 {
+		t.Errorf("[fatal] %d failed tests: %v", totalFailed, res.Stats.Failed)
 	}
 }
 
diff --git a/testing/coreruleset/go.mod b/testing/coreruleset/go.mod
index 0e556d5..ddae7cc 100644
--- a/testing/coreruleset/go.mod
+++ b/testing/coreruleset/go.mod
@@ -1,12 +1,13 @@
 module github.com/corazawaf/coraza/v3/testing/coreruleset
 
-go 1.22
+go 1.22.3
 
 require (
 	github.com/bmatcuk/doublestar/v4 v4.6.1
-	github.com/corazawaf/coraza-coreruleset/v4 v4.3.0
+	github.com/corazawaf/coraza-coreruleset/v4 v4.5.0
 	github.com/corazawaf/coraza/v3 v3.0.0-00010101000000-000000000000
-	github.com/coreruleset/go-ftw v0.6.4
+	github.com/coreruleset/albedo v0.0.16
+	github.com/coreruleset/go-ftw v1.0.4-0.20240923043156-8474a93d514a
 	github.com/rs/zerolog v1.33.0
 )
 
@@ -15,8 +16,8 @@ require (
 	github.com/Masterminds/semver v1.5.0 // indirect
 	github.com/Masterminds/sprig v2.22.0+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/coreruleset/ftw-tests-schema/v2 v2.1.0 // indirect
+	github.com/fatih/color v1.17.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
@@ -28,9 +29,10 @@ require (
 	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/file v1.1.0 // indirect
 	github.com/knadh/koanf/providers/rawbytes v0.1.0 // indirect
 	github.com/knadh/koanf/v2 v2.1.1 // indirect
+	github.com/kr/text v0.2.0 // 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
@@ -38,15 +40,19 @@ require (
 	github.com/mitchellh/copystructure v1.2.0 // indirect
 	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/petar-dambovaliev/aho-corasick v0.0.0-20240411101913-e07a1f0e8eb4 // indirect
-	github.com/tidwall/gjson v1.17.1 // indirect
+	github.com/rogpeppe/go-internal v1.13.1 // indirect
+	github.com/tidwall/gjson v1.17.3 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
 	github.com/tidwall/pretty v1.2.1 // indirect
+	github.com/valllabh/ocsf-schema-golang v1.0.3 // indirect
 	github.com/yargevad/filepathx v1.0.0 // indirect
-	golang.org/x/crypto v0.26.0 // indirect
-	golang.org/x/net v0.28.0 // indirect
+	golang.org/x/crypto v0.27.0 // indirect
+	golang.org/x/net v0.29.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
-	golang.org/x/sys v0.23.0 // indirect
+	golang.org/x/sys v0.25.0 // indirect
+	golang.org/x/time v0.6.0 // indirect
 	golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
+	google.golang.org/protobuf v1.34.1 // 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 6b843f0..5efec5a 100644
--- a/testing/coreruleset/go.sum
+++ b/testing/coreruleset/go.sum
@@ -6,19 +6,26 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC
 github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
 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-coreruleset/v4 v4.5.0 h1:4BDr9/yWKSJ7Ch3h7SvSqJBASju73+EqIIF0WxjsFgQ=
+github.com/corazawaf/coraza-coreruleset/v4 v4.5.0/go.mod h1:1FQt1p+JSQ6tYrafMqZrEEdDmhq6aVuIJdnk+bM9hMY=
 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/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
+github.com/coreruleset/albedo v0.0.16-0.20240924185852-4b95a321ebfd h1:4KjOGOv4I81ZOuW/TY29oe0yZCNeNGuPt8HXvmjukPI=
+github.com/coreruleset/albedo v0.0.16-0.20240924185852-4b95a321ebfd/go.mod h1:6mYBASfvvRM2ckXgYO7N5nyKAj8OqLnT4+YLbM0/XWE=
+github.com/coreruleset/albedo v0.0.16 h1:YYWEJBSfAwVmZ5tbBgQQhL8uU6IKeA2QwpAkii3UPKY=
+github.com/coreruleset/albedo v0.0.16/go.mod h1:6mYBASfvvRM2ckXgYO7N5nyKAj8OqLnT4+YLbM0/XWE=
+github.com/coreruleset/ftw-tests-schema/v2 v2.1.0 h1:2ilKzKRG5UzzxBcrJLXFtPalStdQ9jzzaYFuFk0OEk0=
+github.com/coreruleset/ftw-tests-schema/v2 v2.1.0/go.mod h1:ZHVFX5ses4+5IxUP0ufCNg/VqRWxziH6ZuUca092Hxo=
+github.com/coreruleset/go-ftw v1.0.4-0.20240809050408-f8169f0325ac h1:I++204ogJDnOyYQrMs6IdfTYRBIMr4A9Dtix+XdtZEc=
+github.com/coreruleset/go-ftw v1.0.4-0.20240809050408-f8169f0325ac/go.mod h1:gI2N2EYdTIZnXQbsdzBRxbj/zSaYEyrhLJUCOJ3VK6I=
+github.com/coreruleset/go-ftw v1.0.4-0.20240923043156-8474a93d514a h1:ucsngMh75QkedILO3N9JRiVWPbkcelhH055adS51hNs=
+github.com/coreruleset/go-ftw v1.0.4-0.20240923043156-8474a93d514a/go.mod h1:/HKpRGHn0rOEb+VdkfjLqltb3Jr5DHDeKPAiC/04lR8=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
+github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
 github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
 github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
 github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@@ -52,12 +59,16 @@ github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP
 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/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o=
+github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI=
 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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 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=
@@ -79,41 +90,50 @@ github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
 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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
+github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
 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/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
-github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
+github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
 github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
 github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
 github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/valllabh/ocsf-schema-golang v1.0.3 h1:eR8k/3jP/OOqB8LRCtdJ4U+vlgd/gk5y3KMXoodrsrw=
+github.com/valllabh/ocsf-schema-golang v1.0.3/go.mod h1:sZ3as9xqm1SSK5feFWIR2CuGeGRhsM7TR1MbpBctzPk=
 github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
 github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA=
-golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
-golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
+golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
+golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
-golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
-golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
+golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
 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=
+google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
+google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=