190 lines
5.4 KiB
Ruby
190 lines
5.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Rule < ApplicationRecord
|
|
# Rule types for the new architecture
|
|
RULE_TYPES = %w[network_v4 network_v6 rate_limit path_pattern].freeze
|
|
ACTIONS = %w[allow deny rate_limit redirect log].freeze
|
|
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default].freeze
|
|
|
|
# Validations
|
|
validates :rule_type, presence: true, inclusion: { in: RULE_TYPES }
|
|
validates :action, presence: true, inclusion: { in: ACTIONS }
|
|
validates :conditions, presence: true
|
|
validates :enabled, inclusion: { in: [true, false] }
|
|
|
|
# Custom validations based on rule type
|
|
validate :validate_conditions_by_type
|
|
validate :validate_metadata_by_action
|
|
|
|
# Scopes
|
|
scope :enabled, -> { where(enabled: true) }
|
|
scope :disabled, -> { where(enabled: false) }
|
|
scope :active, -> { enabled.where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
|
scope :expired, -> { where("expires_at IS NOT NULL AND expires_at <= ?", Time.current) }
|
|
scope :by_type, ->(type) { where(rule_type: type) }
|
|
scope :network_rules, -> { where(rule_type: ["network_v4", "network_v6"]) }
|
|
scope :rate_limit_rules, -> { where(rule_type: "rate_limit") }
|
|
scope :path_pattern_rules, -> { where(rule_type: "path_pattern") }
|
|
scope :by_source, ->(source) { where(source: source) }
|
|
|
|
# Sync queries (ordered by updated_at for incremental sync)
|
|
scope :since, ->(timestamp) { where("updated_at >= ?", timestamp - 0.5.seconds).order(:updated_at, :id) }
|
|
scope :sync_order, -> { order(:updated_at, :id) }
|
|
|
|
# Callbacks
|
|
before_validation :set_defaults
|
|
before_save :calculate_priority_from_cidr
|
|
|
|
# Check if rule is currently active
|
|
def active?
|
|
enabled? && !expired?
|
|
end
|
|
|
|
def expired?
|
|
expires_at.present? && expires_at <= Time.current
|
|
end
|
|
|
|
# Convert to format for agent consumption
|
|
def to_agent_format
|
|
{
|
|
id: id,
|
|
rule_type: rule_type,
|
|
action: action,
|
|
conditions: conditions || {},
|
|
priority: priority,
|
|
expires_at: expires_at&.iso8601,
|
|
enabled: enabled,
|
|
source: source,
|
|
metadata: metadata || {},
|
|
created_at: created_at.iso8601,
|
|
updated_at: updated_at.iso8601
|
|
}
|
|
end
|
|
|
|
# Class method to get latest version (for sync cursor)
|
|
def self.latest_version
|
|
maximum(:updated_at)&.iso8601(6) || Time.current.iso8601(6)
|
|
end
|
|
|
|
# Disable rule (soft delete)
|
|
def disable!(reason: nil)
|
|
update!(
|
|
enabled: false,
|
|
metadata: (metadata || {}).merge(
|
|
disabled_at: Time.current.iso8601,
|
|
disabled_reason: reason
|
|
)
|
|
)
|
|
end
|
|
|
|
# Enable rule
|
|
def enable!
|
|
update!(enabled: true)
|
|
end
|
|
|
|
# Check if this is a network rule
|
|
def network_rule?
|
|
rule_type.in?(%w[network_v4 network_v6])
|
|
end
|
|
|
|
# Get CIDR from conditions (for network rules)
|
|
def cidr
|
|
conditions&.dig("cidr") if network_rule?
|
|
end
|
|
|
|
# Get prefix length from CIDR
|
|
def prefix_length
|
|
return nil unless cidr
|
|
cidr.split("/").last.to_i
|
|
end
|
|
|
|
private
|
|
|
|
def set_defaults
|
|
self.enabled = true if enabled.nil?
|
|
self.conditions ||= {}
|
|
self.metadata ||= {}
|
|
self.source ||= "manual"
|
|
end
|
|
|
|
def calculate_priority_from_cidr
|
|
# For network rules, priority is the prefix length (more specific = higher priority)
|
|
if network_rule? && cidr.present?
|
|
self.priority = prefix_length
|
|
end
|
|
end
|
|
|
|
def validate_conditions_by_type
|
|
case rule_type
|
|
when "network_v4", "network_v6"
|
|
validate_network_conditions
|
|
when "rate_limit"
|
|
validate_rate_limit_conditions
|
|
when "path_pattern"
|
|
validate_path_pattern_conditions
|
|
end
|
|
end
|
|
|
|
def validate_network_conditions
|
|
cidr_value = conditions&.dig("cidr")
|
|
|
|
if cidr_value.blank?
|
|
errors.add(:conditions, "must include 'cidr' for network rules")
|
|
return
|
|
end
|
|
|
|
# Validate CIDR format
|
|
begin
|
|
addr = IPAddr.new(cidr_value)
|
|
|
|
# Check IPv4 vs IPv6 matches rule_type
|
|
if rule_type == "network_v4" && !addr.ipv4?
|
|
errors.add(:conditions, "cidr must be IPv4 for network_v4 rules")
|
|
elsif rule_type == "network_v6" && !addr.ipv6?
|
|
errors.add(:conditions, "cidr must be IPv6 for network_v6 rules")
|
|
end
|
|
rescue IPAddr::InvalidAddressError => e
|
|
errors.add(:conditions, "invalid CIDR format: #{e.message}")
|
|
end
|
|
end
|
|
|
|
def validate_rate_limit_conditions
|
|
scope = conditions&.dig("scope")
|
|
cidr_value = conditions&.dig("cidr")
|
|
|
|
if scope.blank?
|
|
errors.add(:conditions, "must include 'scope' for rate_limit rules")
|
|
end
|
|
|
|
if cidr_value.blank?
|
|
errors.add(:conditions, "must include 'cidr' for rate_limit rules")
|
|
end
|
|
|
|
# Validate metadata has rate limit config
|
|
unless metadata&.dig("limit").present? && metadata&.dig("window").present?
|
|
errors.add(:metadata, "must include 'limit' and 'window' for rate_limit rules")
|
|
end
|
|
end
|
|
|
|
def validate_path_pattern_conditions
|
|
patterns = conditions&.dig("patterns")
|
|
|
|
if patterns.blank? || !patterns.is_a?(Array)
|
|
errors.add(:conditions, "must include 'patterns' array for path_pattern rules")
|
|
end
|
|
end
|
|
|
|
def validate_metadata_by_action
|
|
case action
|
|
when "redirect"
|
|
unless metadata&.dig("redirect_url").present?
|
|
errors.add(:metadata, "must include 'redirect_url' for redirect action")
|
|
end
|
|
when "rate_limit"
|
|
unless metadata&.dig("limit").present? && metadata&.dig("window").present?
|
|
errors.add(:metadata, "must include 'limit' and 'window' for rate_limit action")
|
|
end
|
|
end
|
|
end
|
|
end
|