contrast-agent 7.4.1 → 7.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2b62e21b5f8d2c3d786aaaac33005487eab07fd07438e9fe20532742f926bcb0
4
- data.tar.gz: 37ff12d328d1a58bddb75e4f0e9c799b44df881daaa78bea62f565f95ae715ff
3
+ metadata.gz: f6b34ada48cf9e91e05d85ea0f003575119b751bf135ef49709fb1501c475b3e
4
+ data.tar.gz: 2915fb037c832fec213dfc7835b8d732e1c22005987ef2c4287dfb55264d41a2
5
5
  SHA512:
6
- metadata.gz: 46f9f576d3a28befa4681e73a250af98602c100d50ace13a1e3fcaa5a22ca2a0d0695b4b19492677d57183ff56203d2f57a5470b7c6fa0fc9c1b15bbbd49dc16
7
- data.tar.gz: 427a3dafa3d8108a4aa2130ff02c8c4d52539f7d1c7b265ba78d60dc10f86f142a2b4f1132ae0be02c0c9a41c043ab1459c0a0e3a836dbced2f0db4b550aa970
6
+ metadata.gz: 627d1959c5993100f6bc08624d1a68627b02af18f6aa48c074f0cf374925877761b09077ce21366b6d5ddafb3185907472564753b3fbab65f6f7d74d2b74dc10
7
+ data.tar.gz: 224228b9e9c276c39e6d78a330a456b37cf550b4155c4b96f5ff1d29e7bf7846521c102f510061f8d6ec5e9245b99fe567cce7a5202dd2acfd62c6e7bb9ca795
@@ -27,7 +27,8 @@ module Contrast
27
27
  pp_id: @ppid,
28
28
  process_pid: Process.pid,
29
29
  process_pp_id: Process.ppid)
30
-
30
+ $stdout.puts('[Contrast Agent] Graceful shutdown...')
31
+ report_traces
31
32
  context = Contrast::Agent::REQUEST_TRACKER.current
32
33
  return unless context
33
34
 
@@ -39,6 +40,20 @@ module Contrast
39
40
  end
40
41
  Contrast::Agent.reporter&.send_event_immediately(context.activity)
41
42
  end
43
+
44
+ def self.report_traces
45
+ return unless Contrast::ASSESS.enabled?
46
+
47
+ collection = Contrast::Agent::Reporting::ReportingStorage.collection
48
+
49
+ # report gathered traces:
50
+ return if collection.empty?
51
+
52
+ collection.each do |_id, finding|
53
+ preflight = Contrast::Agent::Reporting::BuildPreflight.generate(finding)
54
+ Contrast::Agent.reporter&.send_event_immediately(preflight)
55
+ end
56
+ end
42
57
  end
43
58
  end
44
59
  end
@@ -20,6 +20,7 @@ require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
20
20
  require 'contrast/agent/protect/rule/xss/xss'
21
21
  require 'contrast/components/logger'
22
22
  require 'contrast/utils/object_share'
23
+ require 'contrast/utils/duck_utils'
23
24
  require 'contrast/agent/protect/rule/input_classification/base64_statistic'
24
25
  require 'json'
25
26
 
@@ -105,12 +106,10 @@ module Contrast
105
106
  return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
106
107
 
107
108
  input_analysis.inputs.each do |input_type, value|
108
- # TODO: RUBY-2110 Update the HEADER handling if possible.
109
- # Analyze only Header values:
110
- # This may break bot blocker rule:
111
- # value = value.values if input_type == HEADER
112
- next if value.nil? || value.empty?
109
+ value = handle_header(input_type, value)
110
+ next if Contrast::Utils::DuckUtils.empty_duck?(value)
113
111
 
112
+ # Traverse only the Header values:
114
113
  Timeout.timeout(interval) do
115
114
  protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
116
115
  end
@@ -156,6 +155,16 @@ module Contrast
156
155
 
157
156
  private
158
157
 
158
+ # Extracts header values, and skips keys if input_type is Header.
159
+ # @param input_type [Symbol]
160
+ # @param value [Hash, nil]
161
+ # @return value [Hash, nil]
162
+ def handle_header input_type, value
163
+ return value&.values || [] if input_type == Contrast::Agent::Reporting::InputType::HEADER
164
+
165
+ value
166
+ end
167
+
159
168
  # Extract the filename and name of the Content Disposition Header.
160
169
  #
161
170
  # @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
@@ -22,32 +22,6 @@ module Contrast
22
22
  class << self
23
23
  include Contrast::Agent::Protect::Rule::InputClassification::Base
24
24
 
