contrast-agent 4.13.1 → 4.14.0

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +1 -0
  3. data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
  4. data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
  5. data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
  6. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
  7. data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
  8. data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
  9. data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
  10. data/lib/contrast/agent/assess/property/tagged.rb +51 -57
  11. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
  12. data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
  13. data/lib/contrast/agent/middleware.rb +5 -75
  14. data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
  15. data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
  16. data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
  17. data/lib/contrast/agent/reporting/report.rb +21 -0
  18. data/lib/contrast/agent/reporting/reporter.rb +142 -0
  19. data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
  20. data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
  21. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
  22. data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
  23. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
  24. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
  25. data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
  26. data/lib/contrast/agent/request.rb +2 -81
  27. data/lib/contrast/agent/request_context.rb +4 -128
  28. data/lib/contrast/agent/request_context_extend.rb +138 -0
  29. data/lib/contrast/agent/response.rb +2 -73
  30. data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
  31. data/lib/contrast/agent/static_analysis.rb +1 -1
  32. data/lib/contrast/agent/telemetry.rb +15 -7
  33. data/lib/contrast/agent/telemetry_event.rb +8 -9
  34. data/lib/contrast/agent/thread_watcher.rb +31 -5
  35. data/lib/contrast/agent/version.rb +1 -1
  36. data/lib/contrast/agent.rb +15 -0
  37. data/lib/contrast/api/communication/connection_status.rb +10 -7
  38. data/lib/contrast/api/communication/messaging_queue.rb +37 -3
  39. data/lib/contrast/api/communication/response_processor.rb +15 -8
  40. data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
  41. data/lib/contrast/api/communication/socket.rb +6 -8
  42. data/lib/contrast/api/communication/socket_client.rb +29 -12
  43. data/lib/contrast/api/communication/speedracer.rb +37 -1
  44. data/lib/contrast/api/communication/tcp_socket.rb +4 -3
  45. data/lib/contrast/api/communication/unix_socket.rb +1 -0
  46. data/lib/contrast/api/decorators/finding.rb +45 -0
  47. data/lib/contrast/components/api.rb +56 -0
  48. data/lib/contrast/components/app_context.rb +10 -65
  49. data/lib/contrast/components/app_context_extend.rb +78 -0
  50. data/lib/contrast/components/base.rb +23 -0
  51. data/lib/contrast/components/config.rb +8 -8
  52. data/lib/contrast/components/contrast_service.rb +5 -0
  53. data/lib/contrast/components/sampling.rb +2 -2
  54. data/lib/contrast/config/agent_configuration.rb +1 -1
  55. data/lib/contrast/config/api_configuration.rb +9 -4
  56. data/lib/contrast/config/api_proxy_configuration.rb +14 -0
  57. data/lib/contrast/config/application_configuration.rb +2 -3
  58. data/lib/contrast/config/assess_configuration.rb +3 -3
  59. data/lib/contrast/config/base_configuration.rb +17 -28
  60. data/lib/contrast/config/certification_configuration.rb +15 -0
  61. data/lib/contrast/config/env_variables.rb +2 -9
  62. data/lib/contrast/config/heap_dump_configuration.rb +6 -6
  63. data/lib/contrast/config/inventory_configuration.rb +1 -5
  64. data/lib/contrast/config/protect_rule_configuration.rb +1 -1
  65. data/lib/contrast/config/request_audit_configuration.rb +18 -0
  66. data/lib/contrast/config/ruby_configuration.rb +6 -6
  67. data/lib/contrast/config/service_configuration.rb +1 -2
  68. data/lib/contrast/config.rb +0 -1
  69. data/lib/contrast/configuration.rb +1 -2
  70. data/lib/contrast/extension/assess/array.rb +5 -7
  71. data/lib/contrast/framework/manager.rb +8 -32
  72. data/lib/contrast/framework/manager_extend.rb +50 -0
  73. data/lib/contrast/framework/rails/railtie.rb +1 -1
  74. data/lib/contrast/framework/sinatra/support.rb +2 -1
  75. data/lib/contrast/logger/log.rb +8 -103
  76. data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
  77. data/lib/contrast/utils/assess/tracking_util.rb +20 -15
  78. data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
  79. data/lib/contrast/utils/class_util.rb +18 -14
  80. data/lib/contrast/utils/findings.rb +62 -0
  81. data/lib/contrast/utils/hash_digest.rb +10 -73
  82. data/lib/contrast/utils/hash_digest_extend.rb +86 -0
  83. data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
  84. data/lib/contrast/utils/heap_dump_util.rb +2 -65
  85. data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
  86. data/lib/contrast/utils/io_util.rb +1 -1
  87. data/lib/contrast/utils/log_utils.rb +108 -0
  88. data/lib/contrast/utils/middleware_utils.rb +87 -0
  89. data/lib/contrast/utils/net_http_base.rb +158 -0
  90. data/lib/contrast/utils/object_share.rb +1 -0
  91. data/lib/contrast/utils/request_utils.rb +88 -0
  92. data/lib/contrast/utils/response_utils.rb +97 -0
  93. data/lib/contrast/utils/substitution_utils.rb +167 -0
  94. data/lib/contrast/utils/tag_util.rb +9 -9
  95. data/lib/contrast/utils/telemetry.rb +4 -2
  96. data/lib/contrast/utils/telemetry_client.rb +90 -0
  97. data/lib/contrast/utils/telemetry_identifier.rb +17 -24
  98. data/ruby-agent.gemspec +5 -5
  99. metadata +48 -23
  100. data/lib/contrast/config/default_value.rb +0 -17
  101. data/lib/contrast/utils/requests_client.rb +0 -150
