contrast-agent 7.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e64852411fae5dd5c52973361e7f54cdf60813105423ed81cd6455c6ec212330
4
- data.tar.gz: d7d6a1ef01242d97b36f1b8fc4767829d300ec2f3ce1dcb21df7fe05aebc22b1
3
+ metadata.gz: 4223bb2218df4bdf98b2600c77c4fa148e71b830575e938a968d41e89138fb81
4
+ data.tar.gz: cc559a6b364c5019d9c50d8ee6b8237486ac1596ec8ade76d8e6ae4e51e1b906
5
5
  SHA512:
6
- metadata.gz: 86626808971cfc1fcd74febe8cdf2d41be668a78cc02d41d4bf7f6a488e550fef9f2a8fe3230eea7ea9ed63824d09e61956878d34d99375dd3595b77d3a9755f
7
- data.tar.gz: 9fb15e1733095b1f7c2a9d5d4bfce96a3a2e9d5f956d77f0cd6c1fb810c44166a7bf0e2b89966a63ca80aa34514dd82e6f87e619a782d5024854d84c1ed93183
6
+ metadata.gz: 6b88f94ad140a8dd0e8099e2d4f3a396f2221de6ba9a3cbdfaa7e9327644703b7dc275c369ffe4efa29109619bf9eb182f2a0f7b85103c67e2a83246c49c137a
7
+ data.tar.gz: eaf65ba9546f37ace248a29657e690966deaf02aa26426b169f1f3e9639aa3f7b355ea858afd4ef0a5787bd9e2549d7927b3db89894eb6ef4bcd26daf01382e9
@@ -16,6 +16,7 @@ module Contrast
16
16
  class PolicyNode < Contrast::Agent::Patching::Policy::PolicyNode
17
17
  include Contrast::Components::Logger::InstanceMethods
18
18
  include PolicyNodeUtils
19
+
19
20
  JSON_TAGS = 'tags'
20
21
  JSON_DATAFLOW = 'dataflow'
21
22
  # The keys used to read from policy.json to create the individual
@@ -48,6 +49,9 @@ module Contrast
48
49
  ].cs__freeze
49
50
  TO_S = %w[to_s to_str].cs__freeze
50
51
 
52
+ # Here are all Responses that will be tracked as sources, or methods they use, like body.
53
+ RESPONSE_SOURCES = %w[Net::HTTPResponse Rack::Response Sinatra::Response].cs__freeze
54
+
51
55
  def initialize policy_hash = {}
52
56
  super(policy_hash)
53
57
  @source_string = policy_hash[JSON_SOURCE]
@@ -57,13 +61,14 @@ module Contrast
57
61
  @targets = convert_policy_markers(target_string)
58
62
  @_use_original_object = ORIGINAL_OBJECT_METHODS.include?(@method_name)
59
63
  @_use_original_on_bang_method = assign_on_bang_check(policy_hash)
64
+ @_use_response_as_source = RESPONSE_SOURCES.include?(@class_name)
60
65
  end
61
66
 
67
+ # If we have KEEP action on String, and the method is to_s, that method would return self:
68
+ # String#to_s => self or string. This method is included here to cover the situations such as
69
+ # String.to_s.html_safe, where normally the dynamic sources properties get lost. To solve this
70
+ # we will simply return the original object here.
62
71
  def assign_on_bang_check policy_hash
63
- # If we have KEEP action on String, and the method is to_s, that method would return self:
64
- # String#to_s => self or string. This method is included here to cover the situations such as
65
- # String.to_s.html_safe, where normally the dynamic sources properties get lost. To solve this
66
- # we will simply return the original object here.
67
72
  return true if @_use_original_object && TO_S.include?(policy_hash[JSON_METHOD_NAME])
68
73
 
69
74
  @_use_original_object &&
@@ -166,7 +171,7 @@ module Contrast
166
171
  # that the method is without bang - it does not change the source, but rather
167
172
  # creates a copy of it.
168
173
  #
169
- # @return true | false
174
+ # @return [Boolean]
170
175
  def use_original_object?
171
176
  @_use_original_object && Contrast::ASSESS.track_original_object?
172
177
  end
@@ -175,10 +180,24 @@ module Contrast
175
180
  # that the target return is the same as object - a bang method modifying the
176
181
  # source.
177
182
  #
