contrast-agent 6.9.0 → 6.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/lib/contrast/agent/assess/rule/response/body_rule.rb +1 -1
  4. data/lib/contrast/agent/middleware.rb +4 -2
  5. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +76 -83
  6. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +40 -35
  7. data/lib/contrast/agent/protect/policy/applies_command_injection_rule.rb +2 -0
  8. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +6 -3
  9. data/lib/contrast/agent/protect/policy/applies_path_traversal_rule.rb +3 -0
  10. data/lib/contrast/agent/protect/policy/applies_sqli_rule.rb +3 -0
  11. data/lib/contrast/agent/protect/policy/rule_applicator.rb +12 -0
  12. data/lib/contrast/agent/protect/rule/base.rb +19 -5
  13. data/lib/contrast/agent/protect/rule/base_service.rb +6 -0
  14. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +1 -1
  15. data/lib/contrast/agent/protect/rule/bot_blocker.rb +8 -0
  16. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +8 -0
  17. data/lib/contrast/agent/protect/rule/deserialization.rb +2 -2
  18. data/lib/contrast/agent/protect/rule/no_sqli.rb +24 -2
  19. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +1 -1
  20. data/lib/contrast/agent/protect/rule/path_traversal.rb +8 -0
  21. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +0 -1
  22. data/lib/contrast/agent/protect/rule/sqli.rb +6 -10
  23. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +6 -2
  24. data/lib/contrast/agent/protect/rule/unsafe_file_upload.rb +20 -0
  25. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +1 -1
  26. data/lib/contrast/agent/protect/rule/xss.rb +8 -0
  27. data/lib/contrast/agent/protect/rule/xxe.rb +2 -2
  28. data/lib/contrast/agent/protect/rule.rb +0 -3
  29. data/lib/contrast/agent/reporting/attack_result/user_input.rb +0 -1
  30. data/lib/contrast/agent/reporting/details/details.rb +0 -1
  31. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +12 -0
  32. data/lib/contrast/agent/reporting/report.rb +1 -0
  33. data/lib/contrast/agent/reporting/reporter.rb +11 -10
  34. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -5
  35. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +13 -1
  36. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_activity.rb +20 -5
  37. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample.rb +0 -1
  38. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +5 -0
  39. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +10 -1
  40. data/lib/contrast/agent/reporting/reporting_events/application_inventory.rb +2 -1
  41. data/lib/contrast/agent/reporting/reporting_events/application_reporting_event.rb +10 -0
  42. data/lib/contrast/agent/reporting/reporting_events/application_settings.rb +40 -0
  43. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +137 -0
  44. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +12 -4
  45. data/lib/contrast/agent/reporting/reporting_utilities/response_extractor.rb +100 -107
  46. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +5 -4
  47. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +97 -63
  48. data/lib/contrast/agent/reporting/reporting_workers/application_server_worker.rb +46 -0
  49. data/lib/contrast/agent/reporting/reporting_workers/reporter_heartbeat.rb +51 -0
  50. data/lib/contrast/agent/reporting/reporting_workers/reporting_workers.rb +14 -0
  51. data/lib/contrast/agent/reporting/reporting_workers/server_settings_worker.rb +46 -0
  52. data/lib/contrast/agent/reporting/settings/assess.rb +14 -1
  53. data/lib/contrast/agent/reporting/settings/assess_rule.rb +18 -0
  54. data/lib/contrast/agent/reporting/settings/helpers.rb +4 -2
  55. data/lib/contrast/agent/reporting/settings/protect.rb +17 -12
  56. data/lib/contrast/agent/reporting/settings/protect_rule.rb +18 -0
  57. data/lib/contrast/agent/reporting/settings/protect_server_feature.rb +1 -1
  58. data/lib/contrast/agent/reporting/settings/sensitive_data_masking.rb +1 -1
  59. data/lib/contrast/agent/reporting/settings/virtual_patch.rb +56 -0
  60. data/lib/contrast/agent/reporting/settings/virtual_patch_condition.rb +47 -0
  61. data/lib/contrast/agent/request_context_extend.rb +20 -0
  62. data/lib/contrast/agent/telemetry/base.rb +11 -10
  63. data/lib/contrast/agent/telemetry/events/exceptions/obfuscate.rb +108 -103
  64. data/lib/contrast/agent/telemetry/events/startup_metrics_event.rb +1 -1
  65. data/lib/contrast/agent/thread_watcher.rb +16 -10
  66. data/lib/contrast/agent/version.rb +1 -1
  67. data/lib/contrast/agent.rb +12 -0
  68. data/lib/contrast/agent_lib/api/init.rb +1 -7
  69. data/lib/contrast/agent_lib/api/input_tracing.rb +2 -4
  70. data/lib/contrast/agent_lib/interface.rb +1 -16
  71. data/lib/contrast/agent_lib/interface_base.rb +52 -39
  72. data/lib/contrast/agent_lib/return_types/eval_result.rb +2 -2
  73. data/lib/contrast/components/assess.rb +26 -4
  74. data/lib/contrast/components/polling.rb +4 -1
  75. data/lib/contrast/components/settings.rb +46 -3
  76. data/lib/contrast/config/config.rb +2 -2
  77. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  78. data/lib/contrast/config/protect_rules_configuration.rb +1 -1
  79. data/lib/contrast/extension/assess/array.rb +3 -3
  80. data/lib/contrast/extension/assess/regexp.rb +2 -2
  81. data/lib/contrast/logger/aliased_logging.rb +48 -15
  82. data/lib/contrast/utils/input_classification_base.rb +21 -4
  83. data/lib/contrast/utils/routes_sent.rb +2 -2
  84. data/lib/contrast/utils/telemetry.rb +1 -1
  85. data/lib/contrast/utils/telemetry_client.rb +1 -1
  86. data/resources/protect/policy.json +8 -0
  87. data/ruby-agent.gemspec +1 -1
  88. metadata +28 -18
  89. data/lib/contrast/agent/protect/rule/http_method_tampering/http_method_tampering_input_classification.rb +0 -96
  90. data/lib/contrast/agent/protect/rule/http_method_tampering.rb +0 -83
  91. data/lib/contrast/agent/reporting/details/http_method_tempering_details.rb +0 -27
  92. data/lib/contrast/agent/reporting/reporter_heartbeat.rb +0 -47
  93. data/lib/contrast/agent/reporting/server_settings_worker.rb +0 -44
  94. data/lib/contrast/agent_lib/api/method_tempering.rb +0 -29
