135 lines
4.3 KiB
Ruby
135 lines
4.3 KiB
Ruby
# 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
|
|
|
|
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
|
|
end |