25
- # Input Classification stage is done to determine if an user input is
26
- # DEFINITEATTACK or to be ignored.
27
- #
28
- # @param rule_id [String] Name of the protect rule.
29
- # @param input_type [Contrast::Agent::Reporting::InputType] The type of the user input.
30
- # @param value [Hash<String>] the value of the input.
31
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] Holds all the results from the
32
- # agent analysis from the current
33
- # Request.
34
- # @return ia [Contrast::Agent::Reporting::InputAnalysis, nil] with updated results.
35
- def classify rule_id, input_type, value, input_analysis
36
- return unless (rule = Contrast::PROTECT.rule(rule_id))
37
- return unless rule.applicable_user_inputs.include?(input_type)
38
- return unless input_analysis.request
39
-
40
- value.each_value do |val|
41
- result = create_new_input_result(input_analysis.request, rule.rule_name, input_type, val)
42
- append_result(input_analysis, result)
43
- end
44
-
45
- input_analysis
46
- rescue StandardError => e
47
- logger.debug("An Error was recorded in the input classification of the #{ rule_id }", error: e)
48
- nil
49
- end
50
-
51
25
  private
52
26
 
53
27
  # This methods checks if input is tagged WORTHWATCHING or IGNORE matches value with it's
@@ -21,6 +21,11 @@ module Contrast
21
21
  include Contrast::Components::Logger::InstanceMethods
22
22
  include Contrast::Agent::Reporting::InputType
23
23
  NAME = 'cmd-injection'
24
+ APPLICABLE_USER_INPUTS = [
25
+ BODY, COOKIE_VALUE, HEADER, PARAMETER_NAME,
26
+ PARAMETER_VALUE, JSON_VALUE, MULTIPART_VALUE,
27
+ MULTIPART_FIELD_NAME, XML_VALUE, DWR_VALUE
28
+ ].cs__freeze
24
29
 
25
30
  def rule_name
26
31
  NAME
@@ -24,7 +24,7 @@ module Contrast
24
24
  COOKIE_VALUE, PARAMETER_VALUE, HEADER, JSON_VALUE, MULTIPART_VALUE, XML_VALUE, DWR_VALUE
25
25
  ].cs__freeze
26
26
 
