contrast-agent 7.3.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) 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/inventory/policy/datastores.rb +0 -3
  5. data/lib/contrast/agent/protect/rule/base.rb +5 -1
  6. data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +17 -5
  7. data/lib/contrast/agent/protect/rule/input_classification/base.rb +7 -2
  8. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +1 -1
  9. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal.rb +8 -1
  10. data/lib/contrast/agent/protect/rule/sqli/sqli.rb +8 -1
  11. data/lib/contrast/agent/protect/state.rb +110 -0
  12. data/lib/contrast/agent/reporting/reporting_events/application_activity.rb +4 -10
  13. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +11 -12
  14. data/lib/contrast/agent/reporting/reporting_events/application_defend_attack_sample_activity.rb +6 -29
  15. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -2
  16. data/lib/contrast/agent/reporting/reporting_events/application_inventory_activity.rb +2 -2
  17. data/lib/contrast/agent/reporting/reporting_events/architecture_component.rb +2 -0
  18. data/lib/contrast/agent/reporting/reporting_events/finding.rb +1 -0
  19. data/lib/contrast/agent/reporting/reporting_events/finding_event.rb +4 -0
  20. data/lib/contrast/agent/reporting/reporting_events/finding_request.rb +4 -2
  21. data/lib/contrast/agent/reporting/reporting_events/observed_library_usage.rb +2 -0
  22. data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +9 -5
  23. data/lib/contrast/agent/reporting/reporting_events/reportable_hash.rb +30 -6
  24. data/lib/contrast/agent/reporting/reporting_utilities/ng_response_extractor.rb +15 -2
  25. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +1 -1
  26. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +1 -1
  27. data/lib/contrast/agent/reporting/reporting_utilities/response.rb +0 -2
  28. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +4 -5
  29. data/lib/contrast/agent/reporting/settings/protect.rb +61 -18
  30. data/lib/contrast/agent/reporting/settings/sampling.rb +5 -4
  31. data/lib/contrast/agent/reporting/settings/server_features.rb +2 -0
  32. data/lib/contrast/agent/version.rb +1 -1
  33. data/lib/contrast/components/agent.rb +3 -5
  34. data/lib/contrast/components/api.rb +3 -3
  35. data/lib/contrast/components/assess_rules.rb +1 -2
  36. data/lib/contrast/components/base.rb +1 -2
  37. data/lib/contrast/components/config/sources.rb +23 -0
  38. data/lib/contrast/components/logger.rb +19 -0
  39. data/lib/contrast/components/protect.rb +69 -15
  40. data/lib/contrast/components/sampling.rb +5 -12
  41. data/lib/contrast/components/security_logger.rb +17 -0
  42. data/lib/contrast/components/settings.rb +114 -70
  43. data/lib/contrast/config/certification_configuration.rb +1 -1
  44. data/lib/contrast/config/configuration_files.rb +0 -2
  45. data/lib/contrast/config/diagnostics/config.rb +3 -3
  46. data/lib/contrast/config/diagnostics/effective_config.rb +1 -1
  47. data/lib/contrast/config/diagnostics/environment_variables.rb +21 -11
  48. data/lib/contrast/config/diagnostics/monitor.rb +1 -1
  49. data/lib/contrast/config/diagnostics/singleton_tools.rb +170 -0
  50. data/lib/contrast/config/diagnostics/source_config_value.rb +14 -9
  51. data/lib/contrast/config/diagnostics/tools.rb +23 -84
  52. data/lib/contrast/config/request_audit_configuration.rb +1 -1
  53. data/lib/contrast/config/server_configuration.rb +3 -15
  54. data/lib/contrast/configuration.rb +5 -2
  55. data/lib/contrast/framework/manager.rb +4 -3
  56. data/lib/contrast/framework/manager_extend.rb +3 -1
  57. data/lib/contrast/framework/rack/support.rb +11 -2
  58. data/lib/contrast/utils/log_utils.rb +1 -1
  59. data/lib/contrast/utils/reporting/application_activity_batch_utils.rb +0 -3
  60. data/lib/contrast/utils/request_utils.rb +1 -1
  61. data/lib/contrast/utils/timer.rb +1 -1
  62. data/lib/contrast.rb +1 -1
  63. metadata +4 -2
@@ -111,7 +111,6 @@ module Contrast
111
111
  include Contrast::Config::BaseConfiguration
112
112
 
113
113
  CANON_NAME = 'assess.sampling'