@@ -0,0 +1,86 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'digest'
5
+ require 'contrast/utils/hash_digest'
6
+
7
+ module Contrast
8
+ module Utils
9
+ # We use this class to provide hashes for our Request and Finding objects
10
+ # based upon our definitions of uniqueness.
11
+ # While the uniqueness of the request object is something internal to the
12
+ # Ruby agent, the uniqueness of the Finding hash is defined by a
13
+ # specification shared across all agent teams. The spec can be found here:
14
+ # https://bitbucket.org/contrastsecurity/assess-specifications/src/master/vulnerability/preflight.md
15
+ module HashDigestExtend
16
+ def generate_request_hash request
17
+ hash = new
18
+ hash.update(request.request_method)
19
+ hash.update(request.normalized_uri)
20
+ request.parameters.each_key do |name|
21
+ hash.update(name)
22
+ end
23
+ cl = request.headers[Contrast::Utils::HashDigest::CONTENT_LENGTH_HEADER]
24
+ hash.update_on_content_length(cl) if cl
25
+ hash.finish
26
+ end
27
+
28
+ def generate_event_hash finding, source, request
29
+ return generate_dataflow_hash(finding, request) if finding.events.length.to_i > 1
30
+
31
+ id = finding.rule_id
32
+ return generate_crypto_hash(finding, source, request) if Contrast::Utils::HashDigest::CRYPTO_RULES.include?(id)
33
+
34
+ generate_trigger_hash(finding, request)
35
+ end
36
+
37
+ def generate_config_hash finding
38
+ hash = new
39
+ hash.update(finding.rule_id)
40
+ path = finding.properties[Contrast::Utils::HashDigest::CONFIG_PATH_KEY]
41
+ hash.update(path)
42
+ method = finding.properties[Contrast::Utils::HashDigest::CONFIG_SESSION_ID_KEY]
43
+ hash.update(method)
44
+ hash.finish
45
+ end
46
+
47
+ def generate_class_scanning_hash finding
48
+ hash = new
49
+ hash.update(finding.rule_id)
50
+ module_name = finding.properties[Contrast::Utils::HashDigest::CLASS_SOURCE_KEY]
51
+ hash.update(module_name)
52
+ # We're not currently collecting this. 30/7/19 HM
53
+ line_no = finding.properties[Contrast::Utils::HashDigest::CLASS_LINE_NO_KEY]
54
+ hash.update(line_no)
55
+ field = finding.properties[Contrast::Utils::HashDigest::CLASS_CONSTANT_NAME_KEY]
56
+ hash.update(field)
57
+ hash.finish
58
+ end
59
+
60
+ private
61
+
62
+ def generate_crypto_hash finding, algorithm, request
63
+ hash = new
64
+ hash.update(finding.rule_id)
65
+ hash.update(algorithm)
66
+ hash.update_on_request(finding, request)
67
+ hash.finish
68
+ end
69
+
70
+ def generate_dataflow_hash finding, request
71
+ hash = new
72
+ hash.update(finding.rule_id)
73
+ hash.update_on_sources(finding.events)
74
+ hash.update_on_request(finding, request)
75
+ hash.finish
76
+ end
77
+
78
+ def generate_trigger_hash finding, request
79
+ hash = new
80
+ hash.update(finding.rule_id)
81
+ hash.update_on_request(finding, request)
82
+ hash.finish
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,74 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ # this module extends HeadDumpUtil
7
+ module HeadDumpExtend
8
+ def log_enabled_warning
9
+ control = Contrast::Utils::HeapDumpUtil.control
10
+ dir = control[:path]
11
+ window = control[:window]
12
+ count = control[:count]
13
+ delay = control[:delay]
14
+ clean = control[:clean]
15
+
16
+ logger.info <<~WARNING
17
+ *****************************************************
18
+ ******** HEAP DUMP HAS BEEN ENABLED ********
19
+ *** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
20
+ *****************************************************
21
+
22
+ Heap dump is a debugging tool that snapshots the entire
23
+ state of the Ruby VM. It is an exceptionally expensive
24
+ process, and should only be used to debug especially
25
+ pernicious errors.
26
+
27
+ It will write multiple memory snaphots, which are liable
28
+ to be multiple gigabytes in size.
29
+ They will be named "[unix timestamp]-heap.dump",
30
+ e.g.: 1020304050-heap.dump
31
+
32
+ It will then call Ruby `exit()`.
33
+
34
+ If this is not your specific intent, you can (and should)
35
+ disable this option in your Contrast config file.
36
+
37
+ HEAP DUMP PARAMETERS:
38
+ \t[write files to this directory] dir: #{ dir }
39
+ \t[wait this many seconds in between dumps] window: #{ window }
40
+ \t[heap dump this many times] count: #{ count }
41
+ \t[wait this many seconds into app lifetime] delay: #{ delay }
42
+ \t[perform gc pass before dump] clean: #{ clean }
43
+
44
+ *****************************************************
45
+ ******** YOU HAVE BEEN WARNED ********
46
+ *****************************************************
47
+ WARNING
48
+ end
49
+
50
+ def capture_heap_dump
51
+ control = Contrast::Utils::HeapDumpUtil.control
52
+ dir = control[:path]
53
+ window = control[:window]
54
+ count = control[:count]
55
+ clean = control[:clean]
56
+ logger.info('HEAP DUMP MAIN LOOP')
57
+ ObjectSpace.trace_object_allocations_start
58
+ count.times do |i|
59
+ logger.info('STARTING HEAP DUMP PASS', current_pass: i, max: count)
60
+ snapshot_heap(dir, clean)
61
+ logger.info('FINISHING HEAP DUMP PASS', current_pass: i, max: count)
62
+ sleep(window)
63
+ end
64
+ ensure
65
+ ObjectSpace.trace_object_allocations_stop
66
+ logger.info('*****************************************************')
67
+ logger.info('******** HEAP DUMP HAS CONCLUDED ********')
68
+ logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
69
+ logger.info('*****************************************************')
70
+ exit # rubocop:disable Rails/Exit We weren't kidding!
71
+ end
72
+ end
73
+ end
74
+ end
@@ -5,6 +5,7 @@ require 'objspace'
5
5
  require 'singleton'
