# 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