Fix some blocked/allow laggards after migrating. Add DuckDB for outstanding analyitcs performance. Start adding an import for all bot networks

This commit is contained in:
Dan Milne
2025-11-18 16:40:05 +11:00
parent ef56779584
commit 3f274c842c
37 changed files with 3522 additions and 151 deletions

134
test/services/ipapi_test.rb Normal file
View File

@@ -0,0 +1,134 @@
require "test_helper"
class IpapiTest < ActiveSupport::TestCase
def setup
@ipapi_data = JSON.parse(
File.read(Rails.root.join("test/fixtures/files/ipapi_91_84_96_0.json"))
)
end
test "parse_company_network_range extracts and converts IP range to CIDR" do
cidr = Ipapi.parse_company_network_range(@ipapi_data)
assert_equal "91.84.96.0/19", cidr
end
test "parse_company_network_range handles already formatted CIDR" do
data = { "company" => { "network" => "1.2.3.0/24" } }
cidr = Ipapi.parse_company_network_range(data)
assert_equal "1.2.3.0/24", cidr
end
test "parse_company_network_range returns nil for invalid range" do
data = { "company" => { "network" => "invalid" } }
cidr = Ipapi.parse_company_network_range(data)
assert_nil cidr
end
test "parse_company_network_range returns nil when no network data present" do
data = { "company" => {} }
cidr = Ipapi.parse_company_network_range(data)
assert_nil cidr
end
test "parse_company_network_range falls back to datacenter.network" do
data = { "datacenter" => { "network" => "1.2.3.0 - 1.2.3.255" } }
cidr = Ipapi.parse_company_network_range(data)
assert_equal "1.2.3.0/24", cidr
end
test "populate_network_attributes sets all network attributes" do
network_range = NetworkRange.new(network: "91.84.96.0/24")
Ipapi.populate_network_attributes(network_range, @ipapi_data)
assert_equal 216071, network_range.asn
assert_equal "SERVERS TECH FZCO", network_range.asn_org
assert_equal "SERVERS TECH FZCO", network_range.company
assert_equal "NL", network_range.country
assert network_range.is_datacenter
refute network_range.is_vpn
refute network_range.is_proxy
end
test "process_ipapi_data creates both company and BGP route networks" do
# Use a different tracking network so BGP route gets created
tracking_network = NetworkRange.create!(
network: "91.84.97.0/24",
source: "auto_generated"
)
assert_difference "NetworkRange.count", 2 do
result = Ipapi.process_ipapi_data(@ipapi_data, tracking_network)
assert_equal 2, result[:networks].length
assert_equal "91.84.96.0/19", result[:broadest_cidr]
end
# Verify company network was created
company_network = NetworkRange.find_by(network: "91.84.96.0/19")
assert_not_nil company_network
assert_equal "api_imported", company_network.source
assert_equal "SERVERS TECH FZCO", company_network.company
assert company_network.is_datacenter
# Verify BGP route network was created
bgp_network = NetworkRange.find_by(network: "91.84.96.0/24")
assert_not_nil bgp_network
assert_equal "SERVERS TECH FZCO", bgp_network.company
end
test "process_ipapi_data handles missing company network gracefully" do
# Create data without company network range
data = @ipapi_data.deep_dup
data["company"].delete("network")
data["datacenter"].delete("network")
tracking_network = NetworkRange.create!(
network: "91.84.96.0/24",
source: "auto_generated"
)
# Should only create the BGP route network (which matches tracking, so 0 new)
assert_no_difference "NetworkRange.count" do
result = Ipapi.process_ipapi_data(data, tracking_network)
assert_equal 0, result[:networks].length
assert_equal "91.84.96.0/24", result[:broadest_cidr]
end
end
test "process_ipapi_data updates existing networks instead of creating duplicates" do
# Pre-create both networks
company_network = NetworkRange.create!(
network: "91.84.96.0/19",
source: "manual",
company: "Old Company"
)
bgp_network = NetworkRange.create!(
network: "91.84.96.0/24",
source: "manual"
)
tracking_network = NetworkRange.create!(
network: "91.84.97.0/24",
source: "auto_generated"
)
# Should not create new networks, just update existing ones
assert_no_difference "NetworkRange.count" do
result = Ipapi.process_ipapi_data(@ipapi_data, tracking_network)
assert_equal 2, result[:networks].length
end
# Verify updates
company_network.reload
assert_equal "SERVERS TECH FZCO", company_network.company
assert company_network.network_data.key?("ipapi")
end
end

