Skip to content

Add RuleFilter for dynamic rule skipping#1349

Open
azakharov-cloudlinux wants to merge 5 commits into
corazawaf:mainfrom
cloudlinux:feature/transaction-rulefilter
Open

Add RuleFilter for dynamic rule skipping#1349
azakharov-cloudlinux wants to merge 5 commits into
corazawaf:mainfrom
cloudlinux:feature/transaction-rulefilter

Conversation

@azakharov-cloudlinux

Copy link
Copy Markdown

Thank you for contributing to Coraza WAF, your effort is greatly appreciated
Before submitting check if what you want to add to coraza list meets quality standards before sending pull request. Thanks!

Make sure that you've checked the boxes below before you submit PR:

Thanks for your contribution ❤️

@azakharov-cloudlinux azakharov-cloudlinux requested a review from a team as a code owner April 9, 2025 14:24
@azakharov-cloudlinux

Copy link
Copy Markdown
Author

Hello Coraza Contributors,

We propose introducing the types.RuleFilter interface and Transaction.UseRuleFilter method to enable dynamic, per-transaction rule skipping.

This addresses the need (highlighted in #799) for programmatic ways to disable rules based on runtime conditions without altering SecLang rules.

The mechanism involves:

  1. Implementations of RuleFilter provide a ShouldIgnore(ruleMetadata) method.
  2. A filter instance is attached to a Transaction via UseRuleFilter.
  3. RuleGroup.Eval checks filter.ShouldIgnore early in the rule loop, skipping the rule if it returns true.

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.

tty2 and others added 2 commits April 10, 2025 08:39
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.
@azakharov-cloudlinux azakharov-cloudlinux force-pushed the feature/transaction-rulefilter branch from ef5a130 to 9637529 Compare April 10, 2025 08:52
@codecov

codecov Bot commented Apr 14, 2025

Copy link
Copy Markdown

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 84.02%. Comparing base (ad40dcb) to head (9637529).

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              
Flag Coverage Δ
coraza.rule.case_sensitive_args_keys 83.98% <100.00%> (+0.01%) ⬆️
coraza.rule.multiphase_evaluation 83.68% <100.00%> (+0.01%) ⬆️
coraza.rule.no_regex_multiline 83.96% <100.00%> (+0.01%) ⬆️
default 84.02% <100.00%> (+0.01%) ⬆️
examples+ 16.49% <25.00%> (-0.01%) ⬇️
examples+coraza.rule.case_sensitive_args_keys 83.98% <100.00%> (+0.01%) ⬆️
examples+coraza.rule.multiphase_evaluation 83.52% <100.00%> (+0.01%) ⬆️
examples+coraza.rule.no_regex_multiline 83.88% <100.00%> (+0.01%) ⬆️
examples+memoize_builders 83.99% <100.00%> (+0.01%) ⬆️
examples+no_fs_access 81.25% <100.00%> (+0.01%) ⬆️
ftw 84.02% <100.00%> (+0.01%) ⬆️
memoize_builders 84.13% <100.00%> (+0.01%) ⬆️
no_fs_access 83.52% <100.00%> (+0.01%) ⬆️
tinygo 84.00% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread types/rules.go Outdated
// 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 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder if this should be an interface or a function given the single method.

@azakharov-cloudlinux azakharov-cloudlinux Apr 25, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@jcchavezs

Copy link
Copy Markdown
Member

Hi @azakharov-cloudlinux this is indeed great work. A couple of requests from my side:

  1. The types must be exposed through the experimental package. Create a function that accepts a transaction and internally do the casting to not to change the interface.
  2. experimental package guarantees we can change the types later without disruptions.
    Could you also show some code that uses this feature?

@azakharov-cloudlinux

azakharov-cloudlinux commented Apr 25, 2025

Copy link
Copy Markdown
Author

Hi, thanks for the feedback!

I have a few questions regarding how to best extend the experimental package:

Regarding placement: Is there a preferred file or location within the experimental package to define the new RuleFilter interface (for example, creating a new types.go, rulefilter.go, or adding it to waf.go)? Similarly, where would be the best place to introduce the new SetRuleFilter(types.Transaction, experimental.RuleFilter) error function?


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 RuleFilter interface to implement this kind of logic. It dynamically associates URL paths with specific rule subsets based on tags. For example, a request to /wordpress triggers only rules tagged as wordpress or wp_theme_1, whereas other URL paths will use the full ruleset by default.

This approach enhances performance without need to reload/unload full ruleset. And it especially useful when working with big rule collections.

Code example
package 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)
  	// ...
  }
}

@jcchavezs

Copy link
Copy Markdown
Member

Regarding placement: Is there a preferred file or location within the experimental package to define the new RuleFilter interface (for example, creating a new types.go, rulefilter.go, or adding it to waf.go)? Similarly, where would be the best place to introduce the new SetRuleFilter(types.Transaction, experimental.RuleFilter) error function?

I think they all can go in a rulefilter package within the experimental package.

@jcchavezs jcchavezs requested a review from M4tteoP April 25, 2025 21:58
@azakharov-cloudlinux

azakharov-cloudlinux commented Apr 28, 2025

Copy link
Copy Markdown
Author

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 SetRuleFilter (from the original UseRuleFilter) to better adhere to common Go naming conventions.

Looking forward to your feedback.

@azakharov-cloudlinux

azakharov-cloudlinux commented May 7, 2025

Copy link
Copy Markdown
Author

Hey, @jcchavezs @M4tteoP
Just checking in to see if you've had a chance to look at this. Let me know if there's anything else needed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants