110 lines
3.4 KiB
Ruby
110 lines
3.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Api::EventsController < ApplicationController
|
|
skip_before_action :verify_authenticity_token
|
|
|
|
# POST /api/:project_id/events
|
|
def create
|
|
project = authenticate_project!
|
|
return head :not_found unless project
|
|
|
|
# Parse the incoming WAF event data
|
|
event_data = parse_event_data(request)
|
|
|
|
# Create event asynchronously
|
|
ProcessWafEventJob.perform_later(
|
|
project_id: project.id,
|
|
event_data: event_data,
|
|
headers: extract_serializable_headers(request)
|
|
)
|
|
|
|
# Include rule version in response for agent optimization
|
|
rule_version = Rule.latest_version
|
|
response.headers['X-Rule-Version'] = rule_version.to_s
|
|
|
|
# Get current sampling for back-pressure management
|
|
current_sampling = HubLoad.current_sampling
|
|
response.headers['X-Sample-Rate'] = current_sampling[:allowed_requests].to_s
|
|
response.headers['X-Sample-Until'] = current_sampling[:effective_until]
|
|
|
|
# Check if agent sent a rule version to compare against
|
|
client_version = request.headers['X-Rule-Version']&.to_i
|
|
|
|
response_data = {
|
|
success: true,
|
|
rule_version: rule_version,
|
|
sampling: current_sampling
|
|
}
|
|
|
|
# If agent has old rules or no version, include new rules in response
|
|
if client_version.blank? || client_version != rule_version
|
|
# Get rules updated since client version
|
|
if client_version.present?
|
|
since_time = Time.at(client_version / 1_000_000, client_version % 1_000_000)
|
|
rules = Rule.where("updated_at > ?", since_time).enabled.sync_order
|
|
else
|
|
# Full sync for new agents
|
|
rules = Rule.active.sync_order
|
|
end
|
|
|
|
response_data[:rules] = rules.map(&:to_agent_format)
|
|
response_data[:rules_changed] = true
|
|
else
|
|
response_data[:rules_changed] = false
|
|
end
|
|
|
|
render json: response_data
|
|
rescue DsnAuthenticationService::AuthenticationError => e
|
|
Rails.logger.warn "DSN authentication failed: #{e.message}"
|
|
head :unauthorized
|
|
rescue JSON::ParserError => e
|
|
Rails.logger.error "Invalid JSON in event data: #{e.message}"
|
|
head :bad_request
|
|
end
|
|
|
|
private
|
|
|
|
def authenticate_project!
|
|
DsnAuthenticationService.authenticate(request, params[:project_id])
|
|
end
|
|
|
|
def parse_event_data(request)
|
|
# Handle different content types
|
|
content_type = request.content_type || "application/json"
|
|
|
|
case content_type
|
|
when /application\/json/
|
|
JSON.parse(request.body.read)
|
|
when /application\/x-www-form-urlencoded/
|
|
# Convert form data to JSON-like hash
|
|
request.request_parameters
|
|
else
|
|
# Try to parse as JSON anyway
|
|
JSON.parse(request.body.read)
|
|
end
|
|
rescue => e
|
|
Rails.logger.error "Failed to parse event data: #{e.message}"
|
|
{}
|
|
ensure
|
|
request.body.rewind if request.body.respond_to?(:rewind)
|
|
end
|
|
|
|
def extract_serializable_headers(request)
|
|
# Only extract the headers we need for analytics, avoiding IO objects
|
|
important_headers = %w[
|
|
User-Agent Content-Type Content-Length Accept
|
|
X-Forwarded-For X-Real-IP X-Forwarded-Proto
|
|
Authorization X-Baffle-Auth X-Sentry-Auth
|
|
Referer Accept-Language Accept-Encoding
|
|
]
|
|
|
|
headers = {}
|
|
important_headers.each do |header|
|
|
value = request.headers[header]
|
|
# Standardize headers to lower case during import phase
|
|
headers[header.downcase] = value if value.present?
|
|
end
|
|
|
|
headers
|
|
end
|
|
end |