114
- NAME_PREFIX = "#{ CONTRAST }.#{ CANON_NAME }".cs__freeze
115
114
  CONFIG_VALUES = %w[enable baseline request_frequency response_frequency window_ms].cs__freeze
116
115
 
117
116
  # @return [Boolean, nil]
@@ -145,19 +144,13 @@ module Contrast
145
144
  def to_effective_config effective_config
146
145
  confirm_sources
147
146
 
148
- add_single_effective_value(effective_config, 'enable', sampling_control[:enabled], canon_name, NAME_PREFIX)
149
- add_single_effective_value(effective_config, 'baseline', sampling_control[:baseline], canon_name, NAME_PREFIX)
150
- add_single_effective_value(effective_config, 'window_ms', sampling_control[:window], canon_name, NAME_PREFIX)
147
+ add_single_effective_value(effective_config, 'enable', sampling_control[:enabled], canon_name)
148
+ add_single_effective_value(effective_config, 'baseline', sampling_control[:baseline], canon_name)
149
+ add_single_effective_value(effective_config, 'window_ms', sampling_control[:window], canon_name)
151
150
  add_single_effective_value(effective_config,
152
- 'request_frequency',
153
- sampling_control[:request_frequency],
154
- canon_name,
155
- NAME_PREFIX)
151
+ 'request_frequency', sampling_control[:request_frequency], canon_name)
156
152
  add_single_effective_value(effective_config,
157
- 'response_frequency',
158
- sampling_control[:response_frequency],
159
- canon_name,
160
- NAME_PREFIX)
153
+ 'response_frequency', sampling_control[:response_frequency], canon_name)
161
154
  end
162
155
 
163
156
  private
@@ -10,6 +10,7 @@ module Contrast
10
10
  class Interface
11
11
  include Contrast::Components::ComponentBase
12
12
 
13
+ DEFAULT_CEF_NAME = 'security.log'
13
14
  CANON_NAME = 'agent.security_logger'
14
15
  CONFIG_VALUES = %w[path level].cs__freeze
15
16
 
@@ -30,6 +31,22 @@ module Contrast
30
31
  @path = hsh[:path]
31
32
  @level = hsh[:level]
32
33
  end
34
+
35
+ def to_effective_config effective_config
36
+ path_setting = nil
37
+ level_setting = nil
38
+
39
+ if defined?(Contrast::SETTINGS)
40
+ path_setting = Contrast::SETTINGS.agent_state.cef_logger_path
41
+ level_setting = Contrast::SETTINGS.agent_state.cef_logger_level
42
+ end
43
+
44
+ path_setting ||= DEFAULT_CEF_NAME
45
+ level_setting ||= Contrast::CONFIG.agent.logger.level
46
+
47
+ add_single_effective_value(effective_config, config_values[0], path || path_setting, CANON_NAME)
48
+ add_single_effective_value(effective_config, config_values[1], level || level_setting, CANON_NAME)
49
+ end
33
50
  end
34
51
  end
35
52
  end
@@ -14,10 +14,12 @@ module Contrast
14
14
  # directives (likely provided by TeamServer) about product operation.
15
15
  # 'Settings' is not a generic term for 'configurable stuff'.
16
16
  module Settings
17
- APPLICATION_STATE_BASE = Struct.new(:modes_by_id).new(Hash.new(:NO_ACTION))
18
- PROTECT_STATE_BASE = Struct.new(:enabled, :rules).new(false, {})
19
- ASSESS_STATE_BASE = Struct.new(:enabled, :sampling_settings, :disabled_assess_rules, :session_id).new(false, nil,
20
- [], nil) do
17
+ AGENT_STATE_BASE = Struct.new(:logger_path, :logger_level, :cef_logger_path, :cef_logger_level).
18
+ new(nil, nil, nil, nil)
19
+ APPLICATION_STATE_BASE = Struct.new(:modes_by_id).new({})
20
+ PROTECT_STATE_BASE = Struct.new(:enabled).new(false)
21
+ ASSESS_STATE_BASE = Struct.new(:enabled, :sampling_settings, :disabled_assess_rules, :session_id).
22
+ new(false, nil, [], nil) do
21
23
  def sampling_settings= new_val
22
24
  @sampling_settings = new_val
23
25
  Contrast::Utils::Assess::SamplingUtil.instance.update
@@ -32,6 +34,13 @@ module Contrast
32
34
 
33
35
  # tainted_columns are database columns that receive unsanitized input.
34
36
  attr_reader :tainted_columns # This can probably go into assess_state?
