contrast-agent 4.10.0 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab65fd574ef84fbe339e4f4d4bf340623235a442ecc11d7ba28c6af3c6f04d4d
4
- data.tar.gz: 0fa9cc44fe85713ee924101139e615d361de6d906c8f5ff4fdd345a930eee9d2
3
+ metadata.gz: 2ad97d601b81e16e1d0263d86b60a3acd0e5a5ff9cb3f42598c262774a518169
4
+ data.tar.gz: 749f87aefffd1e1504834dc7f919d45280cf560a20805184757dd9f6bda97477
5
5
  SHA512:
6
- metadata.gz: cb21ebcad0e772649d16a25d2f00cc057ee10df71638b7bd7cc6a4710a1ded28ce4dca749fa040ba2fb78e992727f4cc5d5f38e1187c363a8ea33a12333fc289
7
- data.tar.gz: c3ad4c82a9e25ecb58c0ea906f1170cffdd6811952f9c1d71a6c75166358a37e0ebba40e55e9a7433156a51122b9ca1450bb5a4a2a87f9081a4a4268d9abfdb4
6
+ metadata.gz: cfa15549565b4f17c431332c7bc7c6fe84bd5bea321860db84f24a2df5e2d5241675200ed6e50ebe53fc319d94ade6ebfed26fe664fc297dde54a1fc9f645fda
7
+ data.tar.gz: 017f02883faee2d281b7b052844653ddbb744fa27f02cf5739c38e50256eb054effb966c5b35b79b05e7d3af62f35d90509352f5c4327c9474e936c7e740ef19
@@ -35,9 +35,6 @@ module Contrast
35
35
  if object
36
36
  @object = Contrast::Utils::ClassUtil.to_contrast_string(object)
37
37
  @object_type = object.cs__class.cs__name
38
- # TODO: RUBY-1084 determine if we need to copy these tags to
39
- # restore immutability. For instance, if these tags were on a
40
- # String that was then #reverse!'d, would our tags be wrong?
41
38
  @tags = Contrast::Agent::Assess::Tracker.properties(object)&.tags
42
39
  else
43
40
  @object = Contrast::Utils::ObjectShare::NIL_STRING
@@ -83,20 +83,21 @@ module Contrast
83
83
  end
84
84
 
85
85
  def append_arg_details preshift, args
86
- preshift.args = args.dup
86
+ args_length = args.length
87
+ preshift.args = Array.new(args_length)
88
+ preshift.arg_lengths = Array.new(args_length)
87
89
  idx = 0
88
- while idx < preshift.args.size
90
+ while idx < args_length
89
91
  original_arg = args[idx]
90
- p_arg = preshift.args[idx]
92
+ p_arg = can_dup?(false, original_arg) ? original_arg.dup : original_arg
93
+ preshift.args[idx] = p_arg
94
+ preshift.arg_lengths[idx] = Contrast::Utils::DuckUtils.quacks_to?(p_arg, :length) ? p_arg.length : 0
91
95
  idx += 1
92
96
  next if p_arg.__id__ == original_arg.__id__
93
97
  next unless Contrast::Agent::Assess::Tracker.tracked?(original_arg)
94
98
 
95
99
  Contrast::Agent::Assess::Tracker.copy(original_arg, p_arg)
96
100
  end
97
- preshift.arg_lengths = preshift.args.map do |preshift_arg|
98
- Contrast::Utils::DuckUtils.quacks_to?(preshift_arg, :length) ? preshift_arg.length : 0
99
- end
100
101
  end
101
102
  end
102
103
  end
@@ -15,7 +15,7 @@ module Contrast
15
15
  case ret
16
16
  when Array
17
17
  idx = 0
18
- while idx < ret.size
18
+ while idx < ret.length
19
19
  return_value = ret[idx]
20
20
  index = idx
21
21
  idx += 1
@@ -34,7 +34,7 @@ module Contrast
34
34
 
35
35
  def captures_tagger propagation_node, preshift, ret, _block
