contrast-agent 6.9.0 → 6.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/ext/build_funchook.rb +1 -1
  4. data/lib/contrast/agent/assess/policy/propagator/split.rb +1 -4
  5. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
  6. data/lib/contrast/agent/middleware.rb +5 -3
  7. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +6 -2
  8. data/lib/contrast/agent/patching/policy/trigger_node.rb +1 -1
  9. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +76 -83
  10. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +40 -35
  11. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +2 -0
  12. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +6 -3
  13. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +5 -2
  14. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +3 -0
  15. data/lib/contrast/agent/protect/policy/rule_applicator.rb +12 -0
  16. data/lib/contrast/agent/protect/rule/base.rb +19 -5
  17. data/lib/contrast/agent/protect/rule/base_service.rb +7 -2
  18. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +1 -1
  19. data/lib/contrast/agent/protect/rule/bot_blocker.rb +8 -0
  20. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  21. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +9 -1
  22. data/lib/contrast/agent/protect/rule/cmdi/cmdi_chained_command.rb +1 -1
  23. data/lib/contrast/agent/protect/rule/cmdi/cmdi_dangerous_path.rb +1 -1
  24. data/lib/contrast/agent/protect/rule/deserialization.rb +2 -2
  25. data/lib/contrast/agent/protect/rule/no_sqli.rb +24 -2
  26. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +1 -1
  27. data/lib/contrast/agent/protect/rule/path_traversal.rb +8 -0
  28. data/lib/contrast/agent/protect/rule/sqli/postgres_sql_scanner.rb +0 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
  30. data/lib/contrast/agent/protect/rule/sqli.rb +6 -10
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
  32. data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
  34. data/lib/contrast/agent/protect/rule/xss.rb +8 -0
  35. data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
  36. data/lib/contrast/agent/protect/rule.rb +0 -3
  37. data/lib/contrast/agent/reporting/attack_result/user_input.rb +0 -1
  38. data/lib/contrast/agent/reporting/details/details.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +12 -0
  40. data/lib/contrast/agent/reporting/report.rb +1 -0
  41. data/lib/contrast/agent/reporting/reporter.rb +12 -15
  42. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -5
  43. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -1
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +20 -5
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +0 -1
  46. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -0
  47. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -1
  48. data/lib/contrast/agent/reporting/reporting_events/application_inventory.rb +2 -1
  49. data/lib/contrast/agent/reporting/reporting_events/application_reporting_event.rb +10 -0
  50. data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
  51. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +9 -5
  52. data/lib/contrast/agent/reporting/reporting_events/observed_route.rb +8 -5
  53. data/lib/contrast/agent/reporting/reporting_utilities/endpoints.rb +7 -7
  54. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
  55. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +12 -4
  56. data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +100 -107
  57. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +5 -4
  58. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +101 -67
  59. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
  60. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
  61. data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
  62. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
  63. data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
  64. data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
  65. data/lib/contrast/agent/reporting/settings/helpers.rb +4 -2
  66. data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
  67. data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
  68. data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +1 -1
  69. data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
  70. data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
  71. data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
  72. data/lib/contrast/agent/request_context_extend.rb +20 -0
  73. data/lib/contrast/agent/telemetry/base.rb +13 -15
  74. data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
  75. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
  76. data/lib/contrast/agent/thread_watcher.rb +16 -10
  77. data/lib/contrast/agent/version.rb +1 -1
  78. data/lib/contrast/agent.rb +12 -0
  79. data/lib/contrast/agent_lib/api/init.rb +1 -7
  80. data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
  81. data/lib/contrast/agent_lib/interface.rb +1 -16
  82. data/lib/contrast/agent_lib/interface_base.rb +52 -39
  83. data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
  84. data/lib/contrast/components/assess.rb +26 -4
  85. data/lib/contrast/components/config.rb +1 -1
  86. data/lib/contrast/components/polling.rb +4 -1
  87. data/lib/contrast/components/settings.rb +46 -3
  88. data/lib/contrast/config/config.rb +2 -2
  89. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  90. data/lib/contrast/config/protect_rules_configuration.rb +1 -1
  91. data/lib/contrast/extension/assess/array.rb +3 -3
  92. data/lib/contrast/extension/assess/regexp.rb +2 -2
  93. data/lib/contrast/framework/rack/patch/session_cookie.rb +2 -1
  94. data/lib/contrast/logger/aliased_logging.rb +48 -15
  95. data/lib/contrast/utils/duck_utils.rb +18 -0
  96. data/lib/contrast/utils/heap_dump_util.rb +1 -1
  97. data/lib/contrast/utils/input_classification_base.rb +21 -4
  98. data/lib/contrast/utils/log_utils.rb +1 -1
  99. data/lib/contrast/utils/middleware_utils.rb +1 -1
  100. data/lib/contrast/utils/patching/policy/patch_utils.rb +2 -2
  101. data/lib/contrast/utils/routes_sent.rb +6 -2
  102. data/lib/contrast/utils/telemetry.rb +2 -2
  103. data/lib/contrast/utils/telemetry_client.rb +1 -1
  104. data/resources/protect/policy.json +8 -0
  105. data/ruby-agent.gemspec +6 -6
  106. metadata +40 -30
  107. data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
  108. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
  109. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
  110. data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -47
  111. data/lib/contrast/agent/reporting/server_settings_worker.rb +0 -44
  112. data/lib/contrast/agent_lib/api/method_tempering.rb +0 -29
