contrast-agent 7.2.0 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/assess/policy/policy_node.rb +25 -6
  3. data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
  4. data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
  5. data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
  6. data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
  7. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
  8. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  9. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  10. data/lib/contrast/agent/protect/rule/base.rb +5 -1
  11. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
  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/input_classification/base.rb +191 -0
  15. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  25. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  26. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  27. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  28. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  29. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  30. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  31. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  32. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
  33. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  34. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  35. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  36. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  37. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
  38. data/lib/contrast/agent/telemetry/base.rb +28 -2
  39. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  40. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  41. data/lib/contrast/agent/telemetry/client.rb +10 -2
  42. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
  43. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  44. data/lib/contrast/agent/telemetry/identifier.rb +13 -26
  45. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  46. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  47. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  48. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  49. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/components/assess.rb +33 -6
  52. data/lib/contrast/components/base.rb +4 -2
  53. data/lib/contrast/components/config.rb +6 -6
  54. data/lib/contrast/components/protect.rb +11 -1
  55. data/lib/contrast/components/sampling.rb +15 -10
  56. data/lib/contrast/config/diagnostics/command_line.rb +2 -2
  57. data/lib/contrast/config/diagnostics/environment_variables.rb +5 -2
  58. data/lib/contrast/config/diagnostics/tools.rb +15 -5
  59. data/lib/contrast/config/yaml_file.rb +8 -0
  60. data/lib/contrast/configuration.rb +61 -29
  61. data/lib/contrast/framework/rails/support.rb +3 -0
  62. data/lib/contrast/logger/application.rb +3 -3
  63. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  64. data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
  65. data/lib/contrast/utils/metrics_hash.rb +1 -1
  66. data/lib/contrast/utils/object_share.rb +2 -1
  67. data/lib/contrast/utils/os.rb +1 -9
  68. data/lib/contrast/utils/response_utils.rb +12 -0
  69. data/lib/contrast/utils/timer.rb +2 -0
  70. data/lib/contrast.rb +9 -2
  71. data/resources/assess/policy.json +80 -3
  72. data/ruby-agent.gemspec +1 -1
  73. metadata +22 -6
  74. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -1,7 +1,7 @@
1
1
  # Copyright (c) 2023 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/utils/input_classification_base'
4
+ require 'contrast/agent/protect/rule/input_classification/base'
5
5
 
6
6
  module Contrast
7
7
  module Agent
@@ -13,28 +13,32 @@ module Contrast
13
13
  REFLECTED_XSS_MATCH = 'reflected-xss-input-tracing-v1'.cs__freeze
14
14
  WORTHWATCHING_MATCH = 'xss-worth-watching-v2'.cs__freeze
15
15
  class << self
16
- include InputClassificationBase
16
+ include Contrast::Agent::Protect::Rule::InputClassification::Base
17
17
 
18
18
  private
19
19
 
20
- # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
21
- # key if needed and Creates new isntance of InputAnalysisResult.
20
+ # Creates new instance of AgentLib evaluation result with direct call to AgentLib.
22
21
  #
23
- # @param request [Contrast::Agent::Request] the current request context.
24
22
  # @param rule_id [String] The name of the Protect Rule.
25
23
  # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
26
24
  # @param value [String, Array<String>] the value of the input.
27
- #
28
- # @return res [Contrast::Agent::Reporting::InputAnalysisResult]
29
- def create_new_input_result request, rule_id, input_type, value
30
- return unless Contrast::AGENT_LIB
31
-
32
- input_eval = Contrast::AGENT_LIB.eval_input(value,
33
- convert_input_type(input_type),
34
- Contrast::AGENT_LIB.rule_set[rule_id],
35
- Contrast::AGENT_LIB.
36
- eval_option[:PREFER_WORTH_WATCHING])
25
+ def build_input_eval rule_id, input_type, value
26
+ Contrast::AGENT_LIB.eval_input(value,
27
+ Contrast::Agent::Protect::Rule::InputClassification::Base.
28
+ convert_input_type(input_type),
29
+ Contrast::AGENT_LIB.rule_set[rule_id],
30
+ Contrast::AGENT_LIB.
31
+ eval_option[:PREFER_WORTH_WATCHING])
32
+ end
37
33
 
