Accepts incoming events and correctly parses them into events. GeoLite2 integration complete"
This commit is contained in:
@@ -47,7 +47,7 @@ class EventNormalizer
|
||||
else :allow
|
||||
end
|
||||
|
||||
@event.action = action_enum
|
||||
@event.waf_action = action_enum
|
||||
end
|
||||
|
||||
def normalize_method
|
||||
|
||||
174
app/services/geo_ip_service.rb
Normal file
174
app/services/geo_ip_service.rb
Normal file
@@ -0,0 +1,174 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'maxmind/db'
|
||||
require 'httparty'
|
||||
require 'digest'
|
||||
require 'tmpdir'
|
||||
require 'fileutils'
|
||||
|
||||
class GeoIpService
|
||||
include HTTParty
|
||||
|
||||
class DatabaseNotAvailable < StandardError; end
|
||||
class InvalidIpAddress < StandardError; end
|
||||
|
||||
attr_reader :database_reader, :database_path
|
||||
|
||||
def initialize(database_path: nil)
|
||||
@database_path = database_path || default_database_path
|
||||
@database_reader = nil
|
||||
load_database if File.exist?(@database_path)
|
||||
end
|
||||
|
||||
# Main lookup method - returns country code for IP address
|
||||
def lookup_country(ip_address)
|
||||
return fallback_country unless database_available?
|
||||
return fallback_country unless valid_ip?(ip_address)
|
||||
|
||||
result = database_reader.get(ip_address)
|
||||
return fallback_country if result.nil? || result.empty?
|
||||
|
||||
# Extract country code from MaxMind result
|
||||
result['country']&.[]('iso_code') || fallback_country
|
||||
rescue => e
|
||||
Rails.logger.error "GeoIP lookup failed for #{ip_address}: #{e.message}"
|
||||
fallback_country
|
||||
end
|
||||
|
||||
# Check if database is available and loaded
|
||||
def database_available?
|
||||
return false unless File.exist?(@database_path)
|
||||
|
||||
load_database unless database_reader
|
||||
database_reader.present?
|
||||
end
|
||||
|
||||
# Get database information
|
||||
def database_info
|
||||
return nil unless File.exist?(@database_path)
|
||||
|
||||
file_stat = File.stat(@database_path)
|
||||
metadata = database_reader&.metadata
|
||||
|
||||
if metadata
|
||||
{
|
||||
type: 'GeoLite2-Country',
|
||||
version: "#{metadata.binary_format_major_version}.#{metadata.binary_format_minor_version}",
|
||||
size: file_stat.size,
|
||||
modified_at: file_stat.mtime,
|
||||
age_days: ((Time.current - file_stat.mtime) / 1.day).round,
|
||||
file_path: @database_path
|
||||
}
|
||||
else
|
||||
{
|
||||
type: 'GeoLite2-Country',
|
||||
version: 'Unknown',
|
||||
size: file_stat.size,
|
||||
modified_at: file_stat.mtime,
|
||||
age_days: ((Time.current - file_stat.mtime) / 1.day).round,
|
||||
file_path: @database_path
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Class method for convenience lookup
|
||||
def self.lookup_country(ip_address)
|
||||
new.lookup_country(ip_address)
|
||||
end
|
||||
|
||||
# Update database from remote source
|
||||
def self.update_database!
|
||||
new.update_from_remote!
|
||||
end
|
||||
|
||||
# Download and install database from remote URL
|
||||
def update_from_remote!
|
||||
config = Rails.application.config.maxmind
|
||||
database_url = config.database_url
|
||||
storage_path = config.storage_path
|
||||
database_filename = config.database_filename
|
||||
temp_file = nil
|
||||
|
||||
Rails.logger.info "Starting GeoIP database download from #{database_url}"
|
||||
|
||||
begin
|
||||
# Ensure storage directory exists
|
||||
FileUtils.mkdir_p(storage_path) unless Dir.exist?(storage_path)
|
||||
|
||||
# Download to temporary file
|
||||
Dir.mktmpdir do |temp_dir|
|
||||
temp_file = File.join(temp_dir, database_filename)
|
||||
|
||||
response = HTTParty.get(database_url, timeout: 60)
|
||||
raise "Failed to download database: #{response.code}" unless response.success?
|
||||
|
||||
File.binwrite(temp_file, response.body)
|
||||
|
||||
# Validate downloaded file
|
||||
validate_downloaded_file(temp_file)
|
||||
|
||||
# Move to final location
|
||||
final_path = File.join(storage_path, database_filename)
|
||||
File.rename(temp_file, final_path)
|
||||
|
||||
# Reload the database with new file
|
||||
@database_reader = nil
|
||||
load_database
|
||||
|
||||
Rails.logger.info "GeoIP database successfully updated: #{final_path}"
|
||||
return true
|
||||
end
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to update GeoIP database: #{e.message}"
|
||||
File.delete(temp_file) if temp_file && File.exist?(temp_file)
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_database
|
||||
return unless File.exist?(@database_path)
|
||||
|
||||
@database_reader = MaxMind::DB.new(@database_path)
|
||||
rescue => e
|
||||
Rails.logger.error "Failed to load GeoIP database: #{e.message}"
|
||||
@database_reader = nil
|
||||
end
|
||||
|
||||
def default_database_path
|
||||
config = Rails.application.config.maxmind
|
||||
File.join(config.storage_path, config.database_filename)
|
||||
end
|
||||
|
||||
def valid_ip?(ip_address)
|
||||
IPAddr.new(ip_address)
|
||||
true
|
||||
rescue IPAddr::InvalidAddressError
|
||||
false
|
||||
end
|
||||
|
||||
def fallback_country
|
||||
config = Rails.application.config.maxmind
|
||||
return nil unless config.enable_fallback
|
||||
config.fallback_country
|
||||
end
|
||||
|
||||
def cache_size
|
||||
return 0 unless Rails.application.config.maxmind.cache_enabled
|
||||
Rails.application.config.maxmind.cache_size
|
||||
end
|
||||
|
||||
def validate_downloaded_file(file_path)
|
||||
# Basic file existence and size check
|
||||
raise "Downloaded file is empty" unless File.exist?(file_path)
|
||||
raise "Downloaded file is too small" if File.size(file_path) < 1_000_000 # ~1MB minimum
|
||||
|
||||
# Try to open with MaxMind reader
|
||||
begin
|
||||
MaxMind::DB.new(file_path)
|
||||
rescue => e
|
||||
raise "Invalid MaxMind database format: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
78
app/services/hub_load.rb
Normal file
78
app/services/hub_load.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# HubLoad - Calculates dynamic event sampling rate based on system load
|
||||
#
|
||||
# This service monitors SolidQueue depth and adjusts sampling rates to prevent
|
||||
# the Hub from being overwhelmed while ensuring critical events are always captured.
|
||||
class HubLoad
|
||||
# Queue depth thresholds
|
||||
THRESHOLDS = {
|
||||
normal: 0..1_000, # 100% sampling
|
||||
moderate: 1_001..5_000, # 50% sampling
|
||||
high: 5_001..10_000, # 20% sampling
|
||||
critical: 10_001..Float::INFINITY # 5% sampling
|
||||
}.freeze
|
||||
|
||||
SAMPLING_RATES = {
|
||||
normal: { allowed: 1.0, blocked: 1.0, rate_limited: 1.0 },
|
||||
moderate: { allowed: 0.5, blocked: 1.0, rate_limited: 1.0 },
|
||||
high: { allowed: 0.2, blocked: 1.0, rate_limited: 1.0 },
|
||||
critical: { allowed: 0.05, blocked: 1.0, rate_limited: 1.0 }
|
||||
}.freeze
|
||||
|
||||
# Get current sampling configuration based on load
|
||||
def self.current_sampling
|
||||
load_level = calculate_load_level
|
||||
rates = SAMPLING_RATES[load_level]
|
||||
|
||||
{
|
||||
allowed_requests: rates[:allowed],
|
||||
blocked_requests: rates[:blocked],
|
||||
rate_limited_requests: rates[:rate_limited],
|
||||
effective_until: next_sync_time,
|
||||
load_level: load_level,
|
||||
queue_depth: queue_depth
|
||||
}
|
||||
end
|
||||
|
||||
# Calculate when sampling should be rechecked (next agent sync)
|
||||
def self.next_sync_time
|
||||
10.seconds.from_now.iso8601(3)
|
||||
end
|
||||
|
||||
# Get current queue depth
|
||||
def self.queue_depth
|
||||
# SolidQueue stores jobs in the jobs table
|
||||
# Count pending/running jobs only
|
||||
SolidQueue::Job.where(finished_at: nil).count
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "Failed to get queue depth: #{e.message}"
|
||||
0
|
||||
end
|
||||
|
||||
# Determine load level based on queue depth
|
||||
def self.calculate_load_level
|
||||
depth = queue_depth
|
||||
|
||||
THRESHOLDS.each do |level, range|
|
||||
return level if range.cover?(depth)
|
||||
end
|
||||
|
||||
:critical # Fallback
|
||||
end
|
||||
|
||||
# Check if hub is under heavy load
|
||||
def self.overloaded?
|
||||
calculate_load_level.in?([:high, :critical])
|
||||
end
|
||||
|
||||
# Get load statistics for monitoring
|
||||
def self.stats
|
||||
{
|
||||
queue_depth: queue_depth,
|
||||
load_level: calculate_load_level,
|
||||
sampling_rates: SAMPLING_RATES[calculate_load_level],
|
||||
overloaded: overloaded?
|
||||
}
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user