contrast-agent 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  4. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  5. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  6. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  7. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  8. data/lib/contrast/agent/protect/rule/base.rb +9 -7
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  10. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  15. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  25. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  26. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  27. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  28. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  30. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  32. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  34. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  35. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  36. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  37. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  38. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  40. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
  41. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  42. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  43. data/lib/contrast/agent/reporting/reporter.rb +11 -26
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  46. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  47. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  48. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
  49. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
  50. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  51. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  52. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  53. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
  54. data/lib/contrast/agent/request/request.rb +27 -12
  55. data/lib/contrast/agent/telemetry/base.rb +44 -19
  56. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  57. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  58. data/lib/contrast/agent/telemetry/client.rb +10 -2
  59. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  60. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  61. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  62. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  63. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  64. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  65. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  66. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/components/config/sources.rb +6 -5
  69. data/lib/contrast/components/config.rb +4 -4
  70. data/lib/contrast/components/protect.rb +11 -1
  71. data/lib/contrast/components/sampling.rb +15 -10
  72. data/lib/contrast/components/settings.rb +9 -0
  73. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  74. data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
  75. data/lib/contrast/config/diagnostics/tools.rb +4 -4
  76. data/lib/contrast/config/validate.rb +2 -2
  77. data/lib/contrast/config/yaml_file.rb +8 -0
  78. data/lib/contrast/configuration.rb +11 -19
  79. data/lib/contrast/framework/grape/support.rb +1 -2
  80. data/lib/contrast/framework/manager.rb +17 -8
  81. data/lib/contrast/framework/rack/support.rb +99 -1
  82. data/lib/contrast/framework/rails/support.rb +4 -2
  83. data/lib/contrast/framework/sinatra/support.rb +1 -2
  84. data/lib/contrast/logger/aliased_logging.rb +18 -9
  85. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  86. data/lib/contrast/utils/hash_utils.rb +21 -2
  87. data/lib/contrast/utils/metrics_hash.rb +1 -1
  88. data/lib/contrast/utils/object_share.rb +2 -1
  89. data/lib/contrast/utils/request_utils.rb +14 -0
  90. data/lib/contrast/utils/response_utils.rb +12 -0
  91. data/lib/contrast/utils/timer.rb +2 -0
  92. data/lib/contrast.rb +9 -2
  93. data/resources/assess/policy.json +11 -0
  94. data/ruby-agent.gemspec +1 -1
  95. metadata +25 -7
  96. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  97. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -54,6 +54,16 @@ module Contrast
54
54
  @_last_application_modified
55
55
  end
56
56
 
57
+ # Cease reporting about this application
58
+ #
59
+ # @param message [String] Message to log
60
+ # @param info_hash [Hash] information about the context to log.
61
+ def stop_reporting message, info_hash
62
+ Contrast::Agent.reporter&.stop!
63
+ log_error_msg(message, info_hash)
64
+ ::Contrast::AGENT.disable!
65
+ end
66
+
57
67
  private
58
68
 
59
69
  # check if response code is valid before analyze it
@@ -127,7 +137,7 @@ module Contrast
127
137
  # log, suspend, disable:
128
138
  if mode == @_mode.running
129
139
  log_error_msg(message,
130
- response: response.__id__,
140
+ response_id: response.__id__,
131
141
  request: Contrast::Agent::REQUEST_TRACKER.current&.request&.type,
132
142
  error_message: error_message || 'none',
133
143
  auth_error: auth_error || 'none')
@@ -176,16 +186,6 @@ module Contrast
176
186
  end
177
187
  end
178
188
 
179
- # Cease reporting about this application
180
- #
181
- # @param message [String] Message to log
182
- # @param info_hash [Hash] information about the context to log.
183
- def stop_reporting message, info_hash
184
- Contrast::Agent.reporter&.stop!
185
- log_error_msg(message, info_hash)
186
- ::Contrast::AGENT.disable!
187
- end
188
-
189
189
  # Applies the settings from the TS response
190
190
  #
191
191
  # @param response [Contrast::Agent::Reporting::Response]
