Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions experimental/rulefilter/rftypes/rftypes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

// This package defines shared types for the rulefilter package.

package rftypes

import "github.com/corazawaf/coraza/v3/types"

// RuleFilter provides an interface for filtering rules during transaction processing.
// Implementations can define custom logic to determine if a specific rule
// should be ignored for a given transaction based on its metadata.
type RuleFilter interface {
// ShouldIgnore evaluates the provided RuleMetadata and returns true if the rule
// should be skipped for the current transaction, false otherwise.
ShouldIgnore(types.RuleMetadata) bool
}
28 changes: 28 additions & 0 deletions experimental/rulefilter/rulefilter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0

// This package provides experimental way to filter rule evaluation
// during transaction processing.

package rulefilter

import (
"fmt"

"github.com/corazawaf/coraza/v3/experimental/rulefilter/rftypes"
"github.com/corazawaf/coraza/v3/internal/corazawaf"
"github.com/corazawaf/coraza/v3/types"
)

// SetRuleFilter applies a RuleFilter to the transaction.
// This filter will be consulted during rule evaluation in each phase
// to determine if specific rules should be skipped for this transaction.
// It returns an error if the provided transaction is not of the expected internal type.
func SetRuleFilter(tx types.Transaction, filter rftypes.RuleFilter) error {
internalTx, ok := tx.(*corazawaf.Transaction)
if !ok {
return fmt.Errorf("transaction type assertion failed, expected *corazawaf.Transaction but got %T", tx)
}
internalTx.SetRuleFilter(filter)
return nil
}
76 changes: 76 additions & 0 deletions experimental/rulefilter/rulefilter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
// SPDX-License-Identifier: Apache-2.0
package rulefilter

import (
"testing"

"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/types"
)

// Simple implementation for testing.
type mockRuleFilter struct{}

func (m *mockRuleFilter) ShouldIgnore(types.RuleMetadata) bool {
return true
}

// Embed the interface to avoid implementing all methods initially.
// We only need this struct to *not* be a *corazawaf.Transaction.
type mockTransaction struct {
types.Transaction
}

