contrast-agent 3.15.0 → 3.16.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent.rb +2 -9
  3. data/lib/contrast/agent/assess/contrast_event.rb +142 -70
  4. data/lib/contrast/agent/assess/events/source_event.rb +1 -1
  5. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +10 -3
  6. data/lib/contrast/agent/assess/policy/policy_node.rb +15 -10
  7. data/lib/contrast/agent/assess/policy/policy_scanner.rb +7 -1
  8. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -1
  9. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +0 -3
  10. data/lib/contrast/agent/assess/policy/propagator/select.rb +1 -3
  11. data/lib/contrast/agent/assess/policy/propagator/splat.rb +0 -5
  12. data/lib/contrast/agent/assess/policy/propagator/split.rb +12 -13
  13. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +21 -14
  14. data/lib/contrast/agent/assess/policy/trigger/reflected_xss.rb +4 -5
  15. data/lib/contrast/agent/assess/policy/trigger_method.rb +39 -14
  16. data/lib/contrast/agent/assess/policy/trigger_node.rb +31 -37
  17. data/lib/contrast/agent/assess/property/evented.rb +5 -18
  18. data/lib/contrast/agent/assess/property/tagged.rb +9 -3
  19. data/lib/contrast/agent/assess/property/updated.rb +0 -5
  20. data/lib/contrast/agent/assess/rule/provider/hardcoded_key.rb +58 -5
  21. data/lib/contrast/agent/assess/rule/provider/hardcoded_password.rb +23 -8
  22. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +82 -14
  23. data/lib/contrast/agent/assess/tag.rb +1 -1
  24. data/lib/contrast/agent/at_exit_hook.rb +5 -5
  25. data/lib/contrast/agent/patching/policy/after_load_patch.rb +5 -5
  26. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +20 -20
  27. data/lib/contrast/agent/patching/policy/module_policy.rb +10 -10
  28. data/lib/contrast/agent/patching/policy/policy.rb +16 -2
  29. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +3 -5
  30. data/lib/contrast/agent/protect/policy/applies_xxe_rule.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/no_sqli/mongo_no_sql_scanner.rb +1 -0
  32. data/lib/contrast/agent/request.rb +34 -34
  33. data/lib/contrast/agent/static_analysis.rb +6 -6
  34. data/lib/contrast/agent/version.rb +1 -1
  35. data/lib/contrast/api/communication/socket_client.rb +36 -1
  36. data/lib/contrast/api/decorators/address.rb +13 -13
  37. data/lib/contrast/api/decorators/message.rb +1 -0
  38. data/lib/contrast/api/decorators/trace_event.rb +20 -18
  39. data/lib/contrast/components/app_context.rb +39 -30
  40. data/lib/contrast/components/contrast_service.rb +9 -9
  41. data/lib/contrast/components/settings.rb +20 -23
  42. data/lib/contrast/config/service_configuration.rb +4 -2
  43. data/lib/contrast/configuration.rb +1 -1
  44. data/lib/contrast/extension/assess/array.rb +7 -3
  45. data/lib/contrast/extension/assess/erb.rb +5 -0
  46. data/lib/contrast/extension/assess/eval_trigger.rb +6 -6
  47. data/lib/contrast/extension/assess/exec_trigger.rb +1 -1
  48. data/lib/contrast/extension/assess/fiber.rb +3 -3
  49. data/lib/contrast/extension/assess/hash.rb +3 -3
  50. data/lib/contrast/extension/assess/kernel.rb +18 -20
  51. data/lib/contrast/extension/assess/marshal.rb +8 -4
  52. data/lib/contrast/extension/assess/regexp.rb +3 -3
  53. data/lib/contrast/extension/assess/string.rb +13 -11
  54. data/lib/contrast/extension/protect/kernel.rb +3 -3
  55. data/lib/contrast/framework/base_support.rb +1 -1
  56. data/lib/contrast/framework/manager.rb +3 -3
  57. data/lib/contrast/framework/rack/patch/session_cookie.rb +9 -9
  58. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +13 -13
  59. data/lib/contrast/framework/rails/patch/rails_application_configuration.rb +10 -10
  60. data/lib/contrast/framework/rails/patch/support.rb +1 -1
  61. data/lib/contrast/framework/rails/rewrite/action_controller_railties_helper_inherited.rb +11 -11
  62. data/lib/contrast/framework/rails/rewrite/active_record_attribute_methods_read.rb +12 -12
  63. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +3 -3
  64. data/lib/contrast/framework/rails/rewrite/active_record_time_zone_inherited.rb +12 -12
  65. data/lib/contrast/framework/sinatra/patch/base.rb +11 -11
  66. data/lib/contrast/framework/sinatra/support.rb +4 -4
  67. data/lib/contrast/logger/log.rb +7 -2
  68. data/lib/contrast/utils/invalid_configuration_util.rb +2 -5
  69. data/resources/assess/policy.json +31 -12
  70. data/ruby-agent.gemspec +4 -3
  71. data/service_executables/VERSION +1 -1
  72. data/service_executables/linux/contrast-service +0 -0
  73. data/service_executables/mac/contrast-service +0 -0
  74. metadata +31 -17