@@ -10,6 +10,7 @@ require 'contrast/utils/hash_digest'
10
10
  require 'contrast/components/logger'
11
11
  require 'contrast/components/scope'
12
12
  require 'contrast/utils/request_utils'
13
+ require 'contrast/utils/duck_utils'
13
14
 
14
15
  module Contrast
15
16
  module Agent
@@ -24,12 +25,7 @@ module Contrast
24
25
 
25
26
  extend Forwardable
26
27
 
27
- INNER_REST_TOKEN = %r{/\d+/}.cs__freeze
28
- LAST_REST_TOKEN = %r{/\d+$}.cs__freeze
29
- INNER_NUMBER_MARKER = '/{n}/'
30
- LAST_NUMBER_MARKER = '/{n}'
31
- STATIC_SUFFIXES = /\.(?:js|css|jpeg|jpg|gif|png|ico|woff|svg|pdf|eot|ttf|jar)$/i.cs__freeze
32
- MEDIA_TYPE_MARKERS = %w[image/ text/css text/javascript].cs__freeze
28
+ EMPTY_PATH = '/'
33
29
 
34
30
  # @return [Rack::Request] The passed to the Agent RackRequest to be wrapped.
35
31
  attr_reader :rack_request
@@ -75,12 +71,31 @@ module Contrast
75
71
  def normalized_uri
76
72
  @_normalized_uri ||= begin
77
73
  path = rack_request.path_info || rack_request.path.to_s
78
- path = '/' if path.empty?
79
-
80
- uri = path.split(Contrast::Utils::ObjectShare::SEMICOLON)[0] # remove ;jsessionid
81
- uri = uri.split(Contrast::Utils::ObjectShare::QUESTION_MARK)[0] # remove ?query_string=
82
- uri.gsub(INNER_REST_TOKEN, INNER_NUMBER_MARKER) # replace interior tokens
83
- uri.gsub(LAST_REST_TOKEN, LAST_NUMBER_MARKER) # replace last token
74
+ path = EMPTY_PATH if Contrast::Utils::DuckUtils.empty_duck?(path)
75
+
76
+ # /foo/bar;jsessionid=123 => /foo/bar
77
+ uri = path.split(Contrast::Utils::ObjectShare::SEMICOLON)[0]
78
+ # /foo/bar?query_string=123 => /foo/bar
79
+ uri = uri.split(Contrast::Utils::ObjectShare::QUESTION_MARK)[0]
80
+
81
+ # Replace with tokens:
82
+ # NUM_ => '/{n}/'
83
+ # ID_ => '{ID}'
84
+ #
85
+ # replace UUIDs: /123e4567-e89b-42d3-a456-556642440000/ => /{ID}/
86
+ uri.gsub!(UUID_PATTERN, ID_)
87
+ # replace hash patterns: /6f1ed002ab5595859014ebf0951522d9/ => /{ID}/
88
+ uri.gsub!(HASH_PATTERN, ID_)
89
+ # replace windows SID: /S-1-5-21-1843332746-572796286-2118856591-1000/ => /{ID}/
90
+ uri.gsub!(WIN_PATTERN, ID_)
91
+ # replace interior number tokens: /123/ => /{n}/
92
+ uri.gsub!(NUM_PATTERN, NUM_)
93
+ # replace last number tokens: /123 => /{n}
94
+ uri.gsub!(END_PATTERN, NUM_[0..-2])
95
+ uri
96
+ rescue StandardError => e
97
+ logger.error('error normalizing uri', error: e, backtrace: e.backtrace)
98
+ EMPTY_PATH
84
99
  end
85
100
  end
86
101
 
@@ -8,6 +8,7 @@ require 'contrast/agent/thread/worker_thread'
8
8
  require 'contrast/agent/telemetry/telemetry'
9
9
  require 'contrast/agent/telemetry/exception'
10
10
  require 'contrast/utils/job_servers_running'
11
+ require 'contrast/agent/reporting/client/interface'
11
12
 
12
13
  module Contrast
13
14
  module Agent
@@ -96,11 +97,7 @@ module Contrast
96
97
  end
97
98
 
98
99
  def client
