contrast-agent 7.1.0 → 7.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ext/extconf_common.rb +88 -14
- data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
- data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
- data/lib/contrast/agent/excluder/excluder.rb +64 -31
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
- data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
- data/lib/contrast/agent/protect/rule/base.rb +9 -7
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
- data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
- data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
- data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
- data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
- data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
- data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
- data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
- data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
- data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
- data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
- data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
- data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
- data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
- data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
- data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
- data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
- data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
- data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
- data/lib/contrast/agent/reporting/client/interface.rb +132 -0
- data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
- data/lib/contrast/agent/reporting/connection_status.rb +0 -1
- data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
- data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
- data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
- data/lib/contrast/agent/reporting/reporter.rb +11 -26
- data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
- data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
- data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
- data/lib/contrast/agent/request/request.rb +27 -12
- data/lib/contrast/agent/telemetry/base.rb +44 -19
- data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
- data/lib/contrast/agent/telemetry/client.rb +10 -2
- data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
- data/lib/contrast/agent/telemetry/exception.rb +1 -0
- data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
- data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
- data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
- data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
- data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
- data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/components/config/sources.rb +6 -5
- data/lib/contrast/components/config.rb +4 -4
- data/lib/contrast/components/protect.rb +11 -1
- data/lib/contrast/components/sampling.rb +15 -10
- data/lib/contrast/components/settings.rb +9 -0
- data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
- data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
- data/lib/contrast/config/diagnostics/tools.rb +4 -4
- data/lib/contrast/config/validate.rb +2 -2
- data/lib/contrast/config/yaml_file.rb +8 -0
- data/lib/contrast/configuration.rb +11 -19
- data/lib/contrast/framework/grape/support.rb +1 -2
- data/lib/contrast/framework/manager.rb +17 -8
- data/lib/contrast/framework/rack/support.rb +99 -1
- data/lib/contrast/framework/rails/support.rb +4 -2
- data/lib/contrast/framework/sinatra/support.rb +1 -2
- data/lib/contrast/logger/aliased_logging.rb +18 -9
- data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
- data/lib/contrast/utils/hash_utils.rb +21 -2
- data/lib/contrast/utils/metrics_hash.rb +1 -1
- data/lib/contrast/utils/object_share.rb +2 -1
- data/lib/contrast/utils/request_utils.rb +14 -0
- data/lib/contrast/utils/response_utils.rb +12 -0
- data/lib/contrast/utils/timer.rb +2 -0
- data/lib/contrast.rb +9 -2
- data/resources/assess/policy.json +11 -0
- data/ruby-agent.gemspec +1 -1
- metadata +25 -7
- data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
- data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -4,7 +4,6 @@
|
|
4
4
|
require 'contrast/utils/metrics_hash'
|
5
5
|
require 'contrast/agent/telemetry/metric_event'
|
6
6
|
require 'contrast/agent/version'
|
7
|
-
require 'contrast/utils/os'
|
8
7
|
|
9
8
|
module Contrast
|
10
9
|
module Agent
|
@@ -15,8 +14,6 @@ module Contrast
|
|
15
14
|
# application framework and version and server framework
|
16
15
|
# It will be initialized and send in Middleware#agent_startup_routine
|
17
16
|
class StartupMetricsEvent < Contrast::Agent::Telemetry::MetricEvent
|
18
|
-
include Contrast::Utils::OS
|
19
|
-
|
20
17
|
APP_AND_SERVER_DATA = ::Contrast::APP_CONTEXT.app_and_server_information.cs__freeze
|
21
18
|
# Multi-tenant Production Environments
|
22
19
|
SAAS_DEFAULT = { addr: 'app.contrastsecurity.com', type: 'SAAS_DEFAULT' }.cs__freeze
|
@@ -82,11 +79,6 @@ module Contrast
|
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
85
|
-
def sys_info
|
86
|
-
@sys_info ||= get_system_information if @sys_info.nil?
|
87
|
-
@sys_info
|
88
|
-
end
|
89
|
-
|
90
82
|
private
|
91
83
|
|
92
84
|
# Here we extract the TeamServer url type
|
@@ -11,14 +11,15 @@ module Contrast
|
|
11
11
|
# This component encapsulates storing the source for each entry in the config,
|
12
12
|
# so that we can report on where the value was set from.
|
13
13
|
class Sources
|
14
|
-
ENVIRONMENT_VARIABLE = '
|
15
|
-
COMMAND_LINE = '
|
16
|
-
CONTRAST_UI = '
|
17
|
-
DEFAULT_VALUE = '
|
14
|
+
ENVIRONMENT_VARIABLE = 'ENVIRONMENT_VARIABLE'
|
15
|
+
COMMAND_LINE = 'COMMAND_LINE'
|
16
|
+
CONTRAST_UI = 'CONTRAST_UI'
|
17
|
+
DEFAULT_VALUE = 'DEFAULT_VALUE'
|
18
|
+
APP_CONFIGURATION_FILE = 'USER_CONFIGURATION_FILE'
|
18
19
|
# Order matters for the Configurations files. This is read when Agent starts up and will always go
|
19
20
|
# through the YAML as priority.
|
20
21
|
# Do not change the order!
|
21
|
-
|
22
|
+
APP_CONFIGURATION_EXTENSIONS = %w[yaml yml].cs__freeze
|
22
23
|
|
23
24
|
# @return [Hash]
|
24
25
|
attr_reader :data
|
@@ -25,7 +25,7 @@ module Contrast
|
|
25
25
|
# time than to silently fail to deliver functionality.
|
26
26
|
module Config
|
27
27
|
CONTRAST_ENV_MARKER = 'CONTRAST__'
|
28
|
-
CONTRAST_LOG = '
|
28
|
+
CONTRAST_LOG = 'contrast.log'
|
29
29
|
CONTRAST_NAME = 'Contrast Agent'
|
30
30
|
DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z'
|
31
31
|
|
@@ -63,7 +63,7 @@ module Contrast
|
|
63
63
|
env_overrides
|
64
64
|
validate
|
65
65
|
rescue ArgumentError => e
|
66
|
-
proto_logger.error('Configuration failed with error: ', e)
|
66
|
+
proto_logger.error('[PROTO_LOGGER] Configuration failed with error: ', e)
|
67
67
|
end
|
68
68
|
alias_method :rebuild, :build
|
69
69
|
|
@@ -157,7 +157,7 @@ module Contrast
|
|
157
157
|
# @return [boolean]
|
158
158
|
def valid_session_metadata?
|
159
159
|
if !session_id&.empty? && !session_metadata&.empty?
|
160
|
-
proto_logger.error(SESSION_VARIABLES)
|
160
|
+
proto_logger.error("[PROTO_LOGGER] #{ SESSION_VARIABLES }")
|
161
161
|
return false
|
162
162
|
end
|
163
163
|
true
|
@@ -172,7 +172,7 @@ module Contrast
|
|
172
172
|
msg << API_KEY unless api_key
|
173
173
|
msg << API_SERVICE_KEY unless api_service_key
|
174
174
|
msg << API_USERNAME unless api_username
|
175
|
-
msg.any? { |m| proto_logger.error(m) }
|
175
|
+
msg.any? { |m| proto_logger.error("[PROTO_LOGGER] #{ m }") }
|
176
176
|
msg.empty?
|
177
177
|
end
|
178
178
|
|
@@ -15,12 +15,14 @@ module Contrast
|
|
15
15
|
include Contrast::Config::BaseConfiguration
|
16
16
|
|
17
17
|
CANON_NAME = 'protect'
|
18
|
-
CONFIG_VALUES = %w[enabled?].cs__freeze
|
18
|
+
CONFIG_VALUES = %w[enabled? normalize_base64?].cs__freeze
|
19
19
|
RULES = 'rules'
|
20
20
|
MODE = 'mode'
|
21
21
|
|
22
22
|
# @return [Boolean, nil]
|
23
23
|
attr_accessor :enable
|
24
|
+
# @return [Boolean, nil]
|
25
|
+
attr_accessor :normalize_base64
|
24
26
|
# @return [String]
|
25
27
|
attr_reader :canon_name
|
26
28
|
# @return [Array]
|
@@ -36,6 +38,7 @@ module Contrast
|
|
36
38
|
@_exceptions = Contrast::Config::ExceptionConfiguration.new(hsh[:exceptions])
|
37
39
|
@_rules = Contrast::Config::ProtectRulesConfiguration.new(hsh[:rules])
|
38
40
|
@enable = hsh[:enable]
|
41
|
+
@normalize_base64 = hsh[:normalize_base64]
|
39
42
|
@agent_lib = hsh[:agent_lib]
|
40
43
|
end
|
41
44
|
|
@@ -68,6 +71,13 @@ module Contrast
|
|
68
71
|
::Contrast::SETTINGS.protect_state.enabled == true
|
69
72
|
end
|
70
73
|
|
74
|
+
# Check to determine if the base64 decoding is required for user inputs.
|
75
|
+
def normalize_base64?
|
76
|
+
@normalize_base64 = Contrast::CONFIG.protect.normalize_base64 if @normalize_base64.nil?
|
77
|
+
|
78
|
+
true?(@normalize_base64)
|
79
|
+
end
|
80
|
+
|
71
81
|
# Current Configuration for the protect rules
|
72
82
|
#
|
73
83
|
# @return [Contrast::Config::ProtectRulesConfiguration]
|
@@ -2,6 +2,7 @@
|
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
4
|
require 'contrast/components/base'
|
5
|
+
require 'contrast/utils/duck_utils'
|
5
6
|
|
6
7
|
module Contrast
|
7
8
|
module Components
|
@@ -27,7 +28,7 @@ module Contrast
|
|
27
28
|
config_settings = ::Contrast::CONFIG.assess&.sampling
|
28
29
|
settings = ::Contrast::SETTINGS&.assess_state&.sampling_settings
|
29
30
|
{
|
30
|
-
enabled: enabled?(config_settings, settings),
|
31
|
+
enabled: enabled?(config_settings&.enable, settings&.enabled),
|
31
32
|
baseline: check_baseline(config_settings, settings),
|
32
33
|
request_frequency: check_request_frequency(config_settings, settings),
|
33
34
|
response_frequency: check_response_frequency(config_settings, settings),
|
@@ -43,13 +44,20 @@ module Contrast
|
|
43
44
|
|
44
45
|
private
|
45
46
|
|
46
|
-
# @param
|
47
|
+
# @param config_value [Boolean, nil] the Sampling configuration as provided by
|
47
48
|
# local user input
|
48
|
-
# @param
|
49
|
+
# @param settings_value [Boolean, nil] the Sampling settings as provided by
|
49
50
|
# TeamServer
|
50
51
|
# @return [Boolean] the resolution of the config_settings, settings, and default value
|
51
|
-
def enabled?
|
52
|
-
|
52
|
+
def enabled? config_value, settings_value
|
53
|
+
# Check the Assess State received from TS:
|
54
|
+
sampling_enable = settings_value unless Contrast::Utils::DuckUtils.empty_duck?(settings_value)
|
55
|
+
# Check for local settings, YAML, ENV, CLI (it's with higher priority than Web Interface)
|
56
|
+
# see: https://docs.contrastsecurity.com/en/order-of-precedence.html
|
57
|
+
sampling_enable = config_value unless Contrast::Utils::DuckUtils.empty_duck?(config_value)
|
58
|
+
# Use default value, unless a false or true is set from local or TS settings.
|
59
|
+
sampling_enable = DEFAULT_SAMPLING_ENABLED if Contrast::Utils::DuckUtils.empty_duck?(sampling_enable)
|
60
|
+
true?(sampling_enable)
|
53
61
|
end
|
54
62
|
|
55
63
|
# @param config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by
|
@@ -106,6 +114,8 @@ module Contrast
|
|
106
114
|
NAME_PREFIX = "#{ CONTRAST }.#{ CANON_NAME }".cs__freeze
|
107
115
|
CONFIG_VALUES = %w[enable baseline request_frequency response_frequency window_ms].cs__freeze
|
108
116
|
|
117
|
+
# @return [Boolean, nil]
|
118
|
+
attr_accessor :enable
|
109
119
|
# @return [Integer, nil]
|
110
120
|
attr_accessor :baseline
|
111
121
|
# @return [Integer, nil]
|
@@ -128,11 +138,6 @@ module Contrast
|
|
128
138
|
@window_ms = hsh[:window_ms]
|
129
139
|
end
|
130
140
|
|
131
|
-
# @return [Boolean, false]
|
132
|
-
def enable
|
133
|
-
!!@enable
|
134
|
-
end
|
135
|
-
|
136
141
|
# Converts current configuration to effective config values class and appends them to
|
137
142
|
# EffectiveConfig class.
|
138
143
|
#
|
@@ -5,6 +5,7 @@ require 'contrast/agent/excluder/excluder'
|
|
5
5
|
require 'contrast/agent/reporting/settings/sensitive_data_masking'
|
6
6
|
require 'contrast/components/config'
|
7
7
|
require 'contrast/components/logger'
|
8
|
+
require 'contrast/utils/duck_utils'
|
8
9
|
|
9
10
|
module Contrast
|
10
11
|
module Components
|
@@ -219,6 +220,14 @@ module Contrast
|
|
219
220
|
exclusions.input_exclusions.each do |exclusion|
|
220
221
|
matchers << Contrast::Agent::ExclusionMatcher.new(exclusion)
|
221
222
|
end
|
223
|
+
# Do not populate the matchers unless we have any. There are certain checks in
|
224
|
+
# SourceMethod that will safe-guard return if there are no exclusions received.
|
225
|
+
# The matching operation is expensive, and the excluder calls are made for each
|
226
|
+
# source, and we do not want to check for exclusions if they are empty. This is
|
227
|
+
# probably redundant as all exclusions default to empty, but will save useless
|
228
|
+
# new object creation at very least.
|
229
|
+
return if Contrast::Utils::DuckUtils.empty_duck?(matchers)
|
230
|
+
|
222
231
|
@excluder = Contrast::Agent::Excluder.new(matchers)
|
223
232
|
end
|
224
233
|
|
@@ -11,7 +11,9 @@ module Contrast
|
|
11
11
|
# Reads All ENV variables.
|
12
12
|
module EnvironmentVariables
|
13
13
|
class << self
|
14
|
-
NON_COMMON_ENV = %w[
|
14
|
+
NON_COMMON_ENV = %w[
|
15
|
+
CONTRAST_CONFIG_PATH CONTRAST_AGENT_TELEMETRY_OPTOUT CONTRAST_AGENT_TELEMETRY_TEST
|
16
|
+
].cs__freeze
|
15
17
|
|
16
18
|
# This method will fill the canonical name for each env var and will check for any uncommon ones.
|
17
19
|
#
|
@@ -41,7 +41,11 @@ module Contrast
|
|
41
41
|
# @param source [String] name of the source file yaml | yml
|
42
42
|
# @return [Array<String>, nil]
|
43
43
|
def assign_filename source
|
44
|
-
Contrast::Components::Config::Sources::
|
44
|
+
Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS.each do |type|
|
45
|
+
# We use the source to transfer the file's name from the mapping of the extensions.
|
46
|
+
# This is done b/c the user_configuration file has a second field to be filled,
|
47
|
+
# and other sources don't. Transfer the source as filename and set the default value
|
48
|
+
# for it later when we find the sources.
|
45
49
|
instance_variable_set(:@filename, source) if source.include?(".#{ type.downcase }")
|
46
50
|
end
|
47
51
|
end
|
@@ -164,10 +164,10 @@ module Contrast
|
|
164
164
|
# For files we keep the whole path as source.
|
165
165
|
source = Contrast::CONFIG.sources.get(new_effective_value.canonical_name)
|
166
166
|
new_effective_value.assign_filename(source)
|
167
|
-
new_source = if source.include?(Contrast::Config::LocalSourceValue::YAML_EXT)
|
168
|
-
|
169
|
-
|
170
|
-
Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
|
167
|
+
new_source = if source.include?(Contrast::Config::LocalSourceValue::YAML_EXT) ||
|
168
|
+
source.include?(Contrast::Config::LocalSourceValue::YML_EXT)
|
169
|
+
|
170
|
+
Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
|
171
171
|
else
|
172
172
|
Contrast::Components::Config::Sources::DEFAULT_VALUE
|
173
173
|
end
|
@@ -108,9 +108,9 @@ module Contrast
|
|
108
108
|
|
109
109
|
def test_connection reporter
|
110
110
|
puts(" Connection failed #{ FAIL }") unless reporter
|
111
|
-
connection = reporter.
|
111
|
+
connection = reporter.client.send(:reporting_connection)
|
112
112
|
abort("Failed to Initialize Connection please check error logs for details #{ FAIL } ") unless connection
|
113
|
-
abort('Failed to Start Client please check error logs for details') unless reporter.client.startup
|
113
|
+
abort('Failed to Start Client please check error logs for details') unless reporter.client.startup
|
114
114
|
last_response = reporter.client.response_handler.last_response_code
|
115
115
|
if last_response.include?('40')
|
116
116
|
puts(" Last response code: #{ last_response } #{ FAIL }")
|
@@ -11,6 +11,8 @@ module Contrast
|
|
11
11
|
# instrumentation.
|
12
12
|
module YamlFile
|
13
13
|
CONFIG_FILE_NAME = 'contrast_security'
|
14
|
+
CONTRAST_ENV_MARKER = 'CONTRAST__'
|
15
|
+
|
14
16
|
EXT = { yml: 'yml', yaml: 'yaml' }.freeze # rubocop:disable Security/Object/Freeze
|
15
17
|
|
16
18
|
POSSIBLE_TARGET_PATHS = %w[
|
@@ -89,6 +91,8 @@ module Contrast
|
|
89
91
|
#
|
90
92
|
def create
|
91
93
|
# rubocop:disable Rails/Output
|
94
|
+
return puts("\u{02C3} Contrast configuration set by ENV variables.") if env_config_set?
|
95
|
+
|
92
96
|
puts("\u{1F48E} Generating: Contrast Configuration file.")
|
93
97
|
if Contrast::Config::YamlFile.created?
|
94
98
|
puts("\u{2705} Configuration file already exists: #{ Contrast::Config::YamlFile.find! }")
|
@@ -123,6 +127,10 @@ module Contrast
|
|
123
127
|
def file_name
|
124
128
|
ENV['CONTRAST_YAML_FILE_TEST_CREATE_CONFIG_FILE_NAME_VALUE'] || CONFIG_FILE_NAME
|
125
129
|
end
|
130
|
+
|
131
|
+
def env_config_set?
|
132
|
+
ENV.keys&.select { |config| config.include?(CONTRAST_ENV_MARKER) }&.any?
|
133
|
+
end
|
126
134
|
end
|
127
135
|
end
|
128
136
|
end
|
@@ -81,11 +81,11 @@ module Contrast
|
|
81
81
|
# Overlay CLI options - they take precedence over config file
|
82
82
|
cli_options = Contrast::Utils::HashUtils.deep_symbolize_all_keys(cli_options)
|
83
83
|
if cli_options
|
84
|
-
config_kv = Contrast::Utils::HashUtils.
|
84
|
+
config_kv = Contrast::Utils::HashUtils.precedence_merge(cli_options, config_kv)
|
85
85
|
@_source_file_extensions = Contrast::Utils::HashUtils.
|
86
|
-
|
87
|
-
|
88
|
-
|
86
|
+
precedence_merge(assign_source_to(cli_options,
|
87
|
+
Contrast::Components::Config::Sources::COMMAND_LINE),
|
88
|
+
@_source_file_extensions)
|
89
89
|
end
|
90
90
|
|
91
91
|
# Some in-flight rewrites to maintain backwards compatibility
|
@@ -157,7 +157,7 @@ module Contrast
|
|
157
157
|
@_configuration_paths ||= begin
|
158
158
|
basename = default_name.split('.').first
|
159
159
|
# Order of extensions comes from here:
|
160
|
-
extensions = Contrast::Components::Config::Sources::
|
160
|
+
extensions = Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS
|
161
161
|
|
162
162
|
paths = []
|
163
163
|
# Environment paths takes precedence here. Look first through them.
|
@@ -216,7 +216,6 @@ module Contrast
|
|
216
216
|
end
|
217
217
|
origin.add_source_file(path, (yaml_to_hash(path) || {}))
|
218
218
|
end
|
219
|
-
|
220
219
|
# Legacy usage: Assign main configuration file for reference.
|
221
220
|
@config_file = origin.main_file
|
222
221
|
# merge all settings keeping the top yaml files values as priority.
|
@@ -225,13 +224,15 @@ module Contrast
|
|
225
224
|
# precedence of paths: see Contrast::Configuration::CONFIG_BASE_PATHS
|
226
225
|
extensions_maps = []
|
227
226
|
origin.source_files.each do |file|
|
228
|
-
|
229
|
-
precedence_merge!(config, file.values)
|
227
|
+
config = Contrast::Utils::HashUtils.precedence_merge(config, file.values)
|
230
228
|
# assign source values extentions:
|
231
229
|
extensions_maps << assign_source_to(Contrast::Utils::HashUtils.deep_symbolize_all_keys(file.values), file.path)
|
232
230
|
end
|
231
|
+
|
233
232
|
# merge all origin paths to be used as extension classification to preserve the precedence of config files:
|
234
|
-
extensions_maps.each
|
233
|
+
extensions_maps.each do |path|
|
234
|
+
@_source_file_extensions = Contrast::Utils::HashUtils.precedence_merge!(@_source_file_extensions, path)
|
235
|
+
end
|
235
236
|
|
236
237
|
config
|
237
238
|
end
|
@@ -357,7 +358,7 @@ module Contrast
|
|
357
358
|
KEYS_TO_REDACT.include?(key.to_sym)
|
358
359
|
end
|
359
360
|
|
360
|
-
def assign_source_to hash, source = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
|
361
|
+
def assign_source_to hash, source = Contrast::Components::Config::Sources::APP_CONFIGURATION_FILE
|
361
362
|
hash.transform_values do |value|
|
362
363
|
if value.is_a?(Hash)
|
363
364
|
assign_source_to(value, source)
|
@@ -366,14 +367,5 @@ module Contrast
|
|
366
367
|
end
|
367
368
|
end
|
368
369
|
end
|
369
|
-
|
370
|
-
# Merges two hashes, first hash will preserve it's values and will only add unique values.
|
371
|
-
#
|
372
|
-
# @param hsh [Hash]
|
373
|
-
# @param other_hsh [Hash]
|
374
|
-
# @return [Hash]
|
375
|
-
def precedence_merge! hsh, other_hsh
|
376
|
-
hsh.merge!(other_hsh) { |_key, old_val, _new_val| old_val }
|
377
|
-
end
|
378
370
|
end
|
379
371
|
end
|
@@ -72,8 +72,7 @@ module Contrast
|
|
72
72
|
|
73
73
|
# @param request [Contrast::Agent::Request] a contrast tracked request.
|
74
74
|
# @param controller [::Grape::API] optionally use this controller instead of global ::Grape::API.
|
75
|
-
# @return [Contrast::Agent::Reporting::RouteCoverage, nil]
|
76
|
-
# matched to the request if a match was found.
|
75
|
+
# @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
|
77
76
|
def current_route_coverage request, controller = ::Grape::API, full_route = nil
|
78
77
|
return unless grape_controller?(controller)
|
79
78
|
|
@@ -37,6 +37,9 @@ module Contrast
|
|
37
37
|
logger.info('Framework detected. Enabling support.', framework: framework_klass.detection_class)
|
38
38
|
framework_klass
|
39
39
|
end
|
40
|
+
|
41
|
+
# Delete Rack if we have more than one framework detected
|
42
|
+
@_frameworks.delete(Contrast::Framework::Rack::Support) if @_frameworks.length > 1
|
40
43
|
@_frameworks.compact!
|
41
44
|
end
|
42
45
|
|
@@ -87,16 +90,22 @@ module Contrast
|
|
87
90
|
# this particular Request
|
88
91
|
# @return [::Rack::Request] either a rack request or subclass thereof.
|
89
92
|
def retrieve_request env
|
90
|
-
|
91
|
-
|
92
|
-
return Contrast::Framework::Rails::Support.retrieve_request(env)
|
93
|
+
if Contrast::Utils::DuckUtils.empty_duck?(@_frameworks)
|
94
|
+
return Contrast::Framework::Rack::Support.retrieve_request(env)
|
93
95
|
end
|
94
96
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
97
|
+
framework = @_frameworks[0]
|
98
|
+
|
99
|
+
case framework.cs__name
|
100
|
+
when 'Contrast::Framework::Rails::Support'
|
101
|
+
Contrast::Framework::Rails::Support.retrieve_request(env)
|
102
|
+
when 'Contrast::Framework::Grape::Support'
|
103
|
+
Contrast::Framework::Grape::Support.retrieve_request(env)
|
104
|
+
when 'Contrast::Framework::Sinatra::Support'
|
105
|
+
Contrast::Framework::Sinatra::Support.retrieve_request(env)
|
106
|
+
else
|
107
|
+
Contrast::Framework::Rack::Support.retrieve_request(env)
|
108
|
+
end
|
100
109
|
rescue StandardError => e
|
101
110
|
logger.warn('Unable to retrieve_request', e)
|
102
111
|
end
|
@@ -14,7 +14,105 @@ module Contrast
|
|
14
14
|
extend Contrast::Framework::Rack::Patch::Support
|
15
15
|
class << self
|
16
16
|
def detection_class
|
17
|
-
'
|
17
|
+
'Rack'
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [String] the Rack version
|
21
|
+
def version
|
22
|
+
::Rack.version
|
23
|
+
rescue StandardError
|
24
|
+
''
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String] the Rack application name
|
28
|
+
def application_name
|
29
|
+
'Rack Application'
|
30
|
+
end
|
31
|
+
|
32
|
+
def application_root
|
33
|
+
Dir.pwd
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] the server type
|
37
|
+
def server_type
|
38
|
+
'Rack'
|
39
|
+
end
|
40
|
+
|
41
|
+
# Find all the predefined routes for this application
|
42
|
+
#
|
43
|
+
# Extracting the Rack application routes is not trivial. Routes are evaluated dynamically
|
44
|
+
# when a request comes in, so they are not loaded before and stored in a data structure
|
45
|
+
# available somewhere. This mean that route discovery is only available through the rack map,
|
46
|
+
# but this is limited as not showing the actual method (GET, POST, etc...). For now The Agent
|
47
|
+
# will use only the current_route_coverage for Rack applications.
|
48
|
+
#
|
49
|
+
# @return [Array<Contrast::Agent::Reporting::DiscoveredRoute>]
|
50
|
+
# @raise [NoMethodError] raises error if subclass does not implement this method
|
51
|
+
def collect_routes
|
52
|
+
# return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless defined?(Rack)
|
53
|
+
# Rack::URLMap is used for mapping different rack apps to different paths.
|
54
|
+
# The Rack app could be separated into smaller rack applications.
|
55
|
+
# Rack::Builder is another option.
|
56
|
+
# return Contrast::Utils::ObjectShare::EMPTY_ARRAY unless rack_map
|
57
|
+
|
58
|
+
# This method is disabled for now, as it is not returning the actual routes. Code is left for as
|
59
|
+
# comment for future reference.
|
60
|
+
#
|
61
|
+
# routes = []
|
62
|
+
# rack_map.any? do |path, meta|
|
63
|
+
# routes << Contrast::Agent::Reporting::DiscoveredRoute.from_rack_route(meta[1], meta[0], path)
|
64
|
+
# end
|
65
|
+
# routes
|
66
|
+
Contrast::Utils::ObjectShare::EMPTY_ARRAY
|
67
|
+
end
|
68
|
+
|
69
|
+
# Given the current request - return a RouteCoverage object
|
70
|
+
|
71
|
+
# @param request [Contrast::Agent::Request] a contrast tracked request.
|
72
|
+
# @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
|
73
|
+
# @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
|
74
|
+
def current_route_coverage request, _controller = nil, full_route = nil
|
75
|
+
method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
|
76
|
+
|
77
|
+
full_route ||= request.env[::Rack::PATH_INFO]
|
78
|
+
return unless full_route && method
|
79
|
+
|
80
|
+
route_coverage = Contrast::Agent::Reporting::RouteCoverage.new
|
81
|
+
# We might not have controller, or even if there is defined one, it could not bare the name of the
|
82
|
+
# route to match as an object, it could be one router class with base controller with several methods
|
83
|
+
# describing each class, search for final controller might be resource heavy, and not efficient.
|
84
|
+
# For now to identify the controller the Agent will use the route name, this may lead to recording
|
85
|
+
# of false routes, but it is better than nothing. If route do no match a pattern it is a good practice
|
86
|
+
# to notify the user by displaying a not found page, in a sense this is a exercise of the application, but
|
87
|
+
# not correctly recorded controller name. Try to see if there is a define Rack::URLMap, and use it first.
|
88
|
+
mapped_controller = rack_map[full_route]&.last
|
89
|
+
final_controller = mapped_controller || full_route
|
90
|
+
route_coverage.attach_rack_based_data(final_controller, method, nil, full_route)
|
91
|
+
route_coverage
|
92
|
+
end
|
93
|
+
|
94
|
+
# Try and get map of Rack application { "path" => ["pattern", "controller"] }.
|
95
|
+
#
|
96
|
+
# @return [Hash<String, Array<String>>] the rack map
|
97
|
+
def rack_map
|
98
|
+
rack_map = {}
|
99
|
+
maps = ObjectSpace.each_object(::Rack::URLMap).to_a
|
100
|
+
maps.any? do |map|
|
101
|
+
mapping = map.instance_variable_get(:@mapping)
|
102
|
+
mapping.any? do |arr|
|
103
|
+
path = arr[1]
|
104
|
+
pattern = arr[2]
|
105
|
+
controller = arr[3]&.cs__class&.cs__name
|
106
|
+
rack_map[path] = [pattern, controller] if path&.cs__is_a?(String) && controller
|
107
|
+
end
|
108
|
+
end
|
109
|
+
rack_map
|
110
|
+
rescue StandardError
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
|
114
|
+
def retrieve_request env
|
115
|
+
::Rack::Request.new(env)
|
18
116
|
end
|
19
117
|
end
|
20
118
|
end
|
@@ -4,6 +4,7 @@
|
|
4
4
|
require 'contrast/framework/base_support'
|
5
5
|
require 'contrast/framework/rails/patch/support'
|
6
6
|
require 'contrast/utils/string_utils'
|
7
|
+
require 'contrast/utils/duck_utils'
|
7
8
|
|
8
9
|
module Contrast
|
9
10
|
module Framework
|
@@ -51,8 +52,7 @@ module Contrast
|
|
51
52
|
# Find the current route, based on the provided Request wrapper
|
52
53
|
#
|
53
54
|
# @param request[Contrast::Agent::Request]
|
54
|
-
# @return [Contrast::Agent::Reporting::RouteCoverage, nil]
|
55
|
-
# matched to the request if a match was found.
|
55
|
+
# @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
|
56
56
|
def current_route_coverage request
|
57
57
|
return unless ::Rails.cs__respond_to?(:application)
|
58
58
|
|
@@ -130,6 +130,8 @@ module Contrast
|
|
130
130
|
if engine_route?(route)
|
131
131
|
new_req = retrieve_request(request.env)
|
132
132
|
new_req.path_info = new_req.path_info.gsub(match.to_s, '')
|
133
|
+
# solves the issue when requiring base path '/' without the slash
|
134
|
+
new_req.path_info = '/' if Contrast::Utils::DuckUtils.empty_duck?(new_req.path_info)
|
133
135
|
get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
|
134
136
|
else
|
135
137
|
[match, params, route, path]
|
@@ -68,8 +68,7 @@ module Contrast
|
|
68
68
|
|
69
69
|
# @param request [Contrast::Agent::Request] a contrast tracked request.
|
70
70
|
# @param _controller [::Sinatra::Base] optionally use this controller instead of global ::Sinatra::Base.
|
71
|
-
# @return [Contrast::Agent::Reporting::RouteCoverage, nil]
|
72
|
-
# matched to the request if a match was found.
|
71
|
+
# @return [Contrast::Agent::Reporting::RouteCoverage, nil] the route coverage object or nil if no route
|
73
72
|
def current_route_coverage request, _controller = ::Sinatra::Base, full_route = nil
|
74
73
|
method = request.env[::Rack::REQUEST_METHOD] # GET, PUT, POST, etc...
|
75
74
|
route = _cleaned_route(request)
|
@@ -72,13 +72,11 @@ module Contrast
|
|
72
72
|
def build_exception type, message = nil, exception = nil, data = nil
|
73
73
|
return unless buildable?
|
74
74
|
|
75
|
-
|
76
|
-
caller_idx = stack_trace&.find_index { |stack| stack.to_s.include?(type) } || 0
|
77
|
-
# The caller_stack is the method in which the error occurred, so has to be above this method
|
75
|
+
caller_idx = wrapped_caller_locations&.find_index { |stack| stack.to_s.include?(type) } || 0
|
78
76
|
caller_idx += 1
|
79
|
-
|
80
|
-
stack_frame_type =
|
81
|
-
stack_frame_function =
|
77
|
+
caller = wrapped_caller_locations[caller_idx]
|
78
|
+
stack_frame_type = obfuscate_type(caller)
|
79
|
+
stack_frame_function = caller.label
|
82
80
|
key = "#{ stack_frame_type }|#{ stack_frame_function }|#{ message }"
|
83
81
|
if Contrast::TELEMETRY_EXCEPTIONS[key]
|
84
82
|
Contrast::TELEMETRY_EXCEPTIONS.increment(key)
|
@@ -92,7 +90,7 @@ module Contrast
|
|
92
90
|
stack_frame_type, message_exception_type,
|
93
91
|
data, exception,
|
94
92
|
message)
|
95
|
-
build_stack(event_message,
|
93
|
+
build_stack(event_message, wrapped_caller_locations, caller_idx)
|
96
94
|
TELEMETRY_EXCEPTIONS[key] = event_message
|
97
95
|
rescue StandardError => e
|
98
96
|
debug('[Telemetry] Unable to report exception', e)
|
@@ -141,8 +139,11 @@ module Contrast
|
|
141
139
|
caller_stack.each_with_index do |caller, idx|
|
142
140
|
next unless idx > caller_idx
|
143
141
|
|
144
|
-
|
145
|
-
|
142
|
+
obfuscated_label = Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(caller.label)
|
143
|
+
obfuscated_path = Contrast::Agent::Telemetry::Exception::Obfuscate.
|
144
|
+
obfuscate_path(caller.path.delete_prefix(Dir.pwd))
|
145
|
+
|
146
|
+
stack_frame = Contrast::Agent::Telemetry::Exception::StackFrame.build(obfuscated_label, obfuscated_path)
|
146
147
|
event_exception_message.push(stack_frame)
|
147
148
|
end
|
148
149
|
end
|
@@ -153,6 +154,14 @@ module Contrast
|
|
153
154
|
def wrapped_caller_locations
|
154
155
|
caller_locations
|
155
156
|
end
|
157
|
+
|
158
|
+
# @param caller [Thread::Backtrace::Location, nil]
|
159
|
+
# @return [String]
|
160
|
+
def obfuscate_type caller
|
161
|
+
return '' unless caller.cs__respond_to?(:path)
|
162
|
+
|
163
|
+
Contrast::Agent::Telemetry::Exception::Obfuscate.obfuscate_path(caller.path.delete_prefix(Dir.pwd).to_s)
|
164
|
+
end
|
156
165
|
end
|
157
166
|
end
|
158
167
|
end
|