Add 'tags' to event model. Add a dataimport system - currently for MaxMind zip files
This commit is contained in:
177
app/services/waf_policy_matcher.rb
Normal file
177
app/services/waf_policy_matcher.rb
Normal file
@@ -0,0 +1,177 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# WafPolicyMatcher - Service to match NetworkRanges against active WafPolicies
|
||||
#
|
||||
# This service provides efficient matching of network ranges against firewall policies
|
||||
# and can generate rules when matches are found.
|
||||
class WafPolicyMatcher
|
||||
include ActiveModel::Model
|
||||
include ActiveModel::Attributes
|
||||
|
||||
attr_accessor :network_range
|
||||
attr_reader :matching_policies, :generated_rules
|
||||
|
||||
def initialize(network_range:)
|
||||
@network_range = network_range
|
||||
@matching_policies = []
|
||||
@generated_rules = []
|
||||
end
|
||||
|
||||
# Find all active policies that match the given network range
|
||||
def find_matching_policies
|
||||
return [] unless network_range.present?
|
||||
|
||||
@matching_policies = active_policies.select do |policy|
|
||||
policy.matches_network_range?(network_range)
|
||||
end
|
||||
|
||||
# Sort by priority: country > asn > company > network_type, then by creation date
|
||||
@matching_policies.sort_by do |policy|
|
||||
priority_score = case policy.policy_type
|
||||
when 'country'
|
||||
1
|
||||
when 'asn'
|
||||
2
|
||||
when 'company'
|
||||
3
|
||||
when 'network_type'
|
||||
4
|
||||
else
|
||||
99
|
||||
end
|
||||
|
||||
[priority_score, policy.created_at]
|
||||
end
|
||||
end
|
||||
|
||||
# Generate rules from matching policies
|
||||
def generate_rules
|
||||
return [] if matching_policies.empty?
|
||||
|
||||
@generated_rules = matching_policies.map do |policy|
|
||||
# Check if rule already exists for this network range and policy
|
||||
existing_rule = Rule.find_by(
|
||||
network_range: network_range,
|
||||
waf_policy: policy,
|
||||
enabled: true
|
||||
)
|
||||
|
||||
if existing_rule
|
||||
Rails.logger.debug "Rule already exists for network_range #{network_range.cidr} and policy #{policy.name}"
|
||||
existing_rule
|
||||
else
|
||||
rule = policy.create_rule_for_network_range(network_range)
|
||||
if rule
|
||||
Rails.logger.info "Generated rule for network_range #{network_range.cidr} from policy #{policy.name}"
|
||||
end
|
||||
rule
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
|
||||
# Find and generate rules in one step
|
||||
def match_and_generate_rules
|
||||
find_matching_policies
|
||||
generate_rules
|
||||
end
|
||||
|
||||
# Class methods for batch processing
|
||||
def self.process_network_range(network_range)
|
||||
matcher = new(network_range: network_range)
|
||||
matcher.match_and_generate_rules
|
||||
end
|
||||
|
||||
def self.batch_process_network_ranges(network_ranges)
|
||||
results = []
|
||||
|
||||
network_ranges.each do |network_range|
|
||||
matcher = new(network_range: network_range)
|
||||
result = matcher.match_and_generate_rules
|
||||
results << {
|
||||
network_range: network_range,
|
||||
matching_policies: matcher.matching_policies,
|
||||
generated_rules: matcher.generated_rules
|
||||
}
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
# Process network ranges that need policy evaluation
|
||||
def self.process_ranges_without_policy_rules(limit: 100)
|
||||
# Find network ranges that don't have policy-generated rules
|
||||
# but have intelligence data that could match policies
|
||||
ranges_needing_evaluation = NetworkRange
|
||||
.left_joins(:rules)
|
||||
.where("rules.id IS NULL OR rules.waf_policy_id IS NULL")
|
||||
.where("(country IS NOT NULL OR asn IS NOT NULL OR company IS NOT NULL OR is_datacenter = true OR is_proxy = true OR is_vpn = true)")
|
||||
.limit(limit)
|
||||
.includes(:rules)
|
||||
|
||||
batch_process_network_ranges(ranges_needing_evaluation)
|
||||
end
|
||||
|
||||
# Re-evaluate all network ranges for policy changes
|
||||
def self.reprocess_all_for_policy(waf_policy)
|
||||
# Find all network ranges that could potentially match this policy
|
||||
potential_ranges = case waf_policy.policy_type
|
||||
when 'country'
|
||||
NetworkRange.where(country: waf_policy.targets)
|
||||
when 'asn'
|
||||
NetworkRange.where(asn: waf_policy.targets)
|
||||
when 'network_type'
|
||||
NetworkRange.where(
|
||||
"is_datacenter = ? OR is_proxy = ? OR is_vpn = ?",
|
||||
waf_policy.targets.include?('datacenter'),
|
||||
waf_policy.targets.include?('proxy'),
|
||||
waf_policy.targets.include?('vpn')
|
||||
)
|
||||
when 'company'
|
||||
# For company matching, we need to do text matching
|
||||
NetworkRange.where("company ILIKE ANY (array[?])",
|
||||
waf_policy.targets.map { |c| "%#{c}%" })
|
||||
else
|
||||
NetworkRange.none
|
||||
end
|
||||
|
||||
results = []
|
||||
potential_ranges.find_each do |network_range|
|
||||
matcher = new(network_range: network_range)
|
||||
if waf_policy.matches_network_range?(network_range)
|
||||
rule = waf_policy.create_rule_for_network_range(network_range)
|
||||
results << { network_range: network_range, generated_rule: rule } if rule
|
||||
end
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
|
||||
# Statistics and reporting
|
||||
def self.matching_policies_for_network_range(network_range)
|
||||
matcher = new(network_range: network_range)
|
||||
matcher.find_matching_policies
|
||||
end
|
||||
|
||||
def self.policy_effectiveness_stats(waf_policy, days: 30)
|
||||
cutoff_date = days.days.ago
|
||||
|
||||
rules = waf_policy.generated_rules.where('created_at > ?', cutoff_date)
|
||||
|
||||
{
|
||||
policy_name: waf_policy.name,
|
||||
policy_type: waf_policy.policy_type,
|
||||
action: waf_policy.action,
|
||||
rules_generated: rules.count,
|
||||
active_rules: rules.active.count,
|
||||
networks_protected: rules.joins(:network_range).count('distinct network_ranges.id'),
|
||||
period_days: days,
|
||||
generation_rate: rules.count.to_f / days
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def active_policies
|
||||
@active_policies ||= WafPolicy.active
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user