99
- @_client ||= Contrast::Agent::Telemetry::Client.new
100
- end
101
-
102
- def connection
103
- @_connection ||= client.initialize_connection(URL)
100
+ @_client ||= Contrast::Agent::Reporting::Telemetry::Interface.new
104
101
  end
105
102
 
106
103
  def attempt_to_start?
@@ -146,10 +143,8 @@ module Contrast
146
143
  super
147
144
  delete_queue!
148
145
  Contrast::TELEMETRY_EXCEPTIONS&.clear
149
- end
150
-
151
- def request_with_response event
152
- client.handle_response(client.send_request(event, connection))
146
+ Contrast::TELEMETRY_IA_CACHE&.clear
147
+ Contrast::TELEMETRY_BASE64_HASH&.clear
153
148
  end
154
149
 
155
150
  private
@@ -158,6 +153,18 @@ module Contrast
158
153
  @_queue ||= Queue.new
159
154
  end
160
155
 
156
+ # Starts sending Telemetry messages.
157
+ def process_event event
158
+ logger.debug('[Telemetry] This is the current processed event', event)
159
+ if (sleep_time = client.request_with_response(event))
160
+ return sleep(sleep_time)
161
+ end
162
+
163
+ logger.debug('[Telemetry] Retrying to process event', event)
164
+ retry_sleep_time = client.request_with_response(event)
165
+ sleep(retry_sleep_time) unless retry_sleep_time.nil?
166
+ end
167
+
161
168
  # It is recommended that implementations send a single payload of general metrics every 3 hours, starting from
162
169
  # implementation startup. This returns a thread configured to do so.
163
170
  #
@@ -165,22 +172,15 @@ module Contrast
165
172
  def create_thread
166
173
  Contrast::Agent::Thread.new do
167
174
  loop do
168
- next unless client && connection
175
+ next unless client.connected?
169
176
  break unless attempt_to_start?
170
177
 
171
178
  # Start pushing exceptions to queue for reporting.
172
- Contrast::TELEMETRY_EXCEPTIONS.each_value { |value| queue << value }
173
- Contrast::TELEMETRY_EXCEPTIONS.clear
179
+ gather_telemetry_events
174
180
  until queue.empty?
175
181
  event = queue.pop
176
182
  begin
177
- logger.debug('[Telemetry] This is the current processed event', event)
178
- if (sleep_time = request_with_response(event))
179
- sleep(sleep_time)
180
- logger.debug('[Telemetry] Retrying to process event', event)
181
- retry_sleep_time = request_with_response(event)
182
- sleep(retry_sleep_time) unless retry_sleep_time.nil?
183
- end
183
+ process_event(event)
184
184
  rescue StandardError => e
185
185
  logger.error('[Telemetry] Could not send message to service from telemetry queue.', e)
186
186
  stop!
@@ -190,6 +190,31 @@ module Contrast
190
190
  end
191
191
  end
192
192
  end
193
+
194
+ # Fills the queue with events that were not able to be sent previously.
195
+ def gather_telemetry_events
196
+ gather_exceptions
197
+ gather_encoding_events
198
+ gather_ia_cache_events
199
+ end
200
+
201
+ # Retrieves the exceptions that were accumulated.
202
+ def gather_exceptions
203
+ Contrast::TELEMETRY_EXCEPTIONS&.each_value { |value| queue << value }
204
+ Contrast::TELEMETRY_EXCEPTIONS&.clear
205
+ end
206
+
207
+ # Retrieves the base64 encoded events that were accumulated.
208
+ def gather_encoding_events
209
+ Contrast::TELEMETRY_BASE64_HASH&.each_value { |values| values.each { |event| queue << event } }
210
+ Contrast::TELEMETRY_BASE64_HASH&.clear
211
+ end
212
+
213
+ # Retrieves the IA cache events that were accumulated.
214
+ def gather_ia_cache_events
215
+ Contrast::TELEMETRY_IA_CACHE&.each_value { |values| values.each { |event| queue << event } }
216
+ Contrast::TELEMETRY_IA_CACHE&.clear
217
+ end
193
218
  end
194
219
  end
195
220
  end
