contrast-agent 7.2.0 → 7.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/lib/contrast/agent/assess/policy/policy_node.rb +25 -6
  3. data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
  4. data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
  5. data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
  6. data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
  7. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
  8. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  9. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  10. data/lib/contrast/agent/protect/rule/base.rb +5 -1
  11. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +27 -11
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  15. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  25. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  26. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  27. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  28. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  29. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  30. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  31. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  32. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +11 -0
  33. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  34. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  35. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  36. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  37. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +1 -1
  38. data/lib/contrast/agent/telemetry/base.rb +28 -2
  39. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  40. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  41. data/lib/contrast/agent/telemetry/client.rb +10 -2
  42. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
  43. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  44. data/lib/contrast/agent/telemetry/identifier.rb +13 -26
  45. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  46. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  47. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  48. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  49. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  50. data/lib/contrast/agent/version.rb +1 -1
  51. data/lib/contrast/components/assess.rb +33 -6
  52. data/lib/contrast/components/base.rb +4 -2
  53. data/lib/contrast/components/config.rb +6 -6
  54. data/lib/contrast/components/protect.rb +11 -1
  55. data/lib/contrast/components/sampling.rb +15 -10
  56. data/lib/contrast/config/diagnostics/command_line.rb +2 -2
  57. data/lib/contrast/config/diagnostics/environment_variables.rb +5 -2
  58. data/lib/contrast/config/diagnostics/tools.rb +15 -5
  59. data/lib/contrast/config/yaml_file.rb +8 -0
  60. data/lib/contrast/configuration.rb +61 -29
  61. data/lib/contrast/framework/rails/support.rb +3 -0
  62. data/lib/contrast/logger/application.rb +3 -3
  63. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  64. data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
  65. data/lib/contrast/utils/metrics_hash.rb +1 -1
  66. data/lib/contrast/utils/object_share.rb +2 -1
  67. data/lib/contrast/utils/os.rb +1 -9
  68. data/lib/contrast/utils/response_utils.rb +12 -0
  69. data/lib/contrast/utils/timer.rb +2 -0
  70. data/lib/contrast.rb +9 -2
  71. data/resources/assess/policy.json +80 -3
  72. data/ruby-agent.gemspec +1 -1
  73. metadata +22 -6
  74. data/lib/contrast/utils/input_classification_base.rb +0 -169
@@ -3,6 +3,8 @@
3
3
 
4
4
  require 'contrast/utils/metrics_hash'
5
5
  require 'contrast/agent/telemetry/event'
6
+ require 'contrast/utils/duck_utils'
7
+ require 'contrast/utils/os'
6
8
 
7
9
  module Contrast
8
10
  module Agent
@@ -10,6 +12,7 @@ module Contrast
10
12
  # This class will hold the basic information for a Telemetry Event
11
13
  class MetricEvent < Contrast::Agent::Telemetry::Event
12
14
  include Contrast::Utils
15
+ include Contrast::Utils::OS
13
16
 
14
17
  attr_reader :fields
15
18
 
@@ -19,6 +22,15 @@ module Contrast
19
22
  @fields['_filler'] = 0
20
23
  end
21
24
 
25
+ def sys_info
26
+ @sys_info ||= get_system_information if @sys_info.nil?
27
+ @sys_info
28
+ end
29
+
30
+ def empty?
31
+ Contrast::Utils::DuckUtils.empty_duck?(@fields)
32
+ end
33
+
22
34
  def to_controlled_hash **_args
23
35
  super.merge!({ fields: @fields })
24
36
  end
@@ -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
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '7.2.0'
6
+ VERSION = '7.3.1'
7
7
  end
8
8
  end
@@ -17,14 +17,26 @@ module Contrast
17
17
 
18
18
  # @return [Boolean, nil]
19
19
  attr_accessor :enable
20
- # @return [Array, nil]
21
- attr_writer :enable_scan_response, :enable_dynamic_sources, :sampling, :rules, :stacktraces, :tags
20
+ # @return [Boolean, nil]
21
+ attr_writer :enable_scan_response
22
+ # @return [Boolean, nil]
23
+ attr_writer :enable_dynamic_sources
24
+ # @return [Contrast::Components::Sampling::Interface]
25
+ attr_writer :sampling
26
+ # @return [Contrast::Components::AssessRules::Interface]
27
+ attr_writer :rules
28
+ # @return [String, nil]
29
+ attr_writer :stacktraces
30
+ # @return [Array<String>, nil]
31
+ attr_writer :tags
22
32
  # @return [String]
