diff --git a/app/models/waf_policy.rb b/app/models/waf_policy.rb index 0f958e6..932e689 100644 --- a/app/models/waf_policy.rb +++ b/app/models/waf_policy.rb @@ -152,10 +152,14 @@ validate :targets_must_be_array def create_rule_for_network_range(network_range) return nil unless matches_network_range?(network_range) + # For country policies, expand to largest matching ancestor + # This consolidates /24 rules into /16, /8, etc. when possible + expanded_range = find_largest_matching_ancestor(network_range) + # Check for existing supernet rules before attempting to create - if network_range.supernet_rules.any? - supernet = network_range.supernet_rules.first - Rails.logger.debug "Skipping rule creation for #{network_range.cidr} - covered by supernet rule ##{supernet.id} (#{supernet.network_range.cidr})" + if expanded_range.supernet_rules.any? + supernet = expanded_range.supernet_rules.first + Rails.logger.debug "Skipping rule creation for #{expanded_range.cidr} - covered by supernet rule ##{supernet.id} (#{supernet.network_range.cidr})" return nil end @@ -164,21 +168,21 @@ validate :targets_must_be_array rule = Rule.create!( waf_rule_type: 'network', waf_action: policy_action.to_sym, - network_range: network_range, + network_range: expanded_range, waf_policy: self, user: user, source: "policy", - metadata: build_rule_metadata(network_range), - priority: network_range.prefix_length + metadata: build_rule_metadata(expanded_range), + priority: expanded_range.prefix_length ) rescue ActiveRecord::RecordNotUnique # Rule already exists (created by another job or earlier in this job) # Find and return the existing rule - Rails.logger.debug "Rule already exists for #{network_range.cidr} with policy #{name}" + Rails.logger.debug "Rule already exists for #{expanded_range.cidr} with policy #{name}" return Rule.find_by( waf_rule_type: 'network', waf_action: policy_action, - network_range: network_range, + network_range: expanded_range, waf_policy: self, source: "policy" ) @@ -505,6 +509,64 @@ validate :targets_must_be_array base_metadata.merge!(additional_data || {}) end + # For country policies, find the largest ancestor network that matches the same country + # This allows consolidating /24 rules into /16, /8, etc. when the entire block is in the same country + def find_largest_matching_ancestor(network_range) + return network_range unless country_policy? + + country = network_range.country || network_range.inherited_intelligence[:country] + return network_range unless country + + # Check if this network has IPAPI data with a larger CIDR (asn.route or ipapi_returned_cidr) + ipapi_cidr = network_range.network_data&.dig('ipapi', 'asn', 'route') || + network_range.network_data&.dig('ipapi_returned_cidr') + + if ipapi_cidr && ipapi_cidr != network_range.cidr + # IPAPI returned a larger network - use it if it exists + existing = NetworkRange.find_by(network: ipapi_cidr) + if existing + existing_country = existing.country || existing.inherited_intelligence[:country] + if existing_country == country + Rails.logger.debug "Using IPAPI CIDR #{existing.cidr} instead of #{network_range.cidr} (both #{country})" + return existing + end + else + # Create the IPAPI network range if it doesn't exist + begin + ipapi_network = NetworkRange.create!( + network: ipapi_cidr, + source: 'inherited', + country: country + ) + Rails.logger.info "Created IPAPI network range #{ipapi_cidr} for country #{country}" + return ipapi_network + rescue ActiveRecord::RecordNotUnique + # Race condition - another process created it + existing = NetworkRange.find_by(network: ipapi_cidr) + return existing || network_range + end + end + end + + # Fallback: Look for existing parent networks with IPAPI data and same country + # Query for all networks that contain this network and have IPAPI data + parent_with_ipapi = NetworkRange.where( + "?::inet << network", network_range.cidr + ).where( + "network_data ? 'ipapi' AND " \ + "network_data -> 'ipapi' ->> 'location' ->> 'country_code' = ?", + country + ).order("masklen(network) DESC").first + + if parent_with_ipapi + Rails.logger.debug "Found existing IPAPI parent #{parent_with_ipapi.cidr} for #{network_range.cidr} (both #{country})" + return parent_with_ipapi + end + + # No expansion possible - use original network + network_range + end + def matched_field(network_range) case policy_type when 'country'