# frozen_string_literal: true # AnalyticsController - Overview dashboard with statistics and charts class AnalyticsController < ApplicationController # All actions require authentication def index authorize :analytics, :index? # Time period selector (default: last 24 hours) @time_period = params[:period]&.to_sym || :day @start_time = calculate_start_time(@time_period) # Core statistics @total_events = Event.where("timestamp >= ?", @start_time).count @total_rules = Rule.enabled.count @network_ranges_with_events = NetworkRange.with_events.count @total_network_ranges = NetworkRange.count # Event breakdown by action @event_breakdown = Event.where("timestamp >= ?", @start_time) .group(:waf_action) .count .transform_keys do |action_id| case action_id when 0 then 'allow' when 1 then 'deny' when 2 then 'redirect' when 3 then 'challenge' else 'unknown' end end # Top countries by event count @top_countries = 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") .count .sort_by { |_, count| -count } .first(10) # Top blocked IPs @top_blocked_ips = Event.where("timestamp >= ?", @start_time) .where(waf_action: 1) # deny action in enum .group(:ip_address) .count .sort_by { |_, count| -count } .first(10) # Network range intelligence breakdown @network_intelligence = { datacenter_ranges: NetworkRange.datacenter.count, vpn_ranges: NetworkRange.vpn.count, proxy_ranges: NetworkRange.proxy.count, total_ranges: NetworkRange.count } # Recent activity @recent_events = Event.recent.limit(10) @recent_rules = Rule.order(created_at: :desc).limit(5) # System health indicators @system_health = { total_users: User.count, active_rules: Rule.enabled.count, disabled_rules: Rule.where(enabled: false).count, recent_errors: Event.where("timestamp >= ? AND waf_action = ?", @start_time, 1).count # 1 = deny } # Prepare data for charts @chart_data = prepare_chart_data respond_to do |format| format.html format.turbo_stream end end def networks authorize :analytics, :index? # Time period selector (default: last 24 hours) @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") .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") .select("network_ranges.*, COUNT(events.id) as event_count, COUNT(DISTINCT events.ip_address) as unique_ips") .order("event_count DESC") .limit(50) # 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) # 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) # 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) # Suspicious network activity patterns @suspicious_patterns = calculate_suspicious_patterns(@start_time) respond_to do |format| format.html format.json { render json: network_analytics_json } end end private def calculate_start_time(period) case period when :hour 1.hour.ago when :day 24.hours.ago when :week 1.week.ago when :month 1.month.ago else 24.hours.ago end end def prepare_chart_data # Events over time (hourly buckets for last 24 hours) events_by_hour = Event.where("timestamp >= ?", 24.hours.ago) .group("DATE_TRUNC('hour', timestamp)") .count # Convert to chart format - keep everything in UTC for consistency timeline_data = (0..23).map do |hour_ago| hour_time = hour_ago.hours.ago hour_key = hour_time.utc.beginning_of_hour { # Store as ISO string for JavaScript to handle timezone conversion time_iso: hour_time.iso8601, total: events_by_hour[hour_key] || 0 } end # Action distribution for pie chart action_distribution = @event_breakdown.map do |action, count| { action: action.humanize, count: count, percentage: ((count.to_f / [@total_events, 1].max) * 100).round(1) } end { timeline: timeline_data, actions: action_distribution, countries: @top_countries.map { |country, count| { country: country, count: count } }, network_types: [ { type: "Datacenter", count: @network_intelligence[:datacenter_ranges] }, { type: "VPN", count: @network_intelligence[:vpn_ranges] }, { type: "Proxy", count: @network_intelligence[:proxy_ranges] }, { type: "Standard", count: @network_intelligence[:total_ranges] - @network_intelligence[:datacenter_ranges] - @network_intelligence[:vpn_ranges] - @network_intelligence[:proxy_ranges] } ] } end def calculate_network_type_stats(start_time) # Get all network types with their traffic statistics network_types = [ { type: 'datacenter', label: 'Datacenter' }, { type: 'vpn', label: 'VPN' }, { type: 'proxy', label: '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 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 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 results['standard'] = { label: 'Standard', networks: standard_stats.network_count, events: standard_stats.event_count, unique_ips: standard_stats.unique_ips, percentage: total_events > 0 ? ((standard_stats.event_count.to_f / total_events) * 100).round(1) : 0 } results end 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_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 patterns[:high_volume] = { count: high_volume_networks.count, networks: high_volume_networks.keys } # 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 patterns[:high_deny_rate] = { count: high_deny_networks.count, network_ids: high_deny_networks.map(&: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) patterns[:distributed_companies] = company_subnets.map do |company| { company: company.company, subnets: company.subnet_count } end patterns end def network_analytics_json { top_networks: @top_networks.map { |network| { id: network.id, cidr: network.cidr, company: network.company, asn: network.asn, country: network.country, network_type: network.network_type, event_count: network.event_count, unique_ips: network.unique_ips } }, network_breakdown: @network_breakdown, top_companies: @top_companies, top_asns: @top_asns, top_countries: @top_countries, suspicious_patterns: @suspicious_patterns } end end