@@ -38,15 +38,18 @@ module Contrast
38
38
  NON_PASSWORD_PARTIAL_NAMES.none? { |name| constant_string.index(name) }
39
39
  end
40
40
 
41
- # If the value is a string, it passes for this rule
42
- def value_type_passes? value
43
- value.is_a?(String)
44
- end
41
+ # Determine if the given value node violates the hardcode key rule
42
+ # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to
43
+ # evaluate
44
+ # @return [Boolean]
45
+ def value_node_passes? value_node
46
+ # If it's a freeze call, then evaluate the entity being frozen
47
+ value_node = value_node.children[0] if freeze_call?(value_node)
48
+ return false unless value_node.type == :STR
45
49
 
46
- # If the value probably isn't a property name, it passes for this
47
- # rule
48
- def value_passes? value
49
- !probably_property_name?(value)
50
+ # https://www.rubydoc.info/gems/ruby-internal/Node/STR
51
+ string = value_node.children[0]
52
+ !probably_property_name?(string)
50
53
  end
51
54
 
52
55
  # If a field name matches an expected password field, we'll check it's
@@ -65,6 +68,18 @@ module Contrast
65
68
  def redacted_marker
66
69
  REDACTED_MARKER
67
70
  end
71
+
72
+ # TODO: RUBY-1014 remove `#value_type_passes?` and `#value_passes?`
73
+ # If the value is a string, it passes for this rule
74
+ def value_type_passes? value
75
+ value.is_a?(String)
76
+ end
77
+
78
+ # If the value probably isn't a property name, it passes for this
79
+ # rule
80
+ def value_passes? value
81
+ !probably_property_name?(value)
82
+ end
68
83
  end
69
84
  end
70
85
  end
@@ -1,6 +1,7 @@
1
1
  # Copyright (c) 2020 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'contrast/agent/assess/policy/trigger_method'
4
5
  require 'contrast/components/interface'
5
6
  require 'contrast/extension/module'
6
7
 
@@ -9,14 +10,13 @@ module Contrast
9
10
  module Assess
10
11
  module Rule
11
12
  module Provider
12
- # Hardcoded rules detect if any secret value has been written directly into the
13
- # sourcecode of the application. To use this base class, a provider must
14
- # implement four methods
13
+ # Hardcoded rules detect if any secret value has been written
14
+ # directly into the sourcecode of the application. To use this base
15
+ # class, a provider must implement three methods:
15
16
  # 1) name_passes? : does the constant name match a given value set
16
- # 2) value_type_passes? : does the type of the value of the constant match the
17
- # type given
18
- # 3) value_passes? : does the value of the constant match a given value set
19
- # 4) redacted_marker : the value to plug in for the obfuscated value
17
+ # 2) value_node_passes? : does the value of the constant match a
18
+ # given value set
19
+ # 3) redacted_marker : the value to plug in for the obfuscated value
20
20
  module HardcodedValueRule
21
21
  include Contrast::Components::Interface
22
22
  access_component :analysis, :app_context, :logging
@@ -25,6 +25,7 @@ module Contrast
25
25
  !ASSESS.enabled? || ASSESS.rule_disabled?(rule_id)
26
26
  end
27
27
 
