First commit!

This commit is contained in:
Dan Milne
2025-11-03 17:37:28 +11:00
commit 429d41eead
141 changed files with 5890 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# frozen_string_literal: true
class Api::EventsController < ApplicationController
skip_before_action :verify_authenticity_token
# POST /api/:project_id/events
def create
project = authenticate_project!
return head :not_found unless project
# Parse the incoming WAF event data
event_data = parse_event_data(request)
# Create event asynchronously
ProcessWafEventJob.perform_later(
project_id: project.id,
event_data: event_data,
headers: extract_serializable_headers(request)
)
# Always return 200 OK to avoid agent retries
head :ok
rescue DsnAuthenticationService::AuthenticationError => e
Rails.logger.warn "DSN authentication failed: #{e.message}"
head :unauthorized
rescue JSON::ParserError => e
Rails.logger.error "Invalid JSON in event data: #{e.message}"
head :bad_request
end
private
def authenticate_project!
DsnAuthenticationService.authenticate(request, params[:project_id])
end
def parse_event_data(request)
# Handle different content types
content_type = request.content_type || "application/json"
case content_type
when /application\/json/
JSON.parse(request.body.read)
when /application\/x-www-form-urlencoded/
# Convert form data to JSON-like hash
request.request_parameters
else
# Try to parse as JSON anyway
JSON.parse(request.body.read)
end
rescue => e
Rails.logger.error "Failed to parse event data: #{e.message}"
{}
ensure
request.body.rewind if request.body.respond_to?(:rewind)
end
def extract_serializable_headers(request)
# Only extract the headers we need for analytics, avoiding IO objects
important_headers = %w[
User-Agent Content-Type Content-Length Accept
X-Forwarded-For X-Real-IP X-Forwarded-Proto
Authorization X-Baffle-Auth X-Sentry-Auth
Referer Accept-Language Accept-Encoding
]
headers = {}
important_headers.each do |header|
value = request.headers[header]
headers[header] = value if value.present?
end
headers
end
end

View File

@@ -0,0 +1,9 @@
class ApplicationController < ActionController::Base
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
# Changes to the importmap will invalidate the etag for HTML responses
stale_when_importmap_changes
include Pagy::Backend
end

View File

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
class EventsController < ApplicationController
before_action :set_project
def index
@events = @project.events.order(timestamp: :desc)
Rails.logger.debug "Found project? #{@project.name} / #{@project.events.count} / #{@events.count}"
Rails.logger.debug "Action: #{params[:waf_action]}"
# Apply filters
@events = @events.by_ip(params[:ip]) if params[:ip].present?
@events = @events.by_waf_action(params[:waf_action]) if params[:waf_action].present?
@events = @events.where(country_code: params[:country]) if params[:country].present?
Rails.logger.debug "after filter #{@project.name} / #{@project.events.count} / #{@events.count}"
# Debug info
Rails.logger.debug "Events count before pagination: #{@events.count}"
Rails.logger.debug "Project: #{@project&.name} (ID: #{@project&.id})"
# Paginate
@pagy, @events = pagy(@events, items: 50)
Rails.logger.debug "Events count after pagination: #{@events.count}"
Rails.logger.debug "Pagy info: #{@pagy.count} total, #{@pagy.pages} pages"
end
private
def set_project
@project = Project.find(params[:project_id]) || Project.find_by(slug: params[:project_id])
redirect_to projects_path, alert: "Project not found" unless @project
end
end

View File

@@ -0,0 +1,99 @@
# frozen_string_literal: true
class ProjectsController < ApplicationController
before_action :set_project, only: [:show, :edit, :update, :events, :analytics]
def index
@projects = Project.order(created_at: :desc)
end
def show
@recent_events = @project.recent_events(limit: 10)
@event_count = @project.event_count(24.hours.ago)
@blocked_count = @project.blocked_count(24.hours.ago)
@waf_status = @project.waf_status
end
def new
@project = Project.new
end
def create
@project = Project.new(project_params)
if @project.save
redirect_to @project, notice: "Project was successfully created. Use this DSN for your baffle-agent: #{@project.dsn}"
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @project.update(project_params)
redirect_to @project, notice: "Project was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def events
@events = @project.events.recent.includes(:project)
# Apply filters
@events = @events.by_ip(params[:ip]) if params[:ip].present?
@events = @events.by_action(params[:action]) if params[:action].present?
@events = @events.where(country_code: params[:country]) if params[:country].present?
# Debug info
Rails.logger.debug "Events count before pagination: #{@events.count}"
Rails.logger.debug "Project: #{@project&.name} (ID: #{@project&.id})"
# Paginate
@pagy, @events = pagy(@events, items: 50)
Rails.logger.debug "Events count after pagination: #{@events.count}"
Rails.logger.debug "Pagy info: #{@pagy.count} total, #{@pagy.pages} pages"
end
def analytics
@time_range = params[:time_range]&.to_i || 24 # hours
# Basic analytics
@total_events = @project.event_count(@time_range.hours.ago)
@blocked_events = @project.blocked_count(@time_range.hours.ago)
@allowed_events = @project.allowed_count(@time_range.hours.ago)
# Top blocked IPs
@top_blocked_ips = @project.top_blocked_ips(limit: 10, time_range: @time_range.hours.ago)
# Country distribution
@country_stats = @project.events
.where(timestamp: @time_range.hours.ago..Time.current)
.where.not(country_code: nil)
.group(:country_code)
.select('country_code, COUNT(*) as count')
.order('count DESC')
.limit(10)
# Action distribution
@action_stats = @project.events
.where(timestamp: @time_range.hours.ago..Time.current)
.group(:action)
.select('action, COUNT(*) as count')
.order('count DESC')
end
private
def set_project
@project = Project.find_by(slug: params[:id]) || Project.find_by(id: params[:id])
redirect_to projects_path, alert: "Project not found" unless @project
end
def project_params
params.require(:project).permit(:name, :enabled, settings: {})
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
class RuleSetsController < ApplicationController
before_action :set_rule_set, only: [:show, :edit, :update, :push_to_agents]
def index
@rule_sets = RuleSet.includes(:rules).by_priority
end
def show
@rules = @rule_set.rules.includes(:rule_set).by_priority
end
def new
@rule_set = RuleSet.new
end
def create
@rule_set = RuleSet.new(rule_set_params)
if @rule_set.save
redirect_to @rule_set, notice: "Rule set was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def edit
end
def update
if @rule_set.update(rule_set_params)
redirect_to @rule_set, notice: "Rule set was successfully updated."
else
render :edit, status: :unprocessable_entity
end
end
def push_to_agents
@rule_set.push_to_agents!
redirect_to @rule_set, notice: "Rule set pushed to agents successfully."
end
private
def set_rule_set
@rule_set = RuleSet.find_by(slug: params[:id]) || RuleSet.find(params[:id])
end
def rule_set_params
params.require(:rule_set).permit(:name, :description, :enabled, :priority)
end
end