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,88 @@
|
|
|
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
|
+
# used in Contrast::Agent::Request
|
|
7
|
+
module RequestUtils
|
|
8
|
+
# Return a flattened hash of params with realized paths for keys, in
|
|
9
|
+
# addition to a separate, valueless, entry for each nest key.
|
|
10
|
+
# See RUBY-621 for more details.
|
|
11
|
+
# { key : { nested_key : ['x','y','z' ] } }
|
|
12
|
+
# becomes
|
|
13
|
+
# {
|
|
14
|
+
# key[nested_key][0] : 'x'
|
|
15
|
+
# key[nested_key][1] : 'y'
|
|
16
|
+
# key[nested_key][2] : 'z'
|
|
17
|
+
# key : ''
|
|
18
|
+
# nested_key : ''
|
|
19
|
+
# }
|
|
20
|
+
def normalize_params val, prefix: nil
|
|
21
|
+
# In non-recursive invocations, val should always be a Hash
|
|
22
|
+
# (rather than breaking this out into two methods)
|
|
23
|
+
case val
|
|
24
|
+
when Tempfile
|
|
25
|
+
# Skip if it's the auto-generated value from rails when it handles
|
|
26
|
+
# file uploads. The file name will still be sent to SR for analysis.
|
|
27
|
+
{}
|
|
28
|
+
when Hash
|
|
29
|
+
res = val.each_with_object({}) do |(k, v), hash|
|
|
30
|
+
k = Contrast::Utils::StringUtils.force_utf8(k)
|
|
31
|
+
nested_prefix = prefix.nil? ? k : "#{ prefix }[#{ k }]"
|
|
32
|
+
hash[k] = Contrast::Utils::ObjectShare::EMPTY_STRING
|
|
33
|
+
hash.merge! normalize_params(v, prefix: nested_prefix)
|
|
34
|
+
end
|
|
35
|
+
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
|
36
|
+
res
|
|
37
|
+
when Enumerable
|
|
38
|
+
idx = 0
|
|
39
|
+
res = {}
|
|
40
|
+
while idx < val.length
|
|
41
|
+
res.merge! normalize_params(val[idx], prefix: "#{ prefix }[#{ idx }]")
|
|
42
|
+
idx += 1
|
|
43
|
+
end
|
|
44
|
+
res[prefix] = Contrast::Utils::ObjectShare::EMPTY_STRING if prefix
|
|
45
|
+
res
|
|
46
|
+
else
|
|
47
|
+
{ prefix => Contrast::Utils::StringUtils.force_utf8(val) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_body body
|
|
52
|
+
return body if body.is_a?(String)
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
can_rewind = Contrast::Utils::DuckUtils.quacks_to?(body, :rewind)
|
|
56
|
+
# if we are after a middleware that failed to rewind
|
|
57
|
+
body.rewind if can_rewind
|
|
58
|
+
body.read
|
|
59
|
+
rescue StandardError => e
|
|
60
|
+
logger.error('Error in attempt to read body', message: e.message)
|
|
61
|
+
logger.trace('With Stack', e)
|
|
62
|
+
body.to_s
|
|
63
|
+
ensure
|
|
64
|
+
# be a good citizen and rewind
|
|
65
|
+
body.rewind if can_rewind
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def traverse_parsed_multipart multipart_data, current_names
|
|
70
|
+
return current_names unless multipart_data
|
|
71
|
+
|
|
72
|
+
multipart_data.each_value do |data_value|
|
|
73
|
+
next unless data_value.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
tempfile = data_value[:tempfile]
|
|
76
|
+
if tempfile.nil?
|
|
77
|
+
traverse_parsed_multipart(data_value, current_names)
|
|
78
|
+
else
|
|
79
|
+
name = data_value[:name].to_s
|
|
80
|
+
file_name = data_value[:filename].to_s
|
|
81
|
+
current_names[name] = file_name
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
current_names
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
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
|
+
# used in Contrast::Agent::Response
|
|
7
|
+
module ResponseUtils
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# From the dtm for normalized_response_headers:
|
|
11
|
+
# Key is UPPERCASE_UNDERSCORE
|
|
12
|
+
#
|
|
13
|
+
# Example: Content-Type: text/html; charset=utf-8
|
|
14
|
+
# "CONTENT_TYPE" => Content-Type,["text/html; charset=utf8"]
|
|
15
|
+
def append_pair map, key, value
|
|
16
|
+
return unless key && value
|
|
17
|
+
return if value.is_a?(Hash)
|
|
18
|
+
|
|
19
|
+
safe_key = Contrast::Utils::StringUtils.force_utf8(key)
|
|
20
|
+
hash_key = Contrast::Utils::StringUtils.normalized_key(safe_key)
|
|
21
|
+
map[hash_key] ||= Contrast::Api::Dtm::Pair.new
|
|
22
|
+
map[hash_key].key = safe_key
|
|
23
|
+
map[hash_key].values << Contrast::Utils::StringUtils.force_utf8(value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
HTTP_PREFIX = /^[Hh][Tt][Tt][Pp][_-]/i.cs__freeze
|
|
27
|
+
|
|
28
|
+
# Given some holder of the content of the response's body, extract that
|
|
29
|
+
# content and return it as a String
|
|
30
|
+
#
|
|
31
|
+
# @param body [String, Rack::File, Rack::BodyProxy,
|
|
32
|
+
# ActionDispatch::Response::RackBody, Rack::Response] Something that
|
|
33
|
+
# holds, wraps, or is the body of the Response
|
|
34
|
+
# @return [nil, String] the content of the body
|
|
35
|
+
def extract_body body
|
|
36
|
+
return unless body
|
|
37
|
+
return if defined?(Rack::File) && body.is_a?(Rack::File)
|
|
38
|
+
|
|
39
|
+
case body
|
|
40
|
+
when Rack::BodyProxy
|
|
41
|
+
handle_rack_body_proxy(body)
|
|
42
|
+
when ActionDispatch::Response::RackBody
|
|
43
|
+
extract_body(body.body) if defined?(ActionDispatch::Response::RackBody)
|
|
44
|
+
when Rack::Response
|
|
45
|
+
extract_body(body.body)
|
|
46
|
+
when Contrast::Utils::DuckUtils.quacks_to?(body, :each)
|
|
47
|
+
acc = []
|
|
48
|
+
body.each { |tmp| acc << read_or_string(tmp) }
|
|
49
|
+
acc.compact.join(Contrast::Utils::ObjectShare::NEW_LINE)
|
|
50
|
+
when ActionView::OutputBuffer
|
|
51
|
+
# https://stackoverflow.com/questions/15654676/how-to-convert-activesupportsafebuffer-to-string
|
|
52
|
+
body.to_str
|
|
53
|
+
else
|
|
54
|
+
read_or_string(body)
|
|
55
|
+
end
|
|
56
|
+
# if body.is_a?(Rack::BodyProxy)
|
|
57
|
+
# handle_rack_body_proxy(body)
|
|
58
|
+
# elsif (defined?(ActionDispatch::Response::RackBody) && body.is_a?(ActionDispatch::Response::RackBody)) ||
|
|
59
|
+
# body.is_a?(Rack::Response)
|
|
60
|
+
#
|
|
61
|
+
# extract_body(body.body)
|
|
62
|
+
# elsif Contrast::Utils::DuckUtils.quacks_to?(body, :each)
|
|
63
|
+
# acc = []
|
|
64
|
+
# body.each { |tmp| acc << read_or_string(tmp) }
|
|
65
|
+
# acc.compact.join(Contrast::Utils::ObjectShare::NEW_LINE)
|
|
66
|
+
# elsif ActionView::OutputBuffer
|
|
67
|
+
# # https://stackoverflow.com/questions/15654676/how-to-convert-activesupportsafebuffer-to-string
|
|
68
|
+
# body.to_str
|
|
69
|
+
# else
|
|
70
|
+
# read_or_string(body)
|
|
71
|
+
# end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def handle_rack_body_proxy body
|
|
75
|
+
next_body = body.instance_variable_get(:@body)
|
|
76
|
+
case next_body
|
|
77
|
+
when Array
|
|
78
|
+
extract_body(next_body[0])
|
|
79
|
+
else
|
|
80
|
+
extract_body(next_body)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def read_or_string obj
|
|
85
|
+
return unless obj
|
|
86
|
+
|
|
87
|
+
if Contrast::Utils::DuckUtils.quacks_to?(obj, :read)
|
|
88
|
+
tmp = obj.read
|
|
89
|
+
obj.rewind
|
|
90
|
+
tmp
|
|
91
|
+
else
|
|
92
|
+
obj.to_s
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
# used in Contrast::Agent::Assesss::Policy::Propagator::Substitution
|
|
7
|
+
module SubstitutionUtils
|
|
8
|
+
CAPTURE_GROUP_REGEXP = /\\[[:digit:]]/.cs__freeze
|
|
9
|
+
CAPTURE_NAME_REGEXP = /\\k<[[:alpha:]]/.cs__freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def substitution_tagger patcher, preshift, ret, block, global = true
|
|
14
|
+
return ret unless ret
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
source = preshift.object
|
|
18
|
+
self_tracked = Contrast::Agent::Assess::Tracker.tracked?(source)
|
|
19
|
+
args = preshift.args[1]
|
|
20
|
+
incoming_tracked = args && determine_tracked(args)
|
|
21
|
+
return ret unless self_tracked || incoming_tracked
|
|
22
|
+
|
|
23
|
+
parent_events = []
|
|
24
|
+
if block
|
|
25
|
+
block_sub(self_tracked, source, ret)
|
|
26
|
+
elsif args.is_a?(String)
|
|
27
|
+
string_sub(parent_events, self_tracked, preshift, ret, args, incoming_tracked, global)
|
|
28
|
+
elsif args.is_a?(Hash)
|
|
29
|
+
hash_sub(self_tracked, source, ret)
|
|
30
|
+
else # Enumerator, only for gsub
|
|
31
|
+
pattern_gsub(parent_events, preshift, ret)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
self_tracked_handle self_tracked, source, parent_events
|
|
35
|
+
string_build_event(parent_events, patcher, preshift, ret)
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
logger.error('Unable to apply gsub propagator', e)
|
|
38
|
+
end
|
|
39
|
+
ret
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self_tracked_handle self_tracked, source, parent_events
|
|
43
|
+
return unless self_tracked
|
|
44
|
+
|
|
45
|
+
source_properties = Contrast::Agent::Assess::Tracker.properties(source)
|
|
46
|
+
parent_event = source_properties&.event
|
|
47
|
+
parent_events.prepend(parent_event) if parent_event
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def determine_tracked args
|
|
51
|
+
# if there's no arg, it can't be tracked
|
|
52
|
+
return false unless args
|
|
53
|
+
|
|
54
|
+
# if it's a string, just ask if it's tracked
|
|
55
|
+
case args
|
|
56
|
+
when String
|
|
57
|
+
Contrast::Agent::Assess::Tracker.tracked?(args)
|
|
58
|
+
# if it's a hash, ask if it has a tracked string
|
|
59
|
+
when Hash
|
|
60
|
+
args.values.any? { |value| value.is_a?(String) && Contrast::Agent::Assess::Tracker.tracked?(value) }
|
|
61
|
+
# this should never happen
|
|
62
|
+
else
|
|
63
|
+
false
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def string_sub parent_events, self_tracked, preshift, ret, incoming, incoming_tracked, global
|
|
68
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
|
69
|
+
|
|
70
|
+
incoming_properties = Contrast::Agent::Assess::Tracker.properties(incoming)
|
|
71
|
+
parent_events << incoming_properties&.event if incoming_properties&.event
|
|
72
|
+
|
|
73
|
+
source = preshift.object
|
|
74
|
+
|
|
75
|
+
# We can't efficiently find the places that things were
|
|
76
|
+
# copied from regexp / captures. Trading accuracy for
|
|
77
|
+
# performance
|
|
78
|
+
if incoming.match?(CAPTURE_GROUP_REGEXP) || incoming.match?(CAPTURE_NAME_REGEXP)
|
|
79
|
+
properties.splat_from(source, ret) if self_tracked
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# if it's just a straight insert, that we can do
|
|
84
|
+
# Copy the tags from us to the return
|
|
85
|
+
ranges = find_string_sub_insert(properties, preshift, incoming, ret, global)
|
|
86
|
+
|
|
87
|
+
properties.delete_tags_at_ranges(ranges)
|
|
88
|
+
properties.shift_tags(ranges)
|
|
89
|
+
return unless incoming_tracked
|
|
90
|
+
return unless incoming_properties
|
|
91
|
+
|
|
92
|
+
tags = incoming_properties.tag_keys
|
|
93
|
+
ranges.each do |range|
|
|
94
|
+
tags.each do |tag|
|
|
95
|
+
properties.add_tag(tag, range)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Find the points at which the new String was placed into the original
|
|
101
|
+
#
|
|
102
|
+
# @param properties [Contrast::Agent::Assess::Properties] the Properties of the ret
|
|
103
|
+
# @param preshift [Contrast::Agent::Assess::PreShift] the capture of the state of the code just prior to
|
|
104
|
+
# the invocation of the patched method
|
|
105
|
+
# @param incoming [String] the new String going into the substitution
|
|
106
|
+
# @param ret [String] the result of the substitution
|
|
107
|
+
# @param global [Boolean] if this was a global or single substitution
|
|
108
|
+
# @return [Array<Range>] the Ranges where substitution occurred
|
|
109
|
+
def find_string_sub_insert properties, preshift, incoming, ret, global
|
|
110
|
+
pattern = preshift.args[0]
|
|
111
|
+
source = preshift.object
|
|
112
|
+
|
|
113
|
+
properties.copy_from(source, ret)
|
|
114
|
+
# Figure out where inserts occurred
|
|
115
|
+
last_idx = 0
|
|
116
|
+
ranges = []
|
|
117
|
+
# For each insert, move the tag ranges
|
|
118
|
+
while last_idx
|
|
119
|
+
idx = source.index(pattern, last_idx)
|
|
120
|
+
break unless idx
|
|
121
|
+
|
|
122
|
+
last_idx = idx ? idx + 1 : nil
|
|
123
|
+
start_index = idx
|
|
124
|
+
end_index = idx + incoming.length
|
|
125
|
+
ranges << (start_index...end_index)
|
|
126
|
+
break unless global
|
|
127
|
+
end
|
|
128
|
+
ranges
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def block_sub self_tracked, source, ret
|
|
132
|
+
return unless self_tracked
|
|
133
|
+
|
|
134
|
+
properties = Contrast::Agent::Assess::Tracker.properties!(ret)
|
|
135
|
+
properties&.splat_from(source, ret)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def hash_sub self_tracked, source, ret
|
|
139
|
+
return unless self_tracked
|
|
140
|
+
|
|
141
|
+
properties = Contrast::Agent::Assess::Tracker.properties!(ret)
|
|
142
|
+
properties&.splat_from(source, ret)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def pattern_gsub parent_events, preshift, ret
|
|
146
|
+
source = preshift.object
|
|
147
|
+
return unless (source_properties = Contrast::Agent::Assess::Tracker.properties(source))
|
|
148
|
+
return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret))
|
|
149
|
+
|
|
150
|
+
source_properties.tag_keys.each do |key|
|
|
151
|
+
properties.add_tag(key, 0...1)
|
|
152
|
+
end
|
|
153
|
+
parent_event = source_properties.event
|
|
154
|
+
parent_events << parent_event if parent_event
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def string_build_event parent_events, patcher, preshift, ret
|
|
158
|
+
return unless Contrast::Agent::Assess::Tracker.tracked?(ret)
|
|
159
|
+
|
|
160
|
+
properties = Contrast::Agent::Assess::Tracker.properties(ret)
|
|
161
|
+
args = preshift.args
|
|
162
|
+
properties.build_event(patcher, ret, preshift.object, ret, args, 2)
|
|
163
|
+
properties.event.instance_variable_set(:@_parent_events, parent_events)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -56,20 +56,20 @@ module Contrast
|
|
|
56
56
|
# Any overlapping ranges are merged before returning
|
|
57
57
|
#
|
|
58
58
|
# arr: the array of tags to which we are adding
|
|
59
|
-
#
|
|
60
|
-
def ordered_merge arr,
|
|
59
|
+
# add_arr: array of Tags or a single Tag to be added
|
|
60
|
+
def ordered_merge arr, add_arr
|
|
61
61
|
# [Contrast::Agent::Assess::Tag, ...]
|
|
62
|
-
if
|
|
63
|
-
return arr unless
|
|
64
|
-
return
|
|
62
|
+
if add_arr.is_a?(Array)
|
|
63
|
+
return arr unless add_arr.any?
|
|
64
|
+
return add_arr unless arr&.any?
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
add_arr.each { |new_element| single_ordered_merge(arr, new_element) }
|
|
67
67
|
# Contrast::Agent::Assess::Tag
|
|
68
68
|
else
|
|
69
|
-
return arr unless
|
|
70
|
-
return [
|
|
69
|
+
return arr unless add_arr
|
|
70
|
+
return [add_arr] unless arr
|
|
71
71
|
|
|
72
|
-
single_ordered_merge(arr,
|
|
72
|
+
single_ordered_merge(arr, add_arr)
|
|
73
73
|
end
|
|
74
74
|
smallerize(arr)
|
|
75
75
|
end
|
|
@@ -19,8 +19,10 @@ module Contrast
|
|
|
19
19
|
"collects usage data in order to help us improve compatibility\n" \
|
|
20
20
|
"and security coverage. The data is anonymous and does not contain application data. It is collected\n" \
|
|
21
21
|
"by Contrast and is never shared. You can opt-out of telemetry by setting the\n" \
|
|
22
|
-
"'CONTRAST_AGENT_TELEMETRY_OPTOUT' environment variable to '1' or 'true'.\n
|
|
23
|
-
|
|
22
|
+
"'CONTRAST_AGENT_TELEMETRY_OPTOUT' environment variable to '1' or 'true'.\n" \
|
|
23
|
+
'Read more about [Contrast Security] [Ruby Agent] telemetry: ' \
|
|
24
|
+
"https://docs.contrastsecurity.com/en/ruby-telemetry.html \n\n" \
|
|
25
|
+
"===================================================================================================\n\n"
|
|
24
26
|
}.cs__freeze
|
|
25
27
|
|
|
26
28
|
# Here we create the .telemetry file. If the file exist we do nothing.
|
|
@@ -0,0 +1,90 @@
|
|
|
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/utils/net_http_base'
|
|
6
|
+
require 'contrast/components/logger'
|
|
7
|
+
require 'contrast/utils/object_share'
|
|
8
|
+
require 'contrast/agent/version'
|
|
9
|
+
require 'json'
|
|
10
|
+
|
|
11
|
+
module Contrast
|
|
12
|
+
module Utils
|
|
13
|
+
# This module creates a Net::HTTP client and initiates a connection to the provided result
|
|
14
|
+
class TelemetryClient < NetHttpBase
|
|
15
|
+
ENDPOINT = 'api/v1/telemetry/metrics' # /TelemetryEvent.path
|
|
16
|
+
SERVICE_NAME = 'Telemetry'
|
|
17
|
+
include Contrast::Components::Logger::InstanceMethods
|
|
18
|
+
# This method initializes the Net::HTTP client we'll need. it will validate
|
|
19
|
+
# the connection and make the first request. If connection is valid and response
|
|
20
|
+
# is available then the open connection is returned.
|
|
21
|
+
#
|
|
22
|
+
# @param url [String]
|
|
23
|
+
# @return [Net::HTTP, nil] Return open connection or nil
|
|
24
|
+
def initialize_connection url
|
|
25
|
+
super(SERVICE_NAME, url, false, false)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# This method will be responsible for building the request. Because the telemetry collector expects to receive
|
|
29
|
+
# multiple events in a single request, we must always wrap the event in an array, even if there is only one.
|
|
30
|
+
#
|
|
31
|
+
# @param event [Contrast::Agent::TelemetryEvent,Contrast::Agent::StartupMetricsTelemetryEvent]
|
|
32
|
+
# @return [Net::HTTP::Post]
|
|
33
|
+
def build_request event
|
|
34
|
+
return unless valid_event? event
|
|
35
|
+
|
|
36
|
+
string_body = [event.to_hash].to_json
|
|
37
|
+
header = {
|
|
38
|
+
'User-Agent' => "<#{ Contrast::Utils::ObjectShare::RUBY }>-<#{ Contrast::Agent::VERSION }>",
|
|
39
|
+
'Content-Type' => 'application/json'
|
|
40
|
+
}
|
|
41
|
+
request = Net::HTTP::Post.new(build_path(event.path), header)
|
|
42
|
+
request.body = string_body
|
|
43
|
+
request
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# This method will create the actual request and send it
|
|
47
|
+
# @param event[Contrast::Agent::TelemetryEvent]
|
|
48
|
+
# @param connection[Net::HTTP]
|
|
49
|
+
def send_request event, connection
|
|
50
|
+
return if connection.nil? || event.nil?
|
|
51
|
+
return unless valid_event? event
|
|
52
|
+
|
|
53
|
+
req = build_request event
|
|
54
|
+
connection.request req
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# This method will handle the response from the tenant
|
|
58
|
+
# @param res [Net::HTTPResponse]
|
|
59
|
+
# @return sleep_time [Integer, nil]
|
|
60
|
+
def handle_response res
|
|
61
|
+
status_code = res.code.to_i
|
|
62
|
+
ready_after = if res.to_hash.keys.map(&:downcase).include?('ready-after')
|
|
63
|
+
res['Ready-After']
|
|
64
|
+
else
|
|
65
|
+
60
|
|
66
|
+
end
|
|
67
|
+
ready_after if status_code == 429
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# This method will be responsible for validating the event
|
|
71
|
+
# @param event[Contrast::Agent::TelemetryEvent,Contrast::Agent::StartupMetricsTelemetryEvent]
|
|
72
|
+
def valid_event? event
|
|
73
|
+
return true if event.cs__is_a?(Contrast::Agent::TelemetryEvent)
|
|
74
|
+
return true if event.cs__is_a?(Contrast::Agent::StartupMetricsTelemetryEvent)
|
|
75
|
+
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# The telemetry instance accepts any path to #{ Contrast::Agent::Telemetry::URL }#{ ENDPOINT }, using the
|
|
82
|
+
# remainder of the path to segregate messages.
|
|
83
|
+
#
|
|
84
|
+
# @return [String] the fully qualified path to send the request
|
|
85
|
+
def build_path event_path
|
|
86
|
+
"#{ Contrast::Agent::Telemetry::URL }#{ ENDPOINT }#{ event_path }"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -34,11 +34,8 @@ module Contrast
|
|
|
34
34
|
# @return [String, nil] MAC address of the primary network interface or
|
|
35
35
|
# the first available one, or nil if nothing found
|
|
36
36
|
def self.mac
|
|
37
|
-
|
|
38
|
-
@_mac = find_mac
|
|
39
|
-
# or find any available
|
|
40
|
-
@_mac = find_mac if @_mac.nil?
|
|
41
|
-
@_mac
|
|
37
|
+
primary = Contrast::Utils::OS.mac? ? MAC_OS_PRIMARY : LINUX_PRIMARY
|
|
38
|
+
@_mac = find_mac(primary) || find_mac
|
|
42
39
|
end
|
|
43
40
|
|
|
44
41
|
class << self
|
|
@@ -60,13 +57,15 @@ module Contrast
|
|
|
60
57
|
while idx < interfaces.length
|
|
61
58
|
addr = interfaces[idx].addr
|
|
62
59
|
name = interfaces[idx].name # rubocop:disable Security/Module/Name
|
|
63
|
-
# retrieving MAC address from primary network interface or first available
|
|
64
|
-
mac = retrieve_mac name, addr, primary
|
|
65
60
|
idx += 1
|
|
61
|
+
next if primary && !name.include?(primary)
|
|
62
|
+
|
|
63
|
+
# retrieving MAC address from primary network interface or first available
|
|
64
|
+
mac = retrieve_mac addr
|
|
66
65
|
next unless mac
|
|
67
66
|
|
|
68
67
|
result = mac if mac && (mac.match? MAC_REGEX)
|
|
69
|
-
break if result
|
|
68
|
+
break if result
|
|
70
69
|
end
|
|
71
70
|
result
|
|
72
71
|
end
|
|
@@ -74,35 +73,26 @@ module Contrast
|
|
|
74
73
|
# Retrieves MAC address for primary or any network interface.
|
|
75
74
|
# This is OS dependent search.
|
|
76
75
|
#
|
|
77
|
-
# @param name [Sting] interface name of ifaddr
|
|
78
76
|
# @param addr [String] address info
|
|
79
77
|
# example: #<Addrinfo: LINK[en0 aa:bb:cc:00:11:22]>
|
|
80
|
-
# @param primary [nil, String] optional param if set look only for primary
|
|
81
|
-
# network adapter's name
|
|
82
78
|
# @return mac [nil, String] MAC address of primary network interface,
|
|
83
79
|
# any network interface, or nil if no interface is found.
|
|
84
|
-
def retrieve_mac
|
|
85
|
-
mac = nil
|
|
80
|
+
def retrieve_mac addr
|
|
86
81
|
# Mac OS allow us to use getnameinfo(sockaddr [, flags]) => [hostname, servicename]
|
|
87
82
|
#
|
|
88
83
|
# returned address:
|
|
89
84
|
# <Socket::Ifaddr en0 UP,BROADCAST,RUNNING,NOTRAILERS,SIMPLEX,MULTICAST LINK[en0 aa:bb:cc:00:11:22]>
|
|
90
|
-
if Contrast::Utils::OS.mac?
|
|
91
|
-
|
|
92
|
-
mac = addr.getnameinfo[0] if primary && name.include?(primary)
|
|
93
|
-
end
|
|
85
|
+
return addr.getnameinfo[0] if Contrast::Utils::OS.mac?
|
|
86
|
+
|
|
94
87
|
# In Linux using Socket::addr#getnameinfo results in ai_family not supported exception.
|
|
95
88
|
# In this case we are relying on match filtering of addresses.
|
|
96
89
|
#
|
|
97
90
|
# returned address:
|
|
98
91
|
# #<Socket::Ifaddr eth0 UP,BROADCAST,RUNNING,MULTICAST,0x10000
|
|
99
92
|
# PACKET[protocol=0 eth0 hatype=1 HOST hwaddr=aa:bb:cc:00:11:22]>
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
mac = Regexp.last_match(1) if addr.inspect =~ LINUX_OS_REG
|
|
104
|
-
end
|
|
105
|
-
mac
|
|
93
|
+
return Regexp.last_match(1) if addr.inspect =~ LINUX_OS_REG
|
|
94
|
+
|
|
95
|
+
nil
|
|
106
96
|
end
|
|
107
97
|
|
|
108
98
|
# Returns array of network interfaces.
|
|
@@ -111,7 +101,10 @@ module Contrast
|
|
|
111
101
|
# @return interfaces [Array] Returns an array of interface addresses.
|
|
112
102
|
# Socket::Ifaddr - represents a result of getifaddrs().
|
|
113
103
|
def interfaces
|
|
114
|
-
@_interfaces
|
|
104
|
+
@_interfaces ||= []
|
|
105
|
+
|
|
106
|
+
return @_interfaces unless @_interfaces.empty?
|
|
107
|
+
|
|
115
108
|
arr = Socket.getifaddrs
|
|
116
109
|
idx = 0
|
|
117
110
|
check_family = 0
|
data/ruby-agent.gemspec
CHANGED
|
@@ -89,11 +89,11 @@ end
|
|
|
89
89
|
|
|
90
90
|
# Dependencies used to run all of our Rubocop during the linting phase.
|
|
91
91
|
def self.add_rubocop spec
|
|
92
|
-
spec.add_development_dependency 'rubocop', '1.
|
|
93
|
-
spec.add_development_dependency 'rubocop-performance', '1.
|
|
94
|
-
spec.add_development_dependency 'rubocop-rails', '2.
|
|
95
|
-
spec.add_development_dependency 'rubocop-rake', '0.
|
|
96
|
-
spec.add_development_dependency 'rubocop-rspec', '2.
|
|
92
|
+
spec.add_development_dependency 'rubocop', '1.22.3'
|
|
93
|
+
spec.add_development_dependency 'rubocop-performance', '1.12.0'
|
|
94
|
+
spec.add_development_dependency 'rubocop-rails', '2.12.4'
|
|
95
|
+
spec.add_development_dependency 'rubocop-rake', '0.6.0'
|
|
96
|
+
spec.add_development_dependency 'rubocop-rspec', '2.6.0'
|
|
97
97
|
end
|
|
98
98
|
|
|
99
99
|
# Dependencies not mocked out during RSpec that we test real code of, beyond just frameworks.
|