23
33
  attr_reader :canon_name
24
- # @return [Array]
34
+ # @return [Array<String>]
25
35
  attr_reader :config_values
26
36
  # @return [Boolean]
27
37
  attr_writer :enable_original_object
38
+ # @return [Boolean]
39
+ attr_writer :enable_response_as_source
28
40
  # @return [Integer]
29
41
  attr_writer :max_context_source_events
30
42
  # @return [Integer]
@@ -46,6 +58,7 @@ module Contrast
46
58
  enable_scan_response
47
59
  enable_original_object
48
60
  enable_dynamic_sources
61
+ enable_response_as_source
49
62
  stacktraces
50
63
  max_context_source_events
51
64
  max_propagation_events
@@ -63,27 +76,33 @@ module Contrast
63
76
  @enable_scan_response = hsh[:enable_scan_response]
64
77
  @enable_dynamic_sources = hsh[:enable_dynamic_sources]
65
78
  @enable_original_object = hsh[:enable_original_object]
79
+ @enable_response_as_source = hsh[:enable_response_as_source]
66
80
  @sampling = Contrast::Components::Sampling::Interface.new(hsh[:sampling])
67
81
  @rules = Contrast::Components::AssessRules::Interface.new(hsh[:rules])
68
82
  @stacktraces = hsh[:stacktraces]
69
83
  assign_limits(hsh)
70
84
  end
71
85
 
72
- # @return [Boolean, true]
86
+ # @return [Boolean]
73
87
  def enable_scan_response
74
88
  @enable_scan_response.nil? ? true : @enable_scan_response
75
89
  end
76
90
 
77
- # @return [Boolean, true]
91
+ # @return [Boolean]
78
92
  def enable_dynamic_sources
79
93
  @enable_dynamic_sources.nil? ? true : @enable_dynamic_sources
80
94
  end
81
95
 
82
- # @return [Boolean, true]
96
+ # @return [Boolean]
83
97
  def enable_original_object
84
98
  @enable_original_object.nil? ? true : @enable_original_object
85
99
  end
86
100
 
101
+ # @return [Boolean]
102
+ def enable_response_as_source
103
+ @enable_response_as_source.nil? ? false : @enable_response_as_source
104
+ end
105
+
87
106
  # @return [Contrast::Components::Sampling::Interface]
88
107
  def sampling
89
108
  @sampling ||= Contrast::Components::Sampling::Interface.new
@@ -209,6 +228,13 @@ module Contrast
209
228
  @_track_original_object
210
229
  end
211
230
 
231
+ def track_response_as_source?
232
+ @track_response_as_source = !false?(enable_response_as_source) if
233
+ @track_response_as_source.nil?
234
+
235
+ @track_response_as_source
236
+ end
237
+
212
238
  # The id for this process, based on the session metadata or id provided by the user, as indicated in
213
239
  # application startup.
214
240
  def session_id
@@ -234,6 +260,7 @@ module Contrast
234
260
  end
235
261
 
236
262
  # Sets Event limits from configuration and converts string numbers to integers.
263
+ # @param hsh [Hash] the configuration hash
237
264
  def assign_limits hsh
238
265
  return unless hsh
239
266
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/config/diagnostics/tools'
5
5
  require 'contrast/utils/object_share'
6
+ require 'contrast/utils/duck_utils'
6
7
 
7
8
  module Contrast
8
9
  module Components
@@ -89,12 +90,13 @@ module Contrast
89
90
  add_effective_config_values(effective_config, config_values, canon_name, "#{ CONTRAST }.#{ canon_name }")
90
91
  end
91
92
 
92
- # attempts to stringifys the config value if it is an array with the join char
93
+ # attempts to stringify the config value if it is an array with the join char
94
+ #
93
95
  # @param val[Object] val to stringify
94
96
  # @param join_char[String, ','] join character defaults to ','
95
97
  # @return [String, Object] the stringified val or the object as is
96
98
  def stringify_array val, join_char = ','
