Add WafPolicies
This commit is contained in:
@@ -76,6 +76,57 @@ class AnalyticsController < ApplicationController
|
||||
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)
|
||||
@@ -132,4 +183,132 @@ class AnalyticsController < ApplicationController
|
||||
]
|
||||
}
|
||||
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
|
||||
@@ -11,6 +11,12 @@ class EventsController < ApplicationController
|
||||
@events = @events.by_waf_action(params[:waf_action]) if params[:waf_action].present?
|
||||
@events = @events.where(country_code: params[:country]) if params[:country].present?
|
||||
|
||||
# Network-based filters
|
||||
@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?
|
||||
@events = @events.by_network_cidr(params[:network_cidr]) if params[:network_cidr].present?
|
||||
|
||||
Rails.logger.debug "Events count after filtering: #{@events.count}"
|
||||
|
||||
# Debug info
|
||||
@@ -19,7 +25,24 @@ 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
|
||||
|
||||
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
|
||||
@@ -14,17 +14,20 @@ class NetworkRangesController < ApplicationController
|
||||
|
||||
# GET /network_ranges
|
||||
def index
|
||||
@pagy, @network_ranges = pagy(policy_scope(NetworkRange.includes(:rules))
|
||||
.order(updated_at: :desc))
|
||||
# Start with base scope
|
||||
base_scope = policy_scope(NetworkRange.includes(:rules)).order(updated_at: :desc)
|
||||
|
||||
# Apply filters
|
||||
@network_ranges = apply_filters(@network_ranges)
|
||||
# Apply filters BEFORE pagination
|
||||
base_scope = apply_filters(base_scope)
|
||||
|
||||
# Apply search
|
||||
# Apply search BEFORE pagination
|
||||
if params[:search].present?
|
||||
@network_ranges = search_network_ranges(@network_ranges, params[:search])
|
||||
base_scope = search_network_ranges(base_scope, params[:search])
|
||||
end
|
||||
|
||||
# Apply pagination to the filtered scope
|
||||
@pagy, @network_ranges = pagy(base_scope)
|
||||
|
||||
# Statistics
|
||||
@total_ranges = NetworkRange.count
|
||||
@ranges_with_intelligence = NetworkRange.where.not(asn: nil).or(NetworkRange.where.not(company: nil)).count
|
||||
@@ -41,14 +44,23 @@ class NetworkRangesController < ApplicationController
|
||||
# GET /network_ranges/:id
|
||||
def show
|
||||
authorize @network_range
|
||||
@related_events = Event.joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.where("network_ranges.id = ?", @network_range.id)
|
||||
.recent
|
||||
.limit(100)
|
||||
|
||||
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)
|
||||
else
|
||||
# Virtual network - find events by IP range containment
|
||||
@related_events = Event.where("ip_address <<= ?::inet", @network_range.to_s)
|
||||
.recent
|
||||
.limit(100)
|
||||
end
|
||||
|
||||
@child_ranges = @network_range.child_ranges.limit(20)
|
||||
@parent_ranges = @network_range.parent_ranges.limit(10)
|
||||
@associated_rules = @network_range.rules.includes(:user).order(created_at: :desc)
|
||||
@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)
|
||||
@@ -57,7 +69,7 @@ class NetworkRangesController < ApplicationController
|
||||
# GET /network_ranges/new
|
||||
def new
|
||||
authorize NetworkRange
|
||||
@network_range = NetworkRange.new
|
||||
@network_range = NetworkRange.new(network: params[:network])
|
||||
end
|
||||
|
||||
# POST /network_ranges
|
||||
@@ -154,7 +166,12 @@ class NetworkRangesController < ApplicationController
|
||||
def set_network_range
|
||||
# Handle CIDR slugs (e.g., "40.77.167.100_32" -> "40.77.167.100/32")
|
||||
cidr = params[:id].gsub('_', '/')
|
||||
@network_range = NetworkRange.find_by!(network: cidr)
|
||||
@network_range = NetworkRange.find_by(network: cidr)
|
||||
|
||||
# If network doesn't exist, create a virtual (unsaved) instance
|
||||
if @network_range.nil?
|
||||
@network_range = NetworkRange.new(network: cidr)
|
||||
end
|
||||
end
|
||||
|
||||
def network_range_params
|
||||
@@ -194,15 +211,43 @@ class NetworkRangesController < ApplicationController
|
||||
end
|
||||
|
||||
def calculate_traffic_stats(network_range)
|
||||
# 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
|
||||
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
|
||||
|
||||
{
|
||||
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
|
||||
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
|
||||
|
||||
total_events = Event.where("ip_address <<= ?::inet", network_range.to_s).count
|
||||
|
||||
{
|
||||
total_requests: network_range.events_count, # Use cached count
|
||||
total_requests: total_events,
|
||||
unique_ips: events.distinct.count(:ip_address),
|
||||
blocked_requests: events.blocked.count,
|
||||
allowed_requests: events.allowed.count,
|
||||
@@ -210,17 +255,6 @@ class NetworkRangesController < ApplicationController
|
||||
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
|
||||
165
app/controllers/waf_policies_controller.rb
Normal file
165
app/controllers/waf_policies_controller.rb
Normal file
@@ -0,0 +1,165 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WafPoliciesController < ApplicationController
|
||||
# Follow proper before_action order:
|
||||
# 1. Authentication/Authorization
|
||||
# All actions require authentication
|
||||
|
||||
# 2. Resource loading
|
||||
before_action :set_waf_policy, only: [:show, :edit, :update, :destroy, :activate, :deactivate]
|
||||
|
||||
# GET /waf_policies
|
||||
def index
|
||||
@pagy, @waf_policies = pagy(policy_scope(WafPolicy).includes(:user, :generated_rules).order(created_at: :desc))
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@actions = WafPolicy::ACTIONS
|
||||
end
|
||||
|
||||
# GET /waf_policies/new
|
||||
def new
|
||||
authorize WafPolicy
|
||||
@waf_policy = WafPolicy.new
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@actions = WafPolicy::ACTIONS
|
||||
|
||||
# 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.targets = params[:targets] if params[:targets].present?
|
||||
end
|
||||
|
||||
# POST /waf_policies
|
||||
def create
|
||||
authorize WafPolicy
|
||||
@waf_policy = WafPolicy.new(waf_policy_params)
|
||||
@waf_policy.user = Current.user
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@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
|
||||
end
|
||||
end
|
||||
|
||||
# GET /waf_policies/:id
|
||||
def show
|
||||
@generated_rules = @waf_policy.generated_rules.includes(:network_range).order(created_at: :desc).limit(20)
|
||||
@effectiveness_stats = @waf_policy.effectiveness_stats
|
||||
end
|
||||
|
||||
# GET /waf_policies/:id/edit
|
||||
def edit
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@actions = WafPolicy::ACTIONS
|
||||
end
|
||||
|
||||
# PATCH/PUT /waf_policies/:id
|
||||
def update
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@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
|
||||
end
|
||||
end
|
||||
|
||||
# DELETE /waf_policies/:id
|
||||
def destroy
|
||||
policy_name = @waf_policy.name
|
||||
|
||||
# Soft delete by disabling and expiring the policy
|
||||
@waf_policy.update!(enabled: false, expires_at: Time.current)
|
||||
|
||||
redirect_to waf_policies_url, notice: "WAF policy '#{policy_name}' was disabled."
|
||||
end
|
||||
|
||||
# POST /waf_policies/:id/activate
|
||||
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
|
||||
|
||||
# POST /waf_policies/:id/deactivate
|
||||
def deactivate
|
||||
@waf_policy.deactivate!
|
||||
|
||||
redirect_to @waf_policy, notice: 'WAF policy was deactivated.'
|
||||
end
|
||||
|
||||
# GET /waf_policies/new_country
|
||||
def new_country
|
||||
authorize WafPolicy
|
||||
@waf_policy = WafPolicy.new(policy_type: 'country', action: 'deny')
|
||||
@policy_types = WafPolicy::POLICY_TYPES
|
||||
@actions = WafPolicy::ACTIONS
|
||||
end
|
||||
|
||||
# POST /waf_policies/create_country
|
||||
def create_country
|
||||
authorize WafPolicy
|
||||
|
||||
countries = params[:countries]&.reject(&:blank?) || []
|
||||
action = params[: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,
|
||||
user: Current.user,
|
||||
description: params[:description]
|
||||
)
|
||||
|
||||
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
|
||||
@actions = WafPolicy::ACTIONS
|
||||
render :new_country, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
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.'
|
||||
end
|
||||
|
||||
def waf_policy_params
|
||||
params.require(:waf_policy).permit(
|
||||
:name,
|
||||
:description,
|
||||
:policy_type,
|
||||
:action,
|
||||
:enabled,
|
||||
:expires_at,
|
||||
targets: [],
|
||||
additional_data: {}
|
||||
)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user