Accepts incoming events and correctly parses them into events. GeoLite2 integration complete"
This commit is contained in:
292
test/models/event_test.rb
Normal file
292
test/models/event_test.rb
Normal file
@@ -0,0 +1,292 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class EventTest < ActiveSupport::TestCase
|
||||
def setup
|
||||
@project = Project.create!(name: "Test Project", slug: "test-project")
|
||||
@sample_payload = {
|
||||
"event_id" => "test-event-123",
|
||||
"timestamp" => Time.now.iso8601,
|
||||
"request" => {
|
||||
"ip" => "192.168.1.1",
|
||||
"method" => "GET",
|
||||
"path" => "/api/test",
|
||||
"headers" => {
|
||||
"host" => "example.com",
|
||||
"user-agent" => "TestAgent/1.0",
|
||||
"content-type" => "application/json"
|
||||
},
|
||||
"query" => { "param" => "value" }
|
||||
},
|
||||
"response" => {
|
||||
"status_code" => 200,
|
||||
"duration_ms" => 150,
|
||||
"size" => 1024
|
||||
},
|
||||
"waf_action" => "allow",
|
||||
"server_name" => "test-server",
|
||||
"environment" => "test",
|
||||
"geo" => {
|
||||
"country_code" => "US",
|
||||
"city" => "Test City"
|
||||
},
|
||||
"tags" => { "source" => "test" },
|
||||
"agent" => {
|
||||
"name" => "baffle-agent",
|
||||
"version" => "1.0.0"
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def teardown
|
||||
Event.delete_all # Delete events first to avoid foreign key constraints
|
||||
Project.delete_all
|
||||
end
|
||||
|
||||
test "create_from_waf_payload! creates event with proper enum values" do
|
||||
event = Event.create_from_waf_payload!("test-123", @sample_payload, @project)
|
||||
|
||||
assert event.persisted?
|
||||
assert_equal @project, event.project
|
||||
assert_equal "test-123", event.event_id
|
||||
assert_equal "192.168.1.1", event.ip_address
|
||||
assert_equal "/api/test", event.request_path
|
||||
assert_equal 200, event.response_status
|
||||
assert_equal 150, event.response_time_ms
|
||||
assert_equal "test-server", event.server_name
|
||||
assert_equal "test", event.environment
|
||||
assert_equal "US", event.country_code
|
||||
assert_equal "Test City", event.city
|
||||
assert_equal "baffle-agent", event.agent_name
|
||||
assert_equal "1.0.0", event.agent_version
|
||||
end
|
||||
|
||||
test "create_from_waf_payload! properly normalizes request_method enum" do
|
||||
test_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
||||
expected_enums = [:get, :post, :put, :patch, :delete, :head, :options]
|
||||
|
||||
test_methods.each_with_index do |method, index|
|
||||
payload = @sample_payload.dup
|
||||
payload["request"]["method"] = method
|
||||
payload["event_id"] = "test-method-#{method.downcase}"
|
||||
|
||||
event = Event.create_from_waf_payload!("test-method-#{method.downcase}", payload, @project)
|
||||
|
||||
assert_equal expected_enums[index].to_s, event.request_method,
|
||||
"Method #{method} should map to enum #{expected_enums[index]}"
|
||||
assert_equal index, event.request_method_before_type_cast,
|
||||
"Method #{method} should be stored as integer #{index}"
|
||||
end
|
||||
end
|
||||
|
||||
test "create_from_waf_payload! properly normalizes waf_action enum" do
|
||||
test_actions = [
|
||||
["allow", :allow, 0],
|
||||
["pass", :allow, 0],
|
||||
["deny", :deny, 1],
|
||||
["block", :deny, 1],
|
||||
["redirect", :redirect, 2],
|
||||
["challenge", :challenge, 3],
|
||||
["unknown", :allow, 0] # Default fallback
|
||||
]
|
||||
|
||||
test_actions.each do |action, expected_enum, expected_int|
|
||||
payload = @sample_payload.dup
|
||||
payload["waf_action"] = action
|
||||
payload["event_id"] = "test-action-#{action}"
|
||||
|
||||
event = Event.create_from_waf_payload!("test-action-#{action}", payload, @project)
|
||||
|
||||
assert_equal expected_enum.to_s, event.waf_action,
|
||||
"Action #{action} should map to enum #{expected_enum}"
|
||||
assert_equal expected_int, event.waf_action_before_type_cast,
|
||||
"Action #{action} should be stored as integer #{expected_int}"
|
||||
end
|
||||
end
|
||||
|
||||
test "create_from_waf_payload! handles header case normalization" do
|
||||
payload = @sample_payload.dup
|
||||
payload["request"]["headers"] = {
|
||||
"HOST" => "EXAMPLE.COM",
|
||||
"User-Agent" => "TestAgent/1.0",
|
||||
"CONTENT-TYPE" => "application/json"
|
||||
}
|
||||
|
||||
event = Event.create_from_waf_payload!("test-headers", payload, @project)
|
||||
|
||||
assert_equal "TestAgent/1.0", event.user_agent
|
||||
# The normalize_payload_headers method should normalize header keys to lowercase
|
||||
# but keep values as-is
|
||||
assert_equal "EXAMPLE.COM", event.headers["host"]
|
||||
assert_equal "application/json", event.headers["content-type"]
|
||||
end
|
||||
|
||||
test "enum values persist after save and reload" do
|
||||
event = Event.create_from_waf_payload!("test-persist", @sample_payload, @project)
|
||||
|
||||
# Verify initial values
|
||||
assert_equal "get", event.request_method
|
||||
assert_equal "allow", event.waf_action
|
||||
assert_equal 0, event.request_method_before_type_cast
|
||||
assert_equal 0, event.waf_action_before_type_cast
|
||||
|
||||
# Reload from database
|
||||
event.reload
|
||||
|
||||
# Values should still be correct
|
||||
assert_equal "get", event.request_method
|
||||
assert_equal "allow", event.waf_action
|
||||
assert_equal 0, event.request_method_before_type_cast
|
||||
assert_equal 0, event.waf_action_before_type_cast
|
||||
end
|
||||
|
||||
test "enum scopes work correctly" do
|
||||
# Create events with different methods and actions
|
||||
Event.create_from_waf_payload!("get-allow", @sample_payload, @project)
|
||||
|
||||
post_payload = @sample_payload.dup
|
||||
post_payload["request"]["method"] = "POST"
|
||||
post_payload["event_id"] = "post-allow"
|
||||
Event.create_from_waf_payload!("post-allow", post_payload, @project)
|
||||
|
||||
deny_payload = @sample_payload.dup
|
||||
deny_payload["waf_action"] = "deny"
|
||||
deny_payload["event_id"] = "get-deny"
|
||||
Event.create_from_waf_payload!("get-deny", deny_payload, @project)
|
||||
|
||||
# Test method scopes - use string values for enum queries
|
||||
get_events = Event.where(request_method: "get")
|
||||
post_events = Event.where(request_method: "post")
|
||||
|
||||
assert_equal 2, get_events.count
|
||||
assert_equal 1, post_events.count
|
||||
|
||||
# Test action scopes - use string values for enum queries
|
||||
allowed_events = Event.where(waf_action: "allow")
|
||||
denied_events = Event.where(waf_action: "deny")
|
||||
|
||||
assert_equal 2, allowed_events.count
|
||||
assert_equal 1, denied_events.count
|
||||
end
|
||||
|
||||
test "event normalization is triggered when needed" do
|
||||
# Create event without enum values (simulating old data)
|
||||
event = Event.create!(
|
||||
project: @project,
|
||||
event_id: "normalization-test",
|
||||
timestamp: Time.current,
|
||||
payload: @sample_payload,
|
||||
ip_address: "192.168.1.1",
|
||||
request_path: "/test",
|
||||
# Don't set request_method or waf_action to trigger normalization
|
||||
request_method: nil,
|
||||
waf_action: nil
|
||||
)
|
||||
|
||||
# Manually set the raw values that would normally be extracted
|
||||
event.instance_variable_set(:@raw_request_method, "POST")
|
||||
event.instance_variable_set(:@raw_action, "deny")
|
||||
|
||||
# Trigger normalization
|
||||
event.send(:normalize_event_fields)
|
||||
event.save!
|
||||
|
||||
# Verify normalization worked
|
||||
event.reload
|
||||
assert_equal "post", event.request_method
|
||||
assert_equal "deny", event.waf_action
|
||||
assert_equal 1, event.request_method_before_type_cast # POST = 1
|
||||
assert_equal 1, event.waf_action_before_type_cast # DENY = 1
|
||||
end
|
||||
|
||||
test "payload extraction methods work correctly" do
|
||||
event = Event.create_from_waf_payload!("extraction-test", @sample_payload, @project)
|
||||
|
||||
# Test request_details
|
||||
request_details = event.request_details
|
||||
assert_equal "192.168.1.1", request_details[:ip]
|
||||
assert_equal "GET", request_details[:method]
|
||||
assert_equal "/api/test", request_details[:path]
|
||||
assert_equal "example.com", request_details[:headers]["host"]
|
||||
|
||||
# Test response_details
|
||||
response_details = event.response_details
|
||||
assert_equal 200, response_details[:status_code]
|
||||
assert_equal 150, response_details[:duration_ms]
|
||||
assert_equal 1024, response_details[:size]
|
||||
|
||||
# Test geo_details
|
||||
geo_details = event.geo_details
|
||||
assert_equal "US", geo_details["country_code"]
|
||||
assert_equal "Test City", geo_details["city"]
|
||||
|
||||
# Test tags
|
||||
tags = event.tags
|
||||
assert_equal "test", tags["source"]
|
||||
end
|
||||
|
||||
test "helper methods work correctly" do
|
||||
event = Event.create_from_waf_payload!("helper-test", @sample_payload, @project)
|
||||
|
||||
# Test boolean methods
|
||||
assert event.allowed?
|
||||
assert_not event.blocked?
|
||||
assert_not event.rate_limited?
|
||||
assert_not event.challenged?
|
||||
assert_not event.rule_matched?
|
||||
|
||||
# Test path methods
|
||||
assert_equal ["api", "test"], event.path_segments
|
||||
assert_equal 2, event.path_depth
|
||||
end
|
||||
|
||||
test "timestamp parsing works with various formats" do
|
||||
timestamps = [
|
||||
Time.now.iso8601,
|
||||
(Time.now.to_f * 1000).to_i, # Unix timestamp in milliseconds
|
||||
Time.now.utc # Time object
|
||||
]
|
||||
|
||||
timestamps.each_with_index do |timestamp, index|
|
||||
payload = @sample_payload.dup
|
||||
payload["timestamp"] = timestamp
|
||||
payload["event_id"] = "timestamp-test-#{index}"
|
||||
|
||||
event = Event.create_from_waf_payload!("timestamp-test-#{index}", payload, @project)
|
||||
assert event.timestamp.is_a?(Time), "Timestamp #{index} should be parsed as Time"
|
||||
assert_not event.timestamp.nil?
|
||||
end
|
||||
end
|
||||
|
||||
test "handles missing optional fields gracefully" do
|
||||
minimal_payload = {
|
||||
"event_id" => "minimal-test",
|
||||
"timestamp" => Time.now.iso8601,
|
||||
"request" => {
|
||||
"ip" => "10.0.0.1",
|
||||
"method" => "GET",
|
||||
"path" => "/simple"
|
||||
},
|
||||
"response" => {
|
||||
"status_code" => 404
|
||||
}
|
||||
}
|
||||
|
||||
event = Event.create_from_waf_payload!("minimal-test", minimal_payload, @project)
|
||||
|
||||
assert event.persisted?
|
||||
assert_equal "10.0.0.1", event.ip_address
|
||||
assert_equal "get", event.request_method
|
||||
assert_equal "/simple", event.request_path
|
||||
assert_equal 404, event.response_status
|
||||
|
||||
# Optional fields should be nil
|
||||
assert_nil event.user_agent
|
||||
assert_nil event.response_time_ms
|
||||
assert_nil event.country_code
|
||||
assert_nil event.city
|
||||
assert_nil event.agent_name
|
||||
assert_nil event.agent_version
|
||||
end
|
||||
end
|
||||
122
test/models/ipv4_range_test.rb
Normal file
122
test/models/ipv4_range_test.rb
Normal file
@@ -0,0 +1,122 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class Ipv4RangeTest < ActiveSupport::TestCase
|
||||
test "creates range from CIDR notation" do
|
||||
range = Ipv4Range.create!(cidr: "192.168.1.0/24")
|
||||
|
||||
assert_equal 24, range.network_prefix
|
||||
assert range.network_start.present?
|
||||
assert range.network_end.present?
|
||||
assert range.network_start < range.network_end
|
||||
end
|
||||
|
||||
test "calculates correct range for /32 single IP" do
|
||||
range = Ipv4Range.create!(cidr: "192.168.1.100/32")
|
||||
|
||||
assert_equal 32, range.network_prefix
|
||||
assert_equal range.network_start, range.network_end
|
||||
end
|
||||
|
||||
test "calculates correct range for /8 large network" do
|
||||
range = Ipv4Range.create!(cidr: "10.0.0.0/8")
|
||||
|
||||
assert_equal 8, range.network_prefix
|
||||
|
||||
# 10.0.0.0 to 10.255.255.255
|
||||
ip_start = IPAddr.new("10.0.0.0").to_i
|
||||
ip_end = IPAddr.new("10.255.255.255").to_i
|
||||
|
||||
assert_equal ip_start, range.network_start
|
||||
assert_equal ip_end, range.network_end
|
||||
end
|
||||
|
||||
test "validates network_prefix range" do
|
||||
range = Ipv4Range.new(cidr: "192.168.1.0/24")
|
||||
range.network_prefix = 33
|
||||
|
||||
assert_not range.valid?
|
||||
assert_includes range.errors[:network_prefix], "must be less than or equal to 32"
|
||||
end
|
||||
|
||||
test "contains_ip? returns true for IP in range" do
|
||||
range = Ipv4Range.create!(cidr: "192.168.1.0/24")
|
||||
|
||||
assert range.contains_ip?("192.168.1.1")
|
||||
assert range.contains_ip?("192.168.1.100")
|
||||
assert range.contains_ip?("192.168.1.255")
|
||||
end
|
||||
|
||||
test "contains_ip? returns false for IP outside range" do
|
||||
range = Ipv4Range.create!(cidr: "192.168.1.0/24")
|
||||
|
||||
assert_not range.contains_ip?("192.168.2.1")
|
||||
assert_not range.contains_ip?("10.0.0.1")
|
||||
end
|
||||
|
||||
test "contains_ip class method finds matching ranges" do
|
||||
range1 = Ipv4Range.create!(cidr: "10.0.0.0/8")
|
||||
range2 = Ipv4Range.create!(cidr: "192.168.1.0/24")
|
||||
|
||||
results = Ipv4Range.contains_ip("10.5.10.50")
|
||||
|
||||
assert_includes results, range1
|
||||
assert_not_includes results, range2
|
||||
end
|
||||
|
||||
test "contains_ip returns most specific range first" do
|
||||
broad_range = Ipv4Range.create!(cidr: "10.0.0.0/8")
|
||||
specific_range = Ipv4Range.create!(cidr: "10.0.1.0/24")
|
||||
|
||||
results = Ipv4Range.contains_ip("10.0.1.50")
|
||||
|
||||
assert_equal specific_range, results.first
|
||||
end
|
||||
|
||||
test "to_cidr returns CIDR notation" do
|
||||
range = Ipv4Range.create!(cidr: "192.168.1.0/24")
|
||||
|
||||
assert_equal "192.168.1.0/24", range.to_cidr
|
||||
end
|
||||
|
||||
test "datacenter scope returns datacenter IPs" do
|
||||
datacenter = Ipv4Range.create!(cidr: "1.2.3.0/24", is_datacenter: true)
|
||||
regular = Ipv4Range.create!(cidr: "192.168.1.0/24", is_datacenter: false)
|
||||
|
||||
results = Ipv4Range.datacenter
|
||||
|
||||
assert_includes results, datacenter
|
||||
assert_not_includes results, regular
|
||||
end
|
||||
|
||||
test "stores and retrieves JSON metadata" do
|
||||
range = Ipv4Range.create!(cidr: "1.2.3.0/24")
|
||||
range.abuser_scores_hash = { "spam" => 0.8, "malware" => 0.3 }
|
||||
range.save!
|
||||
|
||||
range.reload
|
||||
scores = range.abuser_scores_hash
|
||||
|
||||
assert_equal 0.8, scores["spam"]
|
||||
assert_equal 0.3, scores["malware"]
|
||||
end
|
||||
|
||||
test "stores IP intelligence metadata" do
|
||||
range = Ipv4Range.create!(
|
||||
cidr: "1.2.3.0/24",
|
||||
company: "Example Corp",
|
||||
asn: 12345,
|
||||
asn_org: "AS Example",
|
||||
is_datacenter: true,
|
||||
is_proxy: false,
|
||||
is_vpn: false,
|
||||
ip_api_country: "US"
|
||||
)
|
||||
|
||||
assert_equal "Example Corp", range.company
|
||||
assert_equal 12345, range.asn
|
||||
assert range.is_datacenter?
|
||||
assert_not range.is_proxy?
|
||||
end
|
||||
end
|
||||
107
test/models/ipv6_range_test.rb
Normal file
107
test/models/ipv6_range_test.rb
Normal file
@@ -0,0 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class Ipv6RangeTest < ActiveSupport::TestCase
|
||||
test "creates range from CIDR notation" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
|
||||
assert_equal 32, range.network_prefix
|
||||
assert range.network_start.present?
|
||||
assert range.network_end.present?
|
||||
end
|
||||
|
||||
test "calculates correct range for /128 single IP" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::1/128")
|
||||
|
||||
assert_equal 128, range.network_prefix
|
||||
assert_equal range.network_start, range.network_end
|
||||
end
|
||||
|
||||
test "validates network_prefix range" do
|
||||
range = Ipv6Range.new(cidr: "2001:db8::/32")
|
||||
range.network_prefix = 129
|
||||
|
||||
assert_not range.valid?
|
||||
assert_includes range.errors[:network_prefix], "must be less than or equal to 128"
|
||||
end
|
||||
|
||||
test "contains_ip? returns true for IP in range" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
|
||||
assert range.contains_ip?("2001:db8::1")
|
||||
assert range.contains_ip?("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff")
|
||||
end
|
||||
|
||||
test "contains_ip? returns false for IP outside range" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
|
||||
assert_not range.contains_ip?("2001:db9::1")
|
||||
assert_not range.contains_ip?("fe80::1")
|
||||
end
|
||||
|
||||
test "contains_ip class method finds matching ranges" do
|
||||
range1 = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
range2 = Ipv6Range.create!(cidr: "fe80::/10")
|
||||
|
||||
results = Ipv6Range.contains_ip("2001:db8::1")
|
||||
|
||||
assert_includes results, range1
|
||||
assert_not_includes results, range2
|
||||
end
|
||||
|
||||
test "contains_ip returns most specific range first" do
|
||||
broad_range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
specific_range = Ipv6Range.create!(cidr: "2001:db8:1::/48")
|
||||
|
||||
results = Ipv6Range.contains_ip("2001:db8:1::5")
|
||||
|
||||
assert_equal specific_range, results.first
|
||||
end
|
||||
|
||||
test "to_cidr returns CIDR notation" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
|
||||
# IPv6 addresses can be formatted differently
|
||||
assert range.to_cidr.include?("2001:db8")
|
||||
assert range.to_cidr.include?("/32")
|
||||
end
|
||||
|
||||
test "datacenter scope returns datacenter IPs" do
|
||||
datacenter = Ipv6Range.create!(cidr: "2001:db8::/32", is_datacenter: true)
|
||||
regular = Ipv6Range.create!(cidr: "fe80::/10", is_datacenter: false)
|
||||
|
||||
results = Ipv6Range.datacenter
|
||||
|
||||
assert_includes results, datacenter
|
||||
assert_not_includes results, regular
|
||||
end
|
||||
|
||||
test "stores and retrieves JSON metadata" do
|
||||
range = Ipv6Range.create!(cidr: "2001:db8::/32")
|
||||
range.additional_data_hash = { "notes" => "Test network", "verified" => true }
|
||||
range.save!
|
||||
|
||||
range.reload
|
||||
data = range.additional_data_hash
|
||||
|
||||
assert_equal "Test network", data["notes"]
|
||||
assert_equal true, data["verified"]
|
||||
end
|
||||
|
||||
test "stores IP intelligence metadata" do
|
||||
range = Ipv6Range.create!(
|
||||
cidr: "2001:db8::/32",
|
||||
company: "IPv6 Corp",
|
||||
asn: 65000,
|
||||
asn_org: "AS IPv6",
|
||||
is_proxy: true,
|
||||
ip_api_country: "GB"
|
||||
)
|
||||
|
||||
assert_equal "IPv6 Corp", range.company
|
||||
assert_equal 65000, range.asn
|
||||
assert range.is_proxy?
|
||||
assert_equal "GB", range.ip_api_country
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class NetworkRangeTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
end
|
||||
@@ -1,7 +1,179 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class RuleTest < ActiveSupport::TestCase
|
||||
# test "the truth" do
|
||||
# assert true
|
||||
# end
|
||||
# Validation tests
|
||||
test "should create valid network_v4 rule" do
|
||||
rule = Rule.new(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" },
|
||||
source: "manual"
|
||||
)
|
||||
assert rule.valid?
|
||||
rule.save!
|
||||
assert_equal 8, rule.priority # Auto-calculated from CIDR prefix
|
||||
end
|
||||
|
||||
test "should create valid network_v6 rule" do
|
||||
rule = Rule.new(
|
||||
rule_type: "network_v6",
|
||||
action: "deny",
|
||||
conditions: { cidr: "2001:db8::/32" },
|
||||
source: "manual"
|
||||
)
|
||||
assert rule.valid?
|
||||
rule.save!
|
||||
assert_equal 32, rule.priority
|
||||
end
|
||||
|
||||
test "should create valid rate_limit rule" do
|
||||
rule = Rule.new(
|
||||
rule_type: "rate_limit",
|
||||
action: "rate_limit",
|
||||
conditions: { cidr: "0.0.0.0/0", scope: "global" },
|
||||
metadata: { limit: 100, window: 60 },
|
||||
source: "manual"
|
||||
)
|
||||
assert rule.valid?
|
||||
end
|
||||
|
||||
test "should create valid path_pattern rule" do
|
||||
rule = Rule.new(
|
||||
rule_type: "path_pattern",
|
||||
action: "log",
|
||||
conditions: { patterns: ["/.env", "/.git"] },
|
||||
source: "default"
|
||||
)
|
||||
assert rule.valid?
|
||||
end
|
||||
|
||||
test "should require rule_type" do
|
||||
rule = Rule.new(action: "deny", conditions: { cidr: "10.0.0.0/8" })
|
||||
assert_not rule.valid?
|
||||
assert_includes rule.errors[:rule_type], "can't be blank"
|
||||
end
|
||||
|
||||
test "should require action" do
|
||||
rule = Rule.new(rule_type: "network_v4", conditions: { cidr: "10.0.0.0/8" })
|
||||
assert_not rule.valid?
|
||||
assert_includes rule.errors[:action], "can't be blank"
|
||||
end
|
||||
|
||||
test "should validate network_v4 has valid IPv4 CIDR" do
|
||||
rule = Rule.new(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "2001:db8::/32" } # IPv6 in IPv4 rule
|
||||
)
|
||||
assert_not rule.valid?
|
||||
assert_includes rule.errors[:conditions], "cidr must be IPv4 for network_v4 rules"
|
||||
end
|
||||
|
||||
test "should validate rate_limit has limit and window in metadata" do
|
||||
rule = Rule.new(
|
||||
rule_type: "rate_limit",
|
||||
action: "rate_limit",
|
||||
conditions: { cidr: "0.0.0.0/0", scope: "global" },
|
||||
metadata: { limit: 100 } # Missing window
|
||||
)
|
||||
assert_not rule.valid?
|
||||
assert_includes rule.errors[:metadata], "must include 'limit' and 'window' for rate_limit rules"
|
||||
end
|
||||
|
||||
# Default value tests
|
||||
test "should default enabled to true" do
|
||||
rule = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" }
|
||||
)
|
||||
assert rule.enabled?
|
||||
end
|
||||
|
||||
# Priority calculation tests
|
||||
test "should calculate priority from IPv4 CIDR prefix" do
|
||||
rule = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "192.168.1.0/24" }
|
||||
)
|
||||
assert_equal 24, rule.priority
|
||||
end
|
||||
|
||||
# Scope tests
|
||||
test "active scope returns enabled and non-expired rules" do
|
||||
active = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" },
|
||||
enabled: true
|
||||
)
|
||||
|
||||
disabled = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "192.168.0.0/16" },
|
||||
enabled: false
|
||||
)
|
||||
|
||||
expired = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "172.16.0.0/12" },
|
||||
enabled: true,
|
||||
expires_at: 1.hour.ago
|
||||
)
|
||||
|
||||
results = Rule.active.to_a
|
||||
assert_includes results, active
|
||||
assert_not_includes results, disabled
|
||||
assert_not_includes results, expired
|
||||
end
|
||||
|
||||
# Instance method tests
|
||||
test "active? returns true for enabled non-expired rule" do
|
||||
rule = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" },
|
||||
enabled: true
|
||||
)
|
||||
assert rule.active?
|
||||
end
|
||||
|
||||
test "disable! sets enabled to false and adds metadata" do
|
||||
rule = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" }
|
||||
)
|
||||
|
||||
rule.disable!(reason: "False positive")
|
||||
|
||||
assert_not rule.enabled?
|
||||
assert_equal "False positive", rule.metadata["disabled_reason"]
|
||||
assert rule.metadata["disabled_at"].present?
|
||||
end
|
||||
|
||||
# Agent format tests
|
||||
test "to_agent_format returns correct structure" do
|
||||
rule = Rule.create!(
|
||||
rule_type: "network_v4",
|
||||
action: "deny",
|
||||
conditions: { cidr: "10.0.0.0/8" },
|
||||
expires_at: 1.day.from_now,
|
||||
source: "manual",
|
||||
metadata: { reason: "Test" }
|
||||
)
|
||||
|
||||
format = rule.to_agent_format
|
||||
|
||||
assert_equal rule.id, format[:id]
|
||||
assert_equal "network_v4", format[:rule_type]
|
||||
assert_equal "deny", format[:action]
|
||||
assert_equal 8, format[:priority]
|
||||
assert_equal true, format[:enabled]
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user