27
- BASE64_INPUT_TYPES = [BODY, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
27
+ BASE64_INPUT_TYPES = [BODY, HEADER, COOKIE_VALUE, PARAMETER_VALUE, MULTIPART_VALUE, XML_VALUE].cs__freeze
28
28
 
29
29
  class << self
30
30
  include Contrast::Components::Logger::InstanceMethods
@@ -183,9 +183,6 @@ module Contrast
183
183
  return value unless Contrast::PROTECT.normalize_base64?
184
184
  return value unless BASE64_INPUT_TYPES.include?(input_type)
185
185
 
186
- # TODO: RUBY-2110 Update the HEADER handling if possible.
187
- # We need only the Header values.
188
-
189
186
  cs__decode64(value, input_type)
190
187
  end
191
188
  end
@@ -37,7 +37,7 @@ module Contrast
37
37
  # The Base64 method will return printable ascii characters, so we can use this to determine if the value is
38
38
  # encoded or not.
39
39
  #
40
- # The solution in this case is encodind the value, and then decoding it. If the value is already encoded
40
+ # The solution in this case is encoding the value, and then decoding it. If the value is already encoded
41
41
  # it will not be eq to the original value. If the value is not encoded, it will be eq to the original value.
42
42
  #
43
43
  # @param value [String] input to check for encoding status.
@@ -96,11 +96,43 @@ module Contrast
96
96
  def cs__decode64 value, input_type
97
97
  return value unless cs__base64?(value, input_type)
98
98
 
99
- Base64.decode64(value)
99
+ new_value = try_base64_decode(value)
100
+ new_value, success = normalize_encoding(new_value)
101
+ return new_value if success
102
+
103
+ value
100
104
  rescue StandardError => e
101
105
  logger.error('Error while decoding base64', error: e, message: e.message, backtrace: e.backtrace)
102
106
  value
103
107
  end
108
+
109
+ private
110
+
111
+ # Try and decode the value, do not use decoding if the value have zero bytes.
112
+ def try_base64_decode value
113
+ new_value = Base64.decode64(value)
114
+ # check for null bytes:
115
+ return new_value unless new_value.bytes.select(&:zero?).any?
116
+
117
+ value
118
+ end
119
+
120
+ # Detecting encoded Base64 is not perfect. In some cases it will detect certain inputs as
121
+ # encoded and will try to decode them. Even if the decoding is successful, the value may be
122
+ # encoded back to ASCII format. AgentLib will raise UTF-8 error in this case.
123
+ # This method will try to normalize the encoding to UTF-8. If the encoding fails, this means
124
+ # that the decoding was not successful and the value will be returned as is. Otherwise a
125
+ # base64 decoded string with ASCII-8BIT encoding will be parsed to UTF-8 without errors.
126
+ #
127
+ # @param value [String] input to normalize.
128
+ # @return [Array<String, Boolean>] normalized value and success flag.
129
+ def normalize_encoding value
130
+ new_value = value.dup.encode!('Windows-1252').force_encoding('UTF-8')
131
+ [new_value, true]
132
+ rescue StandardError
133
+ # encoding failed, or the decoding from base64 failed.
134
+ [nil, false]
135
+ end
104
136
  end
105
137
  end
106
138
  end
@@ -33,42 +33,12 @@ module Contrast
33
33
  # @return
34
34
  def to_a
35
35
  @_to_a ||= [
36
- UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME, PARAMETER_VALUE,
37
- QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE, MULTIPART_VALUE,
38
- MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST, URL_PARAMETER, UNKNOWN
36
+ UNDEFINED_TYPE, BODY, COOKIE_NAME, COOKIE_VALUE, HEADER, PARAMETER_NAME,
37
+ PARAMETER_VALUE, QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE,
38
+ MULTIPART_VALUE, MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST,
39
+ URL_PARAMETER, UNKNOWN
39
40
  ]
40
41
  end
41
-
42
- # This is a hash of the input types and their corresponding values.
43
- #
44
- # @return [Hash]
45
-
46
- def to_hash
47
- {
48
- UNDEFINED_TYPE: '1',
49
- BODY: '2',
50
- COOKIE_NAME: '3',
51
- COOKIE_VALUE: '4',
52
- HEADER: '5',
53
- PARAMETER_NAME: '6',
54
- PARAMETER_VALUE: '7',
55
- QUERYSTRING: '8',
56
- URI: '9',
57
- SOCKET: '10',
58
- JSON_VALUE: '11',
59
- JSON_ARRAYED_VALUE: '12',
60
- MULTIPART_CONTENT_TYPE: '13',
61
- MULTIPART_VALUE: '14',
62
- MULTIPART_FIELD_NAME: '15',
63
- MULTIPART_NAME: '16',
64
- XML_VALUE: '17',
65
- DWR_VALUE: '18',
66
- METHOD: '19',
67
- REQUEST: '20',
68
- URL_PARAMETER: '21',
69
- UNKNOWN: '22'
70
- }
71
- end
72
42
  end
73
43
  end
74
44
  end
@@ -43,7 +43,7 @@ module Contrast
43
43
  appPath: ::Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name
44
44
  appVersion: ::Contrast::APP_CONTEXT.version,
45
45
  code: CODE,
46
- data: '',
46
+ data: @data || '',
47
47
  key: 0,
48
48
  session_id: ::Contrast::ASSESS.session_id,
49
49
  routes: @routes.map(&:to_controlled_hash)
@@ -17,14 +17,14 @@ module Contrast
17
17
  # @param finding [Contrast::Agent::Reporting::Finding]
18
18
  # @return [Contrast::Agent::Reporting::Preflight, nil]
19
19
  def generate finding
20
- return unless finding
20
+ return unless finding&.cs__is_a?(Contrast::Agent::Reporting::Finding)
21
21
 
22
22
  new_preflight = Contrast::Agent::Reporting::Preflight.new
23
23
  new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
24
- finding.routes.each do |route|
25
- new_preflight_message.routes << route
24
+ routes = finding.routes
25
+ unless Contrast::Utils::DuckUtils.empty_duck?(routes)
26
+ routes.each { |route| new_preflight_message.routes << route }
26
27
  end
27
- new_preflight_message.hash_code = finding.hash_code
28
28
  new_preflight_message.data = "#{ finding.rule_id },#{ finding.hash_code }"
29
29
  new_preflight.messages << new_preflight_message
30
30
  return new_preflight unless Contrast::Utils::DuckUtils.empty_duck?(new_preflight.messages)
@@ -118,7 +118,11 @@ module Contrast
118
118
  mode.resend.reset_rescue_attempts
119
119
  findings_to_return.each do |index|
120
120
  preflight_message = event.messages[index.to_i]
121
- corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
121
+ preflight_data = preflight_message&.data
122
+ corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_data)
123
+ if Contrast::Agent::REQUEST_TRACKER.current
124
+ Contrast::Agent::REQUEST_TRACKER.current.reported_findings << preflight_data
125
+ end
122
126
  next unless corresponding_finding
123
127
 
124
128
  send_event(corresponding_finding, connection)
@@ -263,7 +263,7 @@ module Contrast
263
263
  extract_response_last_modified(response, event)
264
264
  populate_response(response_data, event)
265
265
  rescue StandardError => e
266
- logger.error('Unable to convert response', e)
266
+ logger.error('Unable to convert response', error: e)
267
267
  nil
268
268
  end
269
269
 
@@ -3,6 +3,6 @@
3
3
 
4
4
  module Contrast
5
5
  module Agent
