contrast-agent 7.3.0 → 7.3.2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/ext/cs__scope/cs__scope.c +76 -7
  3. data/ext/cs__scope/cs__scope.h +4 -0
  4. data/lib/contrast/agent/assess/policy/policy_node.rb +25 -6
  5. data/lib/contrast/agent/assess/policy/propagator/response.rb +64 -0
  6. data/lib/contrast/agent/assess/policy/propagator.rb +1 -0
  7. data/lib/contrast/agent/assess/policy/source_method.rb +5 -0
  8. data/lib/contrast/agent/assess/rule/response/body_rule.rb +22 -7
  9. data/lib/contrast/agent/assess/rule/response/cache_control_header_rule.rb +4 -1
  10. data/lib/contrast/agent/inventory/policy/datastores.rb +0 -3
  11. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -10
  12. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +11 -12
  13. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +4 -29
  14. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -2
  15. data/lib/contrast/agent/reporting/reporting_events/application_inventory_activity.rb +2 -2
  16. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +2 -2
  17. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +15 -2
  18. data/lib/contrast/agent/reporting/reporting_utilities/response.rb +0 -2
  19. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +5 -2
  20. data/lib/contrast/agent/reporting/settings/protect.rb +61 -18
  21. data/lib/contrast/agent/reporting/settings/server_features.rb +2 -0
  22. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +4 -3
  23. data/lib/contrast/agent/telemetry/identifier.rb +13 -26
  24. data/lib/contrast/agent/version.rb +1 -1
  25. data/lib/contrast/components/assess.rb +33 -6
  26. data/lib/contrast/components/base.rb +4 -2
  27. data/lib/contrast/components/config.rb +2 -2
  28. data/lib/contrast/components/protect.rb +14 -1
  29. data/lib/contrast/components/settings.rb +11 -1
  30. data/lib/contrast/config/diagnostics/command_line.rb +2 -2
  31. data/lib/contrast/config/diagnostics/environment_variables.rb +2 -1
  32. data/lib/contrast/config/diagnostics/tools.rb +15 -5
  33. data/lib/contrast/configuration.rb +61 -29
  34. data/lib/contrast/logger/application.rb +3 -3
  35. data/lib/contrast/utils/assess/propagation_method_utils.rb +2 -0
  36. data/lib/contrast/utils/os.rb +1 -9
  37. data/lib/contrast/utils/reporting/application_activity_batch_utils.rb +0 -3
  38. data/lib/contrast.rb +1 -1
  39. data/resources/assess/policy.json +80 -3
  40. metadata +3 -2
@@ -232,11 +232,14 @@ module Contrast
232
232
  return unless ::Contrast::AGENT.enabled?
233
233
 
234
234
  logger.trace_with_time('Rebuilding rule modes from TeamServer') do
235
- ::Contrast::SETTINGS.build_protect_rules if ::Contrast::PROTECT.enabled?
235
+ # TODO: RUBY-999999 Make sure when updating Protect rules to reflect the mode source (always DEFAULT_VALUE)
236
236
  ::Contrast::AGENT.reset_ruleset
237
+ ::Contrast::SETTINGS.build_protect_rules if ::Contrast::PROTECT.enabled?
237
238
  logger.info('Current rule settings:')
238
239
  ::Contrast::PROTECT.defend_rules.each { |k, v| logger.info('Protect Rule mode set', rule: k, mode: v.mode) }
239
- logger.info('Disabled Assess Rules', rules: ::Contrast::ASSESS.disabled_rules)
240
+ if ::Contrast::ASSESS.enabled?
241
+ logger.info('Disabled Assess Rules', rules: ::Contrast::ASSESS.disabled_rules)
242
+ end
240
243
  end
241
244
  end
242
245
 
@@ -14,6 +14,13 @@ module Contrast
14
14
  class Protect
15
15
  # modes set by NG endpoints; block at perimeter needs to be check against the blockAtEntry boolean value
16
16
  NG_PROTECT_RULES_MODE = %w[OFF MONITORING BLOCKING].cs__freeze