@@ -30,7 +30,7 @@ module Contrast
30
30
  DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z'
31
31
 
32
32
  class Interface # :nodoc: # rubocop:disable Metrics/ClassLength
33
- SESSION_VARIABLES = 'Invalid configuration. '\
33
+ SESSION_VARIABLES = 'Invalid configuration. ' \
34
34
  "Setting both application.session_id and application.session_metadata is not allowed.\n"
35
35
  API_URL = "Invalid configuration. Missing a required connection value 'url' is not set."
36
36
  API_KEY = "Invalid configuration. Missing a required connection value 'api_key' is not set."
@@ -12,6 +12,8 @@ module Contrast
12
12
 
13
13
  # @return [Integer, nil]
14
14
  attr_reader :server_settings_ms
15
+ # @return [Integer, nil]
16
+ attr_reader :app_settings_ms
15
17
  # @return [String]
16
18
  attr_reader :canon_name
17
19
  # @return [Array]
@@ -20,7 +22,7 @@ module Contrast
20
22
  attr_reader :batch_reporting_interval_ms
21
23
 
22
24
  CANON_NAME = 'agent.polling'
23
- CONFIG_VALUES = %w[server_settings_ms batch_reporting_interval_ms].cs__freeze
25
+ CONFIG_VALUES = %w[server_settings_ms app_settings_ms batch_reporting_interval_ms].cs__freeze
24
26
 
25
27
  def initialize hsh = {}
26
28
  @config_values = CONFIG_VALUES
@@ -29,6 +31,7 @@ module Contrast
29
31
 
30
32
  @server_settings_ms = hsh[:server_settings_ms]
31
33
  @batch_reporting_interval_ms = hsh[:batch_reporting_interval_ms]
34
+ @app_settings_ms = hsh[:app_settings_ms]
32
35
  end
33
36
  end
34
37
  end
@@ -75,6 +75,11 @@ module Contrast
75
75
  # two dates are the same.
76
76
  # format: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
77
77
  attr_reader :server_settings_last_httpdate
78
+ # @return [String] The last update but in string format used to build request header.
79
+ # This value should be sent be TS in the Last-Modified header to sync and save resources if the
80
+ # two dates are the same.
81
+ # format: <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
82
+ attr_reader :app_settings_last_httpdate
78
83
 
79
84
  def initialize
80
85
  reset_state
@@ -85,13 +90,15 @@ module Contrast
85
90
  end
86
91
 
87
92
  # @param features_response [Contrast::Agent::Reporting::Response]
88
- def update_from_server_features features_response
93
+ def update_from_server_features features_response # rubocop:disable Metrics/AbcSize
89
94
  return unless (server_features = features_response&.server_features)
90
95
 
91
96
  log_file = server_features.log_file
92
97
  log_level = server_features.log_level
93
98
  # Update logger:
94
99
  Contrast::Logger::Log.instance.update(log_file, log_level) if log_file || log_level
100
+ # Update AgentLib Logger
101
+ update_agent_lib_log(log_level.to_s)
95
102
  # Update CEFlogger:
96
103
  unless server_features.security_logger.settings_blank?
97
104
  cef_logger.build_logger(server_features.security_logger.log_level, server_features.security_logger.log_file)