37
+ # Agent state. Used for extracting Agent level settings.
38
+ #
39
+ # logger_path[String] Path to the log file.
40
+ # logger_level[String] Log level for the logger.
41
+ # cef_logger_path[String] Path to the log file.
42
+ # cef_logger_level[String] Log level for the logger.
43
+ attr_reader :agent_state
35
44
  # Current state for Assess.
36
45
  # enabled [Boolean] Indicate if the assess feature set is enabled for this server or not.
37
46
  #
@@ -87,26 +96,17 @@ module Contrast
87
96
  end
88
97
 
89
98
  # @param features_response [Contrast::Agent::Reporting::Response]
90
- def update_from_server_features features_response # rubocop:disable Metrics/AbcSize
99
+ def update_from_server_features features_response
91
100
  return unless (server_features = features_response&.server_features)
92
101
 
93
- log_file = server_features.log_file
94
- log_level = server_features.log_level
95
- # Update logger:
96
- Contrast::Logger::Log.instance.update(log_file, log_level) if log_file || log_level
97
- # Update AgentLib Logger
98
- update_agent_lib_log(log_level.to_s)
99
- # Update CEFlogger:
100
- unless server_features.security_logger.settings_blank?
101
- cef_logger.build_logger(server_features.security_logger.log_level, server_features.security_logger.log_file)
102
- end
102
+ update_loggers(server_features)
103
103
  # TODO: RUBY-99999 Update Bot-Blocker from server settings - check enable value.
104
104
  # For now all protection rules are rebuild on Application update. Bot blocker uses the default
105
105
  # enable from the base rule, and update it's mode on app settings update.
106
106
  # Here we receive also bots for that rule.
107
107
  unless settings_empty?(server_features.protect.enabled?)
108
108
  @protect_state.enabled = server_features.protect.enabled?
109
- store_in_config(%i[protect enable], server_features.protect.enabled?)
109
+ update_config_from_settings(%i[protect enable], server_features.protect.enabled?)
110
110
  end
111
111
  update_assess_server_features(server_features.assess)
112
112
  @last_server_update_ms = Contrast::Utils::Timer.now_ms
@@ -118,6 +118,54 @@ module Contrast
118
118
  logger.warn('The following error occurred from server update: ', e: e)
119
119
  end
120
120
 
121
+ # Update Assess server features
122
+ #
123
+ # @param assess [Contrast::Agent::Reporting::Settings::AssessServerFeature]
124
+ def update_assess_server_features assess
125
+ return if settings_empty?(assess.enabled?)
126
+
127
+ @assess_state.enabled = assess.enabled?
128
+ update_config_from_settings(%i[assess enable], assess.enabled?)
129
+ @assess_state.sampling_settings = assess.sampling
130
+
131
+ samplings_path = Contrast::Components::Sampling::Interface::CANON_NAME.split('.').map(&:to_sym)
132
+ Contrast::Components::Sampling::Interface::CONFIG_VALUES.each do |field|
133
+ lookup_field = field == 'enable' ? :enabled : field.to_sym
134
+ update_config_from_settings(samplings_path + [field.to_sym], assess.sampling.send(lookup_field))
135
+ end
136
+ end
137
+
138
+ # Updates logging settings
139
+ # @param [Contrast::Agent::Reporting::Settings::ServerFeatures]
140
+ def update_loggers server_features
141
+ log_file = server_features.log_file
142
+ log_level = server_features.log_level
143
+ # Update logger:
144
+ Contrast::Logger::Log.instance.update(log_file, log_level) if log_file || log_level
145
+ unless settings_empty?(log_file)
146
+ update_config_from_settings(%i[agent logger path], log_file)
147
+ @agent_state.logger_path = log_file
148
+ end
149
+ unless settings_empty?(log_level)
150
+ update_config_from_settings(%i[agent logger level], log_level)
151
+ @agent_state.logger_level = log_level
152
+ end
153
+ # Update AgentLib Logger
154
+ update_agent_lib_log(log_level.to_s)
155
+ # Update CEFlogger:
156
+ return if server_features.security_logger.settings_blank?
157
+
158
+ cef_logger.build_logger(server_features.security_logger.log_level, server_features.security_logger.log_file)
159
+ unless settings_empty?(log_file)
160
+ update_config_from_settings(%i[agent security_logger path], log_file)
161
+ @agent_state.cef_logger_level = log_file
162
+ end
163
+ return unless settings_empty?(log_level)
164
+
165
+ update_config_from_settings(%i[agent security_logger level], log_level)
166
+ @agent_state.cef_logger_level = log_level
167
+ end
168
+
121
169
  # Update AgentLib log level
