Add RuleFilter for dynamic rule skipping#1349
Conversation
|
Hello Coraza Contributors, We propose introducing the This addresses the need (highlighted in #799) for programmatic ways to disable rules based on runtime conditions without altering SecLang rules. The mechanism involves:
This offers an extensible hook for custom logic (e.g., tag-based filtering, conditional skips based on transaction context held by the filter) managed outside of rule files, decoupling complex skipping decisions from SecLang. We welcome your feedback. |
chore(deps): update github/codeql-action digest to 1b549b9 in .github…
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.
ef5a130 to
9637529
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1349 +/- ##
==========================================
+ Coverage 84.01% 84.02% +0.01%
==========================================
Files 170 170
Lines 9803 9811 +8
==========================================
+ Hits 8236 8244 +8
Misses 1323 1323
Partials 244 244
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| // 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 { |
There was a problem hiding this comment.
wonder if this should be an interface or a function given the single method.
There was a problem hiding this comment.
While a function type could work for the single ShouldIgnore method, I think keeping it as an interface provides more flexibility for implementers down the line.
For instance, implementations might need to hold configuration or state (like a list of rule IDs to ignore). Interfaces handle this cleanly via structs.
It also makes composition much easier by enabling standard Go patterns. Users could leverage the Composite pattern to combine multiple filters or the Decorator pattern to add functionality like logging/caching around a filter, all while adhering to the RuleFilter interface. This is cleaner than trying to achieve the same with higher-order functions.
Given it's in experimental, sticking with the more flexible interface seems like a good bet for future usability. What do you think?
|
Hi @azakharov-cloudlinux this is indeed great work. A couple of requests from my side:
|
|
Hi, thanks for the feedback! I have a few questions regarding how to best extend the Regarding placement: Is there a preferred file or location within the Regarding the example. Rather than providing a synthetic code example, I can describe a real-world scenario we've encountered with Coraza. We use 3d-party tool to identify underlying web technologies like WordPress or Joomla. In such cases, we've optimized Coraza's rule processing by selectively activating only the relevant rules based on the identified technology within the URL path. The following example illustrates new This approach enhances performance without need to reload/unload full ruleset. And it especially useful when working with big rule collections. Code examplepackage main
import (
"net/url"
"github.com/corazawaf/coraza/v3"
"github.com/corazawaf/coraza/v3/types"
)
// - - - - - - - - - -
// EnabledTagsRuleFilter applies rules only if they have at least one matching enabled tag.
type EnabledTagsRuleFilter struct {
enabledTags map[string]struct{}
}
func NewEnabledTagsRuleFilter(enabledTags []string) *EnabledTagsRuleFilter {
tagSet := make(map[string]struct{}, len(enabledTags))
for _, tag := range enabledTags {
if tag != "" {
tagSet[tag] = struct{}{}
}
}
return &EnabledTagsRuleFilter{enabledTags: tagSet}
}
func (f *EnabledTagsRuleFilter) ShouldIgnore(rule types.RuleMetadata) bool {
ruleTags := rule.Tags()
for _, tag := range ruleTags {
// Check if ANY tag from the rule exists in the 'enabledTags' map
if _, exists := f.enabledTags[tag]; exists {
// If a tag IS in the 'enabledTags', return 'false' (DO NOT ignore)
return false
}
}
// If we checked all tags and NONE were in the 'enabledTags', return 'true' (DO ignore)
return true
}
// - - - - - - - - - -
// Fallback RuleFilter implementation
type AlwaysApplyFilter struct{}
// ShouldIgnore always returns false, meaning no rule should be ignored
func (f *AlwaysApplyFilter) ShouldIgnore(rule types.RuleMetadata) bool {
return false
}
// - - - - - - - - - -
// AppSpecificEnabledRulesFilter holds the mapping from URL paths to specific tag filters.
type AppSpecificEnabledRulesFilter struct {
pathToRuleFilters map[string]*EnabledTagsRuleFilter
}
// GetRuleFilter returns the specific filter for the given URL path,
// or a default filter if no specific one is found.
func (m *AppSpecificEnabledRulesFilter) GetRuleFilter(url string) types.RuleFilter {
filter, exists := m.pathToRuleFilters[path]
if exists && filter != nil {
return filter
}
return &AlwaysApplyFilter{} // Fallback: apply all rules.
}
// - - - - - - - - - -
// Define the mapping data: URL path -> list of enabled ModSecurity tags
func CreateNewTagsRuleFilter() {
entries := []struct {
Path string
EnabledTags []string
}{
{
Path: "/wordpress",
EnabledTags: []string{"wordpress", "wp_theme_1"},
},
{
Path: "/joomla",
EnabledTags: []string{"joomla", "joomla_plugin_1"},
},
}
data := make(map[string]*EnabledTagsRuleFilter, len(entries))
for _, entry := range entries {
data[entry.Path] = NewEnabledTagsRuleFilter(entry.EnabledTags)
}
return &AppSpecificEnabledRulesFilter{pathToRuleFilters: data}
}
func main() {
conf := coraza.NewWAFConfig().
WithDirectives(`SecRuleEngine On`).
WithDirectivesFromFile("path/to/rules.conf")
waf, _ := coraza.NewWAF(conf)
// Create custom rule filter manager
filterManager := CreateNewTagsRuleFilter()
urlsToTest := []string{
"http://example.com/wordpress", // Will apply only rules with tags "wordpress" and "wp_theme_1"
"http://example.com/joomla", // Will apply only rules with tags "joomla" and "joomla_plugin_1"
"http://example.com/other/folder", // Will apply all rules
}
for _, rawURL := range urlsToTest {
parsedURL, _ := url.Parse(rawURL)
// Create a new Coraza transaction for the request
tx := waf.NewTransaction()
defer tx.Close()
// Get the appropriate rule filter based on the request path
ruleFilter := filterManager.GetRuleFilter(parsedURL.Path)
//Apply the specific filter to this transaction, this tells Coraza to use our ShouldIgnore logic for this tx
tx.UseRuleFilter(ruleFilter)
// Set transaction context
tx.ProcessURI(rawURL, "GET", "HTTP/1.1")
tx.AddRequestHeader("Host", parsedURL.Host)
// ...
}
} |
I think they all can go in a |
|
Hi @jcchavezs, I've moved the rule filtering functionality to the experimental package structure as requested. I also renamed the function for setting the rule filter to Looking forward to your feedback. |
|
Hey, @jcchavezs @M4tteoP |
Make sure that you've checked the boxes below before you submit PR:
Thanks for your contribution ❤️