Files
baffle-hub/app/services/waf_policy_matcher.rb
Dan Milne 6433f6c5bb Updates
2025-11-14 16:35:49 +11:00

244 lines
8.0 KiB
Ruby

# frozen_string_literal: true
# WafPolicyMatcher - Service to match Events against active WafPolicies
#
# This service provides efficient matching of events against firewall policies
# (both network-based and path-based) and can generate rules when matches are found.
class WafPolicyMatcher
include ActiveModel::Model
include ActiveModel::Attributes
attr_accessor :event
attr_reader :matching_policies, :generated_rules
def initialize(event:)
@event = event
@matching_policies = []
@generated_rules = []
end
# Helper method to get network range from event
def network_range
event&.network_range
end
# Find all active policies that match the given event (network or path-based)
def find_matching_policies
return [] unless event.present?
@matching_policies = active_policies.select do |policy|
policy.matches_event?(event)
end
# Sort by priority: country > asn > company > network_type, then by creation date
@matching_policies.sort_by do |policy|
priority_score = case policy.policy_type
when 'country'
1
when 'asn'
2
when 'company'
3
when 'network_type'
4
else
99
end
[priority_score, policy.created_at]
end
end
# Generate rules from matching policies
def generate_rules
return [] if matching_policies.empty?
@generated_rules = matching_policies.map do |policy|
# Check if rule already exists for this network range and policy
existing_rule = Rule.find_by(
network_range: network_range,
waf_policy: policy,
enabled: true
)
if existing_rule
Rails.logger.debug "Rule already exists for network_range #{network_range.cidr} and policy #{policy.name}"
existing_rule
else
rule = policy.create_rule_for_network_range(network_range)
if rule
Rails.logger.info "Generated rule for network_range #{network_range.cidr} from policy #{policy.name}"
end
rule
end
end.compact
end
# Find and generate rules in one step
def match_and_generate_rules
find_matching_policies
generate_rules
# Return hash format expected by ProcessWafPoliciesJob
{
matching_policies: @matching_policies,
generated_rules: @generated_rules
}
end
# Class methods for batch processing
def self.process_event(event)
matcher = new(event: event)
matcher.match_and_generate_rules
end
# Legacy method for backward compatibility - converts network range to event
def self.process_network_range(network_range)
# Find the most recent event for this network range
sample_event = network_range.events.order(created_at: :desc).first
if sample_event
process_event(sample_event)
else
# No events exist for this network range, return empty results
# Network-based policies need real events to trigger rule creation
{ matching_policies: [], generated_rules: [] }
end
end
# Evaluate an event against policies and mark its network range as evaluated
# This is the main entry point for inline policy evaluation
def self.evaluate_and_mark!(event)
return { matching_policies: [], generated_rules: [] } unless event
matcher = new(event: event)
result = matcher.match_and_generate_rules
# Mark the event's network range as evaluated
if event.network_range
event.network_range.update_column(:policies_evaluated_at, Time.current)
end
result
end
# Legacy method for backward compatibility
def self.evaluate_and_mark_network_range!(network_range)
return { matching_policies: [], generated_rules: [] } unless network_range
# Find the most recent event for this network range
sample_event = network_range.events.order(created_at: :desc).first
if sample_event
evaluate_and_mark!(sample_event)
else
# No events exist, use the old network-range based evaluation
process_network_range(network_range)
network_range.update_column(:policies_evaluated_at, Time.current)
{ matching_policies: [], generated_rules: [] }
end
end
def self.batch_process_network_ranges(network_ranges)
results = []
network_ranges.each do |network_range|
matcher = new(network_range: network_range)
result = matcher.match_and_generate_rules
results << {
network_range: network_range,
matching_policies: matcher.matching_policies,
generated_rules: matcher.generated_rules
}
end
results
end
# Process network ranges that need policy evaluation
def self.process_ranges_without_policy_rules(limit: 100)
# Find network ranges that don't have policy-generated rules
# but have intelligence data that could match policies
ranges_needing_evaluation = NetworkRange
.left_joins(:rules)
.where("rules.id IS NULL OR rules.waf_policy_id IS NULL")
.where("(country IS NOT NULL OR asn IS NOT NULL OR company IS NOT NULL OR is_datacenter = true OR is_proxy = true OR is_vpn = true)")
.limit(limit)
.includes(:rules)
batch_process_network_ranges(ranges_needing_evaluation)
end
# Re-evaluate all network ranges for policy changes
def self.reprocess_all_for_policy(waf_policy)
# Find all network ranges that could potentially match this policy
potential_ranges = case waf_policy.policy_type
when 'country'
NetworkRange.where(country: waf_policy.targets)
when 'asn'
NetworkRange.where(asn: waf_policy.targets)
when 'network_type'
NetworkRange.where(
"is_datacenter = ? OR is_proxy = ? OR is_vpn = ?",
waf_policy.targets.include?('datacenter'),
waf_policy.targets.include?('proxy'),
waf_policy.targets.include?('vpn')
)
when 'company'
# For company matching, we need to do text matching
NetworkRange.where("company ILIKE ANY (array[?])",
waf_policy.targets.map { |c| "%#{c}%" })
else
NetworkRange.none
end
results = []
potential_ranges.find_each do |network_range|
matcher = new(network_range: network_range)
if waf_policy.matches_network_range?(network_range)
# Check for supernet rules before creating
if network_range.supernet_rules.any?
supernet = network_range.supernet_rules.first
Rails.logger.info "Skipping rule for #{network_range.cidr} - covered by supernet rule ##{supernet.id}"
next
end
rule = waf_policy.create_rule_for_network_range(network_range)
if rule&.persisted?
results << { network_range: network_range, generated_rule: rule }
elsif rule
Rails.logger.warn "Failed to create rule for #{network_range.cidr}: #{rule.errors.full_messages.join(', ')}"
end
end
end
results
end
# Statistics and reporting
def self.matching_policies_for_network_range(network_range)
matcher = new(network_range: network_range)
matcher.find_matching_policies
end
def self.policy_effectiveness_stats(waf_policy, days: 30)
cutoff_date = days.days.ago
rules = waf_policy.generated_rules.where('created_at > ?', cutoff_date)
{
policy_name: waf_policy.name,
policy_type: waf_policy.policy_type,
action: waf_policy.policy_action,
rules_generated: rules.count,
active_rules: rules.active.count,
networks_protected: rules.joins(:network_range).count('distinct network_ranges.id'),
period_days: days,
generation_rate: rules.count.to_f / days
}
end
private
def active_policies
@active_policies ||= WafPolicy.active
end
end