# frozen_string_literal: true # IpRangeResolver - Service for resolving IP addresses to network ranges # # Provides methods to find matching network ranges for IP addresses, # create surgical blocks, and analyze IP intelligence. class IpRangeResolver # Find all network ranges that contain the given IP address # Returns array of hashes with range data, ordered by specificity (most specific first) def self.resolve(ip_address) return [] unless ip_address.present? NetworkRange.contains_ip(ip_address).map do |range| { range: range, cidr: range.cidr, prefix_length: range.prefix_length, specificity: range.prefix_length, intelligence: range.inherited_intelligence } end.sort_by { |r| -r[:specificity] } # Most specific first end # Find the most specific network range for an IP def self.most_specific_range(ip_address) resolve(ip_address).first end # Find all network ranges that overlap with a given CIDR def self.overlapping_ranges(cidr) return [] unless cidr.present? NetworkRange.overlapping(cidr).map do |range| { range: range, cidr: range.cidr, prefix_length: range.prefix_length, specificity: range.prefix_length, intelligence: range.inherited_intelligence } end.sort_by { |r| -r[:specificity] } end # Create network range if it doesn't exist def self.find_or_create_range(cidr, user: nil, source: nil, reason: nil, **attributes) return nil unless cidr.present? NetworkRange.find_or_create_by_cidr(cidr, user: user, source: source, reason: reason) do |range| # Try to inherit attributes from parent ranges inherited_attrs = inherited_attributes(cidr) range.assign_attributes(inherited_attrs.merge(attributes)) end end # Create surgical block (block parent range, allow specific IP) def self.create_surgical_block(ip_address, parent_cidr, user: nil, reason: nil, **options) return [nil, nil] unless ip_address.present? && parent_cidr.present? Rule.create_surgical_block(ip_address, parent_cidr, user: user, reason: reason, **options) end # Get IP intelligence data def self.get_ip_intelligence(ip_address) ranges = resolve(ip_address) { ip_address: ip_address, ranges: ranges, most_specific_range: ranges.first, intelligence: ranges.first&.dig(:intelligence) || {}, # Suggested blocking ranges suggested_blocks: suggest_blocking_ranges(ip_address, ranges) } end # Suggest CIDR ranges for blocking based on network hierarchy def self.suggest_blocking_ranges(ip_address, ranges = nil) ranges ||= resolve(ip_address) return [] if ranges.empty? ip_obj = IPAddr.new(ip_address) suggestions = [] # Current /32 or /128 (single IP) suggestions << { cidr: "#{ip_address}/#{ip_obj.ipv4? ? '32' : '128'}", type: 'single_ip', description: 'Single IP address', current_block: ranges.any? { |r| r[:prefix_length] == (ip_obj.ipv4? ? 32 : 128) } } # Look for common network sizes if ip_obj.ipv4? [24, 23, 22, 21, 20, 19, 18, 16].each do |prefix| network_cidr = calculate_network_cidr(ip_address, prefix) next unless network_cidr suggestions << { cidr: network_cidr, type: 'network_block', description: "/#{prefix} network block", current_block: ranges.any? { |r| r[:prefix_length] == prefix }, existing_range: ranges.find { |r| r[:prefix_length] <= prefix } } end end suggestions end # Find related IPs from same network ranges def self.find_related_ips(ip_address, limit_per_range: 100, total_limit: 500) ranges = resolve(ip_address) return [] if ranges.empty? related_ips = {} ranges.each do |range_data| range = range_data[:range] # Find events from this range (excluding the original IP) events = Event.where("ip_address <<= ?", range.cidr) # Postgres <<= operator .where.not(ip_address: ip_address) .limit(limit_per_range) .distinct(:ip_address) .pluck(:ip_address) related_ips[range.cidr] = events unless events.empty? break if related_ips.values.flatten.size >= total_limit end related_ips end # Check if IP is currently blocked by any rule def self.ip_blocked?(ip_address) ranges = resolve(ip_address) return false if ranges.empty? range_ids = ranges.map { |r| r[:range].id } Rule.network_rules .where(network_range_id: range_ids) .where(action: 'deny') .enabled .where("expires_at IS NULL OR expires_at > ?", Time.current) .exists? end # Get blocking rules for an IP def self.blocking_rules_for_ip(ip_address) ranges = resolve(ip_address) return Rule.none if ranges.empty? range_ids = ranges.map { |r| r[:range].id } Rule.network_rules .where(network_range_id: range_ids) .where(action: 'deny') .enabled .where("expires_at IS NULL OR expires_at > ?", Time.current) .includes(:network_range) .order('network_ranges.network_prefix DESC') end # Analyze traffic patterns for a network range def self.analyze_network_traffic(cidr, time_range: 1.week.ago..Time.current) network_range = NetworkRange.find_by(network: cidr) return nil unless network_range events = Event.where("ip_address <<= ?", cidr) # Postgres <<= operator .where(timestamp: time_range) { network_range: network_range, total_requests: events.count, unique_ips: events.distinct.count(:ip_address), blocked_requests: events.blocked.count, allowed_requests: events.allowed.count, top_paths: events.group(:request_path).count.sort_by { |_, count| -count }.first(10), top_user_agents: events.group(:user_agent).count.sort_by { |_, count| -count }.first(5), time_distribution: events.group_by_hour(:timestamp).count } end private # Inherit attributes from parent network ranges def self.inherited_attributes(cidr) ip_obj = IPAddr.new(cidr) parent = NetworkRange.where("network <<= ? AND masklen(network) < ?", cidr, ip_obj.prefixlen) .where.not(asn: nil) .order("masklen(network) DESC") .first if parent { asn: parent.asn, asn_org: parent.asn_org, company: parent.company, country: parent.country, is_datacenter: parent.is_datacenter, is_proxy: parent.is_proxy, is_vpn: parent.is_vpn } else {} end end # Calculate network CIDR for an IP and prefix length def self.calculate_network_cidr(ip_address, prefix_length) ip_obj = IPAddr.new(ip_address) network = ip_obj.mask(prefix_length) "#{network}/#{prefix_length}" rescue IPAddr::InvalidAddressError nil end end