17
+ ACTIVE_PROTECT_RULES_LIST = %w[
18
+ bot-blocker cmd-injection cmd-injection-command-backdoors cmd-injection-semantic-chained-commands
19
+ cmd-injection-semantic-dangerous-paths untrusted-deserialization nosql-injection path-traversal
20
+ path-traversal-semantic-file-security-bypass sql-injection sql-injection-semantic-dangerous-functions
21
+ unsafe-file-upload reflected-xss xxe
22
+ ].cs__freeze
23
+
17
24
  # The settings for each protect rule for this application
18
25
  #
19
26
  # @return protection_rules [Array<protectRule>] protectRule: {
@@ -80,30 +87,66 @@ module Contrast
80
87
  modes_by_id = {}
81
88
  protection_rules.each do |rule|
82
89
  setting_mode = rule[:mode] || rule['mode']
83
- api_mode = if NG_PROTECT_RULES_MODE.include?(setting_mode)
84
- case setting_mode
85
- when NG_PROTECT_RULES_MODE[1]
86
- :MONITOR
87
- when NG_PROTECT_RULES_MODE[2]
88
- if rule[:blockAtEntry] || rule['blockAtEntry']
89
- :BLOCK_AT_PERIMETER
90
- else
91
- :BLOCK
92
- end
93
- else
94
- :NO_ACTION
95
- end
96
- else
97
- # modes set by newer settings endpoints are of [OFF MONITOR BLOCK BLOCK_AT_PERIMETER] and
98
- # can just be cast to symbols
99
- setting_mode.to_sym
100
- end
90
+ # BlockAtEnrtry is only available for the protection_rules Array.
91
+ # It is used in both ng and non ng payloads. If the array is empty
92
+ # this method will short circuit at the very first line and return
93
+ # empty hash. this means that the #rules_settings_to_settings_hash
94
+ # will be used next to extract the settings.
95
+ bap = rule[:blockAtEntry] || rule['blockAtEntry']
96
+ api_mode = assign_mode(setting_mode, block_at_entry: !!bap == bap)
101
97
 
102
98
  id = rule[:id] || rule['id']
103
99
  modes_by_id[id] = api_mode
104
100
  end
105
101
  modes_by_id
106
102
  end
103
+
104
+ # Converts settings into Agent Settings understandable hash {RULE_ID => MODE}
105
+ # Takes Hash<String, Contrast::Agent::Reporting::Settings::ProtectRule> and converts it
106
+ # to Hash<RULE_ID => MODE>
107
+ #
108
+ # @return rules [Hash<RULE_ID => MODE>, nil] Hash with rule_id as key and mode as value
109
+ def rules_settings_to_settings_hash
110
+ return {} if rule_settings.empty?
111
+
112
+ modes_by_id = {}
113
+ rule_settings.each do |rule_id, rule_mode|
114
+ next unless active_defend_rules.include?(rule_id.to_s)
115
+
116
+ modes_by_id[rule_id.to_s] = assign_mode(rule_mode.mode)
117
+ end
118
+ modes_by_id
119
+ end
120
+
121
+ # Returns list of actively used protection rules to be updated, or default list.
122
+ # This will be used to query the received settings for the ones used by the Agent.
123
+ def active_defend_rules
124
+ return ACTIVE_PROTECT_RULES_LIST unless defined?(Contrast::SETTINGS)
125
+
126
+ current_rules = (Contrast::PROTECT&.defend_rules&.keys if defined?(Contrast::PROTECT))
127
+ return current_rules unless current_rules&.empty?
128
+
129
+ ACTIVE_PROTECT_RULES_LIST
130
+ end
131
+
132
+ private
133
+
134
+ # Assigns the received settings mode to be used as actual config.
135
+ # @param setting_mode []
136
+ def assign_mode setting_mode, block_at_entry: false
137
+ # modes set by newer settings endpoints are of [OFF MONITOR BLOCK BLOCK_AT_PERIMETER] and
138
+ # can just be cast to symbols
139
+ return setting_mode.to_sym unless NG_PROTECT_RULES_MODE.include?(setting_mode)
140
+
141
+ case setting_mode
142
+ when NG_PROTECT_RULES_MODE[1]
143
+ :MONITOR
144
+ when NG_PROTECT_RULES_MODE[2]
145
+ block_at_entry ? :BLOCK_AT_PERIMETER : :BLOCK
146
+ else
147
+ :NO_ACTION
148
+ end
149
+ end
107
150
  end
