path-matching #1
@@ -9,6 +9,8 @@
|
|||||||
* Consider organizing styles into separate files for maintainability.
|
* Consider organizing styles into separate files for maintainability.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@import "tom-select.css";
|
||||||
|
|
||||||
/* JSON Validator Styles */
|
/* JSON Validator Styles */
|
||||||
.json-valid {
|
.json-valid {
|
||||||
border-color: #10b981 !important;
|
border-color: #10b981 !important;
|
||||||
|
|||||||
@@ -46,8 +46,23 @@ class Api::EventsController < ApplicationController
|
|||||||
rules = Rule.active.sync_order
|
rules = Rule.active.sync_order
|
||||||
end
|
end
|
||||||
|
|
||||||
response_data[:rules] = rules.map(&:to_agent_format)
|
agent_rules = rules.map(&:to_agent_format)
|
||||||
|
response_data[:rules] = agent_rules
|
||||||
response_data[:rules_changed] = true
|
response_data[:rules_changed] = true
|
||||||
|
|
||||||
|
# Include path segments dictionary for path_pattern rules
|
||||||
|
path_segment_ids = agent_rules
|
||||||
|
.select { |r| r[:waf_rule_type] == 'path_pattern' }
|
||||||
|
.flat_map { |r| r.dig(:conditions, :segment_ids) }
|
||||||
|
.compact
|
||||||
|
.uniq
|
||||||
|
|
||||||
|
if path_segment_ids.any?
|
||||||
|
response_data[:path_segments] = PathSegment
|
||||||
|
.where(id: path_segment_ids)
|
||||||
|
.pluck(:id, :segment)
|
||||||
|
.to_h
|
||||||
|
end
|
||||||
else
|
else
|
||||||
response_data[:rules_changed] = false
|
response_data[:rules_changed] = false
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -189,6 +189,38 @@ end
|
|||||||
def process_quick_create_parameters
|
def process_quick_create_parameters
|
||||||
return unless @rule
|
return unless @rule
|
||||||
|
|
||||||
|
# Handle path pattern parameters
|
||||||
|
if @rule.path_pattern_rule? && params[:path_pattern].present? && params[:match_type].present?
|
||||||
|
begin
|
||||||
|
pattern = params[:path_pattern]
|
||||||
|
match_type = params[:match_type]
|
||||||
|
|
||||||
|
# Parse pattern to segments
|
||||||
|
segments = pattern.split('/').reject(&:blank?).map(&:downcase)
|
||||||
|
|
||||||
|
# Find or create PathSegment entries
|
||||||
|
segment_ids = segments.map do |seg|
|
||||||
|
PathSegment.find_or_create_segment(seg).id
|
||||||
|
end
|
||||||
|
|
||||||
|
# Set conditions with segment IDs and match type
|
||||||
|
@rule.conditions = {
|
||||||
|
segment_ids: segment_ids,
|
||||||
|
match_type: match_type,
|
||||||
|
original_pattern: pattern
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add to metadata for display
|
||||||
|
@rule.metadata ||= {}
|
||||||
|
@rule.metadata.merge!({
|
||||||
|
segments: segments,
|
||||||
|
pattern_display: "/" + segments.join("/")
|
||||||
|
})
|
||||||
|
rescue => e
|
||||||
|
@rule.errors.add(:base, "Failed to create path pattern: #{e.message}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Handle rate limiting parameters
|
# Handle rate limiting parameters
|
||||||
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
|
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
|
||||||
rate_limit_data = {
|
rate_limit_data = {
|
||||||
@@ -322,55 +354,4 @@ end
|
|||||||
@rule.priority = 50 # Default priority
|
@rule.priority = 50 # Default priority
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_quick_create_parameters
|
|
||||||
return unless @rule
|
|
||||||
|
|
||||||
# Handle rate limiting parameters
|
|
||||||
if @rule.rate_limit_rule? && params[:rate_limit].present? && params[:rate_window].present?
|
|
||||||
rate_limit_data = {
|
|
||||||
limit: params[:rate_limit].to_i,
|
|
||||||
window_seconds: params[:rate_window].to_i,
|
|
||||||
scope: 'per_ip'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Update conditions with rate limit data
|
|
||||||
@rule.conditions ||= {}
|
|
||||||
@rule.conditions.merge!(rate_limit_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Handle redirect URL
|
|
||||||
if @rule.redirect_action? && params[:redirect_url].present?
|
|
||||||
@rule.metadata ||= {}
|
|
||||||
if @rule.metadata.is_a?(String)
|
|
||||||
begin
|
|
||||||
@rule.metadata = JSON.parse(@rule.metadata)
|
|
||||||
rescue JSON::ParserError
|
|
||||||
@rule.metadata = {}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@rule.metadata.merge!({
|
|
||||||
redirect_url: params[:redirect_url],
|
|
||||||
redirect_status: 302
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parse metadata if it's a string that looks like JSON
|
|
||||||
if @rule.metadata.is_a?(String) && @rule.metadata.starts_with?('{')
|
|
||||||
begin
|
|
||||||
@rule.metadata = JSON.parse(@rule.metadata)
|
|
||||||
rescue JSON::ParserError
|
|
||||||
# Keep as string if not valid JSON
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add reason to metadata if provided
|
|
||||||
if params.dig(:rule, :metadata).present?
|
|
||||||
if @rule.metadata.is_a?(Hash)
|
|
||||||
@rule.metadata['reason'] = params[:rule][:metadata]
|
|
||||||
else
|
|
||||||
@rule.metadata = { 'reason' => params[:rule][:metadata] }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
@@ -236,9 +236,36 @@ class Rule < ApplicationRecord
|
|||||||
end
|
end
|
||||||
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
|
format
|
||||||
end
|
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
|
# Class methods for rule creation
|
||||||
def self.create_network_rule(cidr, action: 'deny', user: nil, **options)
|
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')
|
network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')
|
||||||
@@ -252,6 +279,42 @@ class Rule < ApplicationRecord
|
|||||||
)
|
)
|
||||||
end
|
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)
|
def self.create_surgical_block(ip_address, parent_cidr, user: nil, reason: nil, **options)
|
||||||
# Create block rule for parent range
|
# Create block rule for parent range
|
||||||
network_range = NetworkRange.find_or_create_by_cidr(parent_cidr, user: user, source: 'user_created')
|
network_range = NetworkRange.find_or_create_by_cidr(parent_cidr, user: user, source: 'user_created')
|
||||||
@@ -418,10 +481,24 @@ class Rule < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_path_pattern_conditions
|
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)
|
if segment_ids.blank? || !segment_ids.is_a?(Array)
|
||||||
errors.add(:conditions, "must include 'patterns' array for path_pattern rules")
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
# generate specific Rules when matching network ranges are discovered.
|
# generate specific Rules when matching network ranges are discovered.
|
||||||
class WafPolicy < ApplicationRecord
|
class WafPolicy < ApplicationRecord
|
||||||
# Policy types - different categories of blocking rules
|
# 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 - what to do when traffic matches this policy
|
||||||
ACTIONS = %w[allow deny redirect challenge].freeze
|
ACTIONS = %w[allow deny redirect challenge].freeze
|
||||||
@@ -57,6 +57,10 @@ validate :targets_must_be_array
|
|||||||
policy_type == 'network_type'
|
policy_type == 'network_type'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def path_pattern_policy?
|
||||||
|
policy_type == 'path_pattern'
|
||||||
|
end
|
||||||
|
|
||||||
# Action methods
|
# Action methods
|
||||||
def allow_action?
|
def allow_action?
|
||||||
policy_action == 'allow'
|
policy_action == 'allow'
|
||||||
@@ -130,6 +134,21 @@ validate :targets_must_be_array
|
|||||||
end
|
end
|
||||||
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)
|
def create_rule_for_network_range(network_range)
|
||||||
return nil unless matches_network_range?(network_range)
|
return nil unless matches_network_range?(network_range)
|
||||||
|
|
||||||
@@ -164,6 +183,59 @@ validate :targets_must_be_array
|
|||||||
rule
|
rule
|
||||||
end
|
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
|
# Class methods for creating common policies
|
||||||
def self.create_country_policy(countries, policy_action: 'deny', user:, **options)
|
def self.create_country_policy(countries, policy_action: 'deny', user:, **options)
|
||||||
create!(
|
create!(
|
||||||
@@ -209,6 +281,17 @@ validate :targets_must_be_array
|
|||||||
)
|
)
|
||||||
end
|
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
|
# Redirect/challenge specific methods
|
||||||
def redirect_url
|
def redirect_url
|
||||||
additional_data&.dig('redirect_url')
|
additional_data&.dig('redirect_url')
|
||||||
@@ -283,6 +366,8 @@ validate :targets_must_be_array
|
|||||||
validate_company_targets
|
validate_company_targets
|
||||||
when 'network_type'
|
when 'network_type'
|
||||||
validate_network_type_targets
|
validate_network_type_targets
|
||||||
|
when 'path_pattern'
|
||||||
|
validate_path_pattern_targets
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -311,6 +396,24 @@ validate :targets_must_be_array
|
|||||||
end
|
end
|
||||||
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
|
def validate_redirect_configuration
|
||||||
if additional_data['redirect_url'].blank?
|
if additional_data['redirect_url'].blank?
|
||||||
errors.add(:additional_data, "must include 'redirect_url' for redirect action")
|
errors.add(:additional_data, "must include 'redirect_url' for redirect action")
|
||||||
@@ -413,4 +516,62 @@ validate :targets_must_be_array
|
|||||||
types.join(',') || 'standard'
|
types.join(',') || 'standard'
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ class WafPolicyPolicy < ApplicationPolicy
|
|||||||
!user.viewer? # All authenticated users except viewers can deactivate policies
|
!user.viewer? # All authenticated users except viewers can deactivate policies
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Path pattern policy permissions
|
||||||
|
def new_path_pattern?
|
||||||
|
create?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_path_pattern?
|
||||||
|
create?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Country policy permissions
|
||||||
def new_country?
|
def new_country?
|
||||||
create?
|
create?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,17 +30,19 @@ class WafPolicyMatcher
|
|||||||
policy.matches_event?(event)
|
policy.matches_event?(event)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sort by priority: country > asn > company > network_type, then by creation date
|
# Sort by priority: path_pattern > country > asn > company > network_type, then by creation date
|
||||||
@matching_policies.sort_by do |policy|
|
@matching_policies.sort_by do |policy|
|
||||||
priority_score = case policy.policy_type
|
priority_score = case policy.policy_type
|
||||||
|
when 'path_pattern'
|
||||||
|
1 # Highest priority for path-specific rules
|
||||||
when 'country'
|
when 'country'
|
||||||
1
|
|
||||||
when 'asn'
|
|
||||||
2
|
2
|
||||||
when 'company'
|
when 'asn'
|
||||||
3
|
3
|
||||||
when 'network_type'
|
when 'company'
|
||||||
4
|
4
|
||||||
|
when 'network_type'
|
||||||
|
5
|
||||||
else
|
else
|
||||||
99
|
99
|
||||||
end
|
end
|
||||||
@@ -54,22 +56,21 @@ class WafPolicyMatcher
|
|||||||
return [] if matching_policies.empty?
|
return [] if matching_policies.empty?
|
||||||
|
|
||||||
@generated_rules = matching_policies.map do |policy|
|
@generated_rules = matching_policies.map do |policy|
|
||||||
# Check if rule already exists for this network range and policy
|
# Use the policy's event-based rule creation method
|
||||||
existing_rule = Rule.find_by(
|
rule = policy.create_rule_for_event(event)
|
||||||
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
|
if rule
|
||||||
Rails.logger.info "Generated rule for network_range #{network_range.cidr} from policy #{policy.name}"
|
if rule.persisted?
|
||||||
end
|
Rails.logger.info "Generated rule for event #{event.id} from policy #{policy.name}"
|
||||||
rule
|
rule
|
||||||
|
else
|
||||||
|
# Rule creation failed validation
|
||||||
|
Rails.logger.warn "Failed to create rule for event #{event.id}: #{rule.errors.full_messages.join(', ')}"
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Policy didn't match or returned nil (e.g., supernet already exists)
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,9 +21,6 @@
|
|||||||
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
<%# Includes all stylesheet files in app/assets/stylesheets %>
|
||||||
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
<%= stylesheet_link_tag :app, "data-turbo-track": "reload" %>
|
||||||
|
|
||||||
<%# Tom Select CSS for enhanced multi-select %>
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css" data-turbo-track="reload">
|
|
||||||
|
|
||||||
<%= javascript_importmap_tags %>
|
<%= javascript_importmap_tags %>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -113,13 +113,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Conditions (shown for non-network rules) -->
|
<!-- Path Pattern (shown for path_pattern rules) -->
|
||||||
|
<div id="path_pattern_section" class="hidden space-y-6">
|
||||||
|
<div>
|
||||||
|
<%= form.label :path_pattern, "Path Pattern", class: "block text-sm font-medium text-gray-700" %>
|
||||||
|
<%= text_field_tag :path_pattern, "",
|
||||||
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||||
|
placeholder: "/admin, /wp-login.php, /.env, /phpmyadmin",
|
||||||
|
id: "path_pattern_input" %>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">Enter the path to match (e.g., /admin, /wp-login.php)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= form.label :match_type, "Match Type", class: "block text-sm font-medium text-gray-700" %>
|
||||||
|
<%= select_tag :match_type,
|
||||||
|
options_for_select([
|
||||||
|
["Exact - Matches path exactly", "exact"],
|
||||||
|
["Prefix - Matches path and subpaths (e.g., /admin matches /admin/users)", "prefix"],
|
||||||
|
["Suffix - Matches paths ending with pattern (e.g., /.env matches /backup/.env)", "suffix"],
|
||||||
|
["Contains - Matches paths containing pattern anywhere", "contains"]
|
||||||
|
]),
|
||||||
|
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||||
|
id: "match_type_select" } %>
|
||||||
|
<p class="mt-2 text-sm text-gray-500">How the pattern should be matched against request paths</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Example Matches (dynamically updated) -->
|
||||||
|
<div id="match_examples" class="bg-gray-50 border border-gray-200 rounded-md p-4">
|
||||||
|
<h4 class="text-sm font-medium text-gray-700 mb-2">Example Matches:</h4>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1" id="example_list">
|
||||||
|
<li>Enter a pattern to see examples</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Conditions (shown for other non-network rules) -->
|
||||||
<div id="conditions_section" class="hidden">
|
<div id="conditions_section" class="hidden">
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :conditions, "Conditions", class: "block text-sm font-medium text-gray-700" %>
|
<%= form.label :conditions, "Conditions", class: "block text-sm font-medium text-gray-700" %>
|
||||||
<%= form.text_area :conditions, rows: 4,
|
<%= form.text_area :conditions, rows: 4,
|
||||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
||||||
placeholder: '{"path_pattern": "/admin/*", "user_agent": "bot*"}' %>
|
placeholder: '{"user_agent": "bot*"}' %>
|
||||||
<p class="mt-2 text-sm text-gray-500">JSON format with matching conditions</p>
|
<p class="mt-2 text-sm text-gray-500">JSON format with matching conditions</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,19 +226,83 @@ let selectedNetworkData = null;
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const ruleTypeSelect = document.getElementById('rule_type_select');
|
const ruleTypeSelect = document.getElementById('rule_type_select');
|
||||||
const networkSection = document.getElementById('network_range_section');
|
const networkSection = document.getElementById('network_range_section');
|
||||||
|
const pathPatternSection = document.getElementById('path_pattern_section');
|
||||||
const conditionsSection = document.getElementById('conditions_section');
|
const conditionsSection = document.getElementById('conditions_section');
|
||||||
|
const pathPatternInput = document.getElementById('path_pattern_input');
|
||||||
|
const matchTypeSelect = document.getElementById('match_type_select');
|
||||||
|
|
||||||
function toggleSections() {
|
function toggleSections() {
|
||||||
if (ruleTypeSelect.value === 'network') {
|
const ruleType = ruleTypeSelect.value;
|
||||||
networkSection.classList.remove('hidden');
|
|
||||||
conditionsSection.classList.add('hidden');
|
// Hide all sections first
|
||||||
} else {
|
|
||||||
networkSection.classList.add('hidden');
|
networkSection.classList.add('hidden');
|
||||||
|
pathPatternSection.classList.add('hidden');
|
||||||
|
conditionsSection.classList.add('hidden');
|
||||||
|
|
||||||
|
// Show appropriate section
|
||||||
|
if (ruleType === 'network') {
|
||||||
|
networkSection.classList.remove('hidden');
|
||||||
|
} else if (ruleType === 'path_pattern') {
|
||||||
|
pathPatternSection.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
conditionsSection.classList.remove('hidden');
|
conditionsSection.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updatePathExamples() {
|
||||||
|
const pattern = pathPatternInput.value.trim();
|
||||||
|
const matchType = matchTypeSelect.value;
|
||||||
|
const exampleList = document.getElementById('example_list');
|
||||||
|
|
||||||
|
if (!pattern) {
|
||||||
|
exampleList.innerHTML = '<li>Enter a pattern to see examples</li>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let examples = [];
|
||||||
|
const cleanPattern = pattern.startsWith('/') ? pattern : '/' + pattern;
|
||||||
|
|
||||||
|
switch(matchType) {
|
||||||
|
case 'exact':
|
||||||
|
examples = [
|
||||||
|
`✓ ${cleanPattern}`,
|
||||||
|
`✗ ${cleanPattern}/users (extra segments)`,
|
||||||
|
`✗ /api${cleanPattern} (not at root)`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'prefix':
|
||||||
|
examples = [
|
||||||
|
`✓ ${cleanPattern}`,
|
||||||
|
`✓ ${cleanPattern}/users`,
|
||||||
|
`✓ ${cleanPattern}/dashboard/settings`,
|
||||||
|
`✗ /api${cleanPattern} (not at start)`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'suffix':
|
||||||
|
examples = [
|
||||||
|
`✓ ${cleanPattern}`,
|
||||||
|
`✓ /backup${cleanPattern}`,
|
||||||
|
`✓ /config/backup${cleanPattern}`,
|
||||||
|
`✗ ${cleanPattern}/test (extra at end)`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
case 'contains':
|
||||||
|
examples = [
|
||||||
|
`✓ ${cleanPattern}`,
|
||||||
|
`✓ /api${cleanPattern}/users`,
|
||||||
|
`✓ /super/secret${cleanPattern}/panel`,
|
||||||
|
`✗ ${cleanPattern}tool (different segment)`
|
||||||
|
];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
exampleList.innerHTML = examples.map(ex => `<li>${ex}</li>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
ruleTypeSelect.addEventListener('change', toggleSections);
|
ruleTypeSelect.addEventListener('change', toggleSections);
|
||||||
|
pathPatternInput.addEventListener('input', updatePathExamples);
|
||||||
|
matchTypeSelect.addEventListener('change', updatePathExamples);
|
||||||
|
|
||||||
toggleSections(); // Initial state
|
toggleSections(); // Initial state
|
||||||
|
|
||||||
// Pre-select network range if provided
|
// Pre-select network range if provided
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ pin_all_from "app/javascript/controllers", under: "controllers"
|
|||||||
|
|
||||||
# Tom Select for enhanced multi-select
|
# Tom Select for enhanced multi-select
|
||||||
pin "tom-select", to: "https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"
|
pin "tom-select", to: "https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/js/tom-select.complete.min.js"
|
||||||
pin "tom-select-css", to: "https://cdn.jsdelivr.net/npm/tom-select@2.3.1/dist/css/tom-select.css"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user