# frozen_string_literal: true # NetworkRange - Unified IPv4/IPv6 network range management # # Uses PostgreSQL's inet type to handle both IPv4 and IPv4 networks seamlessly. # Provides network intelligence data including ASN, company, geographic info, # and classification flags (datacenter, proxy, VPN). class NetworkRange < ApplicationRecord # Sources for network range creation SOURCES = %w[api_imported user_created manual auto_generated inherited].freeze # Associations has_many :rules, dependent: :destroy belongs_to :user, optional: true # Validations validates :network, presence: true, uniqueness: true validates :source, inclusion: { in: SOURCES } validates :asn, numericality: { greater_than: 0 }, allow_blank: true # Scopes scope :ipv4, -> { where("family(network) = 4") } scope :ipv6, -> { where("family(network) = 6") } scope :by_country, ->(country) { where(country: country) } scope :by_company, ->(company) { where(company: company) } scope :by_asn, ->(asn) { where(asn: asn) } scope :datacenter, -> { where(is_datacenter: true) } scope :proxy, -> { where(is_proxy: true) } scope :vpn, -> { where(is_vpn: true) } scope :user_created, -> { where(source: 'user_created') } scope :api_imported, -> { where(source: 'api_imported') } # Callbacks before_validation :set_default_source # after_save :update_children_inheritance!, if: :should_update_children_inheritance? # Disabled for now # Virtual attribute for CIDR notation def cidr network.to_s end def cidr=(new_cidr) self.network = new_cidr end # Network properties def prefix_length # Get prefix length from IPAddr object network.prefix end def network_address # Use PostgreSQL's host function or get from IPAddr object network.to_s end def cidr # Return full CIDR notation "#{network_address}/#{prefix_length}" end def broadcast_address # Use PostgreSQL's broadcast function result = self.class.connection.execute("SELECT broadcast('#{network.to_s}')").first result&.values&.first end def family # Check if it's IPv4 or IPv6 by looking at the address addr = network.to_s.split('/').first addr.include?(':') ? 6 : 4 end def ipv4? family == 4 end def ipv6? family == 6 end # Network containment and overlap operations def contains_ip?(ip_string) # Use Postgres >>= operator for containment self.class.where("network >>= ?::inet", ip_string).exists? rescue => e Rails.logger.error "Error checking IP containment: #{e.message}" false end def contains_network?(other_cidr) other_network = IPAddr.new(other_cidr) network_range = IPAddr.new(network) network_range.include?(other_network) rescue IPAddr::InvalidAddressError false end def overlaps?(other_cidr) network_range = IPAddr.new(network) other_network = IPAddr.new(other_cidr) network_range.include?(other_network) || other_network.include?(network_range) rescue IPAddr::InvalidAddressError false end # Parent/child relationships def parent_ranges NetworkRange.where("network << ?::inet AND masklen(network) < ?", network.to_s, prefix_length) .order("masklen(network) DESC") end def child_ranges NetworkRange.where("network >> ?::inet AND masklen(network) > ?", network.to_s, prefix_length) .order("masklen(network) ASC") end def sibling_ranges NetworkRange.where("masklen(network) = ?", prefix_length) .where("network && ?::inet", network.to_s) .where.not(id: id) end # Find nearest parent with intelligence data def parent_with_intelligence # Use Postgres network operators to find parent ranges directly cidr_str = network.to_s if cidr_str.include?('/') addr_parts = network_address.split('.') case addr_parts.length when 4 # IPv4 new_prefix = [prefix_length - 8, 16].max parent_cidr = "#{addr_parts[0]}.#{addr_parts[1]}.#{addr_parts[2]}.0/#{new_prefix}" else # IPv6 - skip for now nil end else nil end return nil unless parent_cidr NetworkRange.where("network <<= ?::inet AND masklen(network) < ?", parent_cidr, prefix_length) .where.not(asn: nil) .order("masklen(network) DESC") .first end def inherited_intelligence return own_intelligence if has_intelligence? parent = parent_with_intelligence parent ? parent.own_intelligence.merge(inherited: true, parent_cidr: parent.cidr) : {} end def has_intelligence? asn.present? || company.present? || country.present? || is_datacenter? || is_proxy? || is_vpn? end def own_intelligence { asn: asn, asn_org: asn_org, company: company, country: country, is_datacenter: is_datacenter, is_proxy: is_proxy, is_vpn: is_vpn, inherited: false, source: source } end # Geographic lookup def geo_lookup_country! return if country.present? sample_ip = network_address geo_country = GeoIpService.lookup_country(sample_ip) update!(country: geo_country) if geo_country.present? rescue => e Rails.logger.error "Failed to lookup geo location for network range #{cidr}: #{e.message}" end # Class methods for network operations def self.contains_ip(ip_string) where("network >>= ?", ip_string) .order("masklen(network) DESC") # Most specific first end def self.overlapping(range_cidr) where("network && ?", range_cidr) end def self.find_or_create_by_cidr(cidr, user: nil, source: nil, reason: nil) find_or_create_by(network: cidr) do |range| range.user = user range.source = source || 'user_created' range.creation_reason = reason end end def self.import_from_cidr(cidr, **attributes) find_or_create_by(network: cidr) do |range| range.assign_attributes(attributes) end end # Convenience methods for JSON fields def abuser_scores_hash abuser_scores ? JSON.parse(abuser_scores) : {} rescue JSON::ParserError {} end def abuser_scores_hash=(hash) self.abuser_scores = hash.to_json end def additional_data_hash additional_data ? JSON.parse(additional_data) : {} rescue JSON::ParserError {} end def additional_data_hash=(hash) self.additional_data = hash.to_json end # String representations def to_s cidr end def to_param cidr.to_s.gsub('/', '_') end # Analytics methods def events_count Event.where(ip_address: child_ranges.pluck(:network_address) + [network_address]).count end def recent_events(limit: 100) Event.where(ip_address: child_ranges.pluck(:network_address) + [network_address]) .recent .limit(limit) end def blocking_rules rules.where(action: 'deny', enabled: true) end def active_rules rules.enabled.where("expires_at IS NULL OR expires_at > ?", Time.current) end private def set_default_source self.source ||= 'api_imported' end def should_update_children_inheritance? saved_change_to_attribute?(:asn) || saved_change_to_attribute?(:company) || saved_change_to_attribute?(:country) || saved_change_to_attribute?(:is_datacenter) || saved_change_to_attribute?(:is_proxy) || saved_change_to_attribute?(:is_vpn) end def update_children_inheritance! # Find child ranges that don't have their own intelligence child_without_intelligence = child_ranges.where( asn: nil, company: nil, country: nil, is_datacenter: false, is_proxy: false, is_vpn: false ) child_without_intelligence.find_each do |child| Rails.logger.info "Child range #{child.cidr} can now inherit from parent #{cidr}" # The inherited_intelligence method will pick up the new parent data end end end