# 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: path_pattern > country > asn > company > network_type, then by creation date @matching_policies.sort_by do |policy| priority_score = case policy.policy_type when 'path_pattern' 1 # Highest priority for path-specific rules when 'country' 2 when 'asn' 3 when 'company' 4 when 'network_type' 5 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| # Use the policy's event-based rule creation method rule = policy.create_rule_for_event(event) if rule if rule.persisted? Rails.logger.info "Generated rule for event #{event.id} from policy #{policy.name}" rule else # Rule creation failed validation Rails.logger.warn "Failed to create rule for event #{event.id}: #{rule.errors.full_messages.join(', ')}" nil end else # Policy didn't match or returned nil (e.g., supernet already exists) nil 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