Accepts incoming events and correctly parses them into events. GeoLite2 integration complete"

This commit is contained in:
Dan Milne
2025-11-04 00:11:10 +11:00
parent 0cbd462e7c
commit 5ff166613e
49 changed files with 4489 additions and 322 deletions

View File

@@ -47,7 +47,7 @@ class EventNormalizer
else :allow
end
@event.action = action_enum
@event.waf_action = action_enum
end
def normalize_method

View 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
View 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