Skip to content

Commit 9637529

Browse files
committed
Add RuleFilter for dynamic rule skipping
Introduces the `types.RuleFilter` interface to allow custom, per-transaction logic for skipping rules based on their metadata. Adds `Transaction.UseRuleFilter` to apply a filter instance and integrates the filter check at the beginning of the rule evaluation loop in `RuleGroup.Eval`. Includes associated unit tests.
1 parent 52fac2a commit 9637529

File tree

6 files changed

+129
-2
lines changed

6 files changed

+129
-2
lines changed

internal/corazawaf/rulegroup.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,16 @@ func (rg *RuleGroup) Eval(phase types.RulePhase, tx *Transaction) bool {
133133
RulesLoop:
134134
for i := range rg.rules {
135135
r := &rg.rules[i]
136+
// Check if a specific rule filter is applied to this transaction
137+
// and if the current rule should be ignored according to the filter.
138+
if tx.ruleFilter != nil {
139+
if tx.ruleFilter.ShouldIgnore(r) {
140+
tx.DebugLogger().Debug().
141+
Int("rule_id", r.ID_).
142+
Msg("Skipping rule due to RulesFilter")
143+
continue RulesLoop
144+
}
145+
}
136146
// if there is already an interruption and the phase isn't logging
137147
// we break the loop
138148
if tx.interruption != nil && phase != types.PhaseLogging {
@@ -159,8 +169,7 @@ RulesLoop:
159169
if trb == r.ID_ {
160170
tx.DebugLogger().Debug().
161171
Int("rule_id", r.ID_).
162-
Msg("Skipping rule")
163-
172+
Msg("Skipping rule due to ruleRemoveByID")
164173
continue RulesLoop
165174
}
166175
}

internal/corazawaf/rulegroup_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
package corazawaf
55

66
import (
7+
"fmt"
78
"testing"
89

910
"github.com/corazawaf/coraza/v3/experimental/plugins/macro"
11+
"github.com/corazawaf/coraza/v3/types"
1012
)
1113

1214
func newTestRule(id int) *Rule {
@@ -85,3 +87,93 @@ func TestRuleGroupDeleteByID(t *testing.T) {
8587
t.Fatal("Unexpected remaining rule in the rulegroup")
8688
}
8789
}
90+
91+
// RuleFilterWrapper provides a flexible way to define rule filtering logic for tests.
92+
type RuleFilterWrapper struct {
93+
shouldIgnore func(rule types.RuleMetadata) bool
94+
}
95+
96+
func (fw *RuleFilterWrapper) ShouldIgnore(rule types.RuleMetadata) bool {
97+
if fw.shouldIgnore == nil {
98+
return false // Default behavior: don't ignore if no function is provided
99+
}
100+
return fw.shouldIgnore(rule)
101+
}
102+
103+
// TestRuleFilterInteraction confirms filter is checked first in Eval loop for all phases.
104+
func TestRuleFilterInteraction(t *testing.T) {
105+
// --- Define Rule (Phase 0 to run in all phases) ---
106+
rule := NewRule()
107+
rule.ID_ = 1
108+
rule.Phase_ = 0 // Phase 0: Always evaluate
109+
rule.operator = nil // No operator means it always matches
110+
if err := rule.AddAction("deny", &dummyDenyAction{}); err != nil {
111+
t.Fatalf("Setup: Failed to add deny action: %v", err)
112+
}
113+
114+
// --- Phases to Test ---
115+
phasesToTest := []types.RulePhase{
116+
types.PhaseRequestHeaders,
117+
types.PhaseRequestBody,
118+
types.PhaseResponseHeaders,
119+
types.PhaseResponseBody,
120+
types.PhaseLogging,
121+
}
122+
123+
// --- Filter Actions ---
124+
filterActions := []struct {
125+
name string
126+
filterShouldIgnore bool
127+
expectInterruption bool // Expect interruption only if filter *allows* the deny rule
128+
}{
129+
{
130+
name: "Rule Filtered",
131+
filterShouldIgnore: true,
132+
expectInterruption: false,
133+
},
134+
{
135+
name: "Rule Allowed",
136+
filterShouldIgnore: false,
137+
expectInterruption: true,
138+
},
139+
}
140+
141+
// --- Iterate through Phases ---
142+
for _, currentPhase := range phasesToTest {
143+
phaseTestName := fmt.Sprintf("Phase_%d", currentPhase)
144+
145+
t.Run(phaseTestName, func(t *testing.T) {
146+
// --- Iterate through Filter Actions ---
147+
for _, fa := range filterActions {
148+
filterActionTestName := fa.name
149+
150+
t.Run(filterActionTestName, func(t *testing.T) {
151+
waf := NewWAF()
152+
if err := waf.Rules.Add(rule); err != nil {
153+
t.Fatalf("Setup: Failed to add rule for %s/%s: %v", phaseTestName, filterActionTestName, err)
154+
}
155+
tx := waf.NewTransaction()
156+
157+
var filterCalled bool
158+
testFilter := &RuleFilterWrapper{
159+
shouldIgnore: func(r types.RuleMetadata) bool {
160+
filterCalled = true
161+
return fa.filterShouldIgnore
162+
},
163+
}
164+
tx.UseRuleFilter(testFilter)
165+
166+
interrupted := waf.Rules.Eval(currentPhase, tx)
167+
if interrupted != fa.expectInterruption {
168+
t.Fatalf("[%s/%s] ShouldFilter is '%t', expecting interruption '%t', but Eval returned '%t'",
169+
phaseTestName, filterActionTestName, fa.filterShouldIgnore, fa.expectInterruption, interrupted,
170+
)
171+
}
172+
if !filterCalled {
173+
t.Fatalf("[%s/%s] ShouldIgnore was *not* called", phaseTestName, filterActionTestName)
174+
}
175+
})
176+
}
177+
})
178+
}
179+
}

internal/corazawaf/transaction.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ type Transaction struct {
123123
variables TransactionVariables
124124

125125
transformationCache map[transformationKey]*transformationValue
126+
127+
// ruleFilter allows applying custom rule filtering logic per transaction.
128+
// If set, it's used during rule evaluation to determine if a rule should be skipped.
129+
ruleFilter types.RuleFilter
126130
}
127131

128132
func (tx *Transaction) ID() string {
@@ -1598,6 +1602,13 @@ func (tx *Transaction) Close() error {
15981602
return fmt.Errorf("transaction close failed: %v", errors.Join(errs...))
15991603
}
16001604

1605+
// UseRuleFilter applies a RuleFilter to the transaction.
1606+
// This filter will be consulted during rule evaluation in each phase
1607+
// to determine if specific rules should be skipped for this transaction.
1608+
func (tx *Transaction) UseRuleFilter(filter types.RuleFilter) {
1609+
tx.ruleFilter = filter
1610+
}
1611+
16011612
// String will return a string with the transaction debug information
16021613
func (tx *Transaction) String() string {
16031614
res := strings.Builder{}

internal/corazawaf/waf.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ func (w *WAF) newTransaction(opts Options) *Transaction {
197197
tx.debugLogger = w.Logger.With(debuglog.Str("tx_id", tx.id))
198198
tx.Timestamp = time.Now().UnixNano()
199199
tx.audit = false
200+
tx.ruleFilter = nil
200201

201202
// Always non-nil if buffers / collections were already initialized so we don't do any of them
202203
// based on the presence of RequestBodyBuffer.

types/rules.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,12 @@ type RuleMetadata interface {
2020
Raw() string
2121
SecMark() string
2222
}
23+
24+
// RuleFilter provides an interface for filtering rules during transaction processing.
25+
// Implementations can define custom logic to determine if a specific rule
26+
// should be ignored for a given transaction based on its metadata.
27+
type RuleFilter interface {
28+
// ShouldIgnore evaluates the provided RuleMetadata and returns true if the rule
29+
// should be skipped for the current transaction, false otherwise.
30+
ShouldIgnore(RuleMetadata) bool
31+
}

types/transaction.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ type Transaction interface {
196196
// ID returns the transaction ID.
197197
ID() string
198198

199+
// UseRuleFilter applies a RuleFilter to the transaction.
200+
// This filter will be consulted during rule evaluation in each phase
201+
// to determine if specific rules should be skipped for this transaction.
202+
UseRuleFilter(RuleFilter)
203+
199204
// Closer closes the transaction and releases any resources associated with it such as request/response bodies.
200205
io.Closer
201206
}

0 commit comments

Comments
 (0)