6
6
  require 'contrast/components/heap_dump'
7
7
  require 'contrast/components/logger'
8
+ require 'contrast/utils/head_dump_utils_extend'
8
9
 
9
10
  module Contrast
10
11
  module Utils
@@ -13,6 +14,7 @@ module Contrast
13
14
  extend Contrast::Components::Logger::InstanceMethods
14
15
  include Contrast::Components::Logger::InstanceMethods
15
16
  extend Contrast::Components::HeapDump::InstanceMethods
17
+ include Contrast::Utils::HeadDumpExtend
16
18
 
17
19
  LOG_ERROR_DUMPS = 'Unable to generate heap dumps'
18
20
  FILE_WRITE_FLAGS = 'w'
@@ -47,71 +49,6 @@ module Contrast
47
49
  nil
48
50
  end
49
51
 
50
- def log_enabled_warning
51
- control = Contrast::Utils::HeapDumpUtil.control
52
- dir = control[:path]
53
- window = control[:window]
54
- count = control[:count]
55
- delay = control[:delay]
56
- clean = control[:clean]
57
-
58
- logger.info <<~WARNING
59
- *****************************************************
60
- ******** HEAP DUMP HAS BEEN ENABLED ********
61
- *** APPLICATION PROCESS WILL EXIT UPON COMPLETION ***
62
- *****************************************************
63
-
64
- Heap dump is a debugging tool that snapshots the entire
65
- state of the Ruby VM. It is an exceptionally expensive
66
- process, and should only be used to debug especially
67
- pernicious errors.
68
-
69
- It will write multiple memory snaphots, which are liable
70
- to be multiple gigabytes in size.
71
- They will be named "[unix timestamp]-heap.dump",
72
- e.g.: 1020304050-heap.dump
73
-
74
- It will then call Ruby `exit()`.
75
-
76
- If this is not your specific intent, you can (and should)
77
- disable this option in your Contrast config file.
78
-
79
- HEAP DUMP PARAMETERS:
80
- \t[write files to this directory] dir: #{ dir }
81
- \t[wait this many seconds in between dumps] window: #{ window }
82
- \t[heap dump this many times] count: #{ count }
83
- \t[wait this many seconds into app lifetime] delay: #{ delay }
84
- \t[perform gc pass before dump] clean: #{ clean }
85
-
86
- *****************************************************
87
- ******** YOU HAVE BEEN WARNED ********
88
- *****************************************************
89
- WARNING
90
- end
91
-
92
- def capture_heap_dump
93
- control = Contrast::Utils::HeapDumpUtil.control
94
- dir = control[:path]
95
- window = control[:window]
96
- count = control[:count]
97
- clean = control[:clean]
98
- logger.info('HEAP DUMP MAIN LOOP')
99
- ObjectSpace.trace_object_allocations_start
100
- count.times do |i|
101
- logger.info('STARTING HEAP DUMP PASS', current_pass: i, max: count)
102
- snapshot_heap(dir, clean)
103
- logger.info('FINISHING HEAP DUMP PASS', current_pass: i, max: count)
104
- sleep(window)
105
- end
106
- ensure
107
- ObjectSpace.trace_object_allocations_stop
108
- logger.info('*****************************************************')
109
- logger.info('******** HEAP DUMP HAS CONCLUDED ********')
110
- logger.info('*** APPLICATION PROCESS WILL EXIT SHORTLY ***')
111
- logger.info('*****************************************************')
112
- exit # rubocop:disable Rails/Exit We weren't kidding!
113
- end
114
-
115
52
  def snapshot_heap dir, clean
