Expand geo ranges when possible
This commit is contained in:
@@ -136,6 +136,33 @@ class NetworkRange < ApplicationRecord
|
||||
.order("masklen(network) ASC") # Least specific child first
|
||||
end
|
||||
|
||||
# Find or create an ancestor network at a specific prefix length
|
||||
# For example, given 192.168.1.0/24 and prefix 16, returns 192.168.0.0/16
|
||||
def find_or_create_ancestor_at_prefix(target_prefix)
|
||||
return self if prefix_length <= target_prefix
|
||||
|
||||
# Use PostgreSQL's set_masklen to create the ancestor CIDR
|
||||
result = self.class.connection.execute(
|
||||
"SELECT set_masklen('#{network}'::inet, #{target_prefix})::text as ancestor_cidr"
|
||||
).first
|
||||
|
||||
return self unless result
|
||||
|
||||
ancestor_cidr = result["ancestor_cidr"]
|
||||
return self if ancestor_cidr == cidr
|
||||
|
||||
# Find or create the ancestor network range
|
||||
ancestor = NetworkRange.find_by(network: ancestor_cidr)
|
||||
|
||||
if ancestor.nil?
|
||||
# Create a virtual ancestor (not persisted, just for reference)
|
||||
# The caller can decide whether to persist it
|
||||
ancestor = NetworkRange.new(network: ancestor_cidr, source: 'inherited')
|
||||
end
|
||||
|
||||
ancestor
|
||||
end
|
||||
|
||||
# Find nearest parent with intelligence data
|
||||
def parent_with_intelligence
|
||||
# Find all parent ranges (networks that contain this network)
|
||||
|
||||
@@ -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,52 @@ 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
|
||||
|
||||
# Walk up from current prefix to /8 (IPv4) or /32 to /1 (IPv6)
|
||||
current_prefix = network_range.prefix_length
|
||||
max_prefix = network_range.ipv4? ? 8 : 1
|
||||
|
||||
current_prefix.step(-1, -1).each do |prefix|
|
||||
break if prefix < max_prefix
|
||||
|
||||
candidate = network_range.find_or_create_ancestor_at_prefix(prefix)
|
||||
# Check if candidate has geo data (either direct or inherited)
|
||||
candidate_country = candidate.country || (candidate.persisted? ? candidate.inherited_intelligence[:country] : nil)
|
||||
|
||||
# For virtual (unpersisted) ancestors, we need to check if any existing records in that range have the country
|
||||
if candidate_country.nil? && !candidate.persisted?
|
||||
# Query database to see if networks in this range have the same country
|
||||
country_check = NetworkRange.where("network <<= ?", candidate.cidr)
|
||||
.where.not(country: nil)
|
||||
.select(:country)
|
||||
.distinct
|
||||
# If all networks in this range have the same country, we can use it
|
||||
if country_check.count == 1 && country_check.first.country == country
|
||||
candidate_country = country
|
||||
end
|
||||
end
|
||||
|
||||
# If ancestor has same country, use it (persist if virtual)
|
||||
if candidate_country == country
|
||||
if !candidate.persisted?
|
||||
candidate.save!
|
||||
Rails.logger.info "Created ancestor network range #{candidate.cidr} for country #{country}"
|
||||
end
|
||||
Rails.logger.debug "Expanded #{network_range.cidr} to #{candidate.cidr} (both #{country})"
|
||||
return candidate
|
||||
end
|
||||
end
|
||||
|
||||
network_range
|
||||
end
|
||||
|
||||
def matched_field(network_range)
|
||||
case policy_type
|
||||
when 'country'
|
||||
|
||||
Reference in New Issue
Block a user