122
170
  def update_agent_lib_log new_log_level
123
171
  agent_lib_log_level = Contrast::AgentLib::InterfaceBase::LOG_LEVEL[0] if new_log_level.empty?
@@ -135,42 +183,46 @@ module Contrast
135
183
  Contrast::AGENT_LIB.change_log_options(true, agent_lib_log_level)
136
184
  end
137
185
 
138
- # Update Assess server features
139
- #
140
- # @param assess [Contrast::Agent::Reporting::Settings::AssessServerFeature]
141
- def update_assess_server_features assess
142
- return if settings_empty?(assess.enabled?)
143
-
144
- @assess_state.enabled = assess.enabled?
145
- store_in_config(%i[assess enable], assess.enabled?)
146
- @assess_state.sampling_settings = assess.sampling
147
-
148
- Contrast::Components::Sampling::Interface::CONFIG_VALUES.each do |field|
149
- lookup_field = field == 'enable' ? :enabled : field.to_sym
150
- store_in_config(Contrast::Components::Sampling::Interface::CANON_NAME.split('.') + [field.to_sym],
151
- assess.sampling.send(lookup_field))
152
- end
153
- end
154
-
155
186
  # @param settings_response [Contrast::Agent::Reporting::Response]
156
187
  def update_from_application_settings settings_response
157
188
  return unless (app_settings = settings_response&.application_settings)
158
189
 
159
- @application_state.modes_by_id = app_settings.protect.protection_rules_to_settings_hash
160
- update_exclusion_matchers(app_settings.exclusions)
161
- app_settings.protect.virtual_patches = app_settings.protect.virtual_patches unless
162
- settings_empty?(app_settings.protect.virtual_patches)
163
- update_sensitive_data_policy(app_settings.sensitive_data_masking)
190
+ extract_protect_app_settings(app_settings)
191
+ update_matchers_and_sensitive_data(app_settings)
164
192
  @assess_state.disabled_assess_rules = app_settings.assess.disabled_rules
193
+ update_config_from_settings(%i[assess rules disabled_rules], app_settings.assess.disabled_rules)
165
194
  new_session_id = app_settings.assess.session_id
166
- @assess_state.session_id = new_session_id if new_session_id && !new_session_id.blank?
195
+ unless settings_empty?(new_session_id)
196
+ @assess_state.session_id = new_session_id
197
+ # TODO: RUBY-99999 Update the default values and effective config update from TS.
198
+ # Using the session_id from the settings response to update the config.
199
+ # The Effective Config sources values are fetched from the
200
+ # Contrast::CONFIG.config.loaded_config. Some values are displayed from
201
+ # their components, however not updated here. Using this may cause some
202
+ # specs to fails check the update of all values from TS.
203
+ # Contrast::CONFIG.application.session_id = new_session_id
204
+ update_config_from_settings(%i[application session_id], new_session_id)
205
+ end
167
206
  @last_app_update_ms = Contrast::Utils::Timer.now_ms
168
207
  @app_settings_last_httpdate = header_application_last_update
169
208
  end
170
209
 
210
+ # @param app_settings [Contrast::Agent::Reporting::Settings::ApplicationSettings]
211
+ def update_matchers_and_sensitive_data app_settings
212
+ update_exclusion_matchers(app_settings.exclusions)
213
+ app_settings.protect.virtual_patches = app_settings.protect.virtual_patches unless
214
+ settings_empty?(app_settings.protect.virtual_patches)
215
+ update_sensitive_data_policy(app_settings.sensitive_data_masking)
216
+ end
217
+
171
218
  # Wipe state to zero.
172
- def reset_state
173
- @protect_state = PROTECT_STATE_BASE.dup
219
+ #
220
+ # @param purge [Boolean] If true, also purge the persistent states.
221
+ def reset_state purge: false
222
+ @agent_state = AGENT_STATE_BASE.dup
223
+ # Keep the protect state, since once set the rules depend ont it.
224
+ # The state will be update on first settings response from TS.
225
+ @protect_state = PROTECT_STATE_BASE.dup if purge || @protect_state.nil?
174
226
  update_assess_state
175
227
  @application_state = APPLICATION_STATE_BASE.dup
176
228
  @tainted_columns = {}
@@ -193,24 +245,6 @@ module Contrast
193
245
  @assess_state
194
246
  end
195
247
 
