contrast-agent 3.12.2 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.dockerignore +0 -1
  3. data/.gitignore +1 -1
  4. data/.simplecov +1 -1
  5. data/Rakefile +31 -0
  6. data/ext/build_funchook.rb +0 -2
  7. data/ext/cs__assess_fiber_track/cs__assess_fiber_track.c +2 -8
  8. data/ext/cs__assess_fiber_track/cs__assess_fiber_track.h +0 -1
  9. data/ext/cs__assess_string_interpolation26/cs__assess_string_interpolation26.c +1 -6
  10. data/ext/cs__assess_yield_track/cs__assess_yield_track.c +1 -5
  11. data/ext/cs__assess_yield_track/cs__assess_yield_track.h +0 -1
  12. data/ext/cs__common/cs__common.c +24 -0
  13. data/ext/cs__common/cs__common.h +3 -0
  14. data/ext/cs__common/extconf.rb +0 -14
  15. data/ext/extconf_common.rb +0 -28
  16. data/lib/contrast.rb +3 -1
  17. data/lib/contrast/agent.rb +14 -2
  18. data/lib/contrast/agent/assess/contrast_event.rb +28 -167
  19. data/lib/contrast/agent/assess/events/source_event.rb +3 -7
  20. data/lib/contrast/agent/assess/policy/dynamic_source_factory.rb +1 -1
  21. data/lib/contrast/agent/assess/policy/policy_node.rb +4 -98
  22. data/lib/contrast/agent/assess/policy/propagation_method.rb +1 -2
  23. data/lib/contrast/agent/assess/policy/propagation_node.rb +5 -1
  24. data/lib/contrast/agent/assess/policy/propagator/base.rb +1 -1
  25. data/lib/contrast/agent/assess/policy/propagator/insert.rb +1 -4
  26. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +9 -1
  27. data/lib/contrast/agent/assess/policy/propagator/remove.rb +6 -11
  28. data/lib/contrast/agent/assess/policy/propagator/select.rb +4 -4
  29. data/lib/contrast/agent/assess/policy/propagator/split.rb +2 -2
  30. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +4 -4
  31. data/lib/contrast/agent/assess/policy/propagator/trim.rb +6 -10
  32. data/lib/contrast/agent/assess/policy/source_method.rb +1 -2
  33. data/lib/contrast/agent/assess/policy/trigger_method.rb +1 -9
  34. data/lib/contrast/agent/assess/policy/trigger_node.rb +16 -4
  35. data/lib/contrast/agent/assess/properties.rb +4 -382
  36. data/lib/contrast/agent/assess/property/evented.rb +78 -0
  37. data/lib/contrast/agent/assess/property/tagged.rb +339 -0
  38. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +2 -20
  39. data/lib/contrast/agent/assess/tag.rb +27 -12
  40. data/lib/contrast/agent/at_exit_hook.rb +3 -1
  41. data/lib/contrast/agent/exclusion_matcher.rb +2 -2
  42. data/lib/contrast/agent/inventory/policy/datastores.rb +0 -1
  43. data/lib/contrast/agent/middleware.rb +2 -14
  44. data/lib/contrast/agent/patching/policy/patch.rb +1 -1
  45. data/lib/contrast/agent/patching/policy/policy.rb +3 -3
  46. data/lib/contrast/agent/patching/policy/policy_node.rb +2 -2
  47. data/lib/contrast/agent/protect/policy/rule_applicator.rb +2 -2
  48. data/lib/contrast/agent/protect/rule/base.rb +19 -31
  49. data/lib/contrast/agent/protect/rule/base_service.rb +1 -1
  50. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +2 -7
  51. data/lib/contrast/agent/protect/rule/xxe.rb +1 -0
  52. data/lib/contrast/agent/reaction_processor.rb +3 -3
  53. data/lib/contrast/agent/request.rb +92 -331
  54. data/lib/contrast/agent/request_context.rb +15 -15
  55. data/lib/contrast/agent/request_handler.rb +1 -1
  56. data/lib/contrast/agent/response.rb +2 -14
  57. data/lib/contrast/agent/scope.rb +1 -1
  58. data/lib/contrast/agent/service_heartbeat.rb +7 -9
  59. data/lib/contrast/agent/static_analysis.rb +1 -1
  60. data/lib/contrast/agent/thread_watcher.rb +49 -0
  61. data/lib/contrast/agent/version.rb +1 -1
  62. data/lib/contrast/agent/worker_thread.rb +24 -0
  63. data/lib/contrast/api.rb +3 -5
  64. data/lib/contrast/api/communication.rb +20 -0
  65. data/lib/contrast/api/communication/connection_status.rb +41 -0
  66. data/lib/contrast/api/communication/messaging_queue.rb +79 -0
  67. data/lib/contrast/{utils/service_response_util.rb → api/communication/response_processor.rb} +9 -18
  68. data/lib/contrast/api/communication/service_lifecycle.rb +61 -0
  69. data/lib/contrast/api/communication/socket.rb +45 -0
  70. data/lib/contrast/api/communication/socket_client.rb +76 -0
  71. data/lib/contrast/api/communication/speedracer.rb +111 -0
  72. data/lib/contrast/api/communication/tcp_socket.rb +31 -0
  73. data/lib/contrast/api/communication/unix_socket.rb +27 -0
  74. data/lib/contrast/api/decorators.rb +10 -0
  75. data/lib/contrast/api/decorators/address.rb +60 -0
  76. data/lib/contrast/api/decorators/application_settings.rb +7 -3
  77. data/lib/contrast/api/decorators/application_update.rb +0 -9
  78. data/lib/contrast/api/decorators/http_request.rb +139 -0
  79. data/lib/contrast/api/decorators/message.rb +75 -0
  80. data/lib/contrast/api/decorators/rasp_rule_sample.rb +28 -0
  81. data/lib/contrast/api/decorators/route_coverage.rb +57 -0
  82. data/lib/contrast/api/decorators/trace_event.rb +99 -0
  83. data/lib/contrast/api/decorators/trace_event_object.rb +57 -0
  84. data/lib/contrast/api/decorators/trace_event_signature.rb +46 -0
  85. data/lib/contrast/api/decorators/trace_taint_range.rb +51 -0
  86. data/lib/contrast/api/decorators/trace_taint_range_tags.rb +109 -0
  87. data/lib/contrast/api/decorators/user_input.rb +40 -0
  88. data/lib/contrast/components/app_context.rb +0 -7
  89. data/lib/contrast/components/config.rb +4 -9
  90. data/lib/contrast/components/interface.rb +1 -1
  91. data/lib/contrast/components/settings.rb +0 -6
  92. data/lib/contrast/extension/assess.rb +0 -1
  93. data/lib/contrast/extension/assess/assess_extension.rb +1 -2
  94. data/lib/contrast/extension/assess/fiber.rb +1 -1
  95. data/lib/contrast/extension/assess/string.rb +1 -1
  96. data/lib/contrast/extension/inventory.rb +0 -1
  97. data/lib/contrast/framework/base_support.rb +0 -23
  98. data/lib/contrast/framework/manager.rb +0 -9
  99. data/lib/contrast/framework/rails/patch/action_controller_live_buffer.rb +1 -3
  100. data/lib/contrast/framework/rails/patch/assess_configuration.rb +3 -4
  101. data/lib/contrast/framework/rails/support.rb +3 -32
  102. data/lib/contrast/framework/sinatra/patch/base.rb +1 -1
  103. data/lib/contrast/framework/sinatra/support.rb +11 -22
  104. data/lib/contrast/funchook/funchook.rb +45 -0
  105. data/lib/contrast/logger/application.rb +1 -1
  106. data/lib/contrast/logger/format.rb +51 -0
  107. data/lib/contrast/logger/log.rb +8 -2
  108. data/lib/contrast/utils/assess/tracking_util.rb +45 -20
  109. data/lib/contrast/utils/hash_digest.rb +11 -2
  110. data/lib/contrast/utils/invalid_configuration_util.rb +1 -17
  111. data/lib/contrast/utils/inventory_util.rb +2 -7
  112. data/lib/contrast/utils/object_share.rb +0 -1
  113. data/lib/contrast/utils/os.rb +16 -4
  114. data/lib/contrast/utils/stack_trace_utils.rb +0 -1
  115. data/lib/contrast/utils/tag_util.rb +1 -1
  116. data/lib/contrast/utils/thread_tracker.rb +1 -14
  117. data/lib/contrast/utils/timer.rb +1 -17
  118. data/ruby-agent.gemspec +4 -4
  119. metadata +48 -72
  120. data/funchook/Makefile +0 -29
  121. data/funchook/autom4te.cache/output.0 +0 -4964
  122. data/funchook/autom4te.cache/requests +0 -77
  123. data/funchook/autom4te.cache/traces.0 +0 -361
  124. data/funchook/config.log +0 -651
  125. data/funchook/config.status +0 -1015
  126. data/funchook/configure +0 -4964
  127. data/funchook/src/Makefile +0 -70
  128. data/funchook/src/config.h +0 -101
  129. data/funchook/src/config.h.in +0 -100
  130. data/funchook/src/decoder.o +0 -0
  131. data/funchook/src/distorm.o +0 -0
  132. data/funchook/src/funchook.o +0 -0
  133. data/funchook/src/funchook_io.o +0 -0
  134. data/funchook/src/funchook_syscall.o +0 -0
  135. data/funchook/src/funchook_unix.o +0 -0
  136. data/funchook/src/funchook_x86.o +0 -0
  137. data/funchook/src/instructions.o +0 -0
  138. data/funchook/src/insts.o +0 -0
  139. data/funchook/src/libfunchook.dylib +0 -0
  140. data/funchook/src/mnemonics.o +0 -0
  141. data/funchook/src/operands.o +0 -0
  142. data/funchook/src/os_func.o +0 -0
  143. data/funchook/src/os_func_unix.o +0 -0
  144. data/funchook/src/prefix.o +0 -0
  145. data/funchook/src/printf_base.o +0 -0
  146. data/funchook/src/textdefs.o +0 -0
  147. data/funchook/src/wstring.o +0 -0
  148. data/funchook/test/Makefile +0 -43
  149. data/funchook/test/funchook_test +0 -0
  150. data/funchook/test/libfunchook_test.so +0 -0
  151. data/funchook/test/libfunchook_test.so.dSYM/Contents/Info.plist +0 -20
  152. data/funchook/test/libfunchook_test.so.dSYM/Contents/Resources/DWARF/libfunchook_test.so +0 -0
  153. data/funchook/test/test_main.o +0 -0
  154. data/funchook/test/x86_64_test.o +0 -0
  155. data/lib/contrast/agent/assess/adjusted_span.rb +0 -27
  156. data/lib/contrast/agent/socket_client.rb +0 -134
  157. data/lib/contrast/api/connection_status.rb +0 -49
  158. data/lib/contrast/api/socket.rb +0 -43
  159. data/lib/contrast/api/speedracer.rb +0 -188
  160. data/lib/contrast/api/tcp_socket.rb +0 -29
  161. data/lib/contrast/api/unix_socket.rb +0 -25
  162. data/lib/contrast/framework/sinatra/application_helper.rb +0 -51
  163. data/lib/contrast/framework/view_technologies_descriptor.rb +0 -21
  164. data/lib/contrast/internal_exception.rb +0 -8
  165. data/lib/contrast/utils/cache.rb +0 -58
  166. data/lib/contrast/utils/service_sender_util.rb +0 -167
  167. data/lib/contrast/utils/sinatra_helper.rb +0 -49
