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

@@ -56,11 +56,10 @@ class AnalyticsController < ApplicationController
end
end
# Top countries by event count - cached (this is the expensive JOIN query)
# Top countries by event count - cached (now uses denormalized country column)
@top_countries = Rails.cache.fetch("#{cache_key_base}/top_countries", expires_in: cache_ttl) do
Event.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
.where("timestamp >= ? AND network_ranges.country IS NOT NULL", @start_time)
.group("network_ranges.country")
Event.where("timestamp >= ? AND country IS NOT NULL", @start_time)
.group(:country)
.count
.sort_by { |_, count| -count }
.first(10)
@@ -126,10 +125,10 @@ class AnalyticsController < ApplicationController
@time_period = params[:period]&.to_sym || :day
@start_time = calculate_start_time(@time_period)
# Top networks by request volume
@top_networks = NetworkRange.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
# Top networks by request volume (using denormalized network_range_id)
@top_networks = NetworkRange.joins("LEFT JOIN events ON events.network_range_id = network_ranges.id")
.where("events.timestamp >= ? OR events.timestamp IS NULL", @start_time)
.group("network_ranges.id", "network_ranges.network", "network_ranges.company", "network_ranges.asn", "network_ranges.country", "network_ranges.is_datacenter", "network_ranges.is_vpn", "network_ranges.is_proxy")
.group("network_ranges.id")
.select("network_ranges.*, COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips")
.order("event_count DESC")
.limit(50)
@@ -137,29 +136,26 @@ class AnalyticsController < ApplicationController
# Network type breakdown with traffic stats
@network_breakdown = calculate_network_type_stats(@start_time)
# Company breakdown for top traffic sources
@top_companies = NetworkRange.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ? AND network_ranges.company IS NOT NULL", @start_time)
.group("network_ranges.company")
.select("network_ranges.company, COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips, COUNT(DISTINCT network_ranges.id) as network_count")
.order("event_count DESC")
.limit(20)
# Company breakdown for top traffic sources (using denormalized company column)
@top_companies = Event.where("timestamp >= ? AND company IS NOT NULL", @start_time)
.group(:company)
.select("company, COUNT(*) as event_count, COUNT(DISTINCT ip_address) as unique_ips")
.order("event_count DESC")
.limit(20)
# ASN breakdown
@top_asns = NetworkRange.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ? AND network_ranges.asn IS NOT NULL", @start_time)
.group("network_ranges.asn", "network_ranges.asn_org")
.select("network_ranges.asn, network_ranges.asn_org, COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips, COUNT(DISTINCT network_ranges.id) as network_count")
.order("event_count DESC")
.limit(15)
# ASN breakdown (using denormalized asn columns)
@top_asns = Event.where("timestamp >= ? AND asn IS NOT NULL", @start_time)
.group(:asn, :asn_org)
.select("asn, asn_org, COUNT(*) as event_count, COUNT(DISTINCT ip_address) as unique_ips")
.order("event_count DESC")
.limit(15)
# Geographic breakdown
@top_countries = NetworkRange.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ? AND network_ranges.country IS NOT NULL", @start_time)
.group("network_ranges.country")
.select("network_ranges.country, COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips, COUNT(DISTINCT network_ranges.id) as network_count")
.order("event_count DESC")
.limit(15)
# Geographic breakdown (using denormalized country column)
@top_countries = Event.where("timestamp >= ? AND country IS NOT NULL", @start_time)
.group(:country)
.select("country, COUNT(*) as event_count, COUNT(DISTINCT ip_address) as unique_ips")
.order("event_count DESC")
.limit(15)
# Suspicious network activity patterns
@suspicious_patterns = calculate_suspicious_patterns(@start_time)
@@ -297,51 +293,41 @@ class AnalyticsController < ApplicationController
end
def calculate_network_type_stats(start_time)
# Get all network types with their traffic statistics
# Get all network types with their traffic statistics using denormalized columns
network_types = [
{ type: 'datacenter', label: 'Datacenter' },
{ type: 'vpn', label: 'VPN' },
{ type: 'proxy', label: 'Proxy' }
{ type: 'datacenter', label: 'Datacenter', column: :is_datacenter },
{ type: 'vpn', label: 'VPN', column: :is_vpn },
{ type: 'proxy', label: 'Proxy', column: :is_proxy }
]
results = {}
total_events = Event.where("timestamp >= ?", start_time).count
network_types.each do |network_type|
scope = case network_type[:type]
when 'datacenter' then NetworkRange.datacenter
when 'vpn' then NetworkRange.vpn
when 'proxy' then NetworkRange.proxy
end
# Query events directly using denormalized flags
event_stats = Event.where("timestamp >= ? AND #{network_type[:column]} = ?", start_time, true)
.select("COUNT(*) as event_count, COUNT(DISTINCT ip_address) as unique_ips, COUNT(DISTINCT network_range_id) as network_count")
.first
if scope
network_stats = scope.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ? OR events.timestamp IS NULL", start_time)
.select("COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips, COUNT(DISTINCT network_ranges.id) as network_count")
.first
results[network_type[:type]] = {
label: network_type[:label],
networks: network_stats.network_count,
events: network_stats.event_count,
unique_ips: network_stats.unique_ips,
percentage: total_events > 0 ? ((network_stats.event_count.to_f / total_events) * 100).round(1) : 0
}
end
results[network_type[:type]] = {
label: network_type[:label],
networks: event_stats.network_count || 0,
events: event_stats.event_count || 0,
unique_ips: event_stats.unique_ips || 0,
percentage: total_events > 0 ? ((event_stats.event_count.to_f / total_events) * 100).round(1) : 0
}
end
# Calculate standard networks (everything else)
standard_stats = NetworkRange.where(is_datacenter: false, is_vpn: false, is_proxy: false)
.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ? OR events.timestamp IS NULL", start_time)
.select("COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips, COUNT(DISTINCT network_ranges.id) as network_count")
.first
standard_stats = Event.where("timestamp >= ? AND is_datacenter = ? AND is_vpn = ? AND is_proxy = ?", start_time, false, false, false)
.select("COUNT(*) as event_count, COUNT(DISTINCT ip_address) as unique_ips, COUNT(DISTINCT network_range_id) as network_count")
.first
results['standard'] = {
label: 'Standard',
networks: standard_stats.network_count,
events: standard_stats.event_count,
unique_ips: standard_stats.unique_ips,
networks: standard_stats.network_count || 0,
events: standard_stats.event_count || 0,
unique_ips: standard_stats.unique_ips || 0,
percentage: total_events > 0 ? ((standard_stats.event_count.to_f / total_events) * 100).round(1) : 0
}
@@ -351,51 +337,51 @@ class AnalyticsController < ApplicationController
def calculate_suspicious_patterns(start_time)
patterns = {}
# High volume networks (top 1% by request count)
total_networks = NetworkRange.joins("LEFT JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ?", start_time)
.distinct.count
# High volume networks (top 1% by request count) - using denormalized network_range_id
total_networks = Event.where("timestamp >= ? AND network_range_id IS NOT NULL", start_time)
.distinct.count(:network_range_id)
high_volume_threshold = [total_networks * 0.01, 1].max
high_volume_networks = NetworkRange.joins("INNER JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ?", start_time)
.group("network_ranges.id")
.having("COUNT(events.id) > ?", Event.where("timestamp >= ?", start_time).count / total_networks)
.count
if total_networks > 0
avg_events_per_network = Event.where("timestamp >= ?", start_time).count / total_networks
high_volume_networks = Event.where("timestamp >= ? AND network_range_id IS NOT NULL", start_time)
.group(:network_range_id)
.having("COUNT(*) > ?", avg_events_per_network * 5)
.count
patterns[:high_volume] = {
count: high_volume_networks.count,
networks: high_volume_networks.keys
}
patterns[:high_volume] = {
count: high_volume_networks.count,
networks: high_volume_networks.keys
}
else
patterns[:high_volume] = { count: 0, networks: [] }
end
# Networks with high deny rates (> 50% blocked requests)
high_deny_networks = NetworkRange.joins("INNER JOIN events ON events.ip_address <<= network_ranges.network")
.where("events.timestamp >= ?", start_time)
.group("network_ranges.id")
.select("network_ranges.id,
COUNT(CASE WHEN events.waf_action = 1 THEN 1 END) as denied_count,
COUNT(events.id) as total_count")
.having("COUNT(CASE WHEN events.waf_action = 1 THEN 1 END)::float / COUNT(events.id) > 0.5")
.having("COUNT(events.id) >= 10") # minimum threshold
# Networks with high deny rates (> 50% blocked requests) - using denormalized network_range_id
high_deny_networks = Event.where("timestamp >= ? AND network_range_id IS NOT NULL", start_time)
.group(:network_range_id)
.select("network_range_id,
COUNT(CASE WHEN waf_action = 1 THEN 1 END) as denied_count,
COUNT(*) as total_count")
.having("COUNT(CASE WHEN waf_action = 1 THEN 1 END)::float / COUNT(*) > 0.5")
.having("COUNT(*) >= 10") # minimum threshold
patterns[:high_deny_rate] = {
count: high_deny_networks.count,
network_ids: high_deny_networks.map(&:id)
network_ids: high_deny_networks.map(&:network_range_id)
}
# Networks appearing as multiple subnets (potential botnets)
company_subnets = NetworkRange.where("company IS NOT NULL")
.where("timestamp >= ? OR timestamp IS NULL", start_time)
.group(:company)
.select(:company, "COUNT(DISTINCT network) as subnet_count")
.having("COUNT(DISTINCT network) > 5")
.order("subnet_count DESC")
.limit(10)
# Companies appearing with multiple IPs (potential botnets) - using denormalized company column
company_subnets = Event.where("timestamp >= ? AND company IS NOT NULL", start_time)
.group(:company)
.select("company, COUNT(DISTINCT ip_address) as ip_count")
.having("COUNT(DISTINCT ip_address) > 5")
.order("ip_count DESC")
.limit(10)
patterns[:distributed_companies] = company_subnets.map do |company|
patterns[:distributed_companies] = company_subnets.map do |stat|
{
company: company.company,
subnets: company.subnet_count
company: stat.company,
subnets: stat.ip_count
}
end

View File

@@ -2,28 +2,35 @@
class EventsController < ApplicationController
def show
@event = Event.find(params[:id])
@network_range = NetworkRange.contains_ip(@event.ip_address.to_s).first
@event = Event.includes(:network_range).find(params[:id])
# Auto-generate network range if no match found
# Use denormalized network_range_id if available (much faster)
@network_range = @event.network_range
# Fallback to IP lookup if network_range_id is missing
unless @network_range
@network_range = NetworkRangeGenerator.find_or_create_for_ip(@event.ip_address)
Rails.logger.debug "Auto-generated network range #{@network_range&.cidr} for IP #{@event.ip_address}" if @network_range
@network_range = NetworkRange.contains_ip(@event.ip_address.to_s).first
# Auto-generate network range if no match found
unless @network_range
@network_range = NetworkRangeGenerator.find_or_create_for_ip(@event.ip_address)
Rails.logger.debug "Auto-generated network range #{@network_range&.cidr} for IP #{@event.ip_address}" if @network_range
end
end
end
def index
@events = Event.order(timestamp: :desc)
@events = Event.includes(:network_range, :rule).order(timestamp: :desc)
Rails.logger.debug "Found #{@events.count} total events"
Rails.logger.debug "Action: #{params[:waf_action]}"
# Apply filters
@events = @events.by_ip(params[:ip]) if params[:ip].present?
@events = @events.by_waf_action(params[:waf_action]) if params[:waf_action].present?
@events = @events.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
.where("network_ranges.country = ?", params[:country]) if params[:country].present?
@events = @events.by_country(params[:country]) if params[:country].present?
@events = @events.where(rule_id: params[:rule_id]) if params[:rule_id].present?
# Network-based filters
# Network-based filters (now using denormalized columns)
@events = @events.by_company(params[:company]) if params[:company].present?
@events = @events.by_network_type(params[:network_type]) if params[:network_type].present?
@events = @events.by_asn(params[:asn]) if params[:asn].present?
@@ -37,24 +44,10 @@ class EventsController < ApplicationController
# Paginate
@pagy, @events = pagy(@events, items: 50)
# Preload network ranges for all unique IPs to avoid N+1 queries
unique_ips = @events.pluck(:ip_address).uniq.compact
@network_ranges_by_ip = {}
unique_ips.each do |ip|
ip_string = ip.to_s # IPAddr objects can be converted to string
range = NetworkRange.contains_ip(ip_string).first
# Auto-generate network range if no match found
unless range
range = NetworkRangeGenerator.find_or_create_for_ip(ip)
Rails.logger.debug "Auto-generated network range #{range&.cidr} for IP #{ip_string}" if range
end
@network_ranges_by_ip[ip_string] = range if range
end
# Network ranges are now preloaded via includes(:network_range)
# The denormalized network_range_id makes this much faster than IP containment lookups
Rails.logger.debug "Events count after pagination: #{@events.count}"
Rails.logger.debug "Pagy info: #{@pagy.count} total, #{@pagy.pages} pages"
Rails.logger.debug "Preloaded network ranges for #{@network_ranges_by_ip.count} unique IPs"
end
end

View File

@@ -46,24 +46,51 @@ class NetworkRangesController < ApplicationController
authorize @network_range
if @network_range.persisted?
# Real network - use existing logic
@related_events = Event.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
.where("network_ranges.id = ?", @network_range.id)
.recent
.limit(100)
# Real network - use direct IP containment for consistency with stats
events_scope = Event.where("ip_address <<= ?", @network_range.cidr).recent
else
# Virtual network - find events by IP range containment
@related_events = Event.where("ip_address <<= ?::inet", @network_range.to_s)
.recent
.limit(100)
events_scope = Event.where("ip_address <<= ?::inet", @network_range.to_s).recent
end
# Paginate events
@events_pagy, @related_events = pagy(events_scope, items: 50)
@child_ranges = @network_range.child_ranges.limit(20)
@parent_ranges = @network_range.parent_ranges.limit(10)
@associated_rules = @network_range.persisted? ? @network_range.rules.includes(:user).order(created_at: :desc) : []
# Traffic analytics (if we have events)
@traffic_stats = calculate_traffic_stats(@network_range)
# Check if we have IPAPI data (or if parent has it)
@has_ipapi_data = @network_range.has_network_data_from?(:ipapi)
@parent_with_ipapi = nil
unless @has_ipapi_data
# Check if parent has IPAPI data
parent = @network_range.parent_with_intelligence
if parent&.has_network_data_from?(:ipapi)
@parent_with_ipapi = parent
@has_ipapi_data = true
end
end
# If we don't have IPAPI data anywhere and no parent has it, queue fetch job
if @network_range.persisted? && @network_range.should_fetch_ipapi_data?
@network_range.mark_as_fetching_api_data!(:ipapi)
FetchIpapiDataJob.perform_later(network_range_id: @network_range.id)
@ipapi_loading = true
end
# Get IPAPI data for display
@ipapi_data = if @parent_with_ipapi
@parent_with_ipapi.network_data_for(:ipapi)
elsif @network_range.has_network_data_from?(:ipapi)
@network_range.network_data_for(:ipapi)
else
nil
end
end
# GET /network_ranges/new
@@ -214,18 +241,27 @@ class NetworkRangesController < ApplicationController
if network_range.persisted?
# Real network - use cached events_count for total requests (much more performant)
if network_range.events_count > 0
events = Event.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
.where("network_ranges.id = ?", network_range.id)
.limit(1000) # Limit the sample for performance
# Base query for consistent IP containment logic
base_query = Event.where("ip_address <<= ?", network_range.cidr)
# Use separate queries: one for grouping (without ordering), one for recent activity (with ordering)
events_for_grouping = base_query.limit(1000)
events_for_activity = base_query.recent.limit(20)
# Calculate counts properly - use consistent base_query for all counts
total_requests = base_query.count
unique_ips = base_query.except(:order).distinct.count(:ip_address)
blocked_requests = base_query.blocked.count
allowed_requests = base_query.allowed.count
{
total_requests: network_range.events_count, # Use cached count
unique_ips: events.distinct.count(:ip_address),
blocked_requests: events.blocked.count,
allowed_requests: events.allowed.count,
top_paths: events.group(:request_path).count.sort_by { |_, count| -count }.first(10),
top_user_agents: events.group(:user_agent).count.sort_by { |_, count| -count }.first(5),
recent_activity: events.recent.limit(20)
total_requests: total_requests,
unique_ips: unique_ips,
blocked_requests: blocked_requests,
allowed_requests: allowed_requests,
top_paths: events_for_grouping.group(:request_path).count.sort_by { |_, count| -count }.first(10),
top_user_agents: events_for_grouping.group(:user_agent).count.sort_by { |_, count| -count }.first(5),
recent_activity: events_for_activity
}
else
# No events - return empty stats
@@ -241,20 +277,35 @@ class NetworkRangesController < ApplicationController
end
else
# Virtual network - calculate stats from events within range
events = Event.where("ip_address <<= ?::inet", network_range.to_s)
.limit(1000) # Limit the sample for performance
base_query = Event.where("ip_address <<= ?", network_range.cidr)
total_events = base_query.count
total_events = Event.where("ip_address <<= ?::inet", network_range.to_s).count
if total_events > 0
# Use separate queries: one for grouping (without ordering), one for recent activity (with ordering)
events_for_grouping = base_query.limit(1000)
events_for_activity = base_query.recent.limit(20)
{
total_requests: total_events,
unique_ips: events.distinct.count(:ip_address),
blocked_requests: events.blocked.count,
allowed_requests: events.allowed.count,
top_paths: events.group(:request_path).count.sort_by { |_, count| -count }.first(10),
top_user_agents: events.group(:user_agent).count.sort_by { |_, count| -count }.first(5),
recent_activity: events.recent.limit(20)
}
{
total_requests: total_events,
unique_ips: base_query.except(:order).distinct.count(:ip_address),
blocked_requests: base_query.blocked.count,
allowed_requests: base_query.allowed.count,
top_paths: events_for_grouping.group(:request_path).count.sort_by { |_, count| -count }.first(10),
top_user_agents: events_for_grouping.group(:user_agent).count.sort_by { |_, count| -count }.first(5),
recent_activity: events_for_activity
}
else
# No events for virtual network
{
total_requests: 0,
unique_ips: 0,
blocked_requests: 0,
allowed_requests: 0,
top_paths: {},
top_user_agents: {},
recent_activity: []
}
end
end
end
end

View File

@@ -11,8 +11,8 @@ class RulesController < ApplicationController
# GET /rules
def index
@pagy, @rules = pagy(policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc))
@rule_types = Rule::RULE_TYPES
@actions = Rule::ACTIONS
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# GET /rules/new
@@ -27,11 +27,11 @@ class RulesController < ApplicationController
end
if params[:cidr].present?
@rule.rule_type = 'network'
@rule.waf_rule_type = 'network'
end
@rule_types = Rule::RULE_TYPES
@actions = Rule::ACTIONS
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# POST /rules
@@ -39,8 +39,8 @@ class RulesController < ApplicationController
authorize Rule
@rule = Rule.new(rule_params)
@rule.user = Current.user
@rule_types = Rule::RULE_TYPES
@actions = Rule::ACTIONS
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
# Process additional form data for quick create
process_quick_create_parameters
@@ -79,16 +79,26 @@ class RulesController < ApplicationController
# GET /rules/:id/edit
def edit
authorize @rule
@rule_types = Rule::RULE_TYPES
@actions = Rule::ACTIONS
@waf_rule_types = Rule.waf_rule_types
@waf_actions = Rule.waf_actions
end
# PATCH/PUT /rules/:id
def update
authorize @rule
# Preserve original attributes in case validation fails
original_attributes = @rule.attributes.dup
original_network_range_id = @rule.network_range_id
if @rule.update(rule_params)
redirect_to @rule, notice: 'Rule was successfully updated.'
else
# Restore original attributes to preserve form state
# This prevents network range dropdown from resetting
@rule.attributes = original_attributes
@rule.network_range_id = original_network_range_id
render :edit, status: :unprocessable_entity
end
end
@@ -116,8 +126,8 @@ class RulesController < ApplicationController
def rule_params
permitted = [
:rule_type,
:action,
:waf_rule_type,
:waf_action,
:metadata,
:expires_at,
:enabled,
@@ -126,7 +136,7 @@ class RulesController < ApplicationController
]
# Only include conditions for non-network rules
if params[:rule][:rule_type] != 'network'
if params[:rule][:waf_rule_type] != 'network'
permitted << :conditions
end
@@ -136,7 +146,7 @@ end
def calculate_rule_priority
return unless @rule
case @rule.rule_type
case @rule.waf_rule_type
when 'network'
# For network rules, priority based on prefix specificity
if @rule.network_range
@@ -167,20 +177,10 @@ def calculate_rule_priority
else
@rule.priority = 100 # Default for network rules without range
end
when 'protocol_violation'
@rule.priority = 95
when 'method_enforcement'
@rule.priority = 90
when 'path_pattern'
@rule.priority = 85
when 'header_pattern', 'query_pattern'
@rule.priority = 80
when 'body_signature'
@rule.priority = 75
when 'rate_limit'
@rule.priority = 70
when 'composite'
@rule.priority = 65
else
@rule.priority = 50 # Default priority
end
@@ -203,7 +203,7 @@ def process_quick_create_parameters
end
# Handle redirect URL
if @rule.action == 'redirect' && params[:redirect_url].present?
if @rule.redirect? && params[:redirect_url].present?
@rule.metadata ||= {}
if @rule.metadata.is_a?(String)
begin
@@ -227,6 +227,24 @@ def process_quick_create_parameters
end
end
# Handle expires_at parsing for text input
if params.dig(:rule, :expires_at).present?
expires_at_str = params[:rule][:expires_at].strip
if expires_at_str.present?
begin
# Try to parse various datetime formats
@rule.expires_at = DateTime.parse(expires_at_str)
rescue ArgumentError
# Try specific format
begin
@rule.expires_at = DateTime.strptime(expires_at_str, '%Y-%m-%d %H:%M')
rescue ArgumentError
@rule.errors.add(:expires_at, 'must be in format YYYY-MM-DD HH:MM')
end
end
end
end
# Add reason to metadata if provided
if params.dig(:rule, :metadata).present?
if @rule.metadata.is_a?(Hash)
@@ -245,8 +263,8 @@ end
def rule_params
permitted = [
:rule_type,
:action,
:waf_rule_type,
:waf_action,
:metadata,
:expires_at,
:enabled,
@@ -255,7 +273,7 @@ end
]
# Only include conditions for non-network rules
if params[:rule][:rule_type] != 'network'
if params[:rule][:waf_rule_type] != 'network'
permitted << :conditions
end
@@ -265,7 +283,7 @@ end
def calculate_rule_priority
return unless @rule
case @rule.rule_type
case @rule.waf_rule_type
when 'network'
# For network rules, priority based on prefix specificity
if @rule.network_range
@@ -296,20 +314,10 @@ end
else
@rule.priority = 100 # Default for network rules without range
end
when 'protocol_violation'
@rule.priority = 95
when 'method_enforcement'
@rule.priority = 90
when 'path_pattern'
@rule.priority = 85
when 'header_pattern', 'query_pattern'
@rule.priority = 80
when 'body_signature'
@rule.priority = 75
when 'rate_limit'
@rule.priority = 70
when 'composite'
@rule.priority = 65
else
@rule.priority = 50 # Default priority
end
@@ -332,7 +340,7 @@ end
end
# Handle redirect URL
if @rule.action == 'redirect' && params[:redirect_url].present?
if @rule.redirect? && params[:redirect_url].present?
@rule.metadata ||= {}
if @rule.metadata.is_a?(String)
begin

View File

@@ -24,7 +24,7 @@ class WafPoliciesController < ApplicationController
# Set default values from URL parameters
@waf_policy.policy_type = params[:policy_type] if params[:policy_type].present?
@waf_policy.action = params[:action] if params[:action].present?
@waf_policy.policy_action = params[:policy_action] if params[:policy_action].present?
@waf_policy.targets = params[:targets] if params[:targets].present?
end
@@ -37,9 +37,6 @@ class WafPoliciesController < ApplicationController
@actions = WafPolicy::ACTIONS
if @waf_policy.save
# Trigger policy processing for existing network ranges
ProcessWafPoliciesJob.perform_later(waf_policy_id: @waf_policy.id)
redirect_to @waf_policy, notice: 'WAF policy was successfully created.'
else
render :new, status: :unprocessable_entity
@@ -64,11 +61,6 @@ class WafPoliciesController < ApplicationController
@actions = WafPolicy::ACTIONS
if @waf_policy.update(waf_policy_params)
# Re-process policies for existing network ranges if policy was changed
if @waf_policy.saved_change_to_targets? || @waf_policy.saved_change_to_action?
ProcessWafPoliciesJob.reprocess_for_policy(@waf_policy)
end
redirect_to @waf_policy, notice: 'WAF policy was successfully updated.'
else
render :edit, status: :unprocessable_entity
@@ -89,9 +81,6 @@ class WafPoliciesController < ApplicationController
def activate
@waf_policy.activate!
# Re-process policies for existing network ranges
ProcessWafPoliciesJob.reprocess_for_policy(@waf_policy)
redirect_to @waf_policy, notice: 'WAF policy was activated.'
end
@@ -105,7 +94,7 @@ class WafPoliciesController < ApplicationController
# GET /waf_policies/new_country
def new_country
authorize WafPolicy
@waf_policy = WafPolicy.new(policy_type: 'country', action: 'deny')
@waf_policy = WafPolicy.new(policy_type: 'country', policy_action: 'deny')
@policy_types = WafPolicy::POLICY_TYPES
@actions = WafPolicy::ACTIONS
end
@@ -115,24 +104,28 @@ class WafPoliciesController < ApplicationController
authorize WafPolicy
countries = params[:countries]&.reject(&:blank?) || []
action = params[:action] || 'deny'
policy_action = params[:policy_action] || 'deny'
if countries.empty?
redirect_to new_country_waf_policies_path, alert: 'Please select at least one country.'
return
end
@waf_policy = WafPolicy.create_country_policy(
countries,
action: action,
# Build the options hash with additional_data if present
options = {
policy_action: policy_action,
user: Current.user,
description: params[:description]
)
}
# Add additional_data if provided (for redirect/challenge actions)
if params[:additional_data].present?
options[:additional_data] = params[:additional_data].to_unsafe_hash
end
@waf_policy = WafPolicy.create_country_policy(countries, **options)
if @waf_policy.persisted?
# Trigger policy processing for existing network ranges
ProcessWafPoliciesJob.reprocess_for_policy(@waf_policy)
redirect_to @waf_policy, notice: "Country blocking policy was successfully created for #{countries.join(', ')}."
else
@policy_types = WafPolicy::POLICY_TYPES
@@ -144,10 +137,22 @@ class WafPoliciesController < ApplicationController
private
def set_waf_policy
@waf_policy = WafPolicy.find(params[:id])
authorize @waf_policy
rescue ActiveRecord::RecordNotFound
redirect_to waf_policies_path, alert: 'WAF policy not found.'
# First try to find by ID (standard Rails behavior)
if params[:id] =~ /^\d+$/
@waf_policy = WafPolicy.find_by(id: params[:id])
end
# If not found by ID, try to find by parameterized name
unless @waf_policy
# Try direct parameterized comparison by parameterizing existing policy names
@waf_policy = WafPolicy.all.find { |policy| policy.to_param == params[:id] }
end
if @waf_policy
authorize @waf_policy
else
redirect_to waf_policies_path, alert: 'WAF policy not found.'
end
end
def waf_policy_params
@@ -155,7 +160,7 @@ class WafPoliciesController < ApplicationController
:name,
:description,
:policy_type,
:action,
:policy_action,
:enabled,
:expires_at,
targets: [],