116
53
  output = "#{ Time.now.to_f }-heap.dump"
117
54
  output = File.join(dir, output)
@@ -34,11 +34,28 @@ module Contrast
34
34
  finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash)
35
35
  finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
36
36
  Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding)
37
+ if Contrast::Agent::Reporter.enabled?
38
+ cs__report_new_finding hash, rule_id, user_provided_options, call_location
39
+ end
37
40
  end
38
41
  rescue StandardError => e
39
42
  logger.error('Unable to build a finding', e, rule: rule_id)
40
43
  end
41
44
 
45
+ def cs__report_new_finding hash_code, rule_id, user_provided_options, call_location
46
+ new_preflight = Contrast::Agent::Reporting::Preflight.new
47
+ new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
48
+ new_preflight_message.hash_code = hash_code
49
+ new_preflight_message.data = "#{ rule_id },#{ hash_code }"
50
+ new_preflight.messages << new_preflight_message
51
+
52
+ ruby_finding = Contrast::Agent::Reporting::Finding.new rule_id
53
+ ruby_finding.hash_code = hash_code
54
+ set_new_finding_properties(ruby_finding, user_provided_options, call_location)
55
+ Contrast::Agent.reporter_queue.send_event_immediately(new_preflight)
56
+ Contrast::Agent::Reporting::ReportingStorage[hash_code] = ruby_finding
57
+ end
58
+
42
59
  private
43
60
 
44
61
  # Set the properties needed to report and subsequently render this finding on the finding given.
