Drop add_headers - headers can now be added to meta[] to be applied for any action. Consilidate Tagging in a service
This commit is contained in:
@@ -226,8 +226,8 @@ class Event < ApplicationRecord
|
||||
# Normalize headers in payload during import phase
|
||||
normalized_payload = normalize_payload_headers(payload)
|
||||
|
||||
# Create the WAF request event
|
||||
create!(
|
||||
# Create the WAF request event with agent-provided tags
|
||||
event = create!(
|
||||
request_id: request_id,
|
||||
timestamp: parse_timestamp(normalized_payload["timestamp"]),
|
||||
payload: normalized_payload,
|
||||
@@ -250,11 +250,18 @@ class Event < ApplicationRecord
|
||||
server_name: normalized_payload["server_name"],
|
||||
environment: normalized_payload["environment"],
|
||||
|
||||
|
||||
# Tags: start with agent-provided tags only
|
||||
tags: normalized_payload["tags"] || [],
|
||||
|
||||
# WAF agent info
|
||||
agent_version: normalized_payload.dig("agent", "version"),
|
||||
agent_name: normalized_payload.dig("agent", "name")
|
||||
)
|
||||
|
||||
# Apply rule tags using EventTagger service
|
||||
EventTagger.tag_event(event)
|
||||
|
||||
event
|
||||
end
|
||||
|
||||
# Normalize headers in payload to lower case during import phase
|
||||
@@ -347,7 +354,10 @@ class Event < ApplicationRecord
|
||||
|
||||
def tags
|
||||
# Use the dedicated tags column (array), fallback to payload during transition
|
||||
super.presence || (payload&.dig("tags") || [])
|
||||
# Ensure we always return an Array, even if payload has malformed data (e.g., {} instead of [])
|
||||
result = super.presence || payload&.dig("tags")
|
||||
return [] if result.nil?
|
||||
result.is_a?(Array) ? result : []
|
||||
end
|
||||
|
||||
def headers
|
||||
|
||||
@@ -34,8 +34,10 @@ class EventDdb
|
||||
SQL
|
||||
|
||||
# Convert to hash like ActiveRecord .group.count returns
|
||||
# DuckDB returns arrays: [waf_action, count]
|
||||
result.to_a.to_h { |row| [row[0], row[1]] }
|
||||
# DuckDB returns integer enum values, map to string names
|
||||
# 0=deny, 1=allow, 2=redirect, 3=challenge, 4=log
|
||||
action_map = { 0 => "deny", 1 => "allow", 2 => "redirect", 3 => "challenge", 4 => "log" }
|
||||
result.to_a.to_h { |row| [action_map[row[0]] || "unknown", row[1]] }
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "[EventDdb] Error in breakdown_by_action: #{e.message}"
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
class Rule < ApplicationRecord
|
||||
# Rule enums (prefix needed to avoid rate_limit collision)
|
||||
# Canonical WAF action order - aligned with Agent and Event models
|
||||
enum :waf_action, { deny: 0, allow: 1, redirect: 2, challenge: 3, log: 4, add_header: 5 }, prefix: :action
|
||||
# Note: allow and log actions can include headers/tags in metadata for automatic injection
|
||||
enum :waf_action, { deny: 0, allow: 1, redirect: 2, challenge: 3, log: 4 }, prefix: :action
|
||||
enum :waf_rule_type, { network: 0, rate_limit: 1, path_pattern: 2 }, prefix: :type
|
||||
|
||||
SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception policy].freeze
|
||||
@@ -120,10 +121,6 @@ class Rule < ApplicationRecord
|
||||
action_challenge?
|
||||
end
|
||||
|
||||
def add_header_action?
|
||||
action_add_header?
|
||||
end
|
||||
|
||||
# Redirect/challenge convenience methods
|
||||
def redirect_url
|
||||
metadata_hash['redirect_url']
|
||||
@@ -141,14 +138,6 @@ class Rule < ApplicationRecord
|
||||
metadata&.dig('challenge_message')
|
||||
end
|
||||
|
||||
def header_name
|
||||
metadata&.dig('header_name')
|
||||
end
|
||||
|
||||
def header_value
|
||||
metadata&.dig('header_value')
|
||||
end
|
||||
|
||||
# Tag-related methods
|
||||
def tags
|
||||
metadata_hash['tags'] || []
|
||||
@@ -469,12 +458,6 @@ class Rule < ApplicationRecord
|
||||
if source&.start_with?('auto:') || source == 'default'
|
||||
self.user ||= User.find_by(role: 1) # admin role
|
||||
end
|
||||
|
||||
# Set default header values for add_header action
|
||||
if add_header_action?
|
||||
self.metadata['header_name'] ||= 'X-Bot-Agent'
|
||||
self.metadata['header_value'] ||= 'Unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_priority_for_network_rules
|
||||
@@ -558,13 +541,6 @@ class Rule < ApplicationRecord
|
||||
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 "add_header"
|
||||
unless metadata&.dig("header_name").present?
|
||||
errors.add(:metadata, "must include 'header_name' for add_header action")
|
||||
end
|
||||
unless metadata&.dig("header_value").present?
|
||||
errors.add(:metadata, "must include 'header_value' for add_header action")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class WafPolicy < ApplicationRecord
|
||||
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 add_header].freeze
|
||||
ACTIONS = %w[allow deny redirect challenge log].freeze
|
||||
|
||||
# Associations
|
||||
belongs_to :user
|
||||
@@ -25,7 +25,6 @@ validate :targets_must_be_array
|
||||
validate :validate_targets_by_type
|
||||
validate :validate_redirect_configuration, if: :redirect_policy_action?
|
||||
validate :validate_challenge_configuration, if: :challenge_policy_action?
|
||||
validate :validate_add_header_configuration, if: :add_header_policy_action?
|
||||
|
||||
# Scopes
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
@@ -96,10 +95,6 @@ validate :targets_must_be_array
|
||||
policy_action == 'challenge'
|
||||
end
|
||||
|
||||
def add_header_policy_action?
|
||||
policy_action == 'add_header'
|
||||
end
|
||||
|
||||
# Lifecycle methods
|
||||
def active?
|
||||
enabled? && !expired?
|
||||
@@ -168,7 +163,7 @@ validate :targets_must_be_array
|
||||
priority: network_range.prefix_length
|
||||
)
|
||||
|
||||
# Handle redirect/challenge/add_header specific data
|
||||
# Handle redirect/challenge specific data
|
||||
if redirect_action? && additional_data['redirect_url']
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
@@ -183,13 +178,6 @@ validate :targets_must_be_array
|
||||
challenge_message: additional_data['challenge_message']
|
||||
)
|
||||
)
|
||||
elsif add_header_action?
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
header_name: additional_data['header_name'],
|
||||
header_value: additional_data['header_value']
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
rule
|
||||
@@ -224,7 +212,7 @@ validate :targets_must_be_array
|
||||
priority: 50 # Default priority for path rules
|
||||
)
|
||||
|
||||
# Handle redirect/challenge/add_header specific data
|
||||
# Handle redirect/challenge specific data
|
||||
if redirect_action? && additional_data['redirect_url']
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
@@ -239,13 +227,6 @@ validate :targets_must_be_array
|
||||
challenge_message: additional_data['challenge_message']
|
||||
)
|
||||
)
|
||||
elsif add_header_action?
|
||||
rule.update!(
|
||||
metadata: rule.metadata.merge(
|
||||
header_name: additional_data['header_name'],
|
||||
header_value: additional_data['header_value']
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
rule
|
||||
@@ -365,12 +346,6 @@ validate :targets_must_be_array
|
||||
self.targets ||= []
|
||||
self.additional_data ||= {}
|
||||
self.enabled = true if enabled.nil?
|
||||
|
||||
# Set default header values for add_header action
|
||||
if add_header_policy_action?
|
||||
self.additional_data['header_name'] ||= 'X-Bot-Agent'
|
||||
self.additional_data['header_value'] ||= 'Unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def targets_must_be_array
|
||||
@@ -455,15 +430,6 @@ validate :targets_must_be_array
|
||||
end
|
||||
end
|
||||
|
||||
def validate_add_header_configuration
|
||||
if additional_data['header_name'].blank?
|
||||
errors.add(:additional_data, "must include 'header_name' for add_header action")
|
||||
end
|
||||
if additional_data['header_value'].blank?
|
||||
errors.add(:additional_data, "must include 'header_value' for add_header action")
|
||||
end
|
||||
end
|
||||
|
||||
# Matching logic for different policy types
|
||||
def matches_country?(network_range)
|
||||
country = network_range.country || network_range.inherited_intelligence[:country]
|
||||
|
||||
Reference in New Issue
Block a user