@@ -110,6 +117,25 @@ module Contrast
110
117
  # next request's header with the same time will save needless update of settings if there
111
118
  # are no new server features updates after the said time.
112
119
  @server_settings_last_httpdate = header_last_update
120
+ rescue StandardError => e
121
+ logger.warn('The following error occurred from server update: ', e: e)
122
+ end
123
+
124
+ # Update AgentLib log level
125
+ def update_agent_lib_log new_log_level
126
+ agent_lib_log_level = Contrast::AgentLib::InterfaceBase::LOG_LEVEL[0] if new_log_level.empty?
127
+ agent_lib_log_level ||= Contrast::AgentLib::InterfaceBase::LOG_LEVEL.key(new_log_level.upcase)
128
+
129
+ # detect if the provided level is invalid and log if it is
130
+ # by default if we pass invalid log level - it will leave the last active
131
+ unless Contrast::AgentLib::InterfaceBase::LOG_LEVEL.value?(new_log_level.upcase)
132
+ cur_active = Contrast::AGENT_LIB.log_level
133
+ logger.debug('The provided level was invalid, so the logger stays to the last active: ',
134
+ active: cur_active,
135
+ provided_level: new_log_level)
136
+ end
137
+
138
+ Contrast::AGENT_LIB.change_log_options(true, agent_lib_log_level)
113
139
  end
114
140
 
115
141
  # Update Assess server features
@@ -135,23 +161,41 @@ module Contrast
135
161
 
136
162
  @application_state.modes_by_id = app_settings.protect.protection_rules_to_settings_hash
137
163
  update_exclusion_matchers(app_settings.exclusions)
164
+ app_settings.protect.virtual_patches = app_settings.protect.virtual_patches unless
165
+ settings_empty?(app_settings.protect.virtual_patches)
138
166
  update_sensitive_data_policy(app_settings.sensitive_data_masking)
139
167
  @assess_state.disabled_assess_rules = app_settings.assess.disabled_rules
140
168
  new_session_id = app_settings.assess.session_id
141
169
  @assess_state.session_id = new_session_id if new_session_id && !new_session_id.blank?
142
170
  @last_app_update_ms = Contrast::Utils::Timer.now_ms
171
+ @app_settings_last_httpdate = header_last_update
143
172
  end
144
173
 
145
174
  # Wipe state to zero.
146
175
  def reset_state
147
176
  @protect_state = PROTECT_STATE_BASE.dup
148
- @assess_state = ASSESS_STATE_BASE.dup
177
+ update_assess_state
149
178
  @application_state = APPLICATION_STATE_BASE.dup
150
179
  @tainted_columns = {}
151
180
  @sensitive_data_masking = SENSITIVE_DATA_MASKING_BASE.dup
152
181
  @excluder = Contrast::Agent::Excluder.new
153
182
  end
154
183
 
184
+ # We save the session_id, reset and set it again if available.
185
+ # This done so that reporting between updates won't trigger argument error
186
+ # for missing session_id given one is already set and used with the first application
187
+ # create response received from TS.
188
+ #
189
+ # @return [Struct]
190
+ def update_assess_state
191
+ current_session_id = @assess_state&.session_id
192
+ @assess_state = ASSESS_STATE_BASE.dup
193
+ # There is application settings update for the session id if new is received.
194
+ # Here we make sure not to delete the already set one.
195
+ @assess_state&.session_id = current_session_id unless current_session_id&.empty?
196
+ @assess_state
197
+ end
198
+
155
199
  def build_protect_rules
156
200
  @protect_state.rules = {}
157
201
 
@@ -160,7 +204,6 @@ module Contrast
160
204
  cmdi = Contrast::Agent::Protect::Rule::CmdInjection.new
161
205
  cmdi.sub_rules
162
206
  Contrast::Agent::Protect::Rule::Deserialization.new
163
- Contrast::Agent::Protect::Rule::HttpMethodTampering.new
164
207
  Contrast::Agent::Protect::Rule::NoSqli.new
165
208
  path = Contrast::Agent::Protect::Rule::PathTraversal.new
166
209
  path.sub_rules
@@ -25,10 +25,10 @@ module Contrast
25
25
  def determine_config_status response
26
26
  return unless response
27
27
  # If we encounter for some of the startup events failure - always return failure
