# Local file logger for CSP violations # Provides local logging even when Sentry is not configured Rails.application.config.after_initialize do # Create a dedicated logger for CSP violations csp_log_path = Rails.root.join("log", "csp_violations.log") csp_logger = Logger.new(csp_log_path) # Rotate logs daily, keep 30 days csp_logger.keep = 30 csp_logger.level = Logger::INFO # Format: [TIMESTAMP] LEVEL MESSAGE csp_logger.formatter = proc do |severity, datetime, progname, msg| "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity} #{msg}\n" end module CspViolationLocalLogger def self.emit(event_data) csp_data = event_data[:data] || {} # Build a structured log message violated_directive = csp_data[:violated_directive] || "unknown" blocked_uri = csp_data[:blocked_uri] || "unknown" document_uri = csp_data[:document_uri] || "unknown" # Create a comprehensive log entry log_message = "CSP VIOLATION DETECTED\n" log_message += " Directive: #{violated_directive}\n" log_message += " Blocked URI: #{blocked_uri}\n" log_message += " Document URI: #{document_uri}\n" log_message += " User Agent: #{csp_data[:user_agent]}\n" log_message += " IP Address: #{csp_data[:ip_address]}\n" log_message += " Timestamp: #{csp_data[:timestamp]}\n" if csp_data[:current_user_id].present? log_message += " Authenticated User ID: #{csp_data[:current_user_id]}\n" log_message += " Session ID: #{csp_data[:session_id]}\n" else log_message += " User: Anonymous\n" end # Add additional details if available if csp_data[:source_file].present? log_message += " Source File: #{csp_data[:source_file]}" log_message += ":#{csp_data[:line_number]}" if csp_data[:line_number].present? log_message += ":#{csp_data[:column_number]}" if csp_data[:column_number].present? log_message += "\n" end if csp_data[:referrer].present? log_message += " Referrer: #{csp_data[:referrer]}\n" end # Determine severity for log level level = determine_log_level(csp_data[:violated_directive]) csp_logger.log(level, log_message) # Also log to main Rails logger for visibility Rails.logger.info "CSP violation logged to csp_violations.log: #{violated_directive} - #{blocked_uri}" rescue => e # Ensure logger errors don't break the CSP reporting flow Rails.logger.error "Failed to log CSP violation to file: #{e.message}" Rails.logger.error e.backtrace.join("\n") if Rails.env.development? end private def self.determine_log_level(violated_directive) return Logger::INFO unless violated_directive.present? case violated_directive.to_sym when :script_src, :script_src_elem, :script_src_attr, :frame_src, :child_src Logger::WARN # Higher priority violations when :connect_src, :default_src, :style_src, :style_src_elem, :style_src_attr Logger::INFO # Medium priority violations else Logger::DEBUG # Lower priority violations end end end # Register the local logger subscriber Rails.event.subscribe("csp.violation", CspViolationLocalLogger) Rails.logger.info "CSP violation local logger registered - logging to: #{csp_log_path}" # Ensure the log file is created and writable begin # Create log file if it doesn't exist FileUtils.touch(csp_log_path) unless File.exist?(csp_log_path) # Test write to ensure permissions are correct csp_logger.info "CSP Logger initialized at #{Time.current}" rescue => e Rails.logger.error "Failed to initialize CSP local logger: #{e.message}" Rails.logger.error "CSP violations will only be sent to Sentry (if configured)" end end