36
36
  idx = 0
37
- while idx < ret.size
37
+ while idx < ret.length
38
38
  return_value = ret[idx]
39
39
  index = idx
40
40
  idx += 1
@@ -48,7 +48,7 @@ module Contrast
48
48
 
49
49
  def to_a_tagger propagation_node, preshift, ret, _block
50
50
  idx = 0
51
- while idx < ret.size
51
+ while idx < ret.length
52
52
  return_value = ret[idx]
53
53
  index = idx
54
54
  idx += 1
@@ -61,7 +61,7 @@ module Contrast
61
61
 
62
62
  def values_at_tagger propagation_node, preshift, ret, _block
63
63
  idx = 0
64
- while idx < ret.size
64
+ while idx < ret.length
65
65
  return_value = ret[idx]
66
66
  return_index = idx
67
67
  idx += 1
@@ -26,7 +26,6 @@ module Contrast
26
26
  return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
27
27
 
28
28
  source_string = source.is_a?(String) ? source : source.to_s
29
-
30
29
  # If the lengths are the same, we should just copy the tags because nothing was removed, but a new
31
30
  # instance could have been created. copy_from will handle the case where the source is the target.
32
31
  if source_string.length == target.length
@@ -34,10 +33,7 @@ module Contrast
34
33
  return
35
34
  end
36
35
 
37
- source_chars = source_string.chars
38
36
  source_idx = 0
39
-
40
- target_chars = target.chars
41
37
  target_idx = 0
42
38
 
43
39
  remove_ranges = []
@@ -45,10 +41,9 @@ module Contrast
45
41
 
46
42
  # loop over the target, the result of the delete every range of characters that it differs from the
47
43
  # source represents a section that was deleted. these sections need to have their tags updated
48
- target_len = target_chars.length
49
- while target_idx < target_len
50
- target_char = target_chars[target_idx]
51
- source_char = source_chars[source_idx]
44
+ while target_idx < target.length
45
+ target_char = target[target_idx]
46
+ source_char = source_string[source_idx]
52
47
  if target_char == source_char
53
48
  target_idx += 1
54
49
  if start
@@ -63,7 +58,7 @@ module Contrast
63
58
 
64
59
  # once we're done looping over the target, anything left over is extra from the source that was
65
60
  # deleted. tags applying to it need to be removed.
66
- remove_ranges << (source_idx...source_chars.length) if source_idx != source_chars.length
61
+ remove_ranges << (source_idx...source_string.length) if source_idx != source_string.length
67
62
 
68
63
  # handle deleting the removed ranges
69
64
  properties.delete_tags_at_ranges(remove_ranges)
@@ -246,9 +246,8 @@ module Contrast
246
246
  logger.debug('Trigger source is untrackable. Unable to inspect.',
247
247
  node_id: trigger_node.id,
248
248
  source_id: source.__id__,
249
- source_type: source.cs__class.to_s,
249
+ source_type: source.cs__class.cs__name,
250
250
  frozen: source.cs__frozen?)
251
- logger.trace(source.to_s[0..99])
252
251
  end
253
252
  end
254
253
 
@@ -60,7 +60,7 @@ module Contrast
60
60
 
61
61
  digest = Contrast::Utils::Sha256Builder.instance.build_from_spec(spec)
62
62
  unless digest
63
- logger.debug('Unable to resolve digest for gem spec', spec: spec.to_s)
63
+ logger.debug('Unable to resolve digest for gem spec', spec: spec.to_s) if logger.debug?
64
64
  return
65
65
  end
66
66
  report_path = adjust_path_for_reporting(path, spec)
@@ -92,10 +92,12 @@ module Contrast
92
92
  defined?(Rack::Multipart::UploadedFile) &&
93
93
  body.is_a?(Rack::Multipart::UploadedFile)
94
94
 
95
- logger.trace("not parsing uploaded file body :: #{ body.original_filename }::#{ body.content_type }")
95
+ logger.trace('not parsing uploaded file body',
96
+ file_name: body.original_filename,
97
+ content_type: body.content_type)
96
98
  @_body = nil
