This commit is contained in:
Dan Milne
2025-11-15 10:51:58 +11:00
parent d9701e4af6
commit 90823a1389
10 changed files with 425 additions and 84 deletions

View File

@@ -6,7 +6,7 @@
# 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
POLICY_TYPES = %w[country asn company network_type path_pattern].freeze
# Actions - what to do when traffic matches this policy
ACTIONS = %w[allow deny redirect challenge].freeze
@@ -57,6 +57,10 @@ validate :targets_must_be_array
policy_type == 'network_type'
end
def path_pattern_policy?
policy_type == 'path_pattern'
end
# Action methods
def allow_action?
policy_action == 'allow'
@@ -130,6 +134,21 @@ validate :targets_must_be_array
end
end
# Event matching methods (for path patterns)
def matches_event?(event)
return false unless active?
case policy_type
when 'country', 'asn', 'company', 'network_type'
# For network-based policies, use the event's network range
event.network_range && matches_network_range?(event.network_range)
when 'path_pattern'
matches_path_patterns?(event)
else
false
end
end
def create_rule_for_network_range(network_range)
return nil unless matches_network_range?(network_range)
@@ -164,6 +183,59 @@ validate :targets_must_be_array
rule
end
def create_rule_for_event(event)
return nil unless matches_event?(event)
# For path pattern policies, create a path_pattern rule
if path_pattern_policy?
# Check for existing path_pattern rule with same policy and patterns
existing_rule = Rule.find_by(
waf_rule_type: 'path_pattern',
waf_action: policy_action,
waf_policy: self,
enabled: true
)
if existing_rule
Rails.logger.debug "Path pattern rule already exists for policy #{name}"
return existing_rule
end
rule = Rule.create!(
waf_rule_type: 'path_pattern',
waf_action: policy_action,
waf_policy: self,
user: user,
source: "policy",
conditions: build_path_pattern_conditions(event),
metadata: build_path_pattern_metadata(event),
priority: 50 # Default priority for path rules
)
# 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
else
# For network-based policies, fall back to network range rule creation
create_rule_for_network_range(event.network_range)
end
end
# Class methods for creating common policies
def self.create_country_policy(countries, policy_action: 'deny', user:, **options)
create!(
@@ -209,6 +281,17 @@ validate :targets_must_be_array
)
end
def self.create_path_pattern_policy(patterns, policy_action: 'deny', user:, **options)
create!(
name: "#{policy_action.capitalize} path patterns: #{Array(patterns).join(', ')}",
policy_type: 'path_pattern',
targets: Array(patterns),
policy_action: policy_action,
user: user,
**options
)
end
# Redirect/challenge specific methods
def redirect_url
additional_data&.dig('redirect_url')
@@ -283,6 +366,8 @@ validate :targets_must_be_array
validate_company_targets
when 'network_type'
validate_network_type_targets
when 'path_pattern'
validate_path_pattern_targets
end
end
@@ -311,6 +396,24 @@ validate :targets_must_be_array
end
end
def validate_path_pattern_targets
unless targets.all? { |target| target.is_a?(String) && target.present? }
errors.add(:targets, "must be valid path pattern strings")
end
# Validate path patterns format (basic validation)
targets.each do |pattern|
begin
# Basic validation - ensure it's a reasonable pattern
unless pattern.match?(/\A[a-zA-Z0-9\-\._\*\/\?\[\]\{\}]+\z/)
errors.add(:targets, "contains invalid characters in pattern: #{pattern}")
end
rescue => e
errors.add(:targets, "invalid path pattern: #{pattern} - #{e.message}")
end
end
end
def validate_redirect_configuration
if additional_data['redirect_url'].blank?
errors.add(:additional_data, "must include 'redirect_url' for redirect action")
@@ -413,4 +516,62 @@ validate :targets_must_be_array
types.join(',') || 'standard'
end
end
# Path pattern matching methods
def matches_path_patterns?(event)
return false if event.request_path.blank?
path = event.request_path.downcase
targets.any? { |pattern| matches_path_pattern?(pattern, path) }
end
def matches_path_pattern?(pattern, path)
pattern = pattern.downcase
# Handle different pattern types
case pattern
when /\*/, /\?/, /\[/
# Glob patterns - simple matching
match_glob_pattern(pattern, path)
when /\.php$/, /\.exe$/, /\.js$/
# File extension patterns
path.end_with?(pattern)
when /\A\//
# Exact path match
path == pattern
else
# Simple substring match
path.include?(pattern)
end
end
def match_glob_pattern(pattern, path)
# Convert simple glob patterns to regex
regex_pattern = pattern
.gsub('*', '.*')
.gsub('?', '.')
.gsub('[', '\[')
.gsub(']', '\]')
path.match?(/\A#{regex_pattern}\z/)
end
def build_path_pattern_conditions(event)
{
"patterns" => targets,
"match_type" => "path_pattern"
}
end
def build_path_pattern_metadata(event)
base_metadata = {
generated_by_policy: id,
policy_name: name,
policy_type: policy_type,
matched_path: event.request_path,
generated_from: "event"
}
base_metadata.merge!(additional_data || {})
end
end