Skip to content

Commit 7e054cf

Browse files
1991-mirectty2dokzlo13azakharov-cloudlinuxcl-vkuznetsov
authored
Beta (#14)
* add tests for the expected behavior * make tests more precise * clean up deafult config * move messages to H part, separate error logs and audit logs * 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. * Moved rulefilter to experimental package * Not using stretchr/testify for tests * Fixed behavior of JSON log when H section enabled * Added code comment * DEF-33873: Implement persistant storage Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-33873: fix based on comments Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-33873: adding variables Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-33873: apply comment changes Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * Clean up Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * Add forgotten arg Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-33873: Add tests and make final fixes Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-35140: Fix find string resolvement (#11) * test log Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * fix find string for persistence Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> --------- Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> * DEF-33864 SCRIPT_FILENAME & SCRIPT_USERNAME variables implementation * DEF-35104: investigation: redirect support --------- Signed-off-by: Miroslav Kovac <mkovac@cloudlinux.com> Co-authored-by: Roman Suvorov <tty2.rs@gmail.com> Co-authored-by: Aleksei Zakharov <dokzlo13@gmail.com> Co-authored-by: azakharov-cloudlinux <azakharov@cloudlinux.com> Co-authored-by: Vadim Kuznetsov <vkuznetsov@cloudlinux.com> Co-authored-by: bliss-cloudlinux <bliss@cloudlnux.com> Co-authored-by: bliss-cloudlinux <bliss@cloudlinux.com>
1 parent 52fac2a commit 7e054cf

40 files changed

+1329
-153
lines changed

collection/collection.go

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,24 +43,46 @@ type Keyed interface {
4343
FindString(key string) []types.MatchData
4444
}
4545

46+
type Editable interface {
47+
Keyed
48+
49+
// Remove deletes the key from the CollectionMap
50+
Remove(key string)
51+
52+
// Set will replace the key's value with this slice
53+
Set(key string, values []string)
54+
55+
// TODO: in v4 this should contain setters for Map and Persistence
56+
}
57+
4658
// Map are used to store VARIABLE data
4759
// for transactions, this data structured is designed
4860
// to store slices of data for keys
4961
// Important: CollectionMaps ARE NOT concurrent safe
5062
type Map interface {
51-
Keyed
63+
Editable
5264

5365
// Add a value to some key
5466
Add(key string, value string)
5567

56-
// Set will replace the key's value with this slice
57-
Set(key string, values []string)
58-
5968
// SetIndex will place the value under the index
6069
// If the index is higher than the current size of the CollectionMap
6170
// it will be appended
6271
SetIndex(key string, index int, value string)
72+
}
6373

64-
// Remove deletes the key from the CollectionMap
65-
Remove(key string)
74+
type Persistent interface {
75+
Editable
76+
77+
// Init the input as the collection key
78+
Init(key string)
79+
80+
// Sum will add the value to the key
81+
Sum(key string, sum int)
82+
83+
// SetOne will replace the key's value with this string
84+
SetOne(key string, value string)
85+
86+
// SetTTL will set the TTL for the key
87+
SetTTL(key string, ttl int)
6688
}

config.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package coraza
55

66
import (
7+
"github.com/corazawaf/coraza/v3/experimental/persistence/ptypes"
78
"io/fs"
89

910
"github.com/corazawaf/coraza/v3/debuglog"
@@ -94,17 +95,18 @@ type wafRule struct {
9495
// int is a signed integer type that is at least 32 bits in size (platform-dependent size).
9596
// We still basically assume 64-bit usage where int are big sizes.
9697
type wafConfig struct {
97-
rules []wafRule
98-
auditLog *auditLogConfig
99-
requestBodyAccess bool
100-
requestBodyLimit *int
101-
requestBodyInMemoryLimit *int
102-
responseBodyAccess bool
103-
responseBodyLimit *int
104-
responseBodyMimeTypes []string
105-
debugLogger debuglog.Logger
106-
errorCallback func(rule types.MatchedRule)
107-
fsRoot fs.FS
98+
rules []wafRule
99+
auditLog *auditLogConfig
100+
requestBodyAccess bool
101+
requestBodyLimit *int
102+
requestBodyInMemoryLimit *int
103+
responseBodyAccess bool
104+
responseBodyLimit *int
105+
responseBodyMimeTypes []string
106+
debugLogger debuglog.Logger
107+
errorCallback func(rule types.MatchedRule)
108+
fsRoot fs.FS
109+
persistenceEngineProvider ptypes.PersistenceEngineProvider
108110
}
109111

110112
func (c *wafConfig) WithRules(rules ...*corazawaf.Rule) WAFConfig {
@@ -193,6 +195,12 @@ func (c *wafConfig) WithResponseBodyMimeTypes(mimeTypes []string) WAFConfig {
193195
return ret
194196
}
195197

198+
func (c *wafConfig) WithPersistenceEngineProvider(provider ptypes.PersistenceEngineProvider) WAFConfig {
199+
ret := c.clone()
200+
ret.persistenceEngineProvider = provider
201+
return ret
202+
}
203+
196204
type auditLogConfig struct {
197205
relevantOnly bool
198206
parts types.AuditLogParts
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package persistence
2+
3+
import (
4+
"fmt"
5+
"github.com/corazawaf/coraza/v3"
6+
"github.com/corazawaf/coraza/v3/experimental/persistence/ptypes"
7+
)
8+
9+
// SetEngine returns a **new** WAFConfig instance with the specified persistence engine configured.
10+
// The provided engine will be initialized using its Init method.
11+
// If the provided engine is nil, the resulting config will effectively disable persistence
12+
// NOTE: This function and the persistence feature are experimental and subject to change.
13+
func SetEngine(config coraza.WAFConfig, engineProvider ptypes.PersistenceEngineProvider) (coraza.WAFConfig, error) {
14+
cfgImpl, ok := config.(interface {
15+
WithPersistenceEngineProvider(provider ptypes.PersistenceEngineProvider) coraza.WAFConfig
16+
})
17+
if !ok {
18+
return nil, fmt.Errorf("unsupported WAFConfig type %T, cannot clone or set engine", config)
19+
}
20+
return cfgImpl.WithPersistenceEngineProvider(engineProvider), nil
21+
}
22+
23+
// ClosePersistentEngine provides a way to gracefully shut down the persistence engine
24+
// associated with a WAF instance. Since the WAF instance manages the engine's lifecycle,
25+
// this helper should be called when the WAF instance is no longer needed, for example,
26+
// during an application shutdown or a configuration reload.
27+
//
28+
// This allows the engine to release resources, such as database connections or
29+
// background garbage collection goroutines.
30+
//
31+
// NOTE: This function and the persistence feature are experimental and subject to change.
32+
func ClosePersistentEngine(waf coraza.WAF) error {
33+
wafImpl, ok := waf.(interface {
34+
ClosePersistentEngine() error
35+
})
36+
if !ok {
37+
return fmt.Errorf("unsupported WAF type %T, cannot close engine", waf)
38+
}
39+
return wafImpl.ClosePersistentEngine()
40+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ptypes
2+
3+
// PersistenceEngineProvider provider for creation of PersistentEngine.
4+
// This way we don't have to worry about cloning when using
5+
// WithPersistenceEngineProvider in config.go
6+
type PersistenceEngineProvider func() (PersistentEngine, error)
7+
8+
// PersistentEngine defines the interface for storing and retrieving persistent collection data (e.g., SESSION, IP, GLOBAL).
9+
type PersistentEngine interface {
10+
// Close releases engine resources.
11+
Close() error
12+
// Sum increments or decrements a numeric value.
13+
Sum(collectionName string, collectionKey string, key string, delta int) error
14+
// Get retrieves a specific value.
15+
Get(collectionName string, collectionKey string, key string) (string, error)
16+
// All retrieves all key-value pairs for a collection key.
17+
All(collectionName string, collectionKey string) (map[string]string, error)
18+
// Set stores a value, overwriting any existing one.
19+
Set(collection string, collectionKey string, key string, value string) error
20+
// SetTTL sets the Time-To-Live (in seconds) for a specific key within a collection instance.
21+
SetTTL(collection string, collectionKey string, key string, ttl int) error
22+
// Remove deletes a specific key.
23+
Remove(collection string, collectionKey string, key string) error
24+
}

experimental/plugins/plugintypes/transaction.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,11 @@ type TransactionVariables interface {
116116
ArgsGetNames() collection.Collection
117117
ArgsPostNames() collection.Collection
118118
MultipartStrictError() collection.Single
119+
ScriptFilename() collection.Single
120+
ScriptUsername() collection.Single
121+
Session() collection.Persistent
122+
User() collection.Persistent
123+
IP() collection.Persistent
124+
Global() collection.Persistent
125+
Resource() collection.Persistent
119126
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// This package defines shared types for the rulefilter package.
5+
6+
package rftypes
7+
8+
import "github.com/corazawaf/coraza/v3/types"
9+
10+
// RuleFilter provides an interface for filtering rules during transaction processing.
11+
// Implementations can define custom logic to determine if a specific rule
12+
// should be ignored for a given transaction based on its metadata.
13+
type RuleFilter interface {
14+
// ShouldIgnore evaluates the provided RuleMetadata and returns true if the rule
15+
// should be skipped for the current transaction, false otherwise.
16+
ShouldIgnore(types.RuleMetadata) bool
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// This package provides experimental way to filter rule evaluation
5+
// during transaction processing.
6+
7+
package rulefilter
8+
9+
import (
10+
"fmt"
11+
12+
"github.com/corazawaf/coraza/v3/experimental/rulefilter/rftypes"
13+
"github.com/corazawaf/coraza/v3/internal/corazawaf"
14+
"github.com/corazawaf/coraza/v3/types"
15+
)
16+
17+
// SetRuleFilter applies a RuleFilter to the transaction.
18+
// This filter will be consulted during rule evaluation in each phase
19+
// to determine if specific rules should be skipped for this transaction.
20+
// It returns an error if the provided transaction is not of the expected internal type.
21+
func SetRuleFilter(tx types.Transaction, filter rftypes.RuleFilter) error {
22+
internalTx, ok := tx.(*corazawaf.Transaction)
23+
if !ok {
24+
return fmt.Errorf("transaction type assertion failed, expected *corazawaf.Transaction but got %T", tx)
25+
}
26+
internalTx.SetRuleFilter(filter)
27+
return nil
28+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2024 Juan Pablo Tosso and the OWASP Coraza contributors
2+
// SPDX-License-Identifier: Apache-2.0
3+
package rulefilter
4+
5+
import (
6+
"testing"
7+
8+
"github.com/corazawaf/coraza/v3"
9+
"github.com/corazawaf/coraza/v3/types"
10+
)
11+
12+
// Simple implementation for testing.
13+
type mockRuleFilter struct{}
14+
15+
func (m *mockRuleFilter) ShouldIgnore(types.RuleMetadata) bool {
16+
return true
17+
}
18+
19+
// Embed the interface to avoid implementing all methods initially.
20+
// We only need this struct to *not* be a *corazawaf.Transaction.
21+
type mockTransaction struct {
22+
types.Transaction
23+
}
24+
25+
func TestSetRuleFilter(t *testing.T) {
26+
// Note: Verification that the filter *works* is covered by internal tests.
27+
// This test specifically checks whether SetRuleFilter returns the expected error.
28+
29+
t.Run("set success", func(t *testing.T) {
30+
conf := coraza.NewWAFConfig()
31+
waf, err := coraza.NewWAF(conf)
32+
if err != nil {
33+
t.Fatalf("Failed to create WAF: %v", err)
34+
}
35+
tx := waf.NewTransaction()
36+
if tx == nil {
37+
t.Fatal("Expected non-nil transaction, but got nil")
38+
}
39+
40+
filter := &mockRuleFilter{}
41+
42+
err = SetRuleFilter(tx, filter)
43+
if err != nil {
44+
t.Fatalf("Setting filter should succeed, but got error: %v", err)
45+
}
46+
})
47+
48+
t.Run("set success for nil", func(t *testing.T) {
49+
conf := coraza.NewWAFConfig()
50+
waf, err := coraza.NewWAF(conf)
51+
if err != nil {
52+
t.Fatalf("Failed to create WAF: %v", err)
53+
}
54+
tx := waf.NewTransaction()
55+
if tx == nil {
56+
t.Fatal("Expected non-nil transaction, but got nil")
57+
}
58+
59+
// resetting the filter should not fail
60+
err = SetRuleFilter(tx, nil)
61+
if err != nil {
62+
t.Fatalf("Setting nil filter should succeed, but got error: %v", err)
63+
}
64+
})
65+
66+
t.Run("fail wrong transaction type", func(t *testing.T) {
67+
// Use our mockTransaction which fulfills the interface but isn't the internal type
68+
mockTx := &mockTransaction{}
69+
filter := &mockRuleFilter{}
70+
71+
err := SetRuleFilter(mockTx, filter)
72+
if err == nil {
73+
t.Fatal("Setting filter on incorrect tx type should fail")
74+
}
75+
})
76+
}

internal/actions/expirevar.go

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@
44
package actions
55

66
import (
7+
"errors"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/corazawaf/coraza/v3/collection"
12+
"github.com/corazawaf/coraza/v3/experimental/plugins/macro"
713
"github.com/corazawaf/coraza/v3/experimental/plugins/plugintypes"
14+
utils "github.com/corazawaf/coraza/v3/internal/strings"
15+
"github.com/corazawaf/coraza/v3/types/variables"
816
)
917

1018
// Action Group: Non-disruptive
@@ -23,15 +31,66 @@ import (
2331
// setvar:session.suspicious=1,expirevar:session.suspicious=3600,phase:1"
2432
//
2533
// ```
26-
type expirevarFn struct{}
34+
35+
type expirevarFn struct {
36+
key macro.Macro
37+
ttl int
38+
collection variables.RuleVariable
39+
}
2740

2841
func (a *expirevarFn) Init(_ plugintypes.RuleMetadata, data string) error {
42+
if len(data) == 0 {
43+
return ErrMissingArguments
44+
}
45+
46+
// Split the input "variable=ttl" (e.g., "ip.request_count=60")
47+
key, ttlStr, ttlOk := strings.Cut(data, "=")
48+
colKey, colVal, colOk := strings.Cut(key, ".")
49+
50+
// Ensure the collection is one of the editable ones
51+
if !utils.InSlice(strings.ToUpper(colKey), supportedColKeys) {
52+
return errors.New("invalid collection, supported collections are: " + strings.Join(supportedColKeys, ", "))
53+
}
54+
if strings.TrimSpace(colVal) == "" {
55+
return ErrInvalidKVArguments
56+
}
57+
58+
// Parse the collection and the variable name
59+
var err error
60+
a.collection, err = variables.Parse(colKey)
61+
if err != nil {
62+
return err
63+
}
64+
if colOk {
65+
a.key, err = macro.NewMacro(colVal)
66+
if err != nil {
67+
return err
68+
}
69+
}
70+
71+
// Parse the TTL value
72+
if !ttlOk {
73+
return errors.New("missing TTL value")
74+
}
75+
ttlSeconds, err := strconv.Atoi(strings.TrimSpace(ttlStr))
76+
if err != nil || ttlSeconds <= 0 {
77+
return errors.New("invalid TTL, must be a positive integer")
78+
}
79+
a.ttl = ttlSeconds
2980
return nil
3081
}
3182

3283
func (a *expirevarFn) Evaluate(r plugintypes.RuleMetadata, tx plugintypes.TransactionState) {
33-
// Not supported
34-
tx.DebugLogger().Warn().Int("rule_id", r.ID()).Msg("Expirevar was used but it's not supported")
84+
// TODO: TX support
85+
// It has collection.Map interface and will not be converted to collection.Persistent
86+
col, ok := tx.Collection(a.collection).(collection.Persistent)
87+
if !ok {
88+
tx.DebugLogger().Error().Msg("collection in expirevar is not editable")
89+
return
90+
}
91+
// update the TTL
92+
key := a.key.Expand(tx)
93+
col.SetTTL(key, a.ttl)
3594
}
3695

3796
func (a *expirevarFn) Type() plugintypes.ActionType {

0 commit comments

Comments
 (0)