97
99
  else
98
- logger.trace("parsing body from request :: #{ body.cs__class.cs__name }")
100
+ logger.trace('parsing body from request', body_type: body.cs__class.cs__name)
99
101
  @_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
100
102
  end
101
103
 
@@ -185,7 +187,7 @@ module Contrast
185
187
  when Enumerable
186
188
  idx = 0
187
189
  res = {}
188
- while idx < val.size
190
+ while idx < val.length
189
191
  res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
190
192
  idx += 1
191
193
  end
@@ -131,8 +131,10 @@ module Contrast
131
131
  handle_protect_state(service_response)
132
132
  ia = service_response.input_analysis
133
133
  if ia
134
- logger.trace("Analysis from Contrast Service: evaluations=#{ ia.results.length }")
135
- logger.trace('Results', input_analysis: ia.inspect)
134
+ if logger.trace?
135
+ logger.trace('Analysis from Contrast Service', evaluations: ia.results.length)
136
+ logger.trace('Results', input_analysis: ia.inspect)
137
+ end
136
138
  @speedracer_input_analysis = ia
137
139
  speedracer_input_analysis.request = request
138
140
  else
@@ -202,7 +204,10 @@ module Contrast
202
204
  rule = ::Contrast::PROTECT.rule(rule_id)
203
205
  next unless rule
204
206
 
205
- logger.debug('Building attack result from Contrast Service input analysis result', result: ia_result.inspect)
207
+ if logger.debug?
208
+ logger.debug('Building attack result from Contrast Service input analysis result',
209
+ result: ia_result.inspect)
210
+ end
206
211
 
207
212
  attack_result = if rule.mode == :BLOCK
208
213
  # special case for rules (like reflected xss) that used to have an infilter / block mode
@@ -104,39 +104,51 @@ module Contrast
104
104
  exit_split_scope!
105
105
  end
106
106
 
107
- # Dynamic versions of the above.
108
- # These are equivalent, but they're slower and riskier.
109
- # Prefer the static methods if you know what scope you need at the call site.
107
+ # Static methods to be used, the cases are defined by the usage from the above methods
108
+ # if more methods are added - please extend the case statements as they are no longed dynamic
110
109
  def in_scope? name
111
- cs__class.ensure_valid_scope! name
112
- call = with_contrast_scope { :"in_#{ name }_scope?" }
113
- send(call)
110
+ case name
111
+ when :contrast
112
+ in_contrast_scope?
113
+ when :deserialization
114
+ in_deserialization_scope?
115
+ when :split
116
+ in_split_scope?
117
+ else
118
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
119
+ end
114
120
  end
115
121
 
116
122
  def enter_scope! name
117
- cs__class.ensure_valid_scope! name
118
- call = with_contrast_scope { :"enter_#{ name }_scope!" }
119
- send(call)
123
+ case name
124
+ when :contrast
125
+ enter_contrast_scope!
126
+ when :deserialization
127
+ enter_deserialization_scope!
128
+ when :split
129
+ enter_split_scope!
130
+ else
131
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
132
+ end
120
133
  end
121
134
 
122
135
  def exit_scope! name
123
- cs__class.ensure_valid_scope! name
124
- call = with_contrast_scope { :"exit_#{ name }_scope!" }
125
- send(call)
136
+ case name
137
+ when :contrast
138
+ exit_contrast_scope!
139
+ when :deserialization
140
+ exit_deserialization_scope!
141
+ when :split
142
+ exit_split_scope!
143
+ else
144
+ raise NoMethodError, "Scope '#{ name.inspect }' is not registered as a scope."
145
+ end
126
146
  end
127
147
 
128
148
  class << self
129
149
  def valid_scope? scope_sym
130
150
  Contrast::Agent::Scope::SCOPE_LIST.include? scope_sym
131
151
  end