6
- VERSION = '7.4.1'
6
+ VERSION = '7.5.0'
7
7
  end
8
8
  end
@@ -14,7 +14,7 @@ module Contrast
14
14
 
15
15
  # Add any known cases where parsing error might arise from older json parser:
16
16
  # @return [Array<String>]
17
- SPECIAL_CASES = ["\"\""].cs__freeze # rubocop:disable Style/StringLiterals
17
+ SPECIAL_CASES = ["\"\"", "\"0\""].cs__freeze # rubocop:disable Style/StringLiterals
18
18
 
19
19
  # Parses a string using JSON.parser. This method is used instead of standard JSON.parse to
20
20
  # support older versions of json gem => not supporting key-value second parameter, which is
data/ruby-agent.gemspec CHANGED
@@ -116,9 +116,10 @@ end
116
116
  # dependencies.csv in this directory to indicate that and create a
117
117
  # corresponding update to the fake gem server data in TeamServer.
118
118
  def self.add_dependencies spec
119
- spec.add_dependency 'ffi', '~> 1.0'
119
+ # TODO: RUBY-99999 investigate init_with_options segmentation fault
120
+ spec.add_dependency 'ffi'
120
121
  spec.add_dependency 'ougai', '>= 1.8', '< 3.0.0'
121
- spec.add_dependency 'rack', '~> 2.0'
122
+ spec.add_dependency 'rack', '>= 2.0', '< 4.0.0'
122
123
 
123
124
  # bind this directly as we've had issues w/ build changes on bug release
124
125
  spec.add_dependency 'contrast-agent-lib', '1.1.1'
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.4.1
4
+ version: 7.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - galen.palmer@contrastsecurity.com
@@ -10,10 +10,10 @@ authors:
10
10
  - alex.macdonald@contrastsecurity.com
11
11
  - mark.petersen@contrastsecurity.com
12
12
  - joshua.reed@contrastsecurity.com
13
- autorequire:
13
+ autorequire:
14
14
  bindir: exe
15
15
  cert_chain: []
16
- date: 2023-09-21 00:00:00.000000000 Z
16
+ date: 2023-10-06 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: bundler
@@ -619,16 +619,16 @@ dependencies:
619
619
  name: ffi
620
620
  requirement: !ruby/object:Gem::Requirement
621
621
  requirements:
622
- - - "~>"
622
+ - - ">="
623
623
  - !ruby/object:Gem::Version
624
- version: '1.0'
624
+ version: '0'
625
625
  type: :runtime
626
626
  prerelease: false
627
627
  version_requirements: !ruby/object:Gem::Requirement
628
628
  requirements:
629
- - - "~>"
629
+ - - ">="
630
630
  - !ruby/object:Gem::Version
631
- version: '1.0'
631
+ version: '0'
632
632
  - !ruby/object:Gem::Dependency
633
633
  name: ougai
634
634
  requirement: !ruby/object:Gem::Requirement
@@ -653,16 +653,22 @@ dependencies:
653
653
  name: rack
654
654
  requirement: !ruby/object:Gem::Requirement
655
655
  requirements:
656
- - - "~>"
656
+ - - ">="
657
657
  - !ruby/object:Gem::Version
658
658
  version: '2.0'
659
+ - - "<"
660
+ - !ruby/object:Gem::Version
661
+ version: 4.0.0
659
662
  type: :runtime
660
663
  prerelease: false
661
664
  version_requirements: !ruby/object:Gem::Requirement
662
665
  requirements:
663
- - - "~>"
666
+ - - ">="
664
667
  - !ruby/object:Gem::Version
665
668
  version: '2.0'
669
+ - - "<"
670
+ - !ruby/object:Gem::Version
671
+ version: 4.0.0
666
672
  - !ruby/object:Gem::Dependency
667
673
  name: contrast-agent-lib
668
674
  requirement: !ruby/object:Gem::Requirement
@@ -1370,7 +1376,7 @@ metadata:
1370
1376
  support_uri: https://support.contrastsecurity.com
1371
1377
  trouble_shooting_uri: https://support.contrastsecurity.com/hc/en-us/search?utf8=%E2%9C%93&query=Ruby
1372
1378
  wiki_uri: https://docs.contrastsecurity.com/
1373
- post_install_message:
1379
+ post_install_message:
1374
1380
  rdoc_options: []
1375
1381
  require_paths:
1376
1382
  - lib
@@ -1388,8 +1394,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1388
1394
  - !ruby/object:Gem::Version
1389
1395
  version: '0'
1390
1396
  requirements: []
1391
- rubygems_version: 3.2.33
1392
- signing_key:
1397
+ rubygems_version: 3.3.26
1398
+ signing_key:
1393
1399
  specification_version: 4
1394
1400
  summary: Contrast Security's agent for rack-based applications.
1395
1401
  test_files: []