# 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