View File

@@ -0,0 +1,216 @@
# frozen_string_literal: true
require "test_helper"
class PathRuleMatcherTest < ActiveSupport::TestCase
setup do
@user = User.create!(email_address: "test@example.com", password: "password123")
# Create path segments
@admin_segment = PathSegment.find_or_create_segment("admin")
@wp_login_segment = PathSegment.find_or_create_segment("wp-login.php")
@api_segment = PathSegment.find_or_create_segment("api")
@v1_segment = PathSegment.find_or_create_segment("v1")
@users_segment = PathSegment.find_or_create_segment("users")
@dashboard_segment = PathSegment.find_or_create_segment("dashboard")
end
test "exact match - matches exact path only" do
rule = Rule.create_path_pattern_rule(
pattern: "/wp-login.php",
match_type: "exact",
action: "deny",
user: @user
)
# Create matching event
matching_event = create_event_with_segments([@wp_login_segment.id])
assert PathRuleMatcher.matches?(rule, matching_event), "Should match exact path"
# Create non-matching event (extra segment)
non_matching_event = create_event_with_segments([@admin_segment.id, @wp_login_segment.id])
refute PathRuleMatcher.matches?(rule, non_matching_event), "Should not match path with extra segments"
end
test "prefix match - matches paths starting with pattern" do
rule = Rule.create_path_pattern_rule(
pattern: "/admin",
match_type: "prefix",
action: "deny",
user: @user
)
# Should match /admin
event1 = create_event_with_segments([@admin_segment.id])
assert PathRuleMatcher.matches?(rule, event1), "Should match exact prefix"
# Should match /admin/dashboard
event2 = create_event_with_segments([@admin_segment.id, @dashboard_segment.id])
assert PathRuleMatcher.matches?(rule, event2), "Should match prefix with additional segments"
# Should match /admin/users/123
event3 = create_event_with_segments([@admin_segment.id, @users_segment.id, create_segment("123").id])
assert PathRuleMatcher.matches?(rule, event3), "Should match prefix with multiple additional segments"
# Should NOT match /api/admin (admin not at start)
event4 = create_event_with_segments([@api_segment.id, @admin_segment.id])
refute PathRuleMatcher.matches?(rule, event4), "Should not match when pattern not at start"
end
test "suffix match - matches paths ending with pattern" do
rule = Rule.create_path_pattern_rule(
pattern: "/wp-login.php",
match_type: "suffix",
action: "deny",
user: @user
)
# Should match /wp-login.php
event1 = create_event_with_segments([@wp_login_segment.id])
assert PathRuleMatcher.matches?(rule, event1), "Should match exact suffix"
# Should match /admin/wp-login.php
event2 = create_event_with_segments([@admin_segment.id, @wp_login_segment.id])
assert PathRuleMatcher.matches?(rule, event2), "Should match suffix with prefix segments"
# Should match /backup/admin/wp-login.php
backup_seg = create_segment("backup")
event3 = create_event_with_segments([backup_seg.id, @admin_segment.id, @wp_login_segment.id])
assert PathRuleMatcher.matches?(rule, event3), "Should match suffix with multiple prefix segments"
# Should NOT match /wp-login.php/test (suffix has extra segment)
test_seg = create_segment("test")
event4 = create_event_with_segments([@wp_login_segment.id, test_seg.id])
refute PathRuleMatcher.matches?(rule, event4), "Should not match when pattern not at end"
end
test "contains match - matches paths containing pattern" do
rule = Rule.create_path_pattern_rule(
pattern: "/admin",
match_type: "contains",
action: "deny",
user: @user
)
# Should match /admin
event1 = create_event_with_segments([@admin_segment.id])
assert PathRuleMatcher.matches?(rule, event1), "Should match exact contains"
# Should match /api/admin/users
event2 = create_event_with_segments([@api_segment.id, @admin_segment.id, @users_segment.id])
assert PathRuleMatcher.matches?(rule, event2), "Should match contains in middle"
# Should match /super/secret/admin/panel
super_seg = create_segment("super")
secret_seg = create_segment("secret")
panel_seg = create_segment("panel")
event3 = create_event_with_segments([super_seg.id, secret_seg.id, @admin_segment.id, panel_seg.id])
assert PathRuleMatcher.matches?(rule, event3), "Should match contains with prefix and suffix"
# Should NOT match /administrator (different segment)
administrator_seg = create_segment("administrator")
event4 = create_event_with_segments([administrator_seg.id])
refute PathRuleMatcher.matches?(rule, event4), "Should not match different segment"
end
test "contains match with multi-segment pattern" do
rule = Rule.create_path_pattern_rule(
pattern: "/api/admin",
match_type: "contains",
action: "deny",
user: @user
)
# Should match /api/admin
event1 = create_event_with_segments([@api_segment.id, @admin_segment.id])
assert PathRuleMatcher.matches?(rule, event1), "Should match exact contains"
# Should match /v1/api/admin/users
event2 = create_event_with_segments([@v1_segment.id, @api_segment.id, @admin_segment.id, @users_segment.id])
assert PathRuleMatcher.matches?(rule, event2), "Should match consecutive segments in middle"
# Should NOT match /api/v1/admin (segments not consecutive)
event3 = create_event_with_segments([@api_segment.id, @v1_segment.id, @admin_segment.id])
refute PathRuleMatcher.matches?(rule, event3), "Should not match non-consecutive segments"
end
test "case insensitive matching through PathSegment normalization" do
# PathSegment.find_or_create_segment normalizes to lowercase
rule = Rule.create_path_pattern_rule(
pattern: "/Admin/Users", # Mixed case
match_type: "exact",
action: "deny",
user: @user
)
# Event with lowercase path should match
event = create_event_with_segments([@admin_segment.id, @users_segment.id])
assert PathRuleMatcher.matches?(rule, event), "Should match case-insensitively"
end
test "matching_rules returns all matching rules" do
rule1 = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "prefix", action: "deny", user: @user)
rule2 = Rule.create_path_pattern_rule(pattern: "/admin/users", match_type: "exact", action: "allow", user: @user)
rule3 = Rule.create_path_pattern_rule(pattern: "/api", match_type: "prefix", action: "deny", user: @user)
event = create_event_with_segments([@admin_segment.id, @users_segment.id])
matching = PathRuleMatcher.matching_rules(event)
assert_includes matching, rule1, "Should include prefix rule"
assert_includes matching, rule2, "Should include exact rule"
refute_includes matching, rule3, "Should not include non-matching rule"
end
test "evaluate returns first matching action" do
Rule.create_path_pattern_rule(pattern: "/admin", match_type: "prefix", action: "deny", user: @user)
event = create_event_with_segments([@admin_segment.id, @dashboard_segment.id])
action = PathRuleMatcher.evaluate(event)
assert_equal "deny", action, "Should return deny action"
end
test "evaluate returns allow for non-matching event" do
Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user)
event = create_event_with_segments([@api_segment.id])
action = PathRuleMatcher.evaluate(event)
assert_equal "allow", action, "Should return allow for non-matching event"
end
test "does not match disabled rules" do
rule = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user)
rule.update!(enabled: false)
event = create_event_with_segments([@admin_segment.id])
matching = PathRuleMatcher.matching_rules(event)
assert_empty matching, "Should not match disabled rules"
end
test "does not match expired rules" do
rule = Rule.create_path_pattern_rule(pattern: "/admin", match_type: "exact", action: "deny", user: @user)
rule.update!(expires_at: 1.hour.ago)
event = create_event_with_segments([@admin_segment.id])
matching = PathRuleMatcher.matching_rules(event)
assert_empty matching, "Should not match expired rules"
end
private
def create_event_with_segments(segment_ids)
Event.new(
request_id: SecureRandom.uuid,
timestamp: Time.current,
request_segment_ids: segment_ids,
ip_address: "1.2.3.4"
)
end
def create_segment(text)
PathSegment.find_or_create_segment(text)
end
end