contrast-agent 4.5.0 → 4.6.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: 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