28
+ # TODO: RUBY-1014 - remove `#analyze`
28
29
  COMMON_CONSTANTS = %i[
29
30
  CONTRAST_ASSESS_POLICY_STATUS
30
31
  VERSION
@@ -69,10 +70,30 @@ module Contrast
69
70
  # if it looks like a placeholder / pointer to a config, skip it
70
71
  next unless value_passes?(value)
71
72
 
72
- report_finding(clazz, constant_string)
73
+ build_finding(clazz, constant_string)
73
74
  end
74
75
  end
75
76
 
77
+ # Parse the file pertaining to the given TracePoint to walk its AST
78
+ # to determine if a Constant is hardcoded. For our purposes, this
79
+ # hard coding means directly set rather than as an interpolated
80
+ # String or through a method call.
81
+ #
82
+ # Note: This is a top layer check, we make no assertions about what
83
+ # the methods or interpolations do. Their presence, even if only
84
+ # calling a hardcoded thing, causes this check to not report.
85
+ #
86
+ # @param trace_point [TracePoint] the TracePoint event created on
87
+ # the :end of a Module being loaded
88
+ def parse trace_point
89
+ return if disabled?
90
+
91
+ ast = RubyVM::AbstractSyntaxTree.parse_file(trace_point.path)
92
+ parse_ast(trace_point.self, ast)
93
+ rescue StandardError => e
94
+ logger.error('Unable to parse AST for hardcoded keys', e, module: trace_point.self)
95
+ end
96
+
76
97
  # Constants can be variable or classes defined in the given
77
98
  # class. We ONLY want the variables, which should be defined in
78
99
  # the MACRO_CASE (upper case & underscore format)
@@ -90,7 +111,57 @@ module Contrast
90
111
 
91
112
  private
92
113
 
93
- def report_finding clazz, constant_string
114
+ # @param mod [Module] the module to which this AST pertains
115
+ # @param ast [RubyVM::AbstractSyntaxTree::Node, Object] a node
116
+ # within the AST, which may be a leaf, so any Object
117
+ def parse_ast mod, ast
118
+ return unless ast.cs__is_a?(RubyVM::AbstractSyntaxTree::Node)
119
+ return unless ast.cs__respond_to?(:children)
120
+
121
+ children = ast.children
122
+ return unless children.any?
123
+
124
+ ast.children.each do |child|
125
+ parse_ast(mod, child)
126
+ end
127
+
128
+ # https://www.rubydoc.info/gems/ruby-internal/Node/CDECL
129
+ return unless ast.type == :CDECL
130
+
131
+ # The CDECL Node has two children, the first being the Constant
132
+ # name as a symbol, the second as the value to assign to that
133
+ # constant
134
+ children = ast.children
135
+ name = children[0].to_s
136
+ # If that constant name doesn't pass our checks, move on.
137
+ return unless name_passes?(name)
138
+
139
+ value = children[1]
140
+ # The assignment node could be a direct value or a call of some
141
+ # sort. We leave it to each rule to properly handle these nodes.
142
+ return unless value_node_passes?(value)
143
+
144
+ build_finding(mod, name)
145
+ end
146
+
147
+ # Constants can be set as frozen directly. We need to account for
148
+ # this change as it means the Node given to the :CDECL call will be
149
+ # a :CALL, not a constant.
150
+ #
151
+ # @param value_node [RubyVM::AbstractSyntaxTree::Node] the node to
152
+ # evaluate
153
+ # @return [Boolean] is this a freeze call or not
154
+ def freeze_call? value_node
155
+ return false unless value_node.type == :CALL
156
+
157
+ children = value_node.children
158
+ return false unless children
159
+ return false unless children.length >= 2
160
+
161
+ children[1] == :freeze
162
+ end
163
+
164
+ def build_finding clazz, constant_string
94
165
  class_name = clazz.cs__name
95
166
 
96
167
  finding = Contrast::Api::Dtm::Finding.new
@@ -104,13 +175,10 @@ module Contrast
104
175
  hash = Contrast::Utils::HashDigest.generate_class_scanning_hash(finding)
105
176
  finding.hash_code = Contrast::Utils::StringUtils.protobuf_safe_string(hash)
106
177
  finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
107
-
108
- activity = Contrast::Api::Dtm::Activity.new
109
- activity.findings << finding
110
-
111
- Contrast::Agent.messaging_queue.send_event_eventually(activity)
178
+ Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding)
112
179
  rescue StandardError => e