@@ -0,0 +1,55 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/telemetry/input_analysis_encoding_event'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Telemetry
9
+ # This hash will store the telemetry data for the Protect InputAnalysis cache.
10
+ class Base64Hash < Hash
11
+ include Contrast::Components::Logger::InstanceMethods
12
+ # Set per request:
13
+ HASH_SIZE_LIMIT = 100
14
+
15
+ # Wrapper to set a value in this Telemetry::Hash only if the provided value is of the data_type for this
16
+ # Telemetry::CacheHash or the hash has not reached its limit for unique keys.
17
+ # Saves Array of reportable events.
18
+ #
19
+ # @param key [Object] the key to which to associate the value
20
+ # @param events [array<Object>]
21
+ # @return [Object, nil] echo back out the value as the Hash#[]= method does, or nil if not of the expected
22
+ # data_type
23
+ def []= key, events
24
+ # If telemetry is not running, do not add more as we want to avoid a memory leak.
25
+ return unless Contrast::Agent.telemetry_queue&.running?
26
+ # If the Hash is full, do not add more as we want to avoid consuming all application resources.
27
+ return if at_limit?
28
+ # If the given value is of unexpected type, do not add it to avoid issues later where type is assumed.
29
+ return unless valid_event?(events)
30
+
31
+ super(key, events)
32
+ end
33
+
34
+ # Determine if hash has reached exception event limit.
35
+ #
36
+ # @return [Boolean]
37
+ def at_limit?
38
+ unless length < HASH_SIZE_LIMIT
39
+ logger.debug("[Telemetry] Number of IA base64 events exceeds limit of #{ HASH_SIZE_LIMIT }")
40
+ return true
41
+ end
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ # Checks to see if the given object is a valid event.
48
+ # @param events [Contrast::Agent::Telemetry::InputAnalysisEncodingEvent]
49
+ def valid_event? events
50
+ events&.all?(Contrast::Agent::Telemetry::InputAnalysisEncodingEvent)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/telemetry/input_analysis_cache_event'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Telemetry
9
+ # This hash will store the telemetry data for the Protect InputAnalysis cache.
10
+ class CacheHash < Hash
11
+ include Contrast::Components::Logger::InstanceMethods
12
+ # Set per request:
13
+ HASH_SIZE_LIMIT = 100
14
+
15
+ # Wrapper to set a value in this Telemetry::Hash only if the provided value is of the data_type for this
16
+ # Telemetry::CacheHash or the hash has not reached its limit for unique keys.
17
+ # Saves Array of reportable events.
18
+ #
19
+ # @param key [Object] the key to which to associate the value
20
+ # @param events [array<Object>]
21
+ # @return [Object, nil] echo back out the value as the Hash#[]= method does, or nil if not of the expected
22
+ # data_type
23
+ def []= key, events
24
+ # If telemetry is not running, do not add more as we want to avoid a memory leak.
25
+ return unless Contrast::Agent.telemetry_queue&.running?
26
+ # If the Hash is full, do not add more as we want to avoid consuming all application resources.
27
+ return if at_limit?
28
+ # If the given value is of unexpected type, do not add it to avoid issues later where type is assumed.
29
+ return unless valid_event?(events)
30
+
31
+ super(key, events)
32
+ end
33
+
34
+ # Determine if hash has reached exception event limit.
35
+ #
36
+ # @return [Boolean]
37
+ def at_limit?
38
+ unless length < HASH_SIZE_LIMIT
39
+ logger.debug("[Telemetry] Number of IA cache events exceeds limit of #{ HASH_SIZE_LIMIT }")
40
+ return true
41
+ end
42
+ false
43
+ end
44
+
45
+ private
46
+
47
+ # Checks to see if the given object is a valid event.
48
+ # @param event [Contrast::Agent::Telemetry::InputAnalysisCacheEvent]
49
+ def valid_event? events
50
+ events&.all?(Contrast::Agent::Telemetry::InputAnalysisCacheEvent)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -65,6 +65,7 @@ module Contrast
65
65
  else
66
66
  60
67
67
  end
68
+ logger.debug('[Telemetry] received response.', response_code: status_code)
68
69
  ready_after if status_code == 429
