Files
clinch/config/initializers/sentry_subscriber.rb
Dan Milne 6049429a41
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled
Fix mobile view menu popout. Add an option SENTRY_DSN support, which uses rails event reporting
2025-11-04 23:16:28 +11:00

120 lines
4.2 KiB
Ruby

# Sentry subscriber for CSP violations via Structured Event Reporting
# This subscriber only sends events to Sentry if Sentry is properly initialized
Rails.application.config.after_initialize do
# Only register the subscriber if Sentry is available and configured
if defined?(Sentry) && Sentry.initialized?
module CspViolationSentrySubscriber
def self.emit(event_data)
# Extract relevant CSP violation data
csp_data = event_data[:data] || {}
# Build a descriptive message for Sentry
violated_directive = csp_data[:violated_directive]
blocked_uri = csp_data[:blocked_uri]
document_uri = csp_data[:document_uri]
message = "CSP Violation: #{violated_directive}"
message += " - Blocked: #{blocked_uri}" if blocked_uri.present?
message += " - On: #{document_uri}" if document_uri.present?
# Extract domain from blocked_uri for better classification
blocked_domain = extract_domain(blocked_uri) if blocked_uri.present?
# Determine severity based on violation type
level = determine_severity(violated_directive, blocked_uri)
# Send to Sentry with rich context
Sentry.capture_message(
message,
level: level,
tags: {
csp_violation: true,
violated_directive: violated_directive,
blocked_domain: blocked_domain,
document_domain: extract_domain(document_uri),
user_authenticated: csp_data[:current_user_id].present?
},
extra: {
# Full CSP report data
csp_violation_details: csp_data,
# Additional context for security analysis
request_context: {
user_agent: csp_data[:user_agent],
ip_address: csp_data[:ip_address],
session_id: csp_data[:session_id],
timestamp: csp_data[:timestamp]
}
},
user: csp_data[:current_user_id] ? { id: csp_data[:current_user_id] } : nil
)
# Log to Rails logger for redundancy
Rails.logger.info "CSP violation sent to Sentry: #{message}"
rescue => e
# Ensure subscriber errors don't break the CSP reporting flow
Rails.logger.error "Failed to send CSP violation to Sentry: #{e.message}"
Rails.logger.error e.backtrace.join("\n") if Rails.env.development?
end
private
# Extract domain from URI for better analysis
def self.extract_domain(uri)
return nil if uri.blank?
begin
parsed = URI.parse(uri)
parsed.host
rescue URI::InvalidURIError
# Handle cases where URI might be malformed or just a path
if uri.start_with?('/')
nil # It's a relative path, no domain
else
uri.split('/').first # Best effort extraction
end
end
end
# Determine severity level based on violation type
def self.determine_severity(violated_directive, blocked_uri)
return :warning unless violated_directive.present?
case violated_directive.to_sym
when :script_src, :script_src_elem, :script_src_attr
# Script violations are highest priority (XSS risk)
:error
when :style_src, :style_src_elem, :style_src_attr
# Style violations are moderate risk
:warning
when :img_src
# Image violations are typically lower priority
:info
when :connect_src
# Network violations are important
:warning
when :font_src, :media_src
# Font/media violations are lower priority
:info
when :frame_src, :child_src
# Frame violations can be security critical
:error
when :default_src
# Default src violations are important
:warning
else
# Unknown or custom directives
:warning
end
end
end
# Register the subscriber for CSP violation events
Rails.event.subscribe("csp.violation", CspViolationSentrySubscriber)
Rails.logger.info "CSP violation Sentry subscriber registered"
else
Rails.logger.info "Sentry not initialized - CSP violations will only be logged locally"
end
end