module Immunio module Context RAILS_TEMPLATE_FILTER = /(.*(_erb|_haml))__+\d+_\d+(.*)/ # https://github.com/rails/rails/blob/v3.2.21/activesupport/lib/active_support/callbacks.rb#L414 ACTIVESUPPORT_FILTER = /(.+)_run__\d+__(.+)__\d+__callbacks(.*)/ # Cache for contexts (named in tribute to our buddy Adam Back who invented proof of work) @@hash_cache = {} FILE_CHECKSUM_CACHE = Hash.new do |cache, filepath| begin contents = IOHooks.paused { File.read(filepath) } cache[filepath] = Digest::SHA1.hexdigest(contents) rescue StandardError cache[filepath] = "" end end # Calculate context hashes and a stack trace. Additional data, in the form # of a String, may be provided to mix into the strict context hash. def self.context(additional_data=nil) # We can filter out at least the top two frames stack = caller(2).join "\n" cache_key = Digest::SHA1.hexdigest stack if @@hash_cache.has_key?(cache_key) then loose_context = @@hash_cache[cache_key]["loose_context"] strict_context = @@hash_cache[cache_key]["strict_context"] if Immunio.agent.config.log_context_data Immunio.logger.info {"Stack contexts from cache"} end else # Use ropes as they're faster than string concatenation loose_context_rope = [] strict_context_rope = [] # drop the top frame as it's us, but retain the rest. Immunio frames # are filtered by the Gem regex. locations = caller(1).map do |frame| frame = frame.split(":", 3) { path: frame[0], line: frame[1], label: frame[2] } end locations.each do |frame| # Filter frame names from template rendering to remove generated random bits template_match = RAILS_TEMPLATE_FILTER.match(frame[:label]) frame[:label] = template_match[1] + template_match[3] if template_match # Filter frame names from activesupport generated callback methods callback_match = ACTIVESUPPORT_FILTER.match(frame[:label]) frame[:label] = callback_match[1] + callback_match[2] + callback_match[3] if callback_match # Reduce paths to be relative to root if possible, to allow # relocation. If there's no rails root, or the path doesn't start with # the rails root, just use the filename part. if defined?(Rails) && defined?(Rails.root) && Rails.root && frame[:path].start_with?(Rails.root.to_s) strict_path = frame[:path].sub(Rails.root.to_s, '') else strict_path = File.basename(frame[:path]) end strict_context_rope << "\n" unless strict_context_rope.empty? strict_context_rope << strict_path strict_context_rope << ":" strict_context_rope << frame[:line] strict_context_rope << ":" strict_context_rope << frame[:label] # Include checksums of file contents in the strict context checksum = FILE_CHECKSUM_CACHE[frame[:path]] strict_context_rope << ":#{checksum}" unless checksum.blank? # Remove pathname from the loose context. The goal here is to prevent # upgrading gem versions from changing the loose context key, so for instance # users don't have to rebuild their whitelists every time they update a gem loose_context_rope << "\n" unless loose_context_rope.empty? loose_context_rope << File.basename(frame[:path]) loose_context_rope << ":" loose_context_rope << frame[:label] end strict_stack = strict_context_rope.join() loose_stack = loose_context_rope.join() if Immunio.agent.config.log_context_data Immunio.logger.info {"Strict context stack:\n#{strict_stack}"} Immunio.logger.info {"Loose context stack:\n#{loose_stack}"} end strict_context = Digest::SHA1.hexdigest(strict_stack) loose_context = Digest::SHA1.hexdigest(loose_stack) @@hash_cache[cache_key] = { "strict_context" => strict_context, "loose_context" => loose_context, } end # Mix in additional context data if additional_data if Immunio.agent.config.log_context_data Immunio.logger.info {"Additional context data:\n#{additional_data}"} end strict_context = Digest::SHA1.hexdigest(strict_context + additional_data) end return strict_context, loose_context, stack end end end