@@ -1,46 +1,16 @@
1
1
  # Copyright (c) 2022 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-lib'
5
+ require 'contrast/agent/reporting/settings/helpers'
6
+
4
7
  module Contrast
5
8
  module AgentLib
6
9
  # Base class to set basic rule sets and input list.
7
10
  class InterfaceBase
8
- # This could be changed to regular Hash.
9
- # This format is used to supports this kind of log_level assignment:
10
- # via Contrast::Api::Settings::LogLevel::WARN => 3
11
- # ( note -1 is not supported in the protobuf LogLevel)
12
11
  LOG_LEVEL = { -1 => 'OFF', 0 => 'TRACE', 1 => 'DEBUG', 2 => 'INFO', 3 => 'WARN', 4 => 'ERROR' }.cs__freeze
13
12
  LOG_DIR = File.join(Dir.pwd).cs__freeze
14
- # Named after Protect rule_id / Names
15
- # Corresponding to Rust's ulong enum
16
- RULE_SET = {
17
- 'unsafe-file-upload' => 1 << 0,
18
- 'path-traversal' => 1 << 1,
19
- 'reflected-xss' => 1 << 2,
20
- 'sql-injection' => 1 << 3,
21
- 'cmd-injection' => 1 << 4,
22
- 'nosql-injection' => 1 << 5,
23
- 'bot-blocker' => 1 << 6,
24
- 'ssjs-injection' => 1 << 7,
25
- 'method-tampering' => 1 << 8
26
- }.cs__freeze
27
- # Named same as Contrast::Agent::Reporting::InputTypes
28
- INPUT_SET = {
29
- COOKIE_NAME: 1,
30
- COOKIE_VALUE: 2,
31
- HEADER_NAME: 3,
32
- HEADER_VALUE: 4,
33
- JSON_NAME: 5,
34
- JSON_VALUE: 6,
35
- METHOD: 7,
36
- PARAMETER_NAME: 8,
37
- PARAMETER_VALUE: 9,
38
- URI: 10,
39
- URL_PARAMETER: 11,
40
- MULTIPART_NAME: 12,
41
- XML_VALUE: 13
42
- }.cs__freeze
43
- EVAL_OPTIONS = { NONE: 0, WORTHWATCHING: 1 }.cs__freeze
13
+ # Parameters passed to AgentLib. WorthWatching is used for those rules only that support it.
44
14
 
