125 lines
3.4 KiB
Ruby
125 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class EventNormalizer
|
|
class NormalizationError < StandardError; end
|
|
|
|
# Normalize an event by populating all the normalized fields
|
|
def self.normalize_event!(event)
|
|
normalizer = new(event)
|
|
normalizer.normalize!
|
|
event
|
|
end
|
|
|
|
def initialize(event)
|
|
@event = event
|
|
end
|
|
|
|
def normalize!
|
|
return unless @event.present?
|
|
|
|
normalize_host
|
|
normalize_action
|
|
normalize_method
|
|
normalize_protocol
|
|
normalize_path_segments
|
|
end
|
|
|
|
private
|
|
|
|
def normalize_host
|
|
hostname = extract_hostname
|
|
return unless hostname
|
|
|
|
host = RequestHost.find_or_create_host(hostname)
|
|
# NOTE: usage_count increment removed for performance (was adding ~50ms per event)
|
|
# Can be recalculated with: RequestHost.all.each { |h| h.update(usage_count: h.events.count) }
|
|
@event.request_host = host
|
|
end
|
|
|
|
def normalize_action
|
|
raw_action = @event.instance_variable_get(:@raw_action)
|
|
return unless raw_action.present?
|
|
|
|
action_enum = case raw_action.to_s.downcase
|
|
when 'deny', 'block' then :deny
|
|
when 'allow', 'pass' then :allow
|
|
when 'redirect' then :redirect
|
|
when 'challenge' then :challenge
|
|
when 'log', 'monitor' then :log
|
|
else :allow
|
|
end
|
|
|
|
@event.waf_action = action_enum
|
|
end
|
|
|
|
def normalize_method
|
|
raw_method = @event.instance_variable_get(:@raw_request_method)
|
|
return unless raw_method.present?
|
|
|
|
method_enum = case raw_method.to_s.upcase
|
|
when 'GET' then :get
|
|
when 'POST' then :post
|
|
when 'PUT' then :put
|
|
when 'PATCH' then :patch
|
|
when 'DELETE' then :delete
|
|
when 'HEAD' then :head
|
|
when 'OPTIONS' then :options
|
|
else :get
|
|
end
|
|
|
|
@event.request_method = method_enum
|
|
end
|
|
|
|
def normalize_protocol
|
|
raw_protocol = @event.instance_variable_get(:@raw_request_protocol)
|
|
return unless raw_protocol.present?
|
|
|
|
# Store the protocol directly (HTTP/1.1, HTTP/2, etc.)
|
|
# Could normalize to enum if needed, but keeping as string for flexibility
|
|
@event.request_protocol = raw_protocol
|
|
end
|
|
|
|
def normalize_path_segments
|
|
segments = @event.path_segments_array
|
|
return if segments.empty?
|
|
|
|
segment_ids = segments.map do |segment|
|
|
path_segment = PathSegment.find_or_create_segment(segment)
|
|
# NOTE: usage_count increment removed for performance (was adding ~100ms per event for paths with many segments)
|
|
# Can be recalculated with: PathSegment.all.each { |ps| ps.update(usage_count: Event.where("request_segment_ids @> ARRAY[?]", ps.id).count) }
|
|
path_segment.id
|
|
end
|
|
|
|
@event.request_segment_ids = segment_ids
|
|
end
|
|
|
|
def extract_hostname
|
|
# Try to extract hostname from various sources
|
|
return @event.request_hostname if @event.respond_to?(:request_hostname)
|
|
|
|
# Extract from request URL if available
|
|
if @event.request_url.present?
|
|
begin
|
|
uri = URI.parse(@event.request_url)
|
|
return uri.hostname
|
|
rescue URI::InvalidURIError
|
|
nil
|
|
end
|
|
end
|
|
|
|
# Extract from payload as fallback
|
|
if @event.payload.present?
|
|
url = @event.payload.dig("request", "url")
|
|
if url.present?
|
|
begin
|
|
uri = URI.parse(url)
|
|
return uri.hostname
|
|
rescue URI::InvalidURIError
|
|
nil
|
|
end
|
|
end
|
|
end
|
|
|
|
nil
|
|
end
|
|
end |