376 lines
10 KiB
Ruby
376 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class RulesController < ApplicationController
|
|
# Follow proper before_action order:
|
|
# 1. Authentication/Authorization
|
|
# All actions require authentication
|
|
|
|
# 2. Resource loading
|
|
before_action :set_rule, only: [:show, :edit, :update, :disable, :enable]
|
|
|
|
# GET /rules
|
|
def index
|
|
@pagy, @rules = pagy(policy_scope(Rule).includes(:user, :network_range).order(created_at: :desc))
|
|
@waf_rule_types = Rule.waf_rule_types
|
|
@waf_actions = Rule.waf_actions
|
|
end
|
|
|
|
# GET /rules/new
|
|
def new
|
|
authorize Rule
|
|
@rule = Rule.new
|
|
|
|
# Pre-fill from URL parameters
|
|
if params[:network_range_id].present?
|
|
network_range = NetworkRange.find_by(id: params[:network_range_id])
|
|
@rule.network_range = network_range if network_range
|
|
end
|
|
|
|
if params[:cidr].present?
|
|
@rule.waf_rule_type = 'network'
|
|
end
|
|
|
|
@waf_rule_types = Rule.waf_rule_types
|
|
@waf_actions = Rule.waf_actions
|
|
end
|
|
|
|
# POST /rules
|
|
def create
|
|
authorize Rule
|
|
@rule = Rule.new(rule_params)
|
|
@rule.user = Current.user
|
|
@waf_rule_types = Rule.waf_rule_types
|
|
@waf_actions = Rule.waf_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|
|
|
range.user = Current.user
|
|
range.source = 'manual'
|
|
range.creation_reason = "Created for rule ##{@rule.id}"
|
|
end
|
|
@rule.network_range = network_range
|
|
end
|
|
|
|
# Calculate priority automatically based on rule type
|
|
calculate_rule_priority
|
|
|
|
if @rule.save
|
|
# 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
|
|
end
|
|
|
|
# GET /rules/:id
|
|
def show
|
|
authorize @rule
|
|
end
|
|
|
|
# GET /rules/:id/edit
|
|
def edit
|
|
authorize @rule
|
|
@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
|
|
|
|
# POST /rules/:id/disable
|
|
def disable
|
|
authorize @rule, :disable?
|
|
reason = params[:reason] || "Disabled manually"
|
|
@rule.disable!(reason: reason)
|
|
redirect_to @rule, notice: 'Rule was successfully disabled.'
|
|
end
|
|
|
|
# POST /rules/:id/enable
|
|
def enable
|
|
authorize @rule, :enable?
|
|
@rule.enable!
|
|
redirect_to @rule, notice: 'Rule was successfully enabled.'
|
|
end
|
|
|
|
private
|
|
|
|
def set_rule
|
|
@rule = Rule.find(params[:id])
|
|
end
|
|
|
|
def rule_params
|
|
permitted = [
|
|
:waf_rule_type,
|
|
:waf_action,
|
|
:metadata,
|
|
:expires_at,
|
|
:enabled,
|
|
:source,
|
|
:network_range_id
|
|
]
|
|
|
|
# Only include conditions for non-network rules
|
|
if params[:rule][:waf_rule_type] != 'network'
|
|
permitted << :conditions
|
|
end
|
|
|
|
params.require(:rule).permit(permitted)
|
|
end
|
|
|
|
def calculate_rule_priority
|
|
return unless @rule
|
|
|
|
case @rule.waf_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 'path_pattern'
|
|
@rule.priority = 85
|
|
when 'rate_limit'
|
|
@rule.priority = 70
|
|
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.redirect_action? && 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
|
|
|
|
# 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)
|
|
@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 = [
|
|
:waf_rule_type,
|
|
:waf_action,
|
|
:metadata,
|
|
:expires_at,
|
|
:enabled,
|
|
:source,
|
|
:network_range_id
|
|
]
|
|
|
|
# Only include conditions for non-network rules
|
|
if params[:rule][:waf_rule_type] != 'network'
|
|
permitted << :conditions
|
|
end
|
|
|
|
params.require(:rule).permit(permitted)
|
|
end
|
|
|
|
def calculate_rule_priority
|
|
return unless @rule
|
|
|
|
case @rule.waf_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 'path_pattern'
|
|
@rule.priority = 85
|
|
when 'rate_limit'
|
|
@rule.priority = 70
|
|
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.redirect_action? && 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 |