400 lines
10 KiB
Ruby
400 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# WafPolicy - High-level firewall policies that generate specific Rules
|
|
#
|
|
# WafPolicies contain strategic decisions like "Block Brazil" that automatically
|
|
# generate specific Rules when matching network ranges are discovered.
|
|
class WafPolicy < ApplicationRecord
|
|
# Policy types - different categories of blocking rules
|
|
POLICY_TYPES = %w[country asn company network_type].freeze
|
|
|
|
# Actions - what to do when traffic matches this policy
|
|
ACTIONS = %w[allow deny redirect challenge].freeze
|
|
|
|
# Associations
|
|
belongs_to :user
|
|
has_many :generated_rules, class_name: 'Rule', dependent: :destroy
|
|
|
|
# Validations
|
|
validates :name, presence: true, uniqueness: true
|
|
validates :policy_type, presence: true, inclusion: { in: POLICY_TYPES }
|
|
validates :action, presence: true, inclusion: { in: ACTIONS }
|
|
validates :targets, presence: true
|
|
validate :targets_must_be_array
|
|
validates :user, presence: true
|
|
validate :validate_targets_by_type
|
|
validate :validate_redirect_configuration, if: :redirect_action?
|
|
validate :validate_challenge_configuration, if: :challenge_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(policy_type: type) }
|
|
scope :country, -> { by_type('country') }
|
|
scope :asn, -> { by_type('asn') }
|
|
scope :company, -> { by_type('company') }
|
|
scope :network_type, -> { by_type('network_type') }
|
|
|
|
# Callbacks
|
|
before_validation :set_defaults
|
|
|
|
# Policy type methods
|
|
def country_policy?
|
|
policy_type == 'country'
|
|
end
|
|
|
|
def asn_policy?
|
|
policy_type == 'asn'
|
|
end
|
|
|
|
def company_policy?
|
|
policy_type == 'company'
|
|
end
|
|
|
|
def network_type_policy?
|
|
policy_type == 'network_type'
|
|
end
|
|
|
|
# Action methods
|
|
def allow_action?
|
|
action == 'allow'
|
|
end
|
|
|
|
def deny_action?
|
|
action == 'deny'
|
|
end
|
|
|
|
def redirect_action?
|
|
action == 'redirect'
|
|
end
|
|
|
|
def challenge_action?
|
|
action == 'challenge'
|
|
end
|
|
|
|
# Lifecycle methods
|
|
def active?
|
|
enabled? && !expired?
|
|
end
|
|
|
|
def expired?
|
|
expires_at.present? && expires_at <= Time.current
|
|
end
|
|
|
|
def activate!
|
|
update!(enabled: true)
|
|
end
|
|
|
|
def deactivate!
|
|
update!(enabled: false)
|
|
end
|
|
|
|
def expire!
|
|
update!(expires_at: Time.current)
|
|
end
|
|
|
|
# Network range matching methods
|
|
def matches_network_range?(network_range)
|
|
return false unless active?
|
|
|
|
case policy_type
|
|
when 'country'
|
|
matches_country?(network_range)
|
|
when 'asn'
|
|
matches_asn?(network_range)
|
|
when 'company'
|
|
matches_company?(network_range)
|
|
when 'network_type'
|
|
matches_network_type?(network_range)
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def create_rule_for_network_range(network_range)
|
|
return nil unless matches_network_range?(network_range)
|
|
|
|
rule = Rule.create!(
|
|
rule_type: 'network',
|
|
action: action,
|
|
network_range: network_range,
|
|
waf_policy: self,
|
|
user: user,
|
|
source: "policy:#{name}",
|
|
metadata: build_rule_metadata(network_range),
|
|
priority: network_range.prefix_length
|
|
)
|
|
|
|
# Handle redirect/challenge specific data
|
|
if redirect_action? && additional_data['redirect_url']
|
|
rule.update!(
|
|
metadata: rule.metadata.merge(
|
|
redirect_url: additional_data['redirect_url'],
|
|
redirect_status: additional_data['redirect_status'] || 302
|
|
)
|
|
)
|
|
elsif challenge_action?
|
|
rule.update!(
|
|
metadata: rule.metadata.merge(
|
|
challenge_type: additional_data['challenge_type'] || 'captcha',
|
|
challenge_message: additional_data['challenge_message']
|
|
)
|
|
)
|
|
end
|
|
|
|
rule
|
|
end
|
|
|
|
# Class methods for creating common policies
|
|
def self.create_country_policy(countries, action: 'deny', user:, **options)
|
|
create!(
|
|
name: "#{action.capitalize} #{countries.join(', ')}",
|
|
policy_type: 'country',
|
|
targets: Array(countries),
|
|
action: action,
|
|
user: user,
|
|
**options
|
|
)
|
|
end
|
|
|
|
def self.create_asn_policy(asns, action: 'deny', user:, **options)
|
|
create!(
|
|
name: "#{action.capitalize} ASNs #{asns.join(', ')}",
|
|
policy_type: 'asn',
|
|
targets: Array(asns).map(&:to_i),
|
|
action: action,
|
|
user: user,
|
|
**options
|
|
)
|
|
end
|
|
|
|
def self.create_company_policy(companies, action: 'deny', user:, **options)
|
|
create!(
|
|
name: "#{action.capitalize} #{companies.join(', ')}",
|
|
policy_type: 'company',
|
|
targets: Array(companies),
|
|
action: action,
|
|
user: user,
|
|
**options
|
|
)
|
|
end
|
|
|
|
def self.create_network_type_policy(types, action: 'deny', user:, **options)
|
|
create!(
|
|
name: "#{action.capitalize} #{types.join(', ')}",
|
|
policy_type: 'network_type',
|
|
targets: Array(types),
|
|
action: action,
|
|
user: user,
|
|
**options
|
|
)
|
|
end
|
|
|
|
# Redirect/challenge specific methods
|
|
def redirect_url
|
|
additional_data&.dig('redirect_url')
|
|
end
|
|
|
|
def redirect_status
|
|
additional_data&.dig('redirect_status') || 302
|
|
end
|
|
|
|
def challenge_type
|
|
additional_data&.dig('challenge_type') || 'captcha'
|
|
end
|
|
|
|
def challenge_message
|
|
additional_data&.dig('challenge_message')
|
|
end
|
|
|
|
# Statistics and analytics
|
|
def generated_rules_count
|
|
generated_rules.count
|
|
end
|
|
|
|
def active_rules_count
|
|
generated_rules.active.count
|
|
end
|
|
|
|
def effectiveness_stats
|
|
recent_rules = generated_rules.where('created_at > ?', 7.days.ago)
|
|
|
|
{
|
|
total_rules_generated: generated_rules_count,
|
|
active_rules: active_rules_count,
|
|
rules_last_7_days: recent_rules.count,
|
|
policy_type: policy_type,
|
|
action: action,
|
|
targets_count: targets&.length || 0
|
|
}
|
|
end
|
|
|
|
# String representations
|
|
def to_s
|
|
name
|
|
end
|
|
|
|
def to_param
|
|
name.parameterize
|
|
end
|
|
|
|
private
|
|
|
|
def set_defaults
|
|
self.targets ||= []
|
|
self.additional_data ||= {}
|
|
self.enabled = true if enabled.nil?
|
|
end
|
|
|
|
def targets_must_be_array
|
|
unless targets.is_a?(Array)
|
|
errors.add(:targets, "must be an array")
|
|
end
|
|
end
|
|
|
|
def validate_targets_by_type
|
|
return if targets.blank?
|
|
|
|
case policy_type
|
|
when 'country'
|
|
validate_country_targets
|
|
when 'asn'
|
|
validate_asn_targets
|
|
when 'company'
|
|
validate_company_targets
|
|
when 'network_type'
|
|
validate_network_type_targets
|
|
end
|
|
end
|
|
|
|
def validate_country_targets
|
|
unless targets.all? { |target| target.is_a?(String) && target.match?(/\A[A-Z]{2}\z/) }
|
|
errors.add(:targets, "must be valid ISO country codes (e.g., 'BR', 'US')")
|
|
end
|
|
end
|
|
|
|
def validate_asn_targets
|
|
unless targets.all? { |target| target.is_a?(Integer) && target > 0 }
|
|
errors.add(:targets, "must be valid ASNs (positive integers)")
|
|
end
|
|
end
|
|
|
|
def validate_company_targets
|
|
unless targets.all? { |target| target.is_a?(String) && target.present? }
|
|
errors.add(:targets, "must be valid company names")
|
|
end
|
|
end
|
|
|
|
def validate_network_type_targets
|
|
valid_types = %w[datacenter proxy vpn standard]
|
|
unless targets.all? { |target| valid_types.include?(target) }
|
|
errors.add(:targets, "must be one of: #{valid_types.join(', ')}")
|
|
end
|
|
end
|
|
|
|
def validate_redirect_configuration
|
|
if additional_data['redirect_url'].blank?
|
|
errors.add(:additional_data, "must include 'redirect_url' for redirect action")
|
|
end
|
|
end
|
|
|
|
def validate_challenge_configuration
|
|
# Challenge is flexible - can use defaults if not specified
|
|
valid_challenge_types = %w[captcha javascript proof_of_work]
|
|
challenge_type_value = additional_data&.dig('challenge_type')
|
|
|
|
if challenge_type_value && !valid_challenge_types.include?(challenge_type_value)
|
|
errors.add(:additional_data, "challenge_type must be one of: #{valid_challenge_types.join(', ')}")
|
|
end
|
|
end
|
|
|
|
# Matching logic for different policy types
|
|
def matches_country?(network_range)
|
|
country = network_range.country || network_range.inherited_intelligence[:country]
|
|
targets.include?(country)
|
|
end
|
|
|
|
def matches_asn?(network_range)
|
|
asn = network_range.asn || network_range.inherited_intelligence[:asn]
|
|
targets.include?(asn)
|
|
end
|
|
|
|
def matches_company?(network_range)
|
|
company = network_range.company || network_range.inherited_intelligence[:company]
|
|
return false if company.blank?
|
|
|
|
targets.any? do |target_company|
|
|
company.downcase.include?(target_company.downcase) ||
|
|
target_company.downcase.include?(company.downcase)
|
|
end
|
|
end
|
|
|
|
def matches_network_type?(network_range)
|
|
intelligence = network_range.inherited_intelligence
|
|
|
|
targets.any? do |target_type|
|
|
case target_type
|
|
when 'datacenter'
|
|
intelligence[:is_datacenter] == true
|
|
when 'proxy'
|
|
intelligence[:is_proxy] == true
|
|
when 'vpn'
|
|
intelligence[:is_vpn] == true
|
|
when 'standard'
|
|
intelligence[:is_datacenter] == false &&
|
|
intelligence[:is_proxy] == false &&
|
|
intelligence[:is_vpn] == false
|
|
else
|
|
false
|
|
end
|
|
end
|
|
end
|
|
|
|
def build_rule_metadata(network_range)
|
|
base_metadata = {
|
|
generated_by_policy: id,
|
|
policy_name: name,
|
|
policy_type: policy_type,
|
|
matched_field: matched_field(network_range),
|
|
matched_value: matched_value(network_range)
|
|
}
|
|
|
|
base_metadata.merge!(additional_data || {})
|
|
end
|
|
|
|
def matched_field(network_range)
|
|
case policy_type
|
|
when 'country'
|
|
'country'
|
|
when 'asn'
|
|
'asn'
|
|
when 'company'
|
|
'company'
|
|
when 'network_type'
|
|
'network_type'
|
|
else
|
|
'unknown'
|
|
end
|
|
end
|
|
|
|
def matched_value(network_range)
|
|
case policy_type
|
|
when 'country'
|
|
network_range.country || network_range.inherited_intelligence[:country]
|
|
when 'asn'
|
|
network_range.asn || network_range.inherited_intelligence[:asn]
|
|
when 'company'
|
|
network_range.company || network_range.inherited_intelligence[:company]
|
|
when 'network_type'
|
|
intelligence = network_range.inherited_intelligence
|
|
types = []
|
|
types << 'datacenter' if intelligence[:is_datacenter]
|
|
types << 'proxy' if intelligence[:is_proxy]
|
|
types << 'vpn' if intelligence[:is_vpn]
|
|
types.join(',') || 'standard'
|
|
end
|
|
end
|
|
end
|