@@ -7,28 +7,42 @@ module Contrast
7
7
  # A Tag represents a range in a given piece of data. It is used by the
8
8
  # Agent to determine if a vulnerable dataflow has occurred.
9
9
  class Tag
10
- attr_reader :length, # length of tagged text within string
10
+ attr_reader :label, # the label of this tag
11
+ :length, # length of tagged text within string
11
12
  :start_idx, # start of range
12
- :end_idx # end of range (calculated from start + length)
13
+ :end_idx # end of range (calculated from start + length), exclusive
13
14
 
14
15
  # Initialize a new tag
15
- # length : the length of the string described with this tag
16
- # start_idx : (default: 0) the starting position in the string for this tag
17
- def initialize length, start_idx = 0
16
+ #
17
+ # @param label [String] the lable of the tag
18
+ # @param length [Integer] the length of the string described with this
19
+ # tag
20
+ # @param start_idx [Integer] (0) the starting position in the string for
21
+ # this tag
22
+ def initialize label, length, start_idx = 0
23
+ @label = label
18
24
  update_range(start_idx, start_idx + length)
19
25
  end
20
26
 
21
27
  # Return true if the tag covers the given position in the string
28
+ #
29
+ # @param idx [Integer] the index to check
30
+ # @return [Boolean]
22
31
  def covers? idx