196
- def build_protect_rules
197
- @protect_state.rules = {}
198
-
199
- # Rules. They add themselves on initialize.
200
- Contrast::Agent::Protect::Rule::BotBlocker.new
201
- cmdi = Contrast::Agent::Protect::Rule::CmdInjection.new
202
- cmdi.sub_rules
203
- Contrast::Agent::Protect::Rule::Deserialization.new
204
- Contrast::Agent::Protect::Rule::NoSqli.new
205
- path = Contrast::Agent::Protect::Rule::PathTraversal.new
206
- path.sub_rules
207
- sqli = Contrast::Agent::Protect::Rule::Sqli.new
208
- sqli.sub_rules
209
- Contrast::Agent::Protect::Rule::UnsafeFileUpload.new
210
- Contrast::Agent::Protect::Rule::Xss.new
211
- Contrast::Agent::Protect::Rule::Xxe.new
212
- end
213
-
214
248
  # @param exclusions [Contrast::Agent::Reporting::Settings::Exclusions]
215
249
  def update_exclusion_matchers exclusions
216
250
  matchers = []
@@ -278,21 +312,31 @@ module Contrast
278
312
  Contrast::Agent.reporter.client.response_handler.last_application_modified
279
313
  end
280
314
 
315
+ # Extract the rules modes from protection_rules or rules_settings fields.
316
+ #
317
+ # @param app_settings [Contrast::Agent::Reporting::Settings::ApplicationSettings]
318
+ def extract_protect_app_settings app_settings
319
+ modes_by_id = app_settings.protect.protection_rules_to_settings_hash
320
+ modes_by_id = app_settings.protect.rules_settings_to_settings_hash if settings_empty?(modes_by_id)
321
+ # Preserve previous state if no new settings are extracted:
322
+ return if settings_empty?(modes_by_id)
323
+
324
+ @application_state.modes_by_id = modes_by_id
325
+ end
326
+
281
327
  # Update the stored config values to ensure that we know about the correct values,
282
328
  # and that the sources are correct for entries updated from the UI.
283
329
  #
284
- # @param parts [Array] the path to the setting in config
330
+ # NOTE: For each passed component path here, there should br implemented a check for the value from
331
+ # Config and Settings. For example if enable from CONFIG is nil, check the SETTINGS.
332
+ # This will keep the value not empty and be reflected in effective config reporting.
333
+ #
334
+ # @param path [String] the canonical name for the config entry (such as api.proxy.enable)
285
335
  # @param value [String, Integer, Array, nil] the value for the configuration setting
286
- def store_in_config parts, value
287
- level = Contrast::CONFIG.config.loaded_config
288
- parts[0...-1].each do |segment|
289
- level[segment] ||= {}
290
- level = level[segment]
291
- end
292
- return unless level.cs__is_a?(Hash)
293
-
294
- level[parts[-1]] = value
295
- Contrast::CONFIG.sources.set(parts.join('.'), Contrast::Components::Config::Sources::CONTRAST_UI)
336
+ def update_config_from_settings path, value
337
+ Contrast::Config::Diagnostics::Tools.update_config(path,
338
+ value,
339
+ Contrast::Components::Config::Sources::CONTRAST_UI)
296
340
  end
297
341
  end
298
342
  end
@@ -40,7 +40,7 @@ module Contrast
40
40
  #
41
41
  # @param effective_config [Contrast::Config::Diagnostics::EffectiveConfig]
42
42
  def to_effective_config effective_config
43
- add_effective_config_values(effective_config, CONFIG_VALUES, CANON_NAME, CONTRAST)
43
+ add_effective_config_values(effective_config, CONFIG_VALUES, CANON_NAME)
44
44
  end
45
45
  end
46
46
  end
@@ -29,8 +29,6 @@ module Contrast
29
29
 
30
30
  # This class will hold all the info about the read file.
31
31
  class LocalSourceValue
32
- YML_EXT = '.yml'
33
- YAML_EXT = '.yaml'
34
32
  # @return [String]
35
33
  attr_reader :path
36
34
  # @return [Hash]
@@ -52,9 +52,9 @@ module Contrast
52
52
  effective_config: effective_config.to_controlled_hash,
53
53
  user_configuration_file: user_configuration_file.to_controlled_hash,
54
54
  environment_variable: Contrast::Config::Diagnostics::EnvironmentVariables.environment_settings(ENV).
