222 lines
6.9 KiB
Ruby
222 lines
6.9 KiB
Ruby
# 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(waf_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(waf_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 |