# frozen_string_literal: true class Rule < ApplicationRecord belongs_to :rule_set validates :rule_type, presence: true, inclusion: { in: RuleSet::RULE_TYPES } validates :target, presence: true validates :action, presence: true, inclusion: { in: RuleSet::ACTIONS } validates :priority, presence: true, numericality: { greater_than: 0 } scope :enabled, -> { where(enabled: true) } scope :by_priority, -> { order(priority: :desc, created_at: :desc) } scope :expired, -> { where("expires_at < ?", Time.current) } scope :not_expired, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) } # Check if rule is currently active def active? enabled? && (expires_at.nil? || expires_at > Time.current) end # Check if rule matches given request context def matches?(context) return false unless active? case rule_type when 'ip' match_ip_rule?(context) when 'cidr' match_cidr_rule?(context) when 'path' match_path_rule?(context) when 'user_agent' match_user_agent_rule?(context) when 'parameter' match_parameter_rule?(context) when 'method' match_method_rule?(context) when 'country' match_country_rule?(context) else false end end def to_waf_format { id: id, type: rule_type, target: target, action: action, conditions: conditions || {}, priority: priority, expires_at: expires_at, active: active? } end private def match_ip_rule?(context) return false unless context[:ip_address] target == context[:ip_address] end def match_cidr_rule?(context) return false unless context[:ip_address] begin range = IPAddr.new(target) range.include?(context[:ip_address]) rescue IPAddr::InvalidAddressError false end end def match_path_rule?(context) return false unless context[:request_path] # Support exact match and regex patterns if conditions&.dig('regex') == true Regexp.new(target).match?(context[:request_path]) else context[:request_path].start_with?(target) end end def match_user_agent_rule?(context) return false unless context[:user_agent] # Support exact match and regex patterns if conditions&.dig('regex') == true Regexp.new(target, Regexp::IGNORECASE).match?(context[:user_agent]) else context[:user_agent].downcase.include?(target.downcase) end end def match_parameter_rule?(context) return false unless context[:query_params] param_name = conditions&.dig('parameter_name') || target param_value = context[:query_params][param_name] return false unless param_value # Check if parameter value matches pattern if conditions&.dig('regex') == true Regexp.new(target, Regexp::IGNORECASE).match?(param_value.to_s) else param_value.to_s.downcase.include?(target.downcase) end end def match_method_rule?(context) return false unless context[:request_method] target.upcase == context[:request_method].upcase end def match_country_rule?(context) return false unless context[:country_code] target.upcase == context[:country_code].upcase end end