Files
baffle-hub/test/models/network_range_test.rb
2025-12-03 17:16:38 +11:00

719 lines
24 KiB
Ruby

require "test_helper"
class NetworkRangeTest < ActiveSupport::TestCase
setup do
@ipv4_range = NetworkRange.new(network: "192.168.1.0/24")
@ipv6_range = NetworkRange.new(network: "2001:db8::/32")
@user = users(:jason)
end
# Validations
test "should be valid with network address" do
assert @ipv4_range.valid?
assert @ipv6_range.valid?
end
test "should not be valid without network" do
@ipv4_range.network = nil
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:network], "can't be blank"
end
test "should validate network uniqueness" do
@ipv4_range.save!
duplicate = NetworkRange.new(network: "192.168.1.0/24")
assert_not duplicate.valid?
assert_includes duplicate.errors[:network], "has already been taken"
end
test "should validate source inclusion" do
valid_sources = %w[api_imported user_created manual auto_generated inherited geolite_asn geolite_country]
valid_sources.each do |source|
@ipv4_range.source = source
assert @ipv4_range.valid?, "Source #{source} should be valid"
end
@ipv4_range.source = "invalid_source"
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:source], "is not included in the list"
end
test "should validate ASN numericality" do
@ipv4_range.asn = 12345
assert @ipv4_range.valid?
@ipv4_range.asn = 0
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:asn], "must be greater than 0"
@ipv4_range.asn = -1
assert_not @ipv4_range.valid?
assert_includes @ipv4_range.errors[:asn], "must be greater than 0"
@ipv4_range.asn = "not_a_number"
assert_not @ipv4_range.valid?
@ipv4_range.asn = nil
assert @ipv4_range.valid?
end
# Callbacks
test "should set default source before validation" do
range = NetworkRange.new(network: "10.0.0.0/8")
range.valid?
assert_equal "api_imported", range.source
end
test "should not override existing source" do
range = NetworkRange.new(network: "10.0.0.0/8", source: "user_created")
range.valid?
assert_equal "user_created", range.source
end
# Virtual Attributes (CIDR)
test "cidr getter returns network as string" do
@ipv4_range.save!
assert_equal "192.168.1.0/24", @ipv4_range.cidr
assert_equal "192.168.1.0/24", @ipv4_range.network.to_s
end
test "cidr setter sets network from string" do
range = NetworkRange.new
range.cidr = "10.0.0.0/16"
assert_equal "10.0.0.0/16", range.network.to_s
end
# Network Properties
test "prefix_length returns correct network prefix" do
@ipv4_range.save!
@ipv6_range.save!
assert_equal 24, @ipv4_range.prefix_length
assert_equal 32, @ipv6_range.prefix_length
end
test "network_address returns network address" do
@ipv4_range.save!
assert_equal "192.168.1.0", @ipv4_range.network_address
end
test "broadcast_address returns correct broadcast for IPv4" do
@ipv4_range.save!
assert_equal "192.168.1.255", @ipv4_range.broadcast_address
end
test "broadcast_address returns nil for IPv6" do
@ipv6_range.save!
assert_nil @ipv6_range.broadcast_address
end
test "family detection works correctly" do
@ipv4_range.save!
@ipv6_range.save!
assert_equal 4, @ipv4_range.family
assert_equal 6, @ipv6_range.family
end
test "ipv4? and ipv6? predicate methods work" do
@ipv4_range.save!
@ipv6_range.save!
assert @ipv4_range.ipv4?
assert_not @ipv4_range.ipv6?
assert @ipv6_range.ipv6?
assert_not @ipv6_range.ipv4?
end
test "virtual? works correctly" do
range = NetworkRange.new(network: "10.0.0.0/8")
assert range.virtual?
range.save!
assert_not range.virtual?
end
# Network Containment
test "contains_ip? works correctly" do
@ipv4_range.save!
assert @ipv4_range.contains_ip?("192.168.1.1")
assert @ipv4_range.contains_ip?("192.168.1.254")
assert_not @ipv4_range.contains_ip?("192.168.2.1")
assert_not @ipv4_range.contains_ip?("2001:db8::1")
# Test IPv6
@ipv6_range.save!
assert @ipv6_range.contains_ip?("2001:db8::1")
assert @ipv6_range.contains_ip?("2001:db8:ffff::ffff")
assert_not @ipv6_range.contains_ip?("2001:db9::1")
end
test "contains_network? works correctly" do
@ipv4_range.save!
# More specific network
assert @ipv4_range.contains_network?("192.168.1.0/25")
assert @ipv4_range.contains_network?("192.168.1.128/25")
# Same network
assert @ipv4_range.contains_network?("192.168.1.0/24")
# Less specific network
assert_not @ipv4_range.contains_network?("192.168.0.0/16")
# Different network
assert_not @ipv4_range.contains_network?("10.0.0.0/8")
end
test "overlaps? works correctly" do
@ipv4_range.save!
# Overlapping networks
assert @ipv4_range.overlaps?("192.168.1.0/25") # More specific
assert @ipv4_range.overlaps?("192.168.0.0/23") # Less specific
assert @ipv4_range.overlaps?("192.168.1.128/25") # Partial overlap
# Non-overlapping
assert_not @ipv4_range.overlaps?("10.0.0.0/8")
assert_not @ipv4_range.overlaps?("172.16.0.0/12")
end
# Parent/Child Relationships
test "parent_ranges finds containing networks" do
# Create parent and child networks
parent = NetworkRange.create!(network: "192.168.0.0/16")
@ipv4_range.save! # 192.168.1.0/24
child = NetworkRange.create!(network: "192.168.1.0/25")
parents = @ipv4_range.parent_ranges
assert_includes parents, parent
assert_not_includes parents, child
assert_not_includes parents, @ipv4_range
# Should be ordered by specificity (more specific first)
assert_equal parent, parents.first
end
test "child_ranges finds contained networks" do
# Create parent and child networks
parent = NetworkRange.create!(network: "192.168.0.0/16")
@ipv4_range.save! # 192.168.1.0/24
child = NetworkRange.create!(network: "192.168.1.0/25")
children = parent.child_ranges
assert_includes children, @ipv4_range
assert_includes children, child
assert_not_includes children, parent
# Should be ordered by specificity (less specific first)
assert_equal @ipv4_range, children.first
end
test "child_ranges works with Apple network hierarchy - 17.240.0.0/14" do
# This test demonstrates the current bug in child_ranges method
# Expected: 17.240.0.0/14 should have parents but no children in this test setup
# Create the target network
target_network = NetworkRange.create!(network: "17.240.0.0/14", source: "manual")
# Create parent networks
parent1 = NetworkRange.create!(network: "17.240.0.0/13", source: "manual") # Should contain 17.240.0.0/14
parent2 = NetworkRange.create!(network: "17.128.0.0/9", source: "manual") # Should also contain 17.240.0.0/14
# Create some child networks (more specific networks contained by 17.240.0.0/14)
child1 = NetworkRange.create!(network: "17.240.0.0/15", source: "manual") # First half of /14
child2 = NetworkRange.create!(network: "17.242.0.0/15", source: "manual") # Second half of /14
child3 = NetworkRange.create!(network: "17.240.0.0/16", source: "manual") # More specific
child4 = NetworkRange.create!(network: "17.241.0.0/16", source: "manual") # More specific
# Test parent_ranges works correctly
parents = target_network.parent_ranges
assert_includes parents, parent1, "17.240.0.0/13 should be a parent of 17.240.0.0/14"
assert_includes parents, parent2, "17.128.0.0/9 should be a parent of 17.240.0.0/14"
# Test child_ranges - this is currently failing due to the bug
children = target_network.child_ranges
assert_includes children, child1, "17.240.0.0/15 should be a child of 17.240.0.0/14"
assert_includes children, child2, "17.242.0.0/15 should be a child of 17.240.0.0/14"
assert_includes children, child3, "17.240.0.0/16 should be a child of 17.240.0.0/14"
assert_includes children, child4, "17.241.0.0/16 should be a child of 17.240.0.0/14"
assert_not_includes children, parent1, "Parent networks should not be in child_ranges"
assert_not_includes children, parent2, "Parent networks should not be in child_ranges"
assert_not_includes children, target_network, "Self should not be in child_ranges"
# Test that parent can find child in its child_ranges
parent1_children = parent1.child_ranges
assert_includes parent1_children, target_network, "17.240.0.0/14 should be in child_ranges of 17.240.0.0/13"
parent2_children = parent2.child_ranges
assert_includes parent2_children, target_network, "17.240.0.0/14 should be in child_ranges of 17.128.0.0/9"
# Test bidirectional consistency
assert target_network.parent_ranges.include?(parent1), "Parent should list child"
assert parent1.child_ranges.include?(target_network), "Child should list parent"
assert target_network.parent_ranges.include?(parent2), "Parent should list child"
assert parent2.child_ranges.include?(target_network), "Child should list parent"
end
# Intelligence and Inheritance
test "has_intelligence? detects available intelligence data" do
range = NetworkRange.new(network: "10.0.0.0/8")
assert_not range.has_intelligence?
range.asn = 12345
assert range.has_intelligence?
range.asn = nil
range.company = "Test Company"
assert range.has_intelligence?
range.company = nil
range.country = "US"
assert range.has_intelligence?
range.country = nil
range.is_datacenter = true
assert range.has_intelligence?
end
test "own_intelligence returns correct data structure" do
range = NetworkRange.create!(
network: "10.0.0.0/8",
asn: 12345,
asn_org: "Test ASN",
company: "Test Company",
country: "US",
is_datacenter: true,
is_proxy: false,
is_vpn: false,
source: "manual"
)
intelligence = range.own_intelligence
assert_equal 12345, intelligence[:asn]
assert_equal "Test ASN", intelligence[:asn_org]
assert_equal "Test Company", intelligence[:company]
assert_equal "US", intelligence[:country]
assert_equal true, intelligence[:is_datacenter]
assert_equal false, intelligence[:is_proxy]
assert_equal false, intelligence[:is_vpn]
assert_equal false, intelligence[:inherited]
assert_equal "manual", intelligence[:source]
end
test "inherited_intelligence returns own data when available" do
child = NetworkRange.create!(
network: "192.168.1.0/24",
country: "US",
company: "Test Company"
)
intelligence = child.inherited_intelligence
assert_equal "US", intelligence[:country]
assert_equal "Test Company", intelligence[:company]
assert_equal false, intelligence[:inherited]
end
test "inherited_intelligence inherits from parent when needed" do
parent = NetworkRange.create!(
network: "192.168.0.0/16",
country: "US",
company: "Test Company"
)
child = NetworkRange.create!(network: "192.168.1.0/24")
intelligence = child.inherited_intelligence
assert_equal "US", intelligence[:country]
assert_equal "Test Company", intelligence[:company]
assert_equal true, intelligence[:inherited]
assert_equal parent.cidr, intelligence[:parent_cidr]
end
test "parent_with_intelligence finds nearest parent with data" do
grandparent = NetworkRange.create!(network: "10.0.0.0/8", country: "US")
parent = NetworkRange.create!(network: "10.1.0.0/16")
child = NetworkRange.create!(network: "10.1.1.0/24")
found_parent = child.parent_with_intelligence
assert_equal grandparent, found_parent
assert_not_equal parent, found_parent
end
# API Data Management
test "is_fetching_api_data? tracks active fetches" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_not range.is_fetching_api_data?(:ipapi)
range.mark_as_fetching_api_data!(:ipapi)
assert range.is_fetching_api_data?(:ipapi)
range.clear_fetching_status!(:ipapi)
assert_not range.is_fetching_api_data?(:ipapi)
end
test "should_fetch_api_data? prevents duplicate fetches" do
range = NetworkRange.create!(network: "10.0.0.0/8")
# Should fetch initially
assert range.should_fetch_api_data?(:ipapi)
# Should not fetch while fetching
range.mark_as_fetching_api_data!(:ipapi)
assert_not range.should_fetch_api_data?(:ipapi)
# Should fetch again after clearing
range.clear_fetching_status!(:ipapi)
assert range.should_fetch_api_data?(:ipapi)
end
test "has_ipapi_data_available? checks inheritance" do
parent = NetworkRange.create!(network: "10.0.0.0/8")
parent.set_network_data(:ipapi, { country: "US" })
parent.save!
child = NetworkRange.create!(network: "10.0.1.0/24")
assert child.has_ipapi_data_available?
end
test "should_fetch_ipapi_data? respects parent fetching status" do
parent = NetworkRange.create!(network: "10.0.0.0/8")
child = NetworkRange.create!(network: "10.0.1.0/24")
parent.mark_as_fetching_api_data!(:ipapi)
assert_not child.should_fetch_ipapi_data?
parent.clear_fetching_status!(:ipapi)
assert child.should_fetch_ipapi_data?
end
# Network Data Management
test "network_data_for and set_network_data work correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.network_data_for(:ipapi))
data = { country: "US", city: "New York" }
range.set_network_data(:ipapi, data)
range.save!
assert_equal data, range.network_data_for(:ipapi)
end
test "has_network_data_from? checks data presence" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_not range.has_network_data_from?(:ipapi)
range.set_network_data(:ipapi, { country: "US" })
range.save!
assert range.has_network_data_from?(:ipapi)
end
# IPAPI Tracking Methods
test "find_or_create_tracking_network_for_ip works correctly" do
# IPv4 - should create /24
tracking_range = NetworkRange.find_or_create_tracking_network_for_ip("192.168.1.100")
assert_equal "192.168.1.0/24", tracking_range.network.to_s
assert_equal "auto_generated", tracking_range.source
assert_equal "IPAPI tracking network", tracking_range.creation_reason
# IPv6 - should create /64
ipv6_tracking = NetworkRange.find_or_create_tracking_network_for_ip("2001:db8::1")
assert_equal "2001:db8::/64", ipv6_tracking.network.to_s
end
test "should_fetch_ipapi_for_ip? works correctly" do
tracking_range = NetworkRange.create!(network: "192.168.1.0/8")
# Should fetch initially
assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
# Mark as queried recently
tracking_range.mark_ipapi_queried!("192.168.1.0/24")
assert_not NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
# Should fetch for old queries
tracking_range.network_data['ipapi_queried_at'] = 2.years.ago.to_i
tracking_range.save!
assert NetworkRange.should_fetch_ipapi_for_ip?("192.168.1.100")
end
test "mark_ipapi_queried! stores query metadata" do
range = NetworkRange.create!(network: "192.168.1.0/24")
range.mark_ipapi_queried!("192.168.1.128/25")
assert range.network_data['ipapi_queried_at'] > 5.seconds.ago.to_i
assert_equal "192.168.1.128/25", range.network_data['ipapi_returned_cidr']
end
# JSON Helper Methods
test "abuser_scores_hash handles JSON correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.abuser_scores_hash)
range.abuser_scores_hash = { ipquality: 85, abuseipdb: 92 }
range.save!
assert_equal({ "ipquality" => 85, "abuseipdb" => 92 }, JSON.parse(range.abuser_scores))
assert_equal({ ipquality: 85, abuseipdb: 92 }, range.abuser_scores_hash)
end
test "additional_data_hash handles JSON correctly" do
range = NetworkRange.create!(network: "10.0.0.0/8")
assert_equal({}, range.additional_data_hash)
range.additional_data_hash = { tags: ["malicious", "botnet"], notes: "Test data" }
range.save!
parsed_data = JSON.parse(range.additional_data)
assert_equal ["malicious", "botnet"], parsed_data["tags"]
assert_equal "Test data", parsed_data["notes"]
end
# Scopes
test "ipv4 and ipv6 scopes work correctly" do
ipv4_range = NetworkRange.create!(network: "192.168.1.0/24")
ipv6_range = NetworkRange.create!(network: "2001:db8::/32")
assert_includes NetworkRange.ipv4, ipv4_range
assert_not_includes NetworkRange.ipv4, ipv6_range
assert_includes NetworkRange.ipv6, ipv6_range
assert_not_includes NetworkRange.ipv6, ipv4_range
end
test "filtering scopes work correctly" do
range1 = NetworkRange.create!(network: "192.168.1.0/24", country: "US", company: "Google", asn: 15169, is_datacenter: true)
range2 = NetworkRange.create!(network: "10.0.0.0/8", country: "BR", company: "Amazon", asn: 16509, is_proxy: true)
assert_includes NetworkRange.by_country("US"), range1
assert_not_includes NetworkRange.by_country("US"), range2
assert_includes NetworkRange.by_company("Google"), range1
assert_not_includes NetworkRange.by_company("Google"), range2
assert_includes NetworkRange.by_asn(15169), range1
assert_not_includes NetworkRange.by_asn(15169), range2
assert_includes NetworkRange.datacenter, range1
assert_not_includes NetworkRange.datacenter, range2
assert_includes NetworkRange.proxy, range2
assert_not_includes NetworkRange.proxy, range1
end
# Class Methods
test "contains_ip class method finds most specific network" do
parent = NetworkRange.create!(network: "192.168.0.0/16")
child = NetworkRange.create!(network: "192.168.1.0/24")
found = NetworkRange.contains_ip("192.168.1.100")
assert_equal child, found.first # More specific should come first
end
test "overlapping class method finds overlapping networks" do
range1 = NetworkRange.create!(network: "192.168.0.0/16")
range2 = NetworkRange.create!(network: "192.168.1.0/24")
range3 = NetworkRange.create!(network: "10.0.0.0/8")
overlapping = NetworkRange.overlapping("192.168.1.0/24")
assert_includes overlapping, range1
assert_includes overlapping, range2
assert_not_includes overlapping, range3
end
test "find_or_create_by_cidr works correctly" do
# Creates new record
range = NetworkRange.find_or_create_by_cidr("10.0.0.0/8", user: @user, source: "manual")
assert range.persisted?
assert_equal "10.0.0.0/8", range.network.to_s
assert_equal @user, range.user
assert_equal "manual", range.source
# Returns existing record
existing = NetworkRange.find_or_create_by_cidr("10.0.0.0/8")
assert_equal range.id, existing.id
end
test "find_by_ip_or_network handles both IP and network inputs" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Find by IP within range
found_by_ip = NetworkRange.find_by_ip_or_network("192.168.1.100")
assert_includes found_by_ip, range
# Find by exact network
found_by_network = NetworkRange.find_by_ip_or_network("192.168.1.0/24")
assert_includes found_by_network, range
# Return none for invalid input
assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("")
assert_equal NetworkRange.none, NetworkRange.find_by_ip_or_network("invalid")
end
# Analytics Methods
test "has_events? correctly detects if network has events" do
range = NetworkRange.create!(network: "192.168.1.0/24")
assert_equal false, range.has_events?
# Create a test event in this network
Event.create!(
request_id: "test-1",
ip_address: "192.168.1.100",
network_range: range,
waf_action: 1,
request_method: 0,
response_status: 200
)
# Should now detect events exist
assert_equal true, range.has_events?
end
test "events method finds events within range" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Create test events
matching_event = Event.create!(
request_id: "test-1",
timestamp: Time.current,
payload: {},
ip_address: "192.168.1.100"
)
non_matching_event = Event.create!(
request_id: "test-2",
timestamp: Time.current,
payload: {},
ip_address: "10.0.0.1"
)
found_events = range.events
assert_includes found_events, matching_event
assert_not_includes found_events, non_matching_event
end
test "blocking_rules and active_rules work correctly" do
range = NetworkRange.create!(network: "192.168.1.0/24")
blocking_rule = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range,
user: @user,
enabled: true
)
allow_rule = Rule.create!(
rule_type: "network",
action: "allow",
network_range: range,
user: @user,
enabled: true
)
disabled_rule = Rule.create!(
rule_type: "network",
action: "deny",
network_range: range,
user: @user,
enabled: false
)
assert_includes range.blocking_rules, blocking_rule
assert_not_includes range.blocking_rules, allow_rule
assert_not_includes range.blocking_rules, disabled_rule
assert_includes range.active_rules, blocking_rule
assert_includes range.active_rules, allow_rule
assert_not_includes range.active_rules, disabled_rule
end
# Policy Evaluation
test "needs_policy_evaluation? works correctly" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Should evaluate if never evaluated
assert range.needs_policy_evaluation?
# Should evaluate if policies updated since last evaluation
range.update!(policies_evaluated_at: 1.hour.ago)
WafPolicy.create!(name: "Test Policy", policy_type: "country", targets: ["US"], policy_action: "deny", user: @user)
assert range.needs_policy_evaluation?
# Should not evaluate if up to date
range.update!(policies_evaluated_at: 5.minutes.ago)
assert_not range.needs_policy_evaluation?
end
# String Representations
test "to_s returns cidr" do
@ipv4_range.save!
assert_equal @ipv4_range.cidr, @ipv4_range.to_s
end
test "to_param parameterizes cidr" do
@ipv4_range.save!
assert_equal "192.168.1.0_24", @ipv4_range.to_param
end
# Geographic Lookup
test "geo_lookup_country! updates country when available" do
range = NetworkRange.create!(network: "8.8.8.0/24") # Google's network
# Mock GeoIpService
GeoIpService.expects(:lookup_country).with("8.8.8.0").returns("US")
range.geo_lookup_country!
assert_equal "US", range.reload.country
end
test "geo_lookup_country! handles errors gracefully" do
range = NetworkRange.create!(network: "192.168.1.0/24")
# Mock GeoIpService to raise error
GeoIpService.expects(:lookup_country).with("192.168.1.0").raises(StandardError.new("Service error"))
# Should not raise error but log it
assert_nothing_raised do
range.geo_lookup_country!
end
assert_nil range.reload.country
end
# Stats Methods
test "import_stats_by_source returns statistics" do
NetworkRange.create!(network: "10.0.0.0/8", source: "manual")
NetworkRange.create!(network: "192.168.1.0/24", source: "api_imported")
NetworkRange.create!(network: "172.16.0.0/12", source: "api_imported")
stats = NetworkRange.import_stats_by_source
assert_equal 2, stats.count
api_stats = stats.find { |s| s.source == "api_imported" }
assert_equal 2, api_stats.count
end
test "geolite_coverage_stats returns detailed coverage information" do
NetworkRange.create!(network: "10.0.0.0/8", source: "geolite_asn", asn: 12345)
NetworkRange.create!(network: "192.168.1.0/24", source: "geolite_country", country: "US")
NetworkRange.create!(network: "172.16.0.0/12", source: "geolite_asn", country: "BR")
stats = NetworkRange.geolite_coverage_stats
assert_equal 3, stats[:total_networks]
assert_equal 2, stats[:asn_networks]
assert_equal 1, stats[:country_networks]
assert_equal 2, stats[:with_asn_data]
assert_equal 1, stats[:with_country_data]
assert_equal 2, stats[:unique_countries]
assert_equal 2, stats[:unique_asns]
end
end