contrast-agent 4.5.0 → 4.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34510a6c078720ccde6e95d4c0ee3a59eb6fa7db96da03ad7d047a3e926979ad
4
- data.tar.gz: c9bceacdf3de12ef0554e066d8275084d4ff199cdff143077f32f08fc5415b31
3
+ metadata.gz: f9748bb3a9e7e6ce4722edc88dbb3bd8d3a59b11dd691f47ac29b3ee9e6a00ba
4
+ data.tar.gz: d5b5ff651f2914bff6da5cbd2fdcdc28a2dd1a2ea8ad4eced6c416cbab72b632
5
5
  SHA512:
6
- metadata.gz: d5a507dcf5c610fbd2dd05b711ea9b1a553bbca720b769fc54f034d2d323184f23fb39ff793edf2d5074b568a4b557f817779f07d7937b966ab19d1acd3bdfb5
7
- data.tar.gz: c00857ade1593231404e02583d9726e29e474a49b984f46a57d9a103591259757c94677cb01075de92c63e096eee0d9dd68c2f7f9703ebf0b9d09ea267c282e4
6
+ metadata.gz: 6e03ec0934d333e79f957515dfdc851861d395e9c768d09ffdfbf56b6e818d27361cffb2f010985f6da2182b1a4b070ef2291b86ccd9280fb117c4125611ae8f
7
+ data.tar.gz: bb60224de8c19b547f311aca8d78a2b99593e329696ab6acbaf59336316109be5e9419d02e6e4cd06d0714627c0942fea6d1c0872a6c69faff531838e1dbc359
@@ -27,8 +27,6 @@ require 'contrast/utils/string_utils'
27
27
  require 'contrast/utils/io_util'
28
28
  require 'contrast/utils/os'
29
29
 
30
- require 'contrast/common_agent_configuration'
31
-
32
30
  require 'contrast/utils/hash_digest'
33
31
  require 'contrast/utils/invalid_configuration_util'
34
32
 
@@ -6,28 +6,35 @@ module Contrast
6
6
  module Assess
7
7
  module Policy
8
8
  module Propagator
9
- # Propagation that results in all the tags of the source being
10
- # applied to the totality of the target and then those sections
11
- # which have been removed from the target are removed from the
12
- # tags. The target's preexisting tags are also updated by this
13
- # removal.
9
+ # Propagation that results in all the tags of the source being applied to the totality of the target and then
10
+ # those sections which have been removed from the target are removed from the tags. The target's preexisting
11
+ # tags are also updated by this removal.
14
12
  class Remove < Contrast::Agent::Assess::Policy::Propagator::Base
15
13
  class << self
16
- # For the source, append its tags to the target.
17
- # Once the tag is applied, remove the section that was removed by the delete.
18
- # Unlike additive propagation, this currently only supports one source
14
+ # For the source, append its tags to the target. Once the tag is applied, remove the section that was
15
+ # removed by the delete. Unlike additive propagation, this currently only supports one source.
19
16
  def propagate propagation_node, preshift, target
20
17
  return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
21
18
 
22
19
  source = find_source(propagation_node.sources[0], preshift)
23
20
  properties.copy_from(source, target, 0, propagation_node.untags)
24
- source_chars = source.is_a?(String) ? source.chars : source.string.chars
25
- handle_removal(source_chars, target)
21
+ handle_removal(propagation_node, source, target)
26
22
  end
27
23
 
28
- def handle_removal source_chars, target
24
+ def handle_removal propagation_node, source, target
25
+ return unless source
29
26
  return unless (properties = Contrast::Agent::Assess::Tracker.properties!(target))
30
27
 
28
+ source_string = source.is_a?(String) ? source : source.to_s
29
+
30
+ # If the lengths are the same, we should just copy the tags because nothing was removed, but a new
31
+ # instance could have been created. copy_from will handle the case where the source is the target.
32
+ if source_string.length == target.length
33
+ properties.copy_from(source, target, 0, propagation_node.untags)
34
+ return
35
+ end
36
+
37
+ source_chars = source_string.chars
31
38
  source_idx = 0
32
39
 
33
40
  target_chars = target.chars
@@ -36,10 +43,8 @@ module Contrast
36
43
  remove_ranges = []
37
44
  start = nil
38
45
 
39
- # loop over the target, the result of the delete
40
- # every range of characters that it differs from the source
41
- # represents a section that was deleted. these sections
42
- # need to have their tags updated
46
+ # loop over the target, the result of the delete every range of characters that it differs from the
47
+ # source represents a section that was deleted. these sections need to have their tags updated
43
48
  target_len = target_chars.length
44
49
  while target_idx < target_len
45
50
  target_char = target_chars[target_idx]
@@ -56,9 +61,8 @@ module Contrast
56
61
  source_idx += 1
57
62
  end
58
63
 
59
- # once we're done looping over the target, anything left
60
- # over is extra from the source that was deleted. tags
61
- # applying to it need to be removed.
64
+ # once we're done looping over the target, anything left over is extra from the source that was
65
+ # deleted. tags applying to it need to be removed.
62
66
  remove_ranges << (source_idx...source_chars.length) if source_idx != source_chars.length
63
67
 
64
68
  # handle deleting the removed ranges
@@ -8,49 +8,30 @@ module Contrast
8
8
  module Propagator
9
9
  # This class is specifically for String#tr(_s) propagation
10
10
  #
11
- # Disclaimer: there may be a better way, but we're
12
- # in a 'get it work' state. hopefully, we'll be in
13
- # a 'get it right' state soon.
11
+ # Disclaimer: there may be a better way, but we're in a 'get it work' state. hopefully, we'll be in a 'get it
12
+ # right' state soon.
14
13
  module Trim
15
14
  class << self