45
15
  # Initializes the Agent lib.
46
16
  #
@@ -54,18 +24,33 @@ module Contrast
54
24
  # Override
55
25
  end
56
26
 
57
- # Return list of available rules
27
+ # Return list of available rules. On first call the constants are extracted from the gem and set
28
+ # to resemble the protect rules ids used across the agent, e.g. CMD_INJECTIONS => cmd-injection.
29
+ #
30
+ # *** Note that the NoSQLI rules is used within the agent, but in future need arise to be used with the
31
+ # AgentLib, the rule_id for that rule atm is 'nosql-injection-mongo' ***
58
32
  #
33
+ # All the rules types extracted from the AgentLib gem will have value corresponding to an ulong enum in
34
+ # Rust land.
59
35
  # @return [Hash]
36
+ # @raise [NameError] If Gem is loaded but not constants are generated there could be not defined
37
+ # module name, or if module was renamed.
60
38
  def rule_set
61
- RULE_SET
39
+ @_rule_set ||= extract_constants(ContrastAgentLib::RuleType, to_rule_id: true)
62
40
  end
63
41
 
64
- # Returns list of available input types.
42
+ # Returns list of available input types
65
43
  #
66
44
  # @return [Hash]
67
45
  def input_set
68
- INPUT_SET
46
+ @_input_set ||= extract_constants(ContrastAgentLib::InputType)
47
+ end
48
+
49
+ # Return the AgentLib supported Database types.
50
+ #
51
+ # @return [Hash]
52
+ def db_set
53
+ @_db_set ||= extract_constants(ContrastAgentLib::DbType)
69
54
  end
70
55
 
71
56
  # Return list of input evaluation options:
@@ -73,7 +58,12 @@ module Contrast
73
58
  #
74
59
  # @return [Hash]
75
60
  def eval_option
76
- EVAL_OPTIONS
61
+ @_eval_option ||= begin
62
+ consts = extract_constants(ContrastAgentLib::EvalOptions).dup
63
+ # Adding the option to call the AgentLib without WorthWatching:
64
+ consts[:NONE] = 0
65
+ consts.cs__freeze
66
+ end
77
67
  end
78
68
 
79
69
  private
@@ -113,6 +103,29 @@ module Contrast
113
103
  FileUtils.mkdir_p(LOG_DIR)
114
104
  LOG_DIR
115
105
  end
106
+
107
+ # This method extracts constats from the AgentLib gem, from specific module.
108
+ # If the to_rule_id flag is set the constant names would be transformed from
109
+ # Symbols to string protect rule ids: e.g. CMD_INJECTIONS => cmd-injection.
110
+ #
111
+ # @param module_name [Module] e.g. ContrastAgentLib::RuleType
112
+ # @param to_rule_id [Boolean] Flag to convert the keys of the hash to protect rule_id style
113
+ # @return [Hash] Frozen hash with the extracted constants
114
+ def extract_constants module_name, to_rule_id: false
115
+ hash = {}
116
+ return hash unless module_name.cs__is_a?(Module)
117
+
118
+ module_name.cs__constants.each do |constant|
119
+ key = to_rule_id ? Contrast::Agent::Reporting::Settings::Helpers.to_rule_id(constant) : constant
120
+ hash[key] = module_name.cs__const_get(constant)
121
+ end
122
+ hash.cs__freeze
123
+ rescue NameError => e
124
+ # This should be log as error since if there is no rule set there will be no ia happening,
125
+ # and something has gone wrong with enabling the gem for the Agent.
126
+ logger.error("[AgentLib] Could not extract #{ module_name } constants", error: e)
127
+ {}.cs__freeze
128
+ end
116
129
  end
