Many updates
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user