16
- def tr_tagger patcher, preshift, ret, _block
15
+
16
+ # @param policy_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
17
+ # propagation event.
18
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
19
+ # the invocation of the patched method.
20
+ # @param ret [nil, String] the target to which to propagate.
21
+ # @return [nil, String] ret
22
+ def tr_tagger policy_node, preshift, ret, _block
17
23
  return ret unless ret && !ret.empty?
18
24
  return ret unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
19
25
 
20
- source = preshift.object
21
- args = preshift.args
22
- properties.copy_from(source, ret)
23
- replace_string = args[1]
24
- source_chars = source.chars
25
- # if the replace string is empty, then there's a bunch of deletes. this
26
- # functions the same as the Removal propagation.
27
- if replace_string == Contrast::Utils::ObjectShare::EMPTY_STRING
28
- Contrast::Agent::Assess::Policy::Propagator::Remove.handle_removal(source_chars, ret)
29
- else
30
- remove_ranges = []
31
- ret_chars = ret.chars
32
- start = nil
33
- source_chars.each_with_index do |char, idx|
34
- if ret_chars[idx] == char
35
- next unless start
36
-
37
- remove_ranges << (start...idx)
38
- start = nil
39
- else
40
- start ||= idx
41
- end
42
- end
43
- # account for the last char being different
44
- remove_ranges << (start...source_chars.length) if start
45
- properties.delete_tags_at_ranges(remove_ranges, false)
46
- end
26
+ properties.copy_from(preshift.object, ret)
27
+ handle_tr(policy_node, preshift, ret, properties)
47
28
 
48
29
  properties.build_event(
49
- patcher,
30
+ policy_node,
50
31
  ret,
51
- source,
32
+ preshift.object,
52
33
  ret,
53
- args,
34
+ preshift.args,
54
35
  1)
55
36
  ret
56
37
  end
@@ -70,6 +51,56 @@ module Contrast
70
51
  args)
71
52
  ret
72
53
  end
54
+
55
+ private
56
+
57
+ # @param policy_node [Contrast::Agent::Assess::Policy::PropagationNode] the node that governs this
58
+ # propagation event.
59
+ # @param preshift [Contrast::Agent::Assess::PreShift] The capture of the state of the code just prior to
60
+ # the invocation of the patched method.
61
+ # @param ret [String] the target to which to propagate.
62
+ # @param properties [Contrast::Agent::Assess::Properties] the properties of the ret
63
+ def handle_tr policy_node, preshift, ret, properties
64
+ source = preshift.object
65
+ replace_string = preshift.args[1]
66
+
67
+ # if the replace string is empty, then there's a bunch of deletes. this functions the same as the
68
+ # Removal propagation.
69
+ if replace_string == Contrast::Utils::ObjectShare::EMPTY_STRING
70
+ Contrast::Agent::Assess::Policy::Propagator::Remove.handle_removal(policy_node, source, ret)
71
+ return
72
+ end
73
+
74
+ # Otherwise, we need to target each insertion point. Based on the spec for #tr & #tr_s, the find is
75
+ # treated as a regex range, excepting the `\` character, which we'll need to escape. This converts to
76
+ # that form, wrapping the input in `[]`.
77
+ find_string = preshift.args[0]
78
+ find_string += '\\' if find_string.end_with?('\\')
79
+ find_regexp = Regexp.new("[#{ find_string }]")
80
+
81
+ # Find the first instance to be replaced. If there isn't one, than nothing changed here.
82
+ idx = source.index(find_regexp)
83
+ return unless idx
84
+
85
+ # Iterate over each change and record where it happened. B/c this is a one to one replace, the index of
86
+ # the replacement is always one; however, there may be adjacent replacements which become a single
87
+ # range.
88
+ start = idx
89
+ stop = idx + 1
90
+ remove_ranges = []
91
+ while (idx = source.index(find_regexp, idx + 1))
92
+ # If the previous range ends at this index, we can expand that range to include this index.
93
+ # Otherwise, we need to record the held range and start a new one.
94
+ if stop != idx
95
+ remove_ranges << (start...stop)
96
+ start = idx
97
+ end
98
+ stop = idx + 1
99
+ end
100
+ # Be sure to capture the last range in the holder.
101
+ remove_ranges << (start...stop)
102
+ properties.delete_tags_at_ranges(remove_ranges, false)
103
+ end
73
104
  end
74
105
  end
75
106
  end
@@ -8,8 +8,8 @@ module Contrast
8
8
  module Agent
9
9
  module Deadzone
10
10
  module Policy
11
- # This is just a holder for our policy. Takes the policy JSON and
12
- # converts it into hashes that we can access nicely
11
+ # This is just a holder for our policy. Takes the policy JSON and converts it into hashes that we can access
12
+ # nicely.
13
13
  class Policy < Contrast::Agent::Patching::Policy::Policy
14
14
  def self.policy_folder
15
15
  'deadzone'
@@ -35,6 +35,10 @@ module Contrast
35
35
  end
36
36
  end
37
37
 
38
+ def module_names
39
+ @_module_names ||= Set.new(deadzones.map(&:class_name))
40
+ end
41
+
38
42
  def add_node node, _node_type = :deadzones
39
43
  unless node
40
44
  logger.error('Node was nil when adding node to policy')
@@ -24,8 +24,8 @@ module Contrast
24
24
  @lock.synchronize { @gemdigest_cache = Hash.new { |hash, key| hash[key] = Set.new } }
25
25
  end
26
26
 
27
- # This method is invoked once, along with the rest of our catchup code to report libraries and their
28
- # associated files that have already been loaded pre-contrast.
27
+ # This method is invoked once, along with the rest of our catchup code to report libraries and their associated
28
+ # files that have already been loaded pre-contrast.
29
29
  def catchup
30
30
  return unless enabled?
31
31
 
@@ -45,8 +45,8 @@ module Contrast
45
45
  end
46
46
  end
47
47
 
48
- # This method is invoked once per TracePoint :end - to map a specific file being required to the gem
49
- # it belongs to.
48
+ # This method is invoked once per TracePoint :end - to map a specific file being required to the gem to which
49
+ # it belongs.
50
50
  #
