Skip to content

Add optional rule observer callback to WAF config#1478

Open
heaven wants to merge 3 commits intocorazawaf:mainfrom
heaven:main
Open

Add optional rule observer callback to WAF config#1478
heaven wants to merge 3 commits intocorazawaf:mainfrom
heaven:main

Conversation

@heaven
Copy link

@heaven heaven commented Jan 17, 2026

Introduce an optional rule observer callback that invokes once for each rule successfully added to the WAF during initialization.

The observer receives rule metadata via the existing RuleMetadata interface, similar to the error callback. When unset, there is no behavioral or performance footprint.

Reasoning

This change allows observing and inspecting the exact rule set Coraza uses internally, for example, giving the ability to represent those in the UI, eliminating the need to maintain a separate dataset, and keeping the source of truth unified.

The observer is intended for logging, metrics and inspection.

@heaven heaven requested a review from a team as a code owner January 17, 2026 08:48
@fzipi fzipi requested a review from Copilot January 17, 2026 14:27
@codecov
Copy link

codecov bot commented Jan 17, 2026

Codecov Report

❌ Patch coverage is 79.16667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.26%. Comparing base (da2d6f3) to head (8694ff8).

Files with missing lines Patch % Lines
experimental/rule_observer.go 64.28% 3 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1478      +/-   ##
==========================================
- Coverage   85.28%   85.26%   -0.02%     
==========================================
  Files         174      175       +1     
  Lines        8448     8472      +24     
==========================================
+ Hits         7205     7224      +19     
- Misses        994      997       +3     
- Partials      249      251       +2     
Flag Coverage Δ
coraza.rule.case_sensitive_args_keys 85.23% <79.16%> (-0.02%) ⬇️
coraza.rule.mandatory_rule_id_check 85.25% <79.16%> (-0.02%) ⬇️
coraza.rule.multiphase_evaluation 84.98% <79.16%> (-0.02%) ⬇️
coraza.rule.no_regex_multiline 85.25% <79.16%> (-0.02%) ⬇️
default 85.26% <79.16%> (-0.02%) ⬇️
examples+ 17.14% <79.16%> (+0.68%) ⬆️
examples+coraza.rule.case_sensitive_args_keys 85.16% <79.16%> (-0.02%) ⬇️
examples+coraza.rule.mandatory_rule_id_check 85.25% <79.16%> (-0.02%) ⬇️
examples+coraza.rule.multiphase_evaluation 84.78% <79.16%> (-0.02%) ⬇️
examples+coraza.rule.no_regex_multiline 85.10% <79.16%> (-0.02%) ⬇️
examples+memoize_builders 85.21% <79.16%> (-0.02%) ⬇️
examples+no_fs_access 82.86% <79.16%> (-0.02%) ⬇️
ftw 85.26% <79.16%> (-0.02%) ⬇️
memoize_builders 85.39% <79.16%> (-0.02%) ⬇️
no_fs_access 84.79% <79.16%> (-0.02%) ⬇️
tinygo 85.24% <79.16%> (-0.02%) ⬇️

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.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces an optional rule observer callback that allows users to observe and inspect rules as they are loaded into the WAF during initialization. The observer receives rule metadata for each successfully added rule, enabling use cases such as logging, metrics collection, and UI representation of the active rule set.

Changes:

  • Added WithRuleObserver method to the WAFConfig interface for configuring the observer callback
  • Implemented observer invocation in RuleGroup.Add() after a rule is successfully added
  • Added comprehensive test coverage with multiple scenarios

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
config.go Added WithRuleObserver method to WAFConfig interface and implementation in wafConfig struct
waf.go Integrated observer setup during WAF initialization by calling SetObserver on the Rules
internal/corazawaf/rulegroup.go Added observer field and SetObserver method; invokes observer after rule is appended
waf_test.go Added comprehensive tests covering observer functionality with and without observer configured

@fzipi
Copy link
Member

fzipi commented Jan 19, 2026

I like this idea. But maybe moving it to the experimental package first?

@fzipi
Copy link
Member

fzipi commented Jan 21, 2026

@jcchavezs WDTY?

@jcchavezs
Copy link
Member

I like the idea too but I was thinking we should move this to experimental. We could

  1. Do not add the method in the interface but the type only
  2. create a function in https://github.com/corazawaf/coraza/tree/main/experimental called WAFConfigWithRuleObserver which does the interface assertion and calles the type method.

What do you think @heaven ?

@heaven
Copy link
Author

heaven commented Jan 26, 2026

@jcchavezs I don't mind moving it.

My goal was to make the footprint as small as possible, though, so I tried the opposite – to avoid new types and other exports. Then I found the almost identical WithErrorCallback and tried to mimic it. Since this is mostly a configuration stage thing, the potential impact on existing installations is probably near zero. In other words, I'm having a hard time thinking of any negative impact it might cause.

@jcchavezs
Copy link
Member

@heaven no negative impact, the thing is:

  1. This sound very reasonable to me so I believe we should land the feature
  2. To avoid future breaking changes, we usually land new features that are still in development under the experimental package so we won't break anyone if we decide to change it later.

@heaven
Copy link
Author

heaven commented Jan 31, 2026

@jcchavezs understood, thanks for the explanation. I'm working on moving this to the experimental package, but I ran into an issue.

This is what I wanted to implement

package experimental

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

// ruleObserverCapable is the private capability interface
type ruleObserverCapable interface {
	WithRuleObserver(func(rule types.RuleMetadata)) coraza.WAFConfig
}

// WAFConfigWithRuleObserver applies a rule observer if supported.
func WAFConfigWithRuleObserver(
	cfg coraza.WAFConfig,
	observer func(rule types.RuleMetadata),
) coraza.WAFConfig {
	if c, ok := cfg.(ruleObserverCapable); ok {
		return c.WithRuleObserver(observer)
	}
	return cfg
}

But this creates a circular dependency, because coraza/waf imports the experimental package for NewTransactionWithOptions. I can work around this by using any instead of importing coraza.WAFConfig, which is probably fine for experimental, but please confirm.

package experimental

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

// ruleObserverCapable is the private capability interface
type ruleObserverCapable interface {
	WithRuleObserver(func(rule types.RuleMetadata)) any
}

// WAFConfigWithRuleObserver applies a rule observer if supported.
func WAFConfigWithRuleObserver(
	cfg any,
	observer func(rule types.RuleMetadata),
) any {
	if c, ok := cfg.(ruleObserverCapable); ok {
		return c.WithRuleObserver(observer)
	}
	return cfg
}

@heaven
Copy link
Author

heaven commented Jan 31, 2026

Because the core coraza package already depends on experimental, the experimental helper cannot reference the WAFConfig directly without creating a cyclic import. So I had to use reflection to preserve the immutable builder semantics.

Alternatively, and since WithRuleObserver is not part of the interface, we can make it return any, so the example above will work, and the type assertion will pass. Personally, I would prefer that approach over reflection.

Of course, the best long-term solution would be to remove the dependency from the core coraza to experimental.

Introduce an optional rule observer callback that is invoked for each rule successfully added to the WAF during initialization.

The observer receives rule metadata via the existing RuleMetadata interface.
@jcchavezs
Copy link
Member

thanks for trying it. I will take it a stab

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.

3 participants