113
180
  logger.error('Unable to build a finding for Hardcoded Rule', e)
181
+ nil
114
182
  end
115
183
  end
116
184
  end
@@ -14,7 +14,7 @@ module Contrast
14
14
 
15
15
  # Initialize a new tag
16
16
  #
17
- # @param label [String] the lable of the tag
17
+ # @param label [String] the label of the tag
18
18
  # @param length [Integer] the length of the string described with this
19
19
  # tag
20
20
  # @param start_idx [Integer] (0) the starting position in the string for
@@ -11,11 +11,11 @@ module Contrast
11
11
  access_component :logging
12
12
  def self.exit_hook
13
13
  @_exit_hook ||= begin
14
- at_exit do
15
- on_exit
16
- end
17
- true
18
- end
14
+ at_exit do
15
+ on_exit
16
+ end
17
+ true
18
+ end
19
19
  end
20
20
 
21
21
  # Actions to take when a process exits. Typically called from our
@@ -15,7 +15,7 @@ module Contrast
15
15
  access_component :scope
16
16
  attr_reader :applied, :module_name, :instrumentation_file_path, :method_to_instrument, :instrumenting_module
17
17
 
18
- def initialize module_name, instrumentation_file_path, method_to_instrument: nil, instrumenting_module:
18
+ def initialize module_name, instrumentation_file_path, method_to_instrument: nil, instrumenting_module: nil
19
19
  @applied = false
20
20
  @module_name = module_name
21
21
  @method_to_instrument = method_to_instrument
@@ -71,10 +71,10 @@ module Contrast
71
71
 
72
72
  def module_lookup
73
73
  @_module_lookup ||= begin
74
- Module.cs__const_get module_name
75
- rescue StandardError => _e
76
- nil
77
- end
74
+ Module.cs__const_get module_name
75
+ rescue StandardError => _e
76
+ nil
77
+ end
78
78
  end
79
79
  end
80
80
  end
@@ -30,20 +30,20 @@ module Contrast
30
30
  # extensions.
31
31
  def apply_direct_patches!
32
32
  @_apply_direct_patches ||= begin
33
- Contrast::Extension::Assess::ArrayPropagator.instrument_array_track
34
- Contrast::Extension::Assess::EvalTrigger.instrument_basic_object_track
35
- Contrast::Extension::Assess::EvalTrigger.instrument_module_track
36
- Contrast::Extension::Assess::FiberPropagator.instrument_fiber_track
37
- Contrast::Extension::Assess::HashPropagator.instrument_hash_track
38
- Contrast::Extension::Assess::KernelPropagator.instrument_kernel_track
39
- Contrast::Extension::Assess::MarshalPropagator.instrument_marshal_load
40
- Contrast::Extension::Assess::RegexpPropagator.instrument_regexp_track
41
- Contrast::Extension::Assess::StringPropagator.instrument_string
42
- Contrast::Extension::Assess::StringPropagator.instrument_string_interpolation
33
+ Contrast::Extension::Assess::ArrayPropagator.instrument_array_track
34
+ Contrast::Extension::Assess::EvalTrigger.instrument_basic_object_track
35
+ Contrast::Extension::Assess::EvalTrigger.instrument_module_track
36
+ Contrast::Extension::Assess::FiberPropagator.instrument_fiber_track
37
+ Contrast::Extension::Assess::HashPropagator.instrument_hash_track
38
+ Contrast::Extension::Assess::KernelPropagator.instrument_kernel_track
39
+ Contrast::Extension::Assess::MarshalPropagator.instrument_marshal_load
40
+ Contrast::Extension::Assess::RegexpPropagator.instrument_regexp_track
41
+ Contrast::Extension::Assess::StringPropagator.instrument_string
42
+ Contrast::Extension::Assess::StringPropagator.instrument_string_interpolation
43
43
 
44
- Contrast::Extension::Protect::Kernel.instrument
45
- true
46
- end
44
+ Contrast::Extension::Protect::Kernel.instrument
45
+ true
46
+ end
47
47
  end
48
48
 
49
49
  def apply_load_patches!