55
- map(&:to_controlled_hash),
56
- command_line: Contrast::Config::Diagnostics::CommandLine.command_line_settings.map(&:to_controlled_hash),
57
- contrast_ui: Contrast::Config::Diagnostics::ContrastUI.contrast_ui_settings.map(&:to_controlled_hash)
55
+ map(&:to_source_hash),
56
+ command_line: Contrast::Config::Diagnostics::CommandLine.command_line_settings.map(&:to_source_hash),
57
+ contrast_ui: Contrast::Config::Diagnostics::ContrastUI.contrast_ui_settings.map(&:to_source_hash)
58
58
  }.compact
59
59
  end
60
60
  end
@@ -20,7 +20,7 @@ module Contrast
20
20
  end
21
21
 
22
22
  def to_controlled_hash
23
- { values: @values&.map(&:to_controlled_hash) }
23
+ @values&.map(&:to_controlled_hash)
24
24
  end
25
25
  end
26
26
  end
@@ -14,36 +14,46 @@ module Contrast
14
14
  NON_COMMON_ENV = %w[
15
15
  CONTRAST_CONFIG_PATH CONTRAST_AGENT_TELEMETRY_OPTOUT CONTRAST_AGENT_TELEMETRY_TEST
16
16
  ].cs__freeze
17
+ MASKED_ENV = %w[CONTRAST__API__API_KEY CONTRAST__API__SERVICE_KEY].cs__freeze
17
18
 
18
19
  # This method will fill the canonical name for each env var and will check for any uncommon ones.
19
20
  #
20
21
  # @param env [Hash]
21
22
  # @return [Array] array of all the values needed to be written.
22
- def environment_settings env
23
+ def environment_settings env # rubocop:disable Metrics/MethodLength
23
24
  env_hash = env.select do |e|
24
25
  e.to_s.start_with?(Contrast::Configuration::CONTRAST_ENV_MARKER) || NON_COMMON_ENV.include?(e.to_s)
25
26
  end
26
27
  environment_settings = []
27
- env_hash.each do |key, value|
28
+ env_hash.each do |key, value| # rubocop:disable Metrics/BlockLength
28
29
  efc_value = Contrast::Config::Diagnostics::EffectiveConfigValue.new.tap do |effective_value|
29
30
  next unless value
30
31
 
31
32
  effective_value.canonical_name = if NON_COMMON_ENV.include?(key)
32
33
  key.gsub(Contrast::Utils::ObjectShare::UNDERSCORE,
33
- Contrast::Utils::ObjectShare::PERIOD).downcase
34
+ Contrast::Utils::ObjectShare::PERIOD).downcase.
35
+ gsub(Contrast::Utils::ObjectShare::CONTRAST_DOT,
36
+ Contrast::Utils::ObjectShare::EMPTY_STRING)
34
37
  else
35
38
  key.gsub(Contrast::Utils::ObjectShare::DOUBLE_UNDERSCORE,
36
- Contrast::Utils::ObjectShare::PERIOD).downcase
39
+ Contrast::Utils::ObjectShare::PERIOD).downcase.
40
+ gsub(Contrast::Utils::ObjectShare::CONTRAST_DOT,
41
+ Contrast::Utils::ObjectShare::EMPTY_STRING)
37
42
  end
38
- if effective_value.canonical_name
39
- effective_value.key =
40
- effective_value.canonical_name.gsub(Contrast::Utils::ObjectShare::CONTRAST_DOT,
41
- Contrast::Utils::ObjectShare::EMPTY_STRING)
42
- end
43
- effective_value.value = Contrast::Config::Diagnostics::Tools.value_to_s(value)
43
+ effective_value.value = if MASKED_ENV.include?(key)
44
+ Contrast::Configuration::EFFECTIVE_REDACTED
45
+ else
46
+ Contrast::Config::Diagnostics::Tools.value_to_s(value)
47
+ end
44
48
  effective_value.key = key
45
49
  end
46
- environment_settings << efc_value if efc_value
50
+ next unless efc_value
51
+
52
+ environment_settings << efc_value
53
+ Contrast::Config::Diagnostics::Tools.
54
+ update_config(efc_value.key,
55
+ efc_value.value,
56
+ Contrast::Components::Config::Sources::ENVIRONMENT_VARIABLE)
47
57
  end
48
58
  environment_settings
49
59
  end
@@ -102,7 +102,7 @@ module Contrast
102
102
  extract_settings
103
103
  File.open(File.join(dir_name, FILE_NAME), WRITE) do |file|
104
104
  file.truncate(0)
105
- file.write(JSON.pretty_generate(to_controlled_hash, { space: Contrast::Utils::ObjectShare::EMPTY_STRING }))
105
+ file.write(JSON.pretty_generate(to_controlled_hash, { space: Contrast::Utils::ObjectShare::SPACE }))
106
106
  status = true if file