178
- # @return true | false
183
+ # @return [Boolean]
179
184
  def use_original_on_bang_method?
180
185
  @_use_original_on_bang_method && Contrast::ASSESS.track_original_object?
181
186
  end
187
+
188
+ # This method will check if policy is fit to use response as source.
189
+ #
190
+ # @return [Boolean]
191
+ def use_response_as_source?
192
+ Contrast::ASSESS.track_response_as_source?
193
+ end
194
+
195
+ # This method will check if the policy node is for response method.
196
+ #
197
+ # @return [Boolean]
198
+ def response_source_node?
199
+ @_use_response_as_source
200
+ end
182
201
  end
183
202
  end
184
203
  end
@@ -0,0 +1,64 @@
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
+ require 'contrast/agent/assess/policy/propagator/select'
5
+ require 'contrast/utils/duck_utils'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Assess
10
+ module Policy
11
+ module Propagator
12
+ # Propagation that results in all the tags of the source being
13
+ # applied to the target at the point of insertion. The target's
14
+ # preexisting tags are shifted to account for this insertion.
15
+ class Response < Contrast::Agent::Assess::Policy::Propagator::Base
16
+ class << self
17
+ # This will path the Net::HTTP.request method. It takes two parameters:
18
+ # - req: Net::HTTPGenericRequest
19
+ # - body: String
20
+ # As body may be optional, we need to check if it's nil or not.
21
+ #
22
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
23
+ # @param preshift [Contrast::Agent::Assess::Preshift]
24
+ # @param ret [Object] Return targer from method invocation.
25
+ # @param _block [nil, {}] block passed.
26
+ def net_response_keep propagation_node, preshift, ret, _block
27
+ return unless Contrast::ASSESS.track_response_as_source?
28
+
29
+ # Check to see if the argument is of correct type, and whether the body is tracked or not.
30
+ # if it's tracked and the body is not nil, then copy the properties from the source's body
31
+ # to the target's body.
32
+ source_body = if preshift.args.length == 2
33
+ preshift.args[1]
34
+ else
35
+ preshift.args[0]&.body
36
+ end
37
+ copy_body_tags(propagation_node, source_body, ret)
38
+ end
39
+
40
+ private
41
+
42
+ # Copy the properties form source body to the response body, if one is present.
43
+ #
44
+ # @param propagation_node [Contrast::Agent::Assess::Policy::PropagationNode]
45
+ # @param source_body [String] the tracked body to copy from.
46
+ # @param ret [String] the return target from method invocation.
47
+ # @return [String, nil]
48
+ def copy_body_tags propagation_node, source_body, ret
49
+ return if Contrast::Utils::DuckUtils.empty_duck?(source_body)
50
+ return unless ret&.body&.cs__is_a?(String)
51
+ return unless source_body&.cs__is_a?(String)
52
+ return unless (properties = Contrast::Agent::Assess::Tracker.properties!(ret.body))
53
+
54
+ # KEEP
55
+ properties.copy_from(source_body, ret.body, 0, propagation_node.untags)
56
+ ret
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -31,6 +31,7 @@ module Contrast
31
31
  require 'contrast/agent/assess/policy/propagator/substitution'
32
32
  require 'contrast/agent/assess/policy/propagator/trim'
33
33
  require 'contrast/agent/assess/policy/propagator/buffer'
34
+ require 'contrast/agent/assess/policy/propagator/response'
34
35
  end
35
36
  end
36
37
  end
@@ -46,6 +46,11 @@ module Contrast
46
46
  # Exclusions makes method slow:
47
47
  return if excluded_by_url?
48
48
 
49
+ # Check to see if the source node is to be used for response as source.
50
+ if method_policy.source_node.response_source_node? && !method_policy.source_node.use_response_as_source?
51
+ return
52
+ end
53
+
49
54
  # used to hold the object and ret
50
55
  source_data = Contrast::Agent::Assess::Events::EventData.new(nil, nil, object, ret, nil)
51
56
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'rack'
5
5
  require 'contrast/utils/hash_digest'
6
+ require 'contrast/utils/duck_utils'
6
7
  require 'contrast/utils/string_utils'
7
8
  require 'contrast/agent/assess/rule/response/base_rule'
8
9
 
@@ -44,21 +45,35 @@ module Contrast
44
45
  # @param element_start_str [String] element to find in html section
45
46
  # @return [Array<Hash>] the found elements of this section, as well as their start and end indexes.