34
+ # Creates specific result from the AgentLib evaluation.
35
+ #
36
+ # @param rule_id [String] The name of the Protect Rule.
37
+ # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
38
+ # @param value [String, Array<String>] the value of the input.
39
+ # @param request [Contrast::Agent::Request] the current request context.
40
+ # @param input_eval [Contrast::AgentLib::EvalResult] the result of the input evaluation.
41
+ def build_ia_result rule_id, input_type, value, request, input_eval
38
42
  score = input_eval&.score || 0
39
43
  ia_result = new_ia_result(rule_id, input_type, request.path, value)
40
44
  if score >= THRESHOLD
@@ -46,8 +50,6 @@ module Contrast
46
50
  else
47
51
  ia_result.score_level = IGNORE
48
52
  end
49
-
50
- add_needed_key(request, ia_result, input_type, value)
51
53
  ia_result
52
54
  end
53
55
  end
@@ -5,6 +5,7 @@ require 'contrast/utils/object_share'
5
5
  require 'contrast/utils/timer'
6
6
  require 'contrast/agent/reporting/attack_result/response_type'
7
7
  require 'contrast/agent/reporting/attack_result/rasp_rule_sample'
8
+ require 'contrast/utils/duck_utils'
8
9
 
9
10
  module Contrast
10
11
  module Agent
@@ -65,6 +66,11 @@ module Contrast
65
66
  def details= protect_details
66
67
  @_details = protect_details if protect_details.is_a?(Contrast::Agent::Reporting::Details::ProtectRuleDetails)
67
68
  end
69
+
70
+ def empty?
71
+ Contrast::Utils::DuckUtils.empty_duck?(samples) || Contrast::Utils::DuckUtils.empty_duck?(rule_id) ||
72
+ response == ::Contrast::Agent::Reporting::ResponseType::NO_ACTION
73
+ end
68
74
  end
69
75
  end
70
76
  end
@@ -9,13 +9,8 @@ module Contrast
9
9
  module Reporting
10
10
  # This class will do ia analysis for our protect rules
11
11
  class InputAnalysis
12
- def inputs
13
- @_inputs
14
- end
15
-
16
- def inputs= extracted_inputs
17
- @_inputs = extracted_inputs
18
- end
12
+ # @return [Hash] Stored request inputs for this context.
13
+ attr_accessor :inputs
19
14
 
20
15
  def triggered_rules
21
16
  @_triggered_rules ||= []
@@ -61,6 +61,17 @@ module Contrast
61
61
  @_key = key if key.is_a?(String)
62
62
  end
63
63
 
64
+ # @param key_type [Symbol]
65
+ # @return @_key [String]
66
+ def key_type= key_type
67
+ @_key_type = key_type if INPUT_TYPE.to_a.include?(key_type)
68
+ end
69
+
70
+ # @return @_key [String]
71
+ def key_type
72
+ @_key_type ||= INPUT_TYPE::UNDEFINED_TYPE
73
+ end
74
+
64
75
  # @return value [String]
65
76
  def value
66
77
  @_value ||= Contrast::Utils::ObjectShare::EMPTY_STRING
@@ -30,13 +30,45 @@ module Contrast
30
30
  UNKNOWN = :UNKNOWN.cs__freeze
31
31
 
32
32
  class << self
33
+ # @return
33
34
  def to_a
