213 lines
5.5 KiB
Ruby
213 lines
5.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Project < ApplicationRecord
|
|
has_many :events, dependent: :destroy
|
|
|
|
validates :name, presence: true
|
|
validates :slug, presence: true, uniqueness: true
|
|
validates :public_key, presence: true, uniqueness: true
|
|
|
|
scope :by_slug, ->(slug) { where(slug: slug) }
|
|
scope :by_public_key, ->(key) { where(public_key: key) }
|
|
scope :enabled, -> { where(enabled: true) }
|
|
|
|
before_validation :generate_slug, if: :name?
|
|
before_validation :generate_public_key, if: -> { public_key.blank? }
|
|
before_validation :set_default_settings, if: -> { settings.blank? }
|
|
|
|
def broadcast_events_refresh
|
|
# Broadcast to the events stream for this project
|
|
broadcast_refresh_to(self, "events")
|
|
end
|
|
|
|
def broadcast_rules_refresh
|
|
# Broadcast to the rules stream for this project (for future rule management UI)
|
|
broadcast_refresh_to(self, "rules")
|
|
end
|
|
|
|
def self.find_by_dsn(dsn)
|
|
# Parse DSN: https://public_key@host/project_id
|
|
return nil unless dsn.present?
|
|
|
|
# Extract public_key from DSN
|
|
match = dsn.match(/https?:\/\/([^@]+)@/)
|
|
return nil unless match
|
|
|
|
public_key = match[1]
|
|
find_by(public_key: public_key)
|
|
end
|
|
|
|
def self.find_by_project_id(project_id)
|
|
# Try slug first (nicer URLs), then fall back to ID
|
|
find_by(slug: project_id.to_s) || find_by(id: project_id.to_i)
|
|
end
|
|
|
|
def dsn
|
|
host = Current.baffle_host || ENV.fetch("BAFFLE_HOST", "localhost:3000")
|
|
protocol = host.include?("localhost") ? "http" : "https"
|
|
"#{protocol}://#{public_key}@#{host}/#{slug}"
|
|
end
|
|
|
|
def internal_dsn
|
|
internal_host = Current.baffle_internal_host || ENV.fetch("BAFFLE_INTERNAL_HOST", nil)
|
|
return nil unless internal_host.present?
|
|
|
|
host = internal_host
|
|
protocol = "http" # Internal connections use HTTP
|
|
"#{protocol}://#{public_key}@#{host}/#{slug}"
|
|
end
|
|
|
|
# WAF Analytics Methods
|
|
def recent_events(limit: 100)
|
|
events.recent.limit(limit)
|
|
end
|
|
|
|
def recent_blocked_events(limit: 100)
|
|
events.blocked.recent.limit(limit)
|
|
end
|
|
|
|
def recent_rate_limited_events(limit: 100)
|
|
events.rate_limited.recent.limit(limit)
|
|
end
|
|
|
|
def top_blocked_ips(limit: 10, time_range: 1.hour.ago)
|
|
events.blocked
|
|
.where(timestamp: time_range)
|
|
.group(:ip_address)
|
|
.select('ip_address, COUNT(*) as count')
|
|
.order('count DESC')
|
|
.limit(limit)
|
|
end
|
|
|
|
def event_count(time_range = nil)
|
|
if time_range
|
|
events.where(timestamp: time_range).count
|
|
else
|
|
events.count
|
|
end
|
|
end
|
|
|
|
def blocked_count(time_range = nil)
|
|
if time_range
|
|
events.blocked.where(timestamp: time_range).count
|
|
else
|
|
events.blocked.count
|
|
end
|
|
end
|
|
|
|
def allowed_count(time_range = nil)
|
|
if time_range
|
|
events.allowed.where(timestamp: time_range).count
|
|
else
|
|
events.allowed.count
|
|
end
|
|
end
|
|
|
|
# Helper method to parse settings safely
|
|
def parsed_settings
|
|
if settings.is_a?(String)
|
|
JSON.parse(settings || '{}')
|
|
else
|
|
settings || {}
|
|
end
|
|
rescue JSON::ParserError
|
|
{}
|
|
end
|
|
|
|
# WAF Configuration Methods
|
|
def rate_limit_enabled?
|
|
parsed_settings.dig('rate_limiting', 'enabled') != false
|
|
end
|
|
|
|
def rate_limit_threshold
|
|
parsed_settings.dig('rate_limiting', 'threshold') || 100
|
|
end
|
|
|
|
def custom_rules_enabled?
|
|
parsed_settings.dig('custom_rules', 'enabled') == true
|
|
end
|
|
|
|
def block_by_country_enabled?
|
|
parsed_settings.dig('geo_blocking', 'enabled') == true
|
|
end
|
|
|
|
def blocked_countries
|
|
parsed_settings.dig('geo_blocking', 'blocked_countries') || []
|
|
end
|
|
|
|
def block_datacenters_enabled?
|
|
parsed_settings.dig('datacenter_blocking', 'enabled') == true
|
|
end
|
|
|
|
# WAF Rule Management
|
|
def add_ip_rule(ip_address, action, expires_at: nil, reason: nil)
|
|
# This will integrate with the IP rules storage system
|
|
# For now, store in settings as a temporary solution
|
|
current_settings = parsed_settings
|
|
ip_rules = current_settings['ip_rules'] || {}
|
|
ip_rules[ip_address] = {
|
|
action: action,
|
|
expires_at: expires_at&.iso8601,
|
|
reason: reason,
|
|
created_at: Time.current.iso8601
|
|
}
|
|
update(settings: current_settings.merge('ip_rules' => ip_rules))
|
|
end
|
|
|
|
def remove_ip_rule(ip_address)
|
|
current_settings = parsed_settings
|
|
ip_rules = current_settings['ip_rules'] || {}
|
|
ip_rules.delete(ip_address)
|
|
update(settings: current_settings.merge('ip_rules' => ip_rules))
|
|
end
|
|
|
|
def blocked_ips
|
|
ip_rules = parsed_settings['ip_rules'] || {}
|
|
ip_rules.select { |_ip, rule| rule['action'] == 'block' }.keys
|
|
end
|
|
|
|
def waf_status
|
|
return 'disabled' unless enabled?
|
|
return 'active' if events.where(timestamp: 1.hour.ago..).exists?
|
|
'idle'
|
|
end
|
|
|
|
private
|
|
|
|
def generate_slug
|
|
self.slug = name&.parameterize&.downcase
|
|
end
|
|
|
|
def generate_public_key
|
|
# Generate a random 32-character hex string for WAF authentication
|
|
self.public_key = SecureRandom.hex(16)
|
|
end
|
|
|
|
def set_default_settings
|
|
self.settings = {
|
|
'rate_limiting' => {
|
|
'enabled' => true,
|
|
'threshold' => 100, # requests per minute
|
|
'window' => 60 # seconds
|
|
},
|
|
'geo_blocking' => {
|
|
'enabled' => false,
|
|
'blocked_countries' => []
|
|
},
|
|
'datacenter_blocking' => {
|
|
'enabled' => false,
|
|
'allow_known_datacenters' => true
|
|
},
|
|
'custom_rules' => {
|
|
'enabled' => false,
|
|
'rules' => []
|
|
},
|
|
'ip_rules' => {},
|
|
'challenge' => {
|
|
'enabled' => true,
|
|
'provider' => 'recaptcha'
|
|
}
|
|
}
|
|
end
|
|
end
|