117
130
  end
118
131
  end
@@ -26,8 +26,8 @@ module Contrast
26
26
  def initialize hsh
27
27
  return unless hsh&.cs__is_a?(Hash)
28
28
 
29
- @rule_id = Contrast::AgentLib::Interface::RULE_SET.key(hsh[:rule_id])
30
- @input_type = Contrast::AgentLib::Interface::INPUT_SET.key(hsh[:input_type])
29
+ @rule_id = Contrast::AGENT_LIB.rule_set.key(hsh[:rule_id])
30
+ @input_type = Contrast::AGENT_LIB.input_set.key(hsh[:input_type])
31
31
  @score = hsh[:score]
32
32
  end
33
33
 
@@ -23,6 +23,16 @@ module Contrast
23
23
  attr_reader :canon_name
24
24
  # @return [Array]
25
25
  attr_reader :config_values
26
+ # @return [Boolean]
27
+ attr_writer :enable_original_object
28
+ # @return [Integer]
29
+ attr_writer :max_context_source_events
30
+ # @return [Integer]
31
+ attr_writer :max_propagation_events
32
+ # @return [Integer]
33
+ attr_writer :max_rule_reported
34
+ # @return [Integer]
35
+ attr_writer :time_limit_threshold
26
36
 
27
37
  DEFAULT_STACKTRACES = 'ALL'
28
38
  DEFAULT_MAX_SOURCE_EVENTS = 50_000
@@ -55,10 +65,7 @@ module Contrast
55
65
  @sampling = Contrast::Components::Sampling::Interface.new(hsh[:sampling])
56
66
  @rules = Contrast::Components::AssessRules::Interface.new(hsh[:rules])
57
67
  @stacktraces = hsh[:stacktraces]
58
- @max_context_source_events = hsh[:max_context_source_events]
59
- @max_propagation_events = hsh[:max_propagation_events]
60
- @max_rule_reported = hsh[:max_rule_reported]
61
- @time_limit_threshold = hsh[:time_limit_threshold]
68
+ assign_limits(hsh)
62
69
  end
63
70
 
64
71
  # @return [Boolean, true]
@@ -224,6 +231,21 @@ module Contrast
224
231
  @_forcibly_enabled = true?(::Contrast::CONFIG.assess.enable) if @_forcibly_enabled.nil?
225
232
  @_forcibly_enabled
226
233
  end
234
+
235
+ # Sets Event limits from configuration and converts string numbers to integers.
236
+ def assign_limits hsh
237
+ return unless hsh
238
+
239
+ source_limit = hsh[:max_context_source_events]&.to_i
240
+ propagation_limit = hsh[:max_propagation_events]&.to_i
241
+ max_rule_reporter = hsh[:max_rule_reported]&.to_i
242
+ time_limit = hsh[:time_limit_threshold]&.to_i
243
+
244
+ @max_context_source_events = source_limit if source_limit
245
+ @max_propagation_events = propagation_limit if propagation_limit
246
+ @max_rule_reported = max_rule_reporter if max_rule_reporter
247
+ @time_limit_threshold = time_limit if time_limit
248
+ end
227
249
  end
228
250
  end
229
251
  end
@@ -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
 
@@ -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
@@ -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
@@ -12,7 +12,7 @@ module Contrast
12
12
  class RoutesSent
13
13
  # include Contrast::Components::Logger::InstanceMethods
14
14
  ROUTES_LIMIT = 500
15
- TIME_LIMIT_IN_SECONDS = 3600
15
+ TIME_LIMIT_IN_SECONDS = 60
16
16
 
17
17
  attr_accessor :cache
18
18
 
@@ -22,7 +22,7 @@ module Contrast
22
22
 
23
23
  # Determine whether the provided route can be sent to TeamServer.
24
24
  #
25
- # @param [Contrast::Agent::Reporting::ObservedRoute] the route
25
+ # @param route [Contrast::Agent::Reporting::ObservedRoute] the route
26
26
  # @return [boolean]
27
27
  def sendable? route
28
28
  route_hash = route.hash_id
@@ -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
@@ -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
@@ -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