107
107
  file.close
108
108
  end
@@ -0,0 +1,170 @@
1
+ # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ module Contrast
5
+ module Config
6
+ module Diagnostics
7
+ # Tools to help with the config diagnostics, called directly from the module.
8
+ module SingletonTools
9
+ API_CREDENTIALS = %w[api_key service_key].cs__freeze
10
+ CONTRAST_MARK = 'CONTRAST_'
11
+
12
+ # Creates new config instances for each read config entry from the flat generated configs.
13
+ #
14
+ # @param flats [Array] of flatten configs produced by #flatten_settings
15
+ # @param source [Boolean] flag to set the desired value class, it may be a effective or source value.
16
+ # @param cli [Boolean] flag to check if the value comes from cli.
17
+ # @return [Array<Contrast::Config::Diagnostics::SourceConfigValue>]
18
+ def to_config_values flats, source: false, cli: false
19
+ config_value_klass = if source
20
+ Contrast::Config::Diagnostics::SourceConfigValue
21
+ else
22
+ Contrast::Config::Diagnostics::EffectiveConfigValue
23
+ end
24
+ settings = []
25
+ flats.each do |entry|
26
+ entry.each do |key, value|
27
+ efc_value = config_value_klass.new.tap do |config_value|
28
+ config_value.canonical_name = key
29
+ if cli && key.to_s.include?(CONTRAST_MARK)
30
+ config_value.canonical_name = key.gsub(Contrast::Utils::ObjectShare::DOUBLE_UNDERSCORE,
31
+ Contrast::Utils::ObjectShare::PERIOD).downcase
32
+ end
33
+ config_value.key = key
34
+ config_value.value = if API_CREDENTIALS.include?(key.to_s)
35
+ Contrast::Configuration::EFFECTIVE_REDACTED
36
+ else
37
+ value_to_s(value)
38
+ end
39
+ end
40
+ next unless efc_value
41
+
42
+ settings << efc_value
43
+ end
44
+ end
45
+ settings
46
+ end
47
+
48
+ # Flattens out the read settings from file, env or contrast ui.
49
+ # example: {"agent.polling.server_settings_ms"=>"50000"}
50
+ #
51
+ # If cli is set we avoid adding the path and additional '.' to the key.
52
+ #
53
+ # @param data [Hash, nil]
54
+ # @param path [String] where to look for settings.
55
+ # @param config [Hash] symbolized config to fetch keys from.
56
+ # @param cli [Boolean] does the config come from cli.
57
+ def flatten_settings data, path = [], config: Contrast::CONFIG.config.loaded_config, cli: false
58
+ return [] unless data
59
+
60
+ data.each_with_object([]) do |(k, v), entries|
61
+ if v.cs__is_a?(Hash)
62
+ entries.concat(flatten_settings(v, path.dup.append(k.to_sym)))
63
+ else
64
+ if API_CREDENTIALS.include?(k.to_s)
65
+ entries << { k.to_s => Contrast::Configuration::EFFECTIVE_REDACTED } if cli
66
+ entries << { "#{ path.join('.') }.#{ k }" => Contrast::Configuration::EFFECTIVE_REDACTED } unless cli
67
+ next
68
+ end
69
+ entries << { k.to_s => value_to_s(config.dig(*path, k)) } if cli
70
+ entries << { "#{ path.join('.') }.#{ k }" => value_to_s(config.dig(*path, k)) } unless cli
71
+ end
72
+ end.flatten # rubocop:disable Style/MethodCalledOnDoEndBlock
73
+ end
74
+
75
+ # Update the stored config values to ensure that we know about the correct values,
76
+ # and that the sources are correct for entries updated from the UI.
77
+ #
78
+ # @param parts [Array<Symbols>, String] the path to the setting in config
79
+ # Accepts Array: [:agent :enable] or String: 'agent.enable'
80
+ # @param value [String, Integer, Array, nil] the value for the configuration setting
81
+ # @param source_type [String] the source of the configuration setting
82
+ def update_config parts, value, source_type
83
+ parts_array, string = handle_parts_array(parts)
84
+ path = string ? parts : parts_array.join('.')
85
+ return unless parts_array
86
+
87
+ # Check to see whether the source has been overridden by local settings,
88
+ # Before updating from Contrast UI.
89
+ if source_type == Contrast::Components::Config::Sources::CONTRAST_UI &&
90
+ Contrast::CONFIG.sources.source_overridden?(path)
91
+
92
+ return
93
+ end
94
+
95
+ level = Contrast::CONFIG.config.loaded_config
96
+ parts_array[0...-1]&.each do |segment|
97
+ level[segment] ||= {}
98
+ level = level[segment]
99
+ end
100
+ return unless level.cs__is_a?(Hash)
101
+
102
+ level[parts_array[-1]] = value
103
+ Contrast::CONFIG.sources.set(path, source_type)
104
+ end
105
+
106
+ # Recursively converts each value to string.
107
+ #
108
+ # @param value [Hash, nil]
109
+ def value_to_s value
110
+ case value
111
+ when String
112
+ if Contrast::Utils::DuckUtils.empty_duck?(value)
113
+ Contrast::Config::Diagnostics::SourceConfigValue::NULL
114
+ else
115
+ value
116
+ end
117
+ when Array
118
+ handle_array_to_s(value)
119
+ when Hash
120
+ handle_hash_to_s(value)
121
+ when TrueClass, FalseClass, Symbol, Integer
122
+ value.to_s
123
+ else
124
+ Contrast::Config::Diagnostics::SourceConfigValue::NULL
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ # Checks the type of path and converts it to array.
131
+ # If the path is string it splits it by '.' and converts each element to symbol.
132
+ #
133
+ # @param parts [Array<Symbols>, String] the path to the setting in config
134
+ # @return [Array<Symbols>, String]
135
+ def handle_parts_array parts
136
+ string = false
137
+ arr = if parts.cs__is_a?(String)
138
+ string = true
139
+ parts.split('.')&.map&.each(&:to_sym)
140
+ else
141
+ parts
142
+ end
143
+ [arr, string]
144
+ end
145
+
146
+ # @param hash [Hash]
147
+ # @return [Hash]
148
+ def handle_hash_to_s hash
149
+ hash&.each_with_object({}) do |(k, v), m| # rubocop:disable Style/HashTransformValues
150
+ m[k] = if v.cs__is_a?(Hash)
151
+ value_to_s(v)
152
+ elsif v.cs__is_a?(Array)
153
+ v.map(&:to_s)
154
+ else
155
+ v.to_s
156
+ end
157
+ end
158
+ end
159
+
160
+ # @param array [Array]
161
+ # @return [String]
162
+ def handle_array_to_s array
163
+ return Contrast::Config::Diagnostics::SourceConfigValue::NULL if Contrast::Utils::DuckUtils.empty_duck?(array)
164
+
165
+ array.join(',')
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -1,11 +1,16 @@
1
1
  # Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'contrast/utils/object_share'