97
- return val.join(join_char) if val.cs__is_a?(Array)
99
+ return val.join(join_char) if val.cs__is_a?(Array) && val.any?
98
100
 
99
101
  val
100
102
  end
@@ -24,8 +24,7 @@ module Contrast
24
24
  # it should break LOUDLY. Better to waste half an hour of the sysadmin's
25
25
  # time than to silently fail to deliver functionality.
26
26
  module Config
27
- CONTRAST_ENV_MARKER = 'CONTRAST__'
28
- CONTRAST_LOG = 'contrast_agent.log'
27
+ CONTRAST_LOG = 'contrast.log'
29
28
  CONTRAST_NAME = 'Contrast Agent'
30
29
  DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z'
31
30
 
@@ -63,7 +62,7 @@ module Contrast
63
62
  env_overrides
64
63
  validate
65
64
  rescue ArgumentError => e
66
- proto_logger.error('Configuration failed with error: ', e)
65
+ proto_logger.error('[PROTO_LOGGER] Configuration failed with error: ', e)
67
66
  end
68
67
  alias_method :rebuild, :build
69
68
 
@@ -157,7 +156,7 @@ module Contrast
157
156
  # @return [boolean]
158
157
  def valid_session_metadata?
159
158
  if !session_id&.empty? && !session_metadata&.empty?
160
- proto_logger.error(SESSION_VARIABLES)
159
+ proto_logger.error("[PROTO_LOGGER] #{ SESSION_VARIABLES }")
161
160
  return false
162
161
  end
163
162
  true
@@ -172,7 +171,7 @@ module Contrast
172
171
  msg << API_KEY unless api_key
173
172
  msg << API_SERVICE_KEY unless api_service_key
174
173
  msg << API_USERNAME unless api_username
175
- msg.any? { |m| proto_logger.error(m) }
174
+ msg.any? { |m| proto_logger.error("[PROTO_LOGGER] #{ m }") }
176
175
  msg.empty?
177
176
  end
178
177
 
@@ -180,7 +179,8 @@ module Contrast
180
179
  # For env variables resembling CONTRAST__WHATEVER__NESTED_VALUE
181
180
  # override raw.whatever.nested_value
182
181
  ENV.each do |env_key, env_value|
183
- next unless env_key.to_s.start_with?(CONTRAST_ENV_MARKER)
182
+ next unless env_key.to_s.start_with?(Contrast::Configuration::CONTRAST_ENV_MARKER)
183
+ next if Contrast::Configuration::DEPRECATED_PROPERTIES.include?(env_key.to_s)
184
184
 
185
185
  config_item = Contrast::Utils::EnvConfigurationItem.new(env_key, env_value)
186
186
  assign_value_to_path_array(self, config_item.dot_path_array, config_item.value)
@@ -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 config_settings [Contrast::Config::SamplingConfiguration] the Sampling configuration as provided by
47
+ # @param config_value [Boolean, nil] the Sampling configuration as provided by
47
48
  # local user input
48
- # @param settings [Contrast::Agent::Reporting::Settings::Sampling, nil] the Sampling settings as provided by
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? config_settings, settings
52
- true?([config_settings&.enable, settings&.enabled, DEFAULT_SAMPLING_ENABLED].compact[0])
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
  #
@@ -13,9 +13,9 @@ module Contrast
13
13
  class << self
14
14
  def command_line_settings
15
15
  cli = Contrast::Config::Diagnostics::Tools.flatten_settings(Contrast::CONFIG.sources.
16
- for(Contrast::Components::Config::Sources::COMMAND_LINE))
16
+ for(Contrast::Components::Config::Sources::COMMAND_LINE), cli: true)
17
17
 
18
- Contrast::Config::Diagnostics::Tools.to_config_values(cli, source: true)
18
+ Contrast::Config::Diagnostics::Tools.to_config_values(cli, source: true, cli: true)
19
19
  end
20
20
  end
21
21
  end
@@ -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[CONTRAST_CONFIG_PATH CONTRAST_AGENT_TELEMETRY_OPTOUT].cs__freeze
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
  #
@@ -19,7 +21,7 @@ module Contrast
19
21
  # @return [Array] array of all the values needed to be written.
20
22
  def environment_settings env
21
23
  env_hash = env.select do |e|
