Add WafPolicies
This commit is contained in:
@@ -32,10 +32,36 @@ class Event < ApplicationRecord
|
||||
scope :by_ip, ->(ip) { where(ip_address: ip) }
|
||||
scope :by_user_agent, ->(user_agent) { where(user_agent: user_agent) }
|
||||
scope :by_waf_action, ->(waf_action) { where(waf_action: waf_action) }
|
||||
scope :blocked, -> { where(waf_action: ['block', 'deny']) }
|
||||
scope :allowed, -> { where(waf_action: ['allow', 'pass']) }
|
||||
scope :blocked, -> { where(waf_action: :deny) }
|
||||
scope :allowed, -> { where(waf_action: :allow) }
|
||||
scope :rate_limited, -> { where(waf_action: 'rate_limit') }
|
||||
|
||||
# Network-based filtering scopes
|
||||
scope :by_company, ->(company) {
|
||||
joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.where("network_ranges.company ILIKE ?", "%#{company}%")
|
||||
}
|
||||
|
||||
scope :by_network_type, ->(type) {
|
||||
joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.case(type)
|
||||
.when("datacenter") { where("network_ranges.is_datacenter = ?", true) }
|
||||
.when("vpn") { where("network_ranges.is_vpn = ?", true) }
|
||||
.when("proxy") { where("network_ranges.is_proxy = ?", true) }
|
||||
.when("standard") { where("network_ranges.is_datacenter = ? AND network_ranges.is_vpn = ? AND network_ranges.is_proxy = ?", false, false, false) }
|
||||
.else { none }
|
||||
}
|
||||
|
||||
scope :by_asn, ->(asn) {
|
||||
joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.where("network_ranges.asn = ?", asn.to_i)
|
||||
}
|
||||
|
||||
scope :by_network_cidr, ->(cidr) {
|
||||
joins("JOIN network_ranges ON events.ip_address <<= network_ranges.network")
|
||||
.where("network_ranges.network = ?", cidr)
|
||||
}
|
||||
|
||||
# Path prefix matching using range queries (uses B-tree index efficiently)
|
||||
scope :with_path_prefix, ->(prefix_segment_ids) {
|
||||
return none if prefix_segment_ids.blank?
|
||||
@@ -112,10 +138,7 @@ class Event < ApplicationRecord
|
||||
server_name: normalized_payload["server_name"],
|
||||
environment: normalized_payload["environment"],
|
||||
|
||||
# Geographic data
|
||||
country_code: normalized_payload.dig("geo", "country_code"),
|
||||
city: normalized_payload.dig("geo", "city"),
|
||||
|
||||
|
||||
# WAF agent info
|
||||
agent_version: normalized_payload.dig("agent", "version"),
|
||||
agent_name: normalized_payload.dig("agent", "name")
|
||||
@@ -269,7 +292,7 @@ class Event < ApplicationRecord
|
||||
def matching_network_ranges
|
||||
return [] unless ip_address.present?
|
||||
|
||||
NetworkRange.contains_ip(ip_address).map do |range|
|
||||
NetworkRange.contains_ip(ip_address.to_s).map do |range|
|
||||
{
|
||||
range: range,
|
||||
cidr: range.cidr,
|
||||
@@ -360,86 +383,34 @@ class Event < ApplicationRecord
|
||||
active_blocking_rules.exists?
|
||||
end
|
||||
|
||||
# GeoIP enrichment methods (now uses network range data when available)
|
||||
def enrich_geo_location!
|
||||
return if ip_address.blank?
|
||||
return if country_code.present? # Already has geo data
|
||||
|
||||
# First try to get from network range
|
||||
network_info = network_intelligence
|
||||
if network_info[:country].present?
|
||||
update!(country_code: network_info[:country])
|
||||
return
|
||||
end
|
||||
|
||||
# Fallback to direct lookup
|
||||
country = GeoIpService.lookup_country(ip_address)
|
||||
update!(country_code: country) if country.present?
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to enrich geo location for event #{id}: #{e.message}"
|
||||
end
|
||||
|
||||
# Class method to enrich multiple events
|
||||
def self.enrich_geo_location_batch(events = nil)
|
||||
events ||= where(country_code: [nil, '']).where.not(ip_address: [nil, ''])
|
||||
updated_count = 0
|
||||
|
||||
events.find_each do |event|
|
||||
next if event.country_code.present?
|
||||
|
||||
# Try network range first
|
||||
network_info = event.network_intelligence
|
||||
if network_info[:country].present?
|
||||
event.update!(country_code: network_info[:country])
|
||||
updated_count += 1
|
||||
next
|
||||
end
|
||||
|
||||
# Fallback to direct lookup
|
||||
country = GeoIpService.lookup_country(event.ip_address)
|
||||
if country.present?
|
||||
event.update!(country_code: country)
|
||||
updated_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
updated_count
|
||||
end
|
||||
|
||||
# Lookup country code for this event's IP
|
||||
def lookup_country
|
||||
return country_code if country_code.present?
|
||||
return nil if ip_address.blank?
|
||||
|
||||
# First try network range
|
||||
network_info = network_intelligence
|
||||
return network_info[:country] if network_info[:country].present?
|
||||
|
||||
# Fallback to direct lookup
|
||||
GeoIpService.lookup_country(ip_address)
|
||||
rescue => e
|
||||
Rails.logger.error "GeoIP lookup failed for #{ip_address}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
# Check if event has valid geo location data
|
||||
def has_geo_data?
|
||||
country_code.present? || city.present? || network_intelligence[:country].present?
|
||||
end
|
||||
|
||||
# Get full geo location details
|
||||
# Get full geo location details from network range
|
||||
def geo_location
|
||||
network_info = network_intelligence
|
||||
|
||||
{
|
||||
country_code: country_code || network_info[:country],
|
||||
city: city,
|
||||
country_code: network_info[:country],
|
||||
ip_address: ip_address,
|
||||
has_data: has_geo_data?,
|
||||
has_data: network_info[:country].present?,
|
||||
network_intelligence: network_info
|
||||
}
|
||||
end
|
||||
|
||||
# Check if event has valid geo location data via network range
|
||||
def has_geo_data?
|
||||
network_intelligence[:country].present?
|
||||
end
|
||||
|
||||
# Lookup country code for this event's IP via network range
|
||||
def lookup_country
|
||||
return nil if ip_address.blank?
|
||||
|
||||
network_info = network_intelligence
|
||||
network_info[:country]
|
||||
rescue => e
|
||||
Rails.logger.error "Network lookup failed for #{ip_address}: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def should_normalize?
|
||||
@@ -483,11 +454,7 @@ class Event < ApplicationRecord
|
||||
self.server_name = payload["server_name"]
|
||||
self.environment = payload["environment"]
|
||||
|
||||
# Extract geographic data
|
||||
geo_data = payload.dig("geo") || {}
|
||||
self.country_code = geo_data["country_code"]
|
||||
self.city = geo_data["city"]
|
||||
|
||||
|
||||
# Extract agent info
|
||||
agent_data = payload.dig("agent") || {}
|
||||
self.agent_version = agent_data["version"]
|
||||
|
||||
@@ -73,6 +73,11 @@ class NetworkRange < ApplicationRecord
|
||||
addr.include?(':') ? 6 : 4
|
||||
end
|
||||
|
||||
def virtual?
|
||||
# Virtual networks are unsaved instances (not persisted to database)
|
||||
!persisted?
|
||||
end
|
||||
|
||||
def ipv4?
|
||||
family == 4
|
||||
end
|
||||
|
||||
@@ -7,12 +7,13 @@
|
||||
class Rule < ApplicationRecord
|
||||
# Rule types and actions
|
||||
RULE_TYPES = %w[network rate_limit path_pattern].freeze
|
||||
ACTIONS = %w[allow deny rate_limit redirect log].freeze
|
||||
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception].freeze
|
||||
ACTIONS = %w[allow deny rate_limit redirect log challenge].freeze
|
||||
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception policy].freeze
|
||||
|
||||
# Associations
|
||||
belongs_to :user
|
||||
belongs_to :network_range, optional: true
|
||||
belongs_to :waf_policy, optional: true
|
||||
|
||||
# Validations
|
||||
validates :rule_type, presence: true, inclusion: { in: RULE_TYPES }
|
||||
@@ -39,6 +40,8 @@ class Rule < ApplicationRecord
|
||||
scope :by_source, ->(source) { where(source: source) }
|
||||
scope :surgical_blocks, -> { where(source: "manual:surgical_block") }
|
||||
scope :surgical_exceptions, -> { where(source: "manual:surgical_exception") }
|
||||
scope :policy_generated, -> { where(source: "policy") }
|
||||
scope :from_waf_policy, ->(waf_policy) { where(waf_policy: waf_policy) }
|
||||
|
||||
# Sync queries
|
||||
scope :since, ->(timestamp) { where("updated_at >= ?", Time.at(timestamp)).order(:updated_at, :id) }
|
||||
@@ -94,6 +97,37 @@ class Rule < ApplicationRecord
|
||||
source == "manual:surgical_exception"
|
||||
end
|
||||
|
||||
# Policy-generated rule methods
|
||||
def policy_generated?
|
||||
source == "policy"
|
||||
end
|
||||
|
||||
# Action-specific methods
|
||||
def redirect_action?
|
||||
action == "redirect"
|
||||
end
|
||||
|
||||
def challenge_action?
|
||||
action == "challenge"
|
||||
end
|
||||
|
||||
# Redirect/challenge convenience methods
|
||||
def redirect_url
|
||||
metadata&.dig('redirect_url')
|
||||
end
|
||||
|
||||
def redirect_status
|
||||
metadata&.dig('redirect_status') || 302
|
||||
end
|
||||
|
||||
def challenge_type
|
||||
metadata&.dig('challenge_type') || 'captcha'
|
||||
end
|
||||
|
||||
def challenge_message
|
||||
metadata&.dig('challenge_message')
|
||||
end
|
||||
|
||||
def related_surgical_rules
|
||||
if surgical_block?
|
||||
# Find the corresponding exception rule
|
||||
@@ -365,6 +399,12 @@ class Rule < ApplicationRecord
|
||||
unless metadata&.dig("redirect_url").present?
|
||||
errors.add(:metadata, "must include 'redirect_url' for redirect action")
|
||||
end
|
||||
when "challenge"
|
||||
# Challenge is flexible - can use defaults
|
||||
challenge_type_value = metadata&.dig("challenge_type")
|
||||
if challenge_type_value && !%w[captcha javascript proof_of_work].include?(challenge_type_value)
|
||||
errors.add(:metadata, "challenge_type must be one of: captcha, javascript, proof_of_work")
|
||||
end
|
||||
when "rate_limit"
|
||||
unless metadata&.dig("limit").present? && metadata&.dig("window").present?
|
||||
errors.add(:metadata, "must include 'limit' and 'window' for rate_limit action")
|
||||
|
||||
399
app/models/waf_policy.rb
Normal file
399
app/models/waf_policy.rb
Normal file
@@ -0,0 +1,399 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# WafPolicy - High-level firewall policies that generate specific Rules
|
||||
#
|
||||
# WafPolicies contain strategic decisions like "Block Brazil" that automatically
|
||||
# generate specific Rules when matching network ranges are discovered.
|
||||
class WafPolicy < ApplicationRecord
|
||||
# Policy types - different categories of blocking rules
|
||||
POLICY_TYPES = %w[country asn company network_type].freeze
|
||||
|
||||
# Actions - what to do when traffic matches this policy
|
||||
ACTIONS = %w[allow deny redirect challenge].freeze
|
||||
|
||||
# Associations
|
||||
belongs_to :user
|
||||
has_many :generated_rules, class_name: 'Rule', dependent: :destroy
|
||||
|
||||
# Validations
|
||||
validates :name, presence: true, uniqueness: true
|
||||
validates :policy_type, presence: true, inclusion: { in: POLICY_TYPES }
|
||||
validates :action, presence: true, inclusion: { in: ACTIONS }
|
||||
validates :targets, presence: true
|
||||
validate :targets_must_be_array
|
||||
validates :user, presence: true
|
||||
validate :validate_targets_by_type
|
||||
validate :validate_redirect_configuration, if: :redirect_action?
|
||||
validate :validate_challenge_configuration, if: :challenge_action?
|
||||
|
||||
# Scopes
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
scope :disabled, -> { where(enabled: false) }
|
||||
scope :active, -> { enabled.where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
||||
scope :expired, -> { where("expires_at IS NOT NULL AND expires_at <= ?", Time.current) }
|
||||
scope :by_type, ->(type) { where(policy_type: type) }
|
||||
scope :country, -> { by_type('country') }
|
||||
scope :asn, -> { by_type('asn') }
|
||||
scope :company, -> { by_type('company') }
|
||||
scope :network_type, -> { by_type('network_type') }
|
||||
|
||||
# Callbacks
|
||||
before_validation :set_defaults
|
||||
|
||||
# Policy type methods
|
||||
def country_policy?
|
||||
policy_type == 'country'
|
||||
end
|
||||
|
||||
def asn_policy?
|
||||
policy_type == 'asn'
|
||||
end
|
||||
|
||||
def company_policy?
|
||||
policy_type == 'company'
|
||||
end
|
||||
|
||||
def network_type_policy?
|
||||
policy_type == 'network_type'
|
||||
end
|
||||
|
||||
# Action methods
|
||||
def allow_action?
|
||||
action == 'allow'
|
||||
end
|
||||
|
||||
def deny_action?
|
||||
action == 'deny'
|
||||
end
|
||||
|
||||
def redirect_action?
|
||||
action == 'redirect'
|
||||
end
|
||||
|
||||
def challenge_action?
|
||||
action == 'challenge'
|
||||
end
|
||||
|
||||
# Lifecycle methods
|
||||
def active?
|
||||
enabled? && !expired?
|
||||
end
|
||||
|
||||
def expired?
|
||||
expires_at.present? && expires_at <= Time.current
|
||||
end
|
||||
|
||||
def activate!
|
||||
update!(enabled: true)
|
||||
end
|
||||
|
||||
def deactivate!
|
||||
update!(enabled: false)
|
||||
end
|
||||
|
||||
def expire!
|
||||
update!(expires_at: Time.current)
|
||||
end
|
||||
|
||||
# Network range matching methods
|
||||
def matches_network_range?(network_range)
|
||||
return false unless active?
|
||||
|
||||
case policy_type
|
||||
when 'country'
|
||||
matches_country?(network_range)
|
||||
when 'asn'
|
||||
matches_asn?(network_range)
|
||||
when 'company'
|
||||
matches_company?(network_range)
|
||||
when 'network_type'
|
||||
matches_network_type?(network_range)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def create_rule_for_network_range(network_range)
|
||||
return nil unless matches_network_range?(network_range)
|
||||
|
||||
rule = Rule.create!(
|
||||
rule_type: 'network',
|
||||
action: action,
|
||||
network_range: network_range,
|
||||
waf_policy: self,
|
||||
user: user,
|
||||
source: "policy:#{name}",
|
||||
metadata: build_rule_metadata(network_range),
|
||||
priority: network_range.prefix_length
|
||||
)
|
||||
|
||||
# Handle redirect/challenge specific data
|
||||
if redirect_action? && additional_data['redirect_url']
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
redirect_url: additional_data['redirect_url'],
|
||||
redirect_status: additional_data['redirect_status'] || 302
|
||||
)
|
||||
)
|
||||
elsif challenge_action?
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
challenge_type: additional_data['challenge_type'] || 'captcha',
|
||||
challenge_message: additional_data['challenge_message']
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
rule
|
||||
end
|
||||
|
||||
# Class methods for creating common policies
|
||||
def self.create_country_policy(countries, action: 'deny', user:, **options)
|
||||
create!(
|
||||
name: "#{action.capitalize} #{countries.join(', ')}",
|
||||
policy_type: 'country',
|
||||
targets: Array(countries),
|
||||
action: action,
|
||||
user: user,
|
||||
**options
|
||||
)
|
||||
end
|
||||
|
||||
def self.create_asn_policy(asns, action: 'deny', user:, **options)
|
||||
create!(
|
||||
name: "#{action.capitalize} ASNs #{asns.join(', ')}",
|
||||
policy_type: 'asn',
|
||||
targets: Array(asns).map(&:to_i),
|
||||
action: action,
|
||||
user: user,
|
||||
**options
|
||||
)
|
||||
end
|
||||
|
||||
def self.create_company_policy(companies, action: 'deny', user:, **options)
|
||||
create!(
|
||||
name: "#{action.capitalize} #{companies.join(', ')}",
|
||||
policy_type: 'company',
|
||||
targets: Array(companies),
|
||||
action: action,
|
||||
user: user,
|
||||
**options
|
||||
)
|
||||
end
|
||||
|
||||
def self.create_network_type_policy(types, action: 'deny', user:, **options)
|
||||
create!(
|
||||
name: "#{action.capitalize} #{types.join(', ')}",
|
||||
policy_type: 'network_type',
|
||||
targets: Array(types),
|
||||
action: action,
|
||||
user: user,
|
||||
**options
|
||||
)
|
||||
end
|
||||
|
||||
# Redirect/challenge specific methods
|
||||
def redirect_url
|
||||
additional_data&.dig('redirect_url')
|
||||
end
|
||||
|
||||
def redirect_status
|
||||
additional_data&.dig('redirect_status') || 302
|
||||
end
|
||||
|
||||
def challenge_type
|
||||
additional_data&.dig('challenge_type') || 'captcha'
|
||||
end
|
||||
|
||||
def challenge_message
|
||||
additional_data&.dig('challenge_message')
|
||||
end
|
||||
|
||||
# Statistics and analytics
|
||||
def generated_rules_count
|
||||
generated_rules.count
|
||||
end
|
||||
|
||||
def active_rules_count
|
||||
generated_rules.active.count
|
||||
end
|
||||
|
||||
def effectiveness_stats
|
||||
recent_rules = generated_rules.where('created_at > ?', 7.days.ago)
|
||||
|
||||
{
|
||||
total_rules_generated: generated_rules_count,
|
||||
active_rules: active_rules_count,
|
||||
rules_last_7_days: recent_rules.count,
|
||||
policy_type: policy_type,
|
||||
action: action,
|
||||
targets_count: targets&.length || 0
|
||||
}
|
||||
end
|
||||
|
||||
# String representations
|
||||
def to_s
|
||||
name
|
||||
end
|
||||
|
||||
def to_param
|
||||
name.parameterize
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_defaults
|
||||
self.targets ||= []
|
||||
self.additional_data ||= {}
|
||||
self.enabled = true if enabled.nil?
|
||||
end
|
||||
|
||||
def targets_must_be_array
|
||||
unless targets.is_a?(Array)
|
||||
errors.add(:targets, "must be an array")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_targets_by_type
|
||||
return if targets.blank?
|
||||
|
||||
case policy_type
|
||||
when 'country'
|
||||
validate_country_targets
|
||||
when 'asn'
|
||||
validate_asn_targets
|
||||
when 'company'
|
||||
validate_company_targets
|
||||
when 'network_type'
|
||||
validate_network_type_targets
|
||||
end
|
||||
end
|
||||
|
||||
def validate_country_targets
|
||||
unless targets.all? { |target| target.is_a?(String) && target.match?(/\A[A-Z]{2}\z/) }
|
||||
errors.add(:targets, "must be valid ISO country codes (e.g., 'BR', 'US')")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_asn_targets
|
||||
unless targets.all? { |target| target.is_a?(Integer) && target > 0 }
|
||||
errors.add(:targets, "must be valid ASNs (positive integers)")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_company_targets
|
||||
unless targets.all? { |target| target.is_a?(String) && target.present? }
|
||||
errors.add(:targets, "must be valid company names")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_network_type_targets
|
||||
valid_types = %w[datacenter proxy vpn standard]
|
||||
unless targets.all? { |target| valid_types.include?(target) }
|
||||
errors.add(:targets, "must be one of: #{valid_types.join(', ')}")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_redirect_configuration
|
||||
if additional_data['redirect_url'].blank?
|
||||
errors.add(:additional_data, "must include 'redirect_url' for redirect action")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_challenge_configuration
|
||||
# Challenge is flexible - can use defaults if not specified
|
||||
valid_challenge_types = %w[captcha javascript proof_of_work]
|
||||
challenge_type_value = additional_data&.dig('challenge_type')
|
||||
|
||||
if challenge_type_value && !valid_challenge_types.include?(challenge_type_value)
|
||||
errors.add(:additional_data, "challenge_type must be one of: #{valid_challenge_types.join(', ')}")
|
||||
end
|
||||
end
|
||||
|
||||
# Matching logic for different policy types
|
||||
def matches_country?(network_range)
|
||||
country = network_range.country || network_range.inherited_intelligence[:country]
|
||||
targets.include?(country)
|
||||
end
|
||||
|
||||
def matches_asn?(network_range)
|
||||
asn = network_range.asn || network_range.inherited_intelligence[:asn]
|
||||
targets.include?(asn)
|
||||
end
|
||||
|
||||
def matches_company?(network_range)
|
||||
company = network_range.company || network_range.inherited_intelligence[:company]
|
||||
return false if company.blank?
|
||||
|
||||
targets.any? do |target_company|
|
||||
company.downcase.include?(target_company.downcase) ||
|
||||
target_company.downcase.include?(company.downcase)
|
||||
end
|
||||
end
|
||||
|
||||
def matches_network_type?(network_range)
|
||||
intelligence = network_range.inherited_intelligence
|
||||
|
||||
targets.any? do |target_type|
|
||||
case target_type
|
||||
when 'datacenter'
|
||||
intelligence[:is_datacenter] == true
|
||||
when 'proxy'
|
||||
intelligence[:is_proxy] == true
|
||||
when 'vpn'
|
||||
intelligence[:is_vpn] == true
|
||||
when 'standard'
|
||||
intelligence[:is_datacenter] == false &&
|
||||
intelligence[:is_proxy] == false &&
|
||||
intelligence[:is_vpn] == false
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_rule_metadata(network_range)
|
||||
base_metadata = {
|
||||
generated_by_policy: id,
|
||||
policy_name: name,
|
||||
policy_type: policy_type,
|
||||
matched_field: matched_field(network_range),
|
||||
matched_value: matched_value(network_range)
|
||||
}
|
||||
|
||||
base_metadata.merge!(additional_data || {})
|
||||
end
|
||||
|
||||
def matched_field(network_range)
|
||||
case policy_type
|
||||
when 'country'
|
||||
'country'
|
||||
when 'asn'
|
||||
'asn'
|
||||
when 'company'
|
||||
'company'
|
||||
when 'network_type'
|
||||
'network_type'
|
||||
else
|
||||
'unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def matched_value(network_range)
|
||||
case policy_type
|
||||
when 'country'
|
||||
network_range.country || network_range.inherited_intelligence[:country]
|
||||
when 'asn'
|
||||
network_range.asn || network_range.inherited_intelligence[:asn]
|
||||
when 'company'
|
||||
network_range.company || network_range.inherited_intelligence[:company]
|
||||
when 'network_type'
|
||||
intelligence = network_range.inherited_intelligence
|
||||
types = []
|
||||
types << 'datacenter' if intelligence[:is_datacenter]
|
||||
types << 'proxy' if intelligence[:is_proxy]
|
||||
types << 'vpn' if intelligence[:is_vpn]
|
||||
types.join(',') || 'standard'
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user