contrast-agent 7.3.0 → 7.3.1

Sign up to get free protection for your applications and to get access to all the features.
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