22
- e.to_s.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER) || NON_COMMON_ENV.include?(e.to_s)
24
+ e.to_s.start_with?(Contrast::Configuration::CONTRAST_ENV_MARKER) || NON_COMMON_ENV.include?(e.to_s)
23
25
  end
24
26
  environment_settings = []
25
27
  env_hash.each do |key, value|
@@ -39,6 +41,7 @@ module Contrast
39
41
  Contrast::Utils::ObjectShare::EMPTY_STRING)
40
42
  end
41
43
  effective_value.value = Contrast::Config::Diagnostics::Tools.value_to_s(value)
44
+ effective_value.key = key
42
45
  end
43
46
  environment_settings << efc_value if efc_value
44
47
  end
@@ -11,13 +11,15 @@ module Contrast
11
11
  # Diagnostics tools to be included in config components.
12
12
  module Tools
13
13
  CHECK = 'd'
14
+ CONTRAST_MARK = 'CONTRAST_'
14
15
  class << self
15
16
  # Creates new config instances for each read config entry from the flat generated configs.
16
17
  #
17
18
  # @param flats [Array] of flatten configs produced by #flatten_settings
18
19
  # @param source [Boolean] flag to set the desired value class, it may be a effective or source value.
20
+ # @param cli [Boolean] flag to check if the value comes from cli.
19
21
  # @return [Array<Contrast::Config::Diagnostics::SourceConfigValue>]
20
- def to_config_values flats, source: false
22
+ def to_config_values flats, source: false, cli: false
21
23
  config_value_klass = if source
22
24
  Contrast::Config::Diagnostics::SourceConfigValue
23
25
  else
@@ -27,7 +29,11 @@ module Contrast
27
29
  flats.each do |entry|
28
30
  entry.each do |key, value|
29
31
  efc_value = config_value_klass.new.tap do |config_value|
30
- config_value.canonical_name = Contrast::Utils::ObjectShare::CONTRAST_DOT + key
32
+ config_value.canonical_name = Contrast::Utils::ObjectShare::CONTRAST_DOT + key unless cli
33
+ if cli && key.to_s.include?(CONTRAST_MARK)
34
+ config_value.canonical_name = key.gsub(Contrast::Utils::ObjectShare::DOUBLE_UNDERSCORE,
35
+ Contrast::Utils::ObjectShare::PERIOD).downcase
36
+ end
31
37
  config_value.key = key
32
38
  config_value.value = value_to_s(value)
33
39
  end
@@ -40,17 +46,21 @@ module Contrast
40
46
  # Flattens out the read settings from file, env or contrast ui.
41
47
  # example: {"agent.polling.server_settings_ms"=>"50000"}
42
48
  #
49
+ # If cli is set we avoid adding the path and additional '.' to the key.
50
+ #
43
51
  # @param data [Hash, nil]
44
52
  # @param path [String] where to look for settings.
45
53
  # @param config [Hash] symbolized config to fetch keys from.
46
- def flatten_settings data, path = [], config: Contrast::CONFIG.config.loaded_config
54
+ # @param cli [Boolean] does the config come from cli.
55
+ def flatten_settings data, path = [], config: Contrast::CONFIG.config.loaded_config, cli: false
47
56
  return [] unless data
48
57
 
49
58
  data.each_with_object([]) do |(k, v), entries|
50
59
  if v.cs__is_a?(Hash)
51
60
  entries.concat(flatten_settings(v, path.dup.append(k.to_sym)))
52
61
  else
53
- entries << { "#{ path.join('.') }.#{ k }" => config.dig(*path, k).to_s }
62
+ entries << { k.to_s => config.dig(*path, k).to_s } if cli
63
+ entries << { "#{ path.join('.') }.#{ k }" => config.dig(*path, k).to_s } unless cli
54
64
  end
55
65
  end.flatten # rubocop:disable Style/MethodCalledOnDoEndBlock
56
66
  end
@@ -62,7 +72,7 @@ module Contrast
62
72
  return if value.nil?
63
73
  return value if value.cs__is_a?(String)
64
74
 
65
- value.each_with_object({}) do |(k, v), m| # rubocop:disable Style/HashTransformValues
75
+ value&.each_with_object({}) do |(k, v), m| # rubocop:disable Style/HashTransformValues
66
76
  m[k] = if v.cs__is_a?(Hash)
