contrast-agent 7.2.0 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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|