132
-
133
- def ensure_valid_scope! scope_sym
134
- unless valid_scope? scope_sym # rubocop:disable Style/GuardClause
135
- with_contrast_scope do
136
- raise NoMethodError, "Scope '#{ scope_sym.inspect }' is not registered as a scope."
137
- end
138
- end
139
- end
140
152
  end
141
153
  end
142
154
  end
@@ -27,14 +27,22 @@ module Contrast
27
27
 
28
28
  private
29
29
 
30
+ # Use the TracePoint from the :end event, meaning the completion of a definition of a Class or Module (or
31
+ # really the completion of that piece of a definition, as determined by an `end` statement since there could be
32
+ # definitions across multiple files) to carry out actions required on definition. This typically involves
33
+ # patching and usage analysis
34
+ #
35
+ # @param tracepoint_event [TracePoint] the TracePoint from the :end
30
36
  def process tracepoint_event
31
37
  with_contrast_scope do
32
- logger.trace('Received TracePoint end event', module: tracepoint_event.self.to_s)
33
-
38
+ # the Module or Class that was loaded during this event
34
39
  loaded_module = tracepoint_event.self
40
+ # the file being loaded that contained this definition
35
41
  path = tracepoint_event.path
36
42
  return if path&.include?('contrast')
37
43
 
44
+ logger.trace('Received TracePoint end event', module: loaded_module, path: path)
45
+
38
46
  Contrast::Agent.framework_manager.register_late_framework(loaded_module)
39
47
  Contrast::Agent::Inventory::DependencyUsageAnalysis.instance.associate_file(path) if path
40
48
  Contrast::Agent::Patching::Policy::Patcher.patch_specific_module(loaded_module)
@@ -43,7 +51,7 @@ module Contrast
43
51
  end
44
52
  Contrast::Agent::Assess::Policy::PolicyScanner.scan(tracepoint_event)
45
53
  rescue StandardError => e
46
- logger.error('Unable to complete TracePoint analysis', e, module: loaded_module)
54
+ logger.error('Unable to complete TracePoint analysis', e, module: loaded_module, path: path)
47
55
  end
48
56
  end
49
57
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '4.10.0'
6
+ VERSION = '4.11.0'
7
7
  end
8
8
  end
@@ -3,11 +3,13 @@
3
3
 
4
4
  require 'contrast/extension/module'
5
5
  require 'contrast/utils/object_share'
6
+ require 'contrast/utils/lru_cache'
6
7
 
7
8
  module Contrast
8
9
  module Utils
9
10
  # Utility methods for exploring the complete space of Objects
10
11
  class ClassUtil
12
+ @lru_cache = LRUCache.new
11
13
  class << self
12
14
  # some classes have had things prepended to them, like Marshal in Rails
13
15
  # 5 and higher. Their ActiveSupport::MarshalWithAutoloading will break
@@ -47,27 +49,32 @@ module Contrast
47
49
  # @param object [Object, nil] the entity to convert to a String
48
50
  # @return [String] the human readable form of the String, as defined by
49
51
  # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/capture-snapshot.md
52
+
50
53
  def to_contrast_string object
51
- # Only treat object like a string if it actually is a string
54
+ # After implementing the LRU Cache, we firstly need to check if already had that object cached
55
+ # and if we have it - we can return it directly
56
+ return @lru_cache[object.__id__] if @lru_cache.key? object.__id__
57
+
58
+ # Only treat object like a string if it actually is a string+
52
59
  # some subclasses of String override string methods we depend on