108
151
  end
109
152
  end
@@ -85,6 +85,8 @@ module Contrast
85
85
  security_logger: security_logger.settings_blank? ? nil : security_logger.to_controlled_hash,
86
86
  assessment: @_assess ? assess.to_controlled_hash : {},
87
87
  defend: @_protect ? protect.to_controlled_hash : {},
88
+ logLevel: log_level,
89
+ logFile: log_file,
88
90
  telemetry: telemetry
89
91
  }.compact
90
92
  end
@@ -16,9 +16,10 @@ module Contrast
16
16
  # is the same.
17
17
  CYPHER = CHARS.chars.shuffle.join.cs__freeze
18
18
  VERSION_MATCH = '[^0-9].-'
19
+ RUBY_EXT = /\.(?:rb|gemspec)$/i
19
20
 
20
21
  # List of known places after witch a user name might appear:
21
- KNOWN_DIRS = %w[app application project projects git github users home user].cs__freeze
22
+ KNOWN_DIRS = %w[app application lib project projects git github users home user].cs__freeze
22
23
 
23
24
  class << self
24
25
  # Returns paths for known gems.
@@ -65,8 +66,8 @@ module Contrast
65
66
  name.tr(VERSION_MATCH, Contrast::Utils::ObjectShare::EMPTY_STRING).downcase)
66
67
 
67
68
  obscure(name)
68
- # obscure username (next dir in line)
69
- obscure(dirs[idx + 1]) if dirs[idx + 1]
69
+ # obscure username (next dir in line), skip if it's a file name.
70
+ obscure(dirs[idx + 1]) if dirs[idx + 1] && (dirs[idx + 1] !~ RUBY_EXT)
70
71
  end
71
72
  cypher = dirs.join(Contrast::Utils::ObjectShare::SLASH)
72
73
  return cypher if cypher
@@ -12,7 +12,7 @@ module Contrast
12
12
  # Gets info about the instrumented application required to build unique identifiers,
13
13
  # used in the agent's Telemetry.
14
14
  module Identifier
15
- MAC_REGEX = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.cs__freeze
15
+ MAC_REGEXP = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.cs__freeze
16
16
  LINUX_OS_REG = /hwaddr=.*?(([A-F0-9]{2}:){5}[A-F0-9]{2})/im.cs__freeze
17
17
  MAC_OS_PRIMARY = 'en0'.cs__freeze
18
18
  LINUX_PRIMARY = 'enp'.cs__freeze
@@ -87,7 +87,7 @@ module Contrast
87
87
  mac = retrieve_mac(addr)
88
88
  next unless mac
89
89
 
90
- result = mac if mac&.match?(MAC_REGEX)
90
+ result = mac if mac.match?(MAC_REGEXP)
91
91
  break if result
92
92
  end
93
93
  result
@@ -118,33 +118,20 @@ module Contrast
118
118
  nil
119
119
  end
120
120
 
121
- # Returns array of network interfaces.
122
- # This is OS dependent search.
121
+ # Returns array of network interfaces belonging to the expected pfamily of this OS.
123
122
  #
124
- # @return interfaces [Array] Returns an array of interface addresses.
125
- # Socket::Ifaddr - represents a result of getifaddrs().
123
+ # @return interfaces [Array<Socket::Ifaddr>]
126
124
  def interfaces
127
- @_interfaces ||= []
128
-
129
- return @_interfaces unless @_interfaces.empty?
125
+ @_interfaces ||= Socket.getifaddrs.select { |interface| interface.addr&.pfamily == check_family }
126
+ end
130
127
 
131
- arr = Socket.getifaddrs
132
- idx = 0
133
- check_family = 0
134
- while idx < arr.length
135
- # We need only network adapters MACs. Checking for pfamily of every socket address:
136
- # 18 for Mac OS and 17 for Linux.
137
- # family should be an address family such as: :INET, :INET6, :UNIX, etc.
138
- check_family = 18 if Contrast::Utils::OS.mac?
139
- check_family = 17 if Contrast::Utils::OS.linux?
140
- if arr[idx].addr.pfamily != check_family
141
- idx += 1
142
- next
143
- end
144
- @_interfaces << arr[idx]
145
- idx += 1
146
- end
147
- @_interfaces
128
+ # We need only network adapters MACs. Checking for pfamily of every socket address:
129
+ # 18 for Mac OS and 17 for Linux. Family should be an address family such as: :INET, :INET6, :UNIX, etc.
130
+ # It corresponds to the Addrinfo.pfamily value.
131
+ #
132
+ # @return [Integer]
133
+ def check_family
134
+ @_check_family ||= Contrast::Utils::OS.mac? ? 18 : 17
148
135
  end