46
47
  def html_elements section, element_start_str = '', capture_overflow: false
48
+ return [] unless section
49
+ return [] unless (potentials = potential_elements(section, element_start_str).flatten).any?
50
+
47
51
  elements = []
48
52
  section_start = 0
49
- return [] unless section
50
53
 
51
- potential_elements(section, element_start_str).flatten.each do |potential_element|
54
+ potentials.each do |potential_element|
52
55
  next unless potential_element
53
56
  next unless element_openings.any? { |opening| potential_element.start_with?(opening) }
54
57
 
55
- section_start = section.index(element_start_str, section_start)
56
- next unless section_start
58
+ start = section&.index(element_start_str, section_start)
59
+ next if Contrast::Utils::DuckUtils.empty_duck?(start)
60
+
61
+ stop = potential_element.index('>').to_i
62
+ next if Contrast::Utils::DuckUtils.empty_duck?(stop)
57
63
 
58
- element_stop = potential_element.index('>').to_i
59
- next unless element_stop
64
+ section_close = start + 6 + stop
65
+ # Now we have valid tag section with start and stop.
66
+ # Save new boundaries. This is to make sure that If
67
+ # on previous iteration there were non valid section,
68
+ # the start_section will be assigned to nil, thus making
69
+ # the detection of new section not possible, and throwing
70
+ # an error. To that end old values are kept safe.
71
+ #
72
+ # Assign new start index.
73
+ section_start = start
74
+ # Assign new end index.
75
+ element_stop = stop
60
76
 
61
- section_close = section_start + 6 + element_stop
62
77
  elements << capture(section, section_start, section_close, element_stop, overflow: capture_overflow)
63
78
  section_start = section_close
64
79
  end
@@ -70,7 +70,10 @@ module Contrast
70
70
  # @param response [Contrast::Agent::Response] the response of the application
71
71
  # @return [Array<Hash<String,String>]
72
72
  def cache_meta_tags response
73
- html_elements(response.body&.split(HEAD_TAG)&.last, META_START_STR).
73
+ head_tag = response.body&.split(HEAD_TAG)&.last
74
+ return [] unless head_tag
75
+
76
+ html_elements(head_tag, META_START_STR, capture_overflow: false).
74
77
  select { |tag| cache_control_tag?(tag[HTML_PROP]) }
75
78
  end
76
79
 
@@ -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.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,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)
@@ -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|
@@ -18,6 +18,7 @@ module Contrast
18
18
  REPLACE_ACTION = 'REPLACE'
19
19
  REMOVE_ACTION = 'REMOVE'
20
20
  REVERSE_ACTION = 'REVERSE'
21
+ RESPONSE_ACTION = 'RESPONSE'
21
22
  SPLAT_ACTION = 'SPLAT'
22
23
  SPLIT_ACTION = 'SPLIT'
23
24
  DB_WRITE_ACTION = 'DB_WRITE'
@@ -37,6 +38,7 @@ module Contrast
37
38
  REPLACE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Replace,
38
39
  REMOVE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Remove,
39
40
  REVERSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Reverse,
41
+ RESPONSE_ACTION => Contrast::Agent::Assess::Policy::Propagator::Response,
40
42
  SPLAT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Splat,
41
43
  SPLIT_ACTION => Contrast::Agent::Assess::Policy::Propagator::Split
42
44
  }.cs__freeze
@@ -8,7 +8,7 @@ module Contrast
8
8
  module Utils
9
9
  # Simple utility used to make OS calls and determine state. For that state
10
10
  # which will not change at runtime, such as the operating system, the
11
- # Utility memozies to avoid multiple lookups.
11
+ # Utility memoizes to avoid multiple lookups.
12
12
  module OS
13
13
  extend Contrast::Components::Scope::InstanceMethods
14
14
 
@@ -25,14 +25,6 @@ module Contrast
25
25
  @_mac = RUBY_PLATFORM.include?('darwin') if @_mac.nil?
26
26
  @_mac
27
27
  end
28
-
29
- def unix?
30
- !windows?
31
- end
32
-
33
- def linux?
34
- (unix? and !mac?)
35
- end
36
28
  end
37
29
  end
38
30
  end
@@ -8,7 +8,35 @@
8
8
  "target":"R",
9
9
  "type":"PARAMETER",
