diff --git a/app/controllers/analytics_controller.rb b/app/controllers/analytics_controller.rb index 4c3c7e6..f59b02f 100644 --- a/app/controllers/analytics_controller.rb +++ b/app/controllers/analytics_controller.rb @@ -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 diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 305fd16..9fe0ae8 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/network_ranges_controller.rb b/app/controllers/network_ranges_controller.rb index e056b9f..761a61f 100644 --- a/app/controllers/network_ranges_controller.rb +++ b/app/controllers/network_ranges_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/rules_controller.rb b/app/controllers/rules_controller.rb index 61d2068..94382fa 100644 --- a/app/controllers/rules_controller.rb +++ b/app/controllers/rules_controller.rb @@ -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 diff --git a/app/controllers/waf_policies_controller.rb b/app/controllers/waf_policies_controller.rb index 6a5bf75..e0e3337 100644 --- a/app/controllers/waf_policies_controller.rb +++ b/app/controllers/waf_policies_controller.rb @@ -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: [], diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 40b6b3b..39397e0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -139,4 +139,15 @@ module ApplicationHelper raw: user_agent } end + + # Convert country code to flag emoji + def country_flag(country_code) + return "" if country_code.blank? + + # Convert ISO 3166-1 alpha-2 country code to flag emoji + # Each letter is converted to its regional indicator symbol + country_code.upcase.chars.map { |c| (c.ord + 127397).chr(Encoding::UTF_8) }.join + rescue + "" + end end diff --git a/app/javascript/controllers/quick_create_rule_controller.js b/app/javascript/controllers/quick_create_rule_controller.js index 0502d9d..236a130 100644 --- a/app/javascript/controllers/quick_create_rule_controller.js +++ b/app/javascript/controllers/quick_create_rule_controller.js @@ -38,7 +38,7 @@ export default class extends Controller { this.hideOptionalFields() // Show relevant fields based on rule type - if (["path_pattern", "header_pattern", "query_pattern", "body_signature"].includes(ruleType)) { + if (["path_pattern"].includes(ruleType)) { if (this.hasPatternFieldsTarget) { this.patternFieldsTarget.classList.remove("hidden") this.updatePatternHelpText(ruleType) @@ -64,18 +64,6 @@ export default class extends Controller { path_pattern: { text: "Regex pattern to match URL paths (e.g.,\\.env$|wp-admin|phpmyadmin)", placeholder: "Example: \\.env$|\\.git|config\\.php|wp-admin" - }, - header_pattern: { - text: 'JSON with header_name and pattern (e.g., {"header_name": "User-Agent", "pattern": "bot.*"})', - placeholder: 'Example: {"header_name": "User-Agent", "pattern": ".*[Bb]ot.*"}' - }, - query_pattern: { - text: "Regex pattern to match query parameters (e.g., union.*select|