28
- return if @config_status == MESSAGE_FAIL || CONN_STATUS_MSG_FAILURE
28
+ return if [MESSAGE_FAIL, CONN_STATUS_MSG_FAILURE].include?(@config_status)
29
29
 
30
30
  response_code = response.code.to_s
31
- @config_status = response_code.starts_with?('2') ? MESSAGE_SUCCESSFUL : MESSAGE_FAIL
31
+ @config_status = response_code.start_with?('2') ? MESSAGE_SUCCESSFUL : MESSAGE_FAIL
32
32
  nil
33
33
  end
34
34
 
@@ -36,7 +36,7 @@ module Contrast
36
36
  # String to its recognized symbol equivalent. If a nonsense value is provided, it'll
37
37
  # be treated the same as disabling the rule.
38
38
  #
39
- # @return [Symbol]
39
+ # @return [Symbol, nil]
40
40
  def applicable_mode
41
41
  return unless mode
42
42
 
@@ -50,7 +50,7 @@ module Contrast
50
50
  Contrast::Config::ProtectRuleConfiguration.new(hsh[:'sql-injection-semantic-dangerous-functions'])
51
51
  @unsafe_file_upload = Contrast::Config::ProtectRuleConfiguration.new(hsh[:'unsafe-file-upload'])
52
52
  @untrusted_deserialization = Contrast::Config::ProtectRuleConfiguration.new(hsh[:'untrusted-deserialization'])
53
- @xxe = Contrast::Config::ProtectRuleConfiguration.new(hsh['xxe'])
53
+ @xxe = Contrast::Config::ProtectRuleConfiguration.new(hsh[:xxe])
54
54
  end
55
55
 
56
56
  def []= key, value
@@ -13,10 +13,10 @@ module Contrast
13
13
  # This is our patch of the Array class required to handle propagation
14
14
  # Disclaimer: there may be a better way, but we're in a 'get it work' state.
15
15
  # Hopefully, we'll be in a 'get it right' state soon.
16
- class ArrayPropagator # rubocop:disable Style/StaticClass
16
+ class ArrayPropagator
17
17
  extend Contrast::Components::Scope::InstanceMethods
18
18
 