@@ -65,13 +65,13 @@ module Contrast
65
65
  # handling
66
66
  def apply_require_patches!
67
67
  @_apply_require_patches ||= begin
68
- require 'contrast/extension/thread'
69
- require 'contrast/extension/kernel'
70
- true
71
- rescue LoadError, StandardError => e
72
- logger.error('failed instrumenting apply_require_patches!', e)
73
- false
74
- end
68
+ require 'contrast/extension/thread'
69
+ require 'contrast/extension/kernel'
70
+ true
71
+ rescue LoadError, StandardError => e
72
+ logger.error('failed instrumenting apply_require_patches!', e)
73
+ false
74
+ end
75
75
  end
76
76
 
77
77
  def after_load_patches
@@ -61,16 +61,16 @@ module Contrast
61
61
  # @return [Integer] count of methods to be patched
62
62
  def num_expected_patches
63
63
  @_num_expected_patches ||= begin
64
- instance_methods = Set.new
65
- singleton_methods = Set.new
66
- sort_method_names(source_nodes, instance_methods, singleton_methods)
67
- sort_method_names(propagator_nodes, instance_methods, singleton_methods)
68
- sort_method_names(trigger_nodes, instance_methods, singleton_methods)
69
- sort_method_names(inventory_nodes, instance_methods, singleton_methods)
70
- sort_method_names(protect_nodes, instance_methods, singleton_methods)
71
- sort_method_names(deadzone_nodes, instance_methods, singleton_methods)
72
- instance_methods.length + singleton_methods.length
73
- end
64
+ instance_methods = Set.new
65
+ singleton_methods = Set.new
66
+ sort_method_names(source_nodes, instance_methods, singleton_methods)
67
+ sort_method_names(propagator_nodes, instance_methods, singleton_methods)
68
+ sort_method_names(trigger_nodes, instance_methods, singleton_methods)
69
+ sort_method_names(inventory_nodes, instance_methods, singleton_methods)
70
+ sort_method_names(protect_nodes, instance_methods, singleton_methods)
71
+ sort_method_names(deadzone_nodes, instance_methods, singleton_methods)
72
+ instance_methods.length + singleton_methods.length
73
+ end
74
74
  end
75
75
 
76
76
  private
@@ -124,12 +124,26 @@ module Contrast
124
124
  end
125
125
 
126
126
  def find_source_node class_name, method_name, instance_method
127
- sources.find { |source| source.class_name == class_name && source.method_name == method_name && source.instance_method == instance_method }
127
+ sources.find do |source|
128
+ source.class_name == class_name &&
129
+ source.method_name == method_name &&
130
+ source.instance_method == instance_method
131
+ end
132
+ end
133
+
134
+ def find_propagator_node class_name, method_name, instance_method
135
+ propagators.find do |propagator|
136
+ propagator.class_name == class_name &&
137
+ propagator.method_name == method_name &&
138
+ propagator.instance_method == instance_method
139
+ end
128
140
  end
129
141
 
130
142
  def find_node rule_id, class_name, method_name, instance_method
131
143
  find_triggers_by_rule(rule_id).find do |node|
132
- node.class_name == class_name && node.method_name == method_name && node.instance_method == instance_method
144
+ node.class_name == class_name &&
145
+ node.method_name == method_name &&
146
+ node.instance_method == instance_method
133
147
  end
134
148
  end
135
149
  end
@@ -30,11 +30,9 @@ module Contrast
30
30
  Contrast::Agent::Protect::Policy::AppliesDeserializationRule.apply_deserialization_command_check(command)
31
31
  return if skip_analysis?
32
32
 
33
- begin
34
- clazz = object.is_a?(Module) ? object : object.cs__class
35
- class_name = clazz.cs__name
36
- rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
37
- end
33
+ clazz = object.is_a?(Module) ? object : object.cs__class
34
+ class_name = clazz.cs__name
35
+ rule.infilter(Contrast::Agent::REQUEST_TRACKER.current, class_name, method, command)
38
36
  end
39
37
 
40
38
  protected
@@ -27,7 +27,7 @@ module Contrast
27
27
  def apply_rule__io method, _exception, _properties, object, args
28
28
  need_rewind = false
29
29
  potential_xml = args[0]