67
77
  value_to_s(v)
68
78
  elsif v.cs__is_a?(Array)
@@ -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
@@ -64,44 +64,29 @@ module Contrast
64
64
  KEYS_TO_REDACT = %i[api_key url service_key user_name].cs__freeze
65
65
  REDACTED = '**REDACTED**'
66
66
 
67
- def initialize cli_options = nil, default_name = DEFAULT_YAML_PATH # rubocop:disable Metrics/AbcSize
67
+ DEPRECATED_PROPERTIES = %w[
68
+ CONTRAST__AGENT__SERVICE__ENABLE CONTRAST__AGENT__SERVICE__LOGGER__LEVEL
69
+ CONTRAST__AGENT__SERVICE__LOGGER__PATH CONTRAST__AGENT__SERVICE__LOGGER__STDOUT
70
+ ].cs__freeze
71
+
72
+ def initialize cli_options = nil, default_name = DEFAULT_YAML_PATH
68
73
  @default_name = default_name
69
74
 
70
75
  # Load config_kv from file
71
76
  config_kv = Contrast::Utils::HashUtils.deep_symbolize_all_keys(load_config)
72
- unless cli_options
73
- cli_options = {}
74
- ENV.each do |key, value|
75
- next unless key.to_s.start_with?(CONTRAST_ENV_MARKER)
76
77
 
77
- cli_options[key] = value
78
- end
79
- end
80
-
81
- # Overlay CLI options - they take precedence over config file
82
- cli_options = Contrast::Utils::HashUtils.deep_symbolize_all_keys(cli_options)
83
- if cli_options
84
- config_kv = Contrast::Utils::HashUtils.precedence_merge(cli_options, config_kv)
85
- @_source_file_extensions = Contrast::Utils::HashUtils.
86
- precedence_merge(assign_source_to(cli_options,
87
- Contrast::Components::Config::Sources::COMMAND_LINE),
88
- @_source_file_extensions)
89
- end
78
+ # Load cli options from env
79
+ cli_options ||= cli_to_hash
80
+ config_kv = Contrast::Utils::HashUtils.precedence_merge(config_kv, cli_options)
81
+ update_sources_from_cli(cli_options)
90
82
 
91
83
  # Some in-flight rewrites to maintain backwards compatibility
92
84
  config_kv = update_prop_keys(config_kv)
85
+ @sources = Contrast::Components::Config::Sources.new(source_file_extensions)
93
86
  @loaded_config = config_kv
94
87
 
95
- @sources = Contrast::Components::Config::Sources.new(@_source_file_extensions)
96
-
97
- @api = Contrast::Components::Api::Interface.new(config_kv[:api])
98
- @enable = config_kv[:enable]
99
- @agent = Contrast::Components::Agent::Interface.new(config_kv[:agent])
100
- @application = Contrast::Components::AppContext::Interface.new(config_kv[:application])
101
- @server = Contrast::Config::ServerConfiguration.new(config_kv[:server])
102
- @assess = Contrast::Components::Assess::Interface.new(config_kv[:assess])
103
- @inventory = Contrast::Components::Inventory::Interface.new(config_kv[:inventory])
104
- @protect = Contrast::Components::Protect::Interface.new(config_kv[:protect])
88
+ # requires loaded_config:
89
+ create_config_components
105
90
  end
106
91
 
107
92
  # Get a loggable YAML format of this configuration
@@ -155,7 +140,7 @@ module Contrast
155
140
  # reverse order of precedence (first is most important).
156
141
  def configuration_paths
157
142
  @_configuration_paths ||= begin
158
- basename = default_name.split('.').first
143
+ basename = default_name.split('.')[0]
159
144
  # Order of extensions comes from here:
160
145
  extensions = Contrast::Components::Config::Sources::APP_CONFIGURATION_EXTENSIONS
161
146
 
@@ -263,6 +248,18 @@ module Contrast
263
248
 
264
249
  private
265
250
 
