First commit!
This commit is contained in:
211
app/models/project.rb
Normal file
211
app/models/project.rb
Normal file
@@ -0,0 +1,211 @@
|
||||
# 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 || "localhost:3000"
|
||||
protocol = host.include?("localhost") ? "http" : "https"
|
||||
"#{protocol}://#{public_key}@#{host}/#{slug}"
|
||||
end
|
||||
|
||||
def internal_dsn
|
||||
return nil unless Current.baffle_internal_host.present?
|
||||
|
||||
host = Current.baffle_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
|
||||
Reference in New Issue
Block a user