30
- return unless potential_xml&.respond_to?(:rewind)
30
+ return unless potential_xml.cs__respond_to?(:rewind)
31
31
 
32
32
  xml = potential_xml.read
33
33
  need_rewind = true
@@ -16,6 +16,7 @@ module Contrast
16
16
  def start_line_comment? char, index, query
17
17
  if char == Contrast::Utils::ObjectShare::SLASH &&
18
18
  query[index + 1] == Contrast::Utils::ObjectShare::SLASH
19
+
19
20
  return true
20
21
  end
21
22
 
@@ -46,42 +46,42 @@ module Contrast
46
46
  # Should also handle the ;jsessionid.
47
47
  def normalized_uri
48
48
  @_normalized_uri ||= begin
49
- path = rack_request.path
50
- uri = path.split(Contrast::Utils::ObjectShare::SEMICOLON)[0] # remove ;jsessionid
51
- uri = uri.split(Contrast::Utils::ObjectShare::QUESTION_MARK)[0] # remove ?query_string=
52
- uri.gsub(INNER_REST_TOKEN, INNER_NUMBER_MARKER) # replace interior tokens
53
- uri.gsub(LAST_REST_TOKEN, LAST_NUMBER_MARKER) # replace last token
54
- end
49
+ path = rack_request.path
50
+ uri = path.split(Contrast::Utils::ObjectShare::SEMICOLON)[0] # remove ;jsessionid
51
+ uri = uri.split(Contrast::Utils::ObjectShare::QUESTION_MARK)[0] # remove ?query_string=
52
+ uri.gsub(INNER_REST_TOKEN, INNER_NUMBER_MARKER) # replace interior tokens
53
+ uri.gsub(LAST_REST_TOKEN, LAST_NUMBER_MARKER) # replace last token
54
+ end
55
55
  end
56
56
 
57
57
  def document_type
58
58
  @_document_type ||= begin
59
- if /xml/i.match?(content_type) || body&.start_with?('<?xml')
60
- :XML
61
- elsif /json/i.match?(content_type) || body&.match?(/\s*[{\[]/)
62
- :JSON
63
- else
64
- :NORMAL
65
- end
66
- end
59
+ if /xml/i.match?(content_type) || body&.start_with?('<?xml')
60
+ :XML
61
+ elsif /json/i.match?(content_type) || body&.match?(/\s*[{\[]/)
62
+ :JSON
63
+ else
64
+ :NORMAL
65
+ end
66
+ end
67
67
  end
68
68
 
69
69
  # Header keys upcased and any underscores replaced with dashes
70
70
  def headers
71
71
  @_headers ||= begin
72
- with_contrast_scope do
73
- hash = {}
74
- env.each do |key, value|
75
- next unless key
76
-
77
- name = key.to_s
78
- next unless name.start_with?(Contrast::Utils::ObjectShare::HTTP_SCORE)
79
-
80
- hash[Contrast::Utils::StringUtils.normalized_key(name)] = value
81
- end
82
- hash
83
- end
84
- end
72
+ with_contrast_scope do
73
+ hash = {}
74
+ env.each do |key, value|
75
+ next unless key
76
+
77
+ name = key.to_s
78
+ next unless name.start_with?(Contrast::Utils::ObjectShare::HTTP_SCORE)
79
+
80
+ hash[Contrast::Utils::StringUtils.normalized_key(name)] = value
81
+ end
82
+ hash
83
+ end
84
+ end
85
85
  end
86
86
 
87
87
  def body
@@ -121,13 +121,13 @@ module Contrast
121
121
 
122
122
  def file_names
123
123
  @_file_names ||= begin
124
- names = {}
125
- parsed_data = Rack::Multipart.parse_multipart(rack_request.env)
126
- traverse_parsed_multipart(parsed_data, names)
127
- rescue StandardError => _e
128
- logger.warn('Unable to parse multipart request!')
129
- {}
130
- end
124
+ names = {}
125
+ parsed_data = Rack::Multipart.parse_multipart(rack_request.env)
126
+ traverse_parsed_multipart(parsed_data, names)
127
+ rescue StandardError => _e
128
+ logger.warn('Unable to parse multipart request!')
129
+ {}
130
+ end
131
131
  end
132
132
 
133
133
  def hash_id