Yeh
This commit is contained in:
@@ -236,9 +236,36 @@ class Rule < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
# For path_pattern rules, include segment IDs and match type
|
||||
if path_pattern_rule?
|
||||
format[:conditions] = {
|
||||
segment_ids: path_segment_ids,
|
||||
match_type: path_match_type
|
||||
}
|
||||
end
|
||||
|
||||
format
|
||||
end
|
||||
|
||||
# Path pattern rule helper methods
|
||||
def path_segment_ids
|
||||
conditions&.dig("segment_ids") || []
|
||||
end
|
||||
|
||||
def path_match_type
|
||||
conditions&.dig("match_type")
|
||||
end
|
||||
|
||||
def path_segments_text
|
||||
return [] if path_segment_ids.empty?
|
||||
PathSegment.where(id: path_segment_ids).order(:id).pluck(:segment)
|
||||
end
|
||||
|
||||
def path_pattern_display
|
||||
return nil unless path_pattern_rule?
|
||||
"/" + path_segments_text.join("/")
|
||||
end
|
||||
|
||||
# Class methods for rule creation
|
||||
def self.create_network_rule(cidr, action: 'deny', user: nil, **options)
|
||||
network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')
|
||||
@@ -252,6 +279,42 @@ class Rule < ApplicationRecord
|
||||
)
|
||||
end
|
||||
|
||||
def self.create_path_pattern_rule(pattern:, match_type:, action: 'deny', user: nil, **options)
|
||||
# Parse pattern string to segments (case-insensitive)
|
||||
segments = pattern.split('/').reject(&:blank?).map(&:downcase)
|
||||
|
||||
if segments.empty?
|
||||
raise ArgumentError, "Pattern must contain at least one path segment"
|
||||
end
|
||||
|
||||
unless %w[exact prefix suffix contains].include?(match_type)
|
||||
raise ArgumentError, "Match type must be one of: exact, prefix, suffix, contains"
|
||||
end
|
||||
|
||||
# Find or create PathSegment entries
|
||||
segment_ids = segments.map do |seg|
|
||||
PathSegment.find_or_create_segment(seg).id
|
||||
end
|
||||
|
||||
create!(
|
||||
waf_rule_type: 'path_pattern',
|
||||
waf_action: action,
|
||||
conditions: {
|
||||
segment_ids: segment_ids,
|
||||
match_type: match_type,
|
||||
original_pattern: pattern
|
||||
},
|
||||
metadata: {
|
||||
segments: segments,
|
||||
pattern_display: "/" + segments.join("/")
|
||||
},
|
||||
user: user,
|
||||
source: options[:source] || 'manual',
|
||||
priority: options[:priority] || 50,
|
||||
**options.except(:source, :priority)
|
||||
)
|
||||
end
|
||||
|
||||
def self.create_surgical_block(ip_address, parent_cidr, user: nil, reason: nil, **options)
|
||||
# Create block rule for parent range
|
||||
network_range = NetworkRange.find_or_create_by_cidr(parent_cidr, user: user, source: 'user_created')
|
||||
@@ -418,10 +481,24 @@ class Rule < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_path_pattern_conditions
|
||||
patterns = conditions&.dig("patterns")
|
||||
segment_ids = conditions&.dig("segment_ids")
|
||||
match_type = conditions&.dig("match_type")
|
||||
|
||||
if patterns.blank? || !patterns.is_a?(Array)
|
||||
errors.add(:conditions, "must include 'patterns' array for path_pattern rules")
|
||||
if segment_ids.blank? || !segment_ids.is_a?(Array)
|
||||
errors.add(:conditions, "must include 'segment_ids' array for path_pattern rules")
|
||||
end
|
||||
|
||||
unless %w[exact prefix suffix contains].include?(match_type)
|
||||
errors.add(:conditions, "match_type must be one of: exact, prefix, suffix, contains")
|
||||
end
|
||||
|
||||
# Validate that all segment IDs exist
|
||||
if segment_ids.is_a?(Array) && segment_ids.any?
|
||||
existing_ids = PathSegment.where(id: segment_ids).pluck(:id)
|
||||
missing_ids = segment_ids - existing_ids
|
||||
if missing_ids.any?
|
||||
errors.add(:conditions, "references non-existent path segment IDs: #{missing_ids.join(', ')}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user