Repository: corazawaf/coraza
Pull request #397 (2022-09-05 07:53:22)

Convert library entrypoints to immutable interfaces
This is a very early look into exploring #371 

While writing it, I noticed another objective, where it could be easier to use the API if `Transaction` is split into the interface for WAF users and those writing custom rules / operators. From my cursory understanding, methods like `AddRequestHeader`, `Process*` only make sense in former and `Capture` only make sense in latter. In this early iteration, we can see the `Transaction` interface for WAF users. `Rule` also probably needs to have a separate interface for defining rules programatically and implementing operators, etc.

The approach for doing this is to first stuff what we have into `internal/corazawaf`, `internal/seclang` and start creating public API from scratch delegating to these. After completing migration, there is probably cleanup that could be done which may result in the removal of delegation, but that could happen after locking in a public API. 

Any methods suffixed `Next()` are placeholders to replace existing methods in future iterations of this PR.

The important files at the moment are (these links load slow! wait a few seconds, don't scroll)

Public API:
[config.go](https://github.com/corazawaf/coraza/compare/v3/dev...anuraaga:waf-interface?expand=1#diff-0e426a43248661127a0c0ee115aef7a1093b635f8993b3f7ebb1dd9f05b8f249)
[waf.go](https://github.com/corazawaf/coraza/compare/v3/dev...anuraaga:waf-interface?expand=1#diff-8fffe240f1ec944a509791e1e15937e995bc56cd6e8039fdd698e06512303066)

Public API Users:
[coraza_test.go](https://github.com/corazawaf/coraza/compare/v3/dev...anuraaga:waf-interface?expand=1#diff-c8bf1238e4dfb3a7412412786e077c987f4ef455c0ca2a039c6e68c62c11ea29)
[http.go](https://github.com/corazawaf/coraza/compare/v3/dev...anuraaga:waf-interface?expand=1#diff-c7bd264032bb4b6d96f25a74e79e88d7c9842f310bea423fb54f76297919a8ef)

Let me know what you think of this very general direction and whether we should pursue it to completion. It will take time to reflect these patterns across all the APIs, but IMO it would be worth it to improve the ease-of-use of the API by making sure they are built on the actual use cases / user personas rather than having too many large interfaces with sort of jumbling of concerns, and chance of safety issues because of mutability. As we move towards using Coraza more in production, v3 is hopefully the right time to settle on the API that avoids a v4 for as long as possible. 

Happy to hear thoughts!

anuraaga: Thanks for all the good comments. I'd like a high-level "keep at it" from @jptosso before proceeding too far as it will be a lot more to do.
anuraaga: Another big addition in https://github.com/corazawaf/coraza/pull/397/commits/911e0aded6a1f5f44e08cdb90e176b3b38fb3df6

I think this shows where things are going in general. It defines a public API for `actions` plugins. Unfortunately it's a bit hard to know what the right API for them is :) While working through the current actions, I noticed most are 1:1 with implementation details, e.g. set a field on transaction which is used by internal coraza logic. I kept these using internal implementation details as they don't really represent a plugin per se.

It means the actions plugin API is rather anemic, but maybe it's still useful enough given it has access to important parts of the transaction. I think the notable addition needed which I didn't do quite yet is a mechanism to propagate state from `Init` to `Evaluate` in a generic way as opposed to static fields on `Rule` (similar to `Context`).

https://github.com/corazawaf/coraza/pull/397/files#diff-d6814085206adf41797e66ab710ce2e744d55ffc10e71a2bb0e28a3f92196b4aR25

The main API is `TransactionState` which attempts to be the mutable bits of a `Transaction`, anything necessary in rule evaluation but not user level orchestration

https://github.com/corazawaf/coraza/pull/397/files#diff-2b27646a11f4d8723da8f9fd16bb53fd4fd25fadf866c44481a87b6fe3ba265fR15

One thing I'm particularly happy about is actions not having to check rule engine before interruption. We can focus on making this API easy to use without being coupled to internal details.

https://github.com/corazawaf/coraza/pull/397/files#diff-081faffa20610c1510154a66fcff0bbf617e176cf3a420ba5f5f1838ceb72503R124
https://github.com/corazawaf/coraza/pull/397/files#diff-0c56e81f05d943bfb010c8ff984018bab39c317d383030232f9164c96b019454L23

Next I'll make the operators a public API and then programatic rules, and then scrub through remaining parts.

@jptosso Feel free to leave any thoughts on where this is going, though I'll keep on going anyways :-)
jptosso: > I think this shows where things are going in general. It defines a public API for actions plugins. Unfortunately it's a bit hard to know what the right API for them is :) While working through the current actions, I noticed most are 1:1 with implementation details, e.g. set a field on transaction which is used by internal coraza logic. I kept these using internal implementation details as they don't really represent a plugin per se.

We must keep enough exported fields for metadata, for example, a rule action can update a transaction and a rule (during bootstrap), for that reason, we should have TransactionMetadata and RuleMetadata (which already exist). Maybe actions should do something like `tx.Metadata.Status = 500`. Or we can just create a setter.

codecov-commenter: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/397?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.15**% // Head: **73.87**% // Decreases project coverage by **`-3.27%`** :warning:
> Coverage data is based on head [(`91cfe16`)](https://codecov.io/gh/corazawaf/coraza/pull/397?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`05d352c`)](https://codecov.io/gh/corazawaf/coraza/commit/05d352c89b24391a043f1cabb49cee78ed73a3a2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 66.11% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           v3/dev     #397      +/-   ##
==========================================
- Coverage   77.15%   73.87%   -3.28%     
==========================================
  Files         137      140       +3     
  Lines        5979     6362     +383     
==========================================
+ Hits         4613     4700      +87     
- Misses       1100     1390     +290     
- Partials      266      272       +6     
```

| Flag | Coverage Δ | |
|---|---|---|
| no-tinygo | `73.93% <66.34%> (-3.30%)` | :arrow_down: |
| overall | `73.87% <66.11%> (-3.28%)` | :arrow_down: |
| tinygo | `72.12% <64.80%> (-3.38%)` | :arrow_down: |

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/397?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [actions/allow.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9hbGxvdy5nbw==) | `0.00% <0.00%> (ø)` | |
| [actions/exec.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9leGVjLmdv) | `0.00% <0.00%> (ø)` | |
| [actions/expirevar.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9leHBpcmV2YXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [actions/initcol.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9pbml0Y29sLmdv) | `0.00% <0.00%> (ø)` | |
| [actions/maturity.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9tYXR1cml0eS5nbw==) | `0.00% <0.00%> (ø)` | |
| [actions/rev.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YWN0aW9ucy9yZXYuZ28=) | `0.00% <0.00%> (ø)` | |
| [internal/corazawaf/body\_buffer.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL2JvZHlfYnVmZmVyLmdv) | `73.21% <ø> (ø)` | |
| [internal/corazawaf/rulegroup.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3J1bGVncm91cC5nbw==) | `93.90% <ø> (ø)` | |
| [loggers/debug.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-bG9nZ2Vycy9kZWJ1Zy5nbw==) | `53.33% <ø> (ø)` | |
| [operators/geo\_lookup.go](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-b3BlcmF0b3JzL2dlb19sb29rdXAuZ28=) | `0.00% <0.00%> (ø)` | |
| ... and [78 more](https://codecov.io/gh/corazawaf/coraza/pull/397/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | |

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/397?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).

anuraaga: I'm going to give this another read-through by Monday but think it's mostly ready for a first step in a few. The API can't be fully cleaned up in this one PR but I think it'll be a big step without losing current functionality like custom operators/actions.
anuraaga: Sorry got distracted by backslashes of all things 😭 Will definitely open for review tomorrow 
anuraaga: @jptosso Sorry for the delay, I think for a first version this is OK, there is still more cleanup to follow up with, in particular removing API added here that we feel is overkill, etc. 

Patch:
diff --git a/.gitignore b/.gitignore
index 77420be..5bd131a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,5 @@ test/crs
 vendor/
 coraza-waf
 __debug_bin
-seclang/crs_test.go
 
 build/
diff --git a/README.md b/README.md
index 63ffe2b..9fbbd27 100644
--- a/README.md
+++ b/README.md
@@ -77,28 +77,26 @@ Coraza can be used as a library for your Go program to implement a security midd
 ```go
 package main
 
-import(
+import (
 	"context"
 	"fmt"
 	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/seclang"
 )
 
 func main() {
 	// First we initialize our waf and our seclang parser
-	waf := coraza.NewWAF()
-	parser := seclang.NewParser(waf)
-
+	waf, err := coraza.NewWAF(coraza.NewWAFConfig().
+		WithDirectives(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`))
 	// Now we parse our rules
-	if err := parser.FromString(`SecRule REMOTE_ADDR "@rx .*" "id:1,phase:1,deny,status:403"`); err != nil {
+	if err != nil {
 		fmt.Println(err)
 	}
 
 	// Then we create a transaction and assign some variables
 	tx := waf.NewTransaction(context.Background())
-	defer func(){
+	defer func() {
 		tx.ProcessLogging()
-		tx.Clean()
+		tx.Close()
 	}()
 	tx.ProcessConnection("127.0.0.1", 8080, "127.0.0.1", 12345)
 
diff --git a/actions/actions.go b/actions/actions.go
index 375ae8c..24674a3 100644
--- a/actions/actions.go
+++ b/actions/actions.go
@@ -7,12 +7,12 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 // ruleActionWrapper is used to wrap a RuleAction so that it can be registered
 // and recreated on each call
-type ruleActionWrapper = func() coraza.RuleAction
+type ruleActionWrapper = func() rules.Action
 
 // TODO maybe change it to sync.Map
 var actionmap = map[string]ruleActionWrapper{}
@@ -20,7 +20,7 @@ var actionmap = map[string]ruleActionWrapper{}
 // RegisterPlugin registers a new RuleAction
 // It can be used also for plugins.
 // If you register an action with an existing name, it will be overwritten.
-func RegisterPlugin(name string, a func() coraza.RuleAction) {
+func RegisterPlugin(name string, a func() rules.Action) {
 	name = strings.ToLower(name)
 	actionmap[name] = a
 }
@@ -62,9 +62,9 @@ func init() {
 	RegisterPlugin("ver", ver)
 }
 
-// GetAction returns an unwrapped RuleAction from the actionmap based on the name
+// Get returns an unwrapped RuleAction from the actionmap based on the name
 // If the action does not exist it returns an error
-func GetAction(name string) (coraza.RuleAction, error) {
+func Get(name string) (rules.Action, error) {
 	name = strings.ToLower(name)
 	if a, ok := actionmap[name]; ok {
 		return a(), nil
diff --git a/actions/allow.go b/actions/allow.go
index 5c5e34d..9a6358f 100644
--- a/actions/allow.go
+++ b/actions/allow.go
@@ -6,8 +6,7 @@ package actions
 import (
 	"fmt"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 // 0 nothing, 1 phase, 2 request
@@ -15,7 +14,7 @@ type allowFn struct {
 	allow int
 }
 
-func (a *allowFn) Init(r *coraza.Rule, b1 string) error {
+func (a *allowFn) Init(r rules.RuleMetadata, b1 string) error {
 	switch b1 {
 	case "phase":
 		a.allow = 2 // skip current phase
@@ -29,7 +28,7 @@ func (a *allowFn) Init(r *coraza.Rule, b1 string) error {
 	return nil
 }
 
-func (a *allowFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *allowFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// TODO implement this:
 	/*
 		if a.allow == 1 {
@@ -42,15 +41,15 @@ func (a *allowFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 	*/
 }
 
-func (a *allowFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *allowFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func allow() coraza.RuleAction {
+func allow() rules.Action {
 	return &allowFn{}
 }
 
 var (
-	_ coraza.RuleAction = (*allowFn)(nil)
+	_ rules.Action      = (*allowFn)(nil)
 	_ ruleActionWrapper = allow
 )
diff --git a/actions/append.go b/actions/append.go
index 6d85af4..7b75b13 100644
--- a/actions/append.go
+++ b/actions/append.go
@@ -4,43 +4,43 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type appendFn struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-func (a *appendFn) Init(r *coraza.Rule, data string) error {
-	macro, err := coraza.NewMacro(data)
+func (a *appendFn) Init(r rules.RuleMetadata, data string) error {
+	macro, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	a.data = *macro
+	a.data = macro
 	return nil
 }
 
-func (a *appendFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	if !tx.WAF.ContentInjection {
-		tx.WAF.Logger.Debug("append rejected because of ContentInjection")
+func (a *appendFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	if !tx.ContentInjection() {
+		tx.DebugLogger().Debug("append rejected because of ContentInjection")
 		return
 	}
 	data := a.data.Expand(tx)
-	if _, err := tx.ResponseBodyBuffer.Write([]byte(data)); err != nil {
-		tx.WAF.Logger.Error("append failed to write to response buffer: %s", err.Error())
+	if _, err := tx.ResponseBodyWriter().Write([]byte(data)); err != nil {
+		tx.DebugLogger().Error("append failed to write to response buffer: %s", err.Error())
 	}
 }
 
-func (a *appendFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *appendFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func append2() coraza.RuleAction {
+func append2() rules.Action {
 	return &appendFn{}
 }
 
 var (
-	_ coraza.RuleAction = &appendFn{}
+	_ rules.Action      = &appendFn{}
 	_ ruleActionWrapper = append2
 )
diff --git a/actions/auditlog.go b/actions/auditlog.go
index 78589d5..d571c84 100644
--- a/actions/auditlog.go
+++ b/actions/auditlog.go
@@ -4,30 +4,31 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type auditlogFn struct{}
 
-func (a *auditlogFn) Init(r *coraza.Rule, data string) error {
-	r.Audit = true
+func (a *auditlogFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Audit = true
 	return nil
 }
 
-func (a *auditlogFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *auditlogFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Nothing here
 }
 
-func (a *auditlogFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *auditlogFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func auditlog() coraza.RuleAction {
+func auditlog() rules.Action {
 	return &auditlogFn{}
 }
 
 var (
-	_ coraza.RuleAction = (*auditlogFn)(nil)
+	_ rules.Action      = (*auditlogFn)(nil)
 	_ ruleActionWrapper = auditlog
 )
diff --git a/actions/block.go b/actions/block.go
index 138bbc5..2eb944f 100644
--- a/actions/block.go
+++ b/actions/block.go
@@ -4,29 +4,28 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type blockFn struct{}
 
-func (a *blockFn) Init(r *coraza.Rule, b1 string) error {
+func (a *blockFn) Init(r rules.RuleMetadata, b1 string) error {
 	return nil
 }
 
-func (a *blockFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *blockFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// This should never run
 }
 
-func (a *blockFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *blockFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func block() coraza.RuleAction {
+func block() rules.Action {
 	return &blockFn{}
 }
 
 var (
-	_ coraza.RuleAction = &blockFn{}
+	_ rules.Action      = &blockFn{}
 	_ ruleActionWrapper = block
 )
diff --git a/actions/capture.go b/actions/capture.go
index fdc3359..4bed9cb 100644
--- a/actions/capture.go
+++ b/actions/capture.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type captureFn struct{}
 
-func (a *captureFn) Init(r *coraza.Rule, b1 string) error {
+func (a *captureFn) Init(r rules.RuleMetadata, b1 string) error {
 	// this will capture only the current rule
-	r.Capture = true
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Capture = true
 	return nil
 }
 
-func (a *captureFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *captureFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 
 }
 
-func (a *captureFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *captureFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func capture() coraza.RuleAction {
+func capture() rules.Action {
 	return &captureFn{}
 }
 
 var (
-	_ coraza.RuleAction = &captureFn{}
+	_ rules.Action      = &captureFn{}
 	_ ruleActionWrapper = capture
 )
diff --git a/actions/chain.go b/actions/chain.go
index bf701d5..943b58a 100644
--- a/actions/chain.go
+++ b/actions/chain.go
@@ -4,30 +4,31 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type chainFn struct{}
 
-func (a *chainFn) Init(r *coraza.Rule, b1 string) error {
-	r.HasChain = true
+func (a *chainFn) Init(r rules.RuleMetadata, b1 string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).HasChain = true
 	return nil
 }
 
-func (a *chainFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *chainFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *chainFn) Type() types.RuleActionType {
-	return types.ActionTypeFlow
+func (a *chainFn) Type() rules.ActionType {
+	return rules.ActionTypeFlow
 }
 
-func chain() coraza.RuleAction {
+func chain() rules.Action {
 	return &chainFn{}
 }
 
 var (
-	_ coraza.RuleAction = &chainFn{}
+	_ rules.Action      = &chainFn{}
 	_ ruleActionWrapper = chain
 )
diff --git a/actions/ctl.go b/actions/ctl.go
index b2475a3..07c33a1 100644
--- a/actions/ctl.go
+++ b/actions/ctl.go
@@ -9,8 +9,9 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
@@ -46,7 +47,7 @@ type ctlFn struct {
 	colRx      *regexp.Regexp
 }
 
-func (a *ctlFn) Init(r *coraza.Rule, data string) error {
+func (a *ctlFn) Init(r rules.RuleMetadata, data string) error {
 	var err error
 	a.action, a.value, a.collection, a.colKey, err = a.parseCtl(data)
 	if len(a.colKey) > 2 && a.colKey[0] == '/' && a.colKey[len(a.colKey)-1] == '/' {
@@ -58,7 +59,9 @@ func (a *ctlFn) Init(r *coraza.Rule, data string) error {
 	return err
 }
 
-func (a *ctlFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *ctlFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	tx := txS.(*corazawaf.Transaction)
 	switch a.action {
 	case ctlRemoveTargetByID:
 		ran, err := a.rangeToInts(tx.WAF.Rules.GetRules(), a.value)
@@ -79,7 +82,7 @@ func (a *ctlFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 	case ctlRemoveTargetByMsg:
 		rules := tx.WAF.Rules.GetRules()
 		for _, r := range rules {
-			if r.Msg.String() == a.value {
+			if r.Msg != nil && r.Msg.String() == a.value {
 				tx.RemoveRuleTargetByID(r.ID, a.collection, a.colKey)
 			}
 		}
@@ -118,7 +121,7 @@ func (a *ctlFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 	case ctlRuleRemoveByMsg:
 		rules := tx.WAF.Rules.GetRules()
 		for _, r := range rules {
-			if r.Msg.String() == a.value {
+			if r.Msg != nil && r.Msg.String() == a.value {
 				tx.RemoveRuleByID(r.ID)
 			}
 		}
@@ -144,8 +147,8 @@ func (a *ctlFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 
 }
 
-func (a *ctlFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *ctlFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
 func (a *ctlFn) parseCtl(data string) (ctlFunctionType, string, variables.RuleVariable, string, error) {
@@ -211,7 +214,7 @@ func (a *ctlFn) parseCtl(data string) (ctlFunctionType, string, variables.RuleVa
 	return act, value, collection, strings.TrimSpace(colkey), nil
 }
 
-func (a *ctlFn) rangeToInts(rules []*coraza.Rule, input string) ([]int, error) {
+func (a *ctlFn) rangeToInts(rules []*corazawaf.Rule, input string) ([]int, error) {
 	ids := []int{}
 	spl := strings.SplitN(input, "-", 2)
 	var start, end int
@@ -240,11 +243,11 @@ func (a *ctlFn) rangeToInts(rules []*coraza.Rule, input string) ([]int, error) {
 	return ids, nil
 }
 
-func ctl() coraza.RuleAction {
+func ctl() rules.Action {
 	return &ctlFn{}
 }
 
 var (
-	_ coraza.RuleAction = &ctlFn{}
+	_ rules.Action      = &ctlFn{}
 	_ ruleActionWrapper = ctl
 )
diff --git a/actions/deny.go b/actions/deny.go
index 0b58279..91f4951 100644
--- a/actions/deny.go
+++ b/actions/deny.go
@@ -4,40 +4,37 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 type denyFn struct{}
 
-func (a *denyFn) Init(r *coraza.Rule, data string) error {
-	r.Disruptive = true
+func (a *denyFn) Init(r rules.RuleMetadata, data string) error {
 	return nil
 }
 
-func (a *denyFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	rid := r.ID
+func (a *denyFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	rid := r.GetID()
 	if rid == 0 {
-		rid = r.ParentID
-	}
-	if tx.RuleEngine == types.RuleEngineOn {
-		tx.Interruption = &types.Interruption{
-			Status: r.DisruptiveStatus,
-			RuleID: rid,
-			Action: "deny",
-		}
+		rid = r.GetParentID()
 	}
+	tx.Interrupt(&types.Interruption{
+		Status: r.Status(),
+		RuleID: rid,
+		Action: "deny",
+	})
 }
 
-func (a *denyFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *denyFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func deny() coraza.RuleAction {
+func deny() rules.Action {
 	return &denyFn{}
 }
 
 var (
-	_ coraza.RuleAction = &denyFn{}
+	_ rules.Action      = &denyFn{}
 	_ ruleActionWrapper = deny
 )
diff --git a/actions/drop.go b/actions/drop.go
index 0a6eeec..73add76 100644
--- a/actions/drop.go
+++ b/actions/drop.go
@@ -4,40 +4,37 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 type dropFn struct{}
 
-func (a *dropFn) Init(r *coraza.Rule, data string) error {
-	r.Disruptive = true
+func (a *dropFn) Init(r rules.RuleMetadata, data string) error {
 	return nil
 }
 
-func (a *dropFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	rid := r.ID
+func (a *dropFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	rid := r.GetID()
 	if rid == 0 {
-		rid = r.ParentID
-	}
-	if tx.RuleEngine == types.RuleEngineOn {
-		tx.Interruption = &types.Interruption{
-			Status: r.DisruptiveStatus,
-			RuleID: rid,
-			Action: "drop",
-		}
+		rid = r.GetParentID()
 	}
+	tx.Interrupt(&types.Interruption{
+		Status: r.Status(),
+		RuleID: rid,
+		Action: "drop",
+	})
 }
 
-func (a *dropFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *dropFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func drop() coraza.RuleAction {
+func drop() rules.Action {
 	return &dropFn{}
 }
 
 var (
-	_ coraza.RuleAction = &dropFn{}
+	_ rules.Action      = &dropFn{}
 	_ ruleActionWrapper = drop
 )
diff --git a/actions/exec.go b/actions/exec.go
index e70338d..1b6a893 100644
--- a/actions/exec.go
+++ b/actions/exec.go
@@ -4,30 +4,29 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type execFn struct {
 }
 
-func (a *execFn) Init(r *coraza.Rule, data string) error {
+func (a *execFn) Init(r rules.RuleMetadata, data string) error {
 	return nil
 }
 
-func (a *execFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *execFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not implemented
 }
 
-func (a *execFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *execFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func exec() coraza.RuleAction {
+func exec() rules.Action {
 	return &execFn{}
 }
 
 var (
-	_ coraza.RuleAction = &execFn{}
+	_ rules.Action      = &execFn{}
 	_ ruleActionWrapper = exec
 )
diff --git a/actions/expirevar.go b/actions/expirevar.go
index e44e5b3..88ccd44 100644
--- a/actions/expirevar.go
+++ b/actions/expirevar.go
@@ -8,8 +8,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type expirevarFn struct {
@@ -18,7 +17,7 @@ type expirevarFn struct {
 	key        string
 }
 
-func (a *expirevarFn) Init(r *coraza.Rule, data string) error {
+func (a *expirevarFn) Init(r rules.RuleMetadata, data string) error {
 	spl := strings.SplitN(data, "=", 2)
 	a.ttl, _ = strconv.Atoi(spl[1])
 	spl = strings.SplitN(spl[0], ".", 2)
@@ -30,20 +29,20 @@ func (a *expirevarFn) Init(r *coraza.Rule, data string) error {
 	return nil
 }
 
-func (a *expirevarFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *expirevarFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not supported
 	// tx.WAF.Logger.Error("Expirevar was used but it's not supported", zap.Int("rule", r.Id))
 }
 
-func (a *expirevarFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *expirevarFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func expirevar() coraza.RuleAction {
+func expirevar() rules.Action {
 	return &expirevarFn{}
 }
 
 var (
-	_ coraza.RuleAction = &expirevarFn{}
+	_ rules.Action      = &expirevarFn{}
 	_ ruleActionWrapper = expirevar
 )
diff --git a/actions/id.go b/actions/id.go
index d0e9ddb..789106f 100644
--- a/actions/id.go
+++ b/actions/id.go
@@ -7,14 +7,14 @@ import (
 	"fmt"
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type idFn struct {
 }
 
-func (a *idFn) Init(r *coraza.Rule, data string) error {
+func (a *idFn) Init(r rules.RuleMetadata, data string) error {
 	if data == "" {
 		return fmt.Errorf("id action requires a parameter")
 	}
@@ -22,29 +22,31 @@ func (a *idFn) Init(r *coraza.Rule, data string) error {
 	if err != nil {
 		return fmt.Errorf("invalid rule id %s", data)
 	}
-	r.ID = int(i)
-	if r.ID < 0 {
-		return fmt.Errorf("rule id (%d) cannot be negative", r.ID)
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	rInt := r.(*corazawaf.Rule)
+	rInt.ID = int(i)
+	if rInt.ID < 0 {
+		return fmt.Errorf("rule id (%d) cannot be negative", rInt.ID)
 	}
-	if r.ID == 0 {
-		return fmt.Errorf("rule id (%d) cannot be zero", r.ID)
+	if rInt.ID == 0 {
+		return fmt.Errorf("rule id (%d) cannot be zero", rInt.ID)
 	}
 	return nil
 }
 
-func (a *idFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *idFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *idFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *idFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func id() coraza.RuleAction {
+func id() rules.Action {
 	return &idFn{}
 }
 
 var (
-	_ coraza.RuleAction = &idFn{}
+	_ rules.Action      = &idFn{}
 	_ ruleActionWrapper = id
 )
diff --git a/actions/initcol.go b/actions/initcol.go
index 8d61c19..8dc2526 100644
--- a/actions/initcol.go
+++ b/actions/initcol.go
@@ -6,8 +6,7 @@ package actions
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 // Initializes a persistent collection and add the data to the standard collections coraza.
@@ -17,7 +16,7 @@ type initcolFn struct {
 	key        string
 }
 
-func (a *initcolFn) Init(r *coraza.Rule, data string) error {
+func (a *initcolFn) Init(r rules.RuleMetadata, data string) error {
 	kv := strings.SplitN(data, "=", 2)
 	a.collection = kv[0]
 	a.key = kv[1]
@@ -25,7 +24,7 @@ func (a *initcolFn) Init(r *coraza.Rule, data string) error {
 	return nil
 }
 
-func (a *initcolFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *initcolFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// tx.WAF.Logger.Error("initcol was used but it's not supported", zap.Int("rule", r.Id))
 	/*
 		key := tx.MacroExpansion(a.key)
@@ -49,15 +48,15 @@ func (a *initcolFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 	*/
 }
 
-func (a *initcolFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *initcolFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func initcol() coraza.RuleAction {
+func initcol() rules.Action {
 	return &initcolFn{}
 }
 
 var (
-	_ coraza.RuleAction = &initcolFn{}
+	_ rules.Action      = &initcolFn{}
 	_ ruleActionWrapper = initcol
 )
diff --git a/actions/log.go b/actions/log.go
index 0c76e14..bcc6bfe 100644
--- a/actions/log.go
+++ b/actions/log.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type logFn struct {
 }
 
-func (a *logFn) Init(r *coraza.Rule, data string) error {
-	r.Log = true
-	r.Audit = true
+func (a *logFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Log = true
+	r.(*corazawaf.Rule).Audit = true
 	return nil
 }
 
-func (a *logFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *logFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 }
 
-func (a *logFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *logFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func log() coraza.RuleAction {
+func log() rules.Action {
 	return &logFn{}
 }
 
 var (
-	_ coraza.RuleAction = &logFn{}
+	_ rules.Action      = &logFn{}
 	_ ruleActionWrapper = log
 )
diff --git a/actions/logdata.go b/actions/logdata.go
index 6ebcaf3..be5f94c 100644
--- a/actions/logdata.go
+++ b/actions/logdata.go
@@ -4,35 +4,38 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type logdataFn struct {
 }
 
-func (a *logdataFn) Init(r *coraza.Rule, data string) error {
-	macro, err := coraza.NewMacro(data)
+func (a *logdataFn) Init(r rules.RuleMetadata, data string) error {
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	r.LogData = *macro
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).LogData = m
 	return nil
 }
 
-func (a *logdataFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	tx.Logdata = r.LogData.Expand(tx)
+func (a *logdataFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	tx.(*corazawaf.Transaction).Logdata = r.(*corazawaf.Rule).LogData.Expand(tx)
 }
 
-func (a *logdataFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *logdataFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func logdata() coraza.RuleAction {
+func logdata() rules.Action {
 	return &logdataFn{}
 }
 
 var (
-	_ coraza.RuleAction = &logdataFn{}
+	_ rules.Action      = &logdataFn{}
 	_ ruleActionWrapper = logdata
 )
diff --git a/actions/maturity.go b/actions/maturity.go
index 5cd38fc..1348dc2 100644
--- a/actions/maturity.go
+++ b/actions/maturity.go
@@ -7,14 +7,14 @@ import (
 	"fmt"
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type maturityFn struct {
 }
 
-func (a *maturityFn) Init(r *coraza.Rule, data string) error {
+func (a *maturityFn) Init(r rules.RuleMetadata, data string) error {
 	m, err := strconv.Atoi(data)
 	if err != nil {
 		return err
@@ -22,23 +22,24 @@ func (a *maturityFn) Init(r *coraza.Rule, data string) error {
 	if m < 1 || m > 9 {
 		return fmt.Errorf("maturity must be between 1 and 9, not %d", m)
 	}
-	r.Maturity = m
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Maturity = m
 	return nil
 }
 
-func (a *maturityFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *maturityFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *maturityFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *maturityFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func maturity() coraza.RuleAction {
+func maturity() rules.Action {
 	return &maturityFn{}
 }
 
 var (
-	_ coraza.RuleAction = &maturityFn{}
+	_ rules.Action      = &maturityFn{}
 	_ ruleActionWrapper = maturity
 )
diff --git a/actions/msg.go b/actions/msg.go
index a7e9e81..69328eb 100644
--- a/actions/msg.go
+++ b/actions/msg.go
@@ -4,37 +4,40 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type msgFn struct {
 }
 
-func (a *msgFn) Init(r *coraza.Rule, data string) error {
+func (a *msgFn) Init(r rules.RuleMetadata, data string) error {
 	data = utils.MaybeRemoveQuotes(data)
-	msg, err := coraza.NewMacro(data)
+	msg, err := macro.NewMacro(data)
+
 	if err != nil {
 		return err
 	}
-	r.Msg = *msg
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Msg = msg
 	return nil
 }
 
-func (a *msgFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *msgFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *msgFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *msgFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func msg() coraza.RuleAction {
+func msg() rules.Action {
 	return &msgFn{}
 }
 
 var (
-	_ coraza.RuleAction = &msgFn{}
+	_ rules.Action      = &msgFn{}
 	_ ruleActionWrapper = msg
 )
diff --git a/actions/multimatch.go b/actions/multimatch.go
index b913ad1..eeec292 100644
--- a/actions/multimatch.go
+++ b/actions/multimatch.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type multimatchFn struct {
 }
 
-func (a *multimatchFn) Init(r *coraza.Rule, data string) error {
-	r.MultiMatch = true
+func (a *multimatchFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).MultiMatch = true
 	return nil
 }
 
-func (a *multimatchFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *multimatchFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *multimatchFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *multimatchFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func multimatch() coraza.RuleAction {
+func multimatch() rules.Action {
 	return &multimatchFn{}
 }
 
 var (
-	_ coraza.RuleAction = &multimatchFn{}
+	_ rules.Action      = &multimatchFn{}
 	_ ruleActionWrapper = multimatch
 )
diff --git a/actions/noauditlog.go b/actions/noauditlog.go
index a7108c4..9a937a6 100644
--- a/actions/noauditlog.go
+++ b/actions/noauditlog.go
@@ -4,30 +4,31 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type noauditlogFn struct {
 }
 
-func (a *noauditlogFn) Init(r *coraza.Rule, data string) error {
-	r.Audit = false
+func (a *noauditlogFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Audit = false
 	return nil
 }
 
-func (a *noauditlogFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *noauditlogFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 }
 
-func (a *noauditlogFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *noauditlogFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func noauditlog() coraza.RuleAction {
+func noauditlog() rules.Action {
 	return &noauditlogFn{}
 }
 
 var (
-	_ coraza.RuleAction = &noauditlogFn{}
+	_ rules.Action      = &noauditlogFn{}
 	_ ruleActionWrapper = noauditlog
 )
diff --git a/actions/nolog.go b/actions/nolog.go
index 2cfe030..897c85a 100644
--- a/actions/nolog.go
+++ b/actions/nolog.go
@@ -4,32 +4,35 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type nologFn struct {
 }
 
-func (a *nologFn) Init(r *coraza.Rule, data string) error {
-	r.Log = false
-	r.Audit = false
+func (a *nologFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Log = false
+	r.(*corazawaf.Rule).Audit = false
 	return nil
 }
 
-func (a *nologFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	r.Audit = false
+func (a *nologFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	// TODO(anuraaga): Confirm this is actually needed.
+	r.(*corazawaf.Rule).Audit = false
 }
 
-func (a *nologFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *nologFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func nolog() coraza.RuleAction {
+func nolog() rules.Action {
 	return &nologFn{}
 }
 
 var (
-	_ coraza.RuleAction = &nologFn{}
+	_ rules.Action      = &nologFn{}
 	_ ruleActionWrapper = nolog
 )
diff --git a/actions/pass.go b/actions/pass.go
index cf6c183..d9ad102 100644
--- a/actions/pass.go
+++ b/actions/pass.go
@@ -4,30 +4,29 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type passFn struct {
 }
 
-func (a *passFn) Init(r *coraza.Rule, data string) error {
+func (a *passFn) Init(r rules.RuleMetadata, data string) error {
 	return nil
 }
 
-func (a *passFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *passFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *passFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *passFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func pass() coraza.RuleAction {
+func pass() rules.Action {
 	return &passFn{}
 }
 
 var (
-	_ coraza.RuleAction = &passFn{}
+	_ rules.Action      = &passFn{}
 	_ ruleActionWrapper = pass
 )
diff --git a/actions/phase.go b/actions/phase.go
index 945151c..1ab76f9 100644
--- a/actions/phase.go
+++ b/actions/phase.go
@@ -4,34 +4,36 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 type phaseFn struct{}
 
-func (a *phaseFn) Init(r *coraza.Rule, data string) error {
+func (a *phaseFn) Init(r rules.RuleMetadata, data string) error {
 	p, err := types.ParseRulePhase(data)
 	if err != nil {
 		return err
 	}
-	r.Phase = p
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Phase = p
 	return nil
 }
 
-func (a *phaseFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *phaseFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *phaseFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *phaseFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func phase() coraza.RuleAction {
+func phase() rules.Action {
 	return &phaseFn{}
 }
 
 var (
-	_ coraza.RuleAction = &phaseFn{}
+	_ rules.Action      = &phaseFn{}
 	_ ruleActionWrapper = phase
 )
diff --git a/actions/prepend.go b/actions/prepend.go
index 8f2b631..ff9f4c3 100644
--- a/actions/prepend.go
+++ b/actions/prepend.go
@@ -6,30 +6,34 @@ package actions
 import (
 	"io"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 type prependFn struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-func (a *prependFn) Init(r *coraza.Rule, data string) error {
-	macro, err := coraza.NewMacro(data)
+func (a *prependFn) Init(r rules.RuleMetadata, data string) error {
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	a.data = *macro
+	a.data = m
 	return nil
 }
 
-func (a *prependFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *prependFn) Evaluate(r rules.RuleMetadata, txS rules.TransactionState) {
+	// TODO(anuraaga): This is quite complicated. Evaluate whether plugin API needs to support this.
+	tx := txS.(*corazawaf.Transaction)
 	if !tx.WAF.ContentInjection {
 		tx.WAF.Logger.Debug("append rejected because of ContentInjection")
 		return
 	}
 	data := a.data.Expand(tx)
-	buf := coraza.NewBodyBuffer(types.BodyBufferOptions{
+	buf := corazawaf.NewBodyBuffer(types.BodyBufferOptions{
 		TmpPath:     tx.WAF.TmpDir,
 		MemoryLimit: tx.WAF.RequestBodyInMemoryLimit,
 	})
@@ -51,15 +55,15 @@ func (a *prependFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
 	// Maybe in the future we could add the prepend function to the BodyBuffer
 }
 
-func (a *prependFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *prependFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func prepend() coraza.RuleAction {
+func prepend() rules.Action {
 	return &prependFn{}
 }
 
 var (
-	_ coraza.RuleAction = &prependFn{}
+	_ rules.Action      = &prependFn{}
 	_ ruleActionWrapper = prepend
 )
diff --git a/actions/redirect.go b/actions/redirect.go
index 44a1038..1d9a81f 100644
--- a/actions/redirect.go
+++ b/actions/redirect.go
@@ -17,7 +17,7 @@ package actions
 import (
 	"fmt"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -25,40 +25,37 @@ type redirectFn struct {
 	target string
 }
 
-func (a *redirectFn) Init(r *coraza.Rule, data string) error {
+func (a *redirectFn) Init(r rules.RuleMetadata, data string) error {
 	if data == "" {
 		return fmt.Errorf("redirect action requires a parameter")
 	}
 
 	a.target = data
-	r.Disruptive = true
 	return nil
 }
 
-func (a *redirectFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	rid := r.ID
+func (a *redirectFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	rid := r.GetID()
 	if rid == 0 {
-		rid = r.ParentID
-	}
-	if tx.RuleEngine == types.RuleEngineOn {
-		tx.Interruption = &types.Interruption{
-			Status: r.DisruptiveStatus,
-			RuleID: rid,
-			Action: "redirect",
-			Data:   a.target,
-		}
+		rid = r.GetParentID()
 	}
+	tx.Interrupt(&types.Interruption{
+		Status: r.Status(),
+		RuleID: rid,
+		Action: "redirect",
+		Data:   a.target,
+	})
 }
 
-func (a *redirectFn) Type() types.RuleActionType {
-	return types.ActionTypeDisruptive
+func (a *redirectFn) Type() rules.ActionType {
+	return rules.ActionTypeDisruptive
 }
 
-func redirect() coraza.RuleAction {
+func redirect() rules.Action {
 	return &redirectFn{}
 }
 
 var (
-	_ coraza.RuleAction = &redirectFn{}
+	_ rules.Action      = &redirectFn{}
 	_ ruleActionWrapper = redirect
 )
diff --git a/actions/rev.go b/actions/rev.go
index 48446df..595a18e 100644
--- a/actions/rev.go
+++ b/actions/rev.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type revFn struct {
 }
 
-func (a *revFn) Init(r *coraza.Rule, data string) error {
-	r.Rev = data
+func (a *revFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Rev = data
 	return nil
 }
 
-func (a *revFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *revFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *revFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *revFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func rev() coraza.RuleAction {
+func rev() rules.Action {
 	return &revFn{}
 }
 
 var (
-	_ coraza.RuleAction = &revFn{}
+	_ rules.Action      = &revFn{}
 	_ ruleActionWrapper = rev
 )
diff --git a/actions/setenv.go b/actions/setenv.go
index b378241..ccdfed4 100644
--- a/actions/setenv.go
+++ b/actions/setenv.go
@@ -8,49 +8,49 @@ import (
 	"os"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type setenvFn struct {
 	key   string
-	value coraza.Macro
+	value macro.Macro
 }
 
-func (a *setenvFn) Init(r *coraza.Rule, data string) error {
+func (a *setenvFn) Init(r rules.RuleMetadata, data string) error {
 	spl := strings.SplitN(data, "=", 2)
 	if len(spl) != 2 {
 		return fmt.Errorf("invalid key value for setvar")
 	}
 	a.key = spl[0]
-	macro, err := coraza.NewMacro(spl[1])
+	m, err := macro.NewMacro(spl[1])
 	if err != nil {
 		return err
 	}
-	a.value = *macro
+	a.value = m
 	return nil
 }
 
-func (a *setenvFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *setenvFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	v := a.value.Expand(tx)
 	// set env variable
 	if err := os.Setenv(a.key, v); err != nil {
-		tx.WAF.Logger.Error("[%s] Error setting env variable for rule %d: %s", tx.ID, r.ID, err.Error())
+		tx.DebugLogger().Error("[%s] Error setting env variable for rule %d: %s", tx.GetID(), r.GetID(), err.Error())
 	}
 	// TODO is this ok?
-	tx.Variables.Env.Set(a.key, []string{v})
+	tx.GetVariables().GetEnv().Set(a.key, []string{v})
 
 }
 
-func (a *setenvFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *setenvFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func setenv() coraza.RuleAction {
+func setenv() rules.Action {
 	return &setenvFn{}
 }
 
 var (
-	_ coraza.RuleAction = &setenvFn{}
+	_ rules.Action      = &setenvFn{}
 	_ ruleActionWrapper = setenv
 )
diff --git a/actions/setvar.go b/actions/setvar.go
index 2ed7467..a32a06f 100644
--- a/actions/setvar.go
+++ b/actions/setvar.go
@@ -8,20 +8,20 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
 	"github.com/corazawaf/coraza/v3/collection"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
 type setvarFn struct {
-	key        coraza.Macro
-	value      coraza.Macro
+	key        macro.Macro
+	value      macro.Macro
 	collection variables.RuleVariable
 	isRemove   bool
 }
 
-func (a *setvarFn) Init(r *coraza.Rule, data string) error {
+func (a *setvarFn) Init(r rules.RuleMetadata, data string) error {
 	if data == "" {
 		return fmt.Errorf("setvar requires arguments")
 	}
@@ -40,35 +40,35 @@ func (a *setvarFn) Init(r *coraza.Rule, data string) error {
 		return err
 	}
 	if len(splcol) == 2 {
-		macro, err := coraza.NewMacro(splcol[1])
+		macro, err := macro.NewMacro(splcol[1])
 		if err != nil {
 			return err
 		}
-		a.key = *macro
+		a.key = macro
 	}
 	if len(spl) == 2 {
-		macro, err := coraza.NewMacro(spl[1])
+		macro, err := macro.NewMacro(spl[1])
 		if err != nil {
 			return err
 		}
-		a.value = *macro
+		a.value = macro
 	}
 	return nil
 }
 
-func (a *setvarFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *setvarFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	key := a.key.Expand(tx)
 	value := a.value.Expand(tx)
-	tx.WAF.Logger.Debug("[%s] Setting var %q to %q by rule %d", tx.ID, key, value, r.ID)
+	tx.DebugLogger().Debug("[%s] Setting var %q to %q by rule %d", tx.GetID(), key, value, r.GetID())
 	a.evaluateTxCollection(r, tx, strings.ToLower(key), value)
 }
 
-func (a *setvarFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *setvarFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func (a *setvarFn) evaluateTxCollection(r *coraza.Rule, tx *coraza.Transaction, key string, value string) {
-	col := (tx.Collections[a.collection]).(*collection.Map)
+func (a *setvarFn) evaluateTxCollection(r rules.RuleMetadata, tx rules.TransactionState, key string, value string) {
+	col := (tx.Collection(a.collection)).(*collection.Map)
 	if col == nil {
 		// fmt.Println("Invalid Collection " + a.Collection) LOG error?
 		return
@@ -93,7 +93,7 @@ func (a *setvarFn) evaluateTxCollection(r *coraza.Rule, tx *coraza.Transaction,
 		if len(value) > 1 {
 			sum, err = strconv.Atoi(value[1:])
 			if err != nil {
-				tx.WAF.Logger.Error("[%s] Invalid value for setvar %q on rule %d", tx.ID, value, r.ID)
+				tx.DebugLogger().Error("[%s] Invalid value for setvar %q on rule %d", tx.GetID(), value, r.GetID())
 				return
 			}
 		}
@@ -101,7 +101,7 @@ func (a *setvarFn) evaluateTxCollection(r *coraza.Rule, tx *coraza.Transaction,
 		if res != "" {
 			val, err = strconv.Atoi(res)
 			if err != nil {
-				tx.WAF.Logger.Error("[%s] Invalid value for setvar %q on rule %d", tx.ID, res, r.ID)
+				tx.DebugLogger().Error("[%s] Invalid value for setvar %q on rule %d", tx.GetID(), res, r.GetID())
 				return
 			}
 		}
@@ -118,11 +118,11 @@ func (a *setvarFn) evaluateTxCollection(r *coraza.Rule, tx *coraza.Transaction,
 	}
 }
 
-func setvar() coraza.RuleAction {
+func setvar() rules.Action {
 	return &setvarFn{}
 }
 
 var (
-	_ coraza.RuleAction = &setvarFn{}
+	_ rules.Action      = &setvarFn{}
 	_ ruleActionWrapper = setvar
 )
diff --git a/actions/severity.go b/actions/severity.go
index 8cfbc15..6c38bed 100644
--- a/actions/severity.go
+++ b/actions/severity.go
@@ -4,35 +4,37 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 type severityFn struct {
 }
 
-func (a *severityFn) Init(r *coraza.Rule, data string) error {
+func (a *severityFn) Init(r rules.RuleMetadata, data string) error {
 	sev, err := types.ParseRuleSeverity(data)
 	if err != nil {
 		return err
 	}
-	r.Severity = sev
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Severity = sev
 	return nil
 }
 
-func (a *severityFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *severityFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *severityFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *severityFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func severity() coraza.RuleAction {
+func severity() rules.Action {
 	return &severityFn{}
 }
 
 var (
-	_ coraza.RuleAction = &severityFn{}
+	_ rules.Action      = &severityFn{}
 	_ ruleActionWrapper = severity
 )
diff --git a/actions/skip.go b/actions/skip.go
index cab2487..8c271e9 100644
--- a/actions/skip.go
+++ b/actions/skip.go
@@ -7,15 +7,15 @@ import (
 	"fmt"
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type skipFn struct {
 	data int
 }
 
-func (a *skipFn) Init(r *coraza.Rule, data string) error {
+func (a *skipFn) Init(r rules.RuleMetadata, data string) error {
 	i, err := strconv.Atoi(data)
 	if err != nil {
 		return fmt.Errorf("invalid value for skip")
@@ -27,19 +27,20 @@ func (a *skipFn) Init(r *coraza.Rule, data string) error {
 	return nil
 }
 
-func (a *skipFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	tx.Skip = a.data
+func (a *skipFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	tx.(*corazawaf.Transaction).Skip = a.data
 }
 
-func (a *skipFn) Type() types.RuleActionType {
-	return types.ActionTypeFlow
+func (a *skipFn) Type() rules.ActionType {
+	return rules.ActionTypeFlow
 }
 
-func skip() coraza.RuleAction {
+func skip() rules.Action {
 	return &skipFn{}
 }
 
 var (
-	_ coraza.RuleAction = &skipFn{}
+	_ rules.Action      = &skipFn{}
 	_ ruleActionWrapper = skip
 )
diff --git a/actions/skipafter.go b/actions/skipafter.go
index 4c5a285..73dc1dd 100644
--- a/actions/skipafter.go
+++ b/actions/skipafter.go
@@ -6,33 +6,34 @@ package actions
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type skipafterFn struct {
 	data string
 }
 
-func (a *skipafterFn) Init(r *coraza.Rule, data string) error {
+func (a *skipafterFn) Init(r rules.RuleMetadata, data string) error {
 	a.data = strings.Trim(data, `"`)
 	return nil
 }
 
-func (a *skipafterFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
-	tx.WAF.Logger.Debug("[%s] Starting secmarker %q", tx.ID, a.data)
-	tx.SkipAfter = a.data
+func (a *skipafterFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
+	tx.DebugLogger().Debug("[%s] Starting secmarker %q", tx.GetID(), a.data)
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	tx.(*corazawaf.Transaction).SkipAfter = a.data
 }
 
-func (a *skipafterFn) Type() types.RuleActionType {
-	return types.ActionTypeFlow
+func (a *skipafterFn) Type() rules.ActionType {
+	return rules.ActionTypeFlow
 }
 
-func skipafter() coraza.RuleAction {
+func skipafter() rules.Action {
 	return &skipafterFn{}
 }
 
 var (
-	_ coraza.RuleAction = &skipafterFn{}
+	_ rules.Action      = &skipafterFn{}
 	_ ruleActionWrapper = skipafter
 )
diff --git a/actions/status.go b/actions/status.go
index 089770d..b1852b5 100644
--- a/actions/status.go
+++ b/actions/status.go
@@ -6,34 +6,35 @@ package actions
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type statusFn struct {
 }
 
-func (a *statusFn) Init(r *coraza.Rule, b1 string) error {
+func (a *statusFn) Init(r rules.RuleMetadata, b1 string) error {
 	status, err := strconv.Atoi(b1)
 	if err != nil {
 		return err
 	}
-	r.DisruptiveStatus = status
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).DisruptiveStatus = status
 	return nil
 }
 
-func (a *statusFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *statusFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 }
 
-func (a *statusFn) Type() types.RuleActionType {
-	return types.ActionTypeData
+func (a *statusFn) Type() rules.ActionType {
+	return rules.ActionTypeData
 }
 
-func status() coraza.RuleAction {
+func status() rules.Action {
 	return &statusFn{}
 }
 
 var (
-	_ coraza.RuleAction = &statusFn{}
+	_ rules.Action      = &statusFn{}
 	_ ruleActionWrapper = status
 )
diff --git a/actions/t.go b/actions/t.go
index a66f3e2..2cf485e 100644
--- a/actions/t.go
+++ b/actions/t.go
@@ -4,41 +4,42 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 	transformations "github.com/corazawaf/coraza/v3/transformations"
-	"github.com/corazawaf/coraza/v3/types"
 )
 
 type tFn struct{}
 
-func (a *tFn) Init(r *coraza.Rule, input string) error {
+func (a *tFn) Init(r rules.RuleMetadata, input string) error {
 	// TODO there is a chance that it won't work, it requires tests
 	// none is a special hardcoded transformation, it must remove previous transformations
 	if input == "none" {
 		// remove elements
-		r.ClearTransformations()
+		// TODO(anuraaga): Confirm this is internal implementation detail
+		r.(*corazawaf.Rule).ClearTransformations()
 		return nil
 	}
 	tt, err := transformations.GetTransformation(input)
 	if err != nil {
 		return err
 	}
-	return r.AddTransformation(input, tt)
+	return r.(*corazawaf.Rule).AddTransformation(input, tt)
 }
 
-func (a *tFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *tFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *tFn) Type() types.RuleActionType {
-	return types.ActionTypeNondisruptive
+func (a *tFn) Type() rules.ActionType {
+	return rules.ActionTypeNondisruptive
 }
 
-func t() coraza.RuleAction {
+func t() rules.Action {
 	return &tFn{}
 }
 
 var (
-	_ coraza.RuleAction = &tFn{}
+	_ rules.Action      = &tFn{}
 	_ ruleActionWrapper = t
 )
diff --git a/actions/tag.go b/actions/tag.go
index 2800c30..5f56857 100644
--- a/actions/tag.go
+++ b/actions/tag.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type tagFn struct {
 }
 
-func (a *tagFn) Init(r *coraza.Rule, data string) error {
-	r.Tags = append(r.Tags, data)
+func (a *tagFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Tags = append(r.(*corazawaf.Rule).Tags, data)
 	return nil
 }
 
-func (a *tagFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *tagFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *tagFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *tagFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func tag() coraza.RuleAction {
+func tag() rules.Action {
 	return &tagFn{}
 }
 
 var (
-	_ coraza.RuleAction = &tagFn{}
+	_ rules.Action      = &tagFn{}
 	_ ruleActionWrapper = tag
 )
diff --git a/actions/ver.go b/actions/ver.go
index f636b65..ba1c221 100644
--- a/actions/ver.go
+++ b/actions/ver.go
@@ -4,31 +4,32 @@
 package actions
 
 import (
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type verFn struct {
 }
 
-func (a *verFn) Init(r *coraza.Rule, data string) error {
-	r.Version = data
+func (a *verFn) Init(r rules.RuleMetadata, data string) error {
+	// TODO(anuraaga): Confirm this is internal implementation detail
+	r.(*corazawaf.Rule).Version = data
 	return nil
 }
 
-func (a *verFn) Evaluate(r *coraza.Rule, tx *coraza.Transaction) {
+func (a *verFn) Evaluate(r rules.RuleMetadata, tx rules.TransactionState) {
 	// Not evaluated
 }
 
-func (a *verFn) Type() types.RuleActionType {
-	return types.ActionTypeMetadata
+func (a *verFn) Type() rules.ActionType {
+	return rules.ActionTypeMetadata
 }
 
-func ver() coraza.RuleAction {
+func ver() rules.Action {
 	return &verFn{}
 }
 
 var (
-	_ coraza.RuleAction = &verFn{}
+	_ rules.Action      = &verFn{}
 	_ ruleActionWrapper = ver
 )
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..fac463d
--- /dev/null
+++ b/config.go
@@ -0,0 +1,260 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package coraza
+
+import (
+	"io/fs"
+
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/types"
+)
+
+// WAFConfig controls the behavior of the WAF.
+//
+// Note: WAFConfig is immutable. Each WithXXX function returjns a new instance including the corresponding change.
+type WAFConfig interface {
+	// WithRule adds a rule to the WAF.
+	WithRule(rule *corazawaf.Rule) WAFConfig
+
+	// WithDirectives parses the directives from the given string and adds them to the WAF.
+	WithDirectives(directives string) WAFConfig
+
+	// WithDirectivesFromFile parses the directives from the given file and adds them to the WAF.
+	WithDirectivesFromFile(path string) WAFConfig
+
+	// WithAuditLog configures audit logging.
+	WithAuditLog(config AuditLogConfig) WAFConfig
+
+	// WithContentInjection enables content injection.
+	WithContentInjection() WAFConfig
+
+	// WithRequestBodyAccess configures access to the request body.
+	WithRequestBodyAccess(config RequestBodyConfig) WAFConfig
+
+	// WithResponseBodyAccess configures access to the response body.
+	WithResponseBodyAccess(config ResponseBodyConfig) WAFConfig
+
+	// WithDebugLogger configures a debug logger.
+	WithDebugLogger(logger loggers.DebugLogger) WAFConfig
+
+	// WithErrorLogger configures an error logger.
+	WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig
+
+	// WithRootFS configures the root file system.
+	WithRootFS(fs fs.FS) WAFConfig
+}
+
+// NewWAFConfig creates a new WAFConfig with the default settings.
+func NewWAFConfig() WAFConfig {
+	return &wafConfig{}
+}
+
+// RequestBodyConfig controls access to the request body.
+type RequestBodyConfig interface {
+	// WithLimit sets the maximum number of bytes that can be read from the request body. Bytes beyond that set
+	// in WithInMemoryLimit will be buffered to disk.
+	WithLimit(limit int) RequestBodyConfig
+
+	// WithInMemoryLimit sets the maximum number of bytes that can be read from the request body and buffered in memory.
+	WithInMemoryLimit(limit int) RequestBodyConfig
+}
+
+// NewRequestBodyConfig returns a new RequestBodyConfig with the default settings.
+func NewRequestBodyConfig() RequestBodyConfig {
+	return &requestBodyConfig{}
+}
+
+// ResponseBodyConfig controls access to the response body.
+type ResponseBodyConfig interface {
+	// WithLimit sets the maximum number of bytes that can be read from the response body and buffered in memory.
+	WithLimit(limit int) ResponseBodyConfig
+
+	// WithMimeTypes sets the mime types of responses that will be processed.
+	WithMimeTypes(mimeTypes []string) ResponseBodyConfig
+}
+
+// NewResponseBodyConfig returns a new ResponseBodyConfig with the default settings.
+func NewResponseBodyConfig() ResponseBodyConfig {
+	return &responseBodyConfig{}
+}
+
+// AuditLogConfig controls audit logging.
+type AuditLogConfig interface {
+	// LogRelevantOnly enables audit logging only for relevant events.
+	LogRelevantOnly() AuditLogConfig
+
+	// WithParts configures the parts of the request/response to be logged.
+	WithParts(parts types.AuditLogParts) AuditLogConfig
+
+	// WithLogger configures the loggers.LogWriter to write logs to.
+	WithLogger(logger loggers.LogWriter) AuditLogConfig
+}
+
+// NewAuditLogConfig returns a new AuditLogConfig with the default settings.
+func NewAuditLogConfig() AuditLogConfig {
+	return &auditLogConfig{}
+}
+
+type wafRule struct {
+	rule *corazawaf.Rule
+	str  string
+	file string
+}
+
+type wafConfig struct {
+	rules            []wafRule
+	auditLog         *auditLogConfig
+	contentInjection bool
+	requestBody      *requestBodyConfig
+	responseBody     *responseBodyConfig
+	debugLogger      loggers.DebugLogger
+	errorLogger      corazawaf.ErrorLogCallback
+	fsRoot           fs.FS
+}
+
+func (c *wafConfig) WithRule(rule *corazawaf.Rule) WAFConfig {
+	ret := c.clone()
+	ret.rules = append(ret.rules, wafRule{rule: rule})
+	return ret
+}
+
+func (c *wafConfig) WithDirectivesFromFile(path string) WAFConfig {
+	ret := c.clone()
+	ret.rules = append(ret.rules, wafRule{file: path})
+	return ret
+}
+
+func (c *wafConfig) WithDirectives(directives string) WAFConfig {
+	ret := c.clone()
+	ret.rules = append(ret.rules, wafRule{str: directives})
+	return ret
+}
+
+func (c *wafConfig) WithAuditLog(config AuditLogConfig) WAFConfig {
+	ret := c.clone()
+	ret.auditLog = config.(*auditLogConfig)
+	return ret
+}
+
+func (c *wafConfig) WithContentInjection() WAFConfig {
+	ret := c.clone()
+	ret.contentInjection = true
+	return ret
+}
+
+func (c *wafConfig) WithRequestBodyAccess(config RequestBodyConfig) WAFConfig {
+	ret := c.clone()
+	ret.requestBody = config.(*requestBodyConfig)
+	return ret
+}
+
+func (c *wafConfig) WithResponseBodyAccess(config ResponseBodyConfig) WAFConfig {
+	ret := c.clone()
+	ret.responseBody = config.(*responseBodyConfig)
+	return ret
+}
+
+func (c *wafConfig) WithDebugLogger(logger loggers.DebugLogger) WAFConfig {
+	ret := c.clone()
+	ret.debugLogger = logger
+	return ret
+}
+
+func (c *wafConfig) WithErrorLogger(logger corazawaf.ErrorLogCallback) WAFConfig {
+	ret := c.clone()
+	ret.errorLogger = logger
+	return ret
+}
+
+func (c *wafConfig) WithRootFS(fs fs.FS) WAFConfig {
+	ret := c.clone()
+	ret.fsRoot = fs
+	return ret
+}
+
+func (c *wafConfig) clone() *wafConfig {
+	ret := *c // copy
+	return &ret
+}
+
+type requestBodyConfig struct {
+	limit         int
+	inMemoryLimit int
+}
+
+func (c *requestBodyConfig) WithLimit(limit int) RequestBodyConfig {
+	ret := c.clone()
+	ret.limit = limit
+	return ret
+}
+
+func (c *requestBodyConfig) WithInMemoryLimit(limit int) RequestBodyConfig {
+	ret := c.clone()
+	ret.inMemoryLimit = limit
+	return ret
+}
+
+func (c *requestBodyConfig) clone() *requestBodyConfig {
+	ret := *c // copy
+	return &ret
+}
+
+type responseBodyConfig struct {
+	limit         int
+	inMemoryLimit int
+	mimeTypes     []string
+}
+
+func (c *responseBodyConfig) WithLimit(limit int) ResponseBodyConfig {
+	ret := c.clone()
+	ret.limit = limit
+	return ret
+}
+
+func (c *responseBodyConfig) WithInMemoryLimit(limit int) ResponseBodyConfig {
+	ret := c.clone()
+	ret.inMemoryLimit = limit
+	return ret
+}
+
+func (c *responseBodyConfig) WithMimeTypes(mimeTypes []string) ResponseBodyConfig {
+	ret := c.clone()
+	ret.mimeTypes = mimeTypes
+	return ret
+}
+
+func (c *responseBodyConfig) clone() *responseBodyConfig {
+	ret := *c // copy
+	return &ret
+}
+
+type auditLogConfig struct {
+	relevantOnly bool
+	parts        types.AuditLogParts
+	logger       loggers.LogWriter
+}
+
+func (c *auditLogConfig) LogRelevantOnly() AuditLogConfig {
+	ret := c.clone()
+	c.relevantOnly = true
+	return ret
+}
+
+func (c *auditLogConfig) WithParts(parts types.AuditLogParts) AuditLogConfig {
+	ret := c.clone()
+	ret.parts = parts
+	return ret
+}
+
+func (c *auditLogConfig) WithLogger(logger loggers.LogWriter) AuditLogConfig {
+	ret := c.clone()
+	ret.logger = logger
+	return ret
+}
+
+func (c *auditLogConfig) clone() *auditLogConfig {
+	ret := *c // copy
+	return &ret
+}
diff --git a/examples/http-server/interceptor.go b/examples/http-server/interceptor.go
index 700a80c..2edba82 100644
--- a/examples/http-server/interceptor.go
+++ b/examples/http-server/interceptor.go
@@ -3,12 +3,12 @@ package main
 import (
 	"net/http"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/types"
 )
 
 type interceptor struct {
 	origWriter  http.ResponseWriter
-	tx          *coraza.Transaction
+	tx          types.Transaction
 	headersSent bool
 }
 
@@ -30,7 +30,7 @@ func (i *interceptor) WriteHeader(rc int) {
 }
 
 func (i *interceptor) Write(b []byte) (int, error) {
-	return i.tx.ResponseBodyBuffer.Write(b)
+	return i.tx.ResponseBodyWriter().Write(b)
 }
 
 func (i *interceptor) Header() http.Header {
diff --git a/examples/http-server/main.go b/examples/http-server/main.go
index 1249d69..646af8b 100644
--- a/examples/http-server/main.go
+++ b/examples/http-server/main.go
@@ -8,50 +8,48 @@ import (
 
 	"github.com/corazawaf/coraza/v3"
 	txhttp "github.com/corazawaf/coraza/v3/http"
-	"github.com/corazawaf/coraza/v3/seclang"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
-var waf *coraza.WAF
-
 func hello(w http.ResponseWriter, req *http.Request) {
 	fmt.Fprintf(w, "hello world, not disrupted.\n")
 }
 
 func main() {
-	if err := setupCoraza(); err != nil {
+	waf, err := setupCoraza()
+	if err != nil {
 		panic(err)
 	}
-	http.Handle("/hello", corazaRequestHandler(http.HandlerFunc(hello)))
+	http.Handle("/hello", corazaRequestHandler(waf, http.HandlerFunc(hello)))
 
 	fmt.Println("Server is running. Listening port: 8090")
 	panic(http.ListenAndServe(":8090", nil))
 }
 
-func setupCoraza() error {
-	waf = coraza.NewWAF()
-	waf.SetDebugLogLevel(9)
-	seclang := seclang.NewParser(waf)
-	if err := seclang.FromString(`
+func setupCoraza() (coraza.WAF, error) {
+	waf, err := coraza.NewWAF(coraza.NewWAFConfig().
+		WithDirectives(`
 		# This is a comment
+		SecDebugLogLevel 9
 		SecRequestBodyAccess On
 		SecRule ARGS:id "@eq 0" "id:1, phase:1,deny, status:403,msg:'Invalid id',log,auditlog"
 		SecRule REQUEST_BODY "somecontent" "id:100, phase:2,deny, status:403,msg:'Invalid request body',log,auditlog"
 		SecRule RESPONSE_BODY "somecontent" "id:200, phase:4,deny, status:403,msg:'Invalid response body',log,auditlog"
-	`); err != nil {
-		return err
+	`))
+	if err != nil {
+		return nil, err
 	}
-	return nil
+	return waf, err
 }
 
-func corazaRequestHandler(h http.Handler) http.Handler {
+func corazaRequestHandler(waf coraza.WAF, h http.Handler) http.Handler {
 	fn := func(w http.ResponseWriter, r *http.Request) {
 		tx := waf.NewTransaction(context.Background())
 		defer func() {
 			// We run phase 5 rules and create audit logs (if enabled)
 			tx.ProcessLogging()
 			// we remove temporary files and free some memory
-			if err := tx.Clean(); err != nil {
+			if err := tx.Close(); err != nil {
 				fmt.Println(err)
 			}
 		}()
@@ -82,7 +80,7 @@ func corazaRequestHandler(h http.Handler) http.Handler {
 			return
 		}
 		// we release the buffer
-		reader, err := tx.ResponseBodyBuffer.Reader()
+		reader, err := tx.ResponseBodyReader()
 		if err != nil {
 			showCorazaError(w, 500, err.Error())
 			return
diff --git a/http/http.go b/http/http.go
index 4cedfc7..6fdc972 100644
--- a/http/http.go
+++ b/http/http.go
@@ -13,7 +13,6 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -22,7 +21,7 @@ import (
 // so this will implement all phase 0, 1 and 2 variables
 // Note: This function will stop after an interruption
 // Note: Do not manually fill any request variables
-func ProcessRequest(tx *coraza.Transaction, req *http.Request) (*types.Interruption, error) {
+func ProcessRequest(tx types.Transaction, req *http.Request) (*types.Interruption, error) {
 	var client string
 	cport := 0
 	// IMPORTANT: Some http.Request.RemoteAddr implementations will not contain port or contain IPV6: [2001:db8::1]:8080
@@ -50,13 +49,13 @@ func ProcessRequest(tx *coraza.Transaction, req *http.Request) (*types.Interrupt
 		return in, nil
 	}
 	if req.Body != nil {
-		_, err := io.Copy(tx.RequestBodyBuffer, req.Body)
+		_, err := io.Copy(tx.RequestBodyWriter(), req.Body)
 		if err != nil {
-			return tx.Interruption, err
+			return tx.GetInterruption(), err
 		}
-		reader, err := tx.RequestBodyBuffer.Reader()
+		reader, err := tx.RequestBodyReader()
 		if err != nil {
-			return tx.Interruption, err
+			return tx.GetInterruption(), err
 		}
 		req.Body = io.NopCloser(reader)
 	}
diff --git a/body_buffer.go b/internal/corazawaf/body_buffer.go
similarity index 99%
rename from body_buffer.go
rename to internal/corazawaf/body_buffer.go
index 229ab11..b8e9bce 100644
--- a/body_buffer.go
+++ b/internal/corazawaf/body_buffer.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"bytes"
diff --git a/internal/corazawaf/logger.go b/internal/corazawaf/logger.go
new file mode 100644
index 0000000..84546da
--- /dev/null
+++ b/internal/corazawaf/logger.go
@@ -0,0 +1,69 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package corazawaf
+
+import (
+	"fmt"
+	"io"
+	"log"
+
+	"github.com/corazawaf/coraza/v3/loggers"
+)
+
+// DebugLogger is a logger that logs to the standard logger
+type stdDebugLogger struct {
+	logger *log.Logger
+	Level  loggers.LogLevel
+}
+
+func (l *stdDebugLogger) formatLog(level loggers.LogLevel, message string, args ...interface{}) {
+	if l.Level >= level {
+		l.logger.Printf("[%s] %s", level.String(), fmt.Sprintf(message, args...))
+	}
+}
+
+// Info logs an info message
+func (l *stdDebugLogger) Info(message string, args ...interface{}) {
+	l.formatLog(loggers.LogLevelInfo, message, args...)
+}
+
+// Warn logs a warning message
+func (l *stdDebugLogger) Warn(message string, args ...interface{}) {
+	l.formatLog(loggers.LogLevelWarn, message, args...)
+}
+
+// Error logs an error message
+func (l *stdDebugLogger) Error(message string, args ...interface{}) {
+	l.formatLog(loggers.LogLevelError, message, args...)
+}
+
+// Debug logs a debug message
+func (l *stdDebugLogger) Debug(message string, args ...interface{}) {
+	l.formatLog(loggers.LogLevelDebug, message, args...)
+}
+
+// Trace logs a trace message
+func (l *stdDebugLogger) Trace(message string, args ...interface{}) {
+	l.formatLog(loggers.LogLevelTrace, message, args...)
+}
+
+// SetLevel sets the log level
+func (l *stdDebugLogger) SetLevel(level loggers.LogLevel) {
+	if level.Invalid() {
+		l.Info("Invalid log level, defaulting to INFO")
+		level = loggers.LogLevelInfo
+	}
+	l.Level = level
+}
+
+// SetOutput sets the output for the logger
+func (l *stdDebugLogger) SetOutput(w io.Writer) {
+	l.logger.SetOutput(w)
+}
+
+// Close closes the logger
+func (l *stdDebugLogger) Close() error {
+	// TODO
+	return nil
+}
diff --git a/rule.go b/internal/corazawaf/rule.go
similarity index 83%
rename from rule.go
rename to internal/corazawaf/rule.go
index 2603264..f775d0e 100644
--- a/rule.go
+++ b/internal/corazawaf/rule.go
@@ -1,38 +1,20 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"fmt"
-	"io/fs"
 	"regexp"
 	"strconv"
 	"strings"
 
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-// RuleTransformation is used to create transformation plugins
-// See the documentation for more information
-// If a transformation fails to run it will return the same string
-// and an error, errors are only used for logging, it won't stop
-// the execution of the rule
-type RuleTransformation = func(input string) (string, error)
-
-// RuleAction is used to create Action plugins
-// See the documentation: https://www.coraza.io/docs/waf/actions
-type RuleAction interface {
-	// Init an action, will be done during compilation
-	Init(*Rule, string) error
-	// Evaluate will be done during rule evaluation
-	Evaluate(*Rule, *Transaction)
-	// Type will return the rule type, it's used by Evaluate
-	// to choose when to evaluate each action
-	Type() types.RuleActionType
-}
-
 // ruleActionParams is used as a wrapper to store the action name
 // and parameters, basically for logging purposes.
 type ruleActionParams struct {
@@ -43,39 +25,13 @@ type ruleActionParams struct {
 	Param string
 
 	// The action to be executed
-	Function RuleAction
+	Function rules.Action
 }
 
-// RuleOperatorOptions is used to store the options for a rule operator
-type RuleOperatorOptions struct {
-	// Arguments is used to store the operator args
-	Arguments string
-
-	// Path is used to store a list of possible data paths
-	Path []string
-
-	// Root is the root to resolve Path from.
-	Root fs.FS
-
-	// Datasets contains input datasets or dictionaries
-	Datasets map[string][]string
-}
-
-// RuleOperator interface is used to define rule @operators
-type RuleOperator interface {
-	// Init is used during compilation to setup and cache
-	// the operator
-	Init(RuleOperatorOptions) error
-	// Evaluate is used during the rule evaluation,
-	// it returns true if the operator succeeded against
-	// the input data for the transaction
-	Evaluate(*Transaction, string) bool
-}
-
-// RuleOperator is a container for an operator,
+// Operator is a container for an operator,
 type ruleOperatorParams struct {
 	// Operator to be used
-	Operator RuleOperator
+	Operator rules.Operator
 
 	// Function name (ex @rx)
 	Function string
@@ -125,7 +81,7 @@ type ruleTransformationParams struct {
 	Name string
 
 	// The transformation function to be used
-	Function RuleTransformation
+	Function rules.Transformation
 }
 
 // Rule is used to test a Transaction against certain operators
@@ -167,10 +123,10 @@ type Rule struct {
 	// Message text to be macro expanded and logged
 	// In future versions we might use a special type of string that
 	// supports cached macro expansions. For performance
-	Msg Macro
+	Msg macro.Macro
 
 	// Rule logdata
-	LogData Macro
+	LogData macro.Macro
 
 	// If true, triggering this rule write to the error log
 	Log bool
@@ -187,10 +143,26 @@ type Rule struct {
 	HasChain bool
 }
 
+func (r *Rule) GetID() int {
+	return r.ID
+}
+
+func (r *Rule) GetParentID() int {
+	return r.ParentID
+}
+
+func (r *Rule) Status() int {
+	return r.DisruptiveStatus
+}
+
 // Evaluate will evaluate the current rule for the indicated transaction
 // If the operator matches, actions will be evaluated, and it will return
 // the matched variables, keys and values (MatchData)
-func (r *Rule) Evaluate(tx *Transaction) []types.MatchData {
+func (r *Rule) Evaluate(tx rules.TransactionState) []types.MatchData {
+	return r.doEvaluate(tx.(*Transaction))
+}
+
+func (r *Rule) doEvaluate(tx *Transaction) []types.MatchData {
 	if r.Capture {
 		tx.Capture = true
 	}
@@ -205,9 +177,13 @@ func (r *Rule) Evaluate(tx *Transaction) []types.MatchData {
 	defer tx.WAF.Logger.Debug("[%s] [%d] Finish evaluating rule %d", tx.ID, rid, r.ID)
 	ruleCol := tx.Variables.Rule
 	ruleCol.SetIndex("id", 0, strconv.Itoa(rid))
-	ruleCol.SetIndex("msg", 0, r.Msg.String())
+	if r.Msg != nil {
+		ruleCol.SetIndex("msg", 0, r.Msg.String())
+	}
 	ruleCol.SetIndex("rev", 0, r.Rev)
-	ruleCol.SetIndex("logdata", 0, r.LogData.String())
+	if r.LogData != nil {
+		ruleCol.SetIndex("logdata", 0, r.LogData.String())
+	}
 	ruleCol.SetIndex("severity", 0, r.Severity.String())
 	// SecMark and SecAction uses nil operator
 	if r.operator == nil {
@@ -259,8 +235,12 @@ func (r *Rule) Evaluate(tx *Transaction) []types.MatchData {
 						// Set the txn variables for expansions before usage
 						r.matchVariable(tx, mr)
 
-						mr.Message = r.Msg.Expand(tx)
-						mr.Data = r.LogData.Expand(tx)
+						if r.Msg != nil {
+							mr.Message = r.Msg.Expand(tx)
+						}
+						if r.LogData != nil {
+							mr.Data = r.LogData.Expand(tx)
+						}
 						matchedValues = append(matchedValues, mr)
 
 						tx.WAF.Logger.Debug("[%s] [%d] Evaluating operator \"%s %s\" against %q: MATCH",
@@ -304,7 +284,7 @@ func (r *Rule) Evaluate(tx *Transaction) []types.MatchData {
 		if tx.RuleEngine != types.RuleEngineDetectionOnly {
 			tx.WAF.Logger.Debug("[%s] [%d] Disrupting transaction by rule %d", tx.ID, rid, r.ID)
 			for _, a := range r.actions {
-				if a.Function.Type() == types.ActionTypeDisruptive || a.Function.Type() == types.ActionTypeFlow {
+				if a.Function.Type() == rules.ActionTypeDisruptive || a.Function.Type() == rules.ActionTypeFlow {
 					tx.WAF.Logger.Debug("[%s] [%d] Evaluating action %s for rule %d", tx.ID, rid, a.Name, r.ID)
 					a.Function.Evaluate(r, tx)
 				}
@@ -332,7 +312,7 @@ func (r *Rule) matchVariable(tx *Transaction, m types.MatchData) {
 	// We run non-disruptive actions even if there is no chain match
 	tx.matchVariable(m)
 	for _, a := range r.actions {
-		if a.Function.Type() == types.ActionTypeNondisruptive {
+		if a.Function.Type() == rules.ActionTypeNondisruptive {
 			tx.WAF.Logger.Debug("[%s] [%d] Evaluating action %s for rule %d", tx.ID, rid, a.Name, r.ID)
 			a.Function.Evaluate(r, tx)
 		}
@@ -340,7 +320,7 @@ func (r *Rule) matchVariable(tx *Transaction, m types.MatchData) {
 }
 
 // AddAction adds an action to the rule
-func (r *Rule) AddAction(name string, action RuleAction) error {
+func (r *Rule) AddAction(name string, action rules.Action) error {
 	// TODO add more logic, like one persistent action per rule etc
 	r.actions = append(r.actions, ruleActionParams{
 		Name:     name,
@@ -400,7 +380,7 @@ func (r *Rule) AddVariableNegation(v variables.RuleVariable, key string) error {
 
 // AddTransformation adds a transformation to the rule
 // it fails if the transformation cannot be found
-func (r *Rule) AddTransformation(name string, t RuleTransformation) error {
+func (r *Rule) AddTransformation(name string, t rules.Transformation) error {
 	if t == nil || name == "" {
 		return fmt.Errorf("invalid transformation %q not found", name)
 	}
@@ -417,7 +397,7 @@ func (r *Rule) ClearTransformations() {
 // SetOperator sets the operator of the rule
 // There can be only one operator per rule
 // functionName and params are used for logging
-func (r *Rule) SetOperator(operator RuleOperator, functionName string, params string) {
+func (r *Rule) SetOperator(operator rules.Operator, functionName string, params string) {
 	r.operator = &ruleOperatorParams{
 		Operator: operator,
 		Function: functionName,
diff --git a/rulegroup.go b/internal/corazawaf/rulegroup.go
similarity index 99%
rename from rulegroup.go
rename to internal/corazawaf/rulegroup.go
index 0c85875..6272b7d 100644
--- a/rulegroup.go
+++ b/internal/corazawaf/rulegroup.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"fmt"
diff --git a/transaction.go b/internal/corazawaf/transaction.go
similarity index 76%
rename from transaction.go
rename to internal/corazawaf/transaction.go
index 0d83f07..52a39a0 100644
--- a/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"bufio"
@@ -19,6 +19,7 @@ import (
 	stringsutil "github.com/corazawaf/coraza/v3/internal/strings"
 	urlutil "github.com/corazawaf/coraza/v3/internal/url"
 	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
@@ -107,6 +108,40 @@ type Transaction struct {
 	Variables TransactionVariables
 }
 
+func (tx *Transaction) GetID() string {
+	return tx.ID
+}
+
+func (tx *Transaction) GetVariables() rules.TransactionVariables {
+	return &tx.Variables
+}
+
+func (tx *Transaction) Collection(idx variables.RuleVariable) collection.Collection {
+	return tx.Collections[idx]
+}
+
+func (tx *Transaction) Interrupt(interruption *types.Interruption) {
+	if tx.RuleEngine == types.RuleEngineOn {
+		tx.Interruption = interruption
+	}
+}
+
+func (tx *Transaction) ContentInjection() bool {
+	return tx.WAF.ContentInjection
+}
+
+func (tx *Transaction) DebugLogger() loggers.DebugLogger {
+	return tx.WAF.Logger
+}
+
+func (tx *Transaction) ResponseBodyReader() (io.Reader, error) {
+	return tx.ResponseBodyBuffer.Reader()
+}
+
+func (tx *Transaction) RequestBodyReader() (io.Reader, error) {
+	return tx.RequestBodyBuffer.Reader()
+}
+
 // AddRequestHeader Adds a request header
 //
 // With this method it is possible to feed Coraza with a request header.
@@ -158,12 +193,18 @@ func (tx *Transaction) AddResponseHeader(key string, value string) {
 	}
 }
 
+func (tx *Transaction) Capturing() bool {
+	return tx.Capture
+}
+
 // CaptureField is used to set the TX:[index] variables by operators
 // that supports capture, like @rx
 func (tx *Transaction) CaptureField(index int, value string) {
-	tx.WAF.Logger.Debug("[%s] Capturing field %d with value %q", tx.ID, index, value)
-	i := strconv.Itoa(index)
-	tx.Variables.TX.SetIndex(i, 0, value)
+	if tx.Capture {
+		tx.WAF.Logger.Debug("[%s] Capturing field %d with value %q", tx.ID, index, value)
+		i := strconv.Itoa(index)
+		tx.Variables.TX.SetIndex(i, 0, value)
+	}
 }
 
 // this function is used to control which variables are reset after a new rule is evaluated
@@ -297,8 +338,8 @@ func (tx *Transaction) MatchRule(r *Rule, mds []types.MatchData) {
 	}
 
 	tx.MatchedRules = append(tx.MatchedRules, mr)
-	if tx.WAF.errorLogCb != nil && r.Log {
-		tx.WAF.errorLogCb(mr)
+	if tx.WAF.ErrorLogCb != nil && r.Log {
+		tx.WAF.ErrorLogCb(mr)
 	}
 }
 
@@ -515,6 +556,10 @@ func (tx *Transaction) ProcessRequestHeaders() *types.Interruption {
 	return tx.Interruption
 }
 
+func (tx *Transaction) RequestBodyWriter() io.Writer {
+	return tx.RequestBodyBuffer
+}
+
 // ProcessRequestBody Performs the request body (if any)
 //
 // This method perform the analysis on the request body. It is optional to
@@ -625,6 +670,10 @@ func (tx *Transaction) IsProcessableResponseBody() bool {
 	return stringsutil.InSlice(ct, tx.WAF.ResponseBodyMimeTypes)
 }
 
+func (tx *Transaction) ResponseBodyWriter() io.Writer {
+	return tx.ResponseBodyBuffer
+}
+
 // ProcessResponseBody Perform the request body (if any)
 //
 // This method perform the analysis on the request body. It is optional to
@@ -710,6 +759,14 @@ func (tx *Transaction) Interrupted() bool {
 	return tx.Interruption != nil
 }
 
+func (tx *Transaction) GetInterruption() *types.Interruption {
+	return tx.Interruption
+}
+
+func (tx *Transaction) GetMatchedRules() []types.MatchedRule {
+	return tx.MatchedRules
+}
+
 // AuditLog returns an AuditLog struct, used to write audit logs
 func (tx *Transaction) AuditLog() *loggers.AuditLog {
 	al := &loggers.AuditLog{}
@@ -836,6 +893,14 @@ func (tx *Transaction) Clean() error {
 	}
 }
 
+func (tx *Transaction) Close() error {
+	return tx.Clean()
+}
+
+func (tx *Transaction) String() string {
+	return tx.Debug()
+}
+
 // Debug will return a string with the transaction debug information
 func (tx *Transaction) Debug() string {
 	res := "\n\n----------------------- ERRORLOG ----------------------\n"
@@ -990,3 +1055,355 @@ type TransactionVariables struct {
 	ArgsGetNames  *collection.TranslationProxy
 	ArgsPostNames *collection.TranslationProxy
 }
+
+func (v *TransactionVariables) GetUserID() *collection.Simple {
+	return v.Userid
+}
+
+func (v *TransactionVariables) GetUrlencodedError() *collection.Simple {
+	return v.UrlencodedError
+}
+
+func (v *TransactionVariables) GetResponseContentType() *collection.Simple {
+	return v.ResponseContentType
+}
+
+func (v *TransactionVariables) GetUniqueID() *collection.Simple {
+	return v.UniqueID
+}
+
+func (v *TransactionVariables) GetArgsCombinedSize() *collection.SizeProxy {
+	return v.ArgsCombinedSize
+}
+
+func (v *TransactionVariables) GetAuthType() *collection.Simple {
+	return v.AuthType
+}
+
+func (v *TransactionVariables) GetFilesCombinedSize() *collection.Simple {
+	return v.FilesCombinedSize
+}
+
+func (v *TransactionVariables) GetFullRequest() *collection.Simple {
+	return v.FullRequest
+}
+
+func (v *TransactionVariables) GetFullRequestLength() *collection.Simple {
+	return v.FullRequestLength
+}
+
+func (v *TransactionVariables) GetInboundDataError() *collection.Simple {
+	return v.InboundDataError
+}
+
+func (v *TransactionVariables) GetMatchedVar() *collection.Simple {
+	return v.MatchedVar
+}
+
+func (v *TransactionVariables) GetMatchedVarName() *collection.Simple {
+	return v.MatchedVarName
+}
+
+func (v *TransactionVariables) GetMultipartBoundaryQuoted() *collection.Simple {
+	return v.MultipartBoundaryQuoted
+}
+
+func (v *TransactionVariables) GetMultipartBoundaryWhitespace() *collection.Simple {
+	return v.MultipartBoundaryWhitespace
+}
+
+func (v *TransactionVariables) GetMultipartCrlfLfLines() *collection.Simple {
+	return v.MultipartCrlfLfLines
+}
+
+func (v *TransactionVariables) GetMultipartDataAfter() *collection.Simple {
+	return v.MultipartDataAfter
+}
+
+func (v *TransactionVariables) GetMultipartDataBefore() *collection.Simple {
+	return v.MultipartDataBefore
+}
+
+func (v *TransactionVariables) GetMultipartFileLimitExceeded() *collection.Simple {
+	return v.MultipartFileLimitExceeded
+}
+
+func (v *TransactionVariables) GetMultipartHeaderFolding() *collection.Simple {
+	return v.MultipartHeaderFolding
+}
+
+func (v *TransactionVariables) GetMultipartInvalidHeaderFolding() *collection.Simple {
+	return v.MultipartInvalidHeaderFolding
+}
+
+func (v *TransactionVariables) GetMultipartInvalidPart() *collection.Simple {
+	return v.MultipartInvalidPart
+}
+
+func (v *TransactionVariables) GetMultipartInvalidQuoting() *collection.Simple {
+	return v.MultipartInvalidQuoting
+}
+
+func (v *TransactionVariables) GetMultipartLfLine() *collection.Simple {
+	return v.MultipartLfLine
+}
+
+func (v *TransactionVariables) GetMultipartMissingSemicolon() *collection.Simple {
+	return v.MultipartMissingSemicolon
+}
+
+func (v *TransactionVariables) GetMultipartStrictError() *collection.Simple {
+	return v.MultipartStrictError
+}
+
+func (v *TransactionVariables) GetMultipartUnmatchedBoundary() *collection.Simple {
+	return v.MultipartUnmatchedBoundary
+}
+
+func (v *TransactionVariables) GetOutboundDataError() *collection.Simple {
+	return v.OutboundDataError
+}
+
+func (v *TransactionVariables) GetPathInfo() *collection.Simple {
+	return v.PathInfo
+}
+
+func (v *TransactionVariables) GetQueryString() *collection.Simple {
+	return v.QueryString
+}
+
+func (v *TransactionVariables) GetRemoteAddr() *collection.Simple {
+	return v.RemoteAddr
+}
+
+func (v *TransactionVariables) GetRemoteHost() *collection.Simple {
+	return v.RemoteHost
+}
+
+func (v *TransactionVariables) GetRemotePort() *collection.Simple {
+	return v.RemotePort
+}
+
+func (v *TransactionVariables) GetReqbodyError() *collection.Simple {
+	return v.ReqbodyError
+}
+
+func (v *TransactionVariables) GetReqbodyErrorMsg() *collection.Simple {
+	return v.ReqbodyErrorMsg
+}
+
+func (v *TransactionVariables) GetReqbodyProcessorError() *collection.Simple {
+	return v.ReqbodyProcessorError
+}
+
+func (v *TransactionVariables) GetReqbodyProcessorErrorMsg() *collection.Simple {
+	return v.ReqbodyProcessorErrorMsg
+}
+
+func (v *TransactionVariables) GetReqbodyProcessor() *collection.Simple {
+	return v.ReqbodyProcessor
+}
+
+func (v *TransactionVariables) GetRequestBasename() *collection.Simple {
+	return v.RequestBasename
+}
+
+func (v *TransactionVariables) GetRequestBody() *collection.Simple {
+	return v.RequestBody
+}
+
+func (v *TransactionVariables) GetRequestBodyLength() *collection.Simple {
+	return v.RequestBodyLength
+}
+
+func (v *TransactionVariables) GetRequestFilename() *collection.Simple {
+	return v.RequestFilename
+}
+
+func (v *TransactionVariables) GetRequestLine() *collection.Simple {
+	return v.RequestLine
+}
+
+func (v *TransactionVariables) GetRequestMethod() *collection.Simple {
+	return v.RequestMethod
+}
+
+func (v *TransactionVariables) GetRequestProtocol() *collection.Simple {
+	return v.RequestProtocol
+}
+
+func (v *TransactionVariables) GetRequestURI() *collection.Simple {
+	return v.RequestURI
+}
+
+func (v *TransactionVariables) GetRequestURIRaw() *collection.Simple {
+	return v.RequestURIRaw
+}
+
+func (v *TransactionVariables) GetResponseBody() *collection.Simple {
+	return v.ResponseBody
+}
+
+func (v *TransactionVariables) GetResponseContentLength() *collection.Simple {
+	return v.ResponseContentLength
+}
+
+func (v *TransactionVariables) GetResponseProtocol() *collection.Simple {
+	return v.ResponseProtocol
+}
+
+func (v *TransactionVariables) GetResponseStatus() *collection.Simple {
+	return v.ResponseStatus
+}
+
+func (v *TransactionVariables) GetServerAddr() *collection.Simple {
+	return v.ServerAddr
+}
+
+func (v *TransactionVariables) GetServerName() *collection.Simple {
+	return v.ServerName
+}
+
+func (v *TransactionVariables) GetServerPort() *collection.Simple {
+	return v.ServerPort
+}
+
+func (v *TransactionVariables) GetSessionid() *collection.Simple {
+	return v.Sessionid
+}
+
+func (v *TransactionVariables) GetHighestSeverity() *collection.Simple {
+	return v.HighestSeverity
+}
+
+func (v *TransactionVariables) GetStatusLine() *collection.Simple {
+	return v.StatusLine
+}
+
+func (v *TransactionVariables) GetInboundErrorData() *collection.Simple {
+	return v.InboundErrorData
+}
+
+func (v *TransactionVariables) GetEnv() *collection.Map {
+	return v.Env
+}
+
+func (v *TransactionVariables) GetTX() *collection.Map {
+	return v.TX
+}
+
+func (v *TransactionVariables) GetRule() *collection.Map {
+	return v.Rule
+}
+
+func (v *TransactionVariables) GetDuration() *collection.Simple {
+	return v.Duration
+}
+
+func (v *TransactionVariables) GetArgs() *collection.Proxy {
+	return v.Args
+}
+
+func (v *TransactionVariables) GetArgsGet() *collection.Map {
+	return v.ArgsGet
+}
+
+func (v *TransactionVariables) GetArgsPost() *collection.Map {
+	return v.ArgsPost
+}
+
+func (v *TransactionVariables) GetArgsPath() *collection.Map {
+	return v.ArgsPath
+}
+
+func (v *TransactionVariables) GetFilesTmpNames() *collection.Map {
+	return v.FilesTmpNames
+}
+
+func (v *TransactionVariables) GetGeo() *collection.Map {
+	return v.Geo
+}
+
+func (v *TransactionVariables) GetFiles() *collection.Map {
+	return v.Files
+}
+
+func (v *TransactionVariables) GetRequestCookies() *collection.Map {
+	return v.RequestCookies
+}
+
+func (v *TransactionVariables) GetRequestHeaders() *collection.Map {
+	return v.RequestHeaders
+}
+
+func (v *TransactionVariables) GetResponseHeaders() *collection.Map {
+	return v.ResponseHeaders
+}
+
+func (v *TransactionVariables) GetMultipartName() *collection.Map {
+	return v.MultipartName
+}
+
+func (v *TransactionVariables) GetMatchedVarsNames() *collection.Map {
+	return v.MatchedVarsNames
+}
+
+func (v *TransactionVariables) GetMultipartFilename() *collection.Map {
+	return v.MultipartFilename
+}
+
+func (v *TransactionVariables) GetMatchedVars() *collection.Map {
+	return v.MatchedVars
+}
+
+func (v *TransactionVariables) GetFilesSizes() *collection.Map {
+	return v.FilesSizes
+}
+
+func (v *TransactionVariables) GetFilesNames() *collection.Map {
+	return v.FilesNames
+}
+
+func (v *TransactionVariables) GetFilesTmpContent() *collection.Map {
+	return v.FilesTmpContent
+}
+
+func (v *TransactionVariables) GetResponseHeadersNames() *collection.Map {
+	return v.ResponseHeadersNames
+}
+
+func (v *TransactionVariables) GetRequestHeadersNames() *collection.Map {
+	return v.RequestHeadersNames
+}
+
+func (v *TransactionVariables) GetRequestCookiesNames() *collection.Map {
+	return v.RequestCookiesNames
+}
+
+func (v *TransactionVariables) GetXML() *collection.Map {
+	return v.XML
+}
+
+func (v *TransactionVariables) GetRequestXML() *collection.Map {
+	return v.RequestXML
+}
+
+func (v *TransactionVariables) GetResponseXML() *collection.Map {
+	return v.ResponseXML
+}
+
+func (v *TransactionVariables) GetIP() *collection.Map {
+	return v.IP
+}
+
+func (v *TransactionVariables) GetArgsNames() *collection.TranslationProxy {
+	return v.ArgsNames
+}
+
+func (v *TransactionVariables) GetArgsGetNames() *collection.TranslationProxy {
+	return v.ArgsGetNames
+}
+
+func (v *TransactionVariables) GetArgsPostNames() *collection.TranslationProxy {
+	return v.ArgsPostNames
+}
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
new file mode 100644
index 0000000..c419fa3
--- /dev/null
+++ b/internal/corazawaf/waf.go
@@ -0,0 +1,481 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package corazawaf
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"io/fs"
+	"log"
+	"os"
+	"regexp"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	utils "github.com/corazawaf/coraza/v3/internal/strings"
+	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// Initializing pool for transactions
+var transactionPool = sync.Pool{
+	// New optionally specifies a function to generate
+	// a value when Get would otherwise return nil.
+	New: func() interface{} { return new(Transaction) },
+}
+
+// ErrorLogCallback is used to set a callback function to log errors
+// It is triggered when an error is raised by the WAF
+// It contains the severity so the cb can decide to log it or not
+type ErrorLogCallback = func(rule types.MatchedRule)
+
+// WAF instance is used to store configurations and rules
+// Every web application should have a different WAF instance,
+// but you can share an instance if you are ok with sharing
+// configurations, rules and logging.
+// Transactions and SecLang parser requires a WAF instance
+// You can use as many WAF instances as you want, and they are
+// concurrent safe
+// All WAF instance fields are immutable, if you update any
+// of them in runtime you might create concurrency issues
+type WAF struct {
+	// ruleGroup object, contains all rules and helpers
+	Rules RuleGroup
+
+	// Audit mode status
+	AuditEngine types.AuditEngineStatus
+
+	// Array of logging parts to be used
+	AuditLogParts types.AuditLogParts
+
+	// Status of the content injection for responses and requests
+	ContentInjection bool
+
+	// If true, transactions will have access to the request body
+	RequestBodyAccess bool
+
+	// Request body page file limit
+	RequestBodyLimit int64
+
+	// Request body in memory limit
+	RequestBodyInMemoryLimit int64
+
+	// If true, transactions will have access to the response body
+	ResponseBodyAccess bool
+
+	// Response body memory limit
+	ResponseBodyLimit int64
+
+	// Defines if rules are going to be evaluated
+	RuleEngine types.RuleEngineStatus
+
+	// If true, transaction will fail if response size is bigger than the page limit
+	RejectOnResponseBodyLimit bool
+
+	// If true, transaction will fail if request size is bigger than the page limit
+	RejectOnRequestBodyLimit bool
+
+	// Responses will only be loaded if mime is listed here
+	ResponseBodyMimeTypes []string
+
+	// Web Application id, apps sharing the same id will share persistent collections
+	WebAppID string
+
+	// Add significant rule components to audit log
+	ComponentNames []string
+
+	// Contains the regular expression for relevant status audit logging
+	AuditLogRelevantStatus *regexp.Regexp
+
+	// If true WAF engine will fail when remote rules cannot be loaded
+	AbortOnRemoteRulesFail bool
+
+	// Instructs the waf to change the Server response header
+	ServerSignature string
+
+	// This directory will be used to store page files
+	TmpDir string
+
+	// Sensor ID identifies the sensor in ac cluster
+	SensorID string
+
+	// Path to store data files (ex. cache)
+	DataDir string
+
+	// If true, the WAF will store the uploaded files in the UploadDir
+	// directory
+	UploadKeepFiles bool
+	// UploadFileMode instructs the waf to set the file mode for uploaded files
+	UploadFileMode fs.FileMode
+	// UploadFileLimit is the maximum size of the uploaded file to be stored
+	UploadFileLimit int
+	// UploadDir is the directory where the uploaded files will be stored
+	UploadDir string
+
+	RequestBodyNoFilesLimit int64
+
+	RequestBodyLimitAction types.RequestBodyLimitAction
+
+	ArgumentSeparator string
+
+	// ProducerConnector is used by connectors to identify the producer
+	// on audit logs, for example, apache-modcoraza
+	ProducerConnector string
+
+	// ProducerConnectorVersion is used by connectors to identify the producer
+	// version on audit logs
+	ProducerConnectorVersion string
+
+	// Used for the debug logger
+	Logger loggers.DebugLogger
+
+	ErrorLogCb ErrorLogCallback
+
+	// AuditLogWriter is used to write audit logs
+	AuditLogWriter loggers.LogWriter
+}
+
+// NewTransaction Creates a new initialized transaction for this WAF instance
+func (w *WAF) NewTransaction(ctx context.Context) *Transaction {
+	tx := transactionPool.Get().(*Transaction)
+	tx.ID = utils.RandomString(19)
+	tx.MatchedRules = []types.MatchedRule{}
+	tx.Interruption = nil
+	tx.Collections = [types.VariablesCount]collection.Collection{}
+	tx.Logdata = ""
+	tx.SkipAfter = ""
+	tx.AuditEngine = w.AuditEngine
+	tx.AuditLogParts = w.AuditLogParts
+	tx.ForceRequestBodyVariable = false
+	tx.RequestBodyAccess = w.RequestBodyAccess
+	tx.RequestBodyLimit = w.RequestBodyLimit
+	tx.ResponseBodyAccess = w.ResponseBodyAccess
+	tx.ResponseBodyLimit = w.ResponseBodyLimit
+	tx.RuleEngine = w.RuleEngine
+	tx.HashEngine = false
+	tx.HashEnforcement = false
+	tx.LastPhase = 0
+	tx.RequestBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
+		TmpPath:     w.TmpDir,
+		MemoryLimit: w.RequestBodyInMemoryLimit,
+	})
+	tx.ResponseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
+		TmpPath:     w.TmpDir,
+		MemoryLimit: w.RequestBodyInMemoryLimit,
+	})
+	tx.bodyProcessor = nil
+	tx.ruleRemoveByID = []int{}
+	tx.ruleRemoveTargetByID = map[int][]ruleVariableParams{}
+	tx.Skip = 0
+	tx.Capture = false
+	tx.stopWatches = map[types.RulePhase]int64{}
+	tx.WAF = w
+	tx.Timestamp = time.Now().UnixNano()
+	tx.audit = false
+
+	tx.Variables.UrlencodedError = collection.NewSimple(variables.UrlencodedError)
+	tx.Collections[variables.UrlencodedError] = tx.Variables.UrlencodedError
+	tx.Variables.ResponseContentType = collection.NewSimple(variables.ResponseContentType)
+	tx.Collections[variables.ResponseContentType] = tx.Variables.ResponseContentType
+	tx.Variables.UniqueID = collection.NewSimple(variables.UniqueID)
+	tx.Collections[variables.UniqueID] = tx.Variables.UniqueID
+	tx.Variables.AuthType = collection.NewSimple(variables.AuthType)
+	tx.Collections[variables.AuthType] = tx.Variables.AuthType
+	tx.Variables.FilesCombinedSize = collection.NewSimple(variables.FilesCombinedSize)
+	tx.Collections[variables.FilesCombinedSize] = tx.Variables.FilesCombinedSize
+	tx.Variables.FullRequest = collection.NewSimple(variables.FullRequest)
+	tx.Collections[variables.FullRequest] = tx.Variables.FullRequest
+	tx.Variables.FullRequestLength = collection.NewSimple(variables.FullRequestLength)
+	tx.Collections[variables.FullRequestLength] = tx.Variables.FullRequestLength
+	tx.Variables.InboundDataError = collection.NewSimple(variables.InboundDataError)
+	tx.Collections[variables.InboundDataError] = tx.Variables.InboundDataError
+	tx.Variables.MatchedVar = collection.NewSimple(variables.MatchedVar)
+	tx.Collections[variables.MatchedVar] = tx.Variables.MatchedVar
+	tx.Variables.MatchedVarName = collection.NewSimple(variables.MatchedVarName)
+	tx.Collections[variables.MatchedVarName] = tx.Variables.MatchedVarName
+	tx.Variables.MultipartBoundaryQuoted = collection.NewSimple(variables.MultipartBoundaryQuoted)
+	tx.Collections[variables.MultipartBoundaryQuoted] = tx.Variables.MultipartBoundaryQuoted
+	tx.Variables.MultipartBoundaryWhitespace = collection.NewSimple(variables.MultipartBoundaryWhitespace)
+	tx.Collections[variables.MultipartBoundaryWhitespace] = tx.Variables.MultipartBoundaryWhitespace
+	tx.Variables.MultipartCrlfLfLines = collection.NewSimple(variables.MultipartCrlfLfLines)
+	tx.Collections[variables.MultipartCrlfLfLines] = tx.Variables.MultipartCrlfLfLines
+	tx.Variables.MultipartDataAfter = collection.NewSimple(variables.MultipartDataAfter)
+	tx.Collections[variables.MultipartDataAfter] = tx.Variables.MultipartDataAfter
+	tx.Variables.MultipartDataBefore = collection.NewSimple(variables.MultipartDataBefore)
+	tx.Collections[variables.MultipartDataBefore] = tx.Variables.MultipartDataBefore
+	tx.Variables.MultipartFileLimitExceeded = collection.NewSimple(variables.MultipartFileLimitExceeded)
+	tx.Collections[variables.MultipartFileLimitExceeded] = tx.Variables.MultipartFileLimitExceeded
+	tx.Variables.MultipartHeaderFolding = collection.NewSimple(variables.MultipartHeaderFolding)
+	tx.Collections[variables.MultipartHeaderFolding] = tx.Variables.MultipartHeaderFolding
+	tx.Variables.MultipartInvalidHeaderFolding = collection.NewSimple(variables.MultipartInvalidHeaderFolding)
+	tx.Collections[variables.MultipartInvalidHeaderFolding] = tx.Variables.MultipartInvalidHeaderFolding
+	tx.Variables.MultipartInvalidPart = collection.NewSimple(variables.MultipartInvalidPart)
+	tx.Collections[variables.MultipartInvalidPart] = tx.Variables.MultipartInvalidPart
+	tx.Variables.MultipartInvalidQuoting = collection.NewSimple(variables.MultipartInvalidQuoting)
+	tx.Collections[variables.MultipartInvalidQuoting] = tx.Variables.MultipartInvalidQuoting
+	tx.Variables.MultipartLfLine = collection.NewSimple(variables.MultipartLfLine)
+	tx.Collections[variables.MultipartLfLine] = tx.Variables.MultipartLfLine
+	tx.Variables.MultipartMissingSemicolon = collection.NewSimple(variables.MultipartMissingSemicolon)
+	tx.Collections[variables.MultipartMissingSemicolon] = tx.Variables.MultipartMissingSemicolon
+	tx.Variables.MultipartStrictError = collection.NewSimple(variables.MultipartStrictError)
+	tx.Collections[variables.MultipartStrictError] = tx.Variables.MultipartStrictError
+	tx.Variables.MultipartUnmatchedBoundary = collection.NewSimple(variables.MultipartUnmatchedBoundary)
+	tx.Collections[variables.MultipartUnmatchedBoundary] = tx.Variables.MultipartUnmatchedBoundary
+	tx.Variables.OutboundDataError = collection.NewSimple(variables.OutboundDataError)
+	tx.Collections[variables.OutboundDataError] = tx.Variables.OutboundDataError
+	tx.Variables.PathInfo = collection.NewSimple(variables.PathInfo)
+	tx.Collections[variables.PathInfo] = tx.Variables.PathInfo
+	tx.Variables.QueryString = collection.NewSimple(variables.QueryString)
+	tx.Collections[variables.QueryString] = tx.Variables.QueryString
+	tx.Variables.RemoteAddr = collection.NewSimple(variables.RemoteAddr)
+	tx.Collections[variables.RemoteAddr] = tx.Variables.RemoteAddr
+	tx.Variables.RemoteHost = collection.NewSimple(variables.RemoteHost)
+	tx.Collections[variables.RemoteHost] = tx.Variables.RemoteHost
+	tx.Variables.RemotePort = collection.NewSimple(variables.RemotePort)
+	tx.Collections[variables.RemotePort] = tx.Variables.RemotePort
+	tx.Variables.ReqbodyError = collection.NewSimple(variables.ReqbodyError)
+	tx.Collections[variables.ReqbodyError] = tx.Variables.ReqbodyError
+	tx.Variables.ReqbodyErrorMsg = collection.NewSimple(variables.ReqbodyErrorMsg)
+	tx.Collections[variables.ReqbodyErrorMsg] = tx.Variables.ReqbodyErrorMsg
+	tx.Variables.ReqbodyProcessorError = collection.NewSimple(variables.ReqbodyProcessorError)
+	tx.Collections[variables.ReqbodyProcessorError] = tx.Variables.ReqbodyProcessorError
+	tx.Variables.ReqbodyProcessorErrorMsg = collection.NewSimple(variables.ReqbodyProcessorErrorMsg)
+	tx.Collections[variables.ReqbodyProcessorErrorMsg] = tx.Variables.ReqbodyProcessorErrorMsg
+	tx.Variables.ReqbodyProcessor = collection.NewSimple(variables.ReqbodyProcessor)
+	tx.Collections[variables.ReqbodyProcessor] = tx.Variables.ReqbodyProcessor
+	tx.Variables.RequestBasename = collection.NewSimple(variables.RequestBasename)
+	tx.Collections[variables.RequestBasename] = tx.Variables.RequestBasename
+	tx.Variables.RequestBody = collection.NewSimple(variables.RequestBody)
+	tx.Collections[variables.RequestBody] = tx.Variables.RequestBody
+	tx.Variables.RequestBodyLength = collection.NewSimple(variables.RequestBodyLength)
+	tx.Collections[variables.RequestBodyLength] = tx.Variables.RequestBodyLength
+	tx.Variables.RequestFilename = collection.NewSimple(variables.RequestFilename)
+	tx.Collections[variables.RequestFilename] = tx.Variables.RequestFilename
+	tx.Variables.RequestLine = collection.NewSimple(variables.RequestLine)
+	tx.Collections[variables.RequestLine] = tx.Variables.RequestLine
+	tx.Variables.RequestMethod = collection.NewSimple(variables.RequestMethod)
+	tx.Collections[variables.RequestMethod] = tx.Variables.RequestMethod
+	tx.Variables.RequestProtocol = collection.NewSimple(variables.RequestProtocol)
+	tx.Collections[variables.RequestProtocol] = tx.Variables.RequestProtocol
+	tx.Variables.RequestURI = collection.NewSimple(variables.RequestURI)
+	tx.Collections[variables.RequestURI] = tx.Variables.RequestURI
+	tx.Variables.RequestURIRaw = collection.NewSimple(variables.RequestURIRaw)
+	tx.Collections[variables.RequestURIRaw] = tx.Variables.RequestURIRaw
+	tx.Variables.ResponseBody = collection.NewSimple(variables.ResponseBody)
+	tx.Collections[variables.ResponseBody] = tx.Variables.ResponseBody
+	tx.Variables.ResponseContentLength = collection.NewSimple(variables.ResponseContentLength)
+	tx.Collections[variables.ResponseContentLength] = tx.Variables.ResponseContentLength
+	tx.Variables.ResponseProtocol = collection.NewSimple(variables.ResponseProtocol)
+	tx.Collections[variables.ResponseProtocol] = tx.Variables.ResponseProtocol
+	tx.Variables.ResponseStatus = collection.NewSimple(variables.ResponseStatus)
+	tx.Collections[variables.ResponseStatus] = tx.Variables.ResponseStatus
+	tx.Variables.ServerAddr = collection.NewSimple(variables.ServerAddr)
+	tx.Collections[variables.ServerAddr] = tx.Variables.ServerAddr
+	tx.Variables.ServerName = collection.NewSimple(variables.ServerName)
+	tx.Collections[variables.ServerName] = tx.Variables.ServerName
+	tx.Variables.ServerPort = collection.NewSimple(variables.ServerPort)
+	tx.Collections[variables.ServerPort] = tx.Variables.ServerPort
+	tx.Variables.Sessionid = collection.NewSimple(variables.Sessionid)
+	tx.Collections[variables.Sessionid] = tx.Variables.Sessionid
+	tx.Variables.HighestSeverity = collection.NewSimple(variables.HighestSeverity)
+	tx.Collections[variables.HighestSeverity] = tx.Variables.HighestSeverity
+	tx.Variables.StatusLine = collection.NewSimple(variables.StatusLine)
+	tx.Collections[variables.StatusLine] = tx.Variables.StatusLine
+	tx.Variables.InboundErrorData = collection.NewSimple(variables.InboundErrorData)
+	tx.Collections[variables.InboundErrorData] = tx.Variables.InboundErrorData
+	tx.Variables.Duration = collection.NewSimple(variables.Duration)
+	tx.Collections[variables.Duration] = tx.Variables.Duration
+	tx.Variables.ResponseHeadersNames = collection.NewMap(variables.ResponseHeadersNames)
+	tx.Collections[variables.ResponseHeadersNames] = tx.Variables.ResponseHeadersNames
+	tx.Variables.RequestHeadersNames = collection.NewMap(variables.RequestHeadersNames)
+	tx.Collections[variables.RequestHeadersNames] = tx.Variables.RequestHeadersNames
+	tx.Variables.Userid = collection.NewSimple(variables.Userid)
+	tx.Collections[variables.Userid] = tx.Variables.Userid
+	tx.Variables.ArgsGet = collection.NewMap(variables.ArgsGet)
+	tx.Collections[variables.ArgsGet] = tx.Variables.ArgsGet
+	tx.Variables.ArgsPost = collection.NewMap(variables.ArgsPost)
+	tx.Collections[variables.ArgsPost] = tx.Variables.ArgsPost
+	tx.Variables.ArgsPath = collection.NewMap(variables.ArgsPath)
+	tx.Collections[variables.ArgsPath] = tx.Variables.ArgsPath
+	tx.Variables.FilesSizes = collection.NewMap(variables.FilesSizes)
+	tx.Collections[variables.FilesSizes] = tx.Variables.FilesSizes
+	tx.Variables.FilesTmpContent = collection.NewMap(variables.FilesTmpContent)
+	tx.Collections[variables.FilesTmpContent] = tx.Variables.FilesTmpContent
+	tx.Variables.MultipartFilename = collection.NewMap(variables.MultipartFilename)
+	tx.Collections[variables.MultipartFilename] = tx.Variables.MultipartFilename
+	tx.Variables.MultipartName = collection.NewMap(variables.MultipartName)
+	tx.Collections[variables.MultipartName] = tx.Variables.MultipartName
+	tx.Variables.MatchedVars = collection.NewMap(variables.MatchedVars)
+	tx.Collections[variables.MatchedVars] = tx.Variables.MatchedVars
+	tx.Variables.RequestCookies = collection.NewMap(variables.RequestCookies)
+	tx.Collections[variables.RequestCookies] = tx.Variables.RequestCookies
+	tx.Variables.RequestHeaders = collection.NewMap(variables.RequestHeaders)
+	tx.Collections[variables.RequestHeaders] = tx.Variables.RequestHeaders
+	tx.Variables.ResponseHeaders = collection.NewMap(variables.ResponseHeaders)
+	tx.Collections[variables.ResponseHeaders] = tx.Variables.ResponseHeaders
+	tx.Variables.Geo = collection.NewMap(variables.Geo)
+	tx.Collections[variables.Geo] = tx.Variables.Geo
+	tx.Variables.TX = collection.NewMap(variables.TX)
+	tx.Collections[variables.TX] = tx.Variables.TX
+	tx.Variables.Rule = collection.NewMap(variables.Rule)
+	tx.Collections[variables.Rule] = tx.Variables.Rule
+	tx.Variables.Env = collection.NewMap(variables.Env)
+	tx.Collections[variables.Env] = tx.Variables.Env
+	tx.Variables.IP = collection.NewMap(variables.IP)
+	tx.Collections[variables.IP] = tx.Variables.IP
+	tx.Variables.Files = collection.NewMap(variables.Files)
+	tx.Collections[variables.Files] = tx.Variables.Files
+	tx.Variables.MatchedVarsNames = collection.NewMap(variables.MatchedVarsNames)
+	tx.Collections[variables.MatchedVarsNames] = tx.Variables.MatchedVarsNames
+	tx.Variables.FilesNames = collection.NewMap(variables.FilesNames)
+	tx.Collections[variables.FilesNames] = tx.Variables.FilesNames
+	tx.Variables.FilesTmpNames = collection.NewMap(variables.FilesTmpNames)
+	tx.Collections[variables.FilesTmpNames] = tx.Variables.FilesTmpNames
+	tx.Variables.RequestCookiesNames = collection.NewMap(variables.RequestCookiesNames)
+	tx.Collections[variables.RequestCookiesNames] = tx.Variables.RequestCookiesNames
+	tx.Variables.ResponseXML = collection.NewMap(variables.ResponseXML)
+	tx.Collections[variables.ResponseXML] = tx.Variables.ResponseXML
+	tx.Variables.RequestXML = collection.NewMap(variables.RequestXML)
+	tx.Collections[variables.RequestXML] = tx.Variables.RequestXML
+
+	tx.Variables.ArgsCombinedSize = collection.NewCollectionSizeProxy(variables.ArgsCombinedSize, tx.Variables.ArgsGet, tx.Variables.ArgsPost)
+	tx.Collections[variables.ArgsCombinedSize] = tx.Variables.ArgsCombinedSize
+
+	// XML is a pointer to RequestXML
+	tx.Variables.XML = tx.Variables.RequestXML
+	tx.Collections[variables.XML] = tx.Variables.RequestXML
+	tx.Variables.Args = collection.NewProxy(
+		variables.Args,
+		tx.Variables.ArgsGet,
+		tx.Variables.ArgsPost,
+		tx.Variables.ArgsPath,
+	)
+	tx.Collections[variables.Args] = tx.Variables.Args
+
+	tx.Variables.ArgsNames = collection.NewTranslationProxy(
+		variables.ArgsNames,
+		tx.Variables.ArgsGet,
+		tx.Variables.ArgsPost,
+		tx.Variables.ArgsPath,
+	)
+	tx.Collections[variables.ArgsNames] = tx.Variables.ArgsNames
+	tx.Variables.ArgsGetNames = collection.NewTranslationProxy(
+		variables.ArgsGetNames,
+		tx.Variables.ArgsGet,
+	)
+	tx.Collections[variables.ArgsGetNames] = tx.Variables.ArgsGetNames
+	tx.Variables.ArgsPostNames = collection.NewTranslationProxy(
+		variables.ArgsPostNames,
+		tx.Variables.ArgsPost,
+	)
+	tx.Collections[variables.ArgsPostNames] = tx.Variables.ArgsPostNames
+
+	// set capture variables
+	for i := 0; i <= 10; i++ {
+		is := strconv.Itoa(i)
+		tx.Variables.TX.Set(is, []string{""})
+	}
+
+	// Some defaults
+	tx.Variables.FilesCombinedSize.Set("0")
+	tx.Variables.UrlencodedError.Set("0")
+	tx.Variables.FullRequestLength.Set("0")
+	tx.Variables.MultipartBoundaryQuoted.Set("0")
+	tx.Variables.MultipartBoundaryWhitespace.Set("0")
+	tx.Variables.MultipartCrlfLfLines.Set("0")
+	tx.Variables.MultipartDataAfter.Set("0")
+	tx.Variables.MultipartDataBefore.Set("0")
+	tx.Variables.MultipartFileLimitExceeded.Set("0")
+	tx.Variables.MultipartHeaderFolding.Set("0")
+	tx.Variables.MultipartInvalidHeaderFolding.Set("0")
+	tx.Variables.MultipartInvalidPart.Set("0")
+	tx.Variables.MultipartInvalidQuoting.Set("0")
+	tx.Variables.MultipartLfLine.Set("0")
+	tx.Variables.MultipartMissingSemicolon.Set("0")
+	tx.Variables.MultipartStrictError.Set("0")
+	tx.Variables.MultipartUnmatchedBoundary.Set("0")
+	tx.Variables.OutboundDataError.Set("0")
+	tx.Variables.ReqbodyError.Set("0")
+	tx.Variables.ReqbodyProcessorError.Set("0")
+	tx.Variables.RequestBodyLength.Set("0")
+	tx.Variables.Duration.Set("0")
+	tx.Variables.HighestSeverity.Set("0")
+	tx.Variables.UniqueID.Set(tx.ID)
+
+	w.Logger.Debug("new transaction created with id %q", tx.ID)
+
+	return tx
+}
+
+// SetDebugLogPath sets the path for the debug log
+// If the path is empty, the debug log will be disabled
+// note: this is not thread safe
+func (w *WAF) SetDebugLogPath(path string) error {
+	if path == "" {
+		w.Logger.SetOutput(io.Discard)
+		return nil
+	}
+	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+	if err != nil {
+		w.Logger.Error("error opening file: %s", err.Error())
+	}
+	w.Logger.SetOutput(f)
+	return nil
+}
+
+// NewWAF creates a new WAF instance with default variables
+func NewWAF() *WAF {
+	logger := &stdDebugLogger{
+		logger: &log.Logger{},
+		Level:  loggers.LogLevelInfo,
+	}
+	logWriter, err := loggers.GetLogWriter("serial")
+	if err != nil {
+		logger.Error("error creating serial log writer: %s", err.Error())
+	}
+	waf := &WAF{
+		ArgumentSeparator:        "&",
+		AuditLogWriter:           logWriter,
+		AuditEngine:              types.AuditEngineOff,
+		AuditLogParts:            types.AuditLogParts("ABCFHZ"),
+		RequestBodyInMemoryLimit: 131072,
+		RequestBodyLimit:         134217728, // 10mb
+		ResponseBodyMimeTypes:    []string{"text/html", "text/plain"},
+		ResponseBodyLimit:        524288,
+		ResponseBodyAccess:       false,
+		RuleEngine:               types.RuleEngineOn,
+		Rules:                    NewRuleGroup(),
+		TmpDir:                   "/tmp",
+		AuditLogRelevantStatus:   regexp.MustCompile(`.*`),
+		RequestBodyAccess:        false,
+		Logger:                   logger,
+	}
+	// We initialize a basic audit log writer that discards output
+	if err := logWriter.Init(types.Config{}); err != nil {
+		fmt.Println(err)
+	}
+	if err := waf.SetDebugLogPath(""); err != nil {
+		fmt.Println(err)
+	}
+	waf.Logger.Debug("a new waf instance was created")
+	return waf
+}
+
+// SetDebugLogLevel changes the debug level of the WAF instance
+func (w *WAF) SetDebugLogLevel(lvl int) error {
+	// setLevel is concurrent safe
+	w.Logger.SetLevel(loggers.LogLevel(lvl))
+	return nil
+}
+
+// SetErrorLogCb sets the callback function for error logging
+// The error callback receives all the error data and some
+// helpers to write modsecurity style logs
+func (w *WAF) SetErrorLogCb(cb ErrorLogCallback) {
+	w.ErrorLogCb = cb
+}
diff --git a/seclang/directives.go b/internal/seclang/directives.go
similarity index 99%
rename from seclang/directives.go
rename to internal/seclang/directives.go
index 598da12..e049de1 100644
--- a/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -11,14 +11,14 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 // DirectiveOptions contains the parsed options for a directive
 type DirectiveOptions struct {
-	WAF      *coraza.WAF
+	WAF      *corazawaf.WAF
 	Config   types.Config
 	Opts     string
 	Path     []string
@@ -33,7 +33,7 @@ func directiveSecComponentSignature(options *DirectiveOptions) error {
 }
 
 func directiveSecMarker(options *DirectiveOptions) error {
-	rule := coraza.NewRule()
+	rule := corazawaf.NewRule()
 	rule.Raw = fmt.Sprintf("SecMarker %s", options.Opts)
 	rule.SecMark = options.Opts
 	rule.ID = 0
diff --git a/seclang/parser.go b/internal/seclang/parser.go
similarity index 98%
rename from seclang/parser.go
rename to internal/seclang/parser.go
index 2607a07..a6c363e 100644
--- a/seclang/parser.go
+++ b/internal/seclang/parser.go
@@ -12,7 +12,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/internal/io"
 	"github.com/corazawaf/coraza/v3/types"
 )
@@ -187,7 +187,7 @@ func (p *Parser) SetRoot(root fs.FS) {
 // NewParser creates a new parser from a WAF instance
 // Rules and settings will be inserted into the WAF
 // rule container (RuleGroup).
-func NewParser(waf *coraza.WAF) *Parser {
+func NewParser(waf *corazawaf.WAF) *Parser {
 	p := &Parser{
 		options: &DirectiveOptions{
 			WAF:      waf,
diff --git a/seclang/rule_parser.go b/internal/seclang/rule_parser.go
similarity index 94%
rename from seclang/rule_parser.go
rename to internal/seclang/rule_parser.go
index a363500..5bb095b 100644
--- a/seclang/rule_parser.go
+++ b/internal/seclang/rule_parser.go
@@ -10,11 +10,12 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
 	actionsmod "github.com/corazawaf/coraza/v3/actions"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/internal/io"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	operators "github.com/corazawaf/coraza/v3/operators"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
@@ -24,13 +25,13 @@ const defaultActionsPhase2 = "phase:2,log,auditlog,pass"
 type ruleAction struct {
 	Key   string
 	Value string
-	Atype types.RuleActionType
-	F     coraza.RuleAction
+	Atype rules.ActionType
+	F     rules.Action
 }
 
 // RuleParser is used to programatically create new rules using seclang formatted strings
 type RuleParser struct {
-	rule           *coraza.Rule
+	rule           *corazawaf.Rule
 	defaultActions map[types.RulePhase][]ruleAction
 	options        RuleOptions
 }
@@ -194,7 +195,7 @@ func (p *RuleParser) ParseOperator(operator string) error {
 		return err
 	}
 
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: opdata,
 		Path: []string{
 			p.options.Config.Get("parser_config_dir", "").(string),
@@ -232,7 +233,7 @@ func (p *RuleParser) ParseDefaultActions(actions string) error {
 			}
 			continue
 		}
-		if action.Atype == types.ActionTypeDisruptive {
+		if action.Atype == rules.ActionTypeDisruptive {
 			defaultDisruptive = action.Key
 		}
 	}
@@ -262,7 +263,7 @@ func (p *RuleParser) ParseActions(actions string) error {
 	}
 	// first we execute metadata rules
 	for _, a := range act {
-		if a.Atype == types.ActionTypeMetadata {
+		if a.Atype == rules.ActionTypeMetadata {
 			errs := a.F.Init(p.rule, a.Value)
 			if errs != nil {
 				return errs
@@ -279,7 +280,7 @@ func (p *RuleParser) ParseActions(actions string) error {
 
 	for _, action := range act {
 		// now we evaluate non-metadata actions
-		if action.Atype == types.ActionTypeMetadata {
+		if action.Atype == rules.ActionTypeMetadata {
 			continue
 		}
 		errs := action.F.Init(p.rule, action.Value)
@@ -294,14 +295,14 @@ func (p *RuleParser) ParseActions(actions string) error {
 }
 
 // Rule returns the compiled rule
-func (p *RuleParser) Rule() *coraza.Rule {
+func (p *RuleParser) Rule() *corazawaf.Rule {
 	return p.rule
 }
 
 // RuleOptions contains the options used to compile a rule
 type RuleOptions struct {
 	WithOperator bool
-	WAF          *coraza.WAF
+	WAF          *corazawaf.WAF
 	Config       types.Config
 	Directive    string
 	Data         string
@@ -315,7 +316,7 @@ var ruleTokenRegex = regexp.MustCompile(`"(?:[^"\\]|\\.)*"`)
 // The string must match the seclang format
 // In case WithOperator is false, the rule will be parsed without operator
 // This function is created for external plugin directives
-func ParseRule(options RuleOptions) (*coraza.Rule, error) {
+func ParseRule(options RuleOptions) (*corazawaf.Rule, error) {
 	if strings.TrimSpace(options.Data) == "" {
 		return nil, errors.New("empty rule")
 	}
@@ -323,7 +324,7 @@ func ParseRule(options RuleOptions) (*coraza.Rule, error) {
 	var err error
 	rp := &RuleParser{
 		options:        options,
-		rule:           coraza.NewRule(),
+		rule:           corazawaf.NewRule(),
 		defaultActions: map[types.RulePhase][]ruleAction{},
 	}
 
@@ -392,7 +393,7 @@ func ParseRule(options RuleOptions) (*coraza.Rule, error) {
 	return rp.rule, nil
 }
 
-func getLastRuleExpectingChain(w *coraza.WAF) *coraza.Rule {
+func getLastRuleExpectingChain(w *corazawaf.WAF) *corazawaf.Rule {
 	rules := w.Rules.GetRules()
 	if len(rules) == 0 {
 		return nil
@@ -425,7 +426,7 @@ actionLoop:
 			// skip whitespaces in key
 			continue actionLoop
 		case !quoted && c == ',':
-			f, err := actionsmod.GetAction(ckey)
+			f, err := actionsmod.Get(ckey)
 			if err != nil {
 				return nil, err
 			}
@@ -457,7 +458,7 @@ actionLoop:
 			ckey += string(c)
 		}
 		if i+1 == len(actions) {
-			f, err := actionsmod.GetAction(ckey)
+			f, err := actionsmod.Get(ckey)
 			if err != nil {
 				return nil, err
 			}
@@ -490,18 +491,18 @@ func mergeActions(origin []ruleAction, defaults []ruleAction) []ruleAction {
 	res := []ruleAction{}
 	var da ruleAction // Disruptive action
 	for _, action := range defaults {
-		if action.Atype == types.ActionTypeDisruptive {
+		if action.Atype == rules.ActionTypeDisruptive {
 			da = action
 			continue
 		}
-		if action.Atype == types.ActionTypeMetadata {
+		if action.Atype == rules.ActionTypeMetadata {
 			continue
 		}
 		res = append(res, action)
 	}
 	hasDa := false
 	for _, action := range origin {
-		if action.Atype == types.ActionTypeDisruptive {
+		if action.Atype == rules.ActionTypeDisruptive {
 			if action.Key != "block" {
 				hasDa = true
 				// We add the default rule DA in case this is no block
diff --git a/seclang/testdata/includes/parent.conf b/internal/seclang/testdata/includes/parent.conf
similarity index 100%
rename from seclang/testdata/includes/parent.conf
rename to internal/seclang/testdata/includes/parent.conf
diff --git a/seclang/testdata/includes/postdirectives.conf b/internal/seclang/testdata/includes/postdirectives.conf
similarity index 100%
rename from seclang/testdata/includes/postdirectives.conf
rename to internal/seclang/testdata/includes/postdirectives.conf
diff --git a/seclang/testdata/includes/subinclude/pmFromFile-01.dat b/internal/seclang/testdata/includes/subinclude/pmFromFile-01.dat
similarity index 100%
rename from seclang/testdata/includes/subinclude/pmFromFile-01.dat
rename to internal/seclang/testdata/includes/subinclude/pmFromFile-01.dat
diff --git a/seclang/testdata/includes/subinclude/rules1.conf b/internal/seclang/testdata/includes/subinclude/rules1.conf
similarity index 100%
rename from seclang/testdata/includes/subinclude/rules1.conf
rename to internal/seclang/testdata/includes/subinclude/rules1.conf
diff --git a/seclang/testdata/includes/subinclude/rules2.conf b/internal/seclang/testdata/includes/subinclude/rules2.conf
similarity index 100%
rename from seclang/testdata/includes/subinclude/rules2.conf
rename to internal/seclang/testdata/includes/subinclude/rules2.conf
diff --git a/seclang/testdata/includes/subinclude/rules3.conf b/internal/seclang/testdata/includes/subinclude/rules3.conf
similarity index 100%
rename from seclang/testdata/includes/subinclude/rules3.conf
rename to internal/seclang/testdata/includes/subinclude/rules3.conf
diff --git a/seclang/testdata/parserbenchmark.conf b/internal/seclang/testdata/parserbenchmark.conf
similarity index 100%
rename from seclang/testdata/parserbenchmark.conf
rename to internal/seclang/testdata/parserbenchmark.conf
diff --git a/loggers/auditlog.go b/loggers/auditlog.go
index 6409ebc..586e06f 100644
--- a/loggers/auditlog.go
+++ b/loggers/auditlog.go
@@ -3,7 +3,9 @@
 
 package loggers
 
-import "github.com/corazawaf/coraza/v3/types"
+import (
+	"github.com/corazawaf/coraza/v3/types"
+)
 
 // AuditLog represents the main struct for audit log data
 type AuditLog struct {
diff --git a/logger.go b/loggers/debug.go
similarity index 53%
rename from logger.go
rename to loggers/debug.go
index e16c9c7..886894e 100644
--- a/logger.go
+++ b/loggers/debug.go
@@ -1,14 +1,30 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package loggers
 
 import (
-	"fmt"
 	"io"
-	"log"
 )
 
+// 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{})
+	// Debug logs a debug message
+	Debug(message string, args ...interface{})
+	// Trace logs a trace message
+	Trace(message string, args ...interface{})
+	// SetLevel sets the log level
+	SetLevel(level LogLevel)
+	// SetOutput sets the output for the logger
+	SetOutput(w io.Writer)
+}
+
 // LogLevel is the type of log level
 type LogLevel int
 
@@ -48,74 +64,3 @@ func (level LogLevel) String() string {
 func (level LogLevel) Invalid() bool {
 	return level < LogLevelInfo || level > LogLevelTrace
 }
-
-// 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{})
-	// Debug logs a debug message
-	Debug(message string, args ...interface{})
-	// Trace logs a trace message
-	Trace(message string, args ...interface{})
-	// SetLevel sets the log level
-	SetLevel(level LogLevel)
-	// SetOutput sets the output for the logger
-	SetOutput(w io.Writer)
-}
-
-// DebugLogger is a logger that logs to the standard logger
-type stdDebugLogger struct {
-	logger *log.Logger
-	Level  LogLevel
-}
-
-func (l *stdDebugLogger) formatLog(level LogLevel, message string, args ...interface{}) {
-	if l.Level >= level {
-		l.logger.Printf("[%s] %s", level.String(), fmt.Sprintf(message, args...))
-	}
-}
-
-// Info logs an info message
-func (l *stdDebugLogger) Info(message string, args ...interface{}) {
-	l.formatLog(LogLevelInfo, message, args...)
-}
-
-// Warn logs a warning message
-func (l *stdDebugLogger) Warn(message string, args ...interface{}) {
-	l.formatLog(LogLevelWarn, message, args...)
-}
-
-// Error logs an error message
-func (l *stdDebugLogger) Error(message string, args ...interface{}) {
-	l.formatLog(LogLevelError, message, args...)
-}
-
-// Debug logs a debug message
-func (l *stdDebugLogger) Debug(message string, args ...interface{}) {
-	l.formatLog(LogLevelDebug, message, args...)
-}
-
-// Trace logs a trace message
-func (l *stdDebugLogger) Trace(message string, args ...interface{}) {
-	l.formatLog(LogLevelTrace, message, args...)
-}
-
-// SetLevel sets the log level
-func (l *stdDebugLogger) SetLevel(level LogLevel) {
-	if level.Invalid() {
-		l.Info("Invalid log level, defaulting to INFO")
-		level = LogLevelInfo
-	}
-	l.Level = level
-}
-
-// SetOutput sets the output for the logger
-func (l *stdDebugLogger) SetOutput(w io.Writer) {
-	l.logger.SetOutput(w)
-}
-
-var _ DebugLogger = (*stdDebugLogger)(nil)
diff --git a/macro.go b/macro/macro.go
similarity index 81%
rename from macro.go
rename to macro/macro.go
index 46b5981..06b0f06 100644
--- a/macro.go
+++ b/macro/macro.go
@@ -1,42 +1,58 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package macro
 
 import (
 	"fmt"
 	"strings"
 
 	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/rules"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
+type Macro interface {
+	Expand(tx rules.TransactionState) string
+	String() string
+}
+
+func NewMacro(data string) (Macro, error) {
+	macro := &macro{
+		tokens: []macroToken{},
+	}
+	if err := macro.compile(data); err != nil {
+		return nil, err
+	}
+	return macro, nil
+}
+
 type macroToken struct {
 	text     string
 	variable *variables.RuleVariable
 	key      string
 }
 
-// Macro is used to create tokenized strings that can be
+// macro is used to create tokenized strings that can be
 // "expanded" at high speed and concurrent-safe.
-// A Macro contains tokens for strings and expansions
+// A macro contains tokens for strings and expansions
 // For example: some string %{tx.var} some string
 // The previous example would create 3 tokens:
 // String token: some string
 // Variable token: Variable: TX, key: var
 // String token: some string
-type Macro struct {
+type macro struct {
 	original string
 	tokens   []macroToken
 }
 
 // Expand the pre-compiled macro expression into a string
-func (m *Macro) Expand(tx *Transaction) string {
+func (m *macro) Expand(tx rules.TransactionState) string {
 	res := strings.Builder{}
 	for _, token := range m.tokens {
 		// now we place the in the index
 		if token.variable != nil {
-			switch col := tx.Collections[*token.variable].(type) {
+			switch col := tx.Collection(*token.variable).(type) {
 			case *collection.Map:
 				if c := col.Get(token.key); len(c) > 0 {
 					res.WriteString(c[0])
@@ -65,16 +81,16 @@ func (m *Macro) Expand(tx *Transaction) string {
 	return res.String()
 }
 
-// Compile is used to parse the input and generate the corresponding token
+// compile is used to parse the input and generate the corresponding token
 // Example input: %{var.foo} and %{var.bar}
 // expected result:
 // [0] macroToken{text: "%{var.foo}", variable: &variables.Var, key: "foo"},
 // [1] macroToken{text: " and ", variable: nil, key: ""}
 // [2] macroToken{text: "%{var.bar}", variable: &variables.Var, key: "bar"}
-func (m *Macro) Compile(input string) error {
+func (m *macro) compile(input string) error {
 	currentToken := strings.Builder{}
 	m.original = input
-	isMacro := false
+	ismacro := false
 	for i := 0; i < len(input); i++ {
 		c := input[i]
 		if c == '%' && (i <= len(input) && input[i+1] == '{') {
@@ -88,14 +104,14 @@ func (m *Macro) Compile(input string) error {
 				})
 			}
 			currentToken.Reset()
-			isMacro = true
+			ismacro = true
 			i++
 			continue
 		}
-		if isMacro {
+		if ismacro {
 			if c == '}' {
 				// we close a macro
-				isMacro = false
+				ismacro = false
 				spl := strings.SplitN(currentToken.String(), ".", 2)
 				key := ""
 				if len(spl) == 2 {
@@ -134,22 +150,11 @@ func (m *Macro) Compile(input string) error {
 }
 
 // String returns the original string
-func (m *Macro) String() string {
+func (m *macro) String() string {
 	return m.original
 }
 
 // IsExpandable return true if there are macro expanadable tokens
-func (m *Macro) IsExpandable() bool {
+func (m *macro) IsExpandable() bool {
 	return len(m.tokens) > 1
 }
-
-// NewMacro creates a new macro
-func NewMacro(data string) (*Macro, error) {
-	macro := &Macro{
-		tokens: []macroToken{},
-	}
-	if err := macro.Compile(data); err != nil {
-		return nil, err
-	}
-	return macro, nil
-}
diff --git a/operators/begins_with.go b/operators/begins_with.go
index 9cee23a..9ff80fe 100644
--- a/operators/begins_with.go
+++ b/operators/begins_with.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type beginsWith struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*beginsWith)(nil)
+var _ rules.Operator = (*beginsWith)(nil)
 
-func (o *beginsWith) Init(options coraza.RuleOperatorOptions) error {
+func (o *beginsWith) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *beginsWith) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *beginsWith) Evaluate(tx rules.TransactionState, value string) bool {
 	data := o.data.Expand(tx)
 	return strings.HasPrefix(value, data)
 }
diff --git a/operators/contains.go b/operators/contains.go
index 8060653..9aa1f00 100644
--- a/operators/contains.go
+++ b/operators/contains.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type contains struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*contains)(nil)
+var _ rules.Operator = (*contains)(nil)
 
-func (o *contains) Init(options coraza.RuleOperatorOptions) error {
+func (o *contains) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *contains) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *contains) Evaluate(tx rules.TransactionState, value string) bool {
 	data := o.data.Expand(tx)
 	return strings.Contains(value, data)
 }
diff --git a/operators/detect_sqli.go b/operators/detect_sqli.go
index 2f2c2d4..5ff5548 100644
--- a/operators/detect_sqli.go
+++ b/operators/detect_sqli.go
@@ -6,22 +6,18 @@ package operators
 import (
 	"github.com/corazawaf/libinjection-go"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type detectSQLi struct{}
 
-var _ coraza.RuleOperator = (*detectSQLi)(nil)
+func (o *detectSQLi) Init(options rules.OperatorOptions) error { return nil }
 
-func (*detectSQLi) Init(coraza.RuleOperatorOptions) error { return nil }
-
-func (*detectSQLi) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *detectSQLi) Evaluate(tx rules.TransactionState, value string) bool {
 	res, fingerprint := libinjection.IsSQLi(value)
 	if !res {
 		return false
 	}
-	if tx.Capture {
-		tx.CaptureField(0, string(fingerprint))
-	}
+	tx.CaptureField(0, string(fingerprint))
 	return true
 }
diff --git a/operators/detect_xss.go b/operators/detect_xss.go
index 50f1dd9..fea6837 100644
--- a/operators/detect_xss.go
+++ b/operators/detect_xss.go
@@ -6,15 +6,15 @@ package operators
 import (
 	"github.com/corazawaf/libinjection-go"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type detectXSS struct{}
 
-var _ coraza.RuleOperator = (*detectXSS)(nil)
+var _ rules.Operator = (*detectXSS)(nil)
 
-func (o *detectXSS) Init(coraza.RuleOperatorOptions) error { return nil }
+func (o *detectXSS) Init(rules.OperatorOptions) error { return nil }
 
-func (o *detectXSS) Evaluate(_ *coraza.Transaction, value string) bool {
+func (o *detectXSS) Evaluate(_ rules.TransactionState, value string) bool {
 	return libinjection.IsXSS(value)
 }
diff --git a/operators/ends_with.go b/operators/ends_with.go
index c7ab305..317663d 100644
--- a/operators/ends_with.go
+++ b/operators/ends_with.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type endsWith struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*endsWith)(nil)
+var _ rules.Operator = (*endsWith)(nil)
 
-func (o *endsWith) Init(options coraza.RuleOperatorOptions) error {
+func (o *endsWith) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *endsWith) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *endsWith) Evaluate(tx rules.TransactionState, value string) bool {
 	data := o.data.Expand(tx)
 	return strings.HasSuffix(value, data)
 }
diff --git a/operators/eq.go b/operators/eq.go
index b97c0a5..c86f701 100644
--- a/operators/eq.go
+++ b/operators/eq.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type eq struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*eq)(nil)
+var _ rules.Operator = (*eq)(nil)
 
-func (o *eq) Init(options coraza.RuleOperatorOptions) error {
+func (o *eq) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *eq) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *eq) Evaluate(tx rules.TransactionState, value string) bool {
 	d1, _ := strconv.Atoi(o.data.Expand(tx))
 	d2, _ := strconv.Atoi(value)
 	return d1 == d2
diff --git a/operators/ge.go b/operators/ge.go
index 9f852d5..77ec388 100644
--- a/operators/ge.go
+++ b/operators/ge.go
@@ -6,28 +6,28 @@ package operators
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
-	engine "github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type ge struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*ge)(nil)
+var _ rules.Operator = (*ge)(nil)
 
-func (o *ge) Init(options coraza.RuleOperatorOptions) error {
+func (o *ge) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *ge) Evaluate(tx *engine.Transaction, value string) bool {
+func (o *ge) Evaluate(tx rules.TransactionState, value string) bool {
 	v, _ := strconv.Atoi(value)
 	data, _ := strconv.Atoi(o.data.Expand(tx))
 	return v >= data
diff --git a/operators/geo_lookup.go b/operators/geo_lookup.go
index d793547..538b749 100644
--- a/operators/geo_lookup.go
+++ b/operators/geo_lookup.go
@@ -4,14 +4,14 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type geoLookup struct{}
 
-var _ coraza.RuleOperator = (*geoLookup)(nil)
+var _ rules.Operator = (*geoLookup)(nil)
 
-func (*geoLookup) Init(coraza.RuleOperatorOptions) error { return nil }
+func (*geoLookup) Init(rules.OperatorOptions) error { return nil }
 
 // kept for compatibility, it requires a plugin.
-func (*geoLookup) Evaluate(*coraza.Transaction, string) bool { return true }
+func (*geoLookup) Evaluate(rules.TransactionState, string) bool { return true }
diff --git a/operators/gt.go b/operators/gt.go
index 9567dfd..8395761 100644
--- a/operators/gt.go
+++ b/operators/gt.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type gt struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*gt)(nil)
+var _ rules.Operator = (*gt)(nil)
 
-func (o *gt) Init(options coraza.RuleOperatorOptions) error {
+func (o *gt) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *gt) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *gt) Evaluate(tx rules.TransactionState, value string) bool {
 	v, _ := strconv.Atoi(value)
 	k, _ := strconv.Atoi(o.data.Expand(tx))
 	return k < v
diff --git a/operators/inspect_file.go b/operators/inspect_file.go
index e27c776..1495406 100644
--- a/operators/inspect_file.go
+++ b/operators/inspect_file.go
@@ -11,21 +11,21 @@ import (
 	"os/exec"
 	"time"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type inspectFile struct {
 	path string
 }
 
-var _ coraza.RuleOperator = (*inspectFile)(nil)
+var _ rules.Operator = (*inspectFile)(nil)
 
-func (o *inspectFile) Init(options coraza.RuleOperatorOptions) error {
+func (o *inspectFile) Init(options rules.OperatorOptions) error {
 	o.path = options.Arguments
 	return nil
 }
 
-func (o *inspectFile) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *inspectFile) Evaluate(tx rules.TransactionState, value string) bool {
 	// TODO parametrize timeout
 	// TODO add relative path capabilities
 	// TODO add lua special support
diff --git a/operators/inspect_file_tinygo.go b/operators/inspect_file_tinygo.go
index 553d280..aadb6d9 100644
--- a/operators/inspect_file_tinygo.go
+++ b/operators/inspect_file_tinygo.go
@@ -7,11 +7,11 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type inspectFile struct{}
 
-func (*inspectFile) Init(coraza.RuleOperatorOptions) error { return nil }
+func (*inspectFile) Init(rules.OperatorOptions) error { return nil }
 
-func (*inspectFile) Evaluate(*coraza.Transaction, string) bool { return true }
+func (*inspectFile) Evaluate(rules.TransactionState, string) bool { return true }
diff --git a/operators/ip_match.go b/operators/ip_match.go
index eac6bb0..eb4c588 100644
--- a/operators/ip_match.go
+++ b/operators/ip_match.go
@@ -7,17 +7,16 @@ import (
 	"net"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
-	engine "github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type ipMatch struct {
 	subnets []*net.IPNet
 }
 
-var _ coraza.RuleOperator = (*ipMatch)(nil)
+var _ rules.Operator = (*ipMatch)(nil)
 
-func (o *ipMatch) Init(options coraza.RuleOperatorOptions) error {
+func (o *ipMatch) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	o.subnets = []*net.IPNet{}
@@ -43,7 +42,7 @@ func (o *ipMatch) Init(options coraza.RuleOperatorOptions) error {
 	return nil
 }
 
-func (o *ipMatch) Evaluate(tx *engine.Transaction, value string) bool {
+func (o *ipMatch) Evaluate(tx rules.TransactionState, value string) bool {
 	ip := net.ParseIP(value)
 	for _, subnet := range o.subnets {
 		if subnet.Contains(ip) {
diff --git a/operators/ip_match_from_dataset.go b/operators/ip_match_from_dataset.go
index 9e54c6d..28bf4d5 100644
--- a/operators/ip_match_from_dataset.go
+++ b/operators/ip_match_from_dataset.go
@@ -7,16 +7,16 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type ipMatchFromDataset struct {
 	matcher *ipMatch
 }
 
-var _ coraza.RuleOperator = (*ipMatchFromDataset)(nil)
+var _ rules.Operator = (*ipMatchFromDataset)(nil)
 
-func (o *ipMatchFromDataset) Init(options coraza.RuleOperatorOptions) error {
+func (o *ipMatchFromDataset) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 	dataset, ok := options.Datasets[data]
 	if !ok || len(dataset) == 0 {
@@ -26,14 +26,12 @@ func (o *ipMatchFromDataset) Init(options coraza.RuleOperatorOptions) error {
 	datasetParsed := strings.Join(dataset, ",")
 
 	o.matcher = &ipMatch{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: datasetParsed,
 	}
 	return o.matcher.Init(opts)
 }
 
-func (o *ipMatchFromDataset) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *ipMatchFromDataset) Evaluate(tx rules.TransactionState, value string) bool {
 	return o.matcher.Evaluate(tx, value)
 }
-
-var _ coraza.RuleOperator = (*ipMatchFromDataset)(nil)
diff --git a/operators/ip_match_from_file.go b/operators/ip_match_from_file.go
index 7bde059..815783a 100644
--- a/operators/ip_match_from_file.go
+++ b/operators/ip_match_from_file.go
@@ -8,16 +8,16 @@ import (
 	"bytes"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type ipMatchFromFile struct {
 	ipMatcher *ipMatch
 }
 
-var _ coraza.RuleOperator = (*ipMatchFromFile)(nil)
+var _ rules.Operator = (*ipMatchFromFile)(nil)
 
-func (o *ipMatchFromFile) Init(options coraza.RuleOperatorOptions) error {
+func (o *ipMatchFromFile) Init(options rules.OperatorOptions) error {
 	path := options.Arguments
 
 	data, err := loadFromFile(path, options.Path, options.Root)
@@ -41,14 +41,14 @@ func (o *ipMatchFromFile) Init(options coraza.RuleOperatorOptions) error {
 	}
 
 	o.ipMatcher = &ipMatch{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: dataParsed.String(),
 	}
 	return o.ipMatcher.Init(opts)
 }
 
-func (o *ipMatchFromFile) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *ipMatchFromFile) Evaluate(tx rules.TransactionState, value string) bool {
 	return o.ipMatcher.Evaluate(tx, value)
 }
 
-var _ coraza.RuleOperator = (*ipMatchFromFile)(nil)
+var _ rules.Operator = (*ipMatchFromFile)(nil)
diff --git a/operators/le.go b/operators/le.go
index d90d01e..734128a 100644
--- a/operators/le.go
+++ b/operators/le.go
@@ -6,26 +6,27 @@ package operators
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type le struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*le)(nil)
+var _ rules.Operator = (*le)(nil)
 
-func (o *le) Init(options coraza.RuleOperatorOptions) error {
+func (o *le) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *le) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *le) Evaluate(tx rules.TransactionState, value string) bool {
 	d, _ := strconv.Atoi(o.data.Expand(tx))
 	v, _ := strconv.Atoi(value)
 	return v <= d
diff --git a/operators/lt.go b/operators/lt.go
index 7e175c7..a90f3f2 100644
--- a/operators/lt.go
+++ b/operators/lt.go
@@ -6,27 +6,28 @@ package operators
 import (
 	"strconv"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type lt struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*lt)(nil)
+var _ rules.Operator = (*lt)(nil)
 
-func (o *lt) Init(options coraza.RuleOperatorOptions) error {
+func (o *lt) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *lt) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *lt) Evaluate(tx rules.TransactionState, value string) bool {
 	vv := o.data.Expand(tx)
 	data, _ := strconv.Atoi(vv)
 	v, _ := strconv.Atoi(value)
diff --git a/operators/no_match.go b/operators/no_match.go
index 4847759..31df672 100644
--- a/operators/no_match.go
+++ b/operators/no_match.go
@@ -4,13 +4,13 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type noMatch struct{}
 
-var _ coraza.RuleOperator = (*noMatch)(nil)
+var _ rules.Operator = (*noMatch)(nil)
 
-func (*noMatch) Init(options coraza.RuleOperatorOptions) error { return nil }
+func (*noMatch) Init(options rules.OperatorOptions) error { return nil }
 
-func (*noMatch) Evaluate(tx *coraza.Transaction, value string) bool { return false }
+func (*noMatch) Evaluate(tx rules.TransactionState, value string) bool { return false }
diff --git a/operators/operators.go b/operators/operators.go
index 0847735..6e5d0bb 100644
--- a/operators/operators.go
+++ b/operators/operators.go
@@ -6,45 +6,45 @@ package operators
 import (
 	"fmt"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
-type operatorsWrapper = func() coraza.RuleOperator
+type operatorsWrapper = func() rules.Operator
 
 var operators = map[string]operatorsWrapper{}
 
 func init() {
-	Register("beginsWith", func() coraza.RuleOperator { return &beginsWith{} })
-	Register("rx", func() coraza.RuleOperator { return &rx{} })
-	Register("eq", func() coraza.RuleOperator { return &eq{} })
-	Register("contains", func() coraza.RuleOperator { return &contains{} })
-	Register("endsWith", func() coraza.RuleOperator { return &endsWith{} })
-	Register("inspectFile", func() coraza.RuleOperator { return &inspectFile{} })
-	Register("ge", func() coraza.RuleOperator { return &ge{} })
-	Register("gt", func() coraza.RuleOperator { return &gt{} })
-	Register("le", func() coraza.RuleOperator { return &le{} })
-	Register("lt", func() coraza.RuleOperator { return &lt{} })
-	Register("unconditionalMatch", func() coraza.RuleOperator { return &unconditionalMatch{} })
-	Register("within", func() coraza.RuleOperator { return &within{} })
-	Register("pmFromFile", func() coraza.RuleOperator { return &pmFromFile{} })
-	Register("pm", func() coraza.RuleOperator { return &pm{} })
-	Register("validateByteRange", func() coraza.RuleOperator { return &validateByteRange{} })
-	Register("validateUrlEncoding", func() coraza.RuleOperator { return &validateURLEncoding{} })
-	Register("streq", func() coraza.RuleOperator { return &streq{} })
-	Register("ipMatch", func() coraza.RuleOperator { return &ipMatch{} })
-	Register("ipMatchFromFile", func() coraza.RuleOperator { return &ipMatchFromFile{} })
-	Register("ipMatchFromDataset", func() coraza.RuleOperator { return &ipMatchFromDataset{} })
-	Register("rbl", func() coraza.RuleOperator { return &rbl{} })
-	Register("validateUtf8Encoding", func() coraza.RuleOperator { return &validateUtf8Encoding{} })
-	Register("noMatch", func() coraza.RuleOperator { return &noMatch{} })
-	Register("validateNid", func() coraza.RuleOperator { return &validateNid{} })
-	Register("geoLookup", func() coraza.RuleOperator { return &geoLookup{} })
-	Register("detectSQLi", func() coraza.RuleOperator { return &detectSQLi{} })
-	Register("detectXSS", func() coraza.RuleOperator { return &detectXSS{} })
+	Register("beginsWith", func() rules.Operator { return &beginsWith{} })
+	Register("rx", func() rules.Operator { return &rx{} })
+	Register("eq", func() rules.Operator { return &eq{} })
+	Register("contains", func() rules.Operator { return &contains{} })
+	Register("endsWith", func() rules.Operator { return &endsWith{} })
+	Register("inspectFile", func() rules.Operator { return &inspectFile{} })
+	Register("ge", func() rules.Operator { return &ge{} })
+	Register("gt", func() rules.Operator { return &gt{} })
+	Register("le", func() rules.Operator { return &le{} })
+	Register("lt", func() rules.Operator { return &lt{} })
+	Register("unconditionalMatch", func() rules.Operator { return &unconditionalMatch{} })
+	Register("within", func() rules.Operator { return &within{} })
+	Register("pmFromFile", func() rules.Operator { return &pmFromFile{} })
+	Register("pm", func() rules.Operator { return &pm{} })
+	Register("validateByteRange", func() rules.Operator { return &validateByteRange{} })
+	Register("validateUrlEncoding", func() rules.Operator { return &validateURLEncoding{} })
+	Register("streq", func() rules.Operator { return &streq{} })
+	Register("ipMatch", func() rules.Operator { return &ipMatch{} })
+	Register("ipMatchFromFile", func() rules.Operator { return &ipMatchFromFile{} })
+	Register("ipMatchFromDataset", func() rules.Operator { return &ipMatchFromDataset{} })
+	Register("rbl", func() rules.Operator { return &rbl{} })
+	Register("validateUtf8Encoding", func() rules.Operator { return &validateUtf8Encoding{} })
+	Register("noMatch", func() rules.Operator { return &noMatch{} })
+	Register("validateNid", func() rules.Operator { return &validateNid{} })
+	Register("geoLookup", func() rules.Operator { return &geoLookup{} })
+	Register("detectSQLi", func() rules.Operator { return &detectSQLi{} })
+	Register("detectXSS", func() rules.Operator { return &detectXSS{} })
 }
 
 // Get returns an operator by name
-func Get(name string) (coraza.RuleOperator, error) {
+func Get(name string) (rules.Operator, error) {
 	if op, ok := operators[name]; ok {
 		return op(), nil
 	}
@@ -53,6 +53,6 @@ func Get(name string) (coraza.RuleOperator, error) {
 
 // Register registers a new operator
 // If the operator already exists it will be overwritten
-func Register(name string, op func() coraza.RuleOperator) {
+func Register(name string, op func() rules.Operator) {
 	operators[name] = op
 }
diff --git a/operators/pm.go b/operators/pm.go
index aab2b8f..a0f9e14 100644
--- a/operators/pm.go
+++ b/operators/pm.go
@@ -8,7 +8,7 @@ import (
 
 	ahocorasick "github.com/petar-dambovaliev/aho-corasick"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 // TODO according to coraza researchs, re2 matching is faster than ahocorasick
@@ -18,9 +18,9 @@ type pm struct {
 	matcher ahocorasick.AhoCorasick
 }
 
-var _ coraza.RuleOperator = (*pm)(nil)
+var _ rules.Operator = (*pm)(nil)
 
-func (o *pm) Init(options coraza.RuleOperatorOptions) error {
+func (o *pm) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	data = strings.ToLower(data)
@@ -37,14 +37,14 @@ func (o *pm) Init(options coraza.RuleOperatorOptions) error {
 	return nil
 }
 
-func (o *pm) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *pm) Evaluate(tx rules.TransactionState, value string) bool {
 	return pmEvaluate(o.matcher, tx, value)
 }
 
-func pmEvaluate(matcher ahocorasick.AhoCorasick, tx *coraza.Transaction, value string) bool {
+func pmEvaluate(matcher ahocorasick.AhoCorasick, tx rules.TransactionState, value string) bool {
 	iter := matcher.Iter(value)
 
-	if !tx.Capture {
+	if !tx.Capturing() {
 		// Not capturing so just one match is enough.
 		return iter.Next() != nil
 	}
diff --git a/operators/pm_from_dataset.go b/operators/pm_from_dataset.go
index 68931d7..7dfb55b 100644
--- a/operators/pm_from_dataset.go
+++ b/operators/pm_from_dataset.go
@@ -8,7 +8,7 @@ import (
 
 	ahocorasick "github.com/petar-dambovaliev/aho-corasick"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 // TODO according to coraza researchs, re2 matching is faster than ahocorasick
@@ -18,9 +18,9 @@ type pmFromDataset struct {
 	matcher ahocorasick.AhoCorasick
 }
 
-var _ coraza.RuleOperator = (*pmFromDataset)(nil)
+var _ rules.Operator = (*pmFromDataset)(nil)
 
-func (o *pmFromDataset) Init(options coraza.RuleOperatorOptions) error {
+func (o *pmFromDataset) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 	dataset, ok := options.Datasets[data]
 	if !ok {
@@ -37,6 +37,6 @@ func (o *pmFromDataset) Init(options coraza.RuleOperatorOptions) error {
 	return nil
 }
 
-func (o *pmFromDataset) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *pmFromDataset) Evaluate(tx rules.TransactionState, value string) bool {
 	return pmEvaluate(o.matcher, tx, value)
 }
diff --git a/operators/pm_from_file.go b/operators/pm_from_file.go
index a7dc523..2f52333 100644
--- a/operators/pm_from_file.go
+++ b/operators/pm_from_file.go
@@ -10,16 +10,16 @@ import (
 
 	ahocorasick "github.com/petar-dambovaliev/aho-corasick"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type pmFromFile struct {
 	matcher ahocorasick.AhoCorasick
 }
 
-var _ coraza.RuleOperator = (*pmFromFile)(nil)
+var _ rules.Operator = (*pmFromFile)(nil)
 
-func (o *pmFromFile) Init(options coraza.RuleOperatorOptions) error {
+func (o *pmFromFile) Init(options rules.OperatorOptions) error {
 	path := options.Arguments
 
 	data, err := loadFromFile(path, options.Path, options.Root)
@@ -52,6 +52,6 @@ func (o *pmFromFile) Init(options coraza.RuleOperatorOptions) error {
 	return nil
 }
 
-func (o *pmFromFile) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *pmFromFile) Evaluate(tx rules.TransactionState, value string) bool {
 	return pmEvaluate(o.matcher, tx, value)
 }
diff --git a/operators/rbl.go b/operators/rbl.go
index b39dcd4..df2eb6a 100644
--- a/operators/rbl.go
+++ b/operators/rbl.go
@@ -12,7 +12,7 @@ import (
 	"net"
 	"time"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 const timeout = 500 * time.Millisecond
@@ -22,9 +22,9 @@ type rbl struct {
 	resolver *net.Resolver
 }
 
-var _ coraza.RuleOperator = (*rbl)(nil)
+var _ rules.Operator = (*rbl)(nil)
 
-func (o *rbl) Init(options coraza.RuleOperatorOptions) error {
+func (o *rbl) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	o.service = data
@@ -35,7 +35,7 @@ func (o *rbl) Init(options coraza.RuleOperatorOptions) error {
 
 // https://github.com/mrichman/godnsbl
 // https://github.com/SpiderLabs/ModSecurity/blob/b66224853b4e9d30e0a44d16b29d5ed3842a6b11/src/operators/rbl.cc
-func (o *rbl) Evaluate(tx *coraza.Transaction, ipAddr string) bool {
+func (o *rbl) Evaluate(tx rules.TransactionState, ipAddr string) bool {
 	// TODO validate address
 	resC := make(chan bool)
 	ctx, cancel := context.WithCancel(context.Background())
@@ -67,7 +67,7 @@ func (o *rbl) Evaluate(tx *coraza.Transaction, ipAddr string) bool {
 			if len(txt) > 0 {
 				status := txt[0]
 				captures = append(captures, status)
-				tx.Variables.TX.Set("httpbl_msg", []string{status})
+				tx.GetVariables().GetTX().Set("httpbl_msg", []string{status})
 			}
 		}
 
diff --git a/operators/rbl_tinygo.go b/operators/rbl_tinygo.go
index bc64e30..8be3ce1 100644
--- a/operators/rbl_tinygo.go
+++ b/operators/rbl_tinygo.go
@@ -7,11 +7,11 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type rbl struct{}
 
-func (o *rbl) Init(_ coraza.RuleOperatorOptions) error { return nil }
+func (o *rbl) Init(_ rules.OperatorOptions) error { return nil }
 
-func (o *rbl) Evaluate(_ *coraza.Transaction, _ string) bool { return true }
+func (o *rbl) Evaluate(_ rules.TransactionState, _ string) bool { return true }
diff --git a/operators/restpath.go b/operators/restpath.go
index d803ca5..99913f7 100644
--- a/operators/restpath.go
+++ b/operators/restpath.go
@@ -8,7 +8,7 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 var rePathTokenRe = regexp.MustCompile(`\{([^\}]+)\}`)
@@ -21,9 +21,9 @@ type restpath struct {
 	re *regexp.Regexp
 }
 
-var _ coraza.RuleOperator = (*restpath)(nil)
+var _ rules.Operator = (*restpath)(nil)
 
-func (o *restpath) Init(options coraza.RuleOperatorOptions) error {
+func (o *restpath) Init(options rules.OperatorOptions) error {
 	data := strings.ReplaceAll(options.Arguments, "/", "\\/")
 	for _, token := range rePathTokenRe.FindAllStringSubmatch(data, -1) {
 		data = strings.Replace(data, token[0], fmt.Sprintf("(?P<%s>.*)", token[1]), 1)
@@ -33,7 +33,7 @@ func (o *restpath) Init(options coraza.RuleOperatorOptions) error {
 	return err
 }
 
-func (o *restpath) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *restpath) Evaluate(tx rules.TransactionState, value string) bool {
 	// we use the re regex to match the path and match named captured groups
 	// to the ARGS_PATH
 	match := o.re.FindStringSubmatch(value)
@@ -42,14 +42,14 @@ func (o *restpath) Evaluate(tx *coraza.Transaction, value string) bool {
 	}
 	for i, m := range o.re.SubexpNames() {
 		if i != 0 && m != "" {
-			tx.Variables.ArgsPath.SetIndex(m, 0, match[i])
+			tx.GetVariables().GetArgsPath().SetIndex(m, 0, match[i])
 		}
 	}
 	return true
 }
 
 func init() {
-	Register("restpath", func() coraza.RuleOperator {
+	Register("restpath", func() rules.Operator {
 		return &restpath{}
 	})
 }
diff --git a/operators/rx.go b/operators/rx.go
index 99a22d4..3551c05 100644
--- a/operators/rx.go
+++ b/operators/rx.go
@@ -6,16 +6,16 @@ package operators
 import (
 	"regexp"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type rx struct {
 	re *regexp.Regexp
 }
 
-var _ coraza.RuleOperator = (*rx)(nil)
+var _ rules.Operator = (*rx)(nil)
 
-func (o *rx) Init(options coraza.RuleOperatorOptions) error {
+func (o *rx) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	re, err := regexp.Compile(data)
@@ -23,13 +23,13 @@ func (o *rx) Init(options coraza.RuleOperatorOptions) error {
 	return err
 }
 
-func (o *rx) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *rx) Evaluate(tx rules.TransactionState, value string) bool {
 	match := o.re.FindStringSubmatch(value)
 	if len(match) == 0 {
 		return false
 	}
 
-	if tx.Capture {
+	if tx.Capturing() {
 		for i, c := range match {
 			if i == 9 {
 				return true
diff --git a/operators/streq.go b/operators/streq.go
index 0ddd27b..6bbc0f2 100644
--- a/operators/streq.go
+++ b/operators/streq.go
@@ -4,27 +4,26 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type streq struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-var _ coraza.RuleOperator = (*streq)(nil)
-
-func (o *streq) Init(options coraza.RuleOperatorOptions) error {
+func (o *streq) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *streq) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *streq) Evaluate(tx rules.TransactionState, value string) bool {
 	data := o.data.Expand(tx)
 	return data == value
 }
diff --git a/operators/unconditional_match.go b/operators/unconditional_match.go
index 2eaaeba..849175b 100644
--- a/operators/unconditional_match.go
+++ b/operators/unconditional_match.go
@@ -4,11 +4,11 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type unconditionalMatch struct{}
 
-func (*unconditionalMatch) Init(coraza.RuleOperatorOptions) error { return nil }
+func (*unconditionalMatch) Init(rules.OperatorOptions) error { return nil }
 
-func (*unconditionalMatch) Evaluate(*coraza.Transaction, string) bool { return true }
+func (*unconditionalMatch) Evaluate(rules.TransactionState, string) bool { return true }
diff --git a/operators/validate_byte_range.go b/operators/validate_byte_range.go
index ea8ad37..4d39f05 100644
--- a/operators/validate_byte_range.go
+++ b/operators/validate_byte_range.go
@@ -8,7 +8,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type byteRange struct {
@@ -20,9 +20,9 @@ type validateByteRange struct {
 	data []byteRange
 }
 
-var _ coraza.RuleOperator = (*validateByteRange)(nil)
+var _ rules.Operator = (*validateByteRange)(nil)
 
-func (o *validateByteRange) Init(options coraza.RuleOperatorOptions) error {
+func (o *validateByteRange) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	if data == "" {
@@ -61,7 +61,7 @@ func (o *validateByteRange) Init(options coraza.RuleOperatorOptions) error {
 	return nil
 }
 
-func (o *validateByteRange) Evaluate(tx *coraza.Transaction, data string) bool {
+func (o *validateByteRange) Evaluate(tx rules.TransactionState, data string) bool {
 	lenData := len(o.data)
 	if lenData == 0 {
 		return true
diff --git a/operators/validate_nid.go b/operators/validate_nid.go
index 0918e82..33fdf17 100644
--- a/operators/validate_nid.go
+++ b/operators/validate_nid.go
@@ -9,7 +9,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type validateNidFunction = func(input string) bool
@@ -19,9 +19,9 @@ type validateNid struct {
 	re *regexp.Regexp
 }
 
-var _ coraza.RuleOperator = (*validateNid)(nil)
+var _ rules.Operator = (*validateNid)(nil)
 
-func (o *validateNid) Init(options coraza.RuleOperatorOptions) error {
+func (o *validateNid) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
 	spl := strings.SplitN(data, " ", 2)
@@ -44,7 +44,7 @@ func (o *validateNid) Init(options coraza.RuleOperatorOptions) error {
 	return err
 }
 
-func (o *validateNid) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *validateNid) Evaluate(tx rules.TransactionState, value string) bool {
 	matches := o.re.FindAllStringSubmatch(value, 11)
 
 	res := false
@@ -55,9 +55,7 @@ func (o *validateNid) Evaluate(tx *coraza.Transaction, value string) bool {
 		// should we capture more than one NID?
 		if o.fn(m[0]) {
 			res = true
-			if tx.Capture {
-				tx.CaptureField(i, m[0])
-			}
+			tx.CaptureField(i, m[0])
 		}
 	}
 	return res
@@ -134,7 +132,7 @@ func digitToInt(d byte) int {
 }
 
 var (
-	_ coraza.RuleOperator = &validateNid{}
+	_ rules.Operator      = &validateNid{}
 	_ validateNidFunction = nidCl
 	_ validateNidFunction = nidUs
 )
diff --git a/operators/validate_url_encoding.go b/operators/validate_url_encoding.go
index d6c7500..2280d10 100644
--- a/operators/validate_url_encoding.go
+++ b/operators/validate_url_encoding.go
@@ -4,16 +4,16 @@
 package operators
 
 import (
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type validateURLEncoding struct{}
 
-var _ coraza.RuleOperator = (*validateURLEncoding)(nil)
+var _ rules.Operator = (*validateURLEncoding)(nil)
 
-func (o *validateURLEncoding) Init(coraza.RuleOperatorOptions) error { return nil }
+func (o *validateURLEncoding) Init(rules.OperatorOptions) error { return nil }
 
-func (o *validateURLEncoding) Evaluate(_ *coraza.Transaction, value string) bool {
+func (o *validateURLEncoding) Evaluate(_ rules.TransactionState, value string) bool {
 	if len(value) == 0 {
 		return false
 	}
diff --git a/operators/validate_utf8_encoding.go b/operators/validate_utf8_encoding.go
index e2fb2f7..7f8b20b 100644
--- a/operators/validate_utf8_encoding.go
+++ b/operators/validate_utf8_encoding.go
@@ -6,15 +6,15 @@ package operators
 import (
 	"unicode/utf8"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type validateUtf8Encoding struct{}
 
-var _ coraza.RuleOperator = (*validateUtf8Encoding)(nil)
+var _ rules.Operator = (*validateUtf8Encoding)(nil)
 
-func (o *validateUtf8Encoding) Init(coraza.RuleOperatorOptions) error { return nil }
+func (o *validateUtf8Encoding) Init(rules.OperatorOptions) error { return nil }
 
-func (o *validateUtf8Encoding) Evaluate(_ *coraza.Transaction, value string) bool {
+func (o *validateUtf8Encoding) Evaluate(_ rules.TransactionState, value string) bool {
 	return utf8.ValidString(value)
 }
diff --git a/operators/within.go b/operators/within.go
index 8dd001b..17abc24 100644
--- a/operators/within.go
+++ b/operators/within.go
@@ -6,25 +6,26 @@ package operators
 import (
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/macro"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type within struct {
-	data coraza.Macro
+	data macro.Macro
 }
 
-func (o *within) Init(options coraza.RuleOperatorOptions) error {
+func (o *within) Init(options rules.OperatorOptions) error {
 	data := options.Arguments
 
-	macro, err := coraza.NewMacro(data)
+	m, err := macro.NewMacro(data)
 	if err != nil {
 		return err
 	}
-	o.data = *macro
+	o.data = m
 	return nil
 }
 
-func (o *within) Evaluate(tx *coraza.Transaction, value string) bool {
+func (o *within) Evaluate(tx rules.TransactionState, value string) bool {
 	data := o.data.Expand(tx)
 	return strings.Contains(data, value)
 }
diff --git a/types/actions.go b/rules/action.go
similarity index 51%
rename from types/actions.go
rename to rules/action.go
index 296af7f..efd7548 100644
--- a/types/actions.go
+++ b/rules/action.go
@@ -1,21 +1,33 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package types
+package rules
 
-// RuleActionType is used to define when an action is going
+// ActionType is used to define when an action is going
 // to be triggered
-type RuleActionType int
+type ActionType int
 
 const (
 	// ActionTypeMetadata is used to provide more information about rules.
-	ActionTypeMetadata RuleActionType = 1
+	ActionTypeMetadata ActionType = 1
 	// ActionTypeDisruptive is used to make the integrator do something like drop the request.
-	ActionTypeDisruptive RuleActionType = 2
+	ActionTypeDisruptive ActionType = 2
 	// ActionTypeData Not really actions, these are mere containers that hold data used by other actions.
-	ActionTypeData RuleActionType = 3
+	ActionTypeData ActionType = 3
 	// ActionTypeNondisruptive is used to do something that does not affect the flow of the rule.
-	ActionTypeNondisruptive RuleActionType = 4
+	ActionTypeNondisruptive ActionType = 4
 	// ActionTypeFlow is used to affect the rule flow (for example skip or skipAfter).
-	ActionTypeFlow RuleActionType = 5
+	ActionTypeFlow ActionType = 5
 )
+
+// Action is an action that can be used within a rule.
+type Action interface {
+	// Init initializes the action.
+	Init(RuleMetadata, string) error
+
+	// Evaluate evaluates the action.
+	Evaluate(RuleMetadata, TransactionState)
+
+	// Type returns the type of action.
+	Type() ActionType
+}
diff --git a/rules/operator.go b/rules/operator.go
new file mode 100644
index 0000000..00ab1e7
--- /dev/null
+++ b/rules/operator.go
@@ -0,0 +1,32 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package rules
+
+import "io/fs"
+
+// OperatorOptions is used to store the options for a rule operator
+type OperatorOptions struct {
+	// Arguments is used to store the operator args
+	Arguments string
+
+	// Path is used to store a list of possible data paths
+	Path []string
+
+	// Root is the root to resolve Path from.
+	Root fs.FS
+
+	// Datasets contains input datasets or dictionaries
+	Datasets map[string][]string
+}
+
+// Operator interface is used to define rule @operators
+type Operator interface {
+	// Init is used during compilation to setup and cache
+	// the operator
+	Init(OperatorOptions) error
+	// Evaluate is used during the rule evaluation,
+	// it returns true if the operator succeeded against
+	// the input data for the transaction
+	Evaluate(TransactionState, string) bool
+}
diff --git a/rules/rule.go b/rules/rule.go
new file mode 100644
index 0000000..32c38eb
--- /dev/null
+++ b/rules/rule.go
@@ -0,0 +1,24 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package rules
+
+import "github.com/corazawaf/coraza/v3/types"
+
+// Rule is a rule executed against a transaction.
+type Rule interface {
+	// Evaluate evaluates the rule, returning data related to matches if any.
+	Evaluate(state TransactionState) []types.MatchData
+}
+
+// RuleMetadata is information about a rule parsed from directives.
+type RuleMetadata interface {
+	// GetID returns the ID of the rule.
+	GetID() int
+
+	// GetParentID returns the ID of the parent of the rule for a chained rule.
+	GetParentID() int
+
+	// Status returns the status to set if the rule matches.
+	Status() int
+}
diff --git a/rules/transaction.go b/rules/transaction.go
new file mode 100644
index 0000000..91dafc6
--- /dev/null
+++ b/rules/transaction.go
@@ -0,0 +1,141 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package rules
+
+import (
+	"io"
+
+	"github.com/corazawaf/coraza/v3/collection"
+	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/types"
+	"github.com/corazawaf/coraza/v3/types/variables"
+)
+
+// TransactionState tracks the state of a transaction for use in actions and operators.
+type TransactionState interface {
+	// GetID returns the ID of the transaction.
+	GetID() string // TODO(anuraaga): If only for logging, can be built into logger
+
+	// GetVariables returns the TransactionVariables of the transaction.
+	GetVariables() TransactionVariables
+
+	// Collection returns a collection from the transaction.
+	Collection(idx variables.RuleVariable) collection.Collection
+
+	// Interrupt interrupts the transaction.
+	Interrupt(interruption *types.Interruption)
+
+	// ResponseBodyWriter allows writing to the response body.
+	// TODO(anuraaga): Should this be combined with interruption? Any action writing anything to response can be dangerous.
+	ResponseBodyWriter() io.Writer
+
+	// ContentInjection returns whether content injection is enabled for this transaction.
+	ContentInjection() bool // TODO(anuraaga): Should be resolved at Init time when WAF is truly immutable.
+	// DebugLogger returns the logger for this transaction.
+	DebugLogger() loggers.DebugLogger
+
+	// Capturing returns whether the transaction is capturing. CaptureField only works if capturing, this can be used
+	// as an optimization to avoid processing specific to capturing fields.
+	Capturing() bool // TODO(anuraaga): Only needed in operators?
+	// CaptureField captures a field.
+	CaptureField(idx int, value string)
+}
+
+// TransactionVariables has pointers to all the variables of the transaction
+type TransactionVariables interface {
+	// Simple Variables
+	GetUserID() *collection.Simple
+	GetUrlencodedError() *collection.Simple
+	GetResponseContentType() *collection.Simple
+	GetUniqueID() *collection.Simple
+	GetArgsCombinedSize() *collection.SizeProxy
+	GetAuthType() *collection.Simple
+	GetFilesCombinedSize() *collection.Simple
+	GetFullRequest() *collection.Simple
+	GetFullRequestLength() *collection.Simple
+	GetInboundDataError() *collection.Simple
+	GetMatchedVar() *collection.Simple
+	GetMatchedVarName() *collection.Simple
+	GetMultipartBoundaryQuoted() *collection.Simple
+	GetMultipartBoundaryWhitespace() *collection.Simple
+	GetMultipartCrlfLfLines() *collection.Simple
+	GetMultipartDataAfter() *collection.Simple
+	GetMultipartDataBefore() *collection.Simple
+	GetMultipartFileLimitExceeded() *collection.Simple
+	GetMultipartHeaderFolding() *collection.Simple
+	GetMultipartInvalidHeaderFolding() *collection.Simple
+	GetMultipartInvalidPart() *collection.Simple
+	GetMultipartInvalidQuoting() *collection.Simple
+	GetMultipartLfLine() *collection.Simple
+	GetMultipartMissingSemicolon() *collection.Simple
+	GetMultipartStrictError() *collection.Simple
+	GetMultipartUnmatchedBoundary() *collection.Simple
+	GetOutboundDataError() *collection.Simple
+	GetPathInfo() *collection.Simple
+	GetQueryString() *collection.Simple
+	GetRemoteAddr() *collection.Simple
+	GetRemoteHost() *collection.Simple
+	GetRemotePort() *collection.Simple
+	GetReqbodyError() *collection.Simple
+	GetReqbodyErrorMsg() *collection.Simple
+	GetReqbodyProcessorError() *collection.Simple
+	GetReqbodyProcessorErrorMsg() *collection.Simple
+	GetReqbodyProcessor() *collection.Simple
+	GetRequestBasename() *collection.Simple
+	GetRequestBody() *collection.Simple
+	GetRequestBodyLength() *collection.Simple
+	GetRequestFilename() *collection.Simple
+	GetRequestLine() *collection.Simple
+	GetRequestMethod() *collection.Simple
+	GetRequestProtocol() *collection.Simple
+	GetRequestURI() *collection.Simple
+	GetRequestURIRaw() *collection.Simple
+	GetResponseBody() *collection.Simple
+	GetResponseContentLength() *collection.Simple
+	GetResponseProtocol() *collection.Simple
+	GetResponseStatus() *collection.Simple
+	GetServerAddr() *collection.Simple
+	GetServerName() *collection.Simple
+	GetServerPort() *collection.Simple
+	GetSessionid() *collection.Simple
+	GetHighestSeverity() *collection.Simple
+	GetStatusLine() *collection.Simple
+	GetInboundErrorData() *collection.Simple
+	// Custom
+	GetEnv() *collection.Map
+	GetTX() *collection.Map
+	GetRule() *collection.Map
+	GetDuration() *collection.Simple
+	// Proxy Variables
+	GetArgs() *collection.Proxy
+	// Maps Variables
+	GetArgsGet() *collection.Map
+	GetArgsPost() *collection.Map
+	GetArgsPath() *collection.Map
+	GetFilesTmpNames() *collection.Map
+	GetGeo() *collection.Map
+	GetFiles() *collection.Map
+	GetRequestCookies() *collection.Map
+	GetRequestHeaders() *collection.Map
+	GetResponseHeaders() *collection.Map
+	GetMultipartName() *collection.Map
+	GetMatchedVarsNames() *collection.Map
+	GetMultipartFilename() *collection.Map
+	GetMatchedVars() *collection.Map
+	GetFilesSizes() *collection.Map
+	GetFilesNames() *collection.Map
+	GetFilesTmpContent() *collection.Map
+	GetResponseHeadersNames() *collection.Map
+	GetRequestHeadersNames() *collection.Map
+	GetRequestCookiesNames() *collection.Map
+	GetXML() *collection.Map
+	GetRequestXML() *collection.Map
+	GetResponseXML() *collection.Map
+	// Persistent variables
+	GetIP() *collection.Map
+	// Translation Proxy Variables
+	GetArgsNames() *collection.TranslationProxy
+	GetArgsGetNames() *collection.TranslationProxy
+	GetArgsPostNames() *collection.TranslationProxy
+}
diff --git a/rules/transformation.go b/rules/transformation.go
new file mode 100644
index 0000000..9a892af
--- /dev/null
+++ b/rules/transformation.go
@@ -0,0 +1,11 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package rules
+
+// Transformation is used to create transformation plugins
+// See the documentation for more information
+// If a transformation fails to run it will return the same string
+// and an error, errors are only used for logging, it won't stop
+// the execution of the rule
+type Transformation = func(input string) (string, error)
diff --git a/transformations/transformations.go b/transformations/transformations.go
index f9886b9..ec1ed4b 100644
--- a/transformations/transformations.go
+++ b/transformations/transformations.go
@@ -7,20 +7,20 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
-var transformations = map[string]coraza.RuleTransformation{}
+var transformations = map[string]rules.Transformation{}
 
 // RegisterPlugin registers a transformation by name
 // If the transformation is already registered, it will be overwritten
-func RegisterPlugin(name string, trans coraza.RuleTransformation) {
+func RegisterPlugin(name string, trans rules.Transformation) {
 	transformations[strings.ToLower(name)] = trans
 }
 
 // GetTransformation returns a transformation by name
 // If the transformation is not found, it returns an error
-func GetTransformation(name string) (coraza.RuleTransformation, error) {
+func GetTransformation(name string) (rules.Transformation, error) {
 	if t, ok := transformations[strings.ToLower(name)]; ok {
 		return t, nil
 	}
diff --git a/types/transaction.go b/types/transaction.go
new file mode 100644
index 0000000..2d67fd7
--- /dev/null
+++ b/types/transaction.go
@@ -0,0 +1,116 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package types
+
+import "io"
+
+// Transaction is created from a WAF instance to handle web requests and responses,
+// it contains a copy of most WAF configurations that can be safely changed.
+// Transactions are used to store all data like URLs, request and response
+// headers. Transactions are used to evaluate rules by phase and generate disruptive
+// actions. Disruptive actions can be read from *tx.Interruption.
+// It is safe to manage multiple transactions but transactions themself are not
+// thread safe
+type Transaction interface {
+	// ProcessConnection should be called at very beginning of a request process, it is
+	// expected to be executed prior to the virtual host resolution, when the
+	// connection arrives on the server.
+	// Important: Remember to check for a possible intervention.
+	ProcessConnection(client string, cPort int, server string, sPort int)
+
+	// ProcessURI Performs the analysis on the URI and all the query string variables.
+	// This method should be called at very beginning of a request process, it is
+	// expected to be executed prior to the virtual host resolution, when the
+	// connection arrives on the server.
+	// note: There is no direct connection between this function and any phase of
+	//
+	//	the SecLanguages phases. It is something that may occur between the
+	//	SecLanguage phase 1 and 2.
+	//
+	// note: This function won't add GET arguments, they must be added with AddArgument
+	ProcessURI(uri string, method string, httpVersion string)
+
+	// AddRequestHeader Adds a request header
+	//
+	// With this method it is possible to feed Coraza with a request header.
+	// Note: Golang's *http.Request object will not contain a "Host" header,
+	// and you might have to force it
+	AddRequestHeader(key string, value string)
+
+	// ProcessRequestHeaders Performs the analysis on the request readers.
+	//
+	// This method perform the analysis on the request headers, notice however
+	// that the headers should be added prior to the execution of this function.
+	//
+	// note: Remember to check for a possible intervention.
+	ProcessRequestHeaders() *Interruption
+
+	// RequestBodyWriter returns a io.Writer for writing the request body to.
+	// Contents will be buffered until the transaction is closed.
+	RequestBodyWriter() io.Writer
+
+	// RequestBodyReader returns a reader for content that has been written by
+	// RequestBodyWriter. This can be useful for buffering the request body
+	// within the Transaction while also passing it further in an HTTP framework.
+	RequestBodyReader() (io.Reader, error)
+
+	// ProcessRequestBody Performs the request body (if any)
+	//
+	// This method perform the analysis on the request body. It is optional to
+	// call that function. If this API consumer already know that there isn't a
+	// body for inspect it is recommended to skip this step.
+	//
+	// Remember to check for a possible intervention.
+	ProcessRequestBody() (*Interruption, error)
+
+	// AddResponseHeader Adds a response header variable
+	//
+	// With this method it is possible to feed Coraza with a response header.
+	AddResponseHeader(key string, value string)
+
+	// ProcessResponseHeaders Perform the analysis on the response readers.
+	//
+	// This method perform the analysis on the response headers, notice however
+	// that the headers should be added prior to the execution of this function.
+	//
+	// note: Remember to check for a possible intervention.
+	ProcessResponseHeaders(code int, proto string) *Interruption
+
+	// ResponseBodyWriter returns a io.Writer for writing the response body to.
+	// Contents will be buffered until the transaction is closed.
+	ResponseBodyWriter() io.Writer
+
+	// ResponseBodyReader returns a reader for content that has been written by
+	// ResponseBodyWriter. This can be useful for buffering the response body
+	// within the Transaction while also passing it further in an HTTP framework.
+	ResponseBodyReader() (io.Reader, error)
+
+	// ProcessResponseBody Perform the request body (if any)
+	//
+	// This method perform the analysis on the request body. It is optional to
+	// call that method. If this API consumer already know that there isn't a
+	// body for inspect it is recommended to skip this step.
+	//
+	// note Remember to check for a possible intervention.
+	ProcessResponseBody() (*Interruption, error)
+
+	// ProcessLogging Logging all information relative to this transaction.
+	// An error log
+	// At this point there is not need to hold the connection, the response can be
+	// delivered prior to the execution of this method.
+	ProcessLogging()
+
+	// Interrupted will return true if the transaction was interrupted
+	Interrupted() bool
+
+	// GetInterruption returns the types.Interruption if the request was interrupted,
+	// or nil otherwise.
+	GetInterruption() *Interruption
+
+	// GetMatchedRules returns the rules that have matched the requests with associated information.
+	GetMatchedRules() []MatchedRule
+
+	// Closer closes the transaction and releases any resources associated with it such as request/response bodies.
+	io.Closer
+}
diff --git a/waf.go b/waf.go
index 39482d6..a961407 100644
--- a/waf.go
+++ b/waf.go
@@ -6,34 +6,12 @@ package coraza
 import (
 	"context"
 	"fmt"
-	"io"
-	"io/fs"
-	"log"
-	"os"
-	"regexp"
-	"strconv"
-	"sync"
-	"time"
 
-	"github.com/corazawaf/coraza/v3/collection"
-	utils "github.com/corazawaf/coraza/v3/internal/strings"
-	"github.com/corazawaf/coraza/v3/loggers"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/internal/seclang"
 	"github.com/corazawaf/coraza/v3/types"
-	"github.com/corazawaf/coraza/v3/types/variables"
 )
 
-// Initializing pool for transactions
-var transactionPool = sync.Pool{
-	// New optionally specifies a function to generate
-	// a value when Get would otherwise return nil.
-	New: func() interface{} { return new(Transaction) },
-}
-
-// ErrorLogCallback is used to set a callback function to log errors
-// It is triggered when an error is raised by the WAF
-// It contains the severity so the cb can decide to log it or not
-type ErrorLogCallback = func(rule types.MatchedRule)
-
 // WAF instance is used to store configurations and rules
 // Every web application should have a different WAF instance,
 // but you can share an instance if you are ok with sharing
@@ -41,441 +19,85 @@ type ErrorLogCallback = func(rule types.MatchedRule)
 // Transactions and SecLang parser requires a WAF instance
 // You can use as many WAF instances as you want, and they are
 // concurrent safe
-// All WAF instance fields are immutable, if you update any
-// of them in runtime you might create concurrency issues
-type WAF struct {
-	// ruleGroup object, contains all rules and helpers
-	Rules RuleGroup
-
-	// Audit mode status
-	AuditEngine types.AuditEngineStatus
-
-	// Array of logging parts to be used
-	AuditLogParts types.AuditLogParts
-
-	// Status of the content injection for responses and requests
-	ContentInjection bool
-
-	// If true, transactions will have access to the request body
-	RequestBodyAccess bool
-
-	// Request body page file limit
-	RequestBodyLimit int64
-
-	// Request body in memory limit
-	RequestBodyInMemoryLimit int64
-
-	// If true, transactions will have access to the response body
-	ResponseBodyAccess bool
-
-	// Response body memory limit
-	ResponseBodyLimit int64
-
-	// Defines if rules are going to be evaluated
-	RuleEngine types.RuleEngineStatus
-
-	// If true, transaction will fail if response size is bigger than the page limit
-	RejectOnResponseBodyLimit bool
-
-	// If true, transaction will fail if request size is bigger than the page limit
-	RejectOnRequestBodyLimit bool
-
-	// Responses will only be loaded if mime is listed here
-	ResponseBodyMimeTypes []string
-
-	// Web Application id, apps sharing the same id will share persistent collections
-	WebAppID string
-
-	// Add significant rule components to audit log
-	ComponentNames []string
-
-	// Contains the regular expression for relevant status audit logging
-	AuditLogRelevantStatus *regexp.Regexp
-
-	// If true WAF engine will fail when remote rules cannot be loaded
-	AbortOnRemoteRulesFail bool
-
-	// Instructs the waf to change the Server response header
-	ServerSignature string
-
-	// This directory will be used to store page files
-	TmpDir string
-
-	// Sensor ID identifies the sensor in ac cluster
-	SensorID string
-
-	// Path to store data files (ex. cache)
-	DataDir string
-
-	// If true, the WAF will store the uploaded files in the UploadDir
-	// directory
-	UploadKeepFiles bool
-	// UploadFileMode instructs the waf to set the file mode for uploaded files
-	UploadFileMode fs.FileMode
-	// UploadFileLimit is the maximum size of the uploaded file to be stored
-	UploadFileLimit int
-	// UploadDir is the directory where the uploaded files will be stored
-	UploadDir string
-
-	RequestBodyNoFilesLimit int64
-
-	RequestBodyLimitAction types.RequestBodyLimitAction
-
-	ArgumentSeparator string
-
-	// ProducerConnector is used by connectors to identify the producer
-	// on audit logs, for example, apache-modcoraza
-	ProducerConnector string
-
-	// ProducerConnectorVersion is used by connectors to identify the producer
-	// version on audit logs
-	ProducerConnectorVersion string
-
-	// Used for the debug logger
-	Logger DebugLogger
-
-	errorLogCb ErrorLogCallback
-
-	// AuditLogWriter is used to write audit logs
-	AuditLogWriter loggers.LogWriter
+type WAF interface {
+	// NewTransaction Creates a new initialized transaction for this WAF instance
+	NewTransaction(ctx context.Context) types.Transaction
 }
 
-// NewTransaction Creates a new initialized transaction for this WAF instance
-func (w *WAF) NewTransaction(ctx context.Context) *Transaction {
-	tx := transactionPool.Get().(*Transaction)
-	tx.ID = utils.RandomString(19)
-	tx.MatchedRules = []types.MatchedRule{}
-	tx.Interruption = nil
-	tx.Collections = [types.VariablesCount]collection.Collection{}
-	tx.Logdata = ""
-	tx.SkipAfter = ""
-	tx.AuditEngine = w.AuditEngine
-	tx.AuditLogParts = w.AuditLogParts
-	tx.ForceRequestBodyVariable = false
-	tx.RequestBodyAccess = w.RequestBodyAccess
-	tx.RequestBodyLimit = w.RequestBodyLimit
-	tx.ResponseBodyAccess = w.ResponseBodyAccess
-	tx.ResponseBodyLimit = w.ResponseBodyLimit
-	tx.RuleEngine = w.RuleEngine
-	tx.HashEngine = false
-	tx.HashEnforcement = false
-	tx.LastPhase = 0
-	tx.RequestBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
-		TmpPath:     w.TmpDir,
-		MemoryLimit: w.RequestBodyInMemoryLimit,
-	})
-	tx.ResponseBodyBuffer = NewBodyBuffer(types.BodyBufferOptions{
-		TmpPath:     w.TmpDir,
-		MemoryLimit: w.RequestBodyInMemoryLimit,
-	})
-	tx.bodyProcessor = nil
-	tx.ruleRemoveByID = []int{}
-	tx.ruleRemoveTargetByID = map[int][]ruleVariableParams{}
-	tx.Skip = 0
-	tx.Capture = false
-	tx.stopWatches = map[types.RulePhase]int64{}
-	tx.WAF = w
-	tx.Timestamp = time.Now().UnixNano()
-	tx.audit = false
-
-	tx.Variables.UrlencodedError = collection.NewSimple(variables.UrlencodedError)
-	tx.Collections[variables.UrlencodedError] = tx.Variables.UrlencodedError
-	tx.Variables.ResponseContentType = collection.NewSimple(variables.ResponseContentType)
-	tx.Collections[variables.ResponseContentType] = tx.Variables.ResponseContentType
-	tx.Variables.UniqueID = collection.NewSimple(variables.UniqueID)
-	tx.Collections[variables.UniqueID] = tx.Variables.UniqueID
-	tx.Variables.AuthType = collection.NewSimple(variables.AuthType)
-	tx.Collections[variables.AuthType] = tx.Variables.AuthType
-	tx.Variables.FilesCombinedSize = collection.NewSimple(variables.FilesCombinedSize)
-	tx.Collections[variables.FilesCombinedSize] = tx.Variables.FilesCombinedSize
-	tx.Variables.FullRequest = collection.NewSimple(variables.FullRequest)
-	tx.Collections[variables.FullRequest] = tx.Variables.FullRequest
-	tx.Variables.FullRequestLength = collection.NewSimple(variables.FullRequestLength)
-	tx.Collections[variables.FullRequestLength] = tx.Variables.FullRequestLength
-	tx.Variables.InboundDataError = collection.NewSimple(variables.InboundDataError)
-	tx.Collections[variables.InboundDataError] = tx.Variables.InboundDataError
-	tx.Variables.MatchedVar = collection.NewSimple(variables.MatchedVar)
-	tx.Collections[variables.MatchedVar] = tx.Variables.MatchedVar
-	tx.Variables.MatchedVarName = collection.NewSimple(variables.MatchedVarName)
-	tx.Collections[variables.MatchedVarName] = tx.Variables.MatchedVarName
-	tx.Variables.MultipartBoundaryQuoted = collection.NewSimple(variables.MultipartBoundaryQuoted)
-	tx.Collections[variables.MultipartBoundaryQuoted] = tx.Variables.MultipartBoundaryQuoted
-	tx.Variables.MultipartBoundaryWhitespace = collection.NewSimple(variables.MultipartBoundaryWhitespace)
-	tx.Collections[variables.MultipartBoundaryWhitespace] = tx.Variables.MultipartBoundaryWhitespace
-	tx.Variables.MultipartCrlfLfLines = collection.NewSimple(variables.MultipartCrlfLfLines)
-	tx.Collections[variables.MultipartCrlfLfLines] = tx.Variables.MultipartCrlfLfLines
-	tx.Variables.MultipartDataAfter = collection.NewSimple(variables.MultipartDataAfter)
-	tx.Collections[variables.MultipartDataAfter] = tx.Variables.MultipartDataAfter
-	tx.Variables.MultipartDataBefore = collection.NewSimple(variables.MultipartDataBefore)
-	tx.Collections[variables.MultipartDataBefore] = tx.Variables.MultipartDataBefore
-	tx.Variables.MultipartFileLimitExceeded = collection.NewSimple(variables.MultipartFileLimitExceeded)
-	tx.Collections[variables.MultipartFileLimitExceeded] = tx.Variables.MultipartFileLimitExceeded
-	tx.Variables.MultipartHeaderFolding = collection.NewSimple(variables.MultipartHeaderFolding)
-	tx.Collections[variables.MultipartHeaderFolding] = tx.Variables.MultipartHeaderFolding
-	tx.Variables.MultipartInvalidHeaderFolding = collection.NewSimple(variables.MultipartInvalidHeaderFolding)
-	tx.Collections[variables.MultipartInvalidHeaderFolding] = tx.Variables.MultipartInvalidHeaderFolding
-	tx.Variables.MultipartInvalidPart = collection.NewSimple(variables.MultipartInvalidPart)
-	tx.Collections[variables.MultipartInvalidPart] = tx.Variables.MultipartInvalidPart
-	tx.Variables.MultipartInvalidQuoting = collection.NewSimple(variables.MultipartInvalidQuoting)
-	tx.Collections[variables.MultipartInvalidQuoting] = tx.Variables.MultipartInvalidQuoting
-	tx.Variables.MultipartLfLine = collection.NewSimple(variables.MultipartLfLine)
-	tx.Collections[variables.MultipartLfLine] = tx.Variables.MultipartLfLine
-	tx.Variables.MultipartMissingSemicolon = collection.NewSimple(variables.MultipartMissingSemicolon)
-	tx.Collections[variables.MultipartMissingSemicolon] = tx.Variables.MultipartMissingSemicolon
-	tx.Variables.MultipartStrictError = collection.NewSimple(variables.MultipartStrictError)
-	tx.Collections[variables.MultipartStrictError] = tx.Variables.MultipartStrictError
-	tx.Variables.MultipartUnmatchedBoundary = collection.NewSimple(variables.MultipartUnmatchedBoundary)
-	tx.Collections[variables.MultipartUnmatchedBoundary] = tx.Variables.MultipartUnmatchedBoundary
-	tx.Variables.OutboundDataError = collection.NewSimple(variables.OutboundDataError)
-	tx.Collections[variables.OutboundDataError] = tx.Variables.OutboundDataError
-	tx.Variables.PathInfo = collection.NewSimple(variables.PathInfo)
-	tx.Collections[variables.PathInfo] = tx.Variables.PathInfo
-	tx.Variables.QueryString = collection.NewSimple(variables.QueryString)
-	tx.Collections[variables.QueryString] = tx.Variables.QueryString
-	tx.Variables.RemoteAddr = collection.NewSimple(variables.RemoteAddr)
-	tx.Collections[variables.RemoteAddr] = tx.Variables.RemoteAddr
-	tx.Variables.RemoteHost = collection.NewSimple(variables.RemoteHost)
-	tx.Collections[variables.RemoteHost] = tx.Variables.RemoteHost
-	tx.Variables.RemotePort = collection.NewSimple(variables.RemotePort)
-	tx.Collections[variables.RemotePort] = tx.Variables.RemotePort
-	tx.Variables.ReqbodyError = collection.NewSimple(variables.ReqbodyError)
-	tx.Collections[variables.ReqbodyError] = tx.Variables.ReqbodyError
-	tx.Variables.ReqbodyErrorMsg = collection.NewSimple(variables.ReqbodyErrorMsg)
-	tx.Collections[variables.ReqbodyErrorMsg] = tx.Variables.ReqbodyErrorMsg
-	tx.Variables.ReqbodyProcessorError = collection.NewSimple(variables.ReqbodyProcessorError)
-	tx.Collections[variables.ReqbodyProcessorError] = tx.Variables.ReqbodyProcessorError
-	tx.Variables.ReqbodyProcessorErrorMsg = collection.NewSimple(variables.ReqbodyProcessorErrorMsg)
-	tx.Collections[variables.ReqbodyProcessorErrorMsg] = tx.Variables.ReqbodyProcessorErrorMsg
-	tx.Variables.ReqbodyProcessor = collection.NewSimple(variables.ReqbodyProcessor)
-	tx.Collections[variables.ReqbodyProcessor] = tx.Variables.ReqbodyProcessor
-	tx.Variables.RequestBasename = collection.NewSimple(variables.RequestBasename)
-	tx.Collections[variables.RequestBasename] = tx.Variables.RequestBasename
-	tx.Variables.RequestBody = collection.NewSimple(variables.RequestBody)
-	tx.Collections[variables.RequestBody] = tx.Variables.RequestBody
-	tx.Variables.RequestBodyLength = collection.NewSimple(variables.RequestBodyLength)
-	tx.Collections[variables.RequestBodyLength] = tx.Variables.RequestBodyLength
-	tx.Variables.RequestFilename = collection.NewSimple(variables.RequestFilename)
-	tx.Collections[variables.RequestFilename] = tx.Variables.RequestFilename
-	tx.Variables.RequestLine = collection.NewSimple(variables.RequestLine)
-	tx.Collections[variables.RequestLine] = tx.Variables.RequestLine
-	tx.Variables.RequestMethod = collection.NewSimple(variables.RequestMethod)
-	tx.Collections[variables.RequestMethod] = tx.Variables.RequestMethod
-	tx.Variables.RequestProtocol = collection.NewSimple(variables.RequestProtocol)
-	tx.Collections[variables.RequestProtocol] = tx.Variables.RequestProtocol
-	tx.Variables.RequestURI = collection.NewSimple(variables.RequestURI)
-	tx.Collections[variables.RequestURI] = tx.Variables.RequestURI
-	tx.Variables.RequestURIRaw = collection.NewSimple(variables.RequestURIRaw)
-	tx.Collections[variables.RequestURIRaw] = tx.Variables.RequestURIRaw
-	tx.Variables.ResponseBody = collection.NewSimple(variables.ResponseBody)
-	tx.Collections[variables.ResponseBody] = tx.Variables.ResponseBody
-	tx.Variables.ResponseContentLength = collection.NewSimple(variables.ResponseContentLength)
-	tx.Collections[variables.ResponseContentLength] = tx.Variables.ResponseContentLength
-	tx.Variables.ResponseProtocol = collection.NewSimple(variables.ResponseProtocol)
-	tx.Collections[variables.ResponseProtocol] = tx.Variables.ResponseProtocol
-	tx.Variables.ResponseStatus = collection.NewSimple(variables.ResponseStatus)
-	tx.Collections[variables.ResponseStatus] = tx.Variables.ResponseStatus
-	tx.Variables.ServerAddr = collection.NewSimple(variables.ServerAddr)
-	tx.Collections[variables.ServerAddr] = tx.Variables.ServerAddr
-	tx.Variables.ServerName = collection.NewSimple(variables.ServerName)
-	tx.Collections[variables.ServerName] = tx.Variables.ServerName
-	tx.Variables.ServerPort = collection.NewSimple(variables.ServerPort)
-	tx.Collections[variables.ServerPort] = tx.Variables.ServerPort
-	tx.Variables.Sessionid = collection.NewSimple(variables.Sessionid)
-	tx.Collections[variables.Sessionid] = tx.Variables.Sessionid
-	tx.Variables.HighestSeverity = collection.NewSimple(variables.HighestSeverity)
-	tx.Collections[variables.HighestSeverity] = tx.Variables.HighestSeverity
-	tx.Variables.StatusLine = collection.NewSimple(variables.StatusLine)
-	tx.Collections[variables.StatusLine] = tx.Variables.StatusLine
-	tx.Variables.InboundErrorData = collection.NewSimple(variables.InboundErrorData)
-	tx.Collections[variables.InboundErrorData] = tx.Variables.InboundErrorData
-	tx.Variables.Duration = collection.NewSimple(variables.Duration)
-	tx.Collections[variables.Duration] = tx.Variables.Duration
-	tx.Variables.ResponseHeadersNames = collection.NewMap(variables.ResponseHeadersNames)
-	tx.Collections[variables.ResponseHeadersNames] = tx.Variables.ResponseHeadersNames
-	tx.Variables.RequestHeadersNames = collection.NewMap(variables.RequestHeadersNames)
-	tx.Collections[variables.RequestHeadersNames] = tx.Variables.RequestHeadersNames
-	tx.Variables.Userid = collection.NewSimple(variables.Userid)
-	tx.Collections[variables.Userid] = tx.Variables.Userid
-	tx.Variables.ArgsGet = collection.NewMap(variables.ArgsGet)
-	tx.Collections[variables.ArgsGet] = tx.Variables.ArgsGet
-	tx.Variables.ArgsPost = collection.NewMap(variables.ArgsPost)
-	tx.Collections[variables.ArgsPost] = tx.Variables.ArgsPost
-	tx.Variables.ArgsPath = collection.NewMap(variables.ArgsPath)
-	tx.Collections[variables.ArgsPath] = tx.Variables.ArgsPath
-	tx.Variables.FilesSizes = collection.NewMap(variables.FilesSizes)
-	tx.Collections[variables.FilesSizes] = tx.Variables.FilesSizes
-	tx.Variables.FilesTmpContent = collection.NewMap(variables.FilesTmpContent)
-	tx.Collections[variables.FilesTmpContent] = tx.Variables.FilesTmpContent
-	tx.Variables.MultipartFilename = collection.NewMap(variables.MultipartFilename)
-	tx.Collections[variables.MultipartFilename] = tx.Variables.MultipartFilename
-	tx.Variables.MultipartName = collection.NewMap(variables.MultipartName)
-	tx.Collections[variables.MultipartName] = tx.Variables.MultipartName
-	tx.Variables.MatchedVars = collection.NewMap(variables.MatchedVars)
-	tx.Collections[variables.MatchedVars] = tx.Variables.MatchedVars
-	tx.Variables.RequestCookies = collection.NewMap(variables.RequestCookies)
-	tx.Collections[variables.RequestCookies] = tx.Variables.RequestCookies
-	tx.Variables.RequestHeaders = collection.NewMap(variables.RequestHeaders)
-	tx.Collections[variables.RequestHeaders] = tx.Variables.RequestHeaders
-	tx.Variables.ResponseHeaders = collection.NewMap(variables.ResponseHeaders)
-	tx.Collections[variables.ResponseHeaders] = tx.Variables.ResponseHeaders
-	tx.Variables.Geo = collection.NewMap(variables.Geo)
-	tx.Collections[variables.Geo] = tx.Variables.Geo
-	tx.Variables.TX = collection.NewMap(variables.TX)
-	tx.Collections[variables.TX] = tx.Variables.TX
-	tx.Variables.Rule = collection.NewMap(variables.Rule)
-	tx.Collections[variables.Rule] = tx.Variables.Rule
-	tx.Variables.Env = collection.NewMap(variables.Env)
-	tx.Collections[variables.Env] = tx.Variables.Env
-	tx.Variables.IP = collection.NewMap(variables.IP)
-	tx.Collections[variables.IP] = tx.Variables.IP
-	tx.Variables.Files = collection.NewMap(variables.Files)
-	tx.Collections[variables.Files] = tx.Variables.Files
-	tx.Variables.MatchedVarsNames = collection.NewMap(variables.MatchedVarsNames)
-	tx.Collections[variables.MatchedVarsNames] = tx.Variables.MatchedVarsNames
-	tx.Variables.FilesNames = collection.NewMap(variables.FilesNames)
-	tx.Collections[variables.FilesNames] = tx.Variables.FilesNames
-	tx.Variables.FilesTmpNames = collection.NewMap(variables.FilesTmpNames)
-	tx.Collections[variables.FilesTmpNames] = tx.Variables.FilesTmpNames
-	tx.Variables.RequestCookiesNames = collection.NewMap(variables.RequestCookiesNames)
-	tx.Collections[variables.RequestCookiesNames] = tx.Variables.RequestCookiesNames
-	tx.Variables.ResponseXML = collection.NewMap(variables.ResponseXML)
-	tx.Collections[variables.ResponseXML] = tx.Variables.ResponseXML
-	tx.Variables.RequestXML = collection.NewMap(variables.RequestXML)
-	tx.Collections[variables.RequestXML] = tx.Variables.RequestXML
+// NewWAF creates a new WAF instance with the provided configuration.
+func NewWAF(config WAFConfig) (WAF, error) {
+	c := config.(*wafConfig)
 
-	tx.Variables.ArgsCombinedSize = collection.NewCollectionSizeProxy(variables.ArgsCombinedSize, tx.Variables.ArgsGet, tx.Variables.ArgsPost)
-	tx.Collections[variables.ArgsCombinedSize] = tx.Variables.ArgsCombinedSize
+	waf := corazawaf.NewWAF()
+	parser := seclang.NewParser(waf)
 
-	// XML is a pointer to RequestXML
-	tx.Variables.XML = tx.Variables.RequestXML
-	tx.Collections[variables.XML] = tx.Variables.RequestXML
-	tx.Variables.Args = collection.NewProxy(
-		variables.Args,
-		tx.Variables.ArgsGet,
-		tx.Variables.ArgsPost,
-		tx.Variables.ArgsPath,
-	)
-	tx.Collections[variables.Args] = tx.Variables.Args
-
-	tx.Variables.ArgsNames = collection.NewTranslationProxy(
-		variables.ArgsNames,
-		tx.Variables.ArgsGet,
-		tx.Variables.ArgsPost,
-		tx.Variables.ArgsPath,
-	)
-	tx.Collections[variables.ArgsNames] = tx.Variables.ArgsNames
-	tx.Variables.ArgsGetNames = collection.NewTranslationProxy(
-		variables.ArgsGetNames,
-		tx.Variables.ArgsGet,
-	)
-	tx.Collections[variables.ArgsGetNames] = tx.Variables.ArgsGetNames
-	tx.Variables.ArgsPostNames = collection.NewTranslationProxy(
-		variables.ArgsPostNames,
-		tx.Variables.ArgsPost,
-	)
-	tx.Collections[variables.ArgsPostNames] = tx.Variables.ArgsPostNames
-
-	// set capture variables
-	for i := 0; i <= 10; i++ {
-		is := strconv.Itoa(i)
-		tx.Variables.TX.Set(is, []string{""})
+	if c.fsRoot != nil {
+		parser.SetRoot(c.fsRoot)
 	}
 
-	// Some defaults
-	tx.Variables.FilesCombinedSize.Set("0")
-	tx.Variables.UrlencodedError.Set("0")
-	tx.Variables.FullRequestLength.Set("0")
-	tx.Variables.MultipartBoundaryQuoted.Set("0")
-	tx.Variables.MultipartBoundaryWhitespace.Set("0")
-	tx.Variables.MultipartCrlfLfLines.Set("0")
-	tx.Variables.MultipartDataAfter.Set("0")
-	tx.Variables.MultipartDataBefore.Set("0")
-	tx.Variables.MultipartFileLimitExceeded.Set("0")
-	tx.Variables.MultipartHeaderFolding.Set("0")
-	tx.Variables.MultipartInvalidHeaderFolding.Set("0")
-	tx.Variables.MultipartInvalidPart.Set("0")
-	tx.Variables.MultipartInvalidQuoting.Set("0")
-	tx.Variables.MultipartLfLine.Set("0")
-	tx.Variables.MultipartMissingSemicolon.Set("0")
-	tx.Variables.MultipartStrictError.Set("0")
-	tx.Variables.MultipartUnmatchedBoundary.Set("0")
-	tx.Variables.OutboundDataError.Set("0")
-	tx.Variables.ReqbodyError.Set("0")
-	tx.Variables.ReqbodyProcessorError.Set("0")
-	tx.Variables.RequestBodyLength.Set("0")
-	tx.Variables.Duration.Set("0")
-	tx.Variables.HighestSeverity.Set("0")
-	tx.Variables.UniqueID.Set(tx.ID)
+	for _, r := range c.rules {
+		switch {
+		case r.rule != nil:
+			if err := waf.Rules.Add(r.rule); err != nil {
+				return nil, fmt.Errorf("invalid WAF config: %w", err)
+			}
+		case r.str != "":
+			if err := parser.FromString(r.str); err != nil {
+				return nil, fmt.Errorf("invalid WAF config: %w", err)
+			}
+		case r.file != "":
+			if err := parser.FromFile(r.file); err != nil {
+				return nil, fmt.Errorf("invalid WAF config: %w", err)
+			}
+		}
+	}
 
-	w.Logger.Debug("new transaction created with id %q", tx.ID)
+	if a := c.auditLog; a != nil {
+		// TODO(anuraaga): Can't override AuditEngineOn from rules to off this way.
+		if a.relevantOnly {
+			waf.AuditEngine = types.AuditEngineRelevantOnly
+		} else {
+			waf.AuditEngine = types.AuditEngineOn
+		}
 
-	return tx
-}
+		waf.AuditLogParts = a.parts
 
-// SetDebugLogPath sets the path for the debug log
-// If the path is empty, the debug log will be disabled
-// note: this is not thread safe
-func (w *WAF) SetDebugLogPath(path string) error {
-	if path == "" {
-		w.Logger.SetOutput(io.Discard)
-		return nil
-	}
-	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
-	if err != nil {
-		w.Logger.Error("error opening file: %s", err.Error())
+		if a.logger != nil {
+			waf.AuditLogWriter = a.logger
+		}
 	}
-	w.Logger.SetOutput(f)
-	return nil
-}
 
-// NewWAF creates a new WAF instance with default variables
-func NewWAF() *WAF {
-	logger := &stdDebugLogger{
-		logger: &log.Logger{},
-		Level:  LogLevelInfo,
+	if c.contentInjection {
+		waf.ContentInjection = true
 	}
-	logWriter, err := loggers.GetLogWriter("serial")
-	if err != nil {
-		logger.Error("error creating serial log writer: %s", err.Error())
+
+	if r := c.requestBody; r != nil {
+		waf.RequestBodyAccess = true
+		waf.RequestBodyLimit = int64(r.limit)
+		waf.RequestBodyInMemoryLimit = int64(r.inMemoryLimit)
 	}
-	waf := &WAF{
-		ArgumentSeparator:        "&",
-		AuditLogWriter:           logWriter,
-		AuditEngine:              types.AuditEngineOff,
-		AuditLogParts:            types.AuditLogParts("ABCFHZ"),
-		RequestBodyInMemoryLimit: 131072,
-		RequestBodyLimit:         134217728, // 10mb
-		ResponseBodyMimeTypes:    []string{"text/html", "text/plain"},
-		ResponseBodyLimit:        524288,
-		ResponseBodyAccess:       false,
-		RuleEngine:               types.RuleEngineOn,
-		Rules:                    NewRuleGroup(),
-		TmpDir:                   "/tmp",
-		AuditLogRelevantStatus:   regexp.MustCompile(`.*`),
-		RequestBodyAccess:        false,
-		Logger:                   logger,
+
+	if r := c.responseBody; r != nil {
+		waf.ResponseBodyAccess = true
+		waf.ResponseBodyLimit = int64(r.limit)
 	}
-	// We initialize a basic audit log writer that discards output
-	if err := logWriter.Init(types.Config{}); err != nil {
-		fmt.Println(err)
+
+	if c.debugLogger != nil {
+		waf.Logger = c.debugLogger
 	}
-	if err := waf.SetDebugLogPath(""); err != nil {
-		fmt.Println(err)
+
+	if c.errorLogger != nil {
+		waf.ErrorLogCb = c.errorLogger
 	}
-	waf.Logger.Debug("a new waf instance was created")
-	return waf
+
+	return wafWrapper{waf: waf}, nil
 }
 
-// SetDebugLogLevel changes the debug level of the WAF instance
-func (w *WAF) SetDebugLogLevel(lvl int) error {
-	// setLevel is concurrent safe
-	w.Logger.SetLevel(LogLevel(lvl))
-	return nil
+type wafWrapper struct {
+	waf *corazawaf.WAF
 }
 
-// SetErrorLogCb sets the callback function for error logging
-// The error callback receives all the error data and some
-// helpers to write modsecurity style logs
-func (w *WAF) SetErrorLogCb(cb ErrorLogCallback) {
-	w.errorLogCb = cb
+// NewTransaction implements the same method on WAF.
+func (w wafWrapper) NewTransaction(ctx context.Context) types.Transaction {
+	return w.waf.NewTransaction(ctx)
 }


Test patch:
diff --git a/actions/ctl_test.go b/actions/ctl_test.go
index b4b1908..76e0eb6 100644
--- a/actions/ctl_test.go
+++ b/actions/ctl_test.go
@@ -7,14 +7,14 @@ import (
 	"context"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 func TestCtl(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	tx := waf.NewTransaction(context.Background())
-	r := coraza.NewRule()
+	r := corazawaf.NewRule()
 	ctlf := ctl()
 
 	if err := ctlf.Init(r, "requestBodyProcessor=XML"); err != nil {
@@ -82,7 +82,7 @@ func TestCtl(t *testing.T) {
 
 func TestCtlParseRange(t *testing.T) {
 	a := &ctlFn{}
-	rules := []*coraza.Rule{
+	rules := []*corazawaf.Rule{
 		{
 			RuleMetadata: types.RuleMetadata{
 				ID: 5,
diff --git a/actions/severity_test.go b/actions/severity_test.go
index b8ccb63..00216c8 100644
--- a/actions/severity_test.go
+++ b/actions/severity_test.go
@@ -6,7 +6,7 @@ package actions
 import (
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -36,7 +36,7 @@ func TestSeverity(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			rule := coraza.NewRule()
+			rule := corazawaf.NewRule()
 			sev := severity()
 			if err := sev.Init(rule, tt.name); err != nil {
 				t.Error(err)
diff --git a/examples/http-server/main_test.go b/examples/http-server/main_test.go
index 11b5f7e..771594a 100644
--- a/examples/http-server/main_test.go
+++ b/examples/http-server/main_test.go
@@ -9,10 +9,11 @@ import (
 
 func setupTestServer(t *testing.T) *httptest.Server {
 	t.Helper()
-	if err := setupCoraza(); err != nil {
+	waf, err := setupCoraza()
+	if err != nil {
 		panic(err)
 	}
-	return httptest.NewServer(corazaRequestHandler(http.HandlerFunc(hello)))
+	return httptest.NewServer(corazaRequestHandler(waf, http.HandlerFunc(hello)))
 }
 
 func doGetRequest(t *testing.T, getPath string) int {
diff --git a/http/http_test.go b/http/http_test.go
index 6812840..89f64cd 100644
--- a/http/http_test.go
+++ b/http/http_test.go
@@ -16,15 +16,18 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+	"strconv"
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/internal/seclang"
+	"github.com/corazawaf/coraza/v3/macro"
 )
 
 func TestRequestExtractionSuccess(t *testing.T) {
 	req, _ := http.NewRequest("POST", "https://www.coraza.io/test", strings.NewReader("test=456"))
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	tx := waf.NewTransaction(context.Background())
 	if _, err := ProcessRequest(tx, req); err != nil {
 		t.Fatal(err)
@@ -90,9 +93,9 @@ func multipartRequest(t *testing.T, req *http.Request) error {
 	return nil
 }
 
-func makeTransaction(t *testing.T) *coraza.Transaction {
+func makeTransaction(t *testing.T) *corazawaf.Transaction {
 	t.Helper()
-	tx := coraza.NewWAF().NewTransaction(context.Background())
+	tx := corazawaf.NewWAF().NewTransaction(context.Background())
 	tx.RequestBodyAccess = true
 	ht := []string{
 		"POST /testurl.php?id=123&b=456 HTTP/1.1",
@@ -108,3 +111,63 @@ func makeTransaction(t *testing.T) *coraza.Transaction {
 	_, _ = tx.ParseRequestReader(strings.NewReader(data))
 	return tx
 }
+
+// from issue https://github.com/corazawaf/coraza/issues/159 @zpeasystart
+func TestDirectiveSecAuditLog(t *testing.T) {
+	waf := corazawaf.NewWAF()
+	p := seclang.NewParser(waf)
+	if err := p.FromString(`
+	SecRule REQUEST_FILENAME "@unconditionalMatch" "id:100, phase:2, t:none, log, setvar:'tx.count=+1',chain"
+	SecRule ARGS:username "@unconditionalMatch" "t:none, setvar:'tx.count=+2',chain"
+	SecRule ARGS:password "@unconditionalMatch" "t:none, setvar:'tx.count=+3'"
+		`); err != nil {
+		t.Error(err)
+	}
+	tx := waf.NewTransaction(context.Background())
+	tx.RequestBodyAccess = true
+	tx.ForceRequestBodyVariable = true
+	// request
+	rdata := []string{
+		"POST /login HTTP/1.1",
+		"Accept: */*",
+		"Accept-Encoding: gzip, deflate",
+		"Connection: close",
+		"Origin: http://test.com",
+		"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
+		"Content-Type: application/x-www-form-urlencoded; charset=UTF-8",
+		"Referer: http://somehost.com/login.jsp",
+		"X-Requested-With: XMLHttpRequest",
+		"Content-Length: 59",
+		"Accept-Language: zh-CN,zh;q=0.9",
+		"",
+		"username=root&password=123&rememberMe=on&time=1644979180757",
+	}
+	data := bytes.NewBuffer([]byte(strings.Join(rdata, "\r\n")))
+	req, err := http.ReadRequest(bufio.NewReader(data))
+	if err != nil {
+		t.Errorf("Description HTTP request parsing failed")
+	}
+	_, err = ProcessRequest(tx, req)
+	if err != nil {
+		t.Errorf("Failed to load the HTTP request")
+	}
+	// There is no problem loading the rules
+	c := 0
+	r := waf.Rules.FindByID(100)
+	for r != nil {
+		c++
+		r = r.Chain
+	}
+	if c != 3 {
+		t.Errorf("failed to compile multiple chains, expected 3, got %d", c)
+	}
+	// Why is the number of matches 4
+	m, err := macro.NewMacro("%{tx.count}")
+	if err != nil {
+		t.Error(err)
+	}
+	c, _ = strconv.Atoi(m.Expand(tx))
+	if c != 6 {
+		t.Errorf("Why is the number of matches %d", c)
+	}
+}
diff --git a/body_buffer_test.go b/internal/corazawaf/body_buffer_test.go
similarity index 99%
rename from body_buffer_test.go
rename to internal/corazawaf/body_buffer_test.go
index 95606e3..1c519c9 100644
--- a/body_buffer_test.go
+++ b/internal/corazawaf/body_buffer_test.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"io"
diff --git a/rule_test.go b/internal/corazawaf/rule_test.go
similarity index 98%
rename from rule_test.go
rename to internal/corazawaf/rule_test.go
index b8b6ceb..1cabed4 100644
--- a/rule_test.go
+++ b/internal/corazawaf/rule_test.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"testing"
diff --git a/rulegroup_test.go b/internal/corazawaf/rulegroup_test.go
similarity index 84%
rename from rulegroup_test.go
rename to internal/corazawaf/rulegroup_test.go
index 035dee3..2d872ea 100644
--- a/rulegroup_test.go
+++ b/internal/corazawaf/rulegroup_test.go
@@ -1,16 +1,18 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"testing"
+
+	"github.com/corazawaf/coraza/v3/macro"
 )
 
 func TestRG(t *testing.T) {
 	r := NewRule()
-	macroMsg, _ := NewMacro("test")
-	r.Msg = *macroMsg
+	macroMsg, _ := macro.NewMacro("test")
+	r.Msg = macroMsg
 	r.ID = 1
 	r.Tags = []string{
 		"test",
diff --git a/transaction_test.go b/internal/corazawaf/transaction_test.go
similarity index 95%
rename from transaction_test.go
rename to internal/corazawaf/transaction_test.go
index f0035f2..78e5afc 100644
--- a/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"context"
@@ -14,6 +14,7 @@ import (
 
 	"github.com/corazawaf/coraza/v3/collection"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
+	"github.com/corazawaf/coraza/v3/macro"
 	"github.com/corazawaf/coraza/v3/types"
 	"github.com/corazawaf/coraza/v3/types/variables"
 )
@@ -655,11 +656,11 @@ func makeTransactionMultipart(t *testing.T) *Transaction {
 
 func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing.T) {
 	for k, v := range tests {
-		macro, err := NewMacro(k)
+		m, err := macro.NewMacro(k)
 		if err != nil {
 			t.Error(err)
 		}
-		res := macro.Expand(tx)
+		res := m.Expand(tx)
 		if res != v {
 			if testing.Verbose() {
 				fmt.Println(tx.Debug())
@@ -669,3 +670,32 @@ func validateMacroExpansion(tests map[string]string, tx *Transaction, t *testing
 		}
 	}
 }
+
+func TestMacro(t *testing.T) {
+	tx := makeTransaction(t)
+	tx.Variables.TX.Set("some", []string{"secretly"})
+	m, err := macro.NewMacro("%{unique_id}")
+	if err != nil {
+		t.Error(err)
+	}
+	if m.Expand(tx) != tx.ID {
+		t.Errorf("%s != %s", m.Expand(tx), tx.ID)
+	}
+	m, err = macro.NewMacro("some complex text %{tx.some} wrapped in m")
+	if err != nil {
+		t.Error(err)
+	}
+	if m.Expand(tx) != "some complex text secretly wrapped in m" {
+		t.Errorf("failed to expand m, got %s\n%v", m.Expand(tx), m)
+	}
+
+	_, err = macro.NewMacro("some complex text %{tx.some} wrapped in m %{tx.some}")
+	if err != nil {
+		t.Error(err)
+		return
+	}
+	// TODO(anuraaga): Decouple this test from transaction implementation.
+	// if !macro.IsExpandable() || len(macro.tokens) != 4 || macro.Expand(tx) != "some complex text secretly wrapped in m secretly" {
+	//   t.Errorf("failed to parse replacements %v", macro.tokens)
+	// }
+}
diff --git a/waf_test.go b/internal/corazawaf/waf_test.go
similarity index 97%
rename from waf_test.go
rename to internal/corazawaf/waf_test.go
index 027a12a..36fb4b2 100644
--- a/waf_test.go
+++ b/internal/corazawaf/waf_test.go
@@ -1,7 +1,7 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
-package coraza
+package corazawaf
 
 import (
 	"context"
diff --git a/seclang/body_test.go b/internal/seclang/body_test.go
similarity index 94%
rename from seclang/body_test.go
rename to internal/seclang/body_test.go
index 2c93406..4fcdf65 100644
--- a/seclang/body_test.go
+++ b/internal/seclang/body_test.go
@@ -7,7 +7,7 @@ import (
 	"context"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 )
 
 /*
@@ -31,7 +31,7 @@ func TestRequestBodyAccessOff(t *testing.T) {
 }*/
 
 func TestRequestBodyAccessOn(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	if err := parser.FromString(`
 	SecRequestBodyAccess On
diff --git a/seclang/directives_log_test.go b/internal/seclang/directives_log_test.go
similarity index 95%
rename from seclang/directives_log_test.go
rename to internal/seclang/directives_log_test.go
index bc46f9b..923bd45 100644
--- a/seclang/directives_log_test.go
+++ b/internal/seclang/directives_log_test.go
@@ -16,14 +16,14 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	utils "github.com/corazawaf/coraza/v3/internal/strings"
 	"github.com/corazawaf/coraza/v3/loggers"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 func TestSecAuditLogDirectivesConcurrent(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	auditpath := t.TempDir()
 	parser := NewParser(waf)
 	if err := parser.FromString(fmt.Sprintf(`
@@ -68,7 +68,7 @@ func TestSecAuditLogDirectivesConcurrent(t *testing.T) {
 }
 
 func TestDebugDirectives(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	tmp := filepath.Join(t.TempDir(), "tmp.log")
 	p := NewParser(waf)
 	err := directiveSecDebugLog(&DirectiveOptions{
diff --git a/seclang/directives_test.go b/internal/seclang/directives_test.go
similarity index 96%
rename from seclang/directives_test.go
rename to internal/seclang/directives_test.go
index 567368b..125b2ae 100644
--- a/seclang/directives_test.go
+++ b/internal/seclang/directives_test.go
@@ -6,7 +6,7 @@ package seclang
 import (
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
@@ -23,7 +23,7 @@ func Test_NonImplementedDirective(t *testing.T) {
 		`SecHashKey "this_is_my_key" KeyOnly`,
 		`SecHashEngine On`,
 	}
-	w := coraza.NewWAF()
+	w := corazawaf.NewWAF()
 	p := NewParser(w)
 	for _, rule := range rules {
 		err := p.FromString(rule)
@@ -34,7 +34,7 @@ func Test_NonImplementedDirective(t *testing.T) {
 }
 
 func Test_directive(t *testing.T) {
-	w := coraza.NewWAF()
+	w := corazawaf.NewWAF()
 	p := NewParser(w)
 	if err := p.FromString("SecWebAppId test123"); err != nil {
 		t.Error("failed to set parser from string")
@@ -132,7 +132,7 @@ func Test_directive(t *testing.T) {
 }
 
 func TestSecRuleUpdateTargetBy(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rule, err := ParseRule(RuleOptions{
 		Data:         "REQUEST_URI \"^/test\" \"id:181,tag:test\"",
 		WAF:          waf,
@@ -157,7 +157,7 @@ func TestSecRuleUpdateTargetBy(t *testing.T) {
 }
 
 func TestInvalidBooleanForDirectives(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString("SecIgnoreRuleCompilationErrors sure"); err == nil {
 		t.Error("failed to error on invalid boolean")
@@ -170,12 +170,12 @@ func TestInvalidRulesWithIgnoredErrors(t *testing.T) {
 	SecRule REQUEST_URI "@no_op ^/test" "id:200,tag:test,invalid:5"
 	SecRule REQUEST_URI "@rx ^/test" "id:181,tag:repeated-id"
 	`
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString("secignorerulecompilationerrors On\n" + directives); err != nil {
 		t.Error(err)
 	}
-	waf = coraza.NewWAF()
+	waf = corazawaf.NewWAF()
 	p = NewParser(waf)
 	if err := p.FromString(directives); err == nil {
 		t.Error("failed to error on invalid rule")
@@ -183,7 +183,7 @@ func TestInvalidRulesWithIgnoredErrors(t *testing.T) {
 }
 
 func TestSecDataset(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString("" +
 		"SecDataset test `\n123\n456\n`\n"); err != nil {
diff --git a/seclang/parser_test.go b/internal/seclang/parser_test.go
similarity index 95%
rename from seclang/parser_test.go
rename to internal/seclang/parser_test.go
index 9c8c3f1..e5e39a3 100644
--- a/seclang/parser_test.go
+++ b/internal/seclang/parser_test.go
@@ -13,7 +13,7 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	coraza "github.com/corazawaf/coraza/v3/internal/corazawaf"
 )
 
 //go:embed testdata
@@ -71,7 +71,7 @@ func TestErrorWithBackticks(t *testing.T) {
 func TestDefaultConfigurationFile(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
-	err := p.FromFile("../coraza.conf-recommended")
+	err := p.FromFile("../../coraza.conf-recommended")
 	if err != nil {
 		t.Error(err)
 	}
@@ -80,7 +80,7 @@ func TestDefaultConfigurationFile(t *testing.T) {
 func TestHardcodedIncludeDirective(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
-	if err := p.FromString("Include ../coraza.conf-recommended"); err != nil {
+	if err := p.FromString("Include ../../coraza.conf-recommended"); err != nil {
 		t.Error(err)
 	}
 	if waf.Rules.Count() == 0 {
@@ -163,7 +163,7 @@ func TestHardcodedIncludeDirectiveDDOS2(t *testing.T) {
 
 func TestChains(t *testing.T) {
 	/*
-		waf := engine.NewWAF()
+		waf := coraza.NewWAF()
 		p, _ := NewParser(waf)
 		if err := p.FromString(`
 		SecAction "id:1,deny,log,phase:1,chain"
diff --git a/seclang/rule_parser_test.go b/internal/seclang/rule_parser_test.go
similarity index 94%
rename from seclang/rule_parser_test.go
rename to internal/seclang/rule_parser_test.go
index 0f4553f..f2018bb 100644
--- a/seclang/rule_parser_test.go
+++ b/internal/seclang/rule_parser_test.go
@@ -7,11 +7,11 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 )
 
 func TestInvalidRule(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 
 	err := p.FromString("")
@@ -26,7 +26,7 @@ func TestInvalidRule(t *testing.T) {
 }
 
 func TestVariables(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 
 	// single variable with key
@@ -59,7 +59,7 @@ func TestVariables(t *testing.T) {
 }
 
 func TestVariableCases(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	err := p.FromString(`SecRule REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/|!REQUEST_COOKIES:/_pk_ref/|REQUEST_COOKIES_NAMES|ARGS_NAMES|ARGS|XML:/* "" "id:7,pass"`)
 	if err != nil {
@@ -68,7 +68,7 @@ func TestVariableCases(t *testing.T) {
 }
 
 func TestSecRuleInlineVariableNegation(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	err := p.FromString(`
 		SecRule REQUEST_URI|!REQUEST_COOKIES "abc" "id:7,phase:2"
@@ -93,7 +93,7 @@ func TestSecRuleInlineVariableNegation(t *testing.T) {
 }
 
 func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	err := p.FromString(`
 		SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:7,phase:2"
@@ -124,7 +124,7 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 }
 
 func TestErrorLine(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	err := p.FromString("SecAction \"id:1\"\n#test\nSomefaulty")
 	if err == nil {
@@ -136,7 +136,7 @@ func TestErrorLine(t *testing.T) {
 }
 
 func TestDefaultActionsForPhase2(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	err := p.FromString(`
 	SecAction "id:1,phase:2"
diff --git a/seclang/rules_test.go b/internal/seclang/rules_test.go
similarity index 97%
rename from seclang/rules_test.go
rename to internal/seclang/rules_test.go
index 5c55753..9247abb 100644
--- a/seclang/rules_test.go
+++ b/internal/seclang/rules_test.go
@@ -10,12 +10,12 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/types"
 )
 
 func TestRuleMatch(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 		SecRuleEngine On
@@ -42,7 +42,7 @@ func TestRuleMatch(t *testing.T) {
 }
 
 func TestRuleMatchWithRegex(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 		SecRuleEngine On
@@ -66,7 +66,7 @@ func TestRuleMatchWithRegex(t *testing.T) {
 }
 
 func TestSecMarkers(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 		SecRuleEngine On		
@@ -101,7 +101,7 @@ func TestSecMarkers(t *testing.T) {
 }
 
 func TestSecAuditLogs(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 		SecAuditEngine On
@@ -130,7 +130,7 @@ func TestSecAuditLogs(t *testing.T) {
 }
 
 func TestRuleLogging(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	logs := []string{}
 	waf.SetErrorLogCb(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
@@ -167,7 +167,7 @@ func TestRuleLogging(t *testing.T) {
 }
 
 func TestRuleChains(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 		SecRule ARGS "123" "id:1,phase:1,log,chain"
@@ -191,7 +191,7 @@ func TestRuleChains(t *testing.T) {
 }
 
 func TestTagsAreNotPrintedTwice(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	logs := []string{}
 	waf.SetErrorLogCb(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
@@ -223,7 +223,7 @@ func TestTagsAreNotPrintedTwice(t *testing.T) {
 }
 
 func TestStatusFromInterruptions(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	logs := []string{}
 	waf.SetErrorLogCb(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
@@ -247,7 +247,7 @@ func TestStatusFromInterruptions(t *testing.T) {
 }
 
 func TestChainWithUnconditionalMatch(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString(`
 	SecAction "id:7, pass, phase:1, log, chain, skip:2"
@@ -261,7 +261,7 @@ func TestChainWithUnconditionalMatch(t *testing.T) {
 }
 
 func TestLogsAreNotPrintedManyTimes(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	logs := []string{}
 	waf.SetErrorLogCb(func(mr types.MatchedRule) {
 		logs = append(logs, mr.ErrorLog(403))
@@ -289,7 +289,7 @@ func TestLogsAreNotPrintedManyTimes(t *testing.T) {
 }
 
 func TestSampleRxRule(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`
 	SecRule REQUEST_METHOD "@rx ^(?:GET|HEAD)$" "phase:1,id:1,log,deny,status:403,chain"
@@ -307,7 +307,7 @@ func TestSampleRxRule(t *testing.T) {
 
 func TestTxIssue147(t *testing.T) {
 	// https://github.com/corazawaf/coraza/issues/147
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := NewParser(waf)
 	err := parser.FromString(`SecRule RESPONSE_BODY "@rx ^#!\s?/" "id:950140,phase:4,log,deny,status:403"`)
 	if err != nil {
@@ -345,7 +345,7 @@ func TestTxIssue147(t *testing.T) {
 }
 
 func TestIssue176(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule REQUEST_COOKIES:sessionId "test" "id:1,phase:1,deny,log,msg:'test rule',logdata:'Matched %{MATCHED_VAR_NAME}'"`
 	parser := NewParser(waf)
 
@@ -391,7 +391,7 @@ func TestIssue176(t *testing.T) {
 }
 
 func TestRxCapture(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule &TX:allowed_request_content_type_charset "@eq 0" \
         "id:901168,\
         phase:1,\
@@ -440,7 +440,7 @@ func TestRxCapture(t *testing.T) {
 }
 
 func TestUnicode(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule ARGS "@rx \x{30cf}\x{30ed}\x{30fc}" "id:101,phase:2,t:lowercase,deny"`
 	parser := NewParser(waf)
 
@@ -470,7 +470,7 @@ func Test941310(t *testing.T) {
 	// not supported on TinyGo
 	// t.Skip("non-utf8 regex not supported")
 
-	// waf := coraza.NewWAF()
+	// waf := corazawaf.NewWAF()
 	// rules := `SecRule REQUEST_COOKIES|!REQUEST_COOKIES:/__utm/|REQUEST_COOKIES_NAMES|ARGS_NAMES|ARGS|XML:/* "@rx \xbc[^\xbe>]*[\xbe>]|<[^\xbe]*\xbe" \
 	// "id:941310,\
 	// phase:2,\
@@ -514,7 +514,7 @@ func Test941310(t *testing.T) {
 }
 
 func TestArgumentNamesCaseSensitive(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule ARGS_NAMES "Test1" "id:3, phase:2, log, deny"`
 	parser := NewParser(waf)
 
@@ -557,7 +557,7 @@ func TestArgumentNamesCaseSensitive(t *testing.T) {
 }
 
 func TestArgumentsCaseSensitive(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule ARGS:Test1 "Xyz" "id:3, phase:2, log, deny"`
 	parser := NewParser(waf)
 
@@ -619,7 +619,7 @@ func TestArgumentsCaseSensitive(t *testing.T) {
 }
 
 func TestCookiesCaseSensitive(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule REQUEST_COOKIES:Test1 "Xyz" "id:3, phase:2, log, deny"`
 	parser := NewParser(waf)
 
@@ -681,7 +681,7 @@ func TestCookiesCaseSensitive(t *testing.T) {
 }
 
 func TestHeadersCaseSensitive(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule REQUEST_HEADERS:Test1 "Xyz" "id:3, phase:2, log, deny"`
 	parser := NewParser(waf)
 
@@ -743,7 +743,7 @@ func TestHeadersCaseSensitive(t *testing.T) {
 }
 
 func TestParameterPollution(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule Args:TESt1 "Xyz" "id:3, phase:2, log, pass"`
 	parser := NewParser(waf)
 
@@ -794,7 +794,7 @@ func TestParameterPollution(t *testing.T) {
 }
 
 func TestURIQueryParamCaseSensitive(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `SecRule ARGS:Test1 "@contains SQLI" "id:3, phase:2, log, pass"`
 	parser := NewParser(waf)
 
@@ -915,7 +915,7 @@ func isMatchData(mds []types.MatchData, key string) (result bool) {
 }
 
 func Test930110_10(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	rules := `
 SecRequestBodyAccess On
 SecRule REQUEST_URI|ARGS|REQUEST_HEADERS|!REQUEST_HEADERS:Referer|FILES|XML:/* "@rx (?:(?:^|[\x5c/])\.{2,3}[\x5c/]|[\x5c/]\.{2,3}(?:[\x5c/]|$))" \
diff --git a/macro_test.go b/macro_test.go
deleted file mode 100644
index 866660a..0000000
--- a/macro_test.go
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-package coraza
-
-import (
-	"testing"
-)
-
-func TestMacro(t *testing.T) {
-	tx := makeTransaction(t)
-	tx.Variables.TX.Set("some", []string{"secretly"})
-
-	testCases := []struct {
-		name              string
-		macro             string
-		IsExpandable      bool
-		expectedExpansion string
-		numTokens         int
-	}{
-		{
-			name:              "empty",
-			macro:             "",
-			expectedExpansion: "",
-			numTokens:         0,
-		},
-		{
-			name:              "unexisting",
-			macro:             "%{unknown}",
-			expectedExpansion: "",
-			numTokens:         1,
-		},
-		{
-			name:              "alone",
-			macro:             "%{unique_id}",
-			expectedExpansion: tx.ID,
-			numTokens:         1,
-		},
-		{
-			name:              "within a text",
-			macro:             "some complex text %{tx.some} wrapped in macro",
-			expectedExpansion: "some complex text secretly wrapped in macro",
-			IsExpandable:      true,
-			numTokens:         3,
-		},
-		{
-			name:              "repeated inside a text",
-			macro:             "some complex text %{tx.some} wrapped in macro %{tx.some}",
-			expectedExpansion: "some complex text secretly wrapped in macro secretly",
-			IsExpandable:      true,
-			numTokens:         4,
-		},
-	}
-
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			macro, err := NewMacro(testCase.macro)
-			if err != nil {
-				t.Error(err)
-			}
-
-			if want, have := testCase.IsExpandable, macro.IsExpandable(); want != have {
-				t.Errorf("unexpected expandability, want %t, have %t", want, have)
-			}
-
-			if want, have := testCase.numTokens, len(macro.tokens); want != have {
-				t.Errorf("unexpected number of tokens, want %d, have %d", want, have)
-			}
-
-			if want, have := testCase.expectedExpansion, macro.Expand(tx); want != have {
-				t.Errorf("unexpected expansion, want %q, have %q", want, have)
-			}
-		})
-	}
-}
diff --git a/operators/inspect_file_test.go b/operators/inspect_file_test.go
index c533c6c..c66b007 100644
--- a/operators/inspect_file_test.go
+++ b/operators/inspect_file_test.go
@@ -10,12 +10,12 @@ import (
 	_ "fmt"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestInspectFile(t *testing.T) {
 	ipf := &inspectFile{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "",
 	}
 	opts.Arguments = "/bin/echo"
diff --git a/operators/ip_match_from_dataset_test.go b/operators/ip_match_from_dataset_test.go
index 133507a..3bd74da 100644
--- a/operators/ip_match_from_dataset_test.go
+++ b/operators/ip_match_from_dataset_test.go
@@ -7,7 +7,7 @@ import (
 	_ "fmt"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestIpMatchFromDataset(t *testing.T) {
@@ -15,7 +15,7 @@ func TestIpMatchFromDataset(t *testing.T) {
 	addrfail := []string{"127.0.0.2", "192.168.1.1"}
 
 	ipm := &ipMatchFromDataset{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "test_1",
 		Datasets: map[string][]string{
 			"test_1": {"127.0.0.1", "192.168.0.0/24"},
@@ -40,7 +40,7 @@ func TestIpMatchFromDataset(t *testing.T) {
 
 func TestIpMatchFromEmptyDataset(t *testing.T) {
 	ipm := &ipMatchFromDataset{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "test_1",
 		Datasets: map[string][]string{
 			"test_1": {},
diff --git a/operators/ip_match_from_file_test.go b/operators/ip_match_from_file_test.go
index d7337ca..5337006 100644
--- a/operators/ip_match_from_file_test.go
+++ b/operators/ip_match_from_file_test.go
@@ -6,8 +6,8 @@ package operators
 import (
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
 	"github.com/corazawaf/coraza/v3/internal/io"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestFromFile(t *testing.T) {
@@ -15,7 +15,7 @@ func TestFromFile(t *testing.T) {
 	addrfail := []string{"127.0.0.2", "192.168.1.1"}
 
 	ipm := &ipMatchFromFile{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "./testdata/op/netranges.dat",
 		Path:      []string{"."},
 		Root:      io.OSFS{},
diff --git a/operators/ip_match_test.go b/operators/ip_match_test.go
index 5a71002..9042e61 100644
--- a/operators/ip_match_test.go
+++ b/operators/ip_match_test.go
@@ -7,7 +7,7 @@ import (
 	_ "fmt"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestOneAddress(t *testing.T) {
@@ -15,7 +15,7 @@ func TestOneAddress(t *testing.T) {
 	addrfail := "127.0.0.2"
 	cidr := "127.0.0.1/32"
 	ipm := &ipMatch{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: cidr,
 	}
 	if err := ipm.Init(opts); err != nil {
@@ -34,7 +34,7 @@ func TestMultipleAddress(t *testing.T) {
 	addrfail := []string{"127.0.0.2", "192.168.1.1"}
 	cidr := "127.0.0.1, 192.168.0.0/24"
 	ipm := &ipMatch{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: cidr,
 	}
 	if err := ipm.Init(opts); err != nil {
diff --git a/operators/operators_test.go b/operators/operators_test.go
index 7ceb645..b0b7447 100644
--- a/operators/operators_test.go
+++ b/operators/operators_test.go
@@ -13,7 +13,8 @@ import (
 
 	"github.com/tidwall/gjson"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type Test struct {
@@ -50,7 +51,7 @@ func TestOperators(t *testing.T) {
 		"without capture": false,
 	}
 
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	for _, f := range files {
 		cases := unmarshalTests(t, f)
 		for _, data := range cases {
@@ -83,7 +84,7 @@ func TestOperators(t *testing.T) {
 						return
 					}
 
-					opts := coraza.RuleOperatorOptions{
+					opts := rules.OperatorOptions{
 						Arguments: data.Param,
 						Path:      []string{"op"},
 						Root:      os.DirFS("testdata"),
diff --git a/operators/pm_from_dataset_test.go b/operators/pm_from_dataset_test.go
index 59f5531..f2c215f 100644
--- a/operators/pm_from_dataset_test.go
+++ b/operators/pm_from_dataset_test.go
@@ -8,12 +8,13 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestPmFromDataset(t *testing.T) {
 	pm := &pmFromDataset{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "test_1",
 		Datasets: map[string][]string{
 			"test_1": {"test_1", "test_2"},
@@ -23,7 +24,7 @@ func TestPmFromDataset(t *testing.T) {
 	if err := pm.Init(opts); err != nil {
 		t.Error(err)
 	}
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	tx := waf.NewTransaction(context.Background())
 	tx.Capture = true
 	res := pm.Evaluate(tx, "test_1")
diff --git a/operators/rbl_test.go b/operators/rbl_test.go
index 1688cd2..f39447a 100644
--- a/operators/rbl_test.go
+++ b/operators/rbl_test.go
@@ -12,7 +12,8 @@ import (
 
 	"github.com/foxcpp/go-mockdns"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 type testLogger struct{ t *testing.T }
@@ -24,7 +25,7 @@ func (l *testLogger) Printf(format string, v ...interface{}) {
 
 func TestRbl(t *testing.T) {
 	rbl := &rbl{}
-	opts := coraza.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: "xbl.spamhaus.org",
 	}
 	if err := rbl.Init(opts); err != nil {
@@ -61,7 +62,7 @@ func TestRbl(t *testing.T) {
 	})
 
 	t.Run("Valid hostname with TXT record", func(t *testing.T) {
-		tx := coraza.NewWAF().NewTransaction(context.Background())
+		tx := corazawaf.NewWAF().NewTransaction(context.Background())
 		if !rbl.Evaluate(tx, "valid_txt") {
 			t.Errorf("Unexpected result for valid hostname")
 		}
@@ -77,7 +78,7 @@ func TestRbl(t *testing.T) {
 	})
 
 	t.Run("Blocked hostname", func(t *testing.T) {
-		tx := coraza.NewWAF().NewTransaction(context.Background())
+		tx := corazawaf.NewWAF().NewTransaction(context.Background())
 		if !rbl.Evaluate(tx, "blocked") {
 			t.Fatal("Unexpected result for blocked hostname")
 		}
diff --git a/operators/restpath_test.go b/operators/restpath_test.go
index 7b6a706..104c322 100644
--- a/operators/restpath_test.go
+++ b/operators/restpath_test.go
@@ -7,16 +7,17 @@ import (
 	"context"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestRestPath(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	tx := waf.NewTransaction(context.Background())
 	exp := "/some-random/url-{id}/{name}"
 	path := "/some-random/url-123/juan"
 	rp := restpath{}
-	if err := rp.Init(coraza.RuleOperatorOptions{
+	if err := rp.Init(rules.OperatorOptions{
 		Arguments: exp,
 	}); err != nil {
 		t.Error(err)
diff --git a/operators/rx_test.go b/operators/rx_test.go
index b007f93..382227d 100644
--- a/operators/rx_test.go
+++ b/operators/rx_test.go
@@ -8,7 +8,8 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestRx(t *testing.T) {
@@ -44,13 +45,13 @@ func TestRx(t *testing.T) {
 		t.Run(fmt.Sprintf("%s/%s", tt.pattern, tt.input), func(t *testing.T) {
 
 			rx := &rx{}
-			opts := coraza.RuleOperatorOptions{
+			opts := rules.OperatorOptions{
 				Arguments: tt.pattern,
 			}
 			if err := rx.Init(opts); err != nil {
 				t.Error(err)
 			}
-			waf := coraza.NewWAF()
+			waf := corazawaf.NewWAF()
 			tx := waf.NewTransaction(context.Background())
 			tx.Capture = true
 			res := rx.Evaluate(tx, tt.input)
diff --git a/operators/validate_byte_range_test.go b/operators/validate_byte_range_test.go
index 37bc4c1..f2c01e9 100644
--- a/operators/validate_byte_range_test.go
+++ b/operators/validate_byte_range_test.go
@@ -7,13 +7,14 @@ import (
 	"context"
 	"testing"
 
-	engine "github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestValidateByteRangeCase4(t *testing.T) {
 	ranges := "0-255"
 	op := &validateByteRange{}
-	opts := engine.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: ranges,
 	}
 	if err := op.Init(opts); err != nil {
@@ -28,7 +29,7 @@ func TestValidateByteRangeCase4(t *testing.T) {
 func TestValidateByteRangeCase5(t *testing.T) {
 	ranges := "9,10,13,32-126,128-255"
 	op := &validateByteRange{}
-	opts := engine.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: ranges,
 	}
 	if err := op.Init(opts); err != nil {
@@ -43,15 +44,15 @@ func TestValidateByteRangeCase5(t *testing.T) {
 	}
 }
 
-func getTransaction() *engine.Transaction {
-	waf := engine.NewWAF()
+func getTransaction() *corazawaf.Transaction {
+	waf := corazawaf.NewWAF()
 	return waf.NewTransaction(context.Background())
 }
 
 func BenchmarkValidateByteRange(b *testing.B) {
 	ranges := "9,10,13,32-126,128-255"
 	op := &validateByteRange{}
-	opts := engine.RuleOperatorOptions{
+	opts := rules.OperatorOptions{
 		Arguments: ranges,
 	}
 	if err := op.Init(opts); err != nil {
diff --git a/operators/validate_nid_test.go b/operators/validate_nid_test.go
index 9b5601b..647d1a1 100644
--- a/operators/validate_nid_test.go
+++ b/operators/validate_nid_test.go
@@ -6,14 +6,14 @@ package operators
 import (
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/rules"
 )
 
 func TestVaildateNid(t *testing.T) {
 	vn := &validateNid{}
 	notOk := []string{"cl11.111.111-1", "us16100407-2", "clc 12345", "uss 1234567"}
 	for _, no := range notOk {
-		opts := coraza.RuleOperatorOptions{
+		opts := rules.OperatorOptions{
 			Arguments: no,
 		}
 		err := vn.Init(opts)
diff --git a/seclang/rules_http_test.go b/seclang/rules_http_test.go
deleted file mode 100644
index f588680..0000000
--- a/seclang/rules_http_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
-// SPDX-License-Identifier: Apache-2.0
-
-// tinygo does not support net.http so this package is not needed for it
-//go:build !tinygo
-// +build !tinygo
-
-package seclang
-
-import (
-	"bufio"
-	"bytes"
-	"context"
-	"net/http"
-	"strconv"
-	"strings"
-	"testing"
-
-	"github.com/corazawaf/coraza/v3"
-	txhttp "github.com/corazawaf/coraza/v3/http"
-)
-
-// from issue https://github.com/corazawaf/coraza/issues/159 @zpeasystart
-func TestDirectiveSecAuditLog(t *testing.T) {
-	waf := coraza.NewWAF()
-	p := NewParser(waf)
-	if err := p.FromString(`
-	SecRule REQUEST_FILENAME "@unconditionalMatch" "id:100, phase:2, t:none, log, setvar:'tx.count=+1',chain"
-	SecRule ARGS:username "@unconditionalMatch" "t:none, setvar:'tx.count=+2',chain"
-	SecRule ARGS:password "@unconditionalMatch" "t:none, setvar:'tx.count=+3'"
-		`); err != nil {
-		t.Error(err)
-	}
-	tx := waf.NewTransaction(context.Background())
-	tx.RequestBodyAccess = true
-	tx.ForceRequestBodyVariable = true
-	// request
-	rdata := []string{
-		"POST /login HTTP/1.1",
-		"Accept: */*",
-		"Accept-Encoding: gzip, deflate",
-		"Connection: close",
-		"Origin: http://test.com",
-		"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36",
-		"Content-Type: application/x-www-form-urlencoded; charset=UTF-8",
-		"Referer: http://somehost.com/login.jsp",
-		"X-Requested-With: XMLHttpRequest",
-		"Content-Length: 59",
-		"Accept-Language: zh-CN,zh;q=0.9",
-		"",
-		"username=root&password=123&rememberMe=on&time=1644979180757",
-	}
-	data := bytes.NewBuffer([]byte(strings.Join(rdata, "\r\n")))
-	req, err := http.ReadRequest(bufio.NewReader(data))
-	if err != nil {
-		t.Errorf("Description HTTP request parsing failed")
-	}
-	_, err = txhttp.ProcessRequest(tx, req)
-	if err != nil {
-		t.Errorf("Failed to load the HTTP request")
-	}
-	// There is no problem loading the rules
-	c := 0
-	r := waf.Rules.FindByID(100)
-	for r != nil {
-		c++
-		r = r.Chain
-	}
-	if c != 3 {
-		t.Errorf("failed to compile multiple chains, expected 3, got %d", c)
-	}
-	// Why is the number of matches 4
-	macro, err := coraza.NewMacro("%{tx.count}")
-	if err != nil {
-		t.Error(err)
-	}
-	c, _ = strconv.Atoi(macro.Expand(tx))
-	if c != 6 {
-		t.Errorf("Why is the number of matches %d", c)
-	}
-}
diff --git a/testing/auditlog_test.go b/testing/auditlog_test.go
index 11fa4cf..e8ca222 100644
--- a/testing/auditlog_test.go
+++ b/testing/auditlog_test.go
@@ -16,13 +16,13 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/internal/seclang"
 	"github.com/corazawaf/coraza/v3/loggers"
-	"github.com/corazawaf/coraza/v3/seclang"
 )
 
 func TestAuditLogMessages(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := seclang.NewParser(waf)
 	// generate a random tmp file
 	file, err := os.Create(filepath.Join(t.TempDir(), "tmp.log"))
@@ -70,7 +70,7 @@ func TestAuditLogMessages(t *testing.T) {
 }
 
 func TestAuditLogRelevantOnly(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := seclang.NewParser(waf)
 	if err := parser.FromString(`
 		SecRuleEngine DetectionOnly
@@ -107,7 +107,7 @@ func TestAuditLogRelevantOnly(t *testing.T) {
 }
 
 func TestAuditLogRelevantOnlyOk(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := seclang.NewParser(waf)
 	// generate a random tmp file
 	file, err := os.Create(filepath.Join(t.TempDir(), "tmp.log"))
@@ -144,7 +144,7 @@ func TestAuditLogRelevantOnlyOk(t *testing.T) {
 }
 
 func TestAuditLogRelevantOnlyNoAuditlog(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := seclang.NewParser(waf)
 	if err := parser.FromString(`
 		SecRuleEngine DetectionOnly
diff --git a/testing/coraza_test.go b/testing/coraza_test.go
index 0566a62..5382d13 100644
--- a/testing/coraza_test.go
+++ b/testing/coraza_test.go
@@ -9,7 +9,6 @@ import (
 	"testing"
 
 	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/seclang"
 	_ "github.com/corazawaf/coraza/v3/testing/engine"
 	"github.com/corazawaf/coraza/v3/testing/profile"
 )
@@ -21,7 +20,7 @@ func TestEngine(t *testing.T) {
 	t.Logf("Loading %d profiles\n", len(profile.Profiles))
 	for _, p := range profile.Profiles {
 		t.Run(p.Meta.Name, func(t *testing.T) {
-			tt, err := testList(&p, nil)
+			tt, err := testList(&p)
 			if err != nil {
 				t.Error(err)
 			}
@@ -34,11 +33,11 @@ func TestEngine(t *testing.T) {
 
 					for _, e := range test.OutputErrors() {
 						debug := ""
-						for _, mr := range test.transaction.MatchedRules {
+						for _, mr := range test.transaction.GetMatchedRules() {
 							debug += fmt.Sprintf(" %d", mr.Rule.ID)
 						}
 						if testing.Verbose() {
-							t.Errorf("\x1b[41m ERROR \x1b[0m: %s:%s: %s, got:%s\n%s\nREQUEST:\n%s", p.Meta.Name, test.Name, e, debug, test.transaction.Debug(), test.Request())
+							t.Errorf("\x1b[41m ERROR \x1b[0m: %s:%s: %s, got:%s\n%s\nREQUEST:\n%s", p.Meta.Name, test.Name, e, debug, test.transaction, test.Request())
 						} else {
 							t.Errorf("%s: ERROR: %s", test.Name, e)
 						}
@@ -46,7 +45,7 @@ func TestEngine(t *testing.T) {
 
 					for _, e := range test.OutputInterruptionErrors() {
 						if testing.Verbose() {
-							t.Errorf("\x1b[41m ERROR \x1b[0m: %s:%s: %s\n %s\nREQUEST:\n%s", p.Meta.Name, test.Name, e, test.transaction.Debug(), test.Request())
+							t.Errorf("\x1b[41m ERROR \x1b[0m: %s:%s: %s\n %s\nREQUEST:\n%s", p.Meta.Name, test.Name, e, test.transaction, test.Request())
 						} else {
 							t.Errorf("%s: ERROR: %s", test.Name, e)
 						}
@@ -57,19 +56,16 @@ func TestEngine(t *testing.T) {
 	}
 }
 
-func testList(p *profile.Profile, waf *coraza.WAF) ([]*Test, error) {
+func testList(p *profile.Profile) ([]*Test, error) {
 	var tests []*Test
 	for _, t := range p.Tests {
 		name := t.Title
 		for _, stage := range t.Stages {
-			w := waf
-			if w == nil || p.Rules != "" {
-				w = coraza.NewWAF()
-				parser := seclang.NewParser(w)
-				parser.SetRoot(os.DirFS("testdata"))
-				if err := parser.FromString(p.Rules); err != nil {
-					return nil, err
-				}
+			w, err := coraza.NewWAF(coraza.NewWAFConfig().
+				WithRootFS(os.DirFS("testdata")).
+				WithDirectives(p.Rules))
+			if err != nil {
+				return nil, err
 			}
 			test := NewTest(name, w)
 			test.ExpectedOutput = stage.Stage.Output
diff --git a/testing/coreruleset_test.go b/testing/coreruleset_test.go
index 2eb93c5..ce6adbd 100644
--- a/testing/coreruleset_test.go
+++ b/testing/coreruleset_test.go
@@ -19,8 +19,8 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/corazawaf/coraza/v3"
-	"github.com/corazawaf/coraza/v3/seclang"
+	"github.com/corazawaf/coraza/v3/internal/corazawaf"
+	"github.com/corazawaf/coraza/v3/internal/seclang"
 )
 
 var crspath string
@@ -49,7 +49,7 @@ func BenchmarkCRSCompilation(b *testing.B) {
 		path.Join(crspath, "rules/", "*.conf"),
 	}
 	for i := 0; i < b.N; i++ {
-		waf := coraza.NewWAF()
+		waf := corazawaf.NewWAF()
 		parser := seclang.NewParser(waf)
 		for _, f := range files {
 			if err := parser.FromFile(f); err != nil {
@@ -120,14 +120,14 @@ func BenchmarkCRSSimplePOST(b *testing.B) {
 	}
 }
 
-func crsWAF(t testing.TB) *coraza.WAF {
+func crsWAF(t testing.TB) *corazawaf.WAF {
 	t.Helper()
 	files := []string{
 		"../coraza.conf-recommended",
 		path.Join(crspath, "crs-setup.conf.example"),
 		path.Join(crspath, "rules/", "*.conf"),
 	}
-	waf := coraza.NewWAF()
+	waf := corazawaf.NewWAF()
 	parser := seclang.NewParser(waf)
 	for _, f := range files {
 		if err := parser.FromFile(f); err != nil {
diff --git a/testing/engine.go b/testing/engine.go
index 23cc614..915cb47 100644
--- a/testing/engine.go
+++ b/testing/engine.go
@@ -12,15 +12,16 @@ import (
 
 	"github.com/corazawaf/coraza/v3"
 	"github.com/corazawaf/coraza/v3/testing/profile"
+	"github.com/corazawaf/coraza/v3/types"
 )
 
 // Test represents a unique transaction within
 // a WAF instance for a test case
 type Test struct {
 	// waf contains a waf instance pointer
-	waf *coraza.WAF
+	waf coraza.WAF
 	// transaction contains the current transaction
-	transaction *coraza.Transaction
+	transaction types.Transaction
 	magic       bool
 	Name        string
 	body        string
@@ -53,7 +54,7 @@ type Test struct {
 }
 
 // SetWAF sets the waf instance pointer
-func (t *Test) SetWAF(waf *coraza.WAF) {
+func (t *Test) SetWAF(waf coraza.WAF) {
 	t.waf = waf
 }
 
@@ -134,7 +135,7 @@ func (t *Test) SetRequestBody(body interface{}) error {
 	if t.magic {
 		t.RequestHeaders["content-length"] = strconv.Itoa(lbody)
 	}
-	if _, err := t.transaction.RequestBodyBuffer.Write([]byte(data)); err != nil {
+	if _, err := t.transaction.RequestBodyWriter().Write([]byte(data)); err != nil {
 		return err
 	}
 	return nil
@@ -151,7 +152,7 @@ func (t *Test) SetResponseBody(body interface{}) error {
 	if lbody == 0 {
 		return nil
 	}
-	if _, err := t.transaction.ResponseBodyBuffer.Write([]byte(data)); err != nil {
+	if _, err := t.transaction.ResponseBodyWriter().Write([]byte(data)); err != nil {
 		return err
 	}
 	return nil
@@ -189,24 +190,24 @@ func (t *Test) OutputInterruptionErrors() []string {
 	var errors []string
 
 	if t.ExpectedOutput.Interruption != nil {
-		if t.ExpectedOutput.Interruption.Action != t.transaction.Interruption.Action {
+		if t.ExpectedOutput.Interruption.Action != t.transaction.GetInterruption().Action {
 			errors = append(errors, fmt.Sprintf("Interruption.Action: expected: '%s', got: '%s'",
-				t.ExpectedOutput.Interruption.Action, t.transaction.Interruption.Action))
+				t.ExpectedOutput.Interruption.Action, t.transaction.GetInterruption().Action))
 		}
 
-		if t.ExpectedOutput.Interruption.Status != t.transaction.Interruption.Status {
+		if t.ExpectedOutput.Interruption.Status != t.transaction.GetInterruption().Status {
 			errors = append(errors, fmt.Sprintf("Interruption.Status: expected: '%d', got: '%d'",
-				t.ExpectedOutput.Interruption.Status, t.transaction.Interruption.Status))
+				t.ExpectedOutput.Interruption.Status, t.transaction.GetInterruption().Status))
 		}
 
-		if t.ExpectedOutput.Interruption.Data != t.transaction.Interruption.Data {
+		if t.ExpectedOutput.Interruption.Data != t.transaction.GetInterruption().Data {
 			errors = append(errors, fmt.Sprintf("Interruption.Data: expected: '%s', got: '%s'",
-				t.ExpectedOutput.Interruption.Data, t.transaction.Interruption.Data))
+				t.ExpectedOutput.Interruption.Data, t.transaction.GetInterruption().Data))
 		}
 
-		if t.ExpectedOutput.Interruption.RuleID != t.transaction.Interruption.RuleID {
+		if t.ExpectedOutput.Interruption.RuleID != t.transaction.GetInterruption().RuleID {
 			errors = append(errors, fmt.Sprintf("Interruption.RuleID: expected: '%d', got: '%d'",
-				t.ExpectedOutput.Interruption.RuleID, t.transaction.Interruption.RuleID))
+				t.ExpectedOutput.Interruption.RuleID, t.transaction.GetInterruption().RuleID))
 		}
 	}
 
@@ -251,7 +252,7 @@ func (t *Test) OutputErrors() []string {
 
 // LogContains checks if the log contains a string
 func (t *Test) LogContains(log string) bool {
-	for _, mr := range t.transaction.MatchedRules {
+	for _, mr := range t.transaction.GetMatchedRules() {
 		if strings.Contains(mr.ErrorLog(t.ResponseCode), log) {
 			return true
 		}
@@ -260,7 +261,7 @@ func (t *Test) LogContains(log string) bool {
 }
 
 // Transaction returns the transaction
-func (t *Test) Transaction() *coraza.Transaction {
+func (t *Test) Transaction() types.Transaction {
 	return t.transaction
 }
 
@@ -278,7 +279,7 @@ func (t *Test) Request() string {
 }
 
 // NewTest creates a new test with default properties
-func NewTest(name string, waf *coraza.WAF) *Test {
+func NewTest(name string, waf coraza.WAF) *Test {
 	t := &Test{
 		Name:           name,
 		transaction:    waf.NewTransaction(context.Background()),
diff --git a/testing/engine_test.go b/testing/engine_test.go
index e6f267c..04a23f2 100644
--- a/testing/engine_test.go
+++ b/testing/engine_test.go
@@ -4,6 +4,7 @@
 package testing
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
@@ -11,7 +12,7 @@ import (
 )
 
 func TestRawRequests(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig())
 	test := NewTest("test", waf)
 	if err := test.SetRawRequest([]byte("OPTIONS /test HTTP/1.1\r\nHost: www.example.com\r\n\r\n")); err != nil {
 		t.Error(err)
@@ -25,7 +26,7 @@ func TestRawRequests(t *testing.T) {
 }
 
 func TestDebug(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig())
 	test := NewTest("test", waf)
 	if err := test.SetRawRequest([]byte("OPTIONS /test HTTP/1.1\r\nHost: www.example.com\r\n\r\n")); err != nil {
 		t.Error(err)
@@ -33,7 +34,7 @@ func TestDebug(t *testing.T) {
 	if err := test.RunPhases(); err != nil {
 		t.Error(err)
 	}
-	debug := test.transaction.Debug()
+	debug := fmt.Sprintf("%s", test.transaction)
 	expected := []string{
 		"REQUEST_URI: /test",
 		"REQUEST_METHOD: OPTIONS",
@@ -46,7 +47,7 @@ func TestDebug(t *testing.T) {
 }
 
 func TestRequest(t *testing.T) {
-	waf := coraza.NewWAF()
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig())
 	test := NewTest("test", waf)
 	req := buildRequest("GET", "/test")
 	if err := test.SetRawRequest([]byte(req)); err != nil {
@@ -68,8 +69,8 @@ func TestRequest(t *testing.T) {
 }
 
 func TestResponse(t *testing.T) {
-	waf := coraza.NewWAF()
-	waf.ResponseBodyAccess = true
+	waf, _ := coraza.NewWAF(coraza.NewWAFConfig().
+		WithResponseBodyAccess(coraza.NewResponseBodyConfig()))
 	test := NewTest("test", waf)
 	req := buildRequest("POST", "/test")
 	if err := test.SetRawRequest([]byte(req)); err != nil {