# frozen_string_literal: true # WafPolicyMatcher - Service to match NetworkRanges against active WafPolicies # # This service provides efficient matching of network ranges against firewall policies # and can generate rules when matches are found. class WafPolicyMatcher include ActiveModel::Model include ActiveModel::Attributes attr_accessor :network_range attr_reader :matching_policies, :generated_rules def initialize(network_range:) @network_range = network_range @matching_policies = [] @generated_rules = [] end # Find all active policies that match the given network range def find_matching_policies return [] unless network_range.present? @matching_policies = active_policies.select do |policy| policy.matches_network_range?(network_range) 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_network_range(network_range) matcher = new(network_range: network_range) matcher.match_and_generate_rules end # Evaluate a network range against policies and mark it as evaluated # This is the main entry point for inline policy evaluation def self.evaluate_and_mark!(network_range) return { matching_policies: [], generated_rules: [] } unless network_range matcher = new(network_range: network_range) result = matcher.match_and_generate_rules # Mark this network range as evaluated network_range.update_column(:policies_evaluated_at, Time.current) result 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) rule = waf_policy.create_rule_for_network_range(network_range) results << { network_range: network_range, generated_rule: rule } if rule 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.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