19
- ARRAY_JOIN_HASH = {
19
+ ARRAY_JOIN_HASH ||= { # rubocop:disable Lint/OrAssignmentToConstant
20
20
  'class_name' => 'Array',
21
21
  'instance_method' => true,
22
22
  'method_visibility' => 'public',
@@ -27,7 +27,7 @@ module Contrast
27
27
  'patch_class' => 'NOOP',
28
28
  'patch_method' => 'cs__track_join'
29
29
  }.cs__freeze
30
- ARRAY_JOIN_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(ARRAY_JOIN_HASH)
30
+ ARRAY_JOIN_NODE ||= Contrast::Agent::Assess::Policy::PropagationNode.new(ARRAY_JOIN_HASH) # rubocop:disable Lint/OrAssignmentToConstant
31
31
 
32
32
  class << self
33
33
  # When you call join, they use an internal thing, so there's no good way to get at the thing being returned.
@@ -17,7 +17,7 @@ module Contrast
17
17
  extend Contrast::Components::Logger::InstanceMethods
18
18
  extend Contrast::Components::Scope::InstanceMethods
19
19
 
20
- REGEXP_EQUAL_SQUIGGLE_HASH = {
20
+ REGEXP_EQUAL_SQUIGGLE_HASH ||= { # rubocop:disable Lint/OrAssignmentToConstant
21
21
  'id' => 'regexp_100',
22
22
  'class_name' => 'Regexp',
23
23
  'instance_method' => true,
@@ -29,7 +29,7 @@ module Contrast
29
29
  'patch_class' => 'Contrast::Extension::Assess::RegexpPropagator',
30
30
  'patch_method' => 'track_equal_squiggle'
31
31
  }.cs__freeze
32
- REGEXP_EQUAL_SQUIGGLE_NODE = Contrast::Agent::Assess::Policy::PropagationNode.new(REGEXP_EQUAL_SQUIGGLE_HASH)
32
+ REGEXP_EQUAL_SQUIGGLE_NODE ||= Contrast::Agent::Assess::Policy::PropagationNode.new(REGEXP_EQUAL_SQUIGGLE_HASH) # rubocop:disable Lint/OrAssignmentToConstant
33
33
  private_constant :REGEXP_EQUAL_SQUIGGLE_HASH
34
34
  private_constant :REGEXP_EQUAL_SQUIGGLE_NODE
35
35
 
@@ -48,7 +48,8 @@ module Contrast
48
48
 
49
49
  def vulnerable_setting?(setting_key,
50
50
  safe_settings_value,
51
- options, safe_default: true,
51
+ options,
52
+ safe_default: true,
52
53
  comparison_type: nil)
53
54
  # In most cases, Rack is pretty nice and the default value is safe
54
55
  return !safe_default unless options&.key?(setting_key)
@@ -41,31 +41,39 @@ module Contrast
41
41
 
42
42
  private
43
43
 
44
+ # @param type [ALIASED_FATAL, ALIASED_ERROR, ALIASED_WARN] the type of error, used to indicate the function used
45
+ # for logging
46
+ # @param message [String] the exception message
47
+ # @param exception [Exception] The exception or error
48
+ # @param data [Object] Any structured data
44
49
  def build_exception type, message = nil, exception = nil, data = nil
45
- stack_trace = get_stack_trace(type)
46
- stack_frame_type = Contrast::Agent::Telemetry::TelemetryException::Obfuscate.obfuscate_type(
47
- stack_trace[1].path.delete_prefix(Dir.pwd))
48
- stack_frame_function = stack_trace[1].label
50
+ stack_trace = wrapped_caller_locations
51
+ caller_idx = stack_trace&.find_index { |stack| stack.to_s.include?(type) } || 0
52
+ # The caller_stack is the method in which the error occurred, so has to be above this method
53
+ caller_idx += 1
54
+ caller_frame = stack_trace[caller_idx]
55
+ stack_frame_type = caller_frame.path.delete_prefix(Dir.pwd)
56
+ stack_frame_function = caller_frame.label
49
57
  key = "#{ stack_frame_type }|#{ stack_frame_function }|#{ message }"
50
- if TELEMETRY_EXCEPTIONS[key]
51
- TELEMETRY_EXCEPTIONS.increment(key)
58
+ if Contrast::TELEMETRY_EXCEPTIONS[key]
59
+ Contrast::TELEMETRY_EXCEPTIONS.increment(key)
52
60
  return
53
61
  end
54
62
 
55
- return if TELEMETRY_EXCEPTIONS.exception_limit?
56
-
57
- message_exception_type = Contrast::Agent::Telemetry::TelemetryException::Obfuscate.obfuscate_exception_type(
58
- exception ? exception.cs__class.to_s : stack_frame_type.split('/').last)
63
+ return if Contrast::TELEMETRY_EXCEPTIONS.exception_limit?
59
64
 
65
+ message_exception_type = exception ? exception.cs__class.to_s : stack_frame_type.split('/').last
60
66
  event_message = create_message(stack_frame_function,
61
67
  stack_frame_type, message_exception_type,
62
68
  data, exception,
63
69
  message)
70
+ build_stack(event_message, stack_trace, caller_idx)
64
71
  TELEMETRY_EXCEPTIONS[key] = event_message
65
72
  rescue StandardError => e
66
- debug('Unable to report exception to telemetry', e)
73
+ debug('[Telemetry] Unable to report exception', e)
67
74
  end
68
75
 
76
+ # @return [Contrast::Agent::Telemetry::TelemetryException::Event]
69
77
  def create_message stack_frame_function, stack_frame_type, message_exception_type, data, exception, message
70
78
  message_for_exception = if exception
71
79
  exception.cs__respond_to?(:message) ? exception.message : exception
@@ -89,12 +97,37 @@ module Contrast
89
97
  message = Contrast::Agent::Telemetry::TelemetryException::Message.build(tags, [message_exception])
90
98
  Contrast::Agent::Telemetry::TelemetryException::Event.new(message)
91
99
  rescue ArgumentError => e
92
- debug('TelemetryException failed from aliased logging with: ', e)
100
+ debug('[Telemetry] TelemetryException failed from aliased logging with: ', e)
101
+ end
102
+
103
+ # Convert the given caller_stack from the event into the exception StackFrame format for reporting, appending it
104
+ # to the Exception wrapped in the Event.
105
+ #
106
+ # @param event_message [Contrast::Agent::Telemetry::TelemetryException::Event]
107
+ # @param caller_stack [(Array<Thread::Backtrace::Location>, nil]
108
+ # @param caller_idx [Integer] the starting location for the exception, which allows filtering out the logger code
109
+ # from the stack
110
+ def build_stack event_message, caller_stack, caller_idx = 0
111
+ return unless caller_stack
112
+
113
+ event_exception = event_message.exceptions[0]
114
+ event_exception_message = event_exception.exceptions[0]
115
+
116
+ caller_stack.each_with_index do |caller, idx|
117
+ next unless idx > caller_idx
118
+
119
+ stack_frame =
120
+ Contrast::Agent::Telemetry::TelemetryException::StackFrame.build(caller.label,
121
+ caller.path.delete_prefix(Dir.pwd))
122
+ event_exception_message.push(stack_frame)
123
+ end
93
124
  end
94
125
 
95
- def get_stack_trace type
96
- start = caller_locations&.find_index { |stack| stack.to_s.include?(type) }
97
- start ? caller_locations(start + 1, 20) : caller_locations(20, 20)
126
+ # This is purely a wrapper around caller_locations used for testing
127
+ #
128
+ # @return [Array<Thread::Backtrace::Location>, nil]
129
+ def wrapped_caller_locations
130
+ caller_locations
98
131
  end
99
132
  end
100
133
  end
@@ -63,6 +63,24 @@ module Contrast
63
63
  # otherwise, don't risk it
64
64
  false
65
65
  end
66
+
67
+ # Every duck quacks to nil, we need to check if it is empty?
68
+ # Utils method to check for blank ( nil or empty object ).
69
+ #
70
+ # @param [Object] object to test.
71
+ # @return [Boolean]
72
+ def empty_duck? object
73
+ # If not quacking to empty it is True/False class, Integer, Regexp or Time instance.
74
+ return false if object.instance_of?(TrueClass) || object.instance_of?(FalseClass)
75
+
76
+ if object.cs__respond_to?(:empty?)
77
+ return true if object.empty?
78
+ elsif object.nil?
79
+ return true
80
+ end
81
+
82
+ false
83
+ end
66
84
  end
67
85
  end
68
86
  end
@@ -35,7 +35,7 @@ module Contrast
35
35
  control = Contrast::Utils::HeapDumpUtil.control
36
36
  log_enabled_warning
37
37
  dir = control[:path]
38
- Dir.mkdir(dir) unless Dir.exist?(dir)
38
+ FileUtils.mkdir_p(dir)
39
39
  return unless File.writable?(dir)
40
40
 
41
41
  delay = control[:delay]
@@ -43,7 +43,7 @@ module Contrast
43
43
  next unless v
44
44
 
45
45
  result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, v)
46
- input_analysis.results << result unless result.nil?
46
+ append_result(input_analysis, result)
47
47
  end
48
48
  end
49
49
 
@@ -51,6 +51,7 @@ module Contrast
51
51
  rescue StandardError => e
52
52
  logger.debug("An Error was recorded in the input classification of the #{ rule_id }")
53
53
  logger.debug(e)
54
+ nil
54
55
  end
55
56
 
56
57
  # Creates new isntance of InputAnalysisResult with basic info.
@@ -103,16 +104,24 @@ module Contrast
103
104
  # @return [Integer<Contrast::AgentLib::Interface::INPUT_SET>]
104
105
  def convert_input_type input_type
105
106
  case input_type
106
- when BODY, DWR_VALUE
107
+ when URI, URL_PARAMETER
108
+ Contrast::AGENT_LIB.input_set[:URI_PATH]
109
+ when BODY, DWR_VALUE, SOCKET, UNDEFINED_TYPE, UNKNOWN, REQUEST, QUERYSTRING
107
110
  Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
108
111
  when HEADER
109
112
  Contrast::AGENT_LIB.input_set[:HEADER_VALUE]
110
113
  when MULTIPART_VALUE, MULTIPART_FIELD_NAME
111
114
  Contrast::AGENT_LIB.input_set[:MULTIPART_NAME]
115
+ when JSON_ARRAYED_VALUE
116
+ Contrast::AGENT_LIB.input_set[:JSON_KEY]
117
+ when PARAMETER_NAME
118
+ Contrast::AGENT_LIB.input_set[:PARAMETER_KEY]
112
119
  else
113
120
  Contrast::AGENT_LIB.input_set[input_type]
114
121
  end
115
- rescue StandardError
122
+ rescue StandardError => e
123
+ logger.debug('Protect Input classification could not determine input type, falling back to default',
124
+ error: e)
116
125
  Contrast::AGENT_LIB.input_set[:PARAMETER_VALUE]
117
126
  end
118
127
 
@@ -133,7 +142,7 @@ module Contrast
133
142
  input_eval = Contrast::AGENT_LIB.eval_input(value,
134
143
  convert_input_type(input_type),
135
144
  Contrast::AGENT_LIB.rule_set[rule_id],
136
- Contrast::AGENT_LIB.eval_option[:WORTHWATCHING])
145
+ Contrast::AGENT_LIB.eval_option[:PREFER_WORTH_WATCHING])
137
146
 