51
51
  # @param path [String] the result of TracePoint#path from the :end event in which the Module was defined.
52
52
  def associate_file path
@@ -71,7 +71,7 @@ module Contrast
71
71
  logger.error('Unable to inventory file path', e, path: path)
72
72
  end
73
73
 
74
- # Populate the library_usages field of the Activity message using the data stored in the @gemdigest_cache
74
+ # Populate the library_usages field of the Activity message using the data stored in the @gemdigest_cache.
75
75
  #
76
76
  # @param activity [Contrast::Api::Dtm::Activity] the message to which to append the usage data
77
77
  def generate_library_usage activity
@@ -79,7 +79,7 @@ module Contrast
79
79
  return unless activity
80
80
 
81
81
  # Copy gemdigest_cache and clear it in sync.
82
- gem_spec_digest_to_files = @lock.synchronize do
82
+ gem_spec_digest_to_files = @lock.synchronize do
83
83
  copy = @gemdigest_cache.dup
84
84
  @gemdigest_cache.clear
85
85
  copy
@@ -16,18 +16,17 @@ require 'contrast/utils/timer'
16
16
 
17
17
  module Contrast
18
18
  module Agent
19
- # This class allows the Agent to plug into the Rack middleware stack. When the
20
- # application is first started, we initialize ourselves as a rack middleware
21
- # inside of #initialize. Afterwards, we process each http request and response
22
- # as it goes through the middleware stack inside of #call.
19
+ # This class allows the Agent to plug into the Rack middleware stack. When the application is first started, we
20
+ # initialize ourselves as a rack middleware inside of #initialize. Afterwards, we process each http request and
21
+ # response as it goes through the middleware stack inside of #call.
23
22
  class Middleware
24
23
  include Contrast::Components::Interface
25
24
  access_component :agent, :config, :logging, :scope, :settings
26
25
 
27
26
  attr_reader :app
28
27
 
29
- # Allows the Agent to function as a middleware. We perform all our one-time whole-app routines in here
30
- # since we're only going to be initialized a single time. Our initialization order is:
28
+ # Allows the Agent to function as a middleware. We perform all our one-time whole-app routines in here since
29
+ # we're only going to be initialized a single time. Our initialization order is:
31
30
  # - capture the application
32
31
  # - setup the Agent
33
32
  # - startup the Agent
@@ -47,14 +46,13 @@ module Contrast
47
46
  agent_startup_routine
48
47
  end
49
48
 
50
- # This is where we're hooked into the middleware stack. If the agent is enabled, we're ready
51
- # to do some processing on a per request basis. If not, we just pass the request along to the
52
- # next middleware in the stack.
49
+ # This is where we're hooked into the middleware stack. If the agent is enabled, we're ready to do some
50
+ # processing on a per request basis. If not, we just pass the request along to the next middleware in the stack.
53
51
  #
54
- # @param env [Hash] the various variables stored by this and other Middlewares to know the state
55
- # and values of this Request
56
- # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back
57
- # to the user up the Rack framework.
52
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
53
+ # this Request
54
+ # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back to the user up
55
+ # the Rack framework.
58
56
  def call env
59
57
  return app.call(env) unless AGENT.enabled?
60
58
 
@@ -100,43 +98,22 @@ module Contrast
100
98
  Contrast::Agent::AtExitHook.exit_hook
101
99
  end
102
100
 
103
- # Some things have to wait until first request to happen, either because
104
- # resolution is not complete or because the framework will preload
105
- # classes, which confuses some of our instrumentation.
101
+ # Some things have to wait until first request to happen, either because resolution is not complete or because
102
+ # the framework will preload classes, which confuses some of our instrumentation.
106
103
  def handle_first_request
107
104
  @_handle_first_request ||= begin
108
105
  Contrast::Agent::StaticAnalysis.catchup
109
- force_patching
110
106
  true
111
107
  end
112
108
  end
113
109
 
114
- # TODO: RUBY-1090 remove this method and those it calls.
115
- #
116
- # These modules are auto-loaded by Rails, meaning they are defined at
117
- # startup, but that they don't actually exist. We account for this in
118
- # most cases by using the ClassUtil.truly_defined? method, but it appears
119
- # to fail for these Modules. In the short term, we can forcing a re-patch
120
- # so that their dead zones apply.
121
- def force_patching
122
- force_patch(ActionDispatch::FileHandler) if defined?(ActionDispatch::FileHandler)
123
- force_patch(ActionDispatch::Http::MimeNegotiation) if defined?(ActionDispatch::Http::MimeNegotiation)
124
- force_patch(ActionDispatch::Journey::Router) if defined?(ActionDispatch::Journey::Router)
125
- force_patch(ActionView::Template) if defined?(ActionView::Template)
126
- end
127
-
128
- def force_patch mod
129
- data = Contrast::Agent::ModuleData.new(mod)
130
- Contrast::Agent::Patching::Policy::Patcher.send(:patch_into_module, data, true)
131
- end
132
-
133
- # This is where we process each request we intercept as a middleware. We make the request context
134
- # available globally so that it can be accessed from anywhere. A RequestHandler object is made
135
- # for each request, which handles prefilter and postfilter operations.
136
- # @param env [Hash] the various variables stored by this and other Middlewares to know the state
137
- # and values of this Request
138
- # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back
139
- # to the user up the Rack framework.
110
+ # This is where we process each request we intercept as a middleware. We make the request context available
111
+ # globally so that it can be accessed from anywhere. A RequestHandler object is made for each request, which
112
+ # handles prefilter and postfilter operations.
113
+ # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
114
+ # this Request
115
+ # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back to the user
116
+ # up the Rack framework.
140
117
  def call_with_agent env
141
118
  Contrast::Agent.thread_watcher.ensure_running?
142
119
  framework_request = Contrast::Agent.framework_manager.retrieve_request(env)
