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 +4 -4
- data/lib/contrast/agent/hooks/at_exit_hook.rb +16 -1
- data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +14 -5
- data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +0 -26
- data/lib/contrast/agent/protect/rule/cmdi/cmd_injection.rb +5 -0
- data/lib/contrast/agent/protect/rule/input_classification/base.rb +1 -4
- data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +34 -2
- data/lib/contrast/agent/reporting/input_analysis/input_type.rb +4 -34
- data/lib/contrast/agent/reporting/reporting_events/preflight_message.rb +1 -1
- data/lib/contrast/agent/reporting/reporting_utilities/build_preflight.rb +4 -4
- data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +5 -1
- data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +1 -1
- data/lib/contrast/agent/version.rb +1 -1
- data/lib/contrast/utils/json.rb +1 -1
- data/ruby-agent.gemspec +3 -2
- metadata +18 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6b34ada48cf9e91e05d85ea0f003575119b751bf135ef49709fb1501c475b3e
|
4
|
+
data.tar.gz: 2915fb037c832fec213dfc7835b8d732e1c22005987ef2c4287dfb55264d41a2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
109
|
-
|
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
|
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
|
-
|
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,
|
37
|
-
QUERYSTRING, URI, SOCKET, JSON_VALUE, JSON_ARRAYED_VALUE, MULTIPART_CONTENT_TYPE,
|
38
|
-
MULTIPART_FIELD_NAME, MULTIPART_NAME, XML_VALUE, DWR_VALUE, METHOD, REQUEST,
|
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
|
25
|
-
|
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
|
-
|
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
|
|
data/lib/contrast/utils/json.rb
CHANGED
@@ -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
|
-
|
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', '
|
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
|
+
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-
|
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: '
|
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: '
|
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.
|
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: []
|