138
147
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
139
148
  score = input_eval&.score || 0
@@ -148,6 +157,14 @@ module Contrast
148
157
  add_needed_key(request, ia_result, input_type, value) if KEYS_NEEDED.include?(input_type)
149
158
  ia_result
150
159
  end
160
+
161
+ def append_result ia_analysis, result
162
+ unless result.nil?
163
+ ia_analysis.results << result
164
+ ia_analysis.triggered_rules << result.rule_id unless ia_analysis.triggered_rules.include?(result.rule_id)
165
+ end
166
+ ia_analysis
167
+ end
151
168
  end
152
169
  end
153
170
  end
@@ -26,7 +26,7 @@ module Contrast
26
26
  return File.writable?(path) if File.exist?(path)
27
27
 
28
28
  dir_name = File.dirname(File.absolute_path(path))
29
- FileUtils.mkdir_p(dir_name) unless Dir.exist?(dir_name)
29
+ FileUtils.mkdir_p(dir_name)
30
30
  File.writable?(dir_name)
31
31
  end
32
32
 
@@ -12,7 +12,7 @@ module Contrast
12
12
  LANGUAGE_DEPRECATION_VERSION = '2.7'
13
13
  LANGUAGE_DEPRECATION_YEAR = '2023'
14
14
  LANGUAGE_DEPRECATION_WARNING =