34
- [
35
+ @_to_a ||= [
35
36
  UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
36
37
  QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
37
38
  MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
38
39
  ]
39
40
  end
41
+
42
+ # This is a hash of the input types and their corresponding values.
43
+ #
44
+ # @return [Hash]
45
+
46
+ def to_hash
47
+ {
48
+ UNDEFINED_TYPE: '1',
49
+ BODY: '2',
50
+ COOKIE_NAME: '3',
51
+ COOKIE_VALUE: '4',
52
+ HEADER: '5',
53
+ PARAMETER_NAME: '6',
54
+ PARAMETER_VALUE: '7',
55
+ QUERYSTRING: '8',
56
+ URI: '9',
57
+ SOCKET: '10',
58
+ JSON_VALUE: '11',
59
+ JSON_ARRAYED_VALUE: '12',
60
+ MULTIPART_CONTENT_TYPE: '13',
61
+ MULTIPART_VALUE: '14',
62
+ MULTIPART_FIELD_NAME: '15',
63
+ MULTIPART_NAME: '16',
64
+ XML_VALUE: '17',
65
+ DWR_VALUE: '18',
66
+ METHOD: '19',
67
+ REQUEST: '20',
68
+ URL_PARAMETER: '21',
69
+ UNKNOWN: '22'
70
+ }
71
+ end
40
72
  end
41
73
  end
42
74
  end
@@ -23,7 +23,7 @@ module Contrast
23
23
  hash = URI.decode_www_form(query).to_h
24
24
  mask_with_dictionary(results, hash)
25
25
  # Restore to string form.
26
- hash.each { |k, v| masked += "#{ k }=#{ v }&" }
26
+ hash.each { |k, v| masked += "#{ k }#{ EQUALS }#{ v }#{ AMPERSAND }" }
27
27
  query = masked
28
28
  query.chomp!(masked[-1])
29
29
  end
@@ -40,6 +40,7 @@ module Contrast
40
40
  # @param attack_result [Contrast::Agent::Reporting::AttackResult]
41
41
  def attach_data attack_result
42
42
  return unless attack_result&.cs__is_a?(Contrast::Agent::Reporting::AttackResult)
43
+ return if attack_result&.empty?
43
44
 
44
45
  attacker_activity = Contrast::Agent::Reporting::ApplicationDefendAttackerActivity.new(ia_request: @request)
45
46
  attacker_activity.attach_data(attack_result)
@@ -5,6 +5,7 @@ require 'contrast/components/logger'
5
5
  require 'contrast/utils/object_share'
6
6
  require 'contrast/utils/duck_utils'
7
7
  require 'contrast/agent/reporting/reporting_events/reportable_hash'
8
+ require 'contrast/agent/reporting/attack_result/response_type'
8
9
  require 'contrast/agent/reporting/reporting_events/application_defend_attack_activity'
9
10
 
10
11
  module Contrast
@@ -118,7 +118,7 @@ module Contrast
118
118
  mode.resend.reset_rescue_attempts
119
119
  findings_to_return.each do |index|
120
120
  preflight_message = event.messages[index.to_i]
121
- corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message.data)
121
+ corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
122
122
  next unless corresponding_finding
123
123
 
124
124
  send_event(corresponding_finding, connection)
@@ -143,6 +143,8 @@ module Contrast
143
143
  super
144
144
  delete_queue!
145
145
  Contrast::TELEMETRY_EXCEPTIONS&.clear
146
+ Contrast::TELEMETRY_IA_CACHE&.clear
147
+ Contrast::TELEMETRY_BASE64_HASH&.clear
146
148
  end
147
149
 
148
150
  private
@@ -174,8 +176,7 @@ module Contrast
174
176
  break unless attempt_to_start?
175
177
 
176
178
  # Start pushing exceptions to queue for reporting.
177
- Contrast::TELEMETRY_EXCEPTIONS&.each_value { |value| queue << value }
178
- Contrast::TELEMETRY_EXCEPTIONS&.clear
179
+ gather_telemetry_events
179
180
  until queue.empty?
180
181
  event = queue.pop
181
182
  begin
@@ -189,6 +190,31 @@ module Contrast
189
190
  end
190
191
  end
191
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
192
218
  end
193
219
  end
194
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)
@@ -16,9 +16,10 @@ module Contrast
16
16
  # is the same.
17
17
  CYPHER = CHARS.chars.shuffle.join.cs__freeze