149
136
  end
150
137
  end
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '7.3.0'
6
+ VERSION = '7.3.2'
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,7 +24,6 @@ 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
27
  CONTRAST_LOG = 'contrast.log'
29
28
  CONTRAST_NAME = 'Contrast Agent'
30
29
  DATE_TIME = '%Y-%m-%dT%H:%M:%S.%L%z'
@@ -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)
@@ -10,7 +10,7 @@ module Contrast
10
10
  module Protect
11
11
  # A wrapper build around the Common Agent Configuration project to allow for access of the values contained in
12
12
  # its parent_configuration_spec.yaml. Specifically, this allows for querying the state of the Protect product.
13
- class Interface
13
+ class Interface # rubocop:disable Metrics/ClassLength
14
14
  include Contrast::Components::ComponentBase
15
15
  include Contrast::Config::BaseConfiguration
16
16
 
@@ -168,6 +168,19 @@ module Contrast
168
168
 
169
169
  @_forcibly_enabled ||= true?(::Contrast::CONFIG.protect.enable)
170
170
  end
171
+
172
+ # Used for conversion to contrast metrics hash:
173
+ def forcibly_enabled
174
+ forcibly_enabled?
175
+ end
176
+
177
+ def forcibly_disabled
178
+ forcibly_disabled?
179
+ end
180
+
181
+ def report_custom_code_sysfile_access
182
+ report_custom_code_sysfile_access?
183
+ end
171
184
  end
172
185
  end
173
186
  end
@@ -156,7 +156,7 @@ module Contrast
156
156
  def update_from_application_settings settings_response
157
157
  return unless (app_settings = settings_response&.application_settings)
158
158
 
159
- @application_state.modes_by_id = app_settings.protect.protection_rules_to_settings_hash
159
+ extract_protect_app_settings(app_settings)
160
160
  update_exclusion_matchers(app_settings.exclusions)
161
161
  app_settings.protect.virtual_patches = app_settings.protect.virtual_patches unless
162
162
  settings_empty?(app_settings.protect.virtual_patches)
@@ -294,6 +294,16 @@ module Contrast
294
294
  level[parts[-1]] = value
295
295
  Contrast::CONFIG.sources.set(parts.join('.'), Contrast::Components::Config::Sources::CONTRAST_UI)
296
296
  end
297
+
298
+ # Extract the rules modes from protection_rules or rules_settings fields.
299
+ #
300
+ # @param app_settings [Contrast::Agent::Reporting::Settings::ApplicationSettings]
301
+ def extract_protect_app_settings app_settings
302
+ modes_by_id = app_settings.protect.protection_rules_to_settings_hash
303
+ modes_by_id = app_settings.protect.rules_settings_to_settings_hash if settings_empty?(modes_by_id)
304
+ # Preserve previous state if no new settings are extracted:
305
+ @application_state.modes_by_id = modes_by_id unless settings_empty?(modes_by_id)
306
+ end
297
307
  end
298
308
  end
299
309
  end
@@ -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
@@ -21,7 +21,7 @@ module Contrast
21
21
  # @return [Array] array of all the values needed to be written.
22
22
  def environment_settings env
23
23
  env_hash = env.select do |e|
24
- 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)
25
25
  end
26
26
  environment_settings = []
27
27
  env_hash.each do |key, value|
@@ -41,6 +41,7 @@ module Contrast
41
41
  Contrast::Utils::ObjectShare::EMPTY_STRING)
42
42
  end
43
43
  effective_value.value = Contrast::Config::Diagnostics::Tools.value_to_s(value)
44
+ effective_value.key = key
44
45
  end
45
46
  environment_settings << efc_value if efc_value
46
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)
@@ -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
@@ -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|