15
- "[Contrast Security] [DEPRECATION] Support for Ruby #{ LANGUAGE_DEPRECATION_VERSION } will be removed in "\
15
+ "[Contrast Security] [DEPRECATION] Support for Ruby #{ LANGUAGE_DEPRECATION_VERSION } will be removed in " \
16
16
  "April #{ LANGUAGE_DEPRECATION_YEAR }. Please contact Customer Support prior if you require continued support."
17
17
 
18
18
  def setup_agent
@@ -23,8 +23,8 @@ module Contrast
23
23
  # Given a method, return a symbol in the format
24
24
  # <method_start>_unbound_<method_name>
25
25
  def build_unbound_method_name patcher_method
26
- "#{ Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START }unbound"\
27
- "#{ Contrast::Utils::ObjectShare::UNDERSCORE }"\
26
+ "#{ Contrast::Utils::ObjectShare::CONTRAST_PATCHED_METHOD_START }unbound" \
27
+ "#{ Contrast::Utils::ObjectShare::UNDERSCORE }" \
28
28
  "#{ patcher_method }".to_sym
29
29
  end
30
30
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  # require 'contrast/components/logger'
5
5
  # require 'contrast/agent/telemetry/events/exceptions/telemetry_exception_event'
6
+ require 'contrast/utils/duck_utils'
6
7
 
7
8
  module Contrast
8
9
  module Utils
@@ -12,7 +13,7 @@ module Contrast
12
13
  class RoutesSent
13
14
  # include Contrast::Components::Logger::InstanceMethods
14
15
  ROUTES_LIMIT = 500
15
- TIME_LIMIT_IN_SECONDS = 3600
16
+ TIME_LIMIT_IN_SECONDS = 60
16
17
 
17
18
  attr_accessor :cache
18
19
 
@@ -22,9 +23,12 @@ module Contrast
22
23
 
23
24
  # Determine whether the provided route can be sent to TeamServer.
24
25
  #
25
- # @param [Contrast::Agent::Reporting::ObservedRoute] the route
26
+ # @param route [Contrast::Agent::Reporting::ObservedRoute] the route
26
27
  # @return [boolean]
27
28
  def sendable? route
