Repository: corazawaf/coraza
Pull request #699 (2023-03-08 06:15:09)

Add typesafe auditlog config
I am looking at removing `types.Config` because it is a very implicit type and has awkwardness in using it, with allocations related to defaultValue and `interface{}`. We can use concrete types instead for all our uses as far as I can tell.

This starts by creating an auditlog config type, which needs to be public as its part of the auditlog plugin mechanism. Then it can be removed by adding an internal type for containing parser state/options which is its only other usage right now.

codecov[bot]: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Patch coverage: **`80.00`**% and project coverage change: **`+0.01`** :tada:
> Comparison is base [(`504fdde`)](https://codecov.io/gh/corazawaf/coraza/commit/504fddef55e740ee8dad4fd324dd1f56b89878ae?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) 80.91% compared to head [(`928c200`)](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) 80.93%.

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


```diff
@@            Coverage Diff             @@
##           v3/dev     #699      +/-   ##
==========================================
+ Coverage   80.91%   80.93%   +0.01%     
==========================================
  Files         146      146              
  Lines        8021     8029       +8     
==========================================
+ Hits         6490     6498       +8     
  Misses       1305     1305              
  Partials      226      226              
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `76.93% <81.25%> (+0.06%)` | :arrow_up: |
| examples | `27.30% <28.12%> (+0.08%)` | :arrow_up: |
| ftw | `50.70% <37.50%> (+0.05%)` | :arrow_up: |
| ftw-multiphase | `50.79% <37.50%> (+0.05%)` | :arrow_up: |
| tinygo | `75.95% <47.82%> (+0.02%)` | :arrow_up: |

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

| [Impacted Files](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) | Coverage Δ | |
|---|---|---|
| [internal/seclang/directives.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9kaXJlY3RpdmVzLmdv) | `70.91% <45.45%> (ø)` | |
| [auditlog/noop\_writer.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YXVkaXRsb2cvbm9vcF93cml0ZXIuZ28=) | `66.66% <66.66%> (ø)` | |
| [auditlog/concurrent\_writer.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YXVkaXRsb2cvY29uY3VycmVudF93cml0ZXIuZ28=) | `83.01% <100.00%> (ø)` | |
| [auditlog/logger.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YXVkaXRsb2cvbG9nZ2VyLmdv) | `75.00% <100.00%> (+12.50%)` | :arrow_up: |
| [auditlog/serial\_writer.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-YXVkaXRsb2cvc2VyaWFsX3dyaXRlci5nbw==) | `90.00% <100.00%> (-0.33%)` | :arrow_down: |
| [internal/seclang/parser.go](https://codecov.io/gh/corazawaf/coraza/pull/699?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9wYXJzZXIuZ28=) | `92.42% <100.00%> (+0.05%)` | :arrow_up: |

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/699?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: @jcchavezs We have the ability to register formatters or writers so it's some sort of pluggability, which is what I mean. Anyways, need to get this in to continue to make some more breaking changes
jcchavezs: Go for it LGTM

On Fri, 10 Mar 2023, 01:37 Anuraag Agrawal, ***@***.***>
wrote:

> @jcchavezs <https://github.com/jcchavezs> We have the ability to register
> formatters or writers so it's some sort of pluggability, which is what I
> mean. Anyways, need to get this in to continue to make some more breaking
> changes
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/corazawaf/coraza/pull/699#issuecomment-1463036599>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAXOYAVKCDMRNXTPHIGM2QLW3JZTLANCNFSM6AAAAAAVTL3JIY>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

anuraaga: @jcchavezs Can't merge without approval for this repo :P

Patch:
diff --git a/actions/ctl.go b/actions/ctl.go
index b2d8d8b..254e1d9 100644
--- a/actions/ctl.go
+++ b/actions/ctl.go
@@ -295,7 +295,7 @@ func (a *ctlFn) Evaluate(_ rules.RuleMetadata, txS rules.TransactionState) {
 			return
 		}
 
-		tx.SetDebugLogLevel(debuglog.LogLevel(lvl))
+		tx.SetDebugLogLevel(debuglog.Level(lvl))
 	}
 }
 
diff --git a/auditlog/concurrent_writer.go b/auditlog/concurrent_writer.go
index 2730944..68fb5a0 100644
--- a/auditlog/concurrent_writer.go
+++ b/auditlog/concurrent_writer.go
@@ -15,8 +15,6 @@ import (
 	"path"
 	"sync"
 	"time"
-
-	"github.com/corazawaf/coraza/v3/types"
 )
 
 type concurrentWriter struct {
@@ -29,16 +27,16 @@ type concurrentWriter struct {
 	closer        func() error
 }
 
-func (cl *concurrentWriter) Init(c types.Config) error {
-	cl.auditFileMode = c.Get("auditlog_file_mode", fs.FileMode(0644)).(fs.FileMode)
-	cl.auditDir = c.Get("auditlog_dir", "").(string)
-	cl.auditDirMode = c.Get("auditlog_dir_mode", fs.FileMode(0755)).(fs.FileMode)
-	cl.formatter = c.Get("auditlog_formatter", nativeFormatter).(LogFormatter)
+func (cl *concurrentWriter) Init(c Config) error {
+	cl.auditFileMode = c.FileMode
+	cl.auditDir = c.Dir
+	cl.auditDirMode = c.DirMode
+	cl.formatter = c.Formatter
 	cl.mux = &sync.RWMutex{}
 
 	w := io.Discard
-	if fileName := c.Get("auditlog_file", "").(string); fileName != "" {
-		f, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, cl.auditFileMode)
+	if c.File != "" {
+		f, err := os.OpenFile(c.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, cl.auditFileMode)
 		if err != nil {
 			return err
 		}
diff --git a/auditlog/logger.go b/auditlog/logger.go
index 9f5a83e..af4466d 100644
--- a/auditlog/logger.go
+++ b/auditlog/logger.go
@@ -5,11 +5,39 @@ package auditlog
 
 import (
 	"fmt"
+	"io/fs"
 	"strings"
-
-	"github.com/corazawaf/coraza/v3/types"
 )
 
+// Config is the configuration of a LogWriter.
+type Config struct {
+	// File is the path to the file to write the raw audit log to.
+	File string
+
+	// FileMode is the mode to use when creating File.
+	FileMode fs.FileMode
+
+	// Dir is the path to the directory to write formatted audit logs to.
+	Dir string
+
+	// DirMode is the mode to use when creating Dir.
+	DirMode fs.FileMode
+
+	// Formatter is the formatter to use when writing formatted audit logs.
+	Formatter LogFormatter
+}
+
+// NewConfig returns a Config with default values.
+func NewConfig() Config {
+	return Config{
+		File:      "",
+		FileMode:  0644,
+		Dir:       "",
+		DirMode:   0755,
+		Formatter: nativeFormatter,
+	}
+}
+
 // LogFormatter is the interface for all log formatters
 // A LogFormatter receives an auditlog and generates "readable" audit log
 type LogFormatter = func(al *Log) ([]byte, error)
@@ -19,7 +47,7 @@ type LogFormatter = func(al *Log) ([]byte, error)
 // An output stream may be a file, a socket, an http request, etc
 type LogWriter interface {
 	// Init the writer requires previous preparations
-	Init(types.Config) error
+	Init(Config) error
 	// Write the audit log
 	// Using the LogFormatter is mandatory to generate a "readable" audit log
 	// It is not sent as a bslice because some writers may require some Audit
diff --git a/auditlog/noop_writer.go b/auditlog/noop_writer.go
index 50017ec..ec0975d 100644
--- a/auditlog/noop_writer.go
+++ b/auditlog/noop_writer.go
@@ -7,15 +7,11 @@
 
 package auditlog
 
-import (
-	"github.com/corazawaf/coraza/v3/types"
-)
-
 // noopWriter is used to store logs in a single file
 type noopWriter struct{}
 
-func (noopWriter) Init(types.Config) error { return nil }
-func (noopWriter) Write(*Log) error        { return nil }
-func (noopWriter) Close() error            { return nil }
+func (noopWriter) Init(Config) error { return nil }
+func (noopWriter) Write(*Log) error  { return nil }
+func (noopWriter) Close() error      { return nil }
 
 var _ LogWriter = (*noopWriter)(nil)
diff --git a/auditlog/serial_writer.go b/auditlog/serial_writer.go
index cce9202..2a991ba 100644
--- a/auditlog/serial_writer.go
+++ b/auditlog/serial_writer.go
@@ -8,11 +8,8 @@ package auditlog
 
 import (
 	"io"
-	"io/fs"
 	"log"
 	"os"
-
-	"github.com/corazawaf/coraza/v3/types"
 )
 
 // serialWriter is used to store logs in a single file
@@ -22,14 +19,13 @@ type serialWriter struct {
 	formatter LogFormatter
 }
 
-func (sl *serialWriter) Init(c types.Config) error {
-	fileMode := c.Get("auditlog_file_mode", fs.FileMode(0644)).(fs.FileMode)
-	sl.formatter = c.Get("auditlog_formatter", nativeFormatter).(LogFormatter)
+func (sl *serialWriter) Init(c Config) error {
+	fileMode := c.FileMode
+	sl.formatter = c.Formatter
 
-	fileName := c.Get("auditlog_file", "").(string)
 	var w io.Writer
-	if fileName != "" {
-		f, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fileMode)
+	if c.File != "" {
+		f, err := os.OpenFile(c.File, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fileMode)
 		if err != nil {
 			return err
 		}
diff --git a/debuglog/default.go b/debuglog/default.go
index 05de9c3..509cb84 100644
--- a/debuglog/default.go
+++ b/debuglog/default.go
@@ -12,7 +12,7 @@ import (
 )
 
 type defaultEvent struct {
-	level   LogLevel
+	level   Level
 	printer Printer
 	fields  []byte
 }
@@ -90,7 +90,7 @@ func (defaultEvent) IsEnabled() bool {
 type defaultLogger struct {
 	printer       Printer
 	factory       PrinterFactory
-	level         LogLevel
+	level         Level
 	defaultFields []byte
 }
 
@@ -103,7 +103,7 @@ func (l defaultLogger) WithOutput(w io.Writer) Logger {
 	}
 }
 
-func (l defaultLogger) WithLevel(lvl LogLevel) Logger {
+func (l defaultLogger) WithLevel(lvl Level) Logger {
 	return defaultLogger{
 		printer:       l.printer,
 		factory:       l.factory,
@@ -126,43 +126,43 @@ func (l defaultLogger) With(fs ...ContextField) Logger {
 }
 
 func (l defaultLogger) Trace() Event {
-	if l.level < LogLevelTrace {
-		return NopEvent{}
+	if l.level < LevelTrace {
+		return noopEvent{}
 	}
 
-	return &defaultEvent{printer: l.printer, level: LogLevelTrace, fields: l.defaultFields}
+	return &defaultEvent{printer: l.printer, level: LevelTrace, fields: l.defaultFields}
 }
 
 func (l defaultLogger) Debug() Event {
-	if l.level < LogLevelDebug {
-		return NopEvent{}
+	if l.level < LevelDebug {
+		return noopEvent{}
 	}
 
-	return &defaultEvent{printer: l.printer, level: LogLevelDebug, fields: l.defaultFields}
+	return &defaultEvent{printer: l.printer, level: LevelDebug, fields: l.defaultFields}
 }
 
 func (l defaultLogger) Info() Event {
-	if l.level < LogLevelInfo {
-		return NopEvent{}
+	if l.level < LevelInfo {
+		return noopEvent{}
 	}
 
-	return &defaultEvent{printer: l.printer, level: LogLevelInfo, fields: l.defaultFields}
+	return &defaultEvent{printer: l.printer, level: LevelInfo, fields: l.defaultFields}
 }
 
 func (l defaultLogger) Warn() Event {
-	if l.level < LogLevelWarn {
-		return NopEvent{}
+	if l.level < LevelWarn {
+		return noopEvent{}
 	}
 
-	return &defaultEvent{printer: l.printer, level: LogLevelWarn, fields: l.defaultFields}
+	return &defaultEvent{printer: l.printer, level: LevelWarn, fields: l.defaultFields}
 }
 
 func (l defaultLogger) Error() Event {
-	if l.level < LogLevelError {
-		return NopEvent{}
+	if l.level < LevelError {
+		return noopEvent{}
 	}
 
-	return &defaultEvent{printer: l.printer, level: LogLevelError, fields: l.defaultFields}
+	return &defaultEvent{printer: l.printer, level: LevelError, fields: l.defaultFields}
 }
 
 // Default returns a default logger that writes to stderr.
@@ -170,13 +170,13 @@ func Default() Logger {
 	return DefaultWithPrinterFactory(defaultPrinterFactory)
 }
 
-type Printer func(lvl LogLevel, message, fields string)
+type Printer func(lvl Level, message, fields string)
 
 type PrinterFactory func(w io.Writer) Printer
 
 var defaultPrinterFactory = func(w io.Writer) Printer {
 	l := log.New(w, "", log.LstdFlags)
-	return func(lvl LogLevel, message, fields string) {
+	return func(lvl Level, message, fields string) {
 		l.Printf("[%s] %s %s", lvl.String(), message, fields)
 	}
 }
@@ -187,6 +187,6 @@ func DefaultWithPrinterFactory(f PrinterFactory) Logger {
 	return defaultLogger{
 		printer: f(os.Stderr),
 		factory: f,
-		level:   LogLevelInfo,
+		level:   LevelInfo,
 	}
 }
diff --git a/debuglog/level.go b/debuglog/level.go
index 52d8c4a..6ba1122 100644
--- a/debuglog/level.go
+++ b/debuglog/level.go
@@ -3,48 +3,48 @@
 
 package debuglog
 
-// LogLevel is the type of log level
-type LogLevel int8
+// Level is the type of log level
+type Level int8
 
 const (
-	// LogLevelUnknown is a default value for unknown log level
-	LogLevelUnknown LogLevel = iota - 1
-	// LogLevelNoLog is the lowest level of logging, no logs are generated
-	LogLevelNoLog
-	// LogLevelError is the level of logging only for errors
-	LogLevelError
-	// LogLevelWarn is the level of logging for warnings
-	LogLevelWarn
-	// LogLevelInfo is the lowest of logging for informational messages
-	LogLevelInfo
-	// LogLevelDebug is the level of logging for debug messages
-	LogLevelDebug
+	// LevelUnknown is a default value for unknown log level
+	LevelUnknown Level = iota - 1
+	// LevelNoLog is the lowest level of logging, no logs are generated
+	LevelNoLog
+	// LevelError is the level of logging only for errors
+	LevelError
+	// LevelWarn is the level of logging for warnings
+	LevelWarn
+	// LevelInfo is the lowest of logging for informational messages
+	LevelInfo
+	// LevelDebug is the level of logging for debug messages
+	LevelDebug
 	// ModSecurity compatibility, levels 4-8 will be Debug level
 	_ = iota + 2
-	// LogLevelTrace is the highest level of logging
-	LogLevelTrace
+	// LevelTrace is the highest level of logging
+	LevelTrace
 )
 
 // String returns the string representation of the log level
-func (level LogLevel) String() string {
+func (level Level) String() string {
 	switch {
-	case level == LogLevelNoLog:
+	case level == LevelNoLog:
 		return "NOLOG"
-	case level == LogLevelError:
+	case level == LevelError:
 		return "ERROR"
-	case level == LogLevelWarn:
+	case level == LevelWarn:
 		return "WARN"
-	case level == LogLevelInfo:
+	case level == LevelInfo:
 		return "INFO"
-	case level >= LogLevelDebug && level < LogLevelTrace:
+	case level >= LevelDebug && level < LevelTrace:
 		return "DEBUG"
-	case level == LogLevelTrace:
+	case level == LevelTrace:
 		return "TRACE"
 	}
 	return "UNKNOWN"
 }
 
 // Valid returns true if the log level is valid
-func (level LogLevel) Valid() bool {
-	return level >= LogLevelNoLog && level <= LogLevelTrace
+func (level Level) Valid() bool {
+	return level >= LevelNoLog && level <= LevelTrace
 }
diff --git a/debuglog/logger.go b/debuglog/logger.go
index bdac47a..57a68ac 100644
--- a/debuglog/logger.go
+++ b/debuglog/logger.go
@@ -73,7 +73,7 @@ type Logger interface {
 	WithOutput(w io.Writer) Logger
 
 	// Level creates a child logger with the minimum accepted level set to level.
-	WithLevel(lvl LogLevel) Logger
+	WithLevel(lvl Level) Logger
 
 	// WithOutput duplicates the current logger and adds context fields to it.
 	With(...ContextField) Logger
diff --git a/debuglog/nop.go b/debuglog/nop.go
index 8220ea1..c56c523 100644
--- a/debuglog/nop.go
+++ b/debuglog/nop.go
@@ -7,21 +7,22 @@ import (
 	"fmt"
 )
 
-type NopEvent struct{}
+type noopEvent struct{}
 
-func (NopEvent) Msg(string)                            {}
-func (e NopEvent) Str(string, string) Event            { return e }
-func (e NopEvent) Err(error) Event                     { return e }
-func (e NopEvent) Bool(string, bool) Event             { return e }
-func (e NopEvent) Int(string, int) Event               { return e }
-func (e NopEvent) Uint(string, uint) Event             { return e }
-func (e NopEvent) Stringer(string, fmt.Stringer) Event { return e }
-func (e NopEvent) IsEnabled() bool                     { return false }
+func (noopEvent) Msg(string)                            {}
+func (e noopEvent) Str(string, string) Event            { return e }
+func (e noopEvent) Err(error) Event                     { return e }
+func (e noopEvent) Bool(string, bool) Event             { return e }
+func (e noopEvent) Int(string, int) Event               { return e }
+func (e noopEvent) Uint(string, uint) Event             { return e }
+func (e noopEvent) Stringer(string, fmt.Stringer) Event { return e }
+func (e noopEvent) IsEnabled() bool                     { return false }
 
-func Nop() Logger {
+// Noop returns a Logger which does no logging.
+func Noop() Logger {
 	return defaultLogger{
-		printer: func(LogLevel, string, string) {},
+		printer: func(Level, string, string) {},
 		factory: defaultPrinterFactory,
-		level:   LogLevelNoLog,
+		level:   LevelNoLog,
 	}
 }
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 00ab7cb..fda6660 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -286,7 +286,7 @@ func (tx *Transaction) DebugLogger() debuglog.Logger {
 	return tx.debugLogger
 }
 
-func (tx *Transaction) SetDebugLogLevel(lvl debuglog.LogLevel) {
+func (tx *Transaction) SetDebugLogLevel(lvl debuglog.Level) {
 	tx.debugLogger = tx.debugLogger.WithLevel(lvl)
 }
 
diff --git a/internal/corazawaf/waf.go b/internal/corazawaf/waf.go
index 2753a01..b513c82 100644
--- a/internal/corazawaf/waf.go
+++ b/internal/corazawaf/waf.go
@@ -251,7 +251,7 @@ const _1gb = 1073741824
 
 // NewWAF creates a new WAF instance with default variables
 func NewWAF() *WAF {
-	logger := debuglog.Nop()
+	logger := debuglog.Noop()
 
 	logWriter, err := auditlog.GetLogWriter("serial")
 	if err != nil {
@@ -286,7 +286,7 @@ func (w *WAF) SetDebugLogOutput(wr io.Writer) {
 }
 
 // SetDebugLogLevel changes the debug level of the WAF instance
-func (w *WAF) SetDebugLogLevel(lvl debuglog.LogLevel) error {
+func (w *WAF) SetDebugLogLevel(lvl debuglog.Level) error {
 	if !lvl.Valid() {
 		return errors.New("invalid log level")
 	}
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index 6b6c9b4..024666b 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -20,13 +20,19 @@ import (
 	"github.com/corazawaf/coraza/v3/types"
 )
 
-// DirectiveOptions contains the parsed options for a directive
+// DirectiveOptions contains the parsed options for a directive. It is mutable and propagated
+// across multiple directives, to support collecting the options for audit logs for example.
+// TODO(anuraaga): Propagation of config probably should be separated from a directive's options.
 type DirectiveOptions struct {
 	WAF      *corazawaf.WAF
 	Config   types.Config
 	Opts     string
 	Path     []string
 	Datasets map[string][]string
+
+	// AuditLog is configuration of audit logging, populated by multiple directives and consumed by
+	// SecAuditLog.
+	AuditLog auditlog.Config
 }
 
 type directive = func(options *DirectiveOptions) error
@@ -575,8 +581,8 @@ func directiveSecAuditLog(options *DirectiveOptions) error {
 		return errEmptyOptions
 	}
 
-	options.Config.Set("auditlog_file", options.Opts)
-	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
+	options.AuditLog.File = options.Opts
+	if err := options.WAF.AuditLogWriter.Init(options.AuditLog); err != nil {
 		return err
 	}
 	return nil
@@ -591,7 +597,7 @@ func directiveSecAuditLogType(options *DirectiveOptions) error {
 	if err != nil {
 		return err
 	}
-	if err := writer.Init(options.Config); err != nil {
+	if err := writer.Init(options.AuditLog); err != nil {
 		return err
 	}
 	options.WAF.AuditLogWriter = writer
@@ -611,9 +617,9 @@ func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 	if err != nil {
 		return err
 	}
-	options.Config.Set("auditlog_formatter", formatter)
+	options.AuditLog.Formatter = formatter
 
-	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
+	if err := options.WAF.AuditLogWriter.Init(options.AuditLog); err != nil {
 		return err
 	}
 	return nil
@@ -624,8 +630,8 @@ func directiveSecAuditLogDir(options *DirectiveOptions) error {
 		return errEmptyOptions
 	}
 
-	options.Config.Set("auditlog_dir", options.Opts)
-	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
+	options.AuditLog.Dir = options.Opts
+	if err := options.WAF.AuditLogWriter.Init(options.AuditLog); err != nil {
 		return err
 	}
 	return nil
@@ -652,8 +658,8 @@ func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 	if err != nil {
 		return err
 	}
-	options.Config.Set("auditlog_dir_mode", fs.FileMode(auditLogDirMode))
-	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
+	options.AuditLog.DirMode = fs.FileMode(auditLogDirMode)
+	if err := options.WAF.AuditLogWriter.Init(options.AuditLog); err != nil {
 		return err
 	}
 	return nil
@@ -678,8 +684,8 @@ func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 	if err != nil {
 		return err
 	}
-	options.Config.Set("auditlog_file_mode", fs.FileMode(auditLogFileMode))
-	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
+	options.AuditLog.FileMode = fs.FileMode(auditLogFileMode)
+	if err := options.WAF.AuditLogWriter.Init(options.AuditLog); err != nil {
 		return err
 	}
 	return nil
@@ -909,7 +915,7 @@ func directiveSecDebugLogLevel(options *DirectiveOptions) error {
 	if err != nil {
 		return err
 	}
-	return options.WAF.SetDebugLogLevel(debuglog.LogLevel(lvl))
+	return options.WAF.SetDebugLogLevel(debuglog.Level(lvl))
 }
 
 func directiveSecRuleUpdateTargetByID(options *DirectiveOptions) error {
diff --git a/internal/seclang/parser.go b/internal/seclang/parser.go
index dcd664b..4e83064 100644
--- a/internal/seclang/parser.go
+++ b/internal/seclang/parser.go
@@ -12,6 +12,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/corazawaf/coraza/v3/auditlog"
 	"github.com/corazawaf/coraza/v3/internal/corazawaf"
 	"github.com/corazawaf/coraza/v3/internal/environment"
 	"github.com/corazawaf/coraza/v3/internal/io"
@@ -207,6 +208,7 @@ func NewParser(waf *corazawaf.WAF) *Parser {
 			WAF:      waf,
 			Config:   make(types.Config),
 			Datasets: make(map[string][]string),
+			AuditLog: auditlog.NewConfig(),
 		},
 		root: io.OSFS{},
 	}
diff --git a/internal/seclang/rule_parser.go b/internal/seclang/rule_parser.go
index d956456..bf98dc4 100644
--- a/internal/seclang/rule_parser.go
+++ b/internal/seclang/rule_parser.go
@@ -454,61 +454,70 @@ const unset = -1
 // function (pkg.actions) for each action split by comma (,)
 // Action arguments are allowed to wrap values between colons(”)
 func parseActions(actions string) ([]ruleAction, error) {
-	iskey := true
-	var key strings.Builder
-	var val strings.Builder
-	key.Reset()
-	val.Reset()
-
-	quoted := false
 	var res []ruleAction
 	var err error
 	disruptiveActionIndex := unset
-actionLoop:
-	for i := 0; i < len(actions); i++ {
+
+	beforeKey := -1 // index before first char of key
+	afterKey := -1  // index after last char of key and before first char of value
+
+	inQuotes := false
+
+	for i := 1; i < len(actions); i++ {
 		c := actions[i]
-		switch {
-		case iskey && c == ' ':
-			// skip whitespaces in key
-			continue actionLoop
-		case !quoted && c == ',':
-			res, disruptiveActionIndex, err = appendRuleAction(res, key.String(), val.String(), disruptiveActionIndex)
-			if err != nil {
-				return nil, err
+		if actions[i-1] == '\\' {
+			// Escaped character, no need to process
+			continue
+		}
+		if c == '\'' {
+			inQuotes = !inQuotes
+			continue
+		}
+		if inQuotes {
+			// Inside quotes, no need to process
+			continue
+		}
+		switch c {
+		case ':':
+			if afterKey != -1 {
+				// Reading value, no need to process
+				continue
 			}
-			key.Reset()
-			val.Reset()
-			iskey = true
-		case iskey && c == ':':
-			iskey = false
-		case !iskey && c == '\'' && actions[i-1] != '\\':
-			if quoted {
-				quoted = false
-				iskey = true
+			afterKey = i
+		case ',':
+			var val string
+			if afterKey == -1 {
+				// No value, we only have a key
+				afterKey = i
 			} else {
-				quoted = true
+				val = actions[afterKey+1 : i]
 			}
-		case !iskey:
-			if c == ' ' && !quoted {
-				// skip unquoted whitespaces
-				continue actionLoop
-			}
-			val.WriteByte(c)
-		case iskey:
-			key.WriteByte(c)
-		}
-		if i+1 == len(actions) {
-			// last action, returned disruptiveActionIndex is not needed
-			res, _, err = appendRuleAction(res, key.String(), val.String(), disruptiveActionIndex)
+			res, disruptiveActionIndex, err = appendRuleAction(res, actions[beforeKey+1:afterKey], val, disruptiveActionIndex)
 			if err != nil {
 				return nil, err
 			}
+			beforeKey = i
+			afterKey = -1
 		}
 	}
+	var val string
+	if afterKey == -1 {
+		// No value, we only have a key
+		afterKey = len(actions)
+	} else {
+		val = actions[afterKey+1:]
+	}
+	res, _, err = appendRuleAction(res, actions[beforeKey+1:afterKey], val, disruptiveActionIndex)
+	if err != nil {
+		return nil, err
+	}
 	return res, nil
 }
 
 func appendRuleAction(res []ruleAction, key string, val string, disruptiveActionIndex int) ([]ruleAction, int, error) {
+	key = strings.TrimSpace(key)
+	val = strings.TrimSpace(val)
+	val = utils.MaybeRemoveQuotes(val)
 	f, err := actionsmod.Get(key)
 	if err != nil {
 		return res, unset, err


Test patch:
diff --git a/actions/ctl_test.go b/actions/ctl_test.go
index eb23264..fd10599 100644
--- a/actions/ctl_test.go
+++ b/actions/ctl_test.go
@@ -318,7 +318,7 @@ func TestCtl(t *testing.T) {
 			defer logsBuf.Reset()
 
 			logger := debuglog.Default().
-				WithLevel(debuglog.LogLevelWarn).
+				WithLevel(debuglog.LevelWarn).
 				WithOutput(logsBuf)
 
 			waf := corazawaf.NewWAF()
diff --git a/auditlog/concurrent_writer_test.go b/auditlog/concurrent_writer_test.go
index aa92afb..49e0501 100644
--- a/auditlog/concurrent_writer_test.go
+++ b/auditlog/concurrent_writer_test.go
@@ -15,12 +15,10 @@ import (
 	"path/filepath"
 	"testing"
 	"time"
-
-	"github.com/corazawaf/coraza/v3/types"
 )
 
 func TestConcurrentWriterNoop(t *testing.T) {
-	config := types.Config{}
+	config := NewConfig()
 	writer := &concurrentWriter{}
 	if err := writer.Init(config); err != nil {
 		t.Errorf("unexpected error: %s", err.Error())
@@ -32,13 +30,13 @@ func TestConcurrentWriterNoop(t *testing.T) {
 }
 
 func TestConcurrentWriterFailsOnInit(t *testing.T) {
-	config := types.Config{
-		"auditlog_file":      "/unexisting.log",
-		"auditlog_dir":       t.TempDir(),
-		"auditlog_file_mode": fs.FileMode(0777),
-		"auditlog_dir_mode":  fs.FileMode(0777),
-		"auditlog_formatter": jsonFormatter,
-	}
+	config := NewConfig()
+	config.File = "/unexisting.log"
+	config.Dir = t.TempDir()
+	config.FileMode = fs.FileMode(0777)
+	config.DirMode = fs.FileMode(0777)
+	config.Formatter = jsonFormatter
+
 	writer := &concurrentWriter{}
 	if err := writer.Init(config); err == nil {
 		t.Error("expected error")
@@ -51,12 +49,12 @@ func TestConcurrentWriterWrites(t *testing.T) {
 	if err != nil {
 		t.Error("failed to create concurrent logger file")
 	}
-	config := types.Config{
-		"auditlog_file":      file.Name(),
-		"auditlog_dir":       dir,
-		"auditlog_file_mode": fs.FileMode(0777),
-		"auditlog_dir_mode":  fs.FileMode(0777),
-		"auditlog_formatter": jsonFormatter,
+	config := Config{
+		File:      file.Name(),
+		Dir:       dir,
+		FileMode:  fs.FileMode(0777),
+		DirMode:   fs.FileMode(0777),
+		Formatter: jsonFormatter,
 	}
 	ts := time.Now().UnixNano()
 	al := &Log{
diff --git a/auditlog/serial_writer_test.go b/auditlog/serial_writer_test.go
index 1f0ac69..c3c9999 100644
--- a/auditlog/serial_writer_test.go
+++ b/auditlog/serial_writer_test.go
@@ -12,12 +12,10 @@ import (
 	"path/filepath"
 	"strings"
 	"testing"
-
-	"github.com/corazawaf/coraza/v3/types"
 )
 
 func TestSerialLoggerFailsOnInit(t *testing.T) {
-	config := types.Config{}
+	config := NewConfig()
 	writer := &serialWriter{}
 	if err := writer.Init(config); err != nil {
 		t.Errorf("unexpected error: %s", err.Error())
@@ -29,13 +27,13 @@ func TestSerialLoggerFailsOnInit(t *testing.T) {
 }
 
 func TestSerialWriterFailsOnInit(t *testing.T) {
-	config := types.Config{
-		"auditlog_file":      "/unexisting.log",
-		"auditlog_dir":       t.TempDir(),
-		"auditlog_file_mode": fs.FileMode(0777),
-		"auditlog_dir_mode":  fs.FileMode(0777),
-		"auditlog_formatter": jsonFormatter,
-	}
+	config := NewConfig()
+	config.File = "/unexisting.log"
+	config.Dir = t.TempDir()
+	config.FileMode = fs.FileMode(0777)
+	config.DirMode = fs.FileMode(0777)
+	config.Formatter = jsonFormatter
+
 	writer := &serialWriter{}
 	if err := writer.Init(config); err == nil {
 		t.Error("expected error")
@@ -45,10 +43,10 @@ func TestSerialWriterFailsOnInit(t *testing.T) {
 func TestSerialWriterWrites(t *testing.T) {
 	tmp := filepath.Join(t.TempDir(), "audit.log")
 	writer := &serialWriter{}
-	config := types.Config{
-		"auditlog_file":      tmp,
-		"auditlog_formatter": jsonFormatter,
-	}
+	config := NewConfig()
+	config.File = tmp
+	config.Formatter = jsonFormatter
+
 	if err := writer.Init(config); err != nil {
 		t.Error(err)
 	}
diff --git a/debuglog/default_test.go b/debuglog/default_test.go
index 8158159..26c8b6b 100644
--- a/debuglog/default_test.go
+++ b/debuglog/default_test.go
@@ -43,11 +43,11 @@ func TestLoggerLogLevels(t *testing.T) {
 			buf := bytes.Buffer{}
 			Default().WithOutput(&buf)
 			for settedLevel := 0; settedLevel <= 9; settedLevel++ {
-				l := Default().WithOutput(io.Discard).WithLevel(LogLevel(settedLevel))
+				l := Default().WithOutput(io.Discard).WithLevel(Level(settedLevel))
 				event := tCase.logFunction(l)()
 				if settedLevel >= tCase.expectedLowestPrintedLevel {
-					if _, ok := event.(NopEvent); ok {
-						t.Fatalf("Missing expected log. Level: %s, Function: %s", LogLevel(settedLevel).String(), name)
+					if _, ok := event.(noopEvent); ok {
+						t.Fatalf("Missing expected log. Level: %s, Function: %s", Level(settedLevel).String(), name)
 					}
 
 					if !event.IsEnabled() {
@@ -55,7 +55,7 @@ func TestLoggerLogLevels(t *testing.T) {
 					}
 				}
 				if settedLevel < tCase.expectedLowestPrintedLevel {
-					if _, ok := event.(NopEvent); !ok {
+					if _, ok := event.(noopEvent); !ok {
 						t.Fatalf("Unexpected log. Level: %d, Function: %s", settedLevel, name)
 					}
 				}
@@ -66,7 +66,7 @@ func TestLoggerLogLevels(t *testing.T) {
 
 func TestMsg(t *testing.T) {
 	t.Run("empty error", func(t *testing.T) {
-		l := Default().WithOutput(io.Discard).WithLevel(LogLevelInfo)
+		l := Default().WithOutput(io.Discard).WithLevel(LevelInfo)
 		fields := l.Info().Err(nil).(*defaultEvent).fields
 		if want, have := 0, len(fields); want != have {
 			t.Fatalf("unexpected number of fields, want %d, have %d", want, have)
@@ -75,7 +75,7 @@ func TestMsg(t *testing.T) {
 
 	t.Run("empty message", func(t *testing.T) {
 		buf := bytes.Buffer{}
-		l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+		l := Default().WithOutput(&buf).WithLevel(LevelInfo)
 		l.Info().Msg("")
 		if want, have := 0, buf.Len(); want != have {
 			t.Fatalf("unexpected message length, want %d, have %d", want, have)
@@ -84,7 +84,7 @@ func TestMsg(t *testing.T) {
 
 	t.Run("message", func(t *testing.T) {
 		buf := bytes.Buffer{}
-		l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+		l := Default().WithOutput(&buf).WithLevel(LevelInfo)
 		l.Info().
 			Bool("a", true).
 			Int("b", -1).
@@ -106,7 +106,7 @@ func TestMsg(t *testing.T) {
 
 func TestLogMessagePrefixes(t *testing.T) {
 	buf := bytes.Buffer{}
-	l := Default().WithOutput(&buf).WithLevel(LogLevelTrace)
+	l := Default().WithOutput(&buf).WithLevel(LevelTrace)
 	testCases := map[string]struct {
 		logPrintEvent Event
 	}{
@@ -129,7 +129,7 @@ func TestLogMessagePrefixes(t *testing.T) {
 
 func TestWithLogger(t *testing.T) {
 	buf := bytes.Buffer{}
-	l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+	l := Default().WithOutput(&buf).WithLevel(LevelInfo)
 	l2 := l.
 		With(
 			Bool("a", true),
@@ -167,7 +167,7 @@ func TestWithLogger(t *testing.T) {
 
 func TestWithLoggerAccumulative(t *testing.T) {
 	buf := bytes.Buffer{}
-	l := Default().WithOutput(&buf).WithLevel(LogLevelInfo)
+	l := Default().WithOutput(&buf).WithLevel(LevelInfo)
 	l2 := l.With(Bool("a", true))
 	l3 := l2.With(Int("b", -1))
 	l3.Info().
diff --git a/debuglog/level_test.go b/debuglog/level_test.go
index e651021..5ae713e 100644
--- a/debuglog/level_test.go
+++ b/debuglog/level_test.go
@@ -10,17 +10,17 @@ import (
 
 func TestLevelString(t *testing.T) {
 	var tests = []struct {
-		level LogLevel
+		level Level
 		want  string
 	}{
-		{LogLevelNoLog, "NOLOG"},
-		{LogLevelError, "ERROR"},
-		{LogLevelWarn, "WARN"},
-		{LogLevelInfo, "INFO"},
-		{LogLevelDebug, "DEBUG"},
-		{LogLevelTrace, "TRACE"},
-		{LogLevelUnknown, "UNKNOWN"},
-		{LogLevel(11), "UNKNOWN"},
+		{LevelNoLog, "NOLOG"},
+		{LevelError, "ERROR"},
+		{LevelWarn, "WARN"},
+		{LevelInfo, "INFO"},
+		{LevelDebug, "DEBUG"},
+		{LevelTrace, "TRACE"},
+		{LevelUnknown, "UNKNOWN"},
+		{Level(11), "UNKNOWN"},
 	}
 
 	for _, test := range tests {
@@ -34,17 +34,17 @@ func TestLevelString(t *testing.T) {
 
 func TestLevelValid(t *testing.T) {
 	var tests = []struct {
-		level   LogLevel
+		level   Level
 		isValid bool
 	}{
-		{LogLevelUnknown, false},
-		{LogLevelNoLog, true},
-		{LogLevelError, true},
-		{LogLevelWarn, true},
-		{LogLevelInfo, true},
-		{LogLevelDebug, true},
-		{LogLevelTrace, true},
-		{LogLevel(11), false},
+		{LevelUnknown, false},
+		{LevelNoLog, true},
+		{LevelError, true},
+		{LevelWarn, true},
+		{LevelInfo, true},
+		{LevelDebug, true},
+		{LevelTrace, true},
+		{Level(11), false},
 	}
 
 	for _, test := range tests {
diff --git a/debuglog/nop_test.go b/debuglog/nop_test.go
index 1394592..353c393 100644
--- a/debuglog/nop_test.go
+++ b/debuglog/nop_test.go
@@ -9,8 +9,8 @@ import (
 )
 
 func TestNop(t *testing.T) {
-	l := Nop().(defaultLogger)
-	if want, have := l.level, LogLevelNoLog; want != have {
+	l := Noop().(defaultLogger)
+	if want, have := l.level, LevelNoLog; want != have {
 		t.Fatalf("unexpected log level when nop")
 	}
 
diff --git a/http/middleware_test.go b/http/middleware_test.go
index 455ac58..c5702e6 100644
--- a/http/middleware_test.go
+++ b/http/middleware_test.go
@@ -295,7 +295,7 @@ func TestHttpServer(t *testing.T) {
 
 	logger := debuglog.Default().
 		WithOutput(testLogOutput{t}).
-		WithLevel(debuglog.LogLevelInfo)
+		WithLevel(debuglog.LevelInfo)
 
 	// Perform tests
 	for name, tCase := range tests {
@@ -357,7 +357,7 @@ func TestHttpServerWithRuleEngineOff(t *testing.T) {
 	}
 	logger := debuglog.Default().
 		WithOutput(testLogOutput{t}).
-		WithLevel(debuglog.LogLevelInfo)
+		WithLevel(debuglog.LevelInfo)
 
 	// Perform tests
 	for name, tCase := range tests {
diff --git a/internal/corazawaf/transaction_test.go b/internal/corazawaf/transaction_test.go
index 18921af..8cea176 100644
--- a/internal/corazawaf/transaction_test.go
+++ b/internal/corazawaf/transaction_test.go
@@ -782,7 +782,7 @@ func TestProcessBodiesSkippedIfHeadersPhasesNotReached(t *testing.T) {
 	logBuffer := &bytes.Buffer{}
 	waf := NewWAF()
 	waf.SetDebugLogOutput(logBuffer)
-	_ = waf.SetDebugLogLevel(debuglog.LogLevelDebug)
+	_ = waf.SetDebugLogLevel(debuglog.LevelDebug)
 	tx := waf.NewTransaction()
 	tx.RuleEngine = types.RuleEngineOn
 	tx.RequestBodyAccess = true
@@ -1008,7 +1008,7 @@ func TestTxSetServerName(t *testing.T) {
 
 	waf := NewWAF()
 	waf.SetDebugLogOutput(logBuffer)
-	_ = waf.SetDebugLogLevel(debuglog.LogLevelWarn)
+	_ = waf.SetDebugLogLevel(debuglog.LevelWarn)
 
 	tx := waf.NewTransaction()
 	tx.lastPhase = types.PhaseRequestHeaders
@@ -1220,7 +1220,7 @@ func TestProcessorsIdempotencyWithAlreadyRaisedInterruption(t *testing.T) {
 
 	waf := NewWAF()
 	waf.SetDebugLogOutput(logBuffer)
-	_ = waf.SetDebugLogLevel(debuglog.LogLevelError)
+	_ = waf.SetDebugLogLevel(debuglog.LevelError)
 
 	expectedInterruption := &types.Interruption{
 		RuleID: 123,
diff --git a/testing/coraza_test.go b/testing/coraza_test.go
index 054330a..70f83a2 100644
--- a/testing/coraza_test.go
+++ b/testing/coraza_test.go
@@ -61,7 +61,7 @@ func TestEngine(t *testing.T) {
 func testList(t *testing.T, p *profile.Profile) ([]*Test, error) {
 	t.Helper()
 	logger := debuglog.Default().
-		WithLevel(debuglog.LogLevelDebug).
+		WithLevel(debuglog.LevelDebug).
 		WithOutput(testLogOutput{t})
 	var tests []*Test
 	for _, test := range p.Tests {