Files
baffle-hub/app/models/project.rb
2025-11-04 09:47:11 +11:00

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