contrast-agent 7.4.1 → 7.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|