Many updates

This commit is contained in:
Dan Milne
2025-11-13 14:42:43 +11:00
parent 5e5198f113
commit df94ac9720
41 changed files with 4760 additions and 516 deletions

View File

@@ -5,7 +5,11 @@
# Rules define actions to take for matching traffic conditions.
# Network rules are associated with NetworkRange objects for rich context.
class Rule < ApplicationRecord
# Rule types and actions
# Rule enums
enum :waf_action, { allow: 0, deny: 1, rate_limit: 2, redirect: 3, log: 4, challenge: 5 }, scopes: false, prefix: true
enum :waf_rule_type, { network: 0, rate_limit: 1, path_pattern: 2 }, scopes: false, prefix: true
# Legacy string constants for backward compatibility
RULE_TYPES = %w[network rate_limit path_pattern].freeze
ACTIONS = %w[allow deny rate_limit redirect log challenge].freeze
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception policy].freeze
@@ -14,14 +18,42 @@ class Rule < ApplicationRecord
belongs_to :user
belongs_to :network_range, optional: true
belongs_to :waf_policy, optional: true
has_many :events, dependent: :nullify
# Backward compatibility accessors for transition period
def action
waf_action
end
def action=(value)
self.waf_action = value
self[:action] = value # Also set the legacy column
end
def rule_type
waf_rule_type
end
def rule_type=(value)
self.waf_rule_type = value
self[:rule_type] = value # Also set the legacy column
end
# Validations
validates :rule_type, presence: true, inclusion: { in: RULE_TYPES }
validates :action, presence: true, inclusion: { in: ACTIONS }
validates :waf_rule_type, presence: true, inclusion: { in: waf_rule_types.keys }
validates :waf_action, presence: true, inclusion: { in: waf_actions.keys }
validates :conditions, presence: true, unless: :network_rule?
validates :enabled, inclusion: { in: [true, false] }
validates :source, inclusion: { in: SOURCES }
# Legacy enum definitions (disabled to prevent conflicts)
# enum :action, { allow: "allow", deny: "deny", rate_limit: "rate_limit", redirect: "redirect", log: "log", challenge: "challenge" }, scopes: false
# enum :rule_type, { network: "network", rate_limit: "rate_limit", path_pattern: "path_pattern" }, scopes: false
# Legacy validations for backward compatibility during transition
# validates :rule_type, presence: true, inclusion: { in: RULE_TYPES }, allow_nil: true
# validates :action, presence: true, inclusion: { in: ACTIONS }, allow_nil: true
# Custom validations
validate :validate_conditions_by_type
validate :validate_metadata_by_action
@@ -33,16 +65,22 @@ class Rule < ApplicationRecord
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") }
scope :rate_limit_rules, -> { where(rule_type: "rate_limit") }
scope :path_pattern_rules, -> { where(rule_type: "path_pattern") }
scope :by_type, ->(type) { where(waf_rule_type: type) }
scope :network_rules, -> { network }
scope :rate_limit_rules, -> { rate_limit }
scope :path_pattern_rules, -> { path_pattern }
scope :by_source, ->(source) { where(source: source) }
scope :surgical_blocks, -> { where(source: "manual:surgical_block") }
scope :surgical_exceptions, -> { where(source: "manual:surgical_exception") }
scope :policy_generated, -> { where(source: "policy") }
scope :from_waf_policy, ->(waf_policy) { where(waf_policy: waf_policy) }
# Legacy scopes for backward compatibility
scope :by_type_legacy, ->(type) { where(rule_type: type) }
scope :network_rules_legacy, -> { where(rule_type: "network") }
scope :rate_limit_rules_legacy, -> { where(rule_type: "rate_limit") }
scope :path_pattern_rules_legacy, -> { where(rule_type: "path_pattern") }
# Sync queries
scope :since, ->(timestamp) { where("updated_at >= ?", Time.at(timestamp)).order(:updated_at, :id) }
scope :sync_order, -> { order(:updated_at, :id) }
@@ -51,18 +89,19 @@ class Rule < ApplicationRecord
before_validation :set_defaults
before_validation :parse_json_fields
before_save :calculate_priority_for_network_rules
before_save :sync_legacy_columns
# Rule type checks
def network_rule?
rule_type == "network"
waf_rule_type_network?
end
def rate_limit_rule?
rule_type == "rate_limit"
waf_rule_type_rate_limit?
end
def path_pattern_rule?
rule_type == "path_pattern"
waf_rule_type_path_pattern?
end
# Network-specific methods
@@ -104,16 +143,16 @@ class Rule < ApplicationRecord
# Action-specific methods
def redirect_action?
action == "redirect"
waf_action_redirect?
end
def challenge_action?
action == "challenge"
waf_action_challenge?
end
# Redirect/challenge convenience methods
def redirect_url
metadata&.dig('redirect_url')
metadata_hash['redirect_url']
end
def redirect_status
@@ -162,12 +201,13 @@ class Rule < ApplicationRecord
end
def disable!(reason: nil)
new_metadata = metadata_hash.merge(
disabled_at: Time.current.iso8601,
disabled_reason: reason
)
update!(
enabled: false,
metadata: metadata.merge(
disabled_at: Time.current.iso8601,
disabled_reason: reason
)
metadata: new_metadata
)
end
@@ -180,8 +220,8 @@ class Rule < ApplicationRecord
def to_agent_format
format = {
id: id,
rule_type: rule_type,
waf_action: action, # Agents expect 'waf_action' field
waf_rule_type: waf_rule_type,
waf_action: waf_action, # Use the enum field directly
conditions: agent_conditions,
priority: agent_priority,
expires_at: expires_at&.to_i, # Agents expect Unix timestamps
@@ -224,8 +264,8 @@ class Rule < ApplicationRecord
network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')
create!(
rule_type: 'network',
action: action,
waf_rule_type: 'network',
waf_action: action,
network_range: network_range,
user: user,
**options
@@ -237,8 +277,8 @@ class Rule < ApplicationRecord
network_range = NetworkRange.find_or_create_by_cidr(parent_cidr, user: user, source: 'user_created')
block_rule = create!(
rule_type: 'network',
action: 'deny',
waf_rule_type: 'network',
waf_action: 'deny',
network_range: network_range,
source: 'manual:surgical_block',
user: user,
@@ -255,8 +295,8 @@ class Rule < ApplicationRecord
ip_network_range = NetworkRange.find_or_create_by_cidr("#{ip_address}/#{ip_address.include?(':') ? '128' : '32'}", user: user, source: 'user_created')
exception_rule = create!(
rule_type: 'network',
action: 'allow',
waf_rule_type: 'network',
waf_action: 'allow',
network_range: ip_network_range,
source: 'manual:surgical_exception',
user: user,
@@ -277,8 +317,8 @@ class Rule < ApplicationRecord
network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')
create!(
rule_type: 'rate_limit',
action: 'rate_limit',
waf_rule_type: 'rate_limit',
waf_action: 'rate_limit',
network_range: network_range,
conditions: { cidr: cidr, scope: 'ip' },
metadata: {
@@ -307,7 +347,7 @@ class Rule < ApplicationRecord
# This would need efficient IP range queries
# For now, simple IP match
Event.where(ip_address: network_range.network_address)
Event.where("ip_address <<= ?", network_range.cidr)
.recent
.limit(limit)
end
@@ -324,6 +364,18 @@ class Rule < ApplicationRecord
}
end
# Helper method to safely access metadata as hash
def metadata_hash
case metadata
when Hash
metadata
when String
metadata.present? ? (JSON.parse(metadata) rescue {}) : {}
else
{}
end
end
private
def set_defaults
@@ -361,7 +413,7 @@ class Rule < ApplicationRecord
end
def validate_conditions_by_type
case rule_type
case waf_rule_type
when "network"
# Network rules don't need conditions in DB - stored in network_range
true
@@ -394,7 +446,7 @@ class Rule < ApplicationRecord
end
def validate_metadata_by_action
case action
case waf_action
when "redirect"
unless metadata&.dig("redirect_url").present?
errors.add(:metadata, "must include 'redirect_url' for redirect action")
@@ -457,4 +509,14 @@ class Rule < ApplicationRecord
self.metadata ||= {}
end
def sync_legacy_columns
# Sync enum values to legacy string columns for backward compatibility
if waf_action.present?
self[:action] = waf_action
end
if waf_rule_type.present?
self[:rule_type] = waf_rule_type
end
end
end