53
- if object.cs__class == String
54
- cached = to_cached_string(object)
55
- return cached if cached
56
-
57
- object.dup
58
- elsif object.nil?
59
- Contrast::Utils::ObjectShare::NIL_STRING
60
- elsif object.cs__is_a?(Symbol)
61
- ":#{ object }"
62
- elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
63
- "#{ object.cs__name }@#{ object.__id__ }"
64
- elsif object.cs__is_a?(Regexp)
65
- object.source
66
- elsif use_to_s?(object)
67
- object.to_s
68
- else
69
- "#{ object.cs__class.cs__name }@#{ object.__id__ }"
70
- end
60
+ @lru_cache[object.__id__] = if object.cs__class == String
61
+ cached = to_cached_string(object)
62
+ return cached if cached
63
+
64
+ object.dup
65
+ elsif object.nil?
66
+ Contrast::Utils::ObjectShare::NIL_STRING
67
+ elsif object.cs__is_a?(Symbol)
68
+ ":#{ object }"
69
+ elsif object.cs__is_a?(Module) || object.cs__is_a?(Class)
70
+ "#{ object.cs__name }@#{ object.__id__ }"
71
+ elsif object.cs__is_a?(Regexp)
72
+ object.source
73
+ elsif use_to_s?(object)
74
+ object.to_s
75
+ else
76
+ "#{ object.cs__class.cs__name }@#{ object.__id__ }"
77
+ end
71
78
  end
72
79
 
73
80
  # The method const_defined? can cause autoload, which is bad for us.
@@ -9,40 +9,48 @@ module Contrast
9
9
  module IOUtil
10
10
  extend Contrast::Components::Logger::InstanceMethods
11
11
 
12
- # We're only going to call rewind on things that we believe are safe to
13
- # call it on. This method white lists those cases and returns false in
14
- # all others.
15
- def self.should_rewind? potential_io
16
- return true if potential_io.is_a?(StringIO)
17
- return false unless io?(potential_io)
18
-
19
- should_rewind_io?(potential_io)
20
- rescue StandardError => e
21
- logger.debug('Encountered an issue determining if rewindable', e, module: potential_io.cs__class.cs__name)
22
- false
23
- end
24
-
25
- # IO cannot be used with streams such as pipes, ttys, and sockets.
26
- def self.should_rewind_io? io
27
- return false if io.tty?
28
-
29
- status = io.stat
30
- return false unless status
31
- return false if status.pipe?
32
- return false if status.socket?
33
-
34
- true
35
- end
36
-
37
- # A class is IO if it is a decedent or delegate of IO
38
- def self.io? object
39
- return true if object.is_a?(IO)
40
-
41
- # DelegateClass, which is a Delegator, defines __getobj__ as a way to
42
- # get the object that the class wraps around (delegates to)
43
- return false unless object.is_a?(Delegator)
44
-
45
- object.__getobj__.is_a?(IO)
12
+ class << self
13
+ # We're only going to call rewind on things that we believe are safe to
14
+ # call it on. This method white lists those cases and returns false in
15
+ # all others.
16
+ def should_rewind? potential_io
17
+ return true if potential_io.is_a?(StringIO)
18
+ return false unless io?(potential_io)
19
+
20
+ should_rewind_io?(potential_io)
21
+ rescue StandardError => e
22
+ logger.debug('Encountered an issue determining if rewindable', e, module: potential_io.cs__class.cs__name)
23
+ false
24
+ end
25
+
26
+ # A class is IO if it is a decedent or delegate of IO
27
+ def io? object
28
+ return true if object.is_a?(IO)
29
+
30
+ # DelegateClass, which is a Delegator, defines __getobj__ as a way to
31
+ # get the object that the class wraps around (delegates to)
32
+ return false unless object.is_a?(Delegator)
33
+
34
+ object.__getobj__.is_a?(IO)
35
+ end
36
+
37
+ private
38
+
39
+ # IO rewind cannot be used with streams such as pipes, ttys, and sockets or for ones which have been closed.
40
+ #
41
+ # @param io [IO] the input to check for the ability to rewind
42
+ # @return [Boolean] if the given IO can be rewound
43
+ def should_rewind_io? io
44
+ return false if io.closed?
45
+ return false if io.tty?
46
+
47
+ status = io.stat
48
+ return false unless status
49
+ return false if status.pipe?
50
+ return false if status.socket?
51
+
52
+ true
53
+ end
46
54
  end
47
55
  end
48
56
  end