10
10
  "tags":["CROSS_SITE"]
11
- }, {
11
+ },
12
+ {
13
+ "class_name":"Net::HTTPResponse",
14
+ "instance_method": true,
15
+ "method_visibility": "public",
16
+ "method_name":"body",
17
+ "target":"R",
18
+ "type":"BODY",
19
+ "tags":["CROSS_SITE"]
20
+ },
21
+ {
22
+ "class_name":"Rack::Response",
23
+ "instance_method": true,
24
+ "method_visibility": "public",
25
+ "method_name":"body",
26
+ "target":"R",
27
+ "type":"BODY",
28
+ "tags":["CROSS_SITE"]
29
+ },
30
+ {
31
+ "class_name":"Sinatra::Response",
32
+ "instance_method": true,
33
+ "method_visibility": "public",
34
+ "method_name":"body",
35
+ "target":"R",
36
+ "type":"BODY",
37
+ "tags":["CROSS_SITE"]
38
+ },
39
+ {
12
40
  "class_name":"Rack::Request::Helpers",
13
41
  "instance_method": true,
14
42
  "method_visibility": "public",
@@ -990,7 +1018,35 @@
990
1018
  "source": "P0",
991
1019
  "target": "R",
992
1020
  "action": "SPLAT"
993
- }, {
1021
+ },
1022
+ {
1023
+ "class_name": "ActiveSupport::JSON",
1024
+ "method_name": "encode",
1025
+ "instance_method": false,
1026
+ "method_visibility": "public",
1027
+ "source": "P0",
1028
+ "target": "R",
1029
+ "action": "SPLAT"
1030
+ },
1031
+ {
1032
+ "class_name": "JSON",
1033
+ "method_name": "generate",
1034
+ "instance_method": false,
1035
+ "method_visibility": "public",
1036
+ "source": "P0",
1037
+ "target": "R",
1038
+ "action": "SPLAT"
1039
+ },
1040
+ {
1041
+ "class_name": "JSON",
1042
+ "method_name": "pretty_generate",
1043
+ "instance_method": false,
1044
+ "method_visibility": "public",
1045
+ "source": "P0",
1046
+ "target": "R",
1047
+ "action": "SPLAT"
1048
+ },
1049
+ {
994
1050
  "class_name": "Zlib::Deflate",
995
1051
  "method_name": "deflate",
996
1052
  "instance_method": false,
@@ -1082,7 +1138,28 @@
1082
1138
  "source": "P0",
1083
1139
  "target": "R",
1084
1140
  "action": "SPLAT"
1085
- }, {
1141
+ },
1142
+ {
1143
+ "class_name":"Net::HTTPRequest",
1144
+ "instance_method": true,
1145
+ "method_visibility": "public",
1146
+ "method_name":"body=",
1147
+ "source":"P0",
1148
+ "target":"O",
1149
+ "action":"KEEP"
1150
+ },
1151
+ {
1152
+ "class_name":"Net::HTTP",
1153
+ "instance_method": true,
1154
+ "method_visibility": "public",
1155
+ "method_name":"request",
1156
+ "action": "CUSTOM",
1157
+ "patch_class": "Contrast::Agent::Assess::Policy::Propagator::Response",
1158
+ "patch_method": "net_response_keep",
1159
+ "source": "O,P1",
1160
+ "target": "R"
1161
+ },
1162
+ {
1086
1163
  "class_name": "URI::Generic",
1087
1164
  "method_name": "initialize",
1088
1165
  "instance_method": true,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: contrast-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.3.0
4
+ version: 7.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - galen.palmer@contrastsecurity.com
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: exe
15
15
  cert_chain: []
16
- date: 2023-07-26 00:00:00.000000000 Z
16
+ date: 2023-08-04 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: bundler
@@ -930,6 +930,7 @@ files:
930
930
  - lib/contrast/agent/assess/policy/propagator/rack_protection.rb
931
931
  - lib/contrast/agent/assess/policy/propagator/remove.rb
932
932
  - lib/contrast/agent/assess/policy/propagator/replace.rb
933
+ - lib/contrast/agent/assess/policy/propagator/response.rb
933
934
  - lib/contrast/agent/assess/policy/propagator/reverse.rb
934
935
  - lib/contrast/agent/assess/policy/propagator/select.rb
935
936
  - lib/contrast/agent/assess/policy/propagator/splat.rb