23
32
  idx >= start_idx && idx < end_idx
24
33
  end
25
34
 
26
35
  # Return true if the tag is above the given position in the string
36
+ # @param idx [Integer] the index to check
37
+ # @return [Boolean]
27
38
  def above? idx
28
39
  idx < start_idx
29
40
  end
30
41
 
31
- # Return the range that this tag covers
42
+ # Return the range that this tag covers, from start (inclusive) to
43
+ # end (exclusive).
44
+ #
45
+ # @return [Range]
32
46
  def range
33
47
  start_idx...end_idx
34
48
  end
@@ -38,10 +52,11 @@ module Contrast
38
52
  end
39
53
 
40
54
  # Return if a given tag overlaps this one
41
- def overlaps? other
42
- return true if @start_idx < other.start_idx && @end_idx >= other.start_idx # we start below other & end in it
43
- return true if @start_idx >= other.start_idx && @end_idx <= other.end_idx # we start and end in other
44
- return true if @start_idx <= other.end_idx && @end_idx > other.end_idx # we start in other & end above it
55
+ def overlaps? start_idx, end_idx
56
+ return true if @start_idx < start_idx && @end_idx >= start_idx # we start below range & end in it
57
+ return true if @start_idx >= start_idx && @end_idx <= end_idx # we start and end in range
58
+
59
+ @start_idx <= end_idx && @end_idx > end_idx # we start in range & end above it
45
60
  end
46
61
 
47
62
  def shift idx
@@ -71,7 +86,7 @@ module Contrast
71
86
  # Returns true if the other tag was merged into
72
87
  # this tag
73
88
  def merge other
74
- return unless overlaps?(other)
89
+ return unless overlaps?(other.start_idx, other.end_idx)
75
90
 
76
91
  start = other.start_idx < @start_idx ? other.start_idx : @start_idx
77
92
  finish = other.end_idx > @end_idx ? other.end_idx : @end_idx
@@ -86,7 +101,7 @@ module Contrast
86
101
  new_start_idx = start >= 0 ? start : 0
87
102
  # If a tag were to go negative, cut off the negative portion from length
88
103
  new_length = start >= 0 ? length : (length + start)
89
- Contrast::Agent::Assess::Tag.new(new_length, new_start_idx)
104
+ Contrast::Agent::Assess::Tag.new(label, new_length, new_start_idx)
90
105
  end
91
106
 
92
107
  def str_val
@@ -29,7 +29,9 @@ module Contrast
29
29
  process_pp_id: Process.ppid)
30
30
 
31
31
  context = Contrast::Agent::REQUEST_TRACKER.current
32
- Contrast::Utils::ServiceSenderUtil.send_event_immediately(context.activity) if context
32
+ return unless context
33
+
34
+ Contrast::Agent.messaging_queue.send_event_immediately(context.activity)
33
35
  end
34
36
  end
35
37
  end
@@ -92,7 +92,7 @@ module Contrast
92
92
  end
93
93
 
94
94
  def code?
95
- @exclusion.type == :CODE
95
+ @exclusion.type == Contrast::Api::Settings::Exclusion::ExclusionType::CODE
96
96
  end
97
97
 
98
98
  def name
@@ -100,7 +100,7 @@ module Contrast
100
100
  end
101
101
 
102
102
  def match_all?
103
- @exclusion.match_strategy == :ALL
103
+ @exclusion.urls.nil? || @exclusion.urls.empty?
104
104
  end
105
105
 
106
106
  # Determine if the given rule is excluded by this exclusion.
@@ -40,7 +40,6 @@ module Contrast
40
40
  context = Contrast::Agent::REQUEST_TRACKER.current
41
41
  return unless context&.activity
42
42
 
43
- context.activity.technologies[data_store] = true
44
43
  context.activity.query_count += 1
45
44
  return unless context.activity.query_count == 1
46
45
 
@@ -7,7 +7,6 @@ cs__scoped_require 'rack'
7
7
 
8
8
  cs__scoped_require 'contrast/security_exception'
9
9
  cs__scoped_require 'contrast/utils/object_share'
10
- cs__scoped_require 'contrast/agent/service_heartbeat'
11
10
  cs__scoped_require 'contrast/components/interface'
12
11
  cs__scoped_require 'contrast/utils/heap_dump_util'
13
12
  cs__scoped_require 'contrast/agent/request_handler'
