Repository: corazawaf/coraza
Pull request #624 (2023-02-10 00:15:25)

fix: fixes the parser to avoid invalid rules.
Currently if the directive is invalid (e.g. a valid directive without options) the parser would not fail. This PR changes that so that the directives will fail if malformed.

**Important:** Until now we are operating under the assumption **the only direction** which does not require options is `directiveSecResponseBodyMimeTypesClear` (to be confirmed by @airween @dune73 @fzipi)

Funny note: this has been found by trying to resurrect the playground

codecov[bot]: # [Codecov](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) Report
Base: **77.86**% // Head: **76.91**% // Decreases project coverage by **`-0.95%`** :warning:
> Coverage data is based on head [(`77eb543`)](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf) compared to base [(`ec9d46c`)](https://codecov.io/gh/corazawaf/coraza/commit/ec9d46c1b177bb92cda788f1a2c7cfd02be8882c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf).
> Patch coverage: 12.96% of modified lines in pull request are covered.

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


```diff
@@            Coverage Diff             @@
##           v3/dev     #624      +/-   ##
==========================================
- Coverage   77.86%   76.91%   -0.95%     
==========================================
  Files         144      137       -7     
  Lines        7509     7547      +38     
==========================================
- Hits         5847     5805      -42     
- Misses       1432     1485      +53     
- Partials      230      257      +27     
```

| Flag | Coverage Δ | |
|---|---|---|
| default | `72.68% <12.96%> (-0.83%)` | :arrow_down: |
| examples | `29.60% <2.77%> (-0.33%)` | :arrow_down: |
| ftw | `54.24% <1.85%> (-0.62%)` | :arrow_down: |
| ftw-multiphase | `?` | |
| tinygo | `?` | |

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/624?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/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9kaXJlY3RpdmVzLmdv) | `55.72% <5.10%> (-12.36%)` | :arrow_down: |
| [types/waf.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-dHlwZXMvd2FmLmdv) | `91.17% <66.66%> (ø)` | |
| [internal/seclang/parser.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9wYXJzZXIuZ28=) | `93.23% <100.00%> (+0.20%)` | :arrow_up: |
| [bodyprocessors/multipart.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-Ym9keXByb2Nlc3NvcnMvbXVsdGlwYXJ0Lmdv) | `78.48% <0.00%> (-3.80%)` | :arrow_down: |
| [internal/corazawaf/rule.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvY29yYXphd2FmL3J1bGUuZ28=) | `67.47% <0.00%> (-1.42%)` | :arrow_down: |
| [internal/seclang/rule\_parser.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc2VjbGFuZy9ydWxlX3BhcnNlci5nbw==) | `82.77% <0.00%> (-0.84%)` | :arrow_down: |
| [internal/sync/pool\_tinygo.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-aW50ZXJuYWwvc3luYy9wb29sX3Rpbnlnby5nbw==) | | |
| [operators/inspect\_file\_tinygo.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-b3BlcmF0b3JzL2luc3BlY3RfZmlsZV90aW55Z28uZ28=) | | |
| [bodyprocessors/xml\_tinygo.go](https://codecov.io/gh/corazawaf/coraza/pull/624?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=corazawaf#diff-Ym9keXByb2Nlc3NvcnMveG1sX3Rpbnlnby5nbw==) | | |
| ... and [4 more](https://codecov.io/gh/corazawaf/coraza/pull/624?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/624?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).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=coraza&pullRequest=624)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL) [99 Code Smells](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_coverage&view=list)  
[![0.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.1%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_duplicated_lines_density&view=list) [0.1% Duplication](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=coraza&pullRequest=624)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=coraza&pullRequest=624&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL) [99 Code Smells](https://sonarcloud.io/project/issues?id=coraza&pullRequest=624&resolved=false&types=CODE_SMELL)

[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/0-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_coverage&view=list) [0.0% Coverage](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_coverage&view=list)  
[![0.1%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.1%')](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_duplicated_lines_density&view=list) [0.1% Duplication](https://sonarcloud.io/component_measures?id=coraza&pullRequest=624&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/internal/seclang/directives.go b/internal/seclang/directives.go
index 8c299e8..0459e05 100644
--- a/internal/seclang/directives.go
+++ b/internal/seclang/directives.go
@@ -14,6 +14,7 @@ import (
 	"strings"
 
 	"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"
 )
@@ -49,7 +50,12 @@ func directiveInclude(_ *DirectiveOptions) error {
 
 var _ directive = directiveInclude
 
+var errEmptyOptions = errors.New("expected options")
+
 func directiveSecComponentSignature(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
 	options.WAF.ComponentNames = append(options.WAF.ComponentNames, options.Opts)
 	return nil
 }
@@ -87,6 +93,10 @@ func directiveSecComponentSignature(options *DirectiveOptions) error {
 //
 // ```
 func directiveSecMarker(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	rule := corazawaf.NewRule()
 	rule.Raw_ = fmt.Sprintf("SecMarker %s", options.Opts)
 	rule.SecMark_ = options.Opts
@@ -95,7 +105,7 @@ func directiveSecMarker(options *DirectiveOptions) error {
 	rule.Line_ = options.Config.Get("parser_last_line", 0).(int)
 	rule.File_ = options.Config.Get("parser_config_file", "").(string)
 	if err := options.WAF.Rules.Add(rule); err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	options.WAF.Logger.Debug("added secmark rule")
 	return nil
@@ -112,6 +122,10 @@ func directiveSecMarker(options *DirectiveOptions) error {
 // SecAction "nolog,phase:1,initcol:RESOURCE=%{REQUEST_FILENAME}"
 // ```
 func directiveSecAction(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	rule, err := ParseRule(RuleOptions{
 		WithOperator: false,
 		WAF:          options.WAF,
@@ -120,10 +134,10 @@ func directiveSecAction(options *DirectiveOptions) error {
 		Data:         options.Opts,
 	})
 	if err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	if err := options.WAF.Rules.Add(rule); err != nil {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	}
 	options.WAF.Logger.Debug("Added SecAction: %s", options.Opts)
 	return nil
@@ -145,6 +159,10 @@ func directiveSecAction(options *DirectiveOptions) error {
 // SecRule ARGS "@rx attack" "phase:1,log,deny,id:1"
 // ```
 func directiveSecRule(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	ignoreErrors := options.Config.Get("ignore_rule_compilation_errors", false).(bool)
 	rule, err := ParseRule(RuleOptions{
 		WithOperator: true,
@@ -154,7 +172,7 @@ func directiveSecRule(options *DirectiveOptions) error {
 		Data:         options.Opts,
 	})
 	if err != nil && !ignoreErrors {
-		return newCompileRuleError(err, options.Opts)
+		return err
 	} else if err != nil && ignoreErrors {
 		options.WAF.Logger.Debug("Ignoring rule compilation error for rule %s: %s", options.Opts, err.Error())
 		return nil
@@ -179,9 +197,13 @@ func directiveSecRule(options *DirectiveOptions) error {
 // configured with `SecResponseBodyMimeType`).
 // - Off: do not buffer response bodies.
 func directiveSecResponseBodyAccess(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	b, err := parseBoolean(strings.ToLower(options.Opts))
 	if err != nil {
-		return newDirectiveError(err, "SecResponseBodyAccess")
+		return err
 	}
 	options.WAF.ResponseBodyAccess = b
 	return nil
@@ -194,6 +216,10 @@ func directiveSecResponseBodyAccess(options *DirectiveOptions) error {
 // Anything over the limit will be rejected with status code 413 (Request Entity Too Large).
 // There is a hard limit of 1 GB.
 func directiveSecRequestBodyLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	limit, err := strconv.ParseInt(options.Opts, 10, 64)
 	if err != nil {
 		return err
@@ -212,9 +238,13 @@ func directiveSecRequestBodyLimit(options *DirectiveOptions) error {
 // - On: buffer request bodies
 // - Off: do not buffer request bodies
 func directiveSecRequestBodyAccess(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	b, err := parseBoolean(strings.ToLower(options.Opts))
 	if err != nil {
-		return newDirectiveError(err, "SecRequestBodyAccess")
+		return err
 	}
 	options.WAF.RequestBodyAccess = b
 	return nil
@@ -240,21 +270,37 @@ func directiveUnsupported(options *DirectiveOptions) error {
 }
 
 func directiveSecWebAppID(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.WebAppID = options.Opts
 	return nil
 }
 
 func directiveSecTmpDir(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.TmpDir = options.Opts
 	return nil
 }
 
 func directiveSecServerSignature(options *DirectiveOptions) error {
-	options.WAF.ServerSignature = options.Opts
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	options.WAF.ServerSignature = utils.MaybeRemoveQuotes(options.Opts)
 	return nil
 }
 
 func directiveSecRuleRemoveByTag(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	for _, r := range options.WAF.Rules.FindByTag(options.Opts) {
 		options.WAF.Rules.DeleteByID(r.ID_)
 	}
@@ -262,6 +308,10 @@ func directiveSecRuleRemoveByTag(options *DirectiveOptions) error {
 }
 
 func directiveSecRuleRemoveByMsg(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	for _, r := range options.WAF.Rules.FindByMsg(options.Opts) {
 		options.WAF.Rules.DeleteByID(r.ID_)
 	}
@@ -269,17 +319,27 @@ func directiveSecRuleRemoveByMsg(options *DirectiveOptions) error {
 }
 
 func directiveSecRuleRemoveByID(options *DirectiveOptions) error {
-	id, _ := strconv.Atoi(options.Opts)
+	id, err := strconv.Atoi(options.Opts)
+	if err != nil {
+		return err
+	}
 	options.WAF.Rules.DeleteByID(id)
 	return nil
 }
 
 func directiveSecResponseBodyMimeTypesClear(options *DirectiveOptions) error {
+	if len(options.Opts) > 0 {
+		return errors.New("unexpected options")
+	}
 	options.WAF.ResponseBodyMimeTypes = nil
 	return nil
 }
 
 func directiveSecResponseBodyMimeType(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.ResponseBodyMimeTypes = strings.Split(options.Opts, " ")
 	return nil
 }
@@ -321,6 +381,10 @@ func directiveSecResponseBodyLimitAction(options *DirectiveOptions) error {
 // This setting will not affect the responses with MIME types that are not selected for
 // buffering. There is a hard limit of 1 GB.
 func directiveSecResponseBodyLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	limit, err := strconv.ParseInt(options.Opts, 10, 64)
 	if err != nil {
 		return err
@@ -355,6 +419,10 @@ func directiveSecRequestBodyLimitAction(options *DirectiveOptions) error {
 // When a `multipart/form-data` request is being processed, once the in-memory limit is reached,
 // the request body will start to be streamed into a temporary file on disk.
 func directiveSecRequestBodyInMemoryLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	limit, err := strconv.ParseInt(options.Opts, 10, 64)
 	if err != nil {
 		return err
@@ -364,7 +432,18 @@ func directiveSecRequestBodyInMemoryLimit(options *DirectiveOptions) error {
 }
 
 func directiveSecRemoteRulesFailAction(options *DirectiveOptions) error {
-	options.WAF.AbortOnRemoteRulesFail = strings.ToLower(options.Opts) == "abort"
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
+	switch strings.ToLower(options.Opts) {
+	case "abort":
+		options.WAF.AbortOnRemoteRulesFail = true
+	case "warn":
+		options.WAF.AbortOnRemoteRulesFail = false
+	default:
+		return errors.New("unknown option")
+	}
 	return nil
 }
 
@@ -377,6 +456,10 @@ func directiveSecConnWriteStateLimit(options *DirectiveOptions) error {
 }
 
 func directiveSecSensorID(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.SensorID = options.Opts
 	return nil
 }
@@ -439,6 +522,10 @@ func directiveSecHashEngine(options *DirectiveOptions) error {
 // Important: Every `SecDefaultAction` directive must specify a disruptive action and a processing
 // phase and cannot contain metadata actions.
 func directiveSecDefaultAction(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	da, _ := options.Config.Get("rule_default_actions", []string{}).([]string)
 	da = append(da, options.Opts)
 	options.Config.Set("rule_default_actions", da)
@@ -485,7 +572,7 @@ func directiveSecCollectionTimeout(options *DirectiveOptions) error {
 // or for the directory.
 func directiveSecAuditLog(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLog /some/absolute/path.log")
+		return errEmptyOptions
 	}
 
 	options.Config.Set("auditlog_file", options.Opts)
@@ -497,8 +584,9 @@ func directiveSecAuditLog(options *DirectiveOptions) error {
 
 func directiveSecAuditLogType(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogType [concurrent/https/serial/...]")
+		return errEmptyOptions
 	}
+
 	writer, err := loggers.GetLogWriter(options.Opts)
 	if err != nil {
 		return err
@@ -516,8 +604,9 @@ func directiveSecAuditLogType(options *DirectiveOptions) error {
 // Default: Native
 func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogFormat [json/native/...]")
+		return errEmptyOptions
 	}
+
 	formatter, err := loggers.GetLogFormatter(options.Opts)
 	if err != nil {
 		return err
@@ -532,8 +621,9 @@ func directiveSecAuditLogFormat(options *DirectiveOptions) error {
 
 func directiveSecAuditLogDir(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogDir /some/absolute/path")
+		return errEmptyOptions
 	}
+
 	options.Config.Set("auditlog_dir", options.Opts)
 	if err := options.WAF.AuditLogWriter.Init(options.Config); err != nil {
 		return err
@@ -555,8 +645,9 @@ func directiveSecAuditLogDir(options *DirectiveOptions) error {
 // ```
 func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogDirMode [0777/0700/...]")
+		return errEmptyOptions
 	}
+
 	auditLogDirMode, err := strconv.ParseInt(options.Opts, 8, 32)
 	if err != nil {
 		return err
@@ -580,8 +671,9 @@ func directiveSecAuditLogDirMode(options *DirectiveOptions) error {
 // ```
 func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 	if len(options.Opts) == 0 {
-		return errors.New("syntax error: SecAuditLogFileMode [0777/0700/...]")
+		return errEmptyOptions
 	}
+
 	auditLogFileMode, err := strconv.ParseInt(options.Opts, 8, 32)
 	if err != nil {
 		return err
@@ -615,6 +707,10 @@ func directiveSecAuditLogFileMode(options *DirectiveOptions) error {
 // and send rule matches to the audit log regardless of status. You must specify noauditlog in the
 // rules manually or set it in `SecDefaultAction`.
 func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.AuditLogRelevantStatus, err = regexp.Compile(options.Opts)
 	return err
@@ -659,6 +755,10 @@ func directiveSecAuditLogRelevantStatus(options *DirectiveOptions) error {
 // matched. The rules are fully qualified and will thus show inherited actions and default operators.
 // - Z: Final boundary, signifies the end of the entry (mandatory).
 func directiveSecAuditLogParts(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.AuditLogParts = types.AuditLogParts(options.Opts)
 	return nil
 }
@@ -690,13 +790,20 @@ func directiveSecAuditLogParts(options *DirectiveOptions) error {
 // SecAuditLogRelevantStatus ^(?:5|4(?!04))
 // ```
 func directiveSecAuditEngine(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	au, err := types.ParseAuditEngineStatus(options.Opts)
 	options.WAF.AuditEngine = au
 	return err
 }
 
 func directiveSecDataDir(options *DirectiveOptions) error {
-	// TODO validations
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	options.WAF.DataDir = options.Opts
 	return nil
 }
@@ -704,13 +811,17 @@ func directiveSecDataDir(options *DirectiveOptions) error {
 func directiveSecUploadKeepFiles(options *DirectiveOptions) error {
 	b, err := parseBoolean(options.Opts)
 	if err != nil {
-		return newDirectiveError(err, "SecUploadKeepFiles")
+		return err
 	}
 	options.WAF.UploadKeepFiles = b
 	return nil
 }
 
 func directiveSecUploadFileMode(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	fm, err := strconv.ParseInt(options.Opts, 8, 32)
 	if err != nil {
 		return err
@@ -720,12 +831,20 @@ func directiveSecUploadFileMode(options *DirectiveOptions) error {
 }
 
 func directiveSecUploadFileLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.UploadFileLimit, err = strconv.Atoi(options.Opts)
 	return err
 }
 
 func directiveSecUploadDir(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	// TODO validations
 	options.WAF.UploadDir = options.Opts
 	return nil
@@ -746,6 +865,10 @@ func directiveSecUploadDir(options *DirectiveOptions) error {
 // should be able to reduce it down to 128 KB or lower. Anything over the limit will be
 // rejected with status code 413 (Request Entity Too Large). There is a hard limit of 1 GB.
 func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	var err error
 	options.WAF.RequestBodyNoFilesLimit, err = strconv.ParseInt(options.Opts, 10, 64)
 	return err
@@ -757,6 +880,10 @@ func directiveSecRequestBodyNoFilesLimit(options *DirectiveOptions) error {
 // Logs will be written to this file. Make sure the process user has write access to the
 // directory.
 func directiveSecDebugLog(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	return options.WAF.SetDebugLogPath(options.Opts)
 }
 
@@ -806,7 +933,7 @@ func directiveSecRuleUpdateTargetByID(options *DirectiveOptions) error {
 func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
 	b, err := parseBoolean(options.Opts)
 	if err != nil {
-		return newDirectiveError(err, "SecIgnoreRuleCompilationErrors")
+		return err
 	}
 	if b {
 		options.WAF.Logger.Warn(`Coraza is running in Compatibility Mode (SecIgnoreRuleCompilationErrors On)
@@ -817,6 +944,10 @@ func directiveSecIgnoreRuleCompilationErrors(options *DirectiveOptions) error {
 }
 
 func directiveSecDataset(options *DirectiveOptions) error {
+	if len(options.Opts) == 0 {
+		return errEmptyOptions
+	}
+
 	name, d, ok := strings.Cut(options.Opts, " ")
 	if !ok {
 		return errors.New("syntax error: SecDataset name `\n...\n`")
@@ -837,14 +968,6 @@ func directiveSecDataset(options *DirectiveOptions) error {
 	return nil
 }
 
-func newCompileRuleError(err error, opts string) error {
-	return fmt.Errorf("failed to compile rule (%s): %s", err, opts)
-}
-
-func newDirectiveError(err error, directive string) error {
-	return fmt.Errorf("syntax error for directive %s: %s", directive, err)
-}
-
 func parseBoolean(data string) (bool, error) {
 	data = strings.ToLower(data)
 	switch data {
diff --git a/internal/seclang/parser.go b/internal/seclang/parser.go
index e521ffd..3a56213 100644
--- a/internal/seclang/parser.go
+++ b/internal/seclang/parser.go
@@ -140,6 +140,7 @@ func (p *Parser) evaluateLine(data string) error {
 	}
 	// first we get the directive
 	dir, opts, _ := strings.Cut(data, " ")
+
 	p.options.WAF.Logger.Debug("parsing directive %q", data)
 	directive := strings.ToLower(dir)
 
@@ -152,14 +153,14 @@ func (p *Parser) evaluateLine(data string) error {
 		// note a user might still include another file that includes the original file
 		// generating a DDOS attack
 		if p.includeCount >= maxIncludeRecursion {
-			return fmt.Errorf("cannot include more than %d files", maxIncludeRecursion)
+			return p.log(fmt.Sprintf("cannot include more than %d files", maxIncludeRecursion))
 		}
 		p.includeCount++
 		return p.FromFile(opts)
 	}
 	d, ok := directivesMap[directive]
 	if !ok || d == nil {
-		return p.log("Unknown directive " + directive)
+		return p.log(fmt.Sprintf("unknown directive %q", directive))
 	}
 
 	p.options.Opts = opts
@@ -173,7 +174,11 @@ func (p *Parser) evaluateLine(data string) error {
 	}
 	p.options.Config.Set("working_dir", wd)
 
-	return d(p.options)
+	if err := d(p.options); err != nil {
+		return fmt.Errorf("failed to compile the directive %q: %w", directive, err)
+	}
+
+	return nil
 }
 
 func (p *Parser) log(msg string) error {
diff --git a/types/waf.go b/types/waf.go
index 1b3c37f..98e7ebe 100644
--- a/types/waf.go
+++ b/types/waf.go
@@ -80,7 +80,7 @@ func ParseRuleEngineStatus(re string) (RuleEngineStatus, error) {
 	case "off":
 		return RuleEngineOff, nil
 	}
-	return -1, fmt.Errorf("invalid rule engine status: %s", re)
+	return -1, fmt.Errorf("invalid rule engine status: %q", re)
 }
 
 // String returns the string representation of the
@@ -88,11 +88,11 @@ func ParseRuleEngineStatus(re string) (RuleEngineStatus, error) {
 func (re RuleEngineStatus) String() string {
 	switch re {
 	case RuleEngineOn:
-		return "on"
+		return "On"
 	case RuleEngineDetectionOnly:
 		return "DetectionOnly"
 	case RuleEngineOff:
-		return "off"
+		return "Off"
 	}
 	return "unknown"
 }


Test patch:
diff --git a/internal/seclang/directives_test.go b/internal/seclang/directives_test.go
index 871cd43..129520e 100644
--- a/internal/seclang/directives_test.go
+++ b/internal/seclang/directives_test.go
@@ -34,110 +34,6 @@ func Test_NonImplementedDirective(t *testing.T) {
 	}
 }
 
-func Test_directive(t *testing.T) {
-	w := corazawaf.NewWAF()
-	p := NewParser(w)
-	if err := p.FromString("SecWebAppId test123"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.WebAppID != "test123" {
-		t.Error("failed to set SecWebAppId")
-	}
-	if err := p.FromString("SecUploadKeepFiles On"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if !w.UploadKeepFiles {
-		t.Error("failed to set SecUploadKeepFiles")
-	}
-	if err := p.FromString("SecUploadFileMode 0700"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecUploadFileLimit 1000"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.UploadFileLimit != 1000 {
-		t.Error("failed to set SecUploadFileLimit")
-	}
-	if err := p.FromString("SecUploadDir /tmp"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.UploadDir != "/tmp" {
-		t.Error("failed to set SecUploadDir")
-	}
-	if err := p.FromString("SecTmpDir /tmp"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.TmpDir != "/tmp" {
-		t.Error("failed to set SecTmpDir")
-	}
-	if err := p.FromString("SecSensorId test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.SensorID != "test" {
-		t.Error("failed to set SecSensorId")
-	}
-	if err := p.FromString("SecRuleEngine DetectionOnly"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if w.RuleEngine != types.RuleEngineDetectionOnly {
-		t.Errorf("failed to set SecRuleEngine, got %s and expected %s", w.RuleEngine.String(), types.RuleEngineDetectionOnly.String())
-	}
-	if err := p.FromString(`SecAction "id:1,tag:test"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveByTag test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveByTag")
-	}
-	if err := p.FromString(`SecAction "id:1,msg:'test'"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveByMsg test"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveByMsg")
-	}
-	if err := p.FromString(`SecAction "id:1"`); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if err := p.FromString("SecRuleRemoveById 1"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.Rules.Count() != 0 {
-		t.Error("Failed to remove rule with SecRuleRemoveById")
-	}
-	if err := p.FromString("SecResponseBodyMimeTypesClear"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if len(p.options.WAF.ResponseBodyMimeTypes) != 0 {
-		t.Error("failed to set SecResponseBodyMimeTypesClear")
-	}
-	if err := p.FromString("SecResponseBodyMimeType text/html"); err != nil {
-		t.Error("failed to set parser from string")
-	}
-	if p.options.WAF.ResponseBodyMimeTypes[0] != "text/html" {
-		t.Error("failed to set SecResponseBodyMimeType")
-	}
-	if err := p.FromString(`SecServerSignature "Microsoft-IIS/6.0"`); err != nil {
-		t.Error("failed to set directive: SecServerSignature")
-	}
-	if err := p.FromString(`SecRequestBodyInMemoryLimit 131072`); err != nil {
-		t.Error("failed to set directive: SecRequestBodyInMemoryLimit")
-	}
-	if err := p.FromString(`SecRemoteRulesFailAction Abort`); err != nil {
-		t.Error("failed to set directive: SecRemoteRulesFailAction")
-	}
-	if err := p.FromString(`SecRequestBodyLimitAction Reject`); err != nil {
-		t.Error("failed to set directive: SecRequestBodyLimitAction")
-	}
-	if err := p.FromString(`SecResponseBodyLimitAction ProcessPartial`); err != nil {
-		t.Error("failed to set directive: SecResponseBodyLimitAction")
-	}
-}
-
 func TestSecRuleUpdateTargetBy(t *testing.T) {
 	waf := corazawaf.NewWAF()
 	rule, err := ParseRule(RuleOptions{
@@ -213,18 +109,127 @@ func TestDirectives(t *testing.T) {
 		check func(*corazawaf.WAF) bool
 	}
 	directiveCases := map[string][]directiveCase{
+		"SecComponentSignature": {
+			{"", expectErrorOnDirective},
+			{"name", func(w *corazawaf.WAF) bool { return len(w.ComponentNames) == 1 }},
+		},
+		"SecMarker": {
+			{"", expectErrorOnDirective},
+			{"999", func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+			{"MY_TEXT", func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+		},
+		"SecWebAppId": {
+			{"", expectErrorOnDirective},
+			{"test123", func(w *corazawaf.WAF) bool { return w.WebAppID == "test123" }},
+		},
+		"SecUploadKeepFiles": {
+			{"", expectErrorOnDirective},
+			{"Ox", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.UploadKeepFiles }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.UploadKeepFiles }},
+		},
+		"SecUploadFileMode": {
+			{"", expectErrorOnDirective},
+			{"888", expectErrorOnDirective},
+			{"700", func(w *corazawaf.WAF) bool { return w.UploadFileMode == 0700 }},
+		},
+		"SecUploadFileLimit": {
+			{"", expectErrorOnDirective},
+			{"1000", func(w *corazawaf.WAF) bool { return w.UploadFileLimit == 1000 }},
+		},
+		"SecUploadDir": {
+			{"", expectErrorOnDirective},
+			{"/tmp", func(w *corazawaf.WAF) bool { return w.UploadDir == "/tmp" }},
+		},
+		"SecTmpDir": {
+			{"", expectErrorOnDirective},
+			{"/tmp", func(w *corazawaf.WAF) bool { return w.TmpDir == "/tmp" }},
+		},
+		"SecSensorId": {
+			{"", expectErrorOnDirective},
+			{"test", func(w *corazawaf.WAF) bool { return w.SensorID == "test" }},
+		},
+		"SecRuleEngine": {
+			{"What?", expectErrorOnDirective},
+			{"DetectionOnly", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineDetectionOnly }},
+			{"On", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineOn }},
+			{"Off", func(w *corazawaf.WAF) bool { return w.RuleEngine == types.RuleEngineOff }},
+		},
+		"SecAction": {
+			{"", expectErrorOnDirective},
+			{`"id:1,tag:test"`, func(w *corazawaf.WAF) bool { return w.Rules.Count() == 1 }},
+		},
+		"SecRuleRemoveByTag": {
+			{"", expectErrorOnDirective},
+		},
+		"SecRuleRemoveByMsg": {
+			{"", expectErrorOnDirective},
+		},
+		"SecRuleRemoveById": {
+			{"", expectErrorOnDirective},
+		},
+		"SecResponseBodyMimeTypesClear": {
+			{"", func(w *corazawaf.WAF) bool { return len(w.ResponseBodyMimeTypes) == 0 }},
+			{"x", expectErrorOnDirective},
+		},
+		"SecResponseBodyMimeType": {
+			{"", expectErrorOnDirective},
+			{"text/html", func(w *corazawaf.WAF) bool { return w.ResponseBodyMimeTypes[0] == "text/html" }},
+		},
+		"SecServerSignature": {
+			{"", expectErrorOnDirective},
+			{`"Microsoft-IIS/6.0"`, func(w *corazawaf.WAF) bool { return w.ServerSignature == "Microsoft-IIS/6.0" }},
+		},
 		"SecRequestBodyLimit": {
+			{"", expectErrorOnDirective},
 			{"x", expectErrorOnDirective},
 			{"123", func(w *corazawaf.WAF) bool { return w.RequestBodyLimit == 123 }},
 		},
 		"SecResponseBodyLimit": {
+			{"", expectErrorOnDirective},
 			{"y", expectErrorOnDirective},
 			{"123", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimit == 123 }},
 		},
 		"SecRequestBodyInMemoryLimit": {
+			{"", expectErrorOnDirective},
 			{"z", expectErrorOnDirective},
 			{"123", func(w *corazawaf.WAF) bool { return *(w.RequestBodyInMemoryLimit()) == 123 }},
 		},
+		"SecRequestBodyLimitAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Reject", func(w *corazawaf.WAF) bool { return w.RequestBodyLimitAction == types.BodyLimitActionReject }},
+			{"ProcessPartial", func(w *corazawaf.WAF) bool { return w.RequestBodyLimitAction == types.BodyLimitActionProcessPartial }},
+		},
+		"SecRequestBodyAccess": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.RequestBodyAccess }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.RequestBodyAccess }},
+		},
+		"SecResponseBodyLimitAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Reject", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimitAction == types.BodyLimitActionReject }},
+			{"ProcessPartial", func(w *corazawaf.WAF) bool { return w.ResponseBodyLimitAction == types.BodyLimitActionProcessPartial }},
+		},
+		"SecResponseBodyAccess": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"On", func(w *corazawaf.WAF) bool { return w.ResponseBodyAccess }},
+			{"Off", func(w *corazawaf.WAF) bool { return !w.ResponseBodyAccess }},
+		},
+		"SecRemoteRulesFailAction": {
+			{"", expectErrorOnDirective},
+			{"What?", expectErrorOnDirective},
+			{"Abort", func(w *corazawaf.WAF) bool { return w.AbortOnRemoteRulesFail }},
+		},
+		"SecDefaultAction": {
+			{"", expectErrorOnDirective},
+		},
+		"SecAuditLog": {
+			{"", expectErrorOnDirective},
+		},
 	}
 
 	for name, dCases := range directiveCases {
diff --git a/internal/seclang/parser_test.go b/internal/seclang/parser_test.go
index b10789a..c5000cc 100644
--- a/internal/seclang/parser_test.go
+++ b/internal/seclang/parser_test.go
@@ -22,7 +22,7 @@ func TestInterruption(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
 	if err := p.FromString(`SecAction "id:1,deny,log,phase:1"`); err != nil {
-		t.Error("Could not create from string")
+		t.Errorf("Could not create from string: %s", err.Error())
 	}
 	tx := waf.NewTransaction()
 	if tx.ProcessRequestHeaders() == nil {
@@ -39,6 +39,20 @@ func TestDirectivesCaseInsensitive(t *testing.T) {
 	}
 }
 
+func TestInvalidDirective(t *testing.T) {
+	waf := coraza.NewWAF()
+	p := NewParser(waf)
+	err := p.FromString("Unknown Rule")
+	if err == nil {
+		t.Error("expected error")
+	}
+
+	err = p.FromString("SecEngineRule")
+	if err == nil {
+		t.Error("expected error")
+	}
+}
+
 func TestCommentsWithBackticks(t *testing.T) {
 	waf := coraza.NewWAF()
 	p := NewParser(waf)
diff --git a/internal/seclang/rule_parser_test.go b/internal/seclang/rule_parser_test.go
index f2018bb..2445ea7 100644
--- a/internal/seclang/rule_parser_test.go
+++ b/internal/seclang/rule_parser_test.go
@@ -4,6 +4,7 @@
 package seclang
 
 import (
+	"errors"
 	"strings"
 	"testing"
 
@@ -87,8 +88,9 @@ func TestSecRuleInlineVariableNegation(t *testing.T) {
 	err = p.FromString(`
 		SecRule REQUEST_URI|!REQUEST_COOKIES: "abc" "id:9,phase:2"
 	`)
-	if !strings.Contains(err.Error(), "failed to compile rule") {
-		t.Errorf("Error should be failed to compile rule, got %s", err)
+	expectedErr := "failed to compile the directive"
+	if !strings.Contains(err.Error(), expectedErr) {
+		t.Errorf("unexpected error, want %q, got %q", expectedErr, err.Error())
 	}
 }
 
@@ -108,8 +110,9 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 		SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:8,phase:2"
 		SecRuleUpdateTargetById 8 "!REQUEST_HEADERS:"
 	`)
-	if err.Error() != "unknown variable" {
-		t.Errorf("Error should be unknown variable, got %s", err.Error())
+	expectedErr := errors.New("unknown variable")
+	if errors.Unwrap(err).Error() != expectedErr.Error() {
+		t.Fatalf("unexpexted error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())
 	}
 
 	// Try to update undefined rule
@@ -117,9 +120,9 @@ func TestSecRuleUpdateTargetVariableNegation(t *testing.T) {
 		SecRule REQUEST_URI|REQUEST_COOKIES "abc" "id:9,phase:2"
 		SecRuleUpdateTargetById 99 "!REQUEST_HEADERS:xyz"
 	`)
-	if err.Error() != "cannot create a variable exception for an undefined rule" {
-		t.Error("Error should be cannot create a variable exception for an undefined rule, got ",
-			err)
+	expectedErr = errors.New("cannot create a variable exception for an undefined rule")
+	if errors.Unwrap(err).Error() != expectedErr.Error() {
+		t.Fatalf("unexpected error, want %q, have %q", expectedErr, errors.Unwrap(err).Error())
 	}
 }