18
18
  VERSION_MATCH = '[^0-9].-'
19
+ RUBY_EXT = /\.(?:rb|gemspec)$/i
19
20
 
20
21
  # 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
+ KNOWN_DIRS = %w[app application lib project projects git github users home user].cs__freeze
22
23
 
23
24
  class << self
24
25
  # Returns paths for known gems.
@@ -65,8 +66,8 @@ module Contrast
65
66
  name.tr(VERSION_MATCH, Contrast::Utils::ObjectShare::EMPTY_STRING).downcase)
66
67
 
67
68
  obscure(name)
68
- # obscure username (next dir in line)
69
- obscure(dirs[idx + 1]) if dirs[idx + 1]
69
+ # obscure username (next dir in line), skip if it's a file name.
70
+ obscure(dirs[idx + 1]) if dirs[idx + 1] && (dirs[idx + 1] !~ RUBY_EXT)
70
71
  end
71
72
  cypher = dirs.join(Contrast::Utils::ObjectShare::SLASH)
72
73
  return cypher if cypher
@@ -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
 
@@ -12,7 +12,7 @@ module Contrast
12
12
  # Gets info about the instrumented application required to build unique identifiers,
13
13
  # used in the agent's Telemetry.
14
14
  module Identifier
15
- MAC_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.cs__freeze
15
+ MAC_REGEXP = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.cs__freeze
16
16
  LINUX_OS_REG = /hwaddr=.*?(([A-F0-9]{2}:){5}[A-F0-9]{2})/im.cs__freeze
17
17
  MAC_OS_PRIMARY = 'en0'.cs__freeze
18
18
  LINUX_PRIMARY = 'enp'.cs__freeze
@@ -87,7 +87,7 @@ module Contrast
87
87
  mac = retrieve_mac(addr)
88
88
  next unless mac
89
89
 
90
- result = mac if mac&.match?(MAC_REGEX)
90
+ result = mac if mac.match?(MAC_REGEXP)
91
91
  break if result
92
92
  end
93
93
  result
@@ -118,33 +118,20 @@ module Contrast
118
118
  nil
119
119
  end
120
120
 
121
- # Returns array of network interfaces.
122
- # This is OS dependent search.
121
+ # Returns array of network interfaces belonging to the expected pfamily of this OS.
123
122
  #
124
- # @return interfaces [Array] Returns an array of interface addresses.
125
- # Socket::Ifaddr - represents a result of getifaddrs().
123
+ # @return interfaces [Array<Socket::Ifaddr>]
126
124
  def interfaces
127
- @_interfaces ||= []
128
-
129
- return @_interfaces unless @_interfaces.empty?
125
+ @_interfaces ||= Socket.getifaddrs.select { |interface| interface.addr&.pfamily == check_family }
126
+ end
130
127
 
131
- arr = Socket.getifaddrs
132
- idx = 0
133
- check_family = 0
134
- while idx < arr.length
135
- # We need only network adapters MACs. Checking for pfamily of every socket address:
136
- # 18 for Mac OS and 17 for Linux.
137
- # family should be an address family such as: :INET, :INET6, :UNIX, etc.
138
- check_family = 18 if Contrast::Utils::OS.mac?
139
- check_family = 17 if Contrast::Utils::OS.linux?
140
- if arr[idx].addr.pfamily != check_family
141
- idx += 1
142
- next
143
- end
144
- @_interfaces << arr[idx]
145
- idx += 1
146
- end
147
- @_interfaces
128
+ # We need only network adapters MACs. Checking for pfamily of every socket address:
129
+ # 18 for Mac OS and 17 for Linux. Family should be an address family such as: :INET, :INET6, :UNIX, etc.
130
+ # It corresponds to the Addrinfo.pfamily value.
131
+ #
132
+ # @return [Integer]
133
+ def check_family
134
+ @_check_family ||= Contrast::Utils::OS.mac? ? 18 : 17
148
135
  end
149
136
  end
150
137
  end
@@ -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