Repository: corazawaf/coraza
Pull request #723 (2023-03-14 18:50:43)

fix: skip action within the phase, adds tests
Decupled from https://github.com/corazawaf/coraza/pull/719/files.
This PR:
- Fixes the skip action containing it within the current phase (Doc: `The skip action works only within the current processing phase`)
- Adds some tests for skip and skipAfter actions


Patch:
diff --git a/auditlog/auditlog.go b/auditlog/auditlog.go
index 4311c5c..e772ac2 100644
--- a/auditlog/auditlog.go
+++ b/auditlog/auditlog.go
@@ -16,7 +16,7 @@ type Log struct {
 	Transaction Transaction `json:"transaction"`
 
 	// Messages contains the triggered rules information
-	Messages []Message `json:"messages"`
+	Messages []Message `json:"messages,omitempty"`
 }
 
 // Transaction contains transaction specific
@@ -32,13 +32,13 @@ type Transaction struct {
 	// Client IP Address string representation
 	ClientIP string `json:"client_ip"`
 
-	ClientPort int                 `json:"client_port"`
-	HostIP     string              `json:"host_ip"`
-	HostPort   int                 `json:"host_port"`
-	ServerID   string              `json:"server_id"`
-	Request    TransactionRequest  `json:"request"`
-	Response   TransactionResponse `json:"response"`
-	Producer   TransactionProducer `json:"producer"`
+	ClientPort int                  `json:"client_port"`
+	HostIP     string               `json:"host_ip"`
+	HostPort   int                  `json:"host_port"`
+	ServerID   string               `json:"server_id"`
+	Request    *TransactionRequest  `json:"request,omitempty"`
+	Response   *TransactionResponse `json:"response,omitempty"`
+	Producer   *TransactionProducer `json:"producer,omitempty"`
 }
 
 // TransactionResponse contains response specific