5
+ require 'contrast/utils/duck_utils'
6
+
4
7
  module Contrast
5
8
  module Config
6
9
  module Diagnostics
7
10
  # All config values from all sources, stored in a easy to write representation.
8
11
  class SourceConfigValue
12
+ NULL = 'null'
13
+
9
14
  # @return [String] Name of the config starting form root of yaml config.
10
15
  attr_accessor :canonical_name
11
16
  # @return [String] Name of the config.
@@ -20,23 +25,23 @@ module Contrast
20
25
  def to_controlled_hash
21
26
  {
22
27
  canonical_name: canonical_name,
23
- name: key,
24
- value: value.cs__is_a?(Array) ? value.map(&:to_s) : value.to_s,
25
- source: source,
26
- filename: filename
27
- }.compact
28
+ name: key || Contrast::Utils::ObjectShare::EMPTY_STRING,
29
+ value: Contrast::Config::Diagnostics::Tools.value_to_s(value),
30
+ source: source || Contrast::Utils::ObjectShare::EMPTY_STRING,
31
+ filename: filename || Contrast::Utils::ObjectShare::EMPTY_STRING
32
+ }
28
33
  end
29
34
 
30
35
  def to_source_hash
31
36
  {
32
37
  canonical_name: canonical_name,
33
- name: key,
34
- value: value.cs__is_a?(Array) ? value.map(&:to_s) : value.to_s
35
- }.compact
38
+ name: key || Contrast::Utils::ObjectShare::EMPTY_STRING,
39
+ value: Contrast::Config::Diagnostics::Tools.value_to_s(value)
40
+ }
36
41
  end
37
42
 
38
43
  # Assigns file name of the config iv viable, Currently supported formats for config file are *.yaml
39
- # and *.yml
44
+ # and *.yml. For the config loaded from file the filename is kept as source.
40
45
  #
41
46
  # @param source [String] name of the source file yaml | yml
42
47
  # @return [Array<String>, nil]