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.
- checksums.yaml +4 -4
- data/.simplecov +1 -0
- data/lib/contrast/agent/assess/policy/policy_node.rb +6 -6
- data/lib/contrast/agent/assess/policy/policy_scanner.rb +5 -0
- data/lib/contrast/agent/assess/policy/propagator/center.rb +1 -1
- data/lib/contrast/agent/assess/policy/propagator/substitution.rb +2 -154
- data/lib/contrast/agent/assess/policy/trigger_method.rb +44 -7
- data/lib/contrast/agent/assess/policy/trigger_node.rb +14 -6
- data/lib/contrast/agent/assess/policy/trigger_validation/xss_validator.rb +1 -1
- data/lib/contrast/agent/assess/property/tagged.rb +51 -57
- data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +40 -6
- data/lib/contrast/agent/metric_telemetry_event.rb +2 -2
- data/lib/contrast/agent/middleware.rb +5 -75
- data/lib/contrast/agent/patching/policy/method_policy.rb +3 -89
- data/lib/contrast/agent/patching/policy/method_policy_extend.rb +111 -0
- data/lib/contrast/agent/patching/policy/patcher.rb +12 -8
- data/lib/contrast/agent/reporting/report.rb +21 -0
- data/lib/contrast/agent/reporting/reporter.rb +142 -0
- data/lib/contrast/agent/reporting/reporting_events/finding.rb +90 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight.rb +25 -0
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +56 -0
- data/lib/contrast/agent/reporting/reporting_events/reporting_event.rb +37 -0
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +127 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +168 -0
- data/lib/contrast/agent/reporting/reporting_utilities/reporting_storage.rb +66 -0
- data/lib/contrast/agent/request.rb +2 -81
- data/lib/contrast/agent/request_context.rb +4 -128
- data/lib/contrast/agent/request_context_extend.rb +138 -0
- data/lib/contrast/agent/response.rb +2 -73
- data/lib/contrast/agent/startup_metrics_telemetry_event.rb +39 -16
- data/lib/contrast/agent/static_analysis.rb +1 -1
- data/lib/contrast/agent/telemetry.rb +15 -7
- data/lib/contrast/agent/telemetry_event.rb +8 -9
- data/lib/contrast/agent/thread_watcher.rb +31 -5
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/agent.rb +15 -0
- data/lib/contrast/api/communication/connection_status.rb +10 -7
- data/lib/contrast/api/communication/messaging_queue.rb +37 -3
- data/lib/contrast/api/communication/response_processor.rb +15 -8
- data/lib/contrast/api/communication/service_lifecycle.rb +13 -3
- data/lib/contrast/api/communication/socket.rb +6 -8
- data/lib/contrast/api/communication/socket_client.rb +29 -12
- data/lib/contrast/api/communication/speedracer.rb +37 -1
- data/lib/contrast/api/communication/tcp_socket.rb +4 -3
- data/lib/contrast/api/communication/unix_socket.rb +1 -0
- data/lib/contrast/api/decorators/finding.rb +45 -0
- data/lib/contrast/components/api.rb +56 -0
- data/lib/contrast/components/app_context.rb +10 -65
- data/lib/contrast/components/app_context_extend.rb +78 -0
- data/lib/contrast/components/base.rb +23 -0
- data/lib/contrast/components/config.rb +8 -8
- data/lib/contrast/components/contrast_service.rb +5 -0
- data/lib/contrast/components/sampling.rb +2 -2
- data/lib/contrast/config/agent_configuration.rb +1 -1
- data/lib/contrast/config/api_configuration.rb +9 -4
- data/lib/contrast/config/api_proxy_configuration.rb +14 -0
- data/lib/contrast/config/application_configuration.rb +2 -3
- data/lib/contrast/config/assess_configuration.rb +3 -3
- data/lib/contrast/config/base_configuration.rb +17 -28
- data/lib/contrast/config/certification_configuration.rb +15 -0
- data/lib/contrast/config/env_variables.rb +2 -9
- data/lib/contrast/config/heap_dump_configuration.rb +6 -6
- data/lib/contrast/config/inventory_configuration.rb +1 -5
- data/lib/contrast/config/protect_rule_configuration.rb +1 -1
- data/lib/contrast/config/request_audit_configuration.rb +18 -0
- data/lib/contrast/config/ruby_configuration.rb +6 -6
- data/lib/contrast/config/service_configuration.rb +1 -2
- data/lib/contrast/config.rb +0 -1
- data/lib/contrast/configuration.rb +1 -2
- data/lib/contrast/extension/assess/array.rb +5 -7
- data/lib/contrast/framework/manager.rb +8 -32
- data/lib/contrast/framework/manager_extend.rb +50 -0
- data/lib/contrast/framework/rails/railtie.rb +1 -1
- data/lib/contrast/framework/sinatra/support.rb +2 -1
- data/lib/contrast/logger/log.rb +8 -103
- data/lib/contrast/utils/assess/property/tagged_utils.rb +23 -0
- data/lib/contrast/utils/assess/tracking_util.rb +20 -15
- data/lib/contrast/utils/assess/trigger_method_utils.rb +1 -1
- data/lib/contrast/utils/class_util.rb +18 -14
- data/lib/contrast/utils/findings.rb +62 -0
- data/lib/contrast/utils/hash_digest.rb +10 -73
- data/lib/contrast/utils/hash_digest_extend.rb +86 -0
- data/lib/contrast/utils/head_dump_utils_extend.rb +74 -0
- data/lib/contrast/utils/heap_dump_util.rb +2 -65
- data/lib/contrast/utils/invalid_configuration_util.rb +29 -0
- data/lib/contrast/utils/io_util.rb +1 -1
- data/lib/contrast/utils/log_utils.rb +108 -0
- data/lib/contrast/utils/middleware_utils.rb +87 -0
- data/lib/contrast/utils/net_http_base.rb +158 -0
- data/lib/contrast/utils/object_share.rb +1 -0
- data/lib/contrast/utils/request_utils.rb +88 -0
- data/lib/contrast/utils/response_utils.rb +97 -0
- data/lib/contrast/utils/substitution_utils.rb +167 -0
- data/lib/contrast/utils/tag_util.rb +9 -9
- data/lib/contrast/utils/telemetry.rb +4 -2
- data/lib/contrast/utils/telemetry_client.rb +90 -0
- data/lib/contrast/utils/telemetry_identifier.rb +17 -24
- data/ruby-agent.gemspec +5 -5
- metadata +48 -23
- data/lib/contrast/config/default_value.rb +0 -17
- 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
|
|
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
|