@@ -76,6 +93,18 @@ module Contrast
76
93
  end
77
94
  call_location&.label&.dup
78
95
  end
96
+
97
+ def set_new_finding_properties finding, user_provided_options, call_location
98
+ path = call_location.path
99
+ # just get the file name, not the full path
100
+ path = path.split(Contrast::Utils::ObjectShare::SLASH).last
101
+ session_id = user_provided_options[:key].to_s if user_provided_options
102
+ finding.properties[CS__SESSION_ID] = session_id
103
+ finding.properties[CS__PATH] = path
104
+ file_path = call_location.absolute_path
105
+ snippet = file_snippet(file_path, call_location)
106
+ finding.properties[CS__SNIPPET] = snippet
107
+ end
79
108
  end
80
109
  end
81
110
  end
@@ -11,7 +11,7 @@ module Contrast
11
11
 
12
12
  class << self
13
13
  # We're only going to call rewind on things that we believe are safe to
14
- # call it on. This method white lists those cases and returns false in
14
+ # call it on. This method allow lists those cases and returns false in
15
15
  # all others.
16
16
  def should_rewind? potential_io
17
17
  return true if potential_io.is_a?(StringIO)
@@ -0,0 +1,108 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ # Method utility used by Contrast::Logger::log
7
+ module LogUtils
8
+ DEFAULT_NAME = 'contrast.log'
9
+ DEFAULT_LEVEL = ::Ougai::Logging::Severity::INFO
10
+ VALID_LEVELS = ::Ougai::Logging::Severity::SEV_LABEL
11
+ STDOUT_STR = 'STDOUT'
12
+ STDERR_STR = 'STDERR'
13
+ PROGNAME = 'Contrast Agent'
14
+ DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%L%z'
15
+
16
+ private
17
+
18
+ def build path: STDOUT_STR, level_const: DEFAULT_LEVEL
19
+ logger = case path
20
+ when STDOUT_STR, STDERR_STR
21
+ ::Ougai::Logger.new(Object.cs__const_get(path))
22
+ else
23
+ ::Ougai::Logger.new(path)
24
+ end
25
+ add_contrast_loggers(logger)
26
+ logger.progname = PROGNAME
27
+ logger.level = level_const
28
+ logger.formatter = Contrast::Logger::Format.new
29
+ logger.formatter.datetime_format = DATE_TIME_FORMAT
30
+ logger
31
+ end
32
+
33
+ def add_contrast_loggers logger
34
+ logger.extend(Contrast::Logger::Application)
35
+ logger.extend(Contrast::Logger::Request)
36
+ logger.extend(Contrast::Logger::Time)
37
+ end
38
+
39
+ # Determine the valid path to which to log, given the precedence of config > settings > default.
40
+ #
41
+ # @param log_file [String, nil] the file to which to log as provided by the settings retrieved from the
42
+ # TeamServer.
43
+ # @return [String] the path to which to log or STDOUT / STDERR if one of those values provided.
44
+ def find_valid_path log_file
45
+ config = ::Contrast::CONFIG.root.agent.logger
46
+ config_path = config&.path&.length.to_i.positive? ? config.path : nil
47
+ valid_path(config_path || log_file)
48
+ end
49
+
50
+ def valid_path path
51
+ path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
52
+ return path if path == STDOUT_STR
53
+ return path if path == STDERR_STR
54
+
55
+ path = DEFAULT_NAME if path.empty?
56
+ if write_permission?(path)
57
+ path
58
+ elsif write_permission?(DEFAULT_NAME)
59
+ # Log once when the path is invalid. We'll change to this path, so no
60
+ # need to log again.
61
+ if previous_path != DEFAULT_NAME
62
+ $stdout.puts "[!] Unable to write to '#{ path }'. Writing to default log '#{ DEFAULT_NAME }' instead."
63
+ end
64
+ DEFAULT_NAME
65
+ else
66
+ # Log once when the path is invalid. We'll change to this path, so no
67
+ # need to log again.
68
+ $stdout.puts "[!] Unable to write to '#{ path }'. Writing to standard out instead."
69
+ STDOUT_STR
70
+ end
71
+ end
72
+
73
+ # Determine the valid level to which to log, given the precedence of config > settings > default.
74
+ #
75
+ # @param log_level [String, nil] the level at which to log as provided by the settings retrieved from the
76
+ # TeamServer.
77
+ # @return [::Ougai::Logging::Severity] the level at which to log
78
+ def find_valid_level log_level
79
+ config = ::Contrast::CONFIG.root.agent.logger
80
+ config_level = config&.level&.length&.positive? ? config.level : nil
81
+
82
+ valid_level(config_level || log_level)
83
+ end
84
+
85
+ def valid_level level
86
+ level ||= DEFAULT_LEVEL
87
+ level = level.upcase
88
+ if VALID_LEVELS.include?(level)
89
+ Object.cs__const_get("::Ougai::Logging::Severity::#{ level }")
90
+ else
91
+ DEFAULT_LEVEL
92
+ end
93
+ rescue StandardError
94
+ DEFAULT_LEVEL
95
+ end
96
+
97
+ # Log that the Agent log has changed and include some default information at the start of the log.
98
+ def log_update
99
+ logger.debug('Initialized new contrast agent logger')
100
+ logger.debug_with_time('middleware: log environment') do
101
+ logger.application_environment
102
+ logger.application_configuration
103
+ logger.application_libraries
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,87 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Utils
6
+ # helper methods for Contrast::Agent::Middleware
7
+ # including disclaimers, deprecation notices, error handling, setup
8
+ module MiddlewareUtils
9
+ private
10
+
11
+ def setup_agent
12
+ ::Contrast::SETTINGS.reset_state
13
+
14
+ inform_deprecations
15
+ telemetry_disclaimer
16
+
17
+ if ::Contrast::CONFIG.invalid?
18
+ ::Contrast::AGENT.disable!
19
+ logger.error('!!! CONFIG FILE IS INVALID - DISABLING CONTRAST AGENT !!!')
20
+ elsif ::Contrast::AGENT.disabled?
21
+ logger.warn('Contrast disabled by configuration. Continuing without instrumentation.')
22
+ else
23
+ ::Contrast::AGENT.enable!
24
+ end
25
+ end
26
+
27
+ SECURITY_EXCEPTION_MARKER = 'Contrast::SecurityException'
28
+ # We're only going to suppress SecurityExceptions indicating a blocked attack. And, only if the
29
+ # config.agent.ruby.exceptions.capture? is set
30
+ def handle_exception exception
31
+ if security_exception?(exception)
32
+ exception_control = ::Contrast::AGENT.exception_control
33
+ raise exception unless exception_control[:enable]
34
+
35
+ [exception_control[:status], {}, [exception_control[:message]]]
36
+ else
37
+ logger.debug('Re-throwing original error', exception)
38
+ raise exception
39
+ end
40
+ end
41
+
42
+ # Is the given exception one raised by our Protect code?
43
+ #
44
+ # @param exception [Exception]
45
+ # @return [Boolean]
46
+ def security_exception? exception
47
+ exception.is_a?(Contrast::SecurityException) || exception.message&.include?(SECURITY_EXCEPTION_MARKER)
48
+ end
49
+
50
+ # As we deprecate support to prepare to remove dead code, we need to inform our users still relying on the now
51
+ # deprecated and soon to be removed functionality. This method handles doing that by leveraging the standard
52
+ # Kernel#warn approach
53
+ def inform_deprecations
54
+ # Ruby 2.5 is currently in security maintenance, meaning int is only receiving updates for security issues. It
55
+ # will move to eol on 31 March 2021. As such, we can remove support for it in Q3. We'll begin the deprecation
56
+ # warnings now so that customers have time to reach out if they'll be impacted.
57
+ # TODO: RUBY-715 remove this part of the method, leaving it empty if there are no other deprecations, when we
58
+ # drop 2.5 support.
59
+ return unless RUBY_VERSION < '2.6.0'
60
+
61
+ Kernel.warn('[Contrast Security] [DEPRECATION] Support for Ruby 2.5 will be removed in April 2021. '\
62
+ 'Please contact Customer Support prior if you require continued support.')
63
+ end
64
+
65
+ # Displays Telemetry disclaimer if Telemetry is enabled.
66
+ # if .telemetry file doesn't exist we create one and then show the disclaimer.
67
+ # if the file already exists we do nothing.
68
+ def telemetry_disclaimer
69
+ return unless Contrast::Agent::Telemetry.enabled?
70
+ return unless Contrast::Utils::Telemetry.create_telemetry_file
71
+
72
+ logger.info Contrast::Utils::Telemetry.disclaimer
73
+ $stdout.print Contrast::Utils::Telemetry.disclaimer
74
+ true
75
+ end
76
+
77
+ def application_code env
78
+ logger.trace_with_time('application') do
79
+ app.call(env)
80
+ end
81
+ rescue Contrast::SecurityException => e
82
+ logger.trace('Security Exception raised during application lifecycle to prevent an attack', e)
83
+ raise e
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,158 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/http'
5
+ require 'contrast/components/logger'
6
+ require 'contrast/utils/object_share'
7
+ require 'socket'
8
+
9
+ module Contrast
10
+ module Utils
11
+ # This module creates a Net::HTTP client base to be used by different services
12
+ # All HTTP clients reporting to Telemetry or TS should inherit from this
13
+ class NetHttpBase
14
+ include Contrast::Components::Logger::InstanceMethods
15
+ # This method initializes the Net::HTTP client we'll need. it will validate
16
+ # the connection and make the first request. If connection is valid and response
17
+ # is available then the open connection is returned.
18
+ #
19
+ # @param service_name [String] Name of service used in logging messages
20
+ # @param url [String]
21
+ # @param use_proxy [Boolean] flag used to indicate proxy connections [default = false]
22
+ # @param use_custom_cert [Boolean] flag used to indicate whether the client is to use
23
+ # self signed certificates provided by config [default = false]
24
+ # @return [Net::HTTP, nil] Return open connection or nil
25
+ def initialize_connection service_name, url, use_proxy = false, use_custom_cert = false
26
+ return unless url
27
+
28
+ addr = URI(url)
29
+ # the proxy is enabled only if there is provided url even if the enable is set to true
30
+ return if addr.host.nil? || addr.port.nil? || addr.scheme != 'https'
31
+
32
+ proxy_addr = URI(Contrast::API.proxy_url) if proxy_enabled?
33
+ net_http_client = initialize_client addr, proxy_addr, use_proxy, use_custom_cert
34
+ return if net_http_client.nil?
35
+
36
+ net_http_client.start
37
+ return unless net_http_client.started?
38
+
39
+ logger.warn("Starting #{ service_name } connection test")
40
+ return unless connection_verified? net_http_client
41
+
42
+ net_http_client
43
+ rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, OpenSSL::SSL::SSLError => e
44
+ logger.warn("#{ service_name } connection failed", e.message)
45
+ nil
46
+ end
47
+
48
+ # Validates connection with assigned domain.
49
+ # If connection is running, SSL certificate of the endpoint is valid, Ip address is resolvable
50
+ # and response is received without peer's reset or refuse of connection,
51
+ # then validation returns true. Error handling is in place so that the work of the agent will continue as
52
+ # normal without Telemetry.
53
+ #
54
+ # @param client [Net::HTTP]
55
+ # @return [Boolean] true | false
56
+ def connection_verified? client
57
+ return @_connection_verified unless @_connection_verified.nil?
58
+ return false if client.nil?
59
+
60
+ # Before RUBY 2.7 there is no #ipaddr
61
+ ipaddr = if RUBY_VERSION < '2.7.0'
62
+ socket = TCPSocket.open(client.address, client.port)
63
+ ipaddr = socket.peeraddr[3]
64
+ socket.close
65
+ ipaddr
66
+ else
67
+ client.ipaddr
68
+ end
69
+ response = client.request(Net::HTTP::Get.new(client.address))
70
+ verify_cert = OpenSSL::SSL.verify_certificate_identity(client.peer_cert, client.address)
71
+ resolved = resolved? client.address, ipaddr
72
+ @_connection_verified = if resolved && response && verify_cert
73
+ true
74
+ else
75
+ false
76
+ end
77
+ rescue OpenSSL::SSL::SSLError, Resolv::ResolvError, Errno::ECONNRESET, Errno::ECONNREFUSED,
78
+ Errno::ETIMEDOUT, Errno::ESHUTDOWN, Errno::EHOSTDOWN, Errno::EHOSTUNREACH, Errno::EISCONN,
79
+ Errno::ECONNABORTED, Errno::ENETRESET, Errno::ENETUNREACH => e
80
+
81
+ logger.warn("#{ service_name } connection failed", e.message)
82
+ false
83
+ end
84
+
85
+ private
86
+
87
+ # Resolves the address of the assigned domain to array of corresponding IPs (if more than one)
88
+ # and runs a matcher to see if current connection IP is in the list.
89
+ # This is called within #verify_connection, if called on it's own there will be no
90
+ # error handling.
91
+ #
92
+ # @param address [String] Human friendly address of assigned domain
93
+ # @param ipaddr [String] Machine friendly IP address of the assigned domain
94
+ # @return[Boolean] true if both addresses are resolved | false if one of the addresses
95
+ # is non-resolvable
96
+ def resolved? address, ipaddr
97
+ return @_resolved unless @_resolved.nil?
98
+
99
+ @_resolved = if (addresses = Resolv.getaddresses address)
100
+ addresses.any? { |addr| addr.include?(ipaddr) }
101
+ else
102
+ false
103
+ end
104
+ end
105
+
106
+ # if the configuration for the use of self-signed certificates is enabled
107
+ # and required for this client then assign the files and keys to the client
108
+ #
109
+ # @param client [Net::HTTP]
110
+ def assign_cert client
111
+ if Contrast::API.certification_ca_file
112
+ client.ca_file = OpenSSL::X509::Certificate.new(File.read(Contrast::API.certification_ca_file)).to_s
113
+ elsif Contrast::API.certification_cert_file
114
+ client.cert = OpenSSL::X509::Certificate.new(File.read(Contrast::API.certification_cert_file)).to_s
115
+ elsif Contrast::API.certification_key_file
116
+ client.key = OpenSSL::PKey::RSA.new(File.read(Contrast::API.certification_key_file)).to_s
117
+ end
118
+ rescue Errno::ENOENT => e
119
+ logger.error('Custom certificates failed', e.message)
120
+ end
121
+
122
+ # sets default setting for client validation of certificates and
123
+ # timeout options
124
+ #
125
+ # @param addr [URI::Generic] uri of assigned domain
126
+ # @param proxy_addr [URI::Generic | nil] uri of proxy
127
+ # @param use_proxy [Boolean] flag used to indicate proxy connections [default = false]
128
+ # @param use_custom_cert [Boolean] flag used to indicate whether the client is to use
129
+ # self signed certificates provided by config [default = false]
130
+ # @return [Net::HTTP]
131
+ def initialize_client addr, proxy_addr, use_proxy, use_custom_cert
132
+ initialize_client = if proxy_enabled? && use_proxy
133
+ Net::HTTP.new(addr.host, nil, proxy_addr&.host, proxy_addr&.port)
134
+ else
135
+ Net::HTTP.new(addr.host, addr.port)
136
+ end
137
+ assign_cert(initialize_client) if use_custom_cert && Contrast::API.certification_enabled?
138
+ initialize_client.use_ssl = true
139
+ initialize_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
140
+ initialize_client.verify_depth = 5
141
+ # open connection timeout in ms
142
+ # if connection reaches timeout this will produce Net::OpenTimeout error
143
+ initialize_client.open_timeout = 10
144
+ # if we can't read the response or a chunk within time this will cause a
145
+ # Net::ReadTimeout error when the request is made
146
+ initialize_client.read_timeout = 10
147
+ initialize_client
148
+ end
149
+
150
+ # Check if proxy is enabled
151
+ #
152
+ # @return @_proxy_enabled [Boolean] True if proxy is enabled and url is present else false
153
+ def proxy_enabled?
154
+ @_proxy_enabled ||= Contrast::API.proxy_enabled? && !Contrast::API.proxy_url.nil? if @_proxy_enabled.nil?
155
+ end
156
+ end
157
+ end
158
+ end
@@ -30,6 +30,7 @@ module Contrast
30
30
  SEMICOLON = ';'
31
31
  SINGLE_QUOTE = '\''
32
32
  SLASH = '/'
33
+ SPACE = ' '
33
34
  UNDERSCORE = '_'
34
35
  DOUBLE_UNDERSCORE = '__'
35
36
  AT = '@'