Module: Immunio::Context

Defined in:
lib/immunio/context.rb

Constant Summary collapse

RAILS_TEMPLATE_FILTER =
/(.*(_erb|_haml))__+\d+_\d+(.*)/
ACTIVESUPPORT_FILTER =
/(.+)_run__\d+__(.+)__\d+__callbacks(.*)/
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
@@hash_cache =

Cache for contexts (named in tribute to our buddy Adam Back who invented proof of work)

{}

Class Method Summary collapse

Class Method Details

.context(additional_data = nil) ⇒ Object

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.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/immunio/context.rb', line 23

def self.context(additional_data=nil)
  # Filter out agent stack frames. This includes string class_evals in `io`.
  filtered_stack_frames = caller.reject do |frame|
    frame =~ /lib\/immunio|^\(eval\):\d+:.+`.+_with_immunio'$/
  end

  stack = filtered_stack_frames.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 = filtered_stack_frames.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