29
+ return false if Contrast::Utils::DuckUtils.empty_duck?(route.signature)
30
+ return false if Contrast::Utils::DuckUtils.empty_duck?(route.url)
31
+
28
32
  route_hash = route.hash_id
29
33
 
30
34
  # If hash doesn't exist in @cache...
@@ -9,7 +9,7 @@ module Contrast
9
9
  module Utils
10
10
  # Tools for supporting the Telemetry feature
11
11
  module Telemetry
12
- DIR = '/ect/contrast/ruby-agent/'.cs__freeze
12
+ DIR = '/etc/contrast/ruby-agent/'.cs__freeze
13
13
  FILE = '.telemetry'.cs__freeze
14
14
  CURRENT_DIR = Dir.pwd.cs__freeze
15
15
  CONFIG_DIR = CURRENT_DIR + '/config/contrast/'.cs__freeze
@@ -81,7 +81,7 @@ module Contrast
81
81
  #
82
82
  # @return[Boolean] true if success, false if fails
83
83
  def touch_marker dir, file
84
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
84
+ FileUtils.mkdir_p(dir)
85
85
  FileUtils.touch(dir + file)
86
86
  File.file?(dir + file)
87
87
  rescue StandardError => _e
@@ -99,7 +99,7 @@ module Contrast
99
99
  def get_event_json event
100
100
  Array(event.to_controlled_hash).to_json
101
101
  rescue Exception => e # rubocop:disable Lint/RescueException
102
- logger.error('Unable to convert TelemetryEvent to JSON string', e, hsh)
102
+ logger.error('[Telemetry] Unable to convert TelemetryEvent to JSON string', e, hsh)
103
103
  raise(e)
104
104
  end
105
105
  end
@@ -128,6 +128,14 @@
128
128
  "database": "MongoDB"
129
129
  }
130
130
  },{
131
+ "class_name": "Mongo::Collection",
132
+ "instance_method": true,
133
+ "method_visibility": "public",
134
+ "method_name": "find",
135
+ "properties": {
136
+ "database": "MongoDB"
137
+ }
138
+ },{
131
139
  "class_name": "Moped::Node",
132
140
  "method_name": "read",
133
141
  "instance_method": true,
data/ruby-agent.gemspec CHANGED
@@ -75,7 +75,7 @@ def self.add_specs spec
75
75
  spec.add_development_dependency 'factory_bot'
76
76
  spec.add_development_dependency 'fake_ftp'
77
77
  spec.add_development_dependency 'openssl'
78
- spec.add_development_dependency 'parallel_tests'
78
+ spec.add_development_dependency 'parallel_tests', '~> 3.0'
79
79
  spec.add_development_dependency 'rspec', '~> 3.0'
80
80
  spec.add_development_dependency 'rspec-benchmark'
81
81
  spec.add_development_dependency 'rspec_junit_formatter', '0.3.0'
@@ -92,11 +92,11 @@ end
92
92
 
93
93
  # Dependencies used to run all of our Rubocop during the linting phase.
94
94
  def self.add_rubocop spec
95
- spec.add_development_dependency 'rubocop', '1.26.1'
96
- spec.add_development_dependency 'rubocop-performance', '1.13.3'
97
- spec.add_development_dependency 'rubocop-rails', '2.14.2'
95
+ spec.add_development_dependency 'rubocop', '1.37.1'
96
+ spec.add_development_dependency 'rubocop-performance', '1.15.0'
97
+ spec.add_development_dependency 'rubocop-rails', '2.17.2'
98
98
  spec.add_development_dependency 'rubocop-rake', '0.6.0'
99
- spec.add_development_dependency 'rubocop-rspec', '2.9.0'
99
+ spec.add_development_dependency 'rubocop-rspec', '2.14.2'
100
100
  end
101
101
 
102
102
  # Dependencies not mocked out during RSpec that we test real code of, beyond just frameworks.
@@ -121,7 +121,7 @@ end
121
121
  def self.add_dependencies spec
122
122
  spec.add_dependency 'ougai', '>= 1.8', '< 3.0.0'
123
123
  spec.add_dependency 'rack', '~> 2.0'
124
- spec.add_dependency 'contrast-agent-lib', '~> 0.1.0'
124
+ spec.add_dependency 'contrast-agent-lib', '~> 0.1.0', '>= 0.1.3'
125
125
  spec.add_dependency 'ffi', '~> 1.0'
126
126
  end
127
127