@@ -15,8 +14,6 @@ cs__scoped_require 'contrast/agent/static_analysis'
15
14
 
16
15
  cs__scoped_require 'contrast/utils/timer'
17
16
  cs__scoped_require 'contrast/utils/freeze_util'
18
- cs__scoped_require 'contrast/utils/service_sender_util'
19
- cs__scoped_require 'contrast/utils/service_response_util'
20
17
 
21
18
  module Contrast
22
19
  module Agent
@@ -60,7 +57,7 @@ module Contrast
60
57
  # instrumentation going forward
61
58
  def agent_startup_routine
62
59
  logger.debug_with_time('middleware: starting service') do
63
- run_service_threads
60
+ Contrast::Agent.thread_watcher.ensure_running?
64
61
  end
65
62
 
66
63
  logger.debug_with_time('middleware: instrument shared libraries and patch') do
@@ -110,6 +107,7 @@ module Contrast
110
107
  # available globally so that it can be accessed from anywhere. A RequestHandler object is made
111
108
  # for each request, which handles prefilter and postfilter operations.
112
109
  def call_with_agent env
110
+ Contrast::Agent.thread_watcher.ensure_running?
113
111
  return unless AGENT.enabled?
114
112
 
115
113
  framework_request = Contrast::Agent.framework_manager.retrieve_request(env)
@@ -176,16 +174,6 @@ module Contrast
176
174
  raise exception
177
175
  end
178
176
  end
179
-
180
- # TODO: RUBY-920
181
- # Move this somewhere that controls our threads, ensuring they're
182
- # recreated on Fork
183
- #
184
- # Rspec stubs over these methods for simplicity's sake in testing
185
- def run_service_threads
186
- Contrast::Utils::ServiceSenderUtil.start
187
- Contrast::Agent::ServiceHeartbeat.new.start
188
- end
189
177
  end
190
178
  end
191
179
  end
@@ -353,7 +353,7 @@ module Contrast
353
353
  elsif target_module.public_instance_methods(false).include?(method_name)
354
354
  :public
355
355
  else
356
- raise NotImplementedError,
356
+ raise NoMethodError,
357
357
  <<~ERR
358
358
  Tried to register a C-defined #{ impl } patch for \
359
359
  #{ target_module_name }##{ method_name }, but can't find :#{ method_name }.
@@ -22,17 +22,17 @@ module Contrast
22
22
 
23
23
  # Indicates the folder in `resources` where this policy lives.
24
24
  def self.policy_folder
25
- raise(NotImplementedError, 'specify policy_folder for patching')
25
+ raise(NoMethodError, 'specify policy_folder for patching')
26
26
  end
27
27
 
28
28
  # Indicates is this feature has been disabled by the configuration,
29
29
  # read at startup, and therefore can never be enabled.
30
30
  def disabled_globally?
31
- raise(NotImplementedError, 'specify disabled_globally? conditions for patching')
31
+ raise(NoMethodError, 'specify disabled_globally? conditions for patching')
32
32
  end
33
33
 
34
34
  def node_type
35
- raise(NotImplementedError, 'specify the concrete node type for this poilcy')
35
+ raise(NoMethodError, 'specify the concrete node type for this poilcy')
36
36
  end
37
37
 
38
38
  access_component :analysis, :logging
@@ -19,11 +19,11 @@ module Contrast
19
19
  attr_reader :properties, :method_scope
20
20
 
21
21
  def node_class
22
- raise NotImplementedError, 'specify the type of the feature for which this node patches'
22
+ raise NoMethodError, 'specify the type of the feature for which this node patches'
23
23
  end
24
24
 
25
25
  def feature
26
- raise NotImplementedError, 'specify the name of the feature for which this node patches'
26
+ raise NoMethodError, 'specify the name of the feature for which this node patches'
27
27
  end
28
28
 
29
29
  def initialize policy_hash = {}
@@ -26,11 +26,11 @@ module Contrast
26
26
  protected
27
27
 
28
28
  def invoke _method, _exception, _properties, _object, _args
29
- raise NotImplementedError, 'This is abstract, override it.'
29
+ raise NoMethodError, 'This is abstract, override it.'
30
30
  end
31
31
 
32
32
  def name
33
- raise NotImplementedError, 'This is abstract, override it.'
33
+ raise NoMethodError, 'This is abstract, override it.'
34
34
  end
35
35
 
36
36
  def rule
@@ -18,15 +18,19 @@ module Contrast
18
18
 
19
19
  UNKNOWN_USER_INPUT = Contrast::Api::Dtm::UserInput.new.tap do |user_input|
20
20
  user_input.input_type = :UNKNOWN
21
- end.cs__freeze
21
+ end
22
22
 
23
- BLOCKING_MODES = Set.new(%i[BLOCK BLOCK_AT_PERIMETER]).cs__freeze
24
- POSTFILTER_MODES = Set.new(%i[BLOCK PERMIT MONITOR]).cs__freeze
25
- STACK_COLLECTION_RESULTS = Set.new(%i[BLOCKED MONITORED]).cs__freeze
23
+ BLOCKING_MODES = Set.new([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
24
+ Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER]).cs__freeze
25
+ POSTFILTER_MODES = Set.new([Contrast::Api::Settings::ProtectionRule::Mode::BLOCK,
26
+ Contrast::Api::Settings::ProtectionRule::Mode::PERMIT,
27
+ Contrast::Api::Settings::ProtectionRule::Mode::MONITOR]).cs__freeze
28
+ STACK_COLLECTION_RESULTS = Set.new([Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED,
29
+ Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED]).cs__freeze
26
30
 
27
31
  attr_reader :mode
28
32
 
29
- def initialize default_mode = :NO_ACTION
33
+ def initialize default_mode = Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION
30
34
  PROTECT.rules[name] = self