func TestSetRuleFilter(t *testing.T) {
// Note: Verification that the filter *works* is covered by internal tests.
// This test specifically checks whether SetRuleFilter returns the expected error.

t.Run("set success", func(t *testing.T) {
conf := coraza.NewWAFConfig()
waf, err := coraza.NewWAF(conf)
if err != nil {
t.Fatalf("Failed to create WAF: %v", err)
}
tx := waf.NewTransaction()
if tx == nil {
t.Fatal("Expected non-nil transaction, but got nil")
}

filter := &mockRuleFilter{}

err = SetRuleFilter(tx, filter)
if err != nil {
t.Fatalf("Setting filter should succeed, but got error: %v", err)
}
})

t.Run("set success for nil", func(t *testing.T) {
conf := coraza.NewWAFConfig()
waf, err := coraza.NewWAF(conf)
if err != nil {
t.Fatalf("Failed to create WAF: %v", err)
}
tx := waf.NewTransaction()
if tx == nil {
t.Fatal("Expected non-nil transaction, but got nil")
}

// resetting the filter should not fail
err = SetRuleFilter(tx, nil)
if err != nil {
t.Fatalf("Setting nil filter should succeed, but got error: %v", err)
}
})

t.Run("fail wrong transaction type", func(t *testing.T) {
// Use our mockTransaction which fulfills the interface but isn't the internal type
mockTx := &mockTransaction{}
filter := &mockRuleFilter{}

err := SetRuleFilter(mockTx, filter)
if err == nil {
t.Fatal("Setting filter on incorrect tx type should fail")
}
})
}
13 changes: 11 additions & 2 deletions internal/corazawaf/rulegroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ func (rg *RuleGroup) Eval(phase types.RulePhase, tx *Transaction) bool {
RulesLoop:
for i := range rg.rules {
r := &rg.rules[i]
// Check if a specific rule filter is applied to this transaction
// and if the current rule should be ignored according to the filter.
if tx.ruleFilter != nil {
if tx.ruleFilter.ShouldIgnore(r) {
tx.DebugLogger().Debug().
Int("rule_id", r.ID_).
Msg("Skipping rule due to RulesFilter")
continue RulesLoop
}
}
// if there is already an interruption and the phase isn't logging
// we break the loop
if tx.interruption != nil && phase != types.PhaseLogging {
Expand All @@ -159,8 +169,7 @@ RulesLoop:
if trb == r.ID_ {
tx.DebugLogger().Debug().
Int("rule_id", r.ID_).
Msg("Skipping rule")

Msg("Skipping rule due to ruleRemoveByID")
continue RulesLoop
}
}
Expand Down
92 changes: 92 additions & 0 deletions internal/corazawaf/rulegroup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
package corazawaf

import (
"fmt"
"testing"

"github.com/corazawaf/coraza/v3/experimental/plugins/macro"
"github.com/corazawaf/coraza/v3/types"
)

func newTestRule(id int) *Rule {
Expand Down Expand Up @@ -85,3 +87,93 @@ func TestRuleGroupDeleteByID(t *testing.T) {
t.Fatal("Unexpected remaining rule in the rulegroup")
}
}

// RuleFilterWrapper provides a flexible way to define rule filtering logic for tests.
type RuleFilterWrapper struct {
shouldIgnore func(rule types.RuleMetadata) bool
}

func (fw *RuleFilterWrapper) ShouldIgnore(rule types.RuleMetadata) bool {
if fw.shouldIgnore == nil {
return false // Default behavior: don't ignore if no function is provided
}
return fw.shouldIgnore(rule)
}

// TestRuleFilterInteraction confirms filter is checked first in Eval loop for all phases.
func TestRuleFilterInteraction(t *testing.T) {
// --- Define Rule (Phase 0 to run in all phases) ---
rule := NewRule()
rule.ID_ = 1
rule.Phase_ = 0 // Phase 0: Always evaluate
rule.operator = nil // No operator means it always matches
if err := rule.AddAction("deny", &dummyDenyAction{}); err != nil {
t.Fatalf("Setup: Failed to add deny action: %v", err)
}

// --- Phases to Test ---
phasesToTest := []types.RulePhase{
types.PhaseRequestHeaders,
types.PhaseRequestBody,
types.PhaseResponseHeaders,
types.PhaseResponseBody,
types.PhaseLogging,
}

// --- Filter Actions ---
filterActions := []struct {
name string
filterShouldIgnore bool
expectInterruption bool // Expect interruption only if filter *allows* the deny rule
}{
{
name: "Rule Filtered",
filterShouldIgnore: true,
expectInterruption: false,
},
{
name: "Rule Allowed",
filterShouldIgnore: false,
expectInterruption: true,
},
}

// --- Iterate through Phases ---
for _, currentPhase := range phasesToTest {
phaseTestName := fmt.Sprintf("Phase_%d", currentPhase)

t.Run(phaseTestName, func(t *testing.T) {
// --- Iterate through Filter Actions ---
for _, fa := range filterActions {
filterActionTestName := fa.name

t.Run(filterActionTestName, func(t *testing.T) {
waf := NewWAF()
if err := waf.Rules.Add(rule); err != nil {
t.Fatalf("Setup: Failed to add rule for %s/%s: %v", phaseTestName, filterActionTestName, err)
}
tx := waf.NewTransaction()

var filterCalled bool
testFilter := &RuleFilterWrapper{
shouldIgnore: func(r types.RuleMetadata) bool {
filterCalled = true
return fa.filterShouldIgnore
},
}
tx.SetRuleFilter(testFilter)

interrupted := waf.Rules.Eval(currentPhase, tx)
if interrupted != fa.expectInterruption {
t.Fatalf("[%s/%s] ShouldFilter is '%t', expecting interruption '%t', but Eval returned '%t'",
phaseTestName, filterActionTestName, fa.filterShouldIgnore, fa.expectInterruption, interrupted,
)
}
if !filterCalled {
t.Fatalf("[%s/%s] ShouldIgnore was *not* called", phaseTestName, filterActionTestName)
}
})
}
})
}
}
12 changes: 12 additions & 0 deletions internal/corazawaf/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"github.com/corazawaf/coraza/v3/collection"
"github.com/corazawaf/coraza/v3/debuglog"
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
"github.com/corazawaf/coraza/v3/experimental/rulefilter/rftypes"
"github.com/corazawaf/coraza/v3/internal/auditlog"
"github.com/corazawaf/coraza/v3/internal/bodyprocessors"
"github.com/corazawaf/coraza/v3/internal/collections"
Expand Down Expand Up @@ -123,6 +124,10 @@ type Transaction struct {
variables TransactionVariables

transformationCache map[transformationKey]*transformationValue

// ruleFilter allows applying custom rule filtering logic per transaction.
// If set, it's used during rule evaluation to determine if a rule should be skipped.
ruleFilter rftypes.RuleFilter
}

func (tx *Transaction) ID() string {
Expand Down Expand Up @@ -1598,6 +1603,13 @@ func (tx *Transaction) Close() error {
return fmt.Errorf("transaction close failed: %v", errors.Join(errs...))
}

// SetRuleFilter applies a RuleFilter to the transaction.
// This filter will be consulted during rule evaluation in each phase
// to determine if specific rules should be skipped for this transaction.
func (tx *Transaction) SetRuleFilter(filter rftypes.RuleFilter) {
tx.ruleFilter = filter
}

// String will return a string with the transaction debug information
func (tx *Transaction) String() string {
res := strings.Builder{}
Expand Down
1 change: 1 addition & 0 deletions internal/corazawaf/waf.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (w *WAF) newTransaction(opts Options) *Transaction {
tx.debugLogger = w.Logger.With(debuglog.Str("tx_id", tx.id))
tx.Timestamp = time.Now().UnixNano()
tx.audit = false
tx.ruleFilter = nil

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