@@ -211,8 +188,8 @@ module Contrast
211
188
  end
212
189
 
213
190
  SECURITY_EXCEPTION_MARKER = 'Contrast::SecurityException'
214
- # We're only going to suppress SecurityExceptions indicating a blocked attack.
215
- # And, only if the config.agent.ruby.exceptions.capture? is set
191
+ # We're only going to suppress SecurityExceptions indicating a blocked attack. And, only if the
192
+ # config.agent.ruby.exceptions.capture? is set
216
193
  def handle_exception exception
217
194
  if security_exception?(exception)
218
195
  exception_control = AGENT.exception_control
@@ -234,18 +211,15 @@ module Contrast
234
211
  exception.message&.include?(SECURITY_EXCEPTION_MARKER)
235
212
  end
236
213
 
237
- # As we deprecate support to prepare to remove dead code, we need to
238
- # inform our users still relying on the now deprecated and soon to be
239
- # removed functionality. This method handles doing that by leveraging the
240
- # standard Kernel#warn approach
214
+ # As we deprecate support to prepare to remove dead code, we need to inform our users still relying on the now
215
+ # deprecated and soon to be removed functionality. This method handles doing that by leveraging the standard
216
+ # Kernel#warn approach
241
217
  def inform_deprecations
242
- # Ruby 2.5 is currently in security maintenance, meaning int is only
243
- # receiving updates for security issues. It will move to eol on 31
244
- # March 2021. As such, we can remove support for it in Q2. We'll begin
245
- # the deprecation warnings now so that customers have time to reach out
246
- # if they'll be impacted.
247
- # TODO: RUBY-715 remove this part of the method, leaving it empty if
248
- # there are no other deprecations, when we drop 2.5 support.
218
+ # Ruby 2.5 is currently in security maintenance, meaning int is only receiving updates for security issues. It
219
+ # will move to eol on 31 March 2021. As such, we can remove support for it in Q3. We'll begin the deprecation
220
+ # warnings now so that customers have time to reach out if they'll be impacted.
221
+ # TODO: RUBY-715 remove this part of the method, leaving it empty if there are no other deprecations, when we
222
+ # drop 2.5 support.
249
223
  return unless RUBY_VERSION < '2.6.0'
250
224
 