31
35
  @mode = mode_from_settings || default_mode
32
36
  end
@@ -135,7 +139,7 @@ module Contrast
135
139
  protected
136
140
 
137
141
  def build_details _input_string, _ia_result
138
- raise Contrast::InternalException, "Rule #{ name } did not implement build_details"
142
+ raise NoMethodError, "Rule #{ name } did not implement build_details"
139
143
  end
140
144
 
141
145
  def mode_from_settings
@@ -176,14 +180,14 @@ module Contrast
176
180
  # @param _kwargs [Hash] key-value pairs used by the rule to build a
177
181
  # report.
178
182
  def find_attacker _context, _potential_attack_string, **_kwargs
179
- raise Contrast::InternalException, "Rule #{ name } did not implement find_attack"
183
+ raise NoMethodError, "Rule #{ name } did not implement find_attack"
180
184
  end
181
185
 
182
186
  def update_successful_attack_response context, ia_result, result, attack_string = nil
183
- if mode == :MONITOR
184
- result.response = :MONITORED
185
- elsif mode == :BLOCK
186
- result.response = :BLOCKED
187
+ if mode == Contrast::Api::Settings::ProtectionRule::Mode::MONITOR
188
+ result.response = Contrast::Api::Dtm::AttackResult::ResponseType::MONITORED
189
+ elsif mode == Contrast::Api::Settings::ProtectionRule::Mode::BLOCK
190
+ result.response = Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED
187
191
  end
188
192
 
189
193
  ia_result.attack_count = ia_result.attack_count + 1 if ia_result
@@ -193,11 +197,11 @@ module Contrast
193
197
  end
194
198
 
195
199
  def update_perimeter_attack_response context, ia_result, result
196
- if mode == :BLOCK_AT_PERIMETER
197
- result.response = :BLOCKED_AT_PERIMETER
200
+ if mode == Contrast::Api::Settings::ProtectionRule::Mode::BLOCK_AT_PERIMETER
201
+ result.response = Contrast::Api::Dtm::AttackResult::ResponseType::BLOCKED_AT_PERIMETER
198
202
  log_rule_matched(context, ia_result, result.response)
199
203
  elsif ia_result.nil? || ia_result.attack_count.zero?
200
- result.response = :PROBED
204
+ result.response = Contrast::Api::Dtm::AttackResult::ResponseType::PROBED
201
205
  log_rule_probed(context, ia_result)
202
206
  end
203
207
 
@@ -237,24 +241,8 @@ module Contrast
237
241
  build_base_sample(context, ia_result)
238
242
  end
239
243
 
240
- def build_user_input ia_result
241
- return UNKNOWN_USER_INPUT unless ia_result
242
-
243
- input = Contrast::Api::Dtm::UserInput.new
244
- input.input_type = ia_result.input_type.to_sym
245
- input.matcher_ids = ia_result.ids
246
- input.path = ia_result.path.to_s
247
- input.key = ia_result.key.to_s
248
- input.value = ia_result.value.to_s
249
- input
250
- end
251
-
252
244
  def build_base_sample context, ia_result
253
- sample = Contrast::Api::Dtm::RaspRuleSample.new
254
- sample.timestamp_ms = context.timer.start_ms
255
- sample.user_input = build_user_input(ia_result)
256
- sample.user_input.document_type = context.request.dtm.document_type unless sample.user_input.cs__frozen?
257
- sample
245
+ Contrast::Api::Dtm::RaspRuleSample.build(context, ia_result)
258
246
  end
259
247
 
260
248
  def log_rule_matched _context, ia_result, response, _matched_string = nil
@@ -79,7 +79,7 @@ module Contrast
79
79
  def find_postfilter_attacker context, potential_attack_string, **kwargs
80
80
  ia_results = gather_ia_results(context)
81
81
  ia_results.select! do |ia_result|
82
- ia_result.score_level == :DEFINITEATTACK
82
+ ia_result.score_level == Contrast::Api::Settings::InputAnalysisResult::ScoreLevel::DEFINITEATTACK
83
83
  end
84
84
  find_attacker_with_results(context, potential_attack_string, ia_results, **kwargs)
85
85
  end
@@ -53,6 +53,7 @@ module Contrast
53
53
  def build_sample context, evaluation, _candidate_string, **kwargs
54
54
  sample = build_base_sample(context, evaluation)
55
55
  sample.user_input.value = kwargs[:method]
56
+ sample.user_input.input_type = :METHOD
56
57
 
57
58
  sample.method_tampering = Contrast::Api::Dtm::HttpMethodTamperingDetails.new
58
59
  sample.method_tampering.method = Contrast::Utils::StringUtils.protobuf_safe_string(kwargs[:method])
@@ -61,17 +62,11 @@ module Contrast
61
62
  sample
62
63
  end
63
64
 
64
- def build_user_input _evaluation
65
- input = Contrast::Api::Dtm::UserInput.new
66
- input.input_type = :METHOD
67
- input
68
- end
69
-
70
65
  private
71
66
 
72
67
  def normal_request? context
73
68
  method = context.request.request_method
74
- context.request.static_request? || method.nil? || STANDARD_METHODS.include?(method.upcase)
69
+ context.request.static? || method.nil? || STANDARD_METHODS.include?(method.upcase)
75
70
  end
76
71
  end
77
72
  end
@@ -77,6 +77,7 @@ module Contrast
77
77
 
78
78
  def build_sample context, ia_result, _url, **kwargs
79
79
  sample = build_base_sample(context, ia_result)
80
+ sample.user_input = build_user_input(ia_result)
80
81
  sample.xxe = kwargs[:details]
81
82
  sample
82
83
  end
@@ -26,14 +26,14 @@ module Contrast
26
26
 