@@ -0,0 +1,43 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/components/logger'
5
+
6
+ module Contrast
7
+ module Utils
8
+ # A LRU(Least Recently Used) Cache store.
9
+ class LRUCache
10
+ def initialize capacity = 500
11
+ raise StandardError 'Capacity must be bigger than 0' if capacity <= 0
12
+
13
+ @capacity = capacity
14
+ @cache = {}
15
+ end
16
+
17
+ def [] key
18
+ val = @cache.delete(key)
19
+ @cache[key] = val if val
20
+ val
21
+ end
22
+
23
+ def []= key, value
24
+ @cache.delete(key)
25
+ @cache[key] = value
26
+ @cache.shift if @cache.size > @capacity
27
+ value # rubocop:disable Lint/Void
28
+ end
29
+
30
+ def keys
31
+ @cache.keys
32
+ end
33
+
34
+ def key? key
35
+ @cache.key?(key)
36
+ end
37
+
38
+ def values
39
+ @cache.values
40
+ end
41
+ end
42
+ end
43
+ end
@@ -38,7 +38,7 @@ module Contrast
38
38
  return if node.children.all? { |child_node| child_node.type == :str }
39
39
  new_content = +'('
40
40
  idx = 0
41
- while idx < node.children.size
41
+ while idx < node.children.length
42
42
  #node.children.each_with_index do |child_node, index|
43
43
  child_node = node.children[idx]
44
44
  # A begin node looks like #{some_code} in ruby, we do a substring
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrast-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.10.0
4
+ version: 4.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - galen.palmer@contrastsecurity.com
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: exe
15
15
  cert_chain: []
16
- date: 2021-08-31 00:00:00.000000000 Z
16
+ date: 2021-09-23 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: bundler
@@ -617,20 +617,20 @@ executables:
617
617
  - contrast_service
618
618
  extensions:
619
619
  - ext/cs__common/extconf.rb
620
- - ext/cs__assess_fiber_track/extconf.rb
621
- - ext/cs__assess_marshal_module/extconf.rb
622
- - ext/cs__assess_kernel/extconf.rb
623
- - ext/cs__assess_basic_object/extconf.rb
624
- - ext/cs__assess_string/extconf.rb
620
+ - ext/cs__assess_array/extconf.rb
625
621
  - ext/cs__assess_regexp/extconf.rb
626
622
  - ext/cs__protect_kernel/extconf.rb
623
+ - ext/cs__assess_marshal_module/extconf.rb
624
+ - ext/cs__assess_yield_track/extconf.rb
625
+ - ext/cs__assess_string_interpolation26/extconf.rb
626
+ - ext/cs__assess_fiber_track/extconf.rb
627
+ - ext/cs__assess_string/extconf.rb
628
+ - ext/cs__assess_hash/extconf.rb
629
+ - ext/cs__assess_kernel/extconf.rb
627
630
  - ext/cs__contrast_patch/extconf.rb
628
- - ext/cs__assess_active_record_named/extconf.rb
631
+ - ext/cs__assess_basic_object/extconf.rb
629
632
  - ext/cs__assess_module/extconf.rb
630
- - ext/cs__assess_hash/extconf.rb
631
- - ext/cs__assess_string_interpolation26/extconf.rb
632
- - ext/cs__assess_array/extconf.rb
633
- - ext/cs__assess_yield_track/extconf.rb
633
+ - ext/cs__assess_active_record_named/extconf.rb
634
634
  extra_rdoc_files: []
635
635
  files:
636
636
  - ".clang-format"
@@ -1079,6 +1079,7 @@ files:
1079
1079
  - lib/contrast/utils/invalid_configuration_util.rb
1080
1080
  - lib/contrast/utils/io_util.rb
1081
1081
  - lib/contrast/utils/job_servers_running.rb
1082
+ - lib/contrast/utils/lru_cache.rb
1082
1083
  - lib/contrast/utils/object_share.rb
1083
1084
  - lib/contrast/utils/os.rb
1084
1085
  - lib/contrast/utils/preflight_util.rb