251
225
  Kernel.warn('[Contrast Security] [DEPRECATION] Support for Ruby 2.5 will be removed in April 2021. '\
@@ -362,24 +362,21 @@ module Contrast
362
362
 
363
363
  case impl
364
364
  when :alias_instance, :alias_singleton
365
+ # Core to patching. Ignore define method usage cop.
366
+ # rubocop:disable Performance/Kernel/DefineMethod
365
367
  unless target_module.instance_methods(false).include? underlying_method_name
366
368
  # alias_method may be private
367
369
  target_module.send(:alias_method, underlying_method_name, method_name)
368
- # TODO: RUBY-1052
369
- # rubocop:disable Performance/Kernel/DefineMethod
370
370
  target_module.send(:define_method, method_name, unbound_method.bind(target_module))
371
- # rubocop:enable Performance/Kernel/DefineMethod
372
371
  end
373
372
  target_module.send(visibility, method_name) # e.g., module.private(:my_method)
374
373
  when :prepend
375
374
  prepending_module = Module.new
376
- # TODO: RUBY-1052
377
- # rubocop:disable Performance/Kernel/DefineMethod
378
375
  prepending_module.send(:define_method, method_name, unbound_method.bind(target_module))
379
- # rubocop:enable Performance/Kernel/DefineMethod
380
376
  prepending_module.send(visibility, method_name)
381
377
  # This prepends to the singleton class (it patches a class method)
382
378
  target_module.prepend prepending_module
379
+ # rubocop:enable Performance/Kernel/DefineMethod
383
380
  end
384
381
  # Ougai::Logger.create_item_with_2args calls Hash#[]=, so we
385
382
  # can't invoke this logging method or we'll seg fault as we'd
@@ -393,7 +390,7 @@ module Contrast
393
390
  # method: method_name,
394
391
  # visibility: visibility)
395
392
  # end
396
- underlying_method_name.to_sym
393
+ underlying_method_name
397
394
  end
398
395
 
399
396
  # @return [Boolean]
@@ -13,8 +13,9 @@ module Contrast
13
13
  module Agent
14
14
  module Patching
15
15
  module Policy
16
- # This is just a holder for our policy. Takes the policy JSON and
17
- # converts it into hashes that we can access nicely
16
+ # This is just a holder for our policy. Takes the policy JSON and converts it into hashes that we can access
17
+ # nicely.
18
+ #
18
19
  # @abstract
19
20
  class Policy
20
21
  include Singleton
@@ -25,8 +26,8 @@ module Contrast
25
26
  raise(NoMethodError, 'specify policy_folder for patching')
26
27
  end
27
28
 
28
- # Indicates is this feature has been disabled by the configuration,
29
- # read at startup, and therefore can never be enabled.
29
+ # Indicates is this feature has been disabled by the configuration, read at startup, and therefore can never
30
+ # be enabled.
30
31
  def disabled_globally?
31
32
  raise(NoMethodError, 'specify disabled_globally? conditions for patching')
32
33
  end
@@ -58,17 +59,15 @@ module Contrast
58
59
  from_hash_string(json)
59
60
  end
60
61
 
61
- # Our policy for patching rules is a 'dope ass' JSON file. Rather than
62
- # hard code in a bunch of things to monkey patch, we let the JSON file
63
- # define the conditions in which modifications are applied.
64
- # This let's us be flexible and extensible.
62
+ # Our policy for patching rules is a 'dope ass' JSON file. Rather than hard code in a bunch of things to
63
+ # monkey patch, we let the JSON file define the conditions in which modifications are applied. This let's us
64
+ # be flexible and extensible.
65
65
  def from_hash_string string
66
- # The default behavior of the agent is to load the policy on startup,
67
- # as at this point we do not know in which mode we'll be run.
66
+ # The default behavior of the agent is to load the policy on startup, as at this point we do not know in
67
+ # which mode we'll be run.
68
68
  #
69
- # If the configuration file explicitly disables a feature, we know
70
- # that we will not ever be able to enable it, so in that case, we
71
- # can skip policy loading.
69
+ # If the configuration file explicitly disables a feature, we know that we will not ever be able to enable
70
+ # it, so in that case, we can skip policy loading.
72
71
  return if disabled_globally?
73
72
 
74
73
  policy_data = JSON.parse(string)
@@ -110,13 +109,7 @@ module Contrast
110
109
  end
111
110
 
112
111
  def module_names
113
- @_module_names ||= begin
114
- m = Set.new
115
- sources.each { |source| m << source.class_name }
116
- propagators.each { |propagator| m << propagator.class_name }
117
- triggers.each { |trigger| m << trigger.class_name }
118
- m
119
- end
112
+ @_module_names ||= Set.new([sources, propagators, triggers].flatten.map!(&:class_name))
120
113
  end
121
114
 
122
115
  def find_triggers_by_rule rule_id
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '4.5.0'
6
+ VERSION = '4.6.0'
7
7
  end
8
8
  end
@@ -70,12 +70,12 @@ module Contrast
70
70
 
71
71
  logger.trace_with_time('Rebuilding rule modes') do
72
72
  SETTINGS.build_protect_rules if PROTECT.enabled?
73
- SETTINGS.build_assess_rules if ASSESS.enabled?
74
73
  AGENT.reset_ruleset
75
74
 
76
75
  logger.info('Current rule settings:')
76
+
77
77
  PROTECT.rules.each { |k, v| logger.info('Protect Rule mode set', rule: k, mode: v.mode) }
78
- ASSESS.rules.each { |k, v| logger.info('Assess Rule mode set', rule: k, mode: v.enabled?) }
78
+ logger.info('Disabled Assess Rules', rules: ASSESS.disabled_rules)
79
79
  end
80
80
  end
81
81
  end
@@ -36,7 +36,7 @@ module Contrast
36
36
  end
37
37
 
38
38
  def ruleset
39
- @_ruleset ||= Contrast::Agent::RuleSet.new(retrieve_ruleset&.values)
39
+ @_ruleset ||= Contrast::Agent::RuleSet.new(retrieve_protect_ruleset&.values)
40
40
  end
41
41
 
42
42
  def reset_ruleset
@@ -98,16 +98,10 @@ module Contrast
98
98
  @_interpolation_patch_possible
99
99
  end
100
100
 
101
- def retrieve_ruleset
102
- return {} unless enabled?
101
+ def retrieve_protect_ruleset
102
+ return {} unless enabled? && PROTECT.enabled?
103
103
 
104
- if ASSESS.enabled? && PROTECT.enabled?
105
- ASSESS.rules.merge(PROTECT.rules)
106
- elsif ASSESS.enabled?
107
- ASSESS.rules
108
- else
109
- PROTECT.rules
110
- end
104
+ PROTECT.rules
111
105
  end
112
106
  end
113
107
 
@@ -19,7 +19,7 @@ module Contrast
19
19
  return false if forcibly_disabled?
20
20
  return true if forcibly_enabled?
21
21
 
22
- SETTINGS.assess_enabled?
22
+ SETTINGS.assess_state.enabled == true
23
23
  end
24
24
 
25
25
  def tainted_columns
@@ -58,10 +58,9 @@ module Contrast
58
58
  # node types (SourceNode, PolicyNode, TriggerNode, PropagationNode)
59
59
  #
60
60
  # @param policy_node [Contrast::Agent::Assess::Policy::PolicyNode] The node in question.
61
- # @param return [Boolean] to capture or not to capture, that is the question.
61
+ # @return [Boolean] to capture or not to capture, that is the question.
62
62
  def capture_stacktrace? policy_node
63
63
  return true if capture_stacktrace_value == :ALL
64
-
65
64
  return false if capture_stacktrace_value == :NONE
66
65
 
67
66
  # Below here capture_stacktrace_value must be :SOME.
@@ -90,8 +89,9 @@ module Contrast
90
89
  CONFIG.root.assess&.tags
91
90
  end
92
91
 
93
- def rules
94
- SETTINGS.assess_rules
92
+ def disabled_rules
93
+ # TODO: RUBY-903
94
+ CONFIG.root.assess&.rules&.disabled_rules || SETTINGS.assess_state.disabled_assess_rules || []
95
95
  end
96
96
 
97
97
  private
@@ -100,11 +100,6 @@ module Contrast
100
100
  @_forcibly_enabled = true?(CONFIG.root.assess.enable) if @_forcibly_enabled.nil?
101
101
  @_forcibly_enabled
102
102
  end
103
-
104
- def disabled_rules
105
- # TODO: RUBY-903
106
- CONFIG.root.assess&.rules&.disabled_rules || SETTINGS.disabled_assess_rules || []
107
- end
108
103
  end
109
104
 
110
105
  COMPONENT_INTERFACE = Interface.new
@@ -4,11 +4,8 @@
4
4
  module Contrast
5
5
  module Components
6
6
  module Protect
7
- # A wrapper build around the Common Agent Configuration project to allow
8
- # for access of the values contained in its
9
- # parent_configuration_spec.yaml.
10
- # Specifically, this allows for querying the state of the Protect
11
- # product.
7
+ # A wrapper build around the Common Agent Configuration project to allow for access of the values contained in
8
+ # its parent_configuration_spec.yaml. Specifically, this allows for querying the state of the Protect product.
12
9
  class Interface
13
10
  include Contrast::Components::ComponentBase
14
11
  include Contrast::Components::Interface
@@ -20,7 +17,7 @@ module Contrast
20
17
  return false if forcibly_disabled?
21
18
  return true if forcibly_enabled?
22
19
 
23
- SETTINGS.protect_enabled?
20
+ SETTINGS.protect_state.enabled == true
24
21
  end
25
22
 
26
23
  def rule_config
@@ -28,17 +25,17 @@ module Contrast
28
25
  end
29
26
 
30
27
  def rules
31
- SETTINGS.protect_rules
28
+ SETTINGS.protect_state.rules
32
29
  end
33
30
 
34
31
  def rule_mode rule_id
35
32
  CONFIG.root.protect.rules[rule_id]&.applicable_mode ||
36
- SETTINGS.modes_by_id[rule_id] ||
33
+ SETTINGS.application_state.modes_by_id[rule_id] ||
37
34
  Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION
38
35
  end
39
36
 
40
37
  def rule name
41
- SETTINGS.protect_rules[name]
38
+ SETTINGS.protect_state.rules[name]
42
39
  end
43
40
 
44
41
  def report_any_command_execution?
@@ -58,15 +55,13 @@ module Contrast
58
55
  end
59
56
 
60
57
  def forcibly_disabled?
61
- @_forcibly_disabled = false?(CONFIG.root.protect.enable) if @_forcibly_disabled.nil?
62
- @_forcibly_disabled
58
+ @_forcibly_disabled ||= false?(CONFIG.root.protect.enable)
63
59
  end
64
60
 
65
61
  private
66
62
 
67
63
  def forcibly_enabled?
68
- @_forcibly_enabled = true?(CONFIG.root.protect.enable) if @_forcibly_enabled.nil?
69
- @_forcibly_enabled
64
+ @_forcibly_enabled ||= true?(CONFIG.root.protect.enable)
70
65
  end
71
66
  end
72
67
 
@@ -109,6 +109,7 @@ module Contrast
109
109
 
110
110
  def with_deserialization_scope
111
111
  scope_for_current_ec.enter_deserialization_scope!
112
+ yield
112
113
  ensure
113
114
  scope_for_current_ec.exit_deserialization_scope!
114
115
  end
@@ -8,134 +8,61 @@ module Contrast
8
8
  # directives (likely provided by TeamServer) about product operation.
9
9
  # 'Settings' is not a generic term for 'configurable stuff'.
10
10
  module Settings
11
+ APPLICATION_STATE_BASE = Struct.new(:modes_by_id, :exclusion_matchers).new(
12
+ Hash.new { Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION }, [])
13
+ PROTECT_STATE_BASE = Struct.new(:enabled, :rules).new(false, {})
14
+ ASSESS_STATE_BASE = Struct.new(:enabled, :sampling_settings, :disabled_assess_rules).new(false, nil, []) do
15
+ def sampling_settings= new_val
16
+ @sampling_settings = new_val
17
+ Contrast::Utils::Assess::SamplingUtil.instance.update
18
+ end
19
+ end
20
+
11
21
  # This is a class.
12
22
  class Interface
13
23
  include Contrast::Components::ComponentBase
14
24
  include Contrast::Components::Interface
15
25
  access_component :config
16
26
 
17
- attr_reader :assess_rules,
18
- :protect_rules
19
-
20
- # Other stateful information that doesn't yet cleanly fit anywhere:
21
-
22
27
  # tainted_columns are database columns that receive unsanitized input.
23
- # this statefulness
24
28
  attr_reader :tainted_columns # This can probably go into assess_state?
25
-
26
- # These three 'state' variables represent atomic config/setting state,
27
- # outside of things like rule defs.
28
-
29
- def assess_state
30
- @assess_state ||= { # rubocop:disable Naming/MemoizedInstanceVariableName
31
- enabled: false, # Boolean
32
- sampling_features: nil # Contrast::Api::Settings::Sampling
33
- }
34
- end
35
-
36
- def protect_state
37
- @protect_state ||= { # rubocop:disable Naming/MemoizedInstanceVariableName
38
- enabled: false
39
- }
40
- end
41
-
42
- def application_state
43
- @application_state ||= { # rubocop:disable Naming/MemoizedInstanceVariableName
44
- modes_by_id: Hash.new(Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION),
45
- exclusion_matchers: [],
46
- disabled_assess_rules: []
47
- }
48
- end
49
-
50
- # These are settings that we receive & store.
51
- # Rules are settings too, but they're more involved.
52
- # So, between this block and rules, that's setting state.
53
- PROTECT_STATE_ATTRS = %i[].cs__freeze
54
- ASSESS_STATE_ATTRS = %i[sampling_features].cs__freeze
55
- APPLICATION_STATE_ATTRS = %i[modes_by_id exclusion_matchers disabled_assess_rules].cs__freeze
56
-
57
- # Meta-define an accessor for each state attribute.
58
-
59
- PROTECT_STATE_ATTRS.each do |attr|
60
- # TODO: RUBY-1052
61
- define_method(attr) do # rubocop:disable Performance/Kernel/DefineMethod
62
- protect_state[attr]
63
- end
64
- end
65
-
66
- ASSESS_STATE_ATTRS.each do |attr|
67
- # TODO: RUBY-1052
68
- define_method(attr) do # rubocop:disable Performance/Kernel/DefineMethod
69
- assess_state[attr]
70
- end
71
- end
72
-
73
- APPLICATION_STATE_ATTRS.each do |attr|
74
- # TODO: RUBY-1052
75
- define_method(attr) do # rubocop:disable Performance/Kernel/DefineMethod
76
- application_state[attr]
77
- end
78
- end
29
+ attr_reader :assess_state, :protect_state, :application_state
79
30
 
80
31
  def initialize
81
32
  reset_state
82
33
  end
83
34
 
84
- def protect_enabled?
85
- @_protect_enabled = !!protect_state[:enabled] if @_protect_enabled.nil?
86
- @_protect_enabled
87
- end
88
-
89
- def assess_enabled?
90
- @_assess_enabled = !!assess_state[:enabled] if @_assess_enabled.nil?
91
- @_assess_enabled
92
- end
93
-
94
35
  def code_exclusions
95
- exclusion_matchers.select(&:code?)
36
+ @application_state.exclusion_matchers.select(&:code?)
96
37
  end
97
38
 
98
39
  # @param server_features [Contrast::Api::Settings::ServerFeatures]
99
40
  def update_from_server_features server_features
100
- # protect
101
-
102
- @_protect_enabled = nil
103
- protect_state[:enabled] = server_features.protect_enabled?
104
-
105
- # assess
106
-
107
- @_assess_enabled = nil
108
- assess_state[:enabled] = server_features.assess_enabled?
109
- assess_state[:sampling_settings] = server_features.assess.sampling
110
- Contrast::Utils::Assess::SamplingUtil.instance.update
41
+ @protect_state.enabled = server_features.protect_enabled?
42
+ @assess_state.enabled = server_features.assess_enabled?
43
+ @assess_state.sampling_settings = server_features.assess.sampling
111
44
  end
112
45
 
113
46
  # @param application_settings [Contrast::Api::Settings::ApplicationSettings]
114
47
  def update_from_application_settings application_settings
115
- application_state.merge!(application_settings.application_state_translation)
48
+ new_vals = application_settings.application_state_translation
49
+ @application_state.modes_by_id = new_vals[:modes_by_id]
50
+ @application_state.exclusion_matchers = new_vals[:exclusion_matchers]
51
+ @assess_state.disabled_assess_rules = new_vals[:disabled_assess_rules]
116
52
  end
117
53
 
118
54
  # Wipe state to zero.
119
55
  def reset_state
120
- @assess_rules = {}
121
- @protect_rules = {}
122
-
56
+ @protect_state = PROTECT_STATE_BASE.dup
57
+ @assess_state = ASSESS_STATE_BASE.dup
58
+ @application_state = APPLICATION_STATE_BASE.dup
123
59
  @tainted_columns = {}
124
-
125
- @assess_state = nil
126
- @protect_state = nil
127
- @application_state = nil
128
- end
129
-
130
- def build_assess_rules
131
- # TODO: RUBY-1120 actually build assess_rules.
132
- @assess_rules = {}
133
60
  end
134
61
 
135
62
  def build_protect_rules
136
- @protect_rules = {}
63
+ @protect_state.rules = {}
137
64
 
138
- # rules
65
+ # Rules. They add themselves on initialize.
139
66
  Contrast::Agent::Protect::Rule::CmdInjection.new
140
67
  Contrast::Agent::Protect::Rule::Deserialization.new
141
68
  Contrast::Agent::Protect::Rule::HttpMethodTampering.new
@@ -107,11 +107,8 @@ module Contrast
107
107
  #
108
108
  # @param request [Contrast::Agent::Request] the current request.
109
109
  # @return [Contrast::Api::Dtm::RouteCoverage] the current route as a Dtm.
110
- # TODO: RUBY-1075 add unit test.
111
110
  def get_route_dtm request
112
- result = nil
113
- @_frameworks.find { |framework_klass| result = framework_klass.current_route(request) }
114
- result
111
+ @_frameworks.lazy.map { |framework_support| framework_support.current_route(request) }.reject(&:nil?).first
115
112
  end
116
113
 
117
114
  private
data/ruby-agent.gemspec CHANGED
@@ -67,6 +67,8 @@ def self.add_specs spec
67
67
  spec.add_development_dependency 'rspec', '~> 3.0'
68
68
  spec.add_development_dependency 'rspec-benchmark'
69
69
  spec.add_development_dependency 'rspec_junit_formatter', '0.3.0'
70
+ spec.add_development_dependency 'rspec-rails', '5.0'
71
+ spec.add_development_dependency 'tzinfo-data' # Alpine rspec-rails requirement.
70
72
  end
71
73
 
72
74
  def self.add_coverage spec
@@ -85,6 +87,7 @@ end
85
87
 
86
88
  # Dependencies not mocked out during RSpec that we test real code of, beyond just frameworks.
87
89
  def self.add_tested_gems spec
90
+ spec.add_development_dependency 'async'
88
91
  spec.add_development_dependency 'execjs'
89
92
  spec.add_development_dependency 'sqlite3'
90
93
  spec.add_development_dependency 'therubyracer'
@@ -1 +1 @@
1
- 2.18.0
1
+ 2.19.0
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.5.0
4
+ version: 4.6.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-03-25 00:00:00.000000000 Z
16
+ date: 2021-04-22 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: bundler
@@ -253,6 +253,20 @@ dependencies:
253
253
  - - ">="
254
254
  - !ruby/object:Gem::Version
255
255
  version: '2'
256
+ - !ruby/object:Gem::Dependency
257
+ name: async
258
+ requirement: !ruby/object:Gem::Requirement
259
+ requirements:
260
+ - - ">="
261
+ - !ruby/object:Gem::Version
262
+ version: '0'
263
+ type: :development
264
+ prerelease: false
265
+ version_requirements: !ruby/object:Gem::Requirement
266
+ requirements:
267
+ - - ">="
268
+ - !ruby/object:Gem::Version
269
+ version: '0'
256
270
  - !ruby/object:Gem::Dependency
257
271
  name: execjs
258
272
  requirement: !ruby/object:Gem::Requirement
@@ -435,6 +449,34 @@ dependencies:
435
449
  - - '='
436
450
  - !ruby/object:Gem::Version
437
451
  version: 0.3.0
452
+ - !ruby/object:Gem::Dependency
453
+ name: rspec-rails
454
+ requirement: !ruby/object:Gem::Requirement
455
+ requirements:
456
+ - - '='
457
+ - !ruby/object:Gem::Version
458
+ version: '5.0'
459
+ type: :development
460
+ prerelease: false
461
+ version_requirements: !ruby/object:Gem::Requirement
462
+ requirements:
463
+ - - '='
464
+ - !ruby/object:Gem::Version
465
+ version: '5.0'
466
+ - !ruby/object:Gem::Dependency
467
+ name: tzinfo-data
468
+ requirement: !ruby/object:Gem::Requirement
469
+ requirements:
470
+ - - ">="
471
+ - !ruby/object:Gem::Version
472
+ version: '0'
473
+ type: :development
474
+ prerelease: false
475
+ version_requirements: !ruby/object:Gem::Requirement
476
+ requirements:
477
+ - - ">="
478
+ - !ruby/object:Gem::Version
479
+ version: '0'
438
480
  - !ruby/object:Gem::Dependency
439
481
  name: ougai
440
482
  requirement: !ruby/object:Gem::Requirement
@@ -499,20 +541,20 @@ executables:
499
541
  - contrast_service
500
542
  extensions:
501
543
  - ext/cs__common/extconf.rb
502
- - ext/cs__contrast_patch/extconf.rb
544
+ - ext/cs__assess_array/extconf.rb
503
545
  - ext/cs__assess_string_interpolation26/extconf.rb
546
+ - ext/cs__assess_marshal_module/extconf.rb
504
547
  - ext/cs__assess_hash/extconf.rb
548
+ - ext/cs__assess_yield_track/extconf.rb
549
+ - ext/cs__assess_string/extconf.rb
505
550
  - ext/cs__protect_kernel/extconf.rb
551
+ - ext/cs__assess_basic_object/extconf.rb
552
+ - ext/cs__contrast_patch/extconf.rb
553
+ - ext/cs__assess_regexp/extconf.rb
506
554
  - ext/cs__assess_fiber_track/extconf.rb
507
- - ext/cs__assess_array/extconf.rb
508
- - ext/cs__assess_active_record_named/extconf.rb
509
- - ext/cs__assess_string/extconf.rb
510
555
  - ext/cs__assess_kernel/extconf.rb
556
+ - ext/cs__assess_active_record_named/extconf.rb
511
557
  - ext/cs__assess_module/extconf.rb
512
- - ext/cs__assess_basic_object/extconf.rb
513
- - ext/cs__assess_marshal_module/extconf.rb
514
- - ext/cs__assess_yield_track/extconf.rb
515
- - ext/cs__assess_regexp/extconf.rb
516
558
  extra_rdoc_files: []
517
559
  files:
518
560
  - ".clang-format"
@@ -868,7 +910,6 @@ files:
868
910
  - lib/contrast/api/decorators/user_input.rb
869
911
  - lib/contrast/api/dtm.pb.rb
870
912
  - lib/contrast/api/settings.pb.rb
871
- - lib/contrast/common_agent_configuration.rb
872
913
  - lib/contrast/components/agent.rb
873
914
  - lib/contrast/components/app_context.rb
874
915
  - lib/contrast/components/assess.rb
@@ -1007,7 +1048,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1007
1048
  - !ruby/object:Gem::Version
1008
1049
  version: '0'
1009
1050
  requirements: []
1010
- rubygems_version: 3.1.4
1051
+ rubygems_version: 3.1.6
1011
1052
  signing_key:
1012
1053
  specification_version: 4
1013
1054
  summary: Contrast Security's agent for rack-based applications.
@@ -1,87 +0,0 @@
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
- # rubocop:disable Style/MissingRespondToMissing
5
- module Contrast
6
- # A wrapper build around the Common Agent Configuration project to allow for
7
- # access of the values contained in its parent_configuration_spec.yaml
8
- class CommonAgentConfiguration
9
- # The CAC spec, deserialized to a hash.
10
-
11
- SPEC = 'spec'
12
- NODES = 'nodes'
13
- PROPERTIES = 'properties'
14
-
15
- def initialize hsh
16
- @hsh = hsh[SPEC]
17
- end
18
-
19
- # Used to indicate those sections of the configuration which have
20
- # references to other nodes or properties, allowing for the parsing of and
21
- # access to the nested configuration structure.
22
- module IsANode
23
- def children
24
- hsh[NODES]&.map { |raw_node| Node.new(raw_node) } || []
25
- end
26
-
27
- def properties
28
- hsh[PROPERTIES].map { |raw_property| Property.new(raw_property) }
29
- end
30
-
31
- def lookup *path
32
- # Path will be N args, representing a path of nodes.
33
- path.reduce(self) do |node, next_arg|
34
- # If we can travel to a node by that name, do that.
35
- candidate_node = node.children.find { |n| n.name == next_arg }
36
- next candidate_node if candidate_node
37
-
38
- # If there's a property, dereference that.
39
- candidate_property = node.properties.find { |n| n.name == next_arg }
40
- next candidate_property if candidate_property
41
-
42
- raise IndexError, "couldn't traverse path:\t#{ path.join(Contrast::Utils::ObjectShare::PERIOD) }"
43
- end
44
- end
45
-
46
- def method_missing method, *args, &block
47
- if args.any?
48
- lookup(method.to_s).public_send(args, block)
49
- else
50
- lookup(method.to_s)
51
- end
52
- rescue IndexError
53
- super(method, args, block)
54
- end
55
- end
56
-
57
- # Used to indicate those sections of the configuration which are for a
58
- # single property, allowing for the parsing of and access to the
59
- # information describing the property.
60
- module IsAProperty
61
- attr_reader :hsh
62
-
63
- def initialize hsh
64
- @hsh = hsh
65
- end
66
-
67
- %w[name default description required_languages display].each do |field|
68
- # TODO: RUBY-1052
69
- define_method(field) { hsh[field].dup } # rubocop:disable Performance/Kernel/DefineMethod
70
- end
71
- end
72
-
73
- include IsANode
74
- include IsAProperty
75
-
76
- # A Property in the Common Agent Configuration
77
- class Property
78
- include IsAProperty
79
- end
80
-
81
- # A Node in the Common Agent Configuration
82
- class Node < Property
83
- include IsANode
84
- end
85
- end
86
- end
87
- # rubocop:enable Style/MissingRespondToMissing