251
+ # Creates and updates the config components with the loaded config values.
252
+ def create_config_components
253
+ @api = Contrast::Components::Api::Interface.new(loaded_config[:api])
254
+ @enable = loaded_config[:enable]
255
+ @agent = Contrast::Components::Agent::Interface.new(loaded_config[:agent])
256
+ @application = Contrast::Components::AppContext::Interface.new(loaded_config[:application])
257
+ @server = Contrast::Config::ServerConfiguration.new(loaded_config[:server])
258
+ @assess = Contrast::Components::Assess::Interface.new(loaded_config[:assess])
259
+ @inventory = Contrast::Components::Inventory::Interface.new(loaded_config[:inventory])
260
+ @protect = Contrast::Components::Protect::Interface.new(loaded_config[:protect])
261
+ end
262
+
266
263
  # We cannot use all access components at this point, unfortunately, as they
267
264
  # may not have been initialized. Instead, we need to access the logger
268
265
  # directly.
@@ -367,5 +364,40 @@ module Contrast
367
364
  end
368
365
  end
369
366
  end
367
+
368
+ # Update the source mapping to reflect the cli values passed. Using raw string rather than path values.
369
+ #
370
+ # @param cli_options[Hash<Symbol, String>]
371
+ def update_sources_from_cli cli_options
372
+ @_source_file_extensions = Contrast::Utils::HashUtils.
373
+ precedence_merge(assign_source_to(cli_options,
374
+ Contrast::Components::Config::Sources::COMMAND_LINE),
375
+ @_source_file_extensions)
376
+ end
377
+
378
+ # Find all the set Contrast environment variables and cast them to their hash form. Keys will be split on __ and
379
+ # converted to symbols to match parsing of the YAML file
380
+ #
381
+ # @return [Hash<Symbol, (Hash, String)>]
382
+ def cli_to_hash
383
+ cli_options ||= ENV.select do |name, _value|
384
+ name.to_s.start_with?(CONTRAST_ENV_MARKER) && !DEPRECATED_PROPERTIES.include?(name.to_s)
385
+ end
386
+
387
+ converted = {}
388
+ cli_options&.each do |key, value|
389
+ # Split the env key into path components
390
+ path = key.to_s.split(Contrast::Utils::ObjectShare::DOUBLE_UNDERSCORE)
391
+ # Remove the `CONTRAST` start
392
+ path&.shift
393
+ # Convert it to hash form, with lowercase symbol keys
394
+ as_hash = path&.reverse&.reduce(value) do |assigned_value, path_segment|
395
+ { path_segment.downcase.to_sym => assigned_value }
396
+ end
397
+ # And join it w/ the parsed keys
398
+ Contrast::Utils::HashUtils.precedence_merge!(converted, as_hash)
399
+ end
400
+ converted
401
+ end
370
402
  end
371
403
  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
@@ -129,6 +130,8 @@ module Contrast
129
130
  if engine_route?(route)
130
131
  new_req = retrieve_request(request.env)
131
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)
132
135
  get_full_route(new_req, route.app.app.routes.router, path << match.to_s)
133
136
  else
134
137
  [match, params, route, path]
@@ -17,8 +17,8 @@ module Contrast
17
17
  ENV.each do |env_key, env_value|
18
18
  env_key = env_key.to_s
19
19
  next unless ENV_KEYS.include?(env_key) ||
20
- (env_key.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER) &&
21
- !env_key.start_with?("#{ Contrast::Components::Config::CONTRAST_ENV_MARKER }API"))
20
+ (env_key.start_with?(Contrast::Configuration::CONTRAST_ENV_MARKER) &&
21
+ !env_key.start_with?("#{ Contrast::Configuration::CONTRAST_ENV_MARKER }API"))
22
22
 
23
23
  info('Environment settings', key: env_key, value: env_value)
24
24
  end
@@ -30,7 +30,7 @@ module Contrast
30
30
  loggable = ::Contrast::CONFIG.loggable
31
31
  info('Current configuration', configuration: loggable)
32
32
  env_keys = ENV.keys.select do |env_key|
33
- env_key&.to_s&.start_with?(Contrast::Components::Config::CONTRAST_ENV_MARKER)
33
+ env_key&.to_s&.start_with?(Contrast::Configuration::CONTRAST_ENV_MARKER)
34
34
  end
35
35
  env_items = env_keys.map { |env_key| Contrast::Utils::EnvConfigurationItem.new(env_key, nil) }
36
36
  env_translations = env_items.each_with_object({}) do |conversion, hash|