@@ -114,16 +114,16 @@ type logLegacy struct {
 	Transaction logLegacyTransaction `json:"transaction"`
 
 	// Section B or C
-	Request logLegacyRequest `json:"request"`
+	Request *logLegacyRequest `json:"request,omitempty"`
 
 	// Section J (File Uploads)
 	// TBI
 
 	// Section E and F
-	Response logLegacyResponse `json:"response"`
+	Response *logLegacyResponse `json:"response,omitempty"`
 
 	// Section H
-	AuditData logLegacyData `json:"audit_data"`
+	AuditData *logLegacyData `json:"audit_data,omitempty"`
 }
 
 type logLegacyTransaction struct {
@@ -140,7 +140,7 @@ type logLegacyRequest struct {
 	RequestLine string `json:"request_line"`
 	// Headers should be a map of slices but in this case they are
 	// joined by comma (,)
-	Headers map[string]string `json:"headers"`
+	Headers map[string]string `json:"headers,omitempty"`
 }
 
 type logLegacyResponse struct {
diff --git a/auditlog/concurrent_writer.go b/auditlog/concurrent_writer.go
index 1144efe..a5dfe8b 100644
--- a/auditlog/concurrent_writer.go
+++ b/auditlog/concurrent_writer.go
@@ -7,7 +7,6 @@
 package auditlog
 
 import (
-	"fmt"
 	"io"
 	"io/fs"
 	"log"
@@ -53,20 +52,12 @@ func (cl concurrentWriter) Write(al *Log) error {
 	// 192.168.3.130 192.168.3.1 - - [22/Aug/2009:13:24:20 +0100] "GET / HTTP/1.1" 200 56 "-" "-" SojdH8AAQEAAAugAQAAAAAA "-" /20090822/20090822-1324/20090822-132420-SojdH8AAQEAAAugAQAAAAAA 0 1248
 	t := time.Unix(0, al.Transaction.UnixTimestamp)
 
-	// append the two directories
-	p2 := fmt.Sprintf("/%s/%s/", t.Format("20060102"), t.Format("20060102-1504"))
-	logdir := path.Join(cl.auditDir, p2)
-	// Append the filename
-	fname := fmt.Sprintf("/%s-%s", t.Format("20060102-150405"), al.Transaction.ID)
-	filepath := path.Join(logdir, fname)
-	str := fmt.Sprintf("%s %s - - [%s] %q %d %d %q %q %s %q %s %d %d",
-		al.Transaction.ClientIP, al.Transaction.HostIP, al.Transaction.Timestamp,
-		fmt.Sprintf("%s %s %s", al.Transaction.Request.Method, al.Transaction.Request.URI,
-			al.Transaction.Request.HTTPVersion),
-		al.Transaction.Response.Status, 0 /*response length*/, "-", "-", al.Transaction.ID,
-		"-", filepath, 0, 0 /*request length*/)
-	err := os.MkdirAll(logdir, cl.auditDirMode)
-	if err != nil {
+	ymd := t.Format("20060102")
+	ymdhm := ymd + t.Format("-1504")
+	filename := ymdhm + t.Format("05") + "-" + al.Transaction.ID
+
+	logdir := path.Join(cl.auditDir, ymd, ymdhm)
+	if err := os.MkdirAll(logdir, cl.auditDirMode); err != nil {
 		return err
 	}
 
@@ -74,13 +65,24 @@ func (cl concurrentWriter) Write(al *Log) error {
 	if err != nil {
 		return err
 	}
-	err = os.WriteFile(filepath, []byte(jsdata), cl.auditFileMode)
-	if err != nil {
+
+	filepath := path.Join(logdir, filename)
+	if err = os.WriteFile(filepath, jsdata, cl.auditFileMode); err != nil {
 		return err
 	}
+
 	cl.mux.Lock()
 	defer cl.mux.Unlock()
-	cl.auditlogger.Println(str)
+
+	cl.auditlogger.Printf("%s %s - - [%s]", al.Transaction.ClientIP, al.Transaction.HostIP, al.Transaction.Timestamp)
+	if al.Transaction.Request != nil {
+		cl.auditlogger.Printf(` "%s %s %s"`, al.Transaction.Request.Method, al.Transaction.Request.URI, al.Transaction.Request.HTTPVersion)
+	}
+	if al.Transaction.Response != nil {
+		cl.auditlogger.Printf(` %d`, al.Transaction.Response.Status)
+	}
+	cl.auditlogger.Printf("%s - %s\n", al.Transaction.ID, filepath)
+
 	return nil
 }
 
diff --git a/auditlog/formats_json.go b/auditlog/formats_json.go
index ab2474d..d5e9448 100644
--- a/auditlog/formats_json.go
+++ b/auditlog/formats_json.go
@@ -24,23 +24,6 @@ func jsonFormatter(al *Log) ([]byte, error) {
 
 // Coraza legacy json format
 func legacyJSONFormatter(al *Log) ([]byte, error) {
-	reqHeaders := map[string]string{}
-	for k, v := range al.Transaction.Request.Headers {
-		reqHeaders[k] = strings.Join(v, ", ")
-	}
-	resHeaders := map[string]string{}
-	for k, v := range al.Transaction.Response.Headers {
-		resHeaders[k] = strings.Join(v, ", ")
-	}
-	var messages []string
-	for _, m := range al.Messages {
-		messages = append(messages, m.Message)
-	}
-	var producers []string
-	if conn := al.Transaction.Producer.Connector; conn != "" {
-		producers = append(producers, conn)
-	}
-	producers = append(producers, al.Transaction.Producer.Rulesets...)
 	al2 := logLegacy{
 		Transaction: logLegacyTransaction{
 			Time:          al.Transaction.Timestamp,
@@ -50,21 +33,49 @@ func legacyJSONFormatter(al *Log) ([]byte, error) {
 			LocalAddress:  al.Transaction.HostIP,
 			LocalPort:     al.Transaction.HostPort,
 		},
-		Request: logLegacyRequest{
+	}
+	if al.Transaction.Request != nil {
+		reqHeaders := map[string]string{}
+		for k, v := range al.Transaction.Request.Headers {
+			reqHeaders[k] = strings.Join(v, ", ")
+		}
+		al2.Request = &logLegacyRequest{
 			RequestLine: fmt.Sprintf("%s %s %s", al.Transaction.Request.Method, al.Transaction.Request.URI, al.Transaction.Request.HTTPVersion),
 			Headers:     reqHeaders,
-		},
-		Response: logLegacyResponse{
+		}
+	}
+	if al.Transaction.Response != nil {
+		resHeaders := map[string]string{}
+		for k, v := range al.Transaction.Response.Headers {
+			resHeaders[k] = strings.Join(v, ", ")
+		}
+		al2.Response = &logLegacyResponse{
 			Status:   al.Transaction.Response.Status,
 			Protocol: al.Transaction.Response.Protocol,
 			Headers:  resHeaders,
-		},
-		AuditData: logLegacyData{
+		}
+	}
+
+	if al.Transaction.Producer != nil {
+		var producers []string
+		if conn := al.Transaction.Producer.Connector; conn != "" {
+			producers = append(producers, conn)
+		}
+		producers = append(producers, al.Transaction.Producer.Rulesets...)
+		al2.AuditData = &logLegacyData{
 			Stopwatch:  logLegacyStopwatch{},
-			Messages:   messages,
 			Producer:   producers,
 			EngineMode: al.Transaction.Producer.RuleEngine,
-		},
+		}
+	}
+
+	if len(al.Messages) > 0 {
+		if al2.AuditData == nil {
+			al2.AuditData = &logLegacyData{}
+		}
+		for _, m := range al.Messages {
+			al2.AuditData.Messages = append(al2.AuditData.Messages, m.Message)
+		}
 	}
 
 	jsdata, err := json.Marshal(al2)
diff --git a/internal/corazawaf/rulegroup.go b/internal/corazawaf/rulegroup.go
index 2ea341e..96c9415 100644
--- a/internal/corazawaf/rulegroup.go
+++ b/internal/corazawaf/rulegroup.go
@@ -214,6 +214,8 @@ RulesLoop:
 	if tx.AllowType == corazatypes.AllowTypePhase {
 		tx.AllowType = corazatypes.AllowTypeUnset
 	}
+	// // Reset Skip counter at the end of each phase. Skip actions work only within the current processing phase
+	tx.Skip = 0
 
 	tx.stopWatches[phase] = time.Now().UnixNano() - ts
 	return tx.interruption != nil
diff --git a/internal/corazawaf/transaction.go b/internal/corazawaf/transaction.go
index 55bb3f1..eff912c 100644
--- a/internal/corazawaf/transaction.go
+++ b/internal/corazawaf/transaction.go
@@ -1291,101 +1291,112 @@ func (tx *Transaction) LastPhase() types.RulePhase {
 // AuditLog returns an AuditLog struct, used to write audit logs
 func (tx *Transaction) AuditLog() *auditlog.Log {
 	al := &auditlog.Log{}
-	al.Messages = nil
-	// YYYY/MM/DD HH:mm:ss
-	ts := time.Unix(0, tx.Timestamp).Format("2006/01/02 15:04:05")
 	al.Parts = tx.AuditLogParts
-	clientPort, _ := strconv.Atoi(tx.variables.remotePort.Get())
-	hostPort, _ := strconv.Atoi(tx.variables.serverPort.Get())
-	status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
-	al.Transaction = auditlog.Transaction{
-		Timestamp:     ts,
-		UnixTimestamp: tx.Timestamp,
-		ID:            tx.id,
-		ClientIP:      tx.variables.remoteAddr.Get(),
-		ClientPort:    clientPort,
-		HostIP:        tx.variables.serverAddr.Get(),
-		HostPort:      hostPort,
-		ServerID:      tx.variables.serverName.Get(), // TODO check
-		Request: auditlog.TransactionRequest{
-			Method:      tx.variables.requestMethod.Get(),
-			Protocol:    tx.variables.requestProtocol.Get(),
-			URI:         tx.variables.requestURI.Get(),
-			HTTPVersion: tx.variables.requestProtocol.Get(),
-			// Body and headers are audit variables.RequestUriRaws
-		},
-		Response: auditlog.TransactionResponse{
-			Status: status,
-			// body and headers are audit parts
-		},
-	}
-	rengine := tx.RuleEngine.String()
-
-	al.Transaction.Request.Headers = tx.variables.requestHeaders.Data()
-	al.Transaction.Request.Body = tx.variables.requestBody.Get()
-	// TODO maybe change to:
-	// al.Transaction.Request.Body = tx.RequestBodyBuffer.String()
-	al.Transaction.Response.Headers = tx.variables.responseHeaders.Data()
-	al.Transaction.Response.Body = tx.variables.responseBody.Get()
-	al.Transaction.Producer = auditlog.TransactionProducer{
-		Connector:  tx.WAF.ProducerConnector,
-		Version:    tx.WAF.ProducerConnectorVersion,
-		Server:     "",
-		RuleEngine: rengine,
-		Stopwatch:  tx.GetStopWatch(),
-		Rulesets:   tx.WAF.ComponentNames,
-	}
-	/*
-	* TODO:
-	* This part is a replacement for part C. It will log the same data as C in
-	* all cases except when multipart/form-data encoding in used. In this case,
-	* it will log a fake application/x-www-form-urlencoded body that contains
-	* the information about parameters but not about the files. This is handy
-	* if you don’t want to have (often large) files stored in your audit logs.
-	 */
-	// upload data
-	var files []auditlog.TransactionRequestFiles
-	al.Transaction.Request.Files = nil
-	for _, file := range tx.variables.files.Get("") {
-		var size int64
-		if fs := tx.variables.filesSizes.Get(file); len(fs) > 0 {
-			size, _ = strconv.ParseInt(fs[0], 10, 64)
-			// we ignore the error as it defaults to 0
-		}
-		ext := filepath.Ext(file)
-		at := auditlog.TransactionRequestFiles{
-			Size: size,
-			Name: file,
-			Mime: mime.TypeByExtension(ext),
-		}
-		files = append(files, at)
-	}
-	al.Transaction.Request.Files = files
-	var mrs []auditlog.Message
-	for _, mr := range tx.matchedRules {
-		r := mr.Rule()
-		for _, matchData := range mr.MatchedDatas() {
-			mrs = append(mrs, auditlog.Message{
-				Actionset: strings.Join(tx.WAF.ComponentNames, " "),
-				Message:   matchData.Message(),
-				Data: auditlog.MessageData{
-					File:     mr.Rule().File(),
-					Line:     mr.Rule().Line(),
-					ID:       r.ID(),
-					Rev:      r.Revision(),
-					Msg:      matchData.Message(),
-					Data:     matchData.Data(),
-					Severity: r.Severity(),
-					Ver:      r.Version(),
-					Maturity: r.Maturity(),
-					Accuracy: r.Accuracy(),
-					Tags:     r.Tags(),
-					Raw:      r.Raw(),
-				},
-			})
+
+	for _, part := range tx.AuditLogParts {
+		switch part {
+		case types.AuditLogPartAuditLogHeader:
+			clientPort, _ := strconv.Atoi(tx.variables.remotePort.Get())
+			hostPort, _ := strconv.Atoi(tx.variables.serverPort.Get())
+			// YYYY/MM/DD HH:mm:ss
+			ts := time.Unix(0, tx.Timestamp).Format("2006/01/02 15:04:05")
+			al.Transaction = auditlog.Transaction{
+				Timestamp:     ts,
+				UnixTimestamp: tx.Timestamp,
+				ID:            tx.id,
+				ClientIP:      tx.variables.remoteAddr.Get(),
+				ClientPort:    clientPort,
+				HostIP:        tx.variables.serverAddr.Get(),
+				HostPort:      hostPort,
+				ServerID:      tx.variables.serverName.Get(), // TODO check
+			}
+		case types.AuditLogPartRequestHeaders:
+			if al.Transaction.Request == nil {
+				al.Transaction.Request = &auditlog.TransactionRequest{}
+			}
+			al.Transaction.Request.Headers = tx.variables.requestHeaders.Data()
+		case types.AuditLogPartRequestBody:
+			if al.Transaction.Request == nil {
+				al.Transaction.Request = &auditlog.TransactionRequest{}
+			}
+			// TODO maybe change to:
+			// al.Transaction.Request.Body = tx.RequestBodyBuffer.String()
+			al.Transaction.Request.Body = tx.variables.requestBody.Get()
+
+			/*
+			* TODO:
+			* This part is a replacement for part C. It will log the same data as C in
+			* all cases except when multipart/form-data encoding in used. In this case,
+			* it will log a fake application/x-www-form-urlencoded body that contains
+			* the information about parameters but not about the files. This is handy
+			* if you don’t want to have (often large) files stored in your audit logs.
+			 */
+			// upload data
+			var files []auditlog.TransactionRequestFiles
+			al.Transaction.Request.Files = nil
+			for _, file := range tx.variables.files.Get("") {
+				var size int64
+				if fs := tx.variables.filesSizes.Get(file); len(fs) > 0 {
+					size, _ = strconv.ParseInt(fs[0], 10, 64)
+					// we ignore the error as it defaults to 0
+				}
+				ext := filepath.Ext(file)
+				at := auditlog.TransactionRequestFiles{
+					Size: size,
+					Name: file,
+					Mime: mime.TypeByExtension(ext),
+				}
+				files = append(files, at)
+			}
+			al.Transaction.Request.Files = files
+		case types.AuditLogPartIntermediaryResponseBody:
+			if al.Transaction.Response == nil {
+				al.Transaction.Response = &auditlog.TransactionResponse{}
+			}
+			al.Transaction.Response.Body = tx.variables.responseBody.Get()
+		case types.AuditLogPartResponseHeaders:
+			if al.Transaction.Response == nil {
+				al.Transaction.Response = &auditlog.TransactionResponse{}
+			}
+			status, _ := strconv.Atoi(tx.variables.responseStatus.Get())
+			al.Transaction.Response.Status = status
+			al.Transaction.Response.Headers = tx.variables.responseHeaders.Data()
+		case types.AuditLogPartAuditLogTrailer:
+			al.Transaction.Producer = &auditlog.TransactionProducer{
+				Connector:  tx.WAF.ProducerConnector,
+				Version:    tx.WAF.ProducerConnectorVersion,
+				Server:     "",
+				RuleEngine: tx.RuleEngine.String(),
+				Stopwatch:  tx.GetStopWatch(),
+				Rulesets:   tx.WAF.ComponentNames,
+			}
+		case types.AuditLogPartRulesMatched:
+			for _, mr := range tx.matchedRules {
+				r := mr.Rule()
+				for _, matchData := range mr.MatchedDatas() {
+					al.Messages = append(al.Messages, auditlog.Message{
+						Actionset: strings.Join(tx.WAF.ComponentNames, " "),
+						Message:   matchData.Message(),
+						Data: auditlog.MessageData{
+							File:     mr.Rule().File(),
+							Line:     mr.Rule().Line(),
+							ID:       r.ID(),
+							Rev:      r.Revision(),
+							Msg:      matchData.Message(),
+							Data:     matchData.Data(),
+							Severity: r.Severity(),
+							Ver:      r.Version(),
+							Maturity: r.Maturity(),
+							Accuracy: r.Accuracy(),
+							Tags:     r.Tags(),
+							Raw:      r.Raw(),
+						},
+					})
+				}
+			}
 		}
 	}
-	al.Messages = mrs
+
 	return al
 }
 


Test patch:
diff --git a/auditlog/concurrent_writer_test.go b/auditlog/concurrent_writer_test.go
index 49e0501..0c988f8 100644
--- a/auditlog/concurrent_writer_test.go
+++ b/auditlog/concurrent_writer_test.go
@@ -13,6 +13,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"reflect"
 	"testing"
 	"time"
 )
@@ -56,40 +57,44 @@ func TestConcurrentWriterWrites(t *testing.T) {
 		DirMode:   fs.FileMode(0777),
 		Formatter: jsonFormatter,
 	}
-	ts := time.Now().UnixNano()
-	al := &Log{
+	ts := time.Now()
+	expectedLog := &Log{
 		Transaction: Transaction{
-			UnixTimestamp: ts,
+			UnixTimestamp: ts.UnixNano(),
 			ID:            "123",
-			Request:       TransactionRequest{},
-			Response:      TransactionResponse{},
+			Request: &TransactionRequest{
+				Method:      "GET",
+				URI:         "/test",
+				HTTPVersion: "HTTP/1.1",
+			},
+			Response: &TransactionResponse{
+				Status: 201,
+			},
 		},
 	}
 	writer := &concurrentWriter{}
 	if err := writer.Init(config); err != nil {
 		t.Error("failed to init concurrent logger", err)
 	}
-	if err := writer.Write(al); err != nil {
+	if err := writer.Write(expectedLog); err != nil {
 		t.Error("failed to write to logger: ", err)
 	}
-	tt := time.Unix(0, ts)
-	p2 := fmt.Sprintf("/%s/%s/", tt.Format("20060102"), tt.Format("20060102-1504"))
-	logdir := path.Join(dir, p2)
-	// Append the filename
-	fname := fmt.Sprintf("/%s-%s", tt.Format("20060102-150405"), al.Transaction.ID)
-	p := path.Join(logdir, fname)
 
-	data, err := os.ReadFile(p)
+	fileName := fmt.Sprintf("/%s-%s", ts.Format("20060102-150405"), expectedLog.Transaction.ID)
+	logFile := path.Join(dir, ts.Format("20060102"), ts.Format("20060102-1504"), fileName)
+
+	logData, err := os.ReadFile(logFile)
 	if err != nil {
 		t.Error("failed to create audit file for concurrent logger")
 		return
 	}
-	al2 := &Log{}
-	if json.Unmarshal(data, al2) != nil {
-		t.Error("failed to parse json from concurrent audit log", p)
+	actualLog := &Log{}
+	if err := json.Unmarshal(logData, actualLog); err != nil {
+		t.Errorf("failed to parse json from concurrent audit log: %s", err.Error())
 	}
 
-	if err := writer.Close(); err != nil {
-		t.Errorf("unexpected error: %s", err.Error())
+	if !reflect.DeepEqual(expectedLog, actualLog) {
+		expectedLogStr, _ := json.Marshal(expectedLog)
+		t.Errorf("unexpected log entry, want:\n%s, have:\n%s", expectedLogStr, logData)
 	}
 }
diff --git a/auditlog/formats_test.go b/auditlog/formats_test.go
index ddda73a..995e93f 100644
--- a/auditlog/formats_test.go
+++ b/auditlog/formats_test.go
@@ -37,7 +37,7 @@ func createAuditLog() *Log {
 			Timestamp:     "02/Jan/2006:15:04:20 -0700",
 			UnixTimestamp: 0,
 			ID:            "123",
-			Request: TransactionRequest{
+			Request: &TransactionRequest{
 				URI:    "/test.php",
 				Method: "GET",
 				Headers: map[string][]string{
@@ -46,7 +46,7 @@ func createAuditLog() *Log {
 					},
 				},
 			},
-			Response: TransactionResponse{
+			Response: &TransactionResponse{
 				Status: 200,
 				Headers: map[string][]string{
 					"some": {
@@ -54,6 +54,10 @@ func createAuditLog() *Log {
 					},
 				},
 			},
+			Producer: &TransactionProducer{
+				Connector: "some connector",
+				Version:   "1.2.3",
+			},
 		},
 		Messages: []Message{
 			{
diff --git a/auditlog/logger_test.go b/auditlog/logger_test.go
index ce262ca..c9b3386 100644
--- a/auditlog/logger_test.go
+++ b/auditlog/logger_test.go
@@ -1,9 +1,15 @@
 // Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
 // SPDX-License-Identifier: Apache-2.0
 
+//go:build !tinygo
+// +build !tinygo
+
 package auditlog
 
-import "testing"
+import (
+	"reflect"
+	"testing"
+)
 
 func TestDefaultWriters(t *testing.T) {
 	ws := []string{"serial", "concurrent"}
@@ -15,3 +21,30 @@ func TestDefaultWriters(t *testing.T) {
 		}
 	}
 }
+
+func TestGetUnknownWriter(t *testing.T) {
+	if _, err := GetWriter("unknown"); err == nil {
+		t.Error("expected error")
+	}
+}
+
+func TestGetFormatters(t *testing.T) {
+	t.Run("missing formatter", func(t *testing.T) {
+		if _, err := GetFormatter("missing"); err == nil {
+			t.Error("expected error")
+		}
+	})
+
+	t.Run("existing formatter", func(t *testing.T) {
+		expectedFn := func(al *Log) ([]byte, error) { return nil, nil }
+		RegisterFormatter("test", expectedFn)
+		actualFn, err := GetFormatter("TeSt")
+		if err != nil {
+			t.Errorf("unexpected error: %s", err.Error())
+		}
+
+		if want, have := reflect.ValueOf(expectedFn), reflect.ValueOf(actualFn); want.Pointer() != have.Pointer() {
+			t.Errorf("unexpected formatter function")
+		}
+	})
+}
diff --git a/testing/auditlog_test.go b/testing/auditlog_test.go
index 3610865..0a52f9f 100644
--- a/testing/auditlog_test.go
+++ b/testing/auditlog_test.go
@@ -36,6 +36,7 @@ func TestAuditLogMessages(t *testing.T) {
 		SecAuditEngine On
 		SecAuditLogFormat json
 		SecAuditLogType serial
+		SecAuditLogParts ABCDEFGHIJKZ
 		SecRule ARGS "@unconditionalMatch" "id:1,phase:1,log,msg:'unconditional match'"
 	`); err != nil {
 		t.Error(err)
diff --git a/testing/engine/skip.go b/testing/engine/skip.go
new file mode 100644
index 0000000..20c9111
--- /dev/null
+++ b/testing/engine/skip.go
@@ -0,0 +1,212 @@
+// Copyright 2022 Juan Pablo Tosso and the OWASP Coraza contributors
+// SPDX-License-Identifier: Apache-2.0
+
+package engine
+
+import (
+	"github.com/corazawaf/coraza/v3/testing/profile"
+)
+
+var _ = profile.RegisterProfile(profile.Profile{
+	Meta: profile.Meta{
+		Author:      "M4tteoP",
+		Description: "Tests skip actions",
+		Enabled:     true,
+		Name:        "skip.yaml",
+	},
+	Tests: []profile.Test{
+		{
+			Title: "skip actions",
+			Stages: []profile.Stage{
+				{
+					Stage: profile.SubStage{
+						Input: profile.StageInput{
+							URI: "/firstskip",
+						},
+						Output: profile.ExpectedOutput{
+							TriggeredRules:    []int{1, 2},
+							NonTriggeredRules: []int{3},
+							Interruption: &profile.ExpectedInterruption{
+								Status: 500,
+								Data:   "",
+								RuleID: 2,
+								Action: "deny",
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+	Rules: `
+SecDebugLogLevel 5
+
+# Skip action of rule 1 will skip the next rule (if any) of the same phase. Therefore rule 3 will be skipped and rule 2 will be triggered.
+SecRule REQUEST_URI "/firstskip" "id:1, phase:1, skip:1,log"
+SecRule REQUEST_URI "/firstskip" "id:2, phase:2, t:none, log, deny, status:500"
+SecRule REQUEST_URI "/firstskip" "id:3, phase:1, t:none, log, deny, status:500"
+`,
+})
+
+var _ = profile.RegisterProfile(profile.Profile{
+	Meta: profile.Meta{
+		Author:      "M4tteoP",
+		Description: "Tests skip actions",
+		Enabled:     true,
+		Name:        "skip_2.yaml",
+	},
+	Tests: []profile.Test{
+		{
+			Title: "skip actions",
+			Stages: []profile.Stage{
+				{
+					Stage: profile.SubStage{
+						Input: profile.StageInput{
+							URI:     "/phase_skip",
+							Method:  "POST",
+							Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+							Data:    "phase_skip",
+						},
+						Output: profile.ExpectedOutput{
+							TriggeredRules: []int{20, 21},
+							Interruption: &profile.ExpectedInterruption{
+								Status: 500,
+								Data:   "",
+								RuleID: 21,
+								Action: "deny",
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+	Rules: `
+SecDebugLogLevel 5
+SecRequestBodyAccess On
+
+# Skip action works only within the current phase, rule 20 should not skip rule 21 
+SecRule REQUEST_URI "/phase_skip" "id:20, phase:1, skip:1,log"
+SecRule REQUEST_BODY "phase_skip" "id:21, phase:2, t:none, log, deny, status:500"
+`,
+})
+var _ = profile.RegisterProfile(profile.Profile{
+	Meta: profile.Meta{
+		Author:      "M4tteoP",
+		Description: "Tests skip actions",
+		Enabled:     true,
+		Name:        "skip_3.yaml",
+	},
+	Tests: []profile.Test{
+		{
+			Title: "skip actions",
+			Stages: []profile.Stage{
+				{
+					Stage: profile.SubStage{
+						Input: profile.StageInput{
+							URI:     "/phase_multi_skip",
+							Method:  "POST",
+							Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+							Data:    "phase_multi_skip",
+						},
+						Output: profile.ExpectedOutput{
+							TriggeredRules:    []int{30, 31},
+							NonTriggeredRules: []int{32},
+						},
+					},
+				},
+			},
+		},
+	},
+	Rules: `
+SecDebugLogLevel 5
+SecRequestBodyAccess On
+
+# Skip action works only within the current phase, rule 30 should skip rule 32, not rule 31
+SecRule REQUEST_URI "/phase_multi_skip" "id:30, phase:2, skip:1,log"
+SecRule REQUEST_URI "/phase_multi_skip" "id:31, phase:1,pass"
+SecRule REQUEST_BODY "phase_multi_skip" "id:32, phase:2, t:none, log, deny, status:500"
+`,
+})
+
+var _ = profile.RegisterProfile(profile.Profile{
+	Meta: profile.Meta{
+		Author:      "M4tteoP",
+		Description: "Tests skipafter action",
+		Enabled:     true,
+		Name:        "skipafter.yaml",
+	},
+	Tests: []profile.Test{
+		{
+			Title: "skipafter action",
+			Stages: []profile.Stage{
+				{
+					Stage: profile.SubStage{
+						Input: profile.StageInput{
+							URI:     "/single_skipafter_phase",
+							Method:  "POST",
+							Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+							Data:    "single_skipafter_phase",
+						},
+						Output: profile.ExpectedOutput{
+							TriggeredRules: []int{40, 41},
+							Interruption: &profile.ExpectedInterruption{
+								Status: 500,
+								Data:   "",
+								RuleID: 41,
+								Action: "deny",
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+	Rules: `
+SecDebugLogLevel 5
+
+# SkipAfter action works only within the current phase
+SecRule REQUEST_URI "/single_skipafter_phase" "id:40, phase:1, skipAfter:LOCATION_ONE,log"
+SecRule REQUEST_URI|REQUEST_BODY "single_skipafter_phase" "id:41, phase:2, t:none, log, deny, status:500"
+SecMarker LOCATION_ONE
+`,
+})
+
+var _ = profile.RegisterProfile(profile.Profile{
+	Meta: profile.Meta{
+		Author:      "M4tteoP",
+		Description: "Tests skipafter action",
+		Enabled:     true,
+		Name:        "skipafter_2.yaml",
+	},
+	Tests: []profile.Test{
+		{
+			Title: "skipafter action",
+			Stages: []profile.Stage{
+				{
+					Stage: profile.SubStage{
+						Input: profile.StageInput{
+							URI:     "/multi_skipafter_phase",
+							Method:  "POST",
+							Headers: map[string]string{"Content-Type": "application/x-www-form-urlencoded"},
+							Data:    "multi_skipafter_phase",
+						},
+						Output: profile.ExpectedOutput{
+							TriggeredRules:    []int{50, 51},
+							NonTriggeredRules: []int{52},
+						},
+					},
+				},
+			},
+		},
+	},
+	Rules: `
+SecDebugLogLevel 5
+
+# skipAfter action works only within the current phase, rule 50 should skip rule 52, not rule 51
+SecRule REQUEST_URI "/multi_skipafter_phase" "id:50, phase:2, skipAfter:LOCATION_TWO,log"
+SecRule REQUEST_URI "/multi_skipafter_phase" "id:51, phase:1,pass"
+SecRule REQUEST_BODY "multi_skipafter_phase" "id:52, phase:2, t:none, log, deny, status:500"
+SecMarker LOCATION_TWO
+`,
+})