69
70
  end
70
71
 
@@ -76,6 +77,8 @@ module Contrast
76
77
  return true if event.cs__is_a?(Contrast::Agent::Telemetry::Event)
77
78
  return true if event.cs__is_a?(Contrast::Agent::Telemetry::StartupMetricsEvent)
78
79
  return true if event.cs__is_a?(Contrast::Agent::Telemetry::Exception::Event)
80
+ return true if event.cs__is_a?(Contrast::Agent::Telemetry::InputAnalysisCacheEvent)
81
+ return true if event.cs__is_a?(Contrast::Agent::Telemetry::InputAnalysisEncodingEvent)
79
82
 
80
83
  false
81
84
  end
@@ -93,12 +96,17 @@ module Contrast
93
96
  "#{ Contrast::Agent::Telemetry::Base::URL }#{ endpoint }#{ path }"
94
97
  end
95
98
 
96
- # Helper Method to get json representation of Telemetry Event data, handles error on to_json
99
+ # Helper Method to get json representation of Telemetry Event data, handles error on to_json.
100
+ # Generating bodies for exceptions and startup metrics is different.
97
101
  #
98
102
  # @param event [Contrast::Agent::Telemetry::Event, Array<Contrast::Agent::Telemetry::Exception::Event>]
99
103
  # @return [String] - JSON
100
104
  def get_event_json event
101
- Array(event.to_controlled_hash).to_json
105
+ if event.cs__is_a?(Contrast::Agent::Telemetry::Exception::Event)
106
+ return Array(event.to_controlled_hash).to_json
107
+ end
108
+
109
+ [event.to_controlled_hash].to_json
102
110
  rescue Exception => e # rubocop:disable Lint/RescueException
103
111
  logger.error('[Telemetry] Unable to convert TelemetryEvent to JSON string', e, hsh)
104
112
  raise(e)
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/duck_utils'
5
+ require 'contrast/utils/object_share'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Telemetry
10
+ module Exception
11
+ # Obfuscate sensitive user data before building exception.
12
+ module Obfuscate
13
+ CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'.cs__freeze
14
+ # This will generate different chars for each agent startup but will be constant
15
+ # for current run and will make identical obfuscation to be compared if given type
16
+ # is the same.
17
+ CYPHER = CHARS.chars.shuffle.join.cs__freeze
18
+ VERSION_MATCH = '[^0-9].-'
19
+
20
+ # List of known places after witch a user name might appear:
21
+ KNOWN_DIRS = %w[app application project projects git github users home user].cs__freeze
22
+
23
+ class << self
24
+ # Returns paths for known gems.
25
+ #
26
+ # @return [Array<Regexp>] known paths
27
+ def known_paths
28
+ @_known_paths ||= KNOWN_DIRS.map do |name|
29
+ to_regexp(name)
30
+ end
31
+ end
32
+
33
+ # Obfuscate a type and replace it with random characters.
34
+ #
35
+ # @param path [String] the StackFrame type to obfuscate
36
+ # @return [String] obfuscated type
37
+ def obfuscate_path path
38
+ return path if Contrast::Utils::DuckUtils.empty_duck?(path)
39
+
40
+ cypher(path)
41
+ end
42
+
43
+ private
44
+
45
+ # Transforms string to regexp
46
+ #
47
+ # @param string [String]
48
+ def to_regexp string
49
+ /#{ string }/
50
+ end
51
+
52
+ # Add cypher to path to make it obscure, but unique enough for
53
+ # comparisons. Mutates original or duplicate if frozen string.
54
+ #
55
+ # @param string [String] string to be transformed.
56
+ # @return [String]
57
+ def cypher string
58
+ cypher = string.cs__frozen? ? string.dup : string
59
+ dirs = cypher.split(Contrast::Utils::ObjectShare::SLASH)
60
+ return string unless dirs.cs__is_a?(Array)
61
+
62
+ dirs.each_with_index do |name, idx|
63
+ next if Contrast::Utils::DuckUtils.empty_duck?(name)
64
+ next unless match_known(known_paths,
65
+ name.tr(VERSION_MATCH, Contrast::Utils::ObjectShare::EMPTY_STRING).downcase)
66
+
67
+ obscure(name)
68
+ # obscure username (next dir in line)
69
+ obscure(dirs[idx + 1]) if dirs[idx + 1]
70
+ end
71
+ cypher = dirs.join(Contrast::Utils::ObjectShare::SLASH)
72
+ return cypher if cypher
73
+
74
+ Contrast::Utils::ObjectShare::EMPTY_STRING
75
+ rescue StandardError
76
+ Contrast::Utils::ObjectShare::EMPTY_STRING
77
+ end
78
+
79
+ # @param known [Array<Regexp>] Array of regexp to match against
80
+ # @param type [String] type to check
81
+ def match_known known, type
82
+ known.any? { |regexp| type =~ regexp }
83
+ end
84
+
85
+ # Replaces chars in name.
86
+ #
87
+ # @param [string] name
88
+ # @return [String, nil]
89
+ def obscure name
90
+ name.to_s.tr!(CHARS, CYPHER)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -17,3 +17,4 @@ require 'contrast/agent/telemetry/exception/stack_frame'
17
17
  require 'contrast/agent/telemetry/exception/message_exception'