27
27
  application_settings.reactions.each do |reaction|
28
28
  # the enums are all uppercase, we need to downcase them before attempting to log
29
- level = reaction.log_level.nil? ? :error : reaction.log_level.downcase
29
+ level = reaction.log_level.nil? ? :error : reaction.log_level.name.downcase
30
30
 
31
31
  logger.with_level(level, reaction.message) if reaction.message
32
32
 
33
33
  case reaction.operation
34
- when :DISABLE
34
+ when Contrast::Api::Settings::Reaction::Operation::DISABLE
35
35
  Contrast::Agent::DisableReaction.run reaction, level
36
- when :NOOP
36
+ when Contrast::Api::Settings::Reaction::Operation::NOOP
37
37
  # NOOP
38
38
  else
39
39
  logger.warn(
@@ -25,66 +25,16 @@ module Contrast
25
25
  LAST_REST_TOKEN = %r{/[\d]+$}.cs__freeze
26
26
  INNER_NUMBER_MARKER = '/{n}/'
27
27
  LAST_NUMBER_MARKER = '/{n}'
28
- OMITTED_BODY = '{{body-omitted-by-contrast}}'
29
28
 
30
29
  attr_reader :rack_request
31
30
 
32
- def_delegators :@rack_request,
33
- :env,
34
- :query_string,
35
- :user_agent,
36
- :path,
37
- :base_url
38
-
39
- # receiver is memoized because it is the address/host/port of the server, once we
40
- # resolve this for the first time, it shouldn't change
41
- def self.receiver
42
- @_receiver ||= build_receiver
43
- end
31
+ # Delegate calls to the following methods to the attribute @rack_request
32
+ def_delegators :@rack_request, :base_url, :content_type, :cookies, :env, :ip, :path, :port, :query_string, :request_method, :scheme, :url, :user_agent
44
33
 
45
34
  def initialize rack_request
46
35
  @rack_request = rack_request
47
36
  end
48
37
 
49
- ACCEPT = 'ACCEPT'
50
- def accept_headers
51
- accepts = Array(normalized_request_headers[ACCEPT])
52
- accepts.any? ? accepts : nil
53
- end
54
-
55
- STATIC_SUFFIXES = /\.(?:js|css|jpeg|jpg|gif|png|ico|woff|svg|pdf|eot|ttf|jar)$/i.cs__freeze
56
- WILDCARD = '*/*'
57
- # Utility method for checking if a request is for a static resource.
58
- # @return [Boolean] true, if the request is for a well-known static
59
- # type, like the following, and false otherwise: .js, .css, .jpg,
60
- # .gif, .png, .ico
61
- def static_request?
62
- return true if trimmed_uri&.match?(STATIC_SUFFIXES)
63
-
64
- accepts = accept_headers
65
- if accepts
66
- return false if accepts[0].to_s.start_with?(WILDCARD)
67
- return true if media_content_type?(accepts[0])
68
- end
69
-
70
- false
71
- end
72
-
73
- MEDIA_TYPE_MARKERS = %w[image/ text/css text/javascript].cs__freeze
74
- def media_content_type? str
75
- str = str.to_s
76
- str.start_with?(*MEDIA_TYPE_MARKERS)
77
- end
78
-
79
- def trimmed_uri
80
- @_trimmed_uri ||= begin
81
- raise ArgumentError, 'url was nil when attempting to trim' unless uri
82
-
83
- trimmed = uri.split(Contrast::Utils::ObjectShare::SEMICOLON).first # remove ;jsessionid
84
- trimmed.split(Contrast::Utils::ObjectShare::QUESTION_MARK).first # remove ?query_string=
85
- end
86
- end
87
-
88
38
  # Returns a normalized form of the URI. In "normal" URIs
89
39
  # this will return an unchanged String, but in REST-y
90
40
  # URIs this will normalize the digit path tokens, e.g.:
@@ -96,69 +46,111 @@ module Contrast
96
46
  # Should also handle the ;jsessionid.
97
47
  def normalized_uri
98
48
  @_normalized_uri ||= begin
99
- uri = trimmed_uri
100
- uri = uri.gsub(INNER_REST_TOKEN, INNER_NUMBER_MARKER) # replace interior tokens
101
- uri.gsub(LAST_REST_TOKEN, LAST_NUMBER_MARKER) # replace last token
102
- 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
103
55
  end
104
56
 
105
- UNKNOWN_REQUEST_METHOD = 'UNKNOWN'
106
-
107
- def request_method
108
- rack_request.get_header(Rack::REQUEST_METHOD)
109
- rescue StandardError => e
110
- logger.warn('Unable to extract request method', e)
111
- UNKNOWN_REQUEST_METHOD
57
+ def document_type
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
112
67
  end
113
68
 
114
- def document_type_from_header
115
- case content_type
116
- when /xml/i
117
- :XML
118
- when /json/i
119
- :JSON
120
- when /html/i
121
- :NORMAL
122
- end
69
+ # Header keys upcased and any underscores replaced with dashes
70
+ def headers
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
123
85
  end
124
86
 
125
- def document_type
126
- @_document_type ||= begin
127
- type = document_type_from_header
128
- if type
129
- type
130
- elsif request_body_str.start_with?('<?xml')
131
- :XML
132
- elsif request_body_str.match?(/\s*[{\[]/)
133
- :JSON
87
+ def body
88
+ # Memoize a flag indicating whether we've tried to read the body or not
89
+ # (can't use body because it might be nil)
90
+ @_body_read ||= begin
91
+ body = rack_request.body
92
+ if defined?(Rack::Multipart) && defined?(Rack::Multipart::UploadedFile) && body.is_a?(Rack::Multipart::UploadedFile)
93
+ logger.trace("not parsing uploaded file body :: #{ body.original_filename }::#{ body.content_type }")
94
+ @_body = nil
134
95
  else
135
- :NORMAL
96
+ logger.trace("parsing body from request :: #{ body.cs__class.cs__name }")
97
+ @_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
136
98
  end
99
+
100
+ true
137
101
  end
102
+
103
+ # Return memoized body (which might be nil)
104
+ @_body
138
105
  end
139
106
 
140
- def request_headers
141
- @_request_headers ||= begin
142
- with_contrast_scope do
143
- headers = header_pairs(env).each_with_object({}) do |pair, h|
144
- h[pair[0]] = pair[1]
145
- end
146
- headers
147
- end
148
- end
107
+ # Unlike most of our translation, which is called where needed for each
108
+ # message and forgotten, we'll leave this method to call the build as we
109
+ # don't want to pay to reconstruct the DTM for this Request multiple
110
+ # times.
111
+ #
112
+ # @return [Contrast::Api::Dtm::HttpRequest] the SpeedRacer compatible
113
+ # form of this Request
114
+ def dtm
115
+ @_dtm ||= Contrast::Api::Dtm::HttpRequest.build(self)
149
116
  end
150
117
 
151
- # Header keys upcased and any underscores replaced with dashes
152
- def normalized_request_headers
153
- @_normalized_request_headers ||= begin
154
- hash = {}
155
- request_headers.each_pair do |header_name, header_value|
156
- hash[Contrast::Utils::StringUtils.normalized_key(header_name)] = header_value
157
- end
158
- hash
159
- end
118
+ def parameters
119
+ @_parameters ||= with_contrast_scope { normalize_params(rack_request.params) }
120
+ end
121
+
122
+ def file_names
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
131
+ end
132
+
133
+ def hash_id
134
+ @_hash_id ||= Contrast::Utils::HashDigest.generate_request_hash(self)
135
+ end
136
+
137
+ STATIC_SUFFIXES = /\.(?:js|css|jpeg|jpg|gif|png|ico|woff|svg|pdf|eot|ttf|jar)$/i.cs__freeze
138
+ MEDIA_TYPE_MARKERS = %w[image/ text/css text/javascript].cs__freeze
139
+ # Utility method for checking if a request is for a static resource.
140
+ # @return [Boolean] true, if the request is for a well-known static
141
+ # type as determined by the request suffix or the accept header.
142
+ def static?
143
+ return true if normalized_uri&.match?(STATIC_SUFFIXES)
144
+
145
+ accepts = Array(headers['ACCEPT'])&.first&.to_s
146
+ return false unless accepts
147
+ return false if accepts.start_with?('*/*')
148
+
149
+ accepts.start_with?(*MEDIA_TYPE_MARKERS)
160
150
  end
161
151
 
152
+ private
153
+
162
154
  # Return a flattened hash of params with realized paths for keys, in
163
155
  # addition to a separate, valueless, entry for each nest key.
164
156
  # See RUBY-621 for more details.
@@ -197,237 +189,6 @@ module Contrast
197
189
  end
198
190
  end
199
191
 
200
- def request_body
201
- # Memoize a flag indicating whether we've tried to read the body or not
202
- # (can't use body because it might be nil)
203
- @_request_body_read ||= begin
204
- body = @rack_request.body
205
- if defined?(Rack::Multipart)
206
- if defined?(Rack::Multipart::UploadedFile) && body.is_a?(Rack::Multipart::UploadedFile)
207
- logger.trace("not parsing uploaded file body :: #{ body.original_filename }::#{ body.content_type }")
208
- @_request_body = nil
209
- else
210
- logger.trace("parsing body from request :: #{ body.cs__class.cs__name }")
211
- @_request_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
212
- end
213
- else
214
- logger.trace('Rack before 1.3.x does not support Rack::Multipart')
215
- @_request_body = Contrast::Utils::StringUtils.force_utf8(read_body(body))
216
- end
217
-
218
- true
219
- end
220
-
221
- # Return memoized body (which might be nil)
222
- @_request_body
223
- end
224
-
225
- def request_body_str
226
- request_body.to_s || Contrast::Utils::ObjectShare::EMPTY_STRING
227
- end
228
-
229
- # This will become to_protobuf
230
- # Expectation is that all data from a previous phase will be populated
231
- # before the subsequent one begins
232
- def dtm
233
- @_dtm ||= begin
234
- with_contrast_scope do
235
- http_request = Contrast::Api::Dtm::HttpRequest.new
236
- http_request.uuid = Contrast::Utils::StringUtils.force_utf8(__id__)
237
- http_request.timestamp_ms = Contrast::Utils::Timer.now_ms.to_i
238
-
239
- append_receiver(http_request)
240
- append_connection(http_request)
241
- append_params(http_request)
242
- append_headers(http_request)
243
- append_body(http_request)
244
-
245
- http_request
246
- end
247
- end
248
- end
249
-
250
- def append_receiver http_request
251
- r = cs__class.receiver
252
- r.port = port.to_i if port
253
- http_request.receiver = r unless r.nil?
254
- end
255
-
256
- def append_connection http_request
257
- http_request.sender = Contrast::Api::Dtm::Address.new
258
- http_request.sender.ip = Contrast::Utils::StringUtils.force_utf8(ip)
259
- http_request.protocol = Contrast::Utils::StringUtils.force_utf8(scheme)
260
- http_request.version = '1.1' # currently not in rack request; hard-coding
261
- http_request.method = Contrast::Utils::StringUtils.force_utf8(request_method)
262
- http_request.raw = Contrast::Utils::StringUtils.force_utf8(@rack_request.path_info)
263
- http_request.parsed_connection = false
264
- end
265
-
266
- def append_params http_request
267
- parameters.each do |k, v|
268
- next unless k && v
269
- next if v.is_a?(Hash)
270
-
271
- key = Contrast::Utils::StringUtils.force_utf8(k)
272
- val = Contrast::Utils::StringUtils.force_utf8(v)
273
- params = http_request.normalized_request_params
274
- params[key] = Contrast::Api::Dtm::Pair.new unless params[key].is_a?(Contrast::Api::Dtm::Pair)
275
- params[key].key = key
276
- params[key].values << val
277
- end
278
- end
279
-
280
- def append_headers http_request
281
- request_headers.each do |k, v|
282
- next unless k && v
283
- next if v.is_a?(Hash)
284
-
285
- key = Contrast::Utils::StringUtils.force_utf8(k)
286
- val = Contrast::Utils::StringUtils.force_utf8(v)
287
- http_request.request_headers[key] = val
288
- end
289
-
290
- http_request.parsed_request_headers = true
291
-
292
- normalized_request_headers.each do |k, v|
293
- next unless k && v
294
- next if v.is_a?(Hash)
295
-
296
- key = Contrast::Utils::StringUtils.force_utf8(k)
297
- val = Contrast::Utils::StringUtils.force_utf8(v)
298
- http_request.normalized_request_headers[key] ||= Contrast::Api::Dtm::Pair.new
299
- http_request.normalized_request_headers[key].key = key
300
- http_request.normalized_request_headers[key].values << val
301
- end
302
-
303
- request_cookies.each do |k, v|
304
- next unless k && v
305
- next if v.is_a?(Hash)
306
-
307
- key = Contrast::Utils::StringUtils.force_utf8(k)
308
- val = Contrast::Utils::StringUtils.force_utf8(v)
309
- http_request.normalized_cookies[key] ||= Contrast::Api::Dtm::Pair.new
310
- http_request.normalized_cookies[key].key = key
311
- http_request.normalized_cookies[key].values << val
312
- end
313
- end
314
-
315
- def append_body http_request
316
- http_request.document_type = Contrast::Utils::StringUtils.force_utf8(document_type)
317
-
318
- http_request.request_body = if omit_body?
319
- OMITTED_BODY
320
- else
321
- Contrast::Utils::StringUtils.force_utf8(request_body)
322
- end
323
- return if file_names.empty?
324
-
325
- file_names.each do |name, filename|
326
- pair = Contrast::Api::Dtm::SimplePair.new
327
- pair.key = Contrast::Utils::StringUtils.force_utf8(name)
328
- pair.value = Contrast::Utils::StringUtils.force_utf8(filename)
329
- http_request.multipart_headers << pair
330
- end
331
- end
332
-
333
- def omit_body?
334
- return true if AGENT.omit_body?
335
- return false if document_type == :XML
336
- return false if document_type == :JSON
337
-
338
- content_type&.include?('multipart/form-data')
339
- end
340
-
341
- def self.build_receiver
342
- address = Contrast::Api::Dtm::Address.new
343
- address.host = 'localhost'
344
- address.ip = '127.0.0.1'
345
- begin
346
- Timeout.timeout(1) do
347
- address.host = Contrast::Utils::StringUtils.force_utf8(Socket.gethostname)
348
- address.ip = Contrast::Utils::StringUtils.force_utf8(Resolv.getaddress(address.host))
349
- end
350
- rescue StandardError => e
351
- logger.warn('Unable to resolve host or ip', e, address: address)
352
- end
353
- address
354
- end
355
-
356
- # Memoized Rack Request Values
357
- def ip
358
- @_ip ||= @rack_request.ip
359
- end
360
-
361
- def scheme
362
- @_scheme ||= @rack_request.scheme
363
- end
364
-
365
- def port
366
- @_port ||= @rack_request.port
367
- end
368
-
369
- def uri
370
- @_uri ||= @rack_request.path
371
- end
372
-
373
- def url
374
- @_url ||= @rack_request.url
375
- end
376
-
377
- def content_type
378
- @_content_type ||= @rack_request.content_type
379
- end
380
-
381
- def request_cookies
382
- @_request_cookies ||= @rack_request.cookies
383
- end
384
-
385
- def parameters
386
- @_parameters ||= with_contrast_scope { normalize_params(@rack_request.params) }
387
- end
388
-
389
- # End of Rack Request memoized values
390
-
391
- def file_names
392
- @_file_names ||= begin
393
- names = {}
394
- parsed_data = Rack::Multipart.parse_multipart(@rack_request.env)
395
- traverse_parsed_multipart(parsed_data, names)
396
- rescue StandardError => _e
397
- logger.warn('Unable to parse multipart request!')
398
- {}
399
- end
400
- end
401
-
402
- def hash_id
403
- @_hash_id ||= Contrast::Utils::HashDigest.generate_request_hash(self)
404
- end
405
-
406
- # we just need to map length to a repeatable value
407
- # unlike Java, we hash with strings, so we'll use single character
408
- # strings for our purposes.
409
- CHARS = %w[a b c d e f g].cs__freeze
410
- def normalized_length_header chr
411
- chr = chr.to_s
412
- tmp = CHARS[Math.log10(chr.length).to_i] if chr
413
- tmp ||= CHARS[6]
414
- tmp
415
- end
416
-
417
- private
418
-
419
- HTTP_PREFIX = /^[Hh][Tt][Tt][Pp][_-]/i.cs__freeze
420
-
421
- def header_pairs env
422
- selected = env.select do |k, _v|
423
- k.to_s.start_with?(Contrast::Utils::ObjectShare::HTTP_SCORE)
424
- end
425
- selected.map do |k, v|
426
- name = k.to_s.sub(HTTP_PREFIX, Contrast::Utils::ObjectShare::EMPTY_STRING)
427
- [name, v]
428
- end
429
- end
430
-
431
192
  def read_body body
432
193
  return body if body.is_a?(String)
433
194