Tidy up homepage and navigation
This commit is contained in:
133
app/controllers/analytics_controller.rb
Normal file
133
app/controllers/analytics_controller.rb
Normal file
@@ -0,0 +1,133 @@
|
||||
# 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
|
||||
timeline_data = (0..23).map do |hour_ago|
|
||||
hour_time = hour_ago.hours.ago
|
||||
hour_key = hour_time.strftime("%Y-%m-%d %H:00:00")
|
||||
{
|
||||
time: hour_time.strftime("%H:00"),
|
||||
total: events_by_hour[hour_key] || 0
|
||||
}
|
||||
end.reverse
|
||||
|
||||
# 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
|
||||
@@ -4,17 +4,16 @@ class Api::EventsController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token
|
||||
allow_unauthenticated_access # Skip normal session auth, use DSN auth instead
|
||||
|
||||
# POST /api/:project_id/events
|
||||
# POST /api/events
|
||||
def create
|
||||
project = authenticate_project!
|
||||
return head :not_found unless project
|
||||
dsn = authenticate_dsn!
|
||||
return head :not_found unless dsn
|
||||
|
||||
# Parse the incoming WAF event data
|
||||
event_data = parse_event_data(request)
|
||||
|
||||
# Create event asynchronously
|
||||
ProcessWafEventJob.perform_later(
|
||||
project_id: project.id,
|
||||
event_data: event_data,
|
||||
headers: extract_serializable_headers(request)
|
||||
)
|
||||
@@ -64,8 +63,8 @@ class Api::EventsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def authenticate_project!
|
||||
DsnAuthenticationService.authenticate(request, params[:project_id])
|
||||
def authenticate_dsn!
|
||||
DsnAuthenticationService.authenticate(request)
|
||||
end
|
||||
|
||||
def parse_event_data(request)
|
||||
|
||||
@@ -7,11 +7,11 @@ module Api
|
||||
# These endpoints are kept for administrative/debugging purposes only
|
||||
|
||||
skip_before_action :verify_authenticity_token
|
||||
allow_unauthenticated_access # Skip normal session auth, use project key auth instead
|
||||
before_action :authenticate_project!
|
||||
before_action :check_project_enabled
|
||||
allow_unauthenticated_access # Skip normal session auth, use DSN auth instead
|
||||
before_action :authenticate_dsn!
|
||||
before_action :check_dsn_enabled
|
||||
|
||||
# GET /api/:public_key/rules/version
|
||||
# GET /api/rules/version
|
||||
# Quick version check - returns latest updated_at timestamp
|
||||
def version
|
||||
current_sampling = HubLoad.current_sampling
|
||||
@@ -24,9 +24,9 @@ module Api
|
||||
}
|
||||
end
|
||||
|
||||
# GET /api/:public_key/rules?since=1730646186
|
||||
# GET /api/rules?since=1730646186
|
||||
# Incremental sync - returns rules updated since timestamp (Unix timestamp in seconds)
|
||||
# GET /api/:public_key/rules
|
||||
# GET /api/rules
|
||||
# Full sync - returns all active rules
|
||||
def index
|
||||
rules = if params[:since].present?
|
||||
@@ -52,20 +52,20 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def authenticate_project!
|
||||
public_key = params[:public_key] || params[:project_id]
|
||||
def authenticate_dsn!
|
||||
@dsn = DsnAuthenticationService.authenticate(request)
|
||||
|
||||
@project = Project.find_by(public_key: public_key)
|
||||
|
||||
unless @project
|
||||
render json: { error: "Invalid project key" }, status: :unauthorized
|
||||
unless @dsn
|
||||
render json: { error: "Invalid DSN key" }, status: :unauthorized
|
||||
return
|
||||
end
|
||||
rescue DsnAuthenticationService::AuthenticationError => e
|
||||
render json: { error: e.message }, status: :unauthorized
|
||||
end
|
||||
|
||||
def check_project_enabled
|
||||
unless @project.enabled?
|
||||
render json: { error: "Project is disabled" }, status: :forbidden
|
||||
def check_dsn_enabled
|
||||
unless @dsn.enabled?
|
||||
render json: { error: "DSN is disabled" }, status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
95
app/controllers/dsns_controller.rb
Normal file
95
app/controllers/dsns_controller.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class DsnsController < ApplicationController
|
||||
before_action :require_authentication
|
||||
before_action :set_dsn, only: [:show, :edit, :update, :disable, :enable]
|
||||
before_action :authorize_dsn_management, except: [:index, :show]
|
||||
|
||||
# GET /dsns
|
||||
def index
|
||||
@dsns = policy_scope(Dsn).order(created_at: :desc)
|
||||
|
||||
# Generate environment DSNs using default DSN key or first enabled DSN
|
||||
default_dsn = Dsn.enabled.first
|
||||
if default_dsn
|
||||
@external_dsn = generate_external_dsn(default_dsn.key)
|
||||
@internal_dsn = generate_internal_dsn(default_dsn.key)
|
||||
end
|
||||
end
|
||||
|
||||
# GET /dsns/new
|
||||
def new
|
||||
authorize Dsn
|
||||
@dsn = Dsn.new
|
||||
end
|
||||
|
||||
# POST /dsns
|
||||
def create
|
||||
authorize Dsn
|
||||
@dsn = Dsn.new(dsn_params)
|
||||
|
||||
if @dsn.save
|
||||
redirect_to @dsn, notice: 'DSN was successfully created.'
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# GET /dsns/:id
|
||||
def show
|
||||
end
|
||||
|
||||
# GET /dsns/:id/edit
|
||||
def edit
|
||||
end
|
||||
|
||||
# PATCH/PUT /dsns/:id
|
||||
def update
|
||||
if @dsn.update(dsn_params)
|
||||
redirect_to @dsn, notice: 'DSN was successfully updated.'
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
# POST /dsns/:id/disable
|
||||
def disable
|
||||
@dsn.update!(enabled: false)
|
||||
redirect_to @dsn, notice: 'DSN was disabled.'
|
||||
end
|
||||
|
||||
# POST /dsns/:id/enable
|
||||
def enable
|
||||
@dsn.update!(enabled: true)
|
||||
redirect_to @dsn, notice: 'DSN was enabled.'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_dsn
|
||||
@dsn = Dsn.find(params[:id])
|
||||
end
|
||||
|
||||
def dsn_params
|
||||
params.require(:dsn).permit(:name, :enabled)
|
||||
end
|
||||
|
||||
def authorize_dsn_management
|
||||
# Only allow admins to manage DSNs
|
||||
redirect_to root_path, alert: 'Access denied' unless Current.user&.admin?
|
||||
end
|
||||
|
||||
def generate_external_dsn(key)
|
||||
host = ENV.fetch("BAFFLE_HOST", "localhost:3000")
|
||||
protocol = host.include?("localhost") ? "http" : "https"
|
||||
"#{protocol}://#{key}@#{host}"
|
||||
end
|
||||
|
||||
def generate_internal_dsn(key)
|
||||
internal_host = ENV.fetch("BAFFLE_INTERNAL_HOST", nil)
|
||||
return nil unless internal_host.present?
|
||||
|
||||
protocol = "http" # Internal connections use HTTP
|
||||
"#{protocol}://#{key}@#{internal_host}"
|
||||
end
|
||||
end
|
||||
@@ -1,21 +1,20 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EventsController < ApplicationController
|
||||
before_action :set_project
|
||||
|
||||
def index
|
||||
@events = @project.events.order(timestamp: :desc)
|
||||
Rails.logger.debug "Found project? #{@project.name} / #{@project.events.count} / #{@events.count}"
|
||||
@events = Event.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.where(country_code: params[:country]) if params[:country].present?
|
||||
|
||||
Rails.logger.debug "after filter #{@project.name} / #{@project.events.count} / #{@events.count}"
|
||||
Rails.logger.debug "Events count after filtering: #{@events.count}"
|
||||
|
||||
# Debug info
|
||||
Rails.logger.debug "Events count before pagination: #{@events.count}"
|
||||
Rails.logger.debug "Project: #{@project&.name} (ID: #{@project&.id})"
|
||||
|
||||
# Paginate
|
||||
@pagy, @events = pagy(@events, items: 50)
|
||||
@@ -23,11 +22,4 @@ class EventsController < ApplicationController
|
||||
Rails.logger.debug "Events count after pagination: #{@events.count}"
|
||||
Rails.logger.debug "Pagy info: #{@pagy.count} total, #{@pagy.pages} pages"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_project
|
||||
@project = Project.find(params[:project_id]) || Project.find_by(slug: params[:project_id])
|
||||
redirect_to projects_path, alert: "Project not found" unless @project
|
||||
end
|
||||
end
|
||||
@@ -7,11 +7,10 @@
|
||||
class NetworkRangesController < ApplicationController
|
||||
# Follow proper before_action order:
|
||||
# 1. Authentication/Authorization
|
||||
allow_unauthenticated_access only: [:index, :show, :lookup]
|
||||
# All actions require authentication
|
||||
|
||||
# 2. Resource loading
|
||||
before_action :set_network_range, only: [:show, :edit, :update, :destroy, :enrich]
|
||||
before_action :set_project, only: [:index, :show]
|
||||
|
||||
# GET /network_ranges
|
||||
def index
|
||||
@@ -158,15 +157,6 @@ class NetworkRangesController < ApplicationController
|
||||
@network_range = NetworkRange.find_by!(network: cidr)
|
||||
end
|
||||
|
||||
def set_project
|
||||
# For now, use the first project or create a default one
|
||||
@project = Project.first || Project.create!(
|
||||
name: 'Default Project',
|
||||
slug: 'default',
|
||||
public_key: SecureRandom.hex(32)
|
||||
)
|
||||
end
|
||||
|
||||
def network_range_params
|
||||
params.require(:network_range).permit(
|
||||
:network,
|
||||
@@ -204,18 +194,33 @@ class NetworkRangesController < ApplicationController
|
||||
end
|
||||
|
||||
def calculate_traffic_stats(network_range)
|
||||
# Calculate traffic statistics for this network range
|
||||
events = Event.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.where("network_ranges.id = ?", network_range.id)
|
||||
# Use the cached events_count for total requests (much more performant)
|
||||
# For detailed breakdown, we still need to query but we can optimize with a limit
|
||||
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
|
||||
|
||||
{
|
||||
total_requests: events.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: 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)
|
||||
}
|
||||
else
|
||||
# No events - return empty stats
|
||||
{
|
||||
total_requests: 0,
|
||||
unique_ips: 0,
|
||||
blocked_requests: 0,
|
||||
allowed_requests: 0,
|
||||
top_paths: {},
|
||||
top_user_agents: {},
|
||||
recent_activity: []
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,99 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProjectsController < ApplicationController
|
||||
before_action :set_project, only: [:show, :edit, :update, :events, :analytics]
|
||||
|
||||
def index
|
||||
@projects = Project.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
@recent_events = @project.recent_events(limit: 10)
|
||||
@event_count = @project.event_count(24.hours.ago)
|
||||
@blocked_count = @project.blocked_count(24.hours.ago)
|
||||
@waf_status = @project.waf_status
|
||||
end
|
||||
|
||||
def new
|
||||
@project = Project.new
|
||||
end
|
||||
|
||||
def create
|
||||
@project = Project.new(project_params)
|
||||
|
||||
if @project.save
|
||||
redirect_to @project, notice: "Project was successfully created. Use this DSN for your baffle-agent: #{@project.dsn}"
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
if @project.update(project_params)
|
||||
redirect_to @project, notice: "Project was successfully updated."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def events
|
||||
@events = @project.events.recent.includes(:project)
|
||||
|
||||
# Apply filters
|
||||
@events = @events.by_ip(params[:ip]) if params[:ip].present?
|
||||
@events = @events.by_waf_action(params[:action]) if params[:action].present?
|
||||
@events = @events.where(country_code: params[:country]) if params[:country].present?
|
||||
|
||||
# Debug info
|
||||
Rails.logger.debug "Events count before pagination: #{@events.count}"
|
||||
Rails.logger.debug "Project: #{@project&.name} (ID: #{@project&.id})"
|
||||
|
||||
# Paginate
|
||||
@pagy, @events = pagy(@events, items: 50)
|
||||
|
||||
Rails.logger.debug "Events count after pagination: #{@events.count}"
|
||||
Rails.logger.debug "Pagy info: #{@pagy.count} total, #{@pagy.pages} pages"
|
||||
end
|
||||
|
||||
def analytics
|
||||
@time_range = params[:time_range]&.to_i || 24 # hours
|
||||
|
||||
# Basic analytics
|
||||
@total_events = @project.event_count(@time_range.hours.ago)
|
||||
@blocked_events = @project.blocked_count(@time_range.hours.ago)
|
||||
@allowed_events = @project.allowed_count(@time_range.hours.ago)
|
||||
|
||||
# Top blocked IPs
|
||||
@top_blocked_ips = @project.top_blocked_ips(limit: 10, time_range: @time_range.hours.ago)
|
||||
|
||||
# Country distribution
|
||||
@country_stats = @project.events
|
||||
.where(timestamp: @time_range.hours.ago..Time.current)
|
||||
.where.not(country_code: nil)
|
||||
.group(:country_code)
|
||||
.select('country_code, COUNT(*) as count')
|
||||
.order('count DESC')
|
||||
.limit(10)
|
||||
|
||||
# Action distribution
|
||||
@action_stats = @project.events
|
||||
.where(timestamp: @time_range.hours.ago..Time.current)
|
||||
.group(:waf_action)
|
||||
.select('waf_action as action, COUNT(*) as count')
|
||||
.order('count DESC')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_project
|
||||
@project = Project.find_by(slug: params[:id]) || Project.find_by(id: params[:id])
|
||||
redirect_to projects_path, alert: "Project not found" unless @project
|
||||
end
|
||||
|
||||
def project_params
|
||||
params.require(:project).permit(:name, :enabled, settings: {})
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
class RegistrationsController < ApplicationController
|
||||
layout "authentication"
|
||||
allow_unauthenticated_access only: [:new, :create]
|
||||
before_action :ensure_no_users_exist, only: [:new, :create]
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
class RulesController < ApplicationController
|
||||
# Follow proper before_action order:
|
||||
# 1. Authentication/Authorization
|
||||
allow_unauthenticated_access only: [:index, :show]
|
||||
# All actions require authentication
|
||||
|
||||
# 2. Resource loading
|
||||
before_action :set_rule, only: [:show, :edit, :update, :disable, :enable]
|
||||
before_action :set_project, only: [:index, :show]
|
||||
|
||||
# GET /rules
|
||||
def index
|
||||
@rules = policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc)
|
||||
@pagy, @rules = pagy(policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc))
|
||||
@rule_types = Rule::RULE_TYPES
|
||||
@actions = Rule::ACTIONS
|
||||
end
|
||||
@@ -43,6 +42,9 @@ class RulesController < ApplicationController
|
||||
@rule_types = Rule::RULE_TYPES
|
||||
@actions = Rule::ACTIONS
|
||||
|
||||
# Process additional form data for quick create
|
||||
process_quick_create_parameters
|
||||
|
||||
# Handle network range creation if CIDR is provided
|
||||
if params[:cidr].present? && @rule.network_rule?
|
||||
network_range = NetworkRange.find_or_create_by(cidr: params[:cidr]) do |range|
|
||||
@@ -53,8 +55,17 @@ class RulesController < ApplicationController
|
||||
@rule.network_range = network_range
|
||||
end
|
||||
|
||||
# Calculate priority automatically based on rule type
|
||||
calculate_rule_priority
|
||||
|
||||
if @rule.save
|
||||
redirect_to @rule, notice: 'Rule was successfully created.'
|
||||
# For quick create from NetworkRange page, redirect back to network range
|
||||
if params[:rule][:network_range_id].present? && request.referer&.include?('/network_ranges/')
|
||||
network_range = NetworkRange.find(params[:rule][:network_range_id])
|
||||
redirect_to network_range, notice: 'Rule was successfully created.'
|
||||
else
|
||||
redirect_to @rule, notice: 'Rule was successfully created.'
|
||||
end
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
@@ -122,13 +133,236 @@ class RulesController < ApplicationController
|
||||
params.require(:rule).permit(permitted)
|
||||
end
|
||||
|
||||
def set_project
|
||||
# For now, use the first project or create a default one
|
||||
@project = Project.first || Project.create!(
|
||||
name: 'Default Project',
|
||||
slug: 'default',
|
||||
public_key: SecureRandom.hex(32)
|
||||
)
|
||||
def calculate_rule_priority
|
||||
return unless @rule
|
||||
|
||||
case @rule.rule_type
|
||||
when 'network'
|
||||
# For network rules, priority based on prefix specificity
|
||||
if @rule.network_range
|
||||
prefix = @rule.network_range.prefix_length
|
||||
@rule.priority = case prefix
|
||||
when 32 then 200 # /32 single IP
|
||||
when 31 then 190
|
||||
when 30 then 180
|
||||
when 29 then 170
|
||||
when 28 then 160
|
||||
when 27 then 150
|
||||
when 26 then 140
|
||||
when 25 then 130
|
||||
when 24 then 120
|
||||
when 23 then 110
|
||||
when 22 then 100
|
||||
when 21 then 90
|
||||
when 20 then 80
|
||||
when 19 then 70
|
||||
when 18 then 60
|
||||
when 17 then 50
|
||||
when 16 then 40
|
||||
when 15 then 30
|
||||
when 14 then 20
|
||||
when 13 then 10
|
||||
else 0
|
||||
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
|
||||
end
|
||||
|
||||
def process_quick_create_parameters
|
||||
return unless @rule
|
||||
|
||||
# Handle rate limiting parameters
|
||||
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
|
||||
rate_limit_data = {
|
||||
limit: params[:rate_limit].to_i,
|
||||
window_seconds: params[:rate_window].to_i,
|
||||
scope: 'per_ip'
|
||||
}
|
||||
|
||||
# Update conditions with rate limit data
|
||||
@rule.conditions ||= {}
|
||||
@rule.conditions.merge!(rate_limit_data)
|
||||
end
|
||||
|
||||
end
|
||||
# Handle redirect URL
|
||||
if @rule.action == 'redirect' && params[:redirect_url].present?
|
||||
@rule.metadata ||= {}
|
||||
if @rule.metadata.is_a?(String)
|
||||
begin
|
||||
@rule.metadata = JSON.parse(@rule.metadata)
|
||||
rescue JSON::ParserError
|
||||
@rule.metadata = {}
|
||||
end
|
||||
end
|
||||
@rule.metadata.merge!({
|
||||
redirect_url: params[:redirect_url],
|
||||
redirect_status: 302
|
||||
})
|
||||
end
|
||||
|
||||
# Parse metadata if it's a string that looks like JSON
|
||||
if @rule.metadata.is_a?(String) && @rule.metadata.starts_with?('{')
|
||||
begin
|
||||
@rule.metadata = JSON.parse(@rule.metadata)
|
||||
rescue JSON::ParserError
|
||||
# Keep as string if not valid JSON
|
||||
end
|
||||
end
|
||||
|
||||
# Add reason to metadata if provided
|
||||
if params.dig(:rule, :metadata).present?
|
||||
if @rule.metadata.is_a?(Hash)
|
||||
@rule.metadata['reason'] = params[:rule][:metadata]
|
||||
else
|
||||
@rule.metadata = { 'reason' => params[:rule][:metadata] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_rule
|
||||
@rule = Rule.find(params[:id])
|
||||
end
|
||||
|
||||
def rule_params
|
||||
permitted = [
|
||||
:rule_type,
|
||||
:action,
|
||||
:metadata,
|
||||
:expires_at,
|
||||
:enabled,
|
||||
:source,
|
||||
:network_range_id
|
||||
]
|
||||
|
||||
# Only include conditions for non-network rules
|
||||
if params[:rule][:rule_type] != 'network'
|
||||
permitted << :conditions
|
||||
end
|
||||
|
||||
params.require(:rule).permit(permitted)
|
||||
end
|
||||
|
||||
def calculate_rule_priority
|
||||
return unless @rule
|
||||
|
||||
case @rule.rule_type
|
||||
when 'network'
|
||||
# For network rules, priority based on prefix specificity
|
||||
if @rule.network_range
|
||||
prefix = @rule.network_range.prefix_length
|
||||
@rule.priority = case prefix
|
||||
when 32 then 200 # /32 single IP
|
||||
when 31 then 190
|
||||
when 30 then 180
|
||||
when 29 then 170
|
||||
when 28 then 160
|
||||
when 27 then 150
|
||||
when 26 then 140
|
||||
when 25 then 130
|
||||
when 24 then 120
|
||||
when 23 then 110
|
||||
when 22 then 100
|
||||
when 21 then 90
|
||||
when 20 then 80
|
||||
when 19 then 70
|
||||
when 18 then 60
|
||||
when 17 then 50
|
||||
when 16 then 40
|
||||
when 15 then 30
|
||||
when 14 then 20
|
||||
when 13 then 10
|
||||
else 0
|
||||
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
|
||||
end
|
||||
|
||||
def process_quick_create_parameters
|
||||
return unless @rule
|
||||
|
||||
# Handle rate limiting parameters
|
||||
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
|
||||
rate_limit_data = {
|
||||
limit: params[:rate_limit].to_i,
|
||||
window_seconds: params[:rate_window].to_i,
|
||||
scope: 'per_ip'
|
||||
}
|
||||
|
||||
# Update conditions with rate limit data
|
||||
@rule.conditions ||= {}
|
||||
@rule.conditions.merge!(rate_limit_data)
|
||||
end
|
||||
|
||||
# Handle redirect URL
|
||||
if @rule.action == 'redirect' && params[:redirect_url].present?
|
||||
@rule.metadata ||= {}
|
||||
if @rule.metadata.is_a?(String)
|
||||
begin
|
||||
@rule.metadata = JSON.parse(@rule.metadata)
|
||||
rescue JSON::ParserError
|
||||
@rule.metadata = {}
|
||||
end
|
||||
end
|
||||
@rule.metadata.merge!({
|
||||
redirect_url: params[:redirect_url],
|
||||
redirect_status: 302
|
||||
})
|
||||
end
|
||||
|
||||
# Parse metadata if it's a string that looks like JSON
|
||||
if @rule.metadata.is_a?(String) && @rule.metadata.starts_with?('{')
|
||||
begin
|
||||
@rule.metadata = JSON.parse(@rule.metadata)
|
||||
rescue JSON::ParserError
|
||||
# Keep as string if not valid JSON
|
||||
end
|
||||
end
|
||||
|
||||
# Add reason to metadata if provided
|
||||
if params.dig(:rule, :metadata).present?
|
||||
if @rule.metadata.is_a?(Hash)
|
||||
@rule.metadata['reason'] = params[:rule][:metadata]
|
||||
else
|
||||
@rule.metadata = { 'reason' => params[:rule][:metadata] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
class SessionsController < ApplicationController
|
||||
layout "authentication"
|
||||
allow_unauthenticated_access only: %i[ new create ]
|
||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: "Try again later." }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user