Repository: corazawaf/coraza
Pull request #519 (2022-11-30 11:10:02)

Corrects SecDebugLogLevel doc, reorders logs levels
Hello! I'm trying to reconcile the log-level implementation with the information provided inside `coraza.conf-recommended`.
This is the summary of the current situation ([ref](https://github.com/corazawaf/coraza/issues/252#issuecomment-1263777437)):
| [coraza.conf-recommended](https://github.com/corazawaf/coraza/blob/5408fcdf553217ea45ec9abda978e3fa593e663b/coraza.conf-recommended#L206-L217)  | Documentation | [Current implementation](https://github.com/corazawaf/coraza/blob/fce26f25ab3e1955de7b396d01d6b43dc8c33b7b/loggers/debug.go#L33) (v3/dev) |
| ------------- | ------------- | ------------- |
| 1: fatal<br>2: panic<br>3: error<br>4: warn<br>5: info<br>6: debug | 0: Fatal<br>1: Panic<br>2: Error<br>3: Warning<br>4: details of how transactions are handled<br>5: log everything |0: LogLevelUnknown<br>1: LogLevelInfo<br>2: LogLevelWarn<br>3: LogLevelError<br>4: LogLevelDebug<br>5: LogLevelTrace  |

The PR:
- Reorders the log levels from most verbose to least one in the following way: trace -> debug -> info -> warning -> error
- Updates the recommended file showing only these five valid values. 

I'm wondering:
- What was the difference between `Fatal`, `Panic` and `Error`? Zap heritage?
- Do we want to provide a ~~`LogLevelFatal`~~ `LogLevelNoLog` that basically is a no-log level? (it would be at `0`).
- What is the reasoning behind `info` log level? The implementation considers it the least verbose level but `recommended` shows it as one of the most verbose ones. The PR places it in an intermediate verbosity, like ModSec behaviour. 

Closes https://github.com/corazawaf/coraza/issues/252.

fzipi: I don't think naming `LogLevelFatal` something that doesn't log makes any sense 🤔 At the bare minimum, it's confusing.
jcchavezs: Naive question: shall we follow modsec levels https://www.feistyduck.com/library/modsecurity-handbook-free/online/ch04-logging.html?

0 | No log | No log
1 | Errors (e.g., fatal processing errors, blocked transactions) | Fatal
2 | Warnings (e.g., non-blocking rule matches) | Warn 
3 | Notices (e.g., non-fatal processing errors) | Error
4 | Informational | info
5 | Detailed | Debug
9 | Everything! | Trace

?
jptosso: What are we going to do about this?

I think we should keep apache error levels for compatibility, but fatal to error should default to error
codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.52**% // Head: **77.42**% // Decreases project coverage by **`-0.10%`** :warning:
> Coverage data is based on head [(`7074eaa`)](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`cc0c325`)](https://codecov.io/gh/corazawaf/coraza/commit/cc0c32509a39b0354a43fbca528058445fbcd33e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 100.00% of modified lines in pull request are covered.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           v3/dev     #519      +/-   ##
==========================================
- Coverage   77.52%   77.42%   -0.10%     
==========================================
  Files         144      144              
  Lines        7474     7474              
==========================================
- Hits         5794     5787       -7     
- Misses       1448     1454       +6     
- Partials      232      233       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `72.96% <100.00%> (+0.02%)` | :arrow_up: |
| examples | `28.40% <60.00%> (ø)` | |
| ftw | `54.72% <0.00%> (ø)` | |
| ftw-multiphase | `?` | |
| tinygo | `71.07% <60.00%> (+0.02%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [loggers/debug.go](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-bG9nZ2Vycy9kZWJ1Zy5nbw==) | `93.33% <100.00%> (ø)` | |
| [internal/corazawaf/rule.go](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3J1bGUuZ28=) | `67.47% <0.00%> (-1.42%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/corazawaf/coraza/pull/519?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).

jcchavezs: @M4tteoP is going to come up with another alternative.
M4tteoP: According to the docs, ModSecurity levels for both v2 and v3 are the following:
```
0: no logging
1: errors (intercepted requests) only
2: warnings
3: notices
4: details of how transactions are handled
5: as above, but including information about each piece of information handled
9: log everything, including very detailed debugging information
```
Therefore, I came up with the following correspondence in Coraza:
```
0: no logging
1: error
2: warnings
3: info
4-5: debug
6-9: trace
```
The last commit:
- Adds the `NoLogLevel`
- Sets "official" Coraza log levels to the lower correct number, therefore no changes on loggers implementations are needed. (E.g. Debug is set with `5`, it will be `>` than `DebugLogLevel` (`4`), therefore it will be printed.

Let me know if it seems right, or if you were expecting another logic. 

Patch:
diff --git a/actions/setvar.go b/actions/setvar.go
index 5f68e51..816b027 100644
--- a/actions/setvar.go
+++ b/actions/setvar.go
@@ -68,7 +68,7 @@ func (a *setvarFn) Type() rules.ActionType {
 }
 
 func (a *setvarFn) evaluateTxCollection(r rules.RuleMetadata, tx rules.TransactionState, key string, value string) {
-	col := (tx.Collection(a.collection)).(*collection.Map)
+	col := (tx.Collection(a.collection)).(collection.Map)
 	if col == nil {
 		// fmt.Println("Invalid Collection " + a.Collection) LOG error?
 		return
diff --git a/collection/collection_map.go b/collection/collection_map.go
index 4319335..13d2b92 100644
--- a/collection/collection_map.go
+++ b/collection/collection_map.go
@@ -5,6 +5,7 @@ package collection
 
 import (
 	"regexp"
+	"strings"
 
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	"github.com/corazawaf/coraza/v3/types"
@@ -15,23 +16,68 @@ import (
 // for transactions, this data structured is designed
 // to store slices of data for keys
 // Important: CollectionMaps ARE NOT concurrent safe
-type Map struct {
+type Map interface {
+	// Get returns a slice of strings for a key
+	Get(key string) []string
+
+	// FindRegex returns a slice of MatchData for the regex
+	FindRegex(key *regexp.Regexp) []types.MatchData
+
+	// FindString returns a slice of MatchData for the string
+	FindString(key string) []types.MatchData
+
+	// FindAll returns all the contained elements
+	FindAll() []types.MatchData
+
+	// Add a value to some key
+	Add(key string, value string)
+
+	// Set will replace the key's value with this slice
+	Set(key string, values []string)
+
+	// SetIndex will place the value under the index
+	// If the index is higher than the current size of the CollectionMap
+	// it will be appended
+	SetIndex(key string, index int, value string)
+
+	// Remove deletes the key from the CollectionMap
+	Remove(key string)
+
+	// Name returns the name for the current CollectionMap
+	Name() string
+
+	// Reset the current CollectionMap
+	Reset()
+
+	// Data returns all the data in the CollectionMap
+	Data() map[string][]string
+}
+
+// Map are used to store VARIABLE data
+// for transactions, this data structured is designed
+// to store slices of data for keys
+// Important: CollectionMaps ARE NOT concurrent safe
+type defaultMap struct {
 	data     map[string][]types.AnchoredVar
 	name     string
 	variable variables.RuleVariable
 }
 
 // Get returns a slice of strings for a key
-func (c *Map) Get(key string) []string {
+func (c *defaultMap) Get(key string) []string {
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
 	var values []string
-	for _, a := range c.data[key] {
+	for _, a := range c.data[keyL] {
 		values = append(values, a.Value)
 	}
 	return values
 }
 
 // FindRegex returns a slice of MatchData for the regex
-func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
+func (c *defaultMap) FindRegex(key *regexp.Regexp) []types.MatchData {
 	var result []types.MatchData
 	for k, data := range c.data {
 		if key.MatchString(k) {
@@ -49,13 +95,17 @@ func (c *Map) FindRegex(key *regexp.Regexp) []types.MatchData {
 }
 
 // FindString returns a slice of MatchData for the string
-func (c *Map) FindString(key string) []types.MatchData {
+func (c *defaultMap) FindString(key string) []types.MatchData {
 	var result []types.MatchData
 	if key == "" {
 		return c.FindAll()
 	}
+	if len(c.data) == 0 {
+		return nil
+	}
+	keyL := strings.ToLower(key)
 	// if key is not empty
-	if e, ok := c.data[key]; ok {
+	if e, ok := c.data[keyL]; ok {
 		for _, aVar := range e {
 			result = append(result, &corazarules.MatchData{
 				VariableName_: c.name,
@@ -69,7 +119,7 @@ func (c *Map) FindString(key string) []types.MatchData {
 }
 
 // FindAll returns all the contained elements
-func (c *Map) FindAll() []types.MatchData {
+func (c *defaultMap) FindAll() []types.MatchData {
 	var result []types.MatchData
 	for _, data := range c.data {
 		for _, d := range data {
@@ -84,114 +134,64 @@ func (c *Map) FindAll() []types.MatchData {
 	return result
 }
 
-func (c *Map) keysRx(rx *regexp.Regexp) []string {
-	var keys []string
-	for k := range c.data {
-		if rx.MatchString(k) {
-			keys = append(keys, k)
-		}
-	}
-	return keys
-}
-
-func (c *Map) keys() []string {
-	var keys []string
-	for k := range c.data {
-		keys = append(keys, k)
-	}
-	return keys
-}
-
-// AddCS a value to some key with case sensitive vKey
-func (c *Map) AddCS(key string, vKey string, vVal string) {
-	aVal := types.AnchoredVar{Name: vKey, Value: vVal}
-	c.data[key] = append(c.data[key], aVal)
-}
-
 // Add a value to some key
-func (c *Map) Add(key string, value string) {
-	c.AddCS(key, key, value)
-}
-
-// AddUniqueCS will add a value to a key if it is not already there
-// with case sensitive vKey
-func (c *Map) AddUniqueCS(key string, vKey string, vVal string) {
-	if c.data[key] == nil {
-		c.AddCS(key, vKey, vVal)
-		return
-	}
-
-	for _, a := range c.data[key] {
-		if a.Value == vVal {
-			return
-		}
-	}
-	c.AddCS(key, vKey, vVal)
-}
-
-// AddUnique will add a value to a key if it is not already there
-func (c *Map) AddUnique(key string, value string) {
-	c.AddUniqueCS(key, key, value)
-}
-
-// SetCS will replace the key's value with this slice
-// internally converts [] string to []types.AnchoredVar
-// with case sensitive vKey
-func (c *Map) SetCS(key string, vKey string, values []string) {
-	c.data[key] = make([]types.AnchoredVar, 0, len(values))
-	for _, v := range values {
-		c.data[key] = append(c.data[key], types.AnchoredVar{Name: vKey, Value: v})
-	}
+func (c *defaultMap) Add(key string, value string) {
+	keyL := strings.ToLower(key)
+	aVal := types.AnchoredVar{Name: key, Value: value}
+	c.data[keyL] = append(c.data[keyL], aVal)
 }
 
 // Set will replace the key's value with this slice
 // internally converts [] string to []types.AnchoredVar
-func (c *Map) Set(key string, values []string) {
-	c.SetCS(key, key, values)
-}
-
-// SetIndexCS will place the value under the index
-// If the index is higher than the current size of the CollectionMap
-// it will be appended
-// with case sensitive vKey
-func (c *Map) SetIndexCS(key string, index int, vKey string, value string) {
-	if c.data[key] == nil {
-		c.data[key] = []types.AnchoredVar{{Name: vKey, Value: value}}
-	}
-	vVal := types.AnchoredVar{Name: vKey, Value: value}
-	if len(c.data[key]) <= index {
-		c.data[key] = append(c.data[key], vVal)
-		return
+func (c *defaultMap) Set(key string, values []string) {
+	keyL := strings.ToLower(key)
+	c.data[keyL] = make([]types.AnchoredVar, 0, len(values))
+	for _, v := range values {
+		c.data[keyL] = append(c.data[keyL], types.AnchoredVar{Name: key, Value: v})
 	}
-	c.data[key][index] = vVal
 }
 
 // SetIndex will place the value under the index
 // If the index is higher than the current size of the CollectionMap
 // it will be appended
-func (c *Map) SetIndex(key string, index int, value string) {
-	c.SetIndexCS(key, index, key, value)
+func (c *defaultMap) SetIndex(key string, index int, value string) {
+	keyL := strings.ToLower(key)
+	values := c.data[keyL]
+	av := types.AnchoredVar{Name: key, Value: value}
+
+	switch {
+	case len(values) == 0:
+		c.data[keyL] = []types.AnchoredVar{av}
+	case len(values) <= index:
+		c.data[keyL] = append(c.data[keyL], av)
+	default:
+		c.data[keyL][index] = av
+	}
 }
 
 // Remove deletes the key from the CollectionMap
-func (c *Map) Remove(key string) {
-	delete(c.data, key)
+func (c *defaultMap) Remove(key string) {
+	if len(c.data) == 0 {
+		return
+	}
+	keyL := strings.ToLower(key)
+	delete(c.data, keyL)
 }
 
 // Name returns the name for the current CollectionMap
-func (c *Map) Name() string {
+func (c *defaultMap) Name() string {
 	return c.name
 }
 
 // Reset the current CollectionMap
-func (c *Map) Reset() {
+func (c *defaultMap) Reset() {
 	for k := range c.data {
 		delete(c.data, k)
 	}
 }
 
 // Data returns all the data in the CollectionMap
-func (c *Map) Data() map[string][]string {
+func (c *defaultMap) Data() map[string][]string {
 	result := map[string][]string{}
 	for k, v := range c.data {
 		result[k] = make([]string, 0, len(v))
@@ -202,11 +202,11 @@ func (c *Map) Data() map[string][]string {
 	return result
 }
 
-var _ Collection = &Map{}
+var _ Collection = &defaultMap{}
 
 // NewMap returns a collection of key->[]values
-func NewMap(variable variables.RuleVariable) *Map {
-	return &Map{
+func NewMap(variable variables.RuleVariable) Map {
+	return &defaultMap{
 		name:     variable.Name(),
 		variable: variable,
 		data:     map[string][]types.AnchoredVar{},
diff --git a/collection/collection_proxy.go b/collection/collection_proxy.go
deleted file mode 100644
index 078bf20..0000000
--- a/collection/collection_proxy.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// Proxy are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionProxys ARE NOT concurrent safe
-type Proxy struct {
-	data     []*Map
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *Proxy) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindRegex(key)...)
-	}
-	return res
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *Proxy) FindString(key string) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindString(key)...)
-	}
-	return res
-}
-
-// FindAll returns all matches for all collections
-func (c *Proxy) FindAll() []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		res = append(res, c.FindAll()...)
-	}
-	return res
-}
-
-// Name returns the name for the current CollectionProxy
-func (c *Proxy) Name() string {
-	return c.name
-}
-
-// Get returns the data for the key
-func (c *Proxy) Get(key string) []string {
-	var res []string
-	for _, c := range c.data {
-		res = append(res, c.Get(key)...)
-	}
-	return res
-}
-
-// Data returns merged data from all CollectionMap
-func (c *Proxy) Data() map[string][]string {
-	res := map[string][]string{}
-	for _, c := range c.data {
-		for k, v := range c.Data() {
-			res[k] = append(res[k], v...)
-		}
-	}
-	return res
-}
-
-// Reset the current CollectionProxy
-func (c *Proxy) Reset() {
-}
-
-var _ Collection = &Proxy{}
-
-// NewProxy returns a Proxy collection that merges all collections
-func NewProxy(variable variables.RuleVariable, data ...*Map) *Proxy {
-	return &Proxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
-	}
-}
diff --git a/collection/collection_translation_proxy.go b/collection/collection_translation_proxy.go
deleted file mode 100644
index 2f0485c..0000000
--- a/collection/collection_translation_proxy.go
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package collection
-
-import (
-	"regexp"
-
-	"github.com/corazawaf/coraza/v3/internal/corazarules"
-	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-// TranslationProxy are used to store VARIABLE data
-// for transactions, this data structured is designed
-// to store slices of data for keys
-// Important: CollectionTranslationProxys ARE NOT concurrent safe
-type TranslationProxy struct {
-	data     []*Map
-	name     string
-	variable variables.RuleVariable
-}
-
-// FindRegex returns a slice of MatchData for the regex
-func (c *TranslationProxy) FindRegex(key *regexp.Regexp) []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		for _, k := range c.keysRx(key) {
-			res = append(res, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Value_:        k,
-			})
-		}
-	}
-	return res
-}
-
-// FindString returns a slice of MatchData for the string
-func (c *TranslationProxy) FindString(key string) []types.MatchData {
-	for _, c := range c.data {
-		if len(c.Get(key)) > 0 {
-			return []types.MatchData{
-				&corazarules.MatchData{
-					VariableName_: c.name,
-					Variable_:     c.variable,
-					Value_:        key,
-				},
-			}
-		}
-	}
-	return nil
-}
-
-// FindAll returns all keys from Proxy Collections
-func (c *TranslationProxy) FindAll() []types.MatchData {
-	var res []types.MatchData
-	for _, c := range c.data {
-		for _, k := range c.keys() {
-			res = append(res, &corazarules.MatchData{
-				VariableName_: c.name,
-				Variable_:     c.variable,
-				Value_:        k,
-			})
-		}
-	}
-	return res
-}
-
-// Data returns the keys of all Proxy collections
-func (c *TranslationProxy) Data() []string {
-	var res []string
-	for _, c := range c.data {
-		res = append(res, c.keys()...)
-	}
-	return res
-}
-
-// Name returns the name for the current CollectionTranslationProxy
-func (c *TranslationProxy) Name() string {
-	return c.name
-}
-
-// Reset the current CollectionTranslationProxy
-func (c *TranslationProxy) Reset() {
-	// do nothing
-}
-
-// Get the value for the index
-func (c *TranslationProxy) Get(index int) string {
-	if index < len(c.data) {
-		if v := c.data[index].Get(""); len(v) > 0 {
-			return v[0]
-		}
-	}
-	return ""
-}
-
-var _ Collection = &TranslationProxy{}
-
-// NewTranslationProxy creates a translation proxy
-// Translation proxies are used to merge variable keys from multiple collections
-func NewTranslationProxy(variable variables.RuleVariable, data ...*Map) *TranslationProxy {
-	return &TranslationProxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
-	}
-}
diff --git a/coraza.conf-recommended b/coraza.conf-recommended
index 128332c..6d9d957 100644
--- a/coraza.conf-recommended
+++ b/coraza.conf-recommended
@@ -205,12 +205,12 @@ SecDataDir /tmp/
 
 # Default debug log path
 # Debug levels:
-# 1: fatal
-# 2: panic
-# 3: error
-# 4: warn
-# 5: info
-# 6: debug
+# 0: no logging (least verbose)
+# 1: error
+# 2: warn
+# 3: info
+# 4-5: debug
+# 6-9: trace (most verbose)
 # Most logging has not been implemented because it will be replaced with
 # advanced rule profiling options
 #SecDebugLog /opt/coraza/var/log/debug.log
diff --git a/examples/http-server/default.conf b/examples/http-server/default.conf
index 9b4c6b5..7f3edc5 100644
--- a/examples/http-server/default.conf
+++ b/examples/http-server/default.conf
@@ -1,4 +1,4 @@
-SecDebugLogLevel 5
+SecDebugLogLevel 9
 SecDebugLog /dev/stdout
 
 SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
diff --git a/internal/collections/concat.go b/internal/collections/concat.go
new file mode 100644
index 0000000..cb48e80
--- /dev/null
+++ b/internal/collections/concat.go
@@ -0,0 +1,64 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+type ConcatCollection struct {
+	data     []collection.Collection
+	name     string
+	variable variables.RuleVariable
+}
+
+var _ collection.Collection = &ConcatCollection{}
+
+func NewConcatCollection(variable variables.RuleVariable, data ...collection.Collection) *ConcatCollection {
+	return &ConcatCollection{
+		data:     data,
+		name:     variable.Name(),
+		variable: variable,
+	}
+}
+
+// FindRegex returns a slice of MatchData for the regex
+func (c *ConcatCollection) FindRegex(key *regexp.Regexp) []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindRegex(key)...)
+	}
+	return res
+}
+
+// FindString returns a slice of MatchData for the string
+func (c *ConcatCollection) FindString(key string) []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindString(key)...)
+	}
+	return res
+}
+
+// FindAll returns all matches for all collections
+func (c *ConcatCollection) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, c := range c.data {
+		res = append(res, c.FindAll()...)
+	}
+	return res
+}
+
+// Name returns the name for the current CollectionconcatCollection
+func (c *ConcatCollection) Name() string {
+	return c.name
+}
+
+// Reset the current ConcatCollection
+func (c *ConcatCollection) Reset() {
+}
diff --git a/internal/collections/named.go b/internal/collections/named.go
new file mode 100644
index 0000000..d1ff88d
--- /dev/null
+++ b/internal/collections/named.go
@@ -0,0 +1,122 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/corazarules"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// NamedCollection is a Collection that also keeps track of unique names.
+type NamedCollection struct {
+	collection.Map
+	names []string
+}
+
+var _ collection.Map = &NamedCollection{}
+
+func NewNamedCollection(rv variables.RuleVariable) *NamedCollection {
+	return &NamedCollection{
+		Map: collection.NewMap(rv),
+	}
+}
+
+// Add a value to some key
+func (c *NamedCollection) Add(key string, value string) {
+	c.Map.Add(key, value)
+	c.addName(key)
+}
+
+// Set will replace the key's value with this slice
+func (c *NamedCollection) Set(key string, values []string) {
+	c.Map.Set(key, values)
+	c.addName(key)
+}
+
+// SetIndex will place the value under the index
+// If the index is higher than the current size of the CollectionMap
+// it will be appended
+func (c *NamedCollection) SetIndex(key string, index int, value string) {
+	c.Map.SetIndex(key, index, value)
+	c.addName(key)
+}
+
+// Remove deletes the key from the CollectionMap
+func (c *NamedCollection) Remove(key string) {
+	c.Map.Remove(key)
+	for i, n := range c.names {
+		if n == key {
+			c.names = append(c.names[:i], c.names[i+1:]...)
+			return
+		}
+	}
+}
+
+// Name returns the name for the current CollectionMap
+func (c *NamedCollection) Name() string {
+	return c.Map.Name()
+}
+
+func (c *NamedCollection) Reset() {
+	c.Map.Reset()
+	c.names = nil
+}
+
+func (c *NamedCollection) Data() map[string][]string {
+	return c.Map.Data()
+}
+
+func (c *NamedCollection) Names(rv variables.RuleVariable) collection.Collection {
+	return &namedCollectionNames{
+		name:       rv.Name(),
+		variable:   rv,
+		collection: c,
+	}
+}
+
+func (c *NamedCollection) addName(key string) {
+	for _, n := range c.names {
+		if n == key {
+			return
+		}
+	}
+	c.names = append(c.names, key)
+}
+
+type namedCollectionNames struct {
+	name       string
+	variable   variables.RuleVariable
+	collection *NamedCollection
+}
+
+func (c *namedCollectionNames) FindRegex(key *regexp.Regexp) []types.MatchData {
+	panic("selection operator not supported")
+}
+
+func (c *namedCollectionNames) FindString(key string) []types.MatchData {
+	panic("selection operator not supported")
+}
+
+func (c *namedCollectionNames) FindAll() []types.MatchData {
+	var res []types.MatchData
+	for _, k := range c.collection.names {
+		res = append(res, &corazarules.MatchData{
+			VariableName_: c.name,
+			Variable_:     c.variable,
+			Value_:        k,
+		})
+	}
+	return res
+}
+
+func (c *namedCollectionNames) Name() string {
+	return c.name
+}
+
+func (c *namedCollectionNames) Reset() {
+}
diff --git a/collection/collection_size_proxy.go b/internal/collections/sized.go
similarity index 56%
rename from collection/collection_size_proxy.go
rename to internal/collections/sized.go
index 3cb4128..82e8206 100644
--- a/collection/collection_size_proxy.go
+++ b/internal/collections/sized.go
@@ -1,78 +1,77 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package collection
+package collections
 
 import (
 	"regexp"
 	"strconv"
 
+	"github.com/corazawaf/coraza/v3/collection"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-// SizeProxy are used to connect the size
-// of many collection map values and return the sum
-type SizeProxy struct {
-	data     []*Map
+type SizeCollection struct {
+	data     []*NamedCollection
 	name     string
 	variable variables.RuleVariable
 }
 
+var _ collection.Collection = &SizeCollection{}
+
+// NewSizeCollection returns a collection that
+// only returns the total sum of all the collections values
+func NewSizeCollection(variable variables.RuleVariable, data ...*NamedCollection) *SizeCollection {
+	return &SizeCollection{
+		name:     variable.Name(),
+		variable: variable,
+		data:     data,
+	}
+}
+
 // FindRegex returns a slice of MatchData for the regex
-func (c *SizeProxy) FindRegex(*regexp.Regexp) []types.MatchData {
+func (c *SizeCollection) FindRegex(*regexp.Regexp) []types.MatchData {
 	return c.FindAll()
 }
 
 // FindString returns a slice of MatchData for the string
-func (c *SizeProxy) FindString(string) []types.MatchData {
+func (c *SizeCollection) FindString(string) []types.MatchData {
 	return c.FindAll()
 }
 
 // FindAll returns a slice of MatchData of all matches
-func (c *SizeProxy) FindAll() []types.MatchData {
+func (c *SizeCollection) FindAll() []types.MatchData {
 	return []types.MatchData{
 		&corazarules.MatchData{
 			VariableName_: c.name,
 			Variable_:     c.variable,
-			Value_:        strconv.FormatInt(c.Size(), 10),
+			Value_:        strconv.Itoa(c.size()),
 		},
 	}
 }
 
-// Size returns the size of all the collections values
-func (c *SizeProxy) Size() int64 {
-	i := 0
-	for _, d := range c.data {
-		// we iterate over d
-		for _, data := range d.data {
-			for _, v := range data {
-				i += len(v.Value)
-			}
-		}
-	}
-	return int64(i)
-}
-
 // Name returns the name for the current CollectionSizeProxy
-func (c *SizeProxy) Name() string {
+func (c *SizeCollection) Name() string {
 	return c.name
 }
 
 // Reset the current CollectionSizeProxy
-func (c *SizeProxy) Reset() {
+func (c *SizeCollection) Reset() {
 	// do nothing
 }
 
-var _ Collection = &SizeProxy{}
-
-// NewCollectionSizeProxy returns a collection that
-// only returns the total sum of all the collections values
-func NewCollectionSizeProxy(variable variables.RuleVariable, data ...*Map) *SizeProxy {
-	return &SizeProxy{
-		name:     variable.Name(),
-		variable: variable,
-		data:     data,
+// Size returns the size of all the collections values
+func (c *SizeCollection) size() int {
+	i := 0
+	for _, d := range c.data {
+		// we iterate over d
+		for _, data := range d.Data() {
+			for _, v := range data {
+				i += len(v)
+			}
+		}
 	}
+	return i
 }
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 8443ea5..adf76da 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -18,6 +18,7 @@ import (
 
 	"github.com/corazawaf/coraza/v3/bodyprocessors"
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/internal/collections"
 	"github.com/corazawaf/coraza/v3/internal/corazarules"
 	stringsutil "github.com/corazawaf/coraza/v3/internal/strings"
 	urlutil "github.com/corazawaf/coraza/v3/internal/url"
@@ -337,8 +338,7 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
 		return
 	}
 	keyl := strings.ToLower(key)
-	tx.variables.requestHeadersNames.AddUniqueCS(keyl, key, keyl)
-	tx.variables.requestHeaders.AddCS(keyl, key, value)
+	tx.variables.requestHeaders.Add(key, value)
 
 	switch keyl {
 	case "content-type":
@@ -352,10 +352,8 @@ func (tx *Transaction) AddRequestHeader(key string, value string) {
 		// Cookies use the same syntax as GET params but with semicolon (;) separator
 		values := urlutil.ParseQuery(value, ';')
 		for k, vr := range values {
-			kl := strings.ToLower(k)
-			tx.variables.requestCookiesNames.AddUniqueCS(kl, k, kl)
 			for _, v := range vr {
-				tx.variables.requestCookies.AddCS(kl, k, v)
+				tx.variables.requestCookies.Add(k, v)
 			}
 		}
 	}
@@ -369,8 +367,7 @@ func (tx *Transaction) AddResponseHeader(key string, value string) {
 		return
 	}
 	keyl := strings.ToLower(key)
-	tx.variables.responseHeadersNames.AddUniqueCS(keyl, key, keyl)
-	tx.variables.responseHeaders.AddCS(keyl, key, value)
+	tx.variables.responseHeaders.Add(key, value)
 
 	// Most headers can be managed like that
 	if keyl == "content-type" {
@@ -472,30 +469,21 @@ func (tx *Transaction) ParseRequestReader(data io.Reader) (*types.Interruption,
 // matchVariable Creates the MATCHED_ variables required by chains and macro expansion
 // MATCHED_VARS, MATCHED_VAR, MATCHED_VAR_NAME, MATCHED_VARS_NAMES
 func (tx *Transaction) matchVariable(match *corazarules.MatchData) {
-	var varName, varNamel string
+	var varName string
 	if match.Key_ != "" {
 		varName = match.VariableName_ + ":" + match.Key_
-		varNamel = match.VariableName_ + ":" + strings.ToLower(match.Key_)
 	} else {
 		varName = match.VariableName_
-		varNamel = match.VariableName_
 	}
 	// Array of values
 	matchedVars := tx.variables.matchedVars
 	// Last key
 	matchedVarName := tx.variables.matchedVarName
 	matchedVarName.Reset()
-	// Array of keys
-	matchedVarsNames := tx.variables.matchedVarsNames
 
-	// We add the key in lowercase for ease of lookup in chains
-	// This is similar to args handling
-	matchedVars.AddCS(varNamel, varName, match.Value_)
+	matchedVars.Add(varName, match.Value_)
 	tx.variables.matchedVar.Set(match.Value_)
 
-	// We add the key in lowercase for ease of lookup in chains
-	// This is similar to args handling
-	matchedVarsNames.AddCS(varNamel, varName, varName)
 	matchedVarName.Set(varName)
 }
 
@@ -666,7 +654,7 @@ func (tx *Transaction) ExtractArguments(orig types.ArgumentType, uri string) {
 // ARGS_(GET|POST)_NAMES
 func (tx *Transaction) AddArgument(argType types.ArgumentType, key string, value string) {
 	// TODO implement ARGS value limit using ArgumentsLimit
-	var vals *collection.Map
+	var vals collection.Map
 	switch argType {
 	case types.ArgumentGET:
 		vals = tx.variables.argsGet
@@ -677,9 +665,8 @@ func (tx *Transaction) AddArgument(argType types.ArgumentType, key string, value
 	default:
 		return
 	}
-	keyl := strings.ToLower(key)
 
-	vals.AddCS(keyl, key, value)
+	vals.Add(key, value)
 }
 
 // ProcessURI Performs the analysis on the URI and all the query string variables.
@@ -1428,12 +1415,8 @@ func (tx *Transaction) Debug() string {
 			data[""] = []string{
 				col.String(),
 			}
-		case *collection.Map:
-			data = col.Data()
-		case *collection.Proxy:
+		case collection.Map:
 			data = col.Data()
-		case *collection.TranslationProxy:
-			data[""] = col.Data()
 		}
 
 		if len(data) == 1 {
@@ -1468,7 +1451,7 @@ type TransactionVariables struct {
 	urlencodedError               *collection.Simple
 	responseContentType           *collection.Simple
 	uniqueID                      *collection.Simple
-	argsCombinedSize              *collection.SizeProxy
+	argsCombinedSize              *collections.SizeCollection
 	authType                      *collection.Simple
 	filesCombinedSize             *collection.Simple
 	fullRequest                   *collection.Simple
@@ -1522,42 +1505,39 @@ type TransactionVariables struct {
 	statusLine                    *collection.Simple
 	inboundErrorData              *collection.Simple
 	// Custom
-	env      *collection.Map
-	tx       *collection.Map
-	rule     *collection.Map
-	duration *collection.Simple
-	// Proxy Variables
-	args *collection.Proxy
-	// Maps Variables
-	argsGet              *collection.Map
-	argsPost             *collection.Map
-	argsPath             *collection.Map
-	filesTmpNames        *collection.Map
-	geo                  *collection.Map
-	files                *collection.Map
-	requestCookies       *collection.Map
-	requestHeaders       *collection.Map
-	responseHeaders      *collection.Map
-	multipartName        *collection.Map
-	matchedVarsNames     *collection.Map
-	multipartFilename    *collection.Map
-	matchedVars          *collection.Map
-	filesSizes           *collection.Map
-	filesNames           *collection.Map
-	filesTmpContent      *collection.Map
-	responseHeadersNames *collection.Map
-	requestHeadersNames  *collection.Map
-	requestCookiesNames  *collection.Map
-	xml                  *collection.Map
-	requestXML           *collection.Map
-	responseXML          *collection.Map
-	multipartPartHeaders *collection.Map
+	env                  collection.Map
+	tx                   collection.Map
+	rule                 collection.Map
+	duration             *collection.Simple
+	args                 *collections.ConcatCollection
+	argsGet              *collections.NamedCollection
+	argsGetNames         collection.Collection
+	argsPost             *collections.NamedCollection
+	argsPostNames        collection.Collection
+	argsPath             *collections.NamedCollection
+	argsNames            *collections.ConcatCollection
+	filesTmpNames        collection.Map
+	geo                  collection.Map
+	files                collection.Map
+	requestCookies       *collections.NamedCollection
+	requestCookiesNames  collection.Collection
+	requestHeaders       *collections.NamedCollection
+	responseHeadersNames collection.Collection
+	responseHeaders      *collections.NamedCollection
+	requestHeadersNames  collection.Collection
+	multipartName        collection.Map
+	multipartFilename    collection.Map
+	matchedVars          *collections.NamedCollection
+	matchedVarsNames     collection.Collection
+	filesSizes           collection.Map
+	filesNames           collection.Map
+	filesTmpContent      collection.Map
+	xml                  collection.Map
+	requestXML           collection.Map
+	responseXML          collection.Map
+	multipartPartHeaders collection.Map
 	// Persistent variables
-	ip *collection.Map
-	// Translation Proxy Variables
-	argsNames     *collection.TranslationProxy
-	argsGetNames  *collection.TranslationProxy
-	argsPostNames *collection.TranslationProxy
+	ip collection.Map
 }
 
 func NewTransactionVariables() *TransactionVariables {
@@ -1618,59 +1598,53 @@ func NewTransactionVariables() *TransactionVariables {
 	v.statusLine = collection.NewSimple(variables.StatusLine)
 	v.inboundErrorData = collection.NewSimple(variables.InboundErrorData)
 	v.duration = collection.NewSimple(variables.Duration)
-	v.responseHeadersNames = collection.NewMap(variables.ResponseHeadersNames)
-	v.requestHeadersNames = collection.NewMap(variables.RequestHeadersNames)
 	v.userID = collection.NewSimple(variables.Userid)
 
-	v.argsGet = collection.NewMap(variables.ArgsGet)
-	v.argsPost = collection.NewMap(variables.ArgsPost)
-	v.argsPath = collection.NewMap(variables.ArgsPath)
 	v.filesSizes = collection.NewMap(variables.FilesSizes)
 	v.filesTmpContent = collection.NewMap(variables.FilesTmpContent)
 	v.multipartFilename = collection.NewMap(variables.MultipartFilename)
 	v.multipartName = collection.NewMap(variables.MultipartName)
-	v.matchedVars = collection.NewMap(variables.MatchedVars)
-	v.requestCookies = collection.NewMap(variables.RequestCookies)
-	v.requestHeaders = collection.NewMap(variables.RequestHeaders)
-	v.responseHeaders = collection.NewMap(variables.ResponseHeaders)
+	v.matchedVars = collections.NewNamedCollection(variables.MatchedVars)
+	v.matchedVarsNames = v.matchedVars.Names(variables.MatchedVarsNames)
+	v.requestCookies = collections.NewNamedCollection(variables.RequestCookies)
+	v.requestCookiesNames = v.requestCookies.Names(variables.RequestCookiesNames)
+	v.requestHeaders = collections.NewNamedCollection(variables.RequestHeaders)
+	v.requestHeadersNames = v.requestHeaders.Names(variables.RequestHeadersNames)
+	v.responseHeaders = collections.NewNamedCollection(variables.ResponseHeaders)
+	v.responseHeadersNames = v.responseHeaders.Names(variables.ResponseHeadersNames)
 	v.geo = collection.NewMap(variables.Geo)
 	v.tx = collection.NewMap(variables.TX)
 	v.rule = collection.NewMap(variables.Rule)
 	v.env = collection.NewMap(variables.Env)
 	v.ip = collection.NewMap(variables.IP)
 	v.files = collection.NewMap(variables.Files)
-	v.matchedVarsNames = collection.NewMap(variables.MatchedVarsNames)
 	v.filesNames = collection.NewMap(variables.FilesNames)
 	v.filesTmpNames = collection.NewMap(variables.FilesTmpNames)
-	v.requestCookiesNames = collection.NewMap(variables.RequestCookiesNames)
 	v.responseXML = collection.NewMap(variables.ResponseXML)
 	v.requestXML = collection.NewMap(variables.RequestXML)
 	v.multipartPartHeaders = collection.NewMap(variables.MultipartPartHeaders)
 
-	v.argsCombinedSize = collection.NewCollectionSizeProxy(variables.ArgsCombinedSize, v.argsGet, v.argsPost)
-
 	// XML is a pointer to RequestXML
 	v.xml = v.requestXML
-	v.args = collection.NewProxy(
+
+	v.argsGet = collections.NewNamedCollection(variables.ArgsGet)
+	v.argsGetNames = v.argsGet.Names(variables.ArgsGetNames)
+	v.argsPost = collections.NewNamedCollection(variables.ArgsPost)
+	v.argsPostNames = v.argsPost.Names(variables.ArgsPostNames)
+	v.argsPath = collections.NewNamedCollection(variables.ArgsPath)
+	v.argsCombinedSize = collections.NewSizeCollection(variables.ArgsCombinedSize, v.argsGet, v.argsPost)
+	v.args = collections.NewConcatCollection(
 		variables.Args,
 		v.argsGet,
 		v.argsPost,
 		v.argsPath,
 	)
-
-	v.argsNames = collection.NewTranslationProxy(
+	v.argsNames = collections.NewConcatCollection(
 		variables.ArgsNames,
-		v.argsGet,
-		v.argsPost,
-		v.argsPath,
-	)
-	v.argsGetNames = collection.NewTranslationProxy(
-		variables.ArgsGetNames,
-		v.argsGet,
-	)
-	v.argsPostNames = collection.NewTranslationProxy(
-		variables.ArgsPostNames,
-		v.argsPost,
+		v.argsGetNames,
+		v.argsPostNames,
+		// Only used in a concatenating collection so variable name doesn't matter.
+		v.argsPath.Names(variables.Unknown),
 	)
 	return v
 }
@@ -1691,7 +1665,7 @@ func (v *TransactionVariables) UniqueID() *collection.Simple {
 	return v.uniqueID
 }
 
-func (v *TransactionVariables) ArgsCombinedSize() *collection.SizeProxy {
+func (v *TransactionVariables) ArgsCombinedSize() collection.Collection {
 	return v.argsCombinedSize
 }
 
@@ -1771,7 +1745,7 @@ func (v *TransactionVariables) MultipartMissingSemicolon() *collection.Simple {
 	return v.multipartMissingSemicolon
 }
 
-func (v *TransactionVariables) MultipartPartHeaders() *collection.Map {
+func (v *TransactionVariables) MultipartPartHeaders() collection.Map {
 	return v.multipartPartHeaders
 }
 
@@ -1907,15 +1881,15 @@ func (v *TransactionVariables) InboundErrorData() *collection.Simple {
 	return v.inboundErrorData
 }
 
-func (v *TransactionVariables) Env() *collection.Map {
+func (v *TransactionVariables) Env() collection.Map {
 	return v.env
 }
 
-func (v *TransactionVariables) TX() *collection.Map {
+func (v *TransactionVariables) TX() collection.Map {
 	return v.tx
 }
 
-func (v *TransactionVariables) Rule() *collection.Map {
+func (v *TransactionVariables) Rule() collection.Map {
 	return v.rule
 }
 
@@ -1923,111 +1897,111 @@ func (v *TransactionVariables) Duration() *collection.Simple {
 	return v.duration
 }
 
-func (v *TransactionVariables) Args() *collection.Proxy {
+func (v *TransactionVariables) Args() collection.Collection {
 	return v.args
 }
 
-func (v *TransactionVariables) ArgsGet() *collection.Map {
+func (v *TransactionVariables) ArgsGet() collection.Map {
 	return v.argsGet
 }
 
-func (v *TransactionVariables) ArgsPost() *collection.Map {
+func (v *TransactionVariables) ArgsPost() collection.Map {
 	return v.argsPost
 }
 
-func (v *TransactionVariables) ArgsPath() *collection.Map {
+func (v *TransactionVariables) ArgsPath() collection.Map {
 	return v.argsPath
 }
 
-func (v *TransactionVariables) FilesTmpNames() *collection.Map {
+func (v *TransactionVariables) FilesTmpNames() collection.Map {
 	return v.filesTmpNames
 }
 
-func (v *TransactionVariables) Geo() *collection.Map {
+func (v *TransactionVariables) Geo() collection.Map {
 	return v.geo
 }
 
-func (v *TransactionVariables) Files() *collection.Map {
+func (v *TransactionVariables) Files() collection.Map {
 	return v.files
 }
 
-func (v *TransactionVariables) RequestCookies() *collection.Map {
+func (v *TransactionVariables) RequestCookies() collection.Map {
 	return v.requestCookies
 }
 
-func (v *TransactionVariables) RequestHeaders() *collection.Map {
+func (v *TransactionVariables) RequestHeaders() collection.Map {
 	return v.requestHeaders
 }
 
-func (v *TransactionVariables) ResponseHeaders() *collection.Map {
+func (v *TransactionVariables) ResponseHeaders() collection.Map {
 	return v.responseHeaders
 }
 
-func (v *TransactionVariables) MultipartName() *collection.Map {
+func (v *TransactionVariables) MultipartName() collection.Map {
 	return v.multipartName
 }
 
-func (v *TransactionVariables) MatchedVarsNames() *collection.Map {
+func (v *TransactionVariables) MatchedVarsNames() collection.Collection {
 	return v.matchedVarsNames
 }
 
-func (v *TransactionVariables) MultipartFilename() *collection.Map {
+func (v *TransactionVariables) MultipartFilename() collection.Map {
 	return v.multipartFilename
 }
 
-func (v *TransactionVariables) MatchedVars() *collection.Map {
+func (v *TransactionVariables) MatchedVars() collection.Map {
 	return v.matchedVars
 }
 
-func (v *TransactionVariables) FilesSizes() *collection.Map {
+func (v *TransactionVariables) FilesSizes() collection.Map {
 	return v.filesSizes
 }
 
-func (v *TransactionVariables) FilesNames() *collection.Map {
+func (v *TransactionVariables) FilesNames() collection.Map {
 	return v.filesNames
 }
 
-func (v *TransactionVariables) FilesTmpContent() *collection.Map {
+func (v *TransactionVariables) FilesTmpContent() collection.Map {
 	return v.filesTmpContent
 }
 
-func (v *TransactionVariables) ResponseHeadersNames() *collection.Map {
+func (v *TransactionVariables) ResponseHeadersNames() collection.Collection {
 	return v.responseHeadersNames
 }
 
-func (v *TransactionVariables) RequestHeadersNames() *collection.Map {
+func (v *TransactionVariables) RequestHeadersNames() collection.Collection {
 	return v.requestHeadersNames
 }
 
-func (v *TransactionVariables) RequestCookiesNames() *collection.Map {
+func (v *TransactionVariables) RequestCookiesNames() collection.Collection {
 	return v.requestCookiesNames
 }
 
-func (v *TransactionVariables) XML() *collection.Map {
+func (v *TransactionVariables) XML() collection.Map {
 	return v.xml
 }
 
-func (v *TransactionVariables) RequestXML() *collection.Map {
+func (v *TransactionVariables) RequestXML() collection.Map {
 	return v.requestXML
 }
 
-func (v *TransactionVariables) ResponseXML() *collection.Map {
+func (v *TransactionVariables) ResponseXML() collection.Map {
 	return v.responseXML
 }
 
-func (v *TransactionVariables) IP() *collection.Map {
+func (v *TransactionVariables) IP() collection.Map {
 	return v.ip
 }
 
-func (v *TransactionVariables) ArgsNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsNames() collection.Collection {
 	return v.argsNames
 }
 
-func (v *TransactionVariables) ArgsGetNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsGetNames() collection.Collection {
 	return v.argsGetNames
 }
 
-func (v *TransactionVariables) ArgsPostNames() *collection.TranslationProxy {
+func (v *TransactionVariables) ArgsPostNames() collection.Collection {
 	return v.argsPostNames
 }
 
diff --git a/loggers/debug.go b/loggers/debug.go
index 3f028bb..b316200 100644
--- a/loggers/debug.go
+++ b/loggers/debug.go
@@ -9,12 +9,12 @@ import (
 
 // DebugLogger is used to log SecDebugLog messages
 type DebugLogger interface {
-	// Info logs an info message
-	Info(message string, args ...interface{})
-	// Warn logs a warning message
-	Warn(message string, args ...interface{})
 	// Error logs an error message
 	Error(message string, args ...interface{})
+	// Warn logs a warning message
+	Warn(message string, args ...interface{})
+	// Info logs an info message
+	Info(message string, args ...interface{})
 	// Debug logs a debug message
 	Debug(message string, args ...interface{})
 	// Trace logs a trace message
@@ -31,31 +31,37 @@ type LogLevel int
 
 const (
 	// LogLevelUnknown is a default value for unknown log level
-	LogLevelUnknown LogLevel = iota
-	// LogLevelInfo is the lowest level of logging
-	LogLevelInfo
+	LogLevelUnknown LogLevel = iota - 1
+	// LogLevelNoLog is the lowest level of logging, no logs are generated
+	LogLevelNoLog
+	// LogLevelError is the level of logging only for errors
+	LogLevelError
 	// LogLevelWarn is the level of logging for warnings
 	LogLevelWarn
-	// LogLevelError is the level of logging for errors
-	LogLevelError
+	// LogLevelInfo is the lowest of logging for informational messages
+	LogLevelInfo
 	// LogLevelDebug is the level of logging for debug messages
 	LogLevelDebug
+	// ModSecurity compatibility, levels 4-8 will be Debug level
+	_ = iota + 2
 	// LogLevelTrace is the highest level of logging
 	LogLevelTrace
 )
 
 // String returns the string representation of the log level
 func (level LogLevel) String() string {
-	switch level {
-	case LogLevelInfo:
-		return "INFO"
-	case LogLevelWarn:
-		return "WARN"
-	case LogLevelError:
+	switch {
+	case level == LogLevelNoLog:
+		return "NOLOG"
+	case level == LogLevelError:
 		return "ERROR"
-	case LogLevelDebug:
+	case level == LogLevelWarn:
+		return "WARN"
+	case level == LogLevelInfo:
+		return "INFO"
+	case level >= LogLevelDebug && level < LogLevelTrace:
 		return "DEBUG"
-	case LogLevelTrace:
+	case level == LogLevelTrace:
 		return "TRACE"
 	}
 	return "UNKNOWN"
@@ -63,5 +69,5 @@ func (level LogLevel) String() string {
 
 // Invalid returns true if the log level is invalid
 func (level LogLevel) Invalid() bool {
-	return level < LogLevelInfo || level > LogLevelTrace
+	return level < LogLevelNoLog || level > LogLevelTrace
 }
diff --git a/macro/macro.go b/macro/macro.go
index 11d7fbf..78b4233 100644
--- a/macro/macro.go
+++ b/macro/macro.go
@@ -63,19 +63,15 @@ func expandToken(tx rules.TransactionState, token macroToken) string {
 		return token.text
 	}
 	switch col := tx.Collection(token.variable).(type) {
-	case *collection.Map:
+	case collection.Map:
 		if c := col.Get(token.key); len(c) > 0 {
 			return c[0]
 		}
 	case *collection.Simple:
 		return col.String()
-	case *collection.Proxy:
-		if c := col.Get(token.key); len(c) > 0 {
-			return c[0]
-		}
-	case *collection.TranslationProxy:
-		if c := col.Get(0); len(c) > 0 {
-			return c
+	case collection.Collection:
+		if c := col.FindString(token.key); len(c) > 0 {
+			return c[0].Value()
 		}
 	}
 
diff --git a/rules/transaction.go b/rules/transaction.go
index 8e797c3..5a107b1 100644
--- a/rules/transaction.go
+++ b/rules/transaction.go
@@ -50,7 +50,7 @@ type TransactionVariables interface {
 	UrlencodedError() *collection.Simple
 	ResponseContentType() *collection.Simple
 	UniqueID() *collection.Simple
-	ArgsCombinedSize() *collection.SizeProxy
+	ArgsCombinedSize() collection.Collection
 	AuthType() *collection.Simple
 	FilesCombinedSize() *collection.Simple
 	FullRequest() *collection.Simple
@@ -64,7 +64,7 @@ type TransactionVariables interface {
 	MultipartDataAfter() *collection.Simple
 	MultipartDataBefore() *collection.Simple
 	MultipartFileLimitExceeded() *collection.Simple
-	MultipartPartHeaders() *collection.Map
+	MultipartPartHeaders() collection.Map
 	MultipartHeaderFolding() *collection.Simple
 	MultipartInvalidHeaderFolding() *collection.Simple
 	MultipartInvalidPart() *collection.Simple
@@ -104,40 +104,35 @@ type TransactionVariables interface {
 	HighestSeverity() *collection.Simple
 	StatusLine() *collection.Simple
 	InboundErrorData() *collection.Simple
-	// Custom
-	Env() *collection.Map
-	TX() *collection.Map
-	Rule() *collection.Map
+	Env() collection.Map
+	TX() collection.Map
+	Rule() collection.Map
 	Duration() *collection.Simple
-	// Proxy Variables
-	Args() *collection.Proxy
-	// Maps Variables
-	ArgsGet() *collection.Map
-	ArgsPost() *collection.Map
-	ArgsPath() *collection.Map
-	FilesTmpNames() *collection.Map
-	Geo() *collection.Map
-	Files() *collection.Map
-	RequestCookies() *collection.Map
-	RequestHeaders() *collection.Map
-	ResponseHeaders() *collection.Map
-	MultipartName() *collection.Map
-	MatchedVarsNames() *collection.Map
-	MultipartFilename() *collection.Map
-	MatchedVars() *collection.Map
-	FilesSizes() *collection.Map
-	FilesNames() *collection.Map
-	FilesTmpContent() *collection.Map
-	ResponseHeadersNames() *collection.Map
-	RequestHeadersNames() *collection.Map
-	RequestCookiesNames() *collection.Map
-	XML() *collection.Map
-	RequestXML() *collection.Map
-	ResponseXML() *collection.Map
-	// Persistent variables
-	IP() *collection.Map
-	// Translation Proxy Variables
-	ArgsNames() *collection.TranslationProxy
-	ArgsGetNames() *collection.TranslationProxy
-	ArgsPostNames() *collection.TranslationProxy
+	Args() collection.Collection
+	ArgsGet() collection.Map
+	ArgsPost() collection.Map
+	ArgsPath() collection.Map
+	FilesTmpNames() collection.Map
+	Geo() collection.Map
+	Files() collection.Map
+	RequestCookies() collection.Map
+	RequestHeaders() collection.Map
+	ResponseHeaders() collection.Map
+	MultipartName() collection.Map
+	MatchedVarsNames() collection.Collection
+	MultipartFilename() collection.Map
+	MatchedVars() collection.Map
+	FilesSizes() collection.Map
+	FilesNames() collection.Map
+	FilesTmpContent() collection.Map
+	ResponseHeadersNames() collection.Collection
+	RequestHeadersNames() collection.Collection
+	RequestCookiesNames() collection.Collection
+	XML() collection.Map
+	RequestXML() collection.Map
+	ResponseXML() collection.Map
+	IP() collection.Map
+	ArgsNames() collection.Collection
+	ArgsGetNames() collection.Collection
+	ArgsPostNames() collection.Collection
 }


Test patch:
diff --git a/collection/collection_map_test.go b/collection/collection_map_test.go
index 8ad68eb..a3831e8 100644
--- a/collection/collection_map_test.go
+++ b/collection/collection_map_test.go
@@ -24,7 +24,7 @@ func TestCollectionMap(t *testing.T) {
 	c := NewMap(variables.ArgsPost)
 	c.SetIndex("key", 1, "value")
 	c.Set("key2", []string{"value2"})
-	if c.Get("key")[1] != "value" {
+	if c.Get("key")[0] != "value" {
 		t.Error("Error setting index")
 	}
 	if len(c.FindAll()) == 0 {
@@ -33,7 +33,7 @@ func TestCollectionMap(t *testing.T) {
 	if len(c.FindString("a")) > 0 {
 		t.Error("Error should not find string")
 	}
-	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 3 {
+	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
 		t.Errorf("Error should find regex, got %d", l)
 	}
 }
diff --git a/collection/collection_proxy_test.go b/collection/collection_proxy_test.go
deleted file mode 100644
index ae07cd2..0000000
--- a/collection/collection_proxy_test.go
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"regexp"
-	"sort"
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewProxy(variables.Args, c1, c2)
-
-	c1.Set("key1", []string{"value1"})
-	c1.Set("key2", []string{"value2"})
-	c2.Set("key3", []string{"value3"})
-
-	p := proxy.FindAll()
-	if len(p) != 3 {
-		t.Error("Error finding all")
-	} else {
-		p := proxy.FindAll()
-		m := false
-		for _, v := range p {
-			if v.Value() == "value1" {
-				m = true
-				break
-			}
-		}
-		if !m {
-			t.Error("Error finding all")
-		}
-	}
-	var f []string
-	for _, r := range p {
-		f = append(f, r.Value())
-	}
-	sort.Strings(f)
-	if f[0] != "value1" || f[1] != "value2" || f[2] != "value3" {
-		t.Error("Error finding all")
-	}
-
-	if len(proxy.FindString("key3")) == 0 {
-		t.Error("Error finding string")
-	}
-	if len(proxy.FindRegex(regexp.MustCompile("k.*"))) != 3 {
-		t.Error("Error finding regex")
-	}
-}
diff --git a/collection/collection_size_proxy_test.go b/collection/collection_size_proxy_test.go
deleted file mode 100644
index 65c54cc..0000000
--- a/collection/collection_size_proxy_test.go
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionSizeProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewCollectionSizeProxy(variables.Args, c1, c2)
-
-	c1.Set("key1", []string{"value1", "value2"})
-	c1.Set("key2", []string{"value2"})
-	c2.Set("key3", []string{"value3"})
-	if proxy.Size() != 24 {
-		t.Errorf("Error finding size for size proxy, got %d", proxy.Size())
-	}
-
-}
diff --git a/collection/collection_translation_proxy_test.go b/collection/collection_translation_proxy_test.go
deleted file mode 100644
index 2cab5a6..0000000
--- a/collection/collection_translation_proxy_test.go
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package collection
-
-import (
-	"regexp"
-	"testing"
-
-	"github.com/corazawaf/coraza/v3/types/variables"
-)
-
-func TestCollectionTranslationProxy(t *testing.T) {
-	c1 := NewMap(variables.ArgsPost)
-	c2 := NewMap(variables.ArgsGet)
-	proxy := NewTranslationProxy(variables.ArgsNames, c1, c2)
-
-	c1.SetCS("key1", "key1", []string{"value1"})
-	c1.Set("key2", []string{"value2"})
-	c2.SetCS("key3", "Key3", []string{"value3"})
-
-	if len(proxy.FindAll()) != 3 {
-		t.Error("Error finding all")
-	}
-	if len(proxy.FindString("key3")) == 0 {
-		t.Error("Error finding string")
-	}
-	if proxy.FindString("key1")[0].Value() != "key1" {
-		t.Error("Error value findstring key1")
-	}
-	if proxy.FindString("key3")[0].Value() != "key3" {
-		t.Error("Error value findstring key3")
-	}
-	if len(proxy.FindRegex(regexp.MustCompile("k.*"))) != 3 {
-		t.Error("Error finding regex")
-	}
-}
diff --git a/internal/collections/concat_test.go b/internal/collections/concat_test.go
new file mode 100644
index 0000000..0707e6b
--- /dev/null
+++ b/internal/collections/concat_test.go
@@ -0,0 +1,68 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"strings"
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestConcatCollection(t *testing.T) {
+	c1 := collection.NewMap(variables.ArgsGet)
+	c2 := collection.NewMap(variables.ArgsPost)
+	c3 := collection.NewMap(variables.ArgsPath)
+
+	c := NewConcatCollection(variables.Args, c1, c2, c3)
+
+	re1 := regexp.MustCompile("anim")
+	re2 := regexp.MustCompile("pla")
+
+	if want, have := "ARGS", c.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, c.FindAll())
+
+	c1.Add("animal", "cat")
+
+	assertValuesMatch(t, c.FindAll(), "cat")
+	assertValuesMatch(t, c.FindString("animal"), "cat")
+	assertValuesMatch(t, c.FindString("plant"))
+	assertValuesMatch(t, c.FindRegex(re1), "cat")
+	assertValuesMatch(t, c.FindRegex(re2))
+
+	c2.Add("animal", "dog")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog")
+	assertValuesMatch(t, c.FindString("animal"), "cat", "dog")
+	assertValuesMatch(t, c.FindString("plant"))
+	assertValuesMatch(t, c.FindRegex(re1), "cat", "dog")
+	assertValuesMatch(t, c.FindRegex(re2))
+
+	c3.Add("plant", "palm")
+
+	assertValuesMatch(t, c.FindAll(), "cat", "dog", "palm")
+	assertValuesMatch(t, c.FindString("animal"), "cat", "dog")
+	assertValuesMatch(t, c.FindString("plant"), "palm")
+	assertValuesMatch(t, c.FindRegex(re1), "cat", "dog")
+	assertValuesMatch(t, c.FindRegex(re2), "palm")
+}
+
+func assertValuesMatch(t *testing.T, matches []types.MatchData, wantValues ...string) {
+	t.Helper()
+
+	haveValues := make([]string, len(matches))
+	for i := range matches {
+		haveValues[i] = matches[i].Value()
+	}
+	// String concat is the simplest way to compare and print a message without reflection
+	if want, have := strings.Join(wantValues, ","), strings.Join(haveValues, ","); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+}
diff --git a/internal/collections/named_test.go b/internal/collections/named_test.go
new file mode 100644
index 0000000..5235a6a
--- /dev/null
+++ b/internal/collections/named_test.go
@@ -0,0 +1,48 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"regexp"
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestNamedCollection(t *testing.T) {
+	c := NewNamedCollection(variables.ArgsPost)
+
+	// Same as collection_map_test
+	c.SetIndex("key", 1, "value")
+	c.Set("key2", []string{"value2"})
+	if c.Get("key")[0] != "value" {
+		t.Error("Error setting index")
+	}
+	if len(c.FindAll()) == 0 {
+		t.Error("Error finding all")
+	}
+	if len(c.FindString("a")) > 0 {
+		t.Error("Error should not find string")
+	}
+	if l := len(c.FindRegex(regexp.MustCompile("k.*"))); l != 2 {
+		t.Errorf("Error should find regex, got %d", l)
+	}
+
+	// Now test names
+
+	names := c.Names(variables.ArgsPostNames)
+	if want, have := "ARGS_POST_NAMES", names.Name(); want != have {
+		t.Errorf("want %q, have %q", want, have)
+	}
+
+	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	c.Add("key", "value2")
+	assertValuesMatch(t, names.FindAll(), "key", "key2")
+	// While selection operators will treat this as case-insensitive, names should have all names
+	// as-is.
+	c.Add("Key", "value3")
+	assertValuesMatch(t, names.FindAll(), "key", "key2", "Key")
+	c.Remove("key2")
+	assertValuesMatch(t, names.FindAll(), "key", "Key")
+}
diff --git a/internal/collections/sized_test.go b/internal/collections/sized_test.go
new file mode 100644
index 0000000..168d39f
--- /dev/null
+++ b/internal/collections/sized_test.go
@@ -0,0 +1,24 @@
+// Copyright 2023 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package collections
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+func TestSizedCollection(t *testing.T) {
+	c1 := NewNamedCollection(variables.ArgsPost)
+	c2 := NewNamedCollection(variables.ArgsGet)
+	proxy := NewSizeCollection(variables.ArgsCombinedSize, c1, c2)
+
+	assertValuesMatch(t, proxy.FindAll(), "0")
+	c1.Set("key1", []string{"value1", "value2"})
+	assertValuesMatch(t, proxy.FindAll(), "12")
+	c1.Set("key2", []string{"value2"})
+	assertValuesMatch(t, proxy.FindAll(), "18")
+	c2.Set("key3", []string{"value3"})
+	assertValuesMatch(t, proxy.FindAll(), "24")
+}
diff --git a/internal/corazawaf/logger_test.go b/internal/corazawaf/logger_test.go
new file mode 100644
index 0000000..c1578f6
--- /dev/null
+++ b/internal/corazawaf/logger_test.go
@@ -0,0 +1,66 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package corazawaf
+
+import (
+	"testing"
+
+	"github.com/corazawaf/coraza/v3/loggers"
+)
+
+func TestLoggerLogLevels(t *testing.T) {
+	waf := NewWAF()
+	testCases := map[string]struct {
+		logFunction                func(message string, args ...interface{})
+		expectedLowestPrintedLevel int
+	}{
+		"Trace": {
+			logFunction:                waf.Logger.Trace,
+			expectedLowestPrintedLevel: 9,
+		},
+		"Debug": {
+			logFunction:                waf.Logger.Debug,
+			expectedLowestPrintedLevel: 4,
+		},
+		"Info": {
+			logFunction:                waf.Logger.Info,
+			expectedLowestPrintedLevel: 3,
+		},
+		"Warn": {
+			logFunction:                waf.Logger.Warn,
+			expectedLowestPrintedLevel: 2,
+		},
+		"Error": {
+			logFunction:                waf.Logger.Error,
+			expectedLowestPrintedLevel: 1,
+		},
+	}
+
+	for name, tCase := range testCases {
+		t.Run(name, func(t *testing.T) {
+			for settedLevel := 0; settedLevel <= 9; settedLevel++ {
+				l := &inspectableLogger{}
+				waf.Logger.SetOutput(l)
+				waf.Logger.SetLevel(loggers.LogLevel(settedLevel))
+				tCase.logFunction("this is a log")
+
+				if settedLevel >= tCase.expectedLowestPrintedLevel && len(l.entries) != 1 {
+					t.Fatalf("Missing expected log. Level: %d, Function: %s", settedLevel, name)
+				}
+				if settedLevel < tCase.expectedLowestPrintedLevel && len(l.entries) == 1 {
+					t.Fatalf("Unexpected log. Level: %d, Function: %s", settedLevel, name)
+				}
+			}
+
+		})
+	}
+}
+
+func TestLoggerLevelDefaultsToInfo(t *testing.T) {
+	waf := NewWAF()
+	waf.Logger.SetLevel(loggers.LogLevel(10))
+	if waf.Logger.(*stdDebugLogger).Level != loggers.LogLevelInfo {
+		t.Fatalf("Unexpected log level: %d. It should default to Info (%s)", waf.Logger.(*stdDebugLogger).Level, loggers.LogLevelInfo)
+	}
+}
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 3c42612..4cf6645 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -732,10 +732,10 @@ func TestHeaderSetters(t *testing.T) {
 	if tx.variables.requestHeaders.Get("cookie")[0] != "abc=def;hij=klm" {
 		t.Error("failed to set request header")
 	}
-	if !utils.InSlice("cookie", tx.variables.requestHeadersNames.Get("cookie")) {
-		t.Error("failed to set header name", tx.variables.requestHeadersNames.Get("cookie"))
+	if !utils.InSlice("cookie", collectionValues(t, tx.variables.requestHeadersNames)) {
+		t.Error("failed to set header name", collectionValues(t, tx.variables.requestHeadersNames))
 	}
-	if !utils.InSlice("abc", tx.variables.requestCookiesNames.Get("abc")) {
+	if !utils.InSlice("abc", collectionValues(t, tx.variables.requestCookiesNames)) {
 		t.Error("failed to set cookie name")
 	}
 	if err := tx.Close(); err != nil {
@@ -743,6 +743,15 @@ func TestHeaderSetters(t *testing.T) {
 	}
 }
 
+func collectionValues(t *testing.T, col collection.Collection) []string {
+	t.Helper()
+	var values []string
+	for _, v := range col.FindAll() {
+		values = append(values, v.Value())
+	}
+	return values
+}
+
 func TestRequestBodyProcessingAlgorithm(t *testing.T) {
 	waf := NewWAF()
 	tx := waf.NewTransaction()
diff --git a/testing/engine/chains.go b/testing/engine/chains.go
index 31c6e1e..f77f144 100644
--- a/testing/engine/chains.go
+++ b/testing/engine/chains.go
@@ -91,7 +91,7 @@ var _ = profile.RegisterProfile(profile.Profile{
 						Output: profile.ExpectedOutput{
 							TriggeredRules:    []int{1, 21},
 							NonTriggeredRules: []int{20, 2, 1313},
-							LogContains:       "FoundSubChain21 REQUEST_HEADERS:Host in MATCHED_VARS_NAMES:REQUEST_HEADERS:Host",
+							LogContains:       "FoundSubChain21 REQUEST_HEADERS:Host in MATCHED_VARS_NAMES",
 						},
 					},
 				},
@@ -154,7 +154,7 @@ SecRule REQUEST_HEADERS "@rx attack21" \
     "msg:'Sub Chain Rule',\
     logdata:'FoundSubChain21 %{MATCHED_VAR} in %{MATCHED_VAR_NAME}',\
     chain"
-    SecRule MATCHED_VAR_NAME "@rx MATCHED_VARS_NAMES:REQUEST_HEADERS:Host" \
+    SecRule MATCHED_VAR_NAME "@rx MATCHED_VARS_NAMES" \
     "msg:'Sub Sub Chain Rule',\
     logdata:'FoundSubSubChain21 %{MATCHED_VAR} in %{MATCHED_VAR_NAME}',\
     block"