18
18
  require 'contrast/agent/telemetry/exception/message'
19
19
  require 'contrast/agent/telemetry/exception/event'
20
+ require 'contrast/agent/telemetry/exception/obfuscate'
@@ -10,7 +10,7 @@ module Contrast
10
10
  # This is the Telemetry::Hash, which will store Contrast::Agent::Telemetry::Exception::Event, so we can push
11
11
  # freely, without worrying about validating the event before that. Telemetry::Hash has a max size of events,
12
12
  # default is 10 events
13
- class Hash < Hash
13
+ class ExceptionHash < Hash
14
14
  include Contrast::Components::Logger::InstanceMethods
15
15
  HASH_SIZE_LIMIT = 10
16
16
 
@@ -0,0 +1,27 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/telemetry/input_analysis_event'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Telemetry
9
+ # Event to report all gather information from the Input Analysis Cache statistics, hits and misses.
10
+ class InputAnalysisCacheEvent < Contrast::Agent::Telemetry::InputAnalysisEvent
11
+ NAME = 'InputAnalysis cache event'
12
+ PATH = '/protect_input_analysis_cache'
13
+
14
+ private
15
+
16
+ # Creates the tags for the event
17
+ #
18
+ # @param rule_id [String]
19
+ # @param match_rates [Contrast::Agent::Protect::Rule::InputClassification::MatchRates]
20
+ def add_tags rule_id, match_rates
21
+ super(rule_id, match_rates)
22
+ @tags['score_level'] = match_rates&.score_level.dup&.to_s || NOT_APPLICABLE
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/telemetry/input_analysis_event'
5
+
6
+ module Contrast
7
+ module Agent
8
+ module Telemetry
9
+ # Event to report all gather information from the Input Analysis Cache statistics, hits and misses.
10
+ class InputAnalysisEncodingEvent < Contrast::Agent::Telemetry::InputAnalysisEvent
11
+ NAME = 'InputAnalysis encoding event'
12
+ PATH = '/protect_input_analysis_encoding'
13
+
14
+ private
15
+
16
+ # Creates the tags for the event
17
+ #
18
+ # @param _rule_id [String]
19
+ # @param encode_rates [Contrast::Agent::Protect::Rule::InputClassification::EncodingRates]
20
+ def add_tags _rule_id, encode_rates
21
+ super(nil, encode_rates)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,91 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/utils/metrics_hash'
5
+ require 'contrast/agent/telemetry/metric_event'
6
+ require 'contrast/agent/version'
7
+ require 'contrast/components/logger'
8
+
9
+ module Contrast
10
+ module Agent
11
+ module Telemetry
12
+ # Event to report all gather information from the Input Analysis metrics.
13
+ class InputAnalysisEvent < Contrast::Agent::Telemetry::MetricEvent
14
+ include Contrast::Components::Logger::InstanceMethods
15
+ NOT_APPLICABLE = 'n/a'
16
+
17
+ attr_reader :fields
18
+
19
+ # Override the name for any derived classes
20
+ NAME = 'InputAnalysis event'
21
+ # Override the path for any derived classes
22
+ PATH = '/protect_input_analysis'
23
+
24
+ # @param rule_id [String] the rule name.
25
+ # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates] base class for all rates.
26
+ def initialize rule_id, rates
27
+ super()
28
+ @fields = MetricsHash.new(Integer)
29
+ add_tags(rule_id, rates)
30
+ generate_fields(rates)
31
+ end
32
+
33
+ # Returns the name of the event.
34
+ #
35
+ # @return [String]
36
+ def name
37
+ cs__class::NAME
38
+ end
39
+
40
+ # Returns the path to report the event.
41
+ #
42
+ # @return [String]
43
+ def path
44
+ cs__class::PATH
45
+ end
46
+
47
+ # Override the empty check for any derived classes if needed.
48
+ def empty?
49
+ super && Contrast::Utils::DuckUtils.empty_duck?(@tags)
50
+ end
51
+
52
+ private
53
+
54
+ # Creates the tags for the event. Overrides the base class to add the rule_id and match_rate.
55
+ #
56
+ # @param rule_id [String, nil]
57
+ # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates]
58
+ def add_tags rule_id, rates
59
+ return if Contrast::Utils::DuckUtils.empty_duck?(rates)
60
+ return unless rates.cs__is_a?(Contrast::Agent::Protect::Rule::InputClassification::Rates)
61
+
62
+ @tags['event_type'] = name # rubocop:disable Security/Module/Name
63
+ @tags['test_environment'] = ENV['CONTRAST_AGENT_TELEMETRY_TEST'] == '1' ? 'true' : 'false'
64
+ @tags['rule_id'] = rule_id if rule_id
65
+ @tags['input_type'] = rates&.input_type.dup&.to_s || NOT_APPLICABLE
66
+ add_system_tags
67
+ end
68
+
69
+ # Creates the fields for the event.
70
+ # Override if needed.
71
+ #
72
+ # @param rates [Contrast::Agent::Protect::Rule::InputClassification::Rates] base class for all rates.
73
+ def generate_fields rates
74
+ return if Contrast::Utils::DuckUtils.empty_duck?(rates)
75
+ return unless rates.cs__is_a?(Contrast::Agent::Protect::Rule::InputClassification::Rates)
76
+
77
+ rates.to_fields.each { |field, value| @fields[field] = value.dup }
78
+ end
79
+
80
+ # Adds the system tags to the event.
81
+ def add_system_tags
82
+ @tags['agent_version'] = VERSION
83
+ @tags['ruby_version'] = RUBY_VERSION
84
+ @tags['os_type'] = sys_info['os_type'] == 'Darwin' ? 'MacOS' : 'Linux'
85
+ @tags['os_arch'] = sys_info['os_arch']
86
+ @tags['os_version'] = sys_info['os_version']
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -3,6 +3,8 @@
3
3
 
4
4
  require 'contrast/utils/metrics_hash'
5
5
  require 'contrast/agent/telemetry/event'
6
+ require 'contrast/utils/duck_utils'
7
+ require 'contrast/utils/os'
6
8
 
7
9
  module Contrast
8
10
  module Agent
@@ -10,6 +12,7 @@ module Contrast
10
12
  # This class will hold the basic information for a Telemetry Event
11
13
  class MetricEvent < Contrast::Agent::Telemetry::Event
12
14
  include Contrast::Utils
15
+ include Contrast::Utils::OS
13
16
 
14
17
  attr_reader :fields
15
18
 
@@ -19,6 +22,15 @@ module Contrast
19
22
  @fields['_filler'] = 0
20
23
  end
21
24
 
25
+ def sys_info
26
+ @sys_info ||= get_system_information if @sys_info.nil?
27
+ @sys_info
28
+ end
29
+
30
+ def empty?
31
+ Contrast::Utils::DuckUtils.empty_duck?(@fields)
32
+ end
33
+
22
34
  def to_controlled_hash **_args
23
35
  super.merge!({ fields: @fields })
24
36
  end