contrast-agent 7.1.0 → 7.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/ext/extconf_common.rb +88 -14
  3. data/lib/contrast/agent/assess/policy/source_method.rb +13 -4
  4. data/lib/contrast/agent/assess/policy/trigger_method.rb +12 -18
  5. data/lib/contrast/agent/excluder/excluder.rb +64 -31
  6. data/lib/contrast/agent/protect/input_analyzer/input_analyzer.rb +62 -23
  7. data/lib/contrast/agent/protect/input_analyzer/worth_watching_analyzer.rb +37 -4
  8. data/lib/contrast/agent/protect/rule/base.rb +9 -7
  9. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker.rb +1 -1
  10. data/lib/contrast/agent/protect/rule/bot_blocker/bot_blocker_input_classification.rb +29 -13
  11. data/lib/contrast/agent/protect/rule/cmdi/cmdi_backdoors.rb +1 -1
  12. data/lib/contrast/agent/protect/rule/cmdi/cmdi_base_rule.rb +0 -1
  13. data/lib/contrast/agent/protect/rule/cmdi/cmdi_input_classification.rb +2 -2
  14. data/lib/contrast/agent/protect/rule/deserialization/deserialization.rb +2 -2
  15. data/lib/contrast/agent/protect/rule/input_classification/base.rb +191 -0
  16. data/lib/contrast/agent/protect/rule/input_classification/base64_statistic.rb +71 -0
  17. data/lib/contrast/agent/protect/rule/input_classification/cached_result.rb +37 -0
  18. data/lib/contrast/agent/protect/rule/input_classification/encoding.rb +109 -0
  19. data/lib/contrast/agent/protect/rule/input_classification/encoding_rates.rb +47 -0
  20. data/lib/contrast/agent/protect/rule/input_classification/extendable.rb +80 -0
  21. data/lib/contrast/agent/protect/rule/input_classification/lru_cache.rb +198 -0
  22. data/lib/contrast/agent/protect/rule/input_classification/match_rates.rb +66 -0
  23. data/lib/contrast/agent/protect/rule/input_classification/rates.rb +53 -0
  24. data/lib/contrast/agent/protect/rule/input_classification/statistics.rb +115 -0
  25. data/lib/contrast/agent/protect/rule/input_classification/utils.rb +23 -0
  26. data/lib/contrast/agent/protect/rule/no_sqli/no_sqli_input_classification.rb +17 -7
  27. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_input_classification.rb +18 -15
  28. data/lib/contrast/agent/protect/rule/path_traversal/path_traversal_semantic_security_bypass.rb +1 -1
  29. data/lib/contrast/agent/protect/rule/sqli/sqli_input_classification.rb +2 -2
  30. data/lib/contrast/agent/protect/rule/sqli/sqli_semantic/sqli_dangerous_functions.rb +1 -1
  31. data/lib/contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input_classification.rb +18 -15
  32. data/lib/contrast/agent/protect/rule/utils/filters.rb +6 -6
  33. data/lib/contrast/agent/protect/rule/xss/reflected_xss_input_classification.rb +19 -17
  34. data/lib/contrast/agent/protect/rule/xxe/xxe.rb +1 -1
  35. data/lib/contrast/agent/reporting/attack_result/attack_result.rb +6 -0
  36. data/lib/contrast/agent/reporting/client/interface.rb +132 -0
  37. data/lib/contrast/agent/reporting/client/interface_base.rb +27 -0
  38. data/lib/contrast/agent/reporting/connection_status.rb +0 -1
  39. data/lib/contrast/agent/reporting/input_analysis/input_analysis.rb +2 -7
  40. data/lib/contrast/agent/reporting/input_analysis/input_analysis_result.rb +17 -4
  41. data/lib/contrast/agent/reporting/input_analysis/input_type.rb +33 -1
  42. data/lib/contrast/agent/reporting/masker/masker_utils.rb +1 -1
  43. data/lib/contrast/agent/reporting/reporter.rb +11 -26
  44. data/lib/contrast/agent/reporting/reporting_events/application_defend_activity.rb +1 -0
  45. data/lib/contrast/agent/reporting/reporting_events/application_defend_attacker_activity.rb +1 -0
  46. data/lib/contrast/agent/reporting/reporting_events/discovered_route.rb +1 -1
  47. data/lib/contrast/agent/reporting/reporting_utilities/audit.rb +10 -3
  48. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client.rb +47 -6
  49. data/lib/contrast/agent/reporting/reporting_utilities/reporter_client_utils.rb +41 -32
  50. data/lib/contrast/agent/reporting/reporting_utilities/resend.rb +144 -0
  51. data/lib/contrast/agent/reporting/reporting_utilities/response_handler.rb +35 -13
  52. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_mode.rb +14 -1
  53. data/lib/contrast/agent/reporting/reporting_utilities/response_handler_utils.rb +11 -11
  54. data/lib/contrast/agent/request/request.rb +27 -12
  55. data/lib/contrast/agent/telemetry/base.rb +44 -19
  56. data/lib/contrast/agent/telemetry/base64_hash.rb +55 -0
  57. data/lib/contrast/agent/telemetry/cache_hash.rb +55 -0
  58. data/lib/contrast/agent/telemetry/client.rb +10 -2
  59. data/lib/contrast/agent/telemetry/exception/obfuscate.rb +97 -0
  60. data/lib/contrast/agent/telemetry/exception.rb +1 -0
  61. data/lib/contrast/agent/telemetry/{hash.rb → exception_hash.rb} +1 -1
  62. data/lib/contrast/agent/telemetry/input_analysis_cache_event.rb +27 -0
  63. data/lib/contrast/agent/telemetry/input_analysis_encoding_event.rb +26 -0
  64. data/lib/contrast/agent/telemetry/input_analysis_event.rb +91 -0
  65. data/lib/contrast/agent/telemetry/metric_event.rb +12 -0
  66. data/lib/contrast/agent/telemetry/startup_metrics_event.rb +0 -8
  67. data/lib/contrast/agent/version.rb +1 -1
  68. data/lib/contrast/components/config/sources.rb +6 -5
  69. data/lib/contrast/components/config.rb +4 -4
  70. data/lib/contrast/components/protect.rb +11 -1
  71. data/lib/contrast/components/sampling.rb +15 -10
  72. data/lib/contrast/components/settings.rb +9 -0
  73. data/lib/contrast/config/diagnostics/environment_variables.rb +3 -1
  74. data/lib/contrast/config/diagnostics/source_config_value.rb +5 -1
  75. data/lib/contrast/config/diagnostics/tools.rb +4 -4
  76. data/lib/contrast/config/validate.rb +2 -2
  77. data/lib/contrast/config/yaml_file.rb +8 -0
  78. data/lib/contrast/configuration.rb +11 -19
  79. data/lib/contrast/framework/grape/support.rb +1 -2
  80. data/lib/contrast/framework/manager.rb +17 -8
  81. data/lib/contrast/framework/rack/support.rb +99 -1
  82. data/lib/contrast/framework/rails/support.rb +4 -2
  83. data/lib/contrast/framework/sinatra/support.rb +1 -2
  84. data/lib/contrast/logger/aliased_logging.rb +18 -9
  85. data/lib/contrast/utils/assess/event_limit_utils.rb +13 -13
  86. data/lib/contrast/utils/hash_utils.rb +21 -2
  87. data/lib/contrast/utils/metrics_hash.rb +1 -1
  88. data/lib/contrast/utils/object_share.rb +2 -1
  89. data/lib/contrast/utils/request_utils.rb +14 -0
  90. data/lib/contrast/utils/response_utils.rb +12 -0
  91. data/lib/contrast/utils/timer.rb +2 -0
  92. data/lib/contrast.rb +9 -2
  93. data/resources/assess/policy.json +11 -0
  94. data/ruby-agent.gemspec +1 -1
  95. metadata +25 -7
  96. data/lib/contrast/agent/reporting/input_analysis/details/bot_blocker_details.rb +0 -27
  97. data/lib/contrast/utils/input_classification_base.rb +0 -169
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81222798666699f86b31b925d531d2ee2229eb7934582d2a502cc61de3ca4e0b
4
- data.tar.gz: 7dd4d41a58600b7d57b5f57cf95c42bd2f6d198f5f1906e93751e33c09efa3e0
3
+ metadata.gz: e64852411fae5dd5c52973361e7f54cdf60813105423ed81cd6455c6ec212330
4
+ data.tar.gz: d7d6a1ef01242d97b36f1b8fc4767829d300ec2f3ce1dcb21df7fe05aebc22b1
5
5
  SHA512:
6
- metadata.gz: 02e5d3aa6b342e8c4277ad6cefde65819aed6f6b4a1079cfe7528fcce236ec702aa8a63fc756c403c0df6e3ef38d5d25dc9f1c6e5c450d272122d699b6fd9872
7
- data.tar.gz: 9cc83b5f69edeea949784ae766be7e02c1d589e58b2af5b5c1bd7975dcee1450d294bc7de647f60a9a3f18a1ea05a43bfe452a52d4760ed1b1cfcb0a7339a6ab
6
+ metadata.gz: 86626808971cfc1fcd74febe8cdf2d41be668a78cc02d41d4bf7f6a488e550fef9f2a8fe3230eea7ea9ed63824d09e61956878d34d99375dd3595b77d3a9755f
7
+ data.tar.gz: 9fb15e1733095b1f7c2a9d5d4bfce96a3a2e9d5f956d77f0cd6c1fb810c44166a7bf0e2b89966a63ca80aa34514dd82e6f87e619a782d5024854d84c1ed93183
@@ -5,6 +5,31 @@ require 'mkmf'
5
5
  require 'rbconfig'
6
6
  require_relative '../lib/contrast/agent/version'
7
7
 
8
+ # Create explicit symbol list, that will be set as promise to be loaded dynamic on run time.
9
+ SYMS = %w[
10
+ _assess
11
+ _policy
12
+ _assess_policy
13
+ _assess_propagator
14
+ _core_assess
15
+ _contrast_patcher
16
+ _contrast_check_prepended
17
+ _contrast_check_and_register_instance_patch
18
+ _contrast_register_singleton_prepend_patch
19
+ _contrast_register_patch
20
+ _contrast_register_singleton_patch
21
+ _inst_methods_enter_cntr_scope
22
+ _inst_methods_enter_method_scope
23
+ _inst_methods_exit_cntr_scope
24
+ _inst_methods_exit_method_scope
25
+ _inst_methods_in_cntr_scope
26
+ _rb_sym_method
27
+ _rb_sym_hash_tracked
28
+ _rb_sym_skip_assess_analysis
29
+ _rb_sym_skip_contrast_analysis
30
+ _patch_via_funchook
31
+ ].freeze # rubocop:disable Security/Object/Freeze
32
+
8
33
  # The mkmf.rb file uses all passed flags from Ruby configuration (RbConfig::CONFIG) on
9
34
  # Ruby build time. Problem with Clang and GCC is that it do not keep up with c89 and finds
10
35
  # error on including <ryby.h> as not allowing inline variables.
@@ -32,8 +57,7 @@ require_relative '../lib/contrast/agent/version'
32
57
  STANDARD_FLAGS = '-std=gnu89'
33
58
  CLANG = 'clang'
34
59
 
35
- # TODO: RUBY-999999 Add -pedantic flag, remove all warning flags and see to it that as many as possible become obsolete.
36
- # Note: Adding -pedantic could raise <ruby.h> warnings, and we are not in control of that code.
60
+ # Adding -pedantic could raise <ruby.h> warnings, and we are not in control of that code.
37
61
  # e.g. error: '_Bool' is a C99 extension [-Werror,-Wc99-extensions] ; empty macros and etc.
38
62
  #
39
63
  # -Wno-int-conversion => Passing VALUEs as function args but required as unsigned long parameters.
@@ -55,8 +79,19 @@ WARNING_FLAGS = %w[
55
79
  # Flags that are only recognized by gcc:
56
80
  GCC_FLAGS = %w[-Wno-maybe-uninitialized].freeze # rubocop:disable Security/Object/Freeze
57
81
 
82
+ def darwin?
83
+ RbConfig::CONFIG['target_os'].include?('darwin')
84
+ end
85
+
58
86
  # Extend $CFLAGS passed directly to compiler in ruby mkmf
87
+ #
88
+ # Extended flags are mainly tested with clang and gcc. Experience with other compilers may vary.
89
+ # To that end if something brakes on client side we must have a mechanism to go back to previous
90
+ # non strict gnu89 standard and be able to maintain the build.
91
+ # We can disable newly added changes with this setting CONTRAST_USE_C89=false.
59
92
  def extend_cflags
93
+ return if ENV['CONTRAST__USE_GNU89'] == 'false'
94
+
60
95
  $CFLAGS += " #{ [STANDARD_FLAGS, WARNING_FLAGS].flatten.join(' ') }"
61
96
  # Extend with GCC specific flags:
62
97
  unless RbConfig::MAKEFILE_CONFIG['CC'].downcase.include?(CLANG) ||
@@ -67,6 +102,52 @@ def extend_cflags
67
102
  end
68
103
  end
69
104
 
105
+ # use C compiler if set.
106
+ def enable_env_cc
107
+ RbConfig::CONFIG['CC'] = RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC']
108
+ end
109
+
110
+ # Since we cannot link directly the bundles created after the extensions being build,
111
+ # we can pass flags to the linker to resolve symbols not being found during compilation,
112
+ # since they are going to be available on load time. Ruby first is loaded then the extensions.
113
+ # MacOS introduced new fixups with Xcode 14. This causes the issue of symbols not being linked during
114
+ # compilation for the Agent, and other ruby related issues with objective C objects being used,
115
+ # bigdecimal being different and so on.
116
+ #
117
+ # Ruby itself is build on MacOS with '--enabled-shared' flag, also Ruby interpreters compiled
118
+ # under Xcode14 no longer specify by default in DLDFLAGS this flag:
119
+ #
120
+ # '-undefined dynamic_lookup'
121
+ # ( As of Xcode14.3 behavior of dynamic_lookup is reverted back.)
122
+ #
123
+ # This simply is telling the linker that any unknown symbols will be resolved dynamic on extension load.
124
+ # However with the new fixup chain introduced an warning may be produced:
125
+ # ld: warning: -undefined dynamic_lookup may not work with chained fixups.
126
+ # The fixups are responsible for the dynamic linking of the dylibs.
127
+ def mac_symbol_resolve
128
+ # If this is braking the build, it can be disabled.
129
+ return if ENV['CONTRAST__NO_MAC_LD_FIX'] == 'true'
130
+ return unless darwin?
131
+
132
+ # Disabled as of the unknown changes to newly ruby interpreter builds:
133
+ # RbConfig::CONFIG['EXTDLDFLAGS'] = "-bundle_loader #{ RbConfig::CONFIG['EXTDLDFLAGS'] }"
134
+
135
+ # Avoid using this, as not desirable:
136
+ #
137
+ # Adding -no_fixup_chains will brake older compilers but will work on newer.
138
+ # This flag solves the problem but on the long run might not be the solution needed, now not being default,
139
+ # any new feature changes on Mac or Ruby side might brake things again.
140
+ # Use this only if explicitly set as ENV var,as a backup on client's end.
141
+ #
142
+ # $LDFLAGS << " " << flag
143
+ append_ldflags('-Wl,-no_fixup_chains -undefined dynamic_lookup') if ENV['CONTRAST__MAC_LD_USE_ALT'] == 'true'
144
+
145
+ # Another alternative is to use -Wl,-U with explicit listed depending symbols.
146
+ # Append the symfile:
147
+ SYMS.each { |sym| $DLDFLAGS << " -Wl,-U,#{ sym }" } unless ENV['CONTRAST__MAC_LD_USE_ALT'] == 'true'
148
+ end
149
+
150
+ # Generate Makefile.
70
151
  def make!
71
152
  create_makefile("#{ $TO_MAKE }/#{ $TO_MAKE }")
72
153
  end
@@ -75,9 +156,9 @@ end
75
156
  # | MOVING CODE BELLOW THIS SECTION MAY BRAKE MAKEFILE. ORDER MATTERS! |
76
157
  # ----------------------------------------------------------------------
77
158
 
159
+ # __dir__ is relative to the file you're reading.
160
+ # this file you're reading is presently within $APP_ROOT/ext/.
78
161
  def ext_path
79
- # __dir__ is relative to the file you're reading.
80
- # this file you're reading is presently within $APP_ROOT/ext/.
81
162
  __dir__
82
163
  end
83
164
 
@@ -85,14 +166,7 @@ end
85
166
  # funchook.h file. Then we can pass CFLAGS and extend makefile flags and invoke make!
86
167
  require_relative './build_funchook'
87
168
 
88
- # Extended flags are mainly tested with clang and gcc. Experience with other compilers may vary.
89
- # To that end if something brakes on client side we must have a mechanism to go back to previous
90
- # non strict gnu89 standard and be able to maintain the build.
91
- # We can disable newly added changes with this setting CONTRAST_USE_C89=false.
92
- extend_cflags unless ENV['CONTRAST__USE_GNU89'] == 'false'
93
-
94
- # use same C compiler if set.
95
- RbConfig::CONFIG['CC'] = RbConfig::MAKEFILE_CONFIG['CC'] = ENV['CC'] if ENV['CC']
96
-
97
- # Generate Makefile.
169
+ enable_env_cc
170
+ mac_symbol_resolve
171
+ extend_cflags
98
172
  make!
@@ -43,6 +43,7 @@ module Contrast
43
43
  return unless analyze?(method_policy, object, ret, args)
44
44
  return if event_limit?(method_policy)
45
45
  return unless (source_node = method_policy.source_node)
46
+ # Exclusions makes method slow:
46
47
  return if excluded_by_url?
47
48
 
48
49
  # used to hold the object and ret
@@ -76,6 +77,7 @@ module Contrast
76
77
 
77
78
  source_name ||= determine_source_name(source_node, source_data.object, source_data.ret, *args)
78
79
 
80
+ # Exclusions makes method slow:
79
81
  return if excluded_by_input?(source_type, source_name)
80
82
 
81
83
  # We know we only work on certain things.
@@ -188,16 +190,23 @@ module Contrast
188
190
  #
189
191
  # @return [Boolean]
190
192
  def excluded_by_input? source_type, source_name
191
- context = Contrast::Agent::REQUEST_TRACKER.current
192
- Contrast::SETTINGS.excluder.assess_excluded_by_input?(context.request, source_type, source_name)
193
+ return false unless Contrast::SETTINGS.excluder.exclusions.any?
194
+ return false unless Contrast::Agent::REQUEST_TRACKER.current
195
+ # skip if the source is collection, it will be evaluated by it's elements. Collections are not
196
+ # trackable.
197
+ return false if source_name.cs__is_a?(Hash)
198
+
199
+ Contrast::SETTINGS.excluder.assess_excluded_by_input?(source_type, source_name)
193
200
  end
194
201
 
195
202
  # Should a source be excluded because it matches one of the url exclusion rules?
196
203
  #
197
204
  # @return [Boolean]
198
205
  def excluded_by_url?
199
- context = Contrast::Agent::REQUEST_TRACKER.current
200
- Contrast::SETTINGS.excluder.assess_excluded_by_url?(context.request)
206
+ return false unless Contrast::SETTINGS.excluder.exclusions.any?
207
+ return false unless Contrast::Agent::REQUEST_TRACKER.current
208
+
209
+ Contrast::SETTINGS.excluder.assess_excluded_by_url?
201
210
  end
202
211
  end
203
212
  end
@@ -93,11 +93,11 @@ module Contrast
93
93
 
94
94
  request = find_request(source)
95
95
  return unless reportable?(request&.env)
96
- return if excluded_by_url_and_rule?(request, trigger_node.rule_id)
96
+ return if excluded_by_url_and_rule?(trigger_node.rule_id)
97
97
 
98
98
  finding = Contrast::Agent::Reporting::Finding.new(trigger_node.rule_id)
99
99
  finding.attach_data(trigger_node, source, object, ret, request, *args)
100
- return if excluded_by_input_and_rule?(request, finding, trigger_node.rule_id)
100
+ return if excluded_by_input_and_rule?(finding, trigger_node.rule_id)
101
101
 
102
102
  finding.hash_code = Contrast::Utils::HashDigest.generate_event_hash(finding, source, request)
103
103
  check_for_stored_xss(finding)
@@ -167,28 +167,22 @@ module Contrast
167
167
  end
168
168
  end
169
169
 
170
- def build_events finding, event
171
- return unless event
172
-
173
- event.parent_events&.each do |parent_event|
174
- build_events(finding, parent_event)
175
- end
176
- # events could technically be nil, but we would have failed the rule check before getting here. not
177
- # worth the nil check
178
- finding.events << event.to_dtm_event
179
- end
180
-
181
170
  # Check if the finding should be excluded due to the assess exclusion rules.
182
171
  #
183
- # @param request [Contrast::Agent::Request] a wrapper around the Rack::Request for the current request
184
172
  # @param rule_id [String]
185
173
  # return [Boolean]
186
- def excluded_by_url_and_rule? request, rule_id
187
- Contrast::SETTINGS.excluder.assess_excluded_by_url_and_rule?(request, rule_id)
174
+ def excluded_by_url_and_rule? rule_id
175
+ return false unless Contrast::SETTINGS.excluder.exclusions.any?
176
+ return unless Contrast::Agent::REQUEST_TRACKER.current
177
+
178
+ Contrast::SETTINGS.excluder.assess_excluded_by_url_and_rule?(rule_id)
188
179
  end
189
180
 
190
- def excluded_by_input_and_rule? request, finding, rule_id
191
- Contrast::SETTINGS.excluder.assess_excluded_by_input_and_rule?(request, finding, rule_id)
181
+ def excluded_by_input_and_rule? finding, rule_id
182
+ return false unless Contrast::SETTINGS.excluder.exclusions.any?
183
+ return unless Contrast::Agent::REQUEST_TRACKER.current
184
+
185
+ Contrast::SETTINGS.excluder.assess_excluded_by_input_and_rule?(finding, rule_id)
192
186
  end
193
187
 
194
188
  # Handles the Stored Xss rule. If a vector is stored in the database
@@ -3,6 +3,8 @@
3
3
 
4
4
  require 'contrast/agent/reporting/settings/url_exclusion'
5
5
  require 'contrast/agent/reporting/input_analysis/input_type'
6
+ require 'contrast/utils/object_share'
7
+ require 'contrast/utils/assess/object_store'
6
8
 
7
9
  module Contrast
8
10
  module Agent
@@ -19,11 +21,14 @@ module Contrast
19
21
  @exclusions = exclusions
20
22
  end
21
23
 
24
+ def cached_paths
25
+ @_cached_paths ||= Contrast::Utils::Assess::ObjectStore.new(10)
26
+ end
27
+
22
28
  # Determine if an input is excluded for protect rule.
23
29
  #
24
30
  # @param results [Array<Contrast::Agent::Reporting::InputAnalysisResult>]
25
- # @param request_path [String] Current request path
26
- def protect_excluded_by_input? results, request_path
31
+ def protect_excluded_by_input? results
27
32
  return false unless results.any?
28
33
 
29
34
  exclusion_matched = 0
@@ -35,8 +40,7 @@ module Contrast
35
40
 
36
41
  # Based on strategy:
37
42
  match = input_match_strategy(exclusion_match,
38
- input_match?(exclusion_match, rule_result.input_type, rule_result.key),
39
- request_path)
43
+ input_match?(exclusion_match, rule_result.input_type, rule_result.key))
40
44
  exclusion_matched += 1 if match
41
45
  end
42
46
  end
@@ -48,25 +52,21 @@ module Contrast
48
52
  # If an assess URL exclusion rule applies to the current url, *and* is defined as "All Rules"
49
53
  # then we can avoid any tracking for the request.
50
54
  #
51
- # @param request [Contrast::Agent::Request] a wrapper around the Rack::Request for the current request
52
55
  # @return [Boolean]
53
- def assess_excluded_by_url? request
56
+ def assess_excluded_by_url?
54
57
  assess_url_exclusions_for_all_rules.any? do |exclusion_matcher|
55
- path_match?(exclusion_matcher, request.path)
58
+ path_match?(exclusion_matcher)
56
59
  end
57
60
  end
58
61
 
59
62
  # If an assess URL exclusion rule applies to the current url, *and* also covers the
60
63
  # provided rule_id, then we can avoid tracking this entry.
61
64
  #
62
- # @param request [Contrast::Agent::Request] a wrapper around the Rack::Request for the current request
63
65
  # @param rule_id [String]
64
66
  # return [Boolean]
65
- def assess_excluded_by_url_and_rule? request, rule_id
66
- request_path = request.path
67
-
67
+ def assess_excluded_by_url_and_rule?rule_id
68
68
  assess_url_exclusions.any? do |exclusion_matcher|
69
- path_match?(exclusion_matcher, request_path) &&
69
+ path_match?(exclusion_matcher) &&
70
70
  (exclusion_matcher.assess_rules.empty? || exclusion_matcher.assess_rules.include?(rule_id))
71
71
  end
72
72
  end
@@ -74,35 +74,30 @@ module Contrast
74
74
  # If an assess INPUT exclusion rule applies to the current url, *and* also covers all
75
75
  # rules, then we can avoid tracking this entry.
76
76
  #
77
- # @param request [Contrast::Agent::Request] a wrapper around the Rack::Request for the current request
78
77
  # @param source_type [String]
79
78
  # @param source_name [String]
80
79
  # return [Boolean]
81
- def assess_excluded_by_input? request, source_type, source_name
82
- request_path = request.path
83
-
80
+ def assess_excluded_by_input?source_type, source_name
84
81
  assess_input_exclusions_for_all_rules.any? do |exclusion_matcher|
85
- input_match?(exclusion_matcher, source_type, source_name) && path_match?(exclusion_matcher, request_path)
82
+ input_match?(exclusion_matcher, source_type, source_name) && path_match?(exclusion_matcher)
86
83
  end
87
84
  end
88
85
 
89
86
  # If an assess INPUT exclusion rule covers the provided rule_id *for all finding event sources*, then we
90
87
  # can avoid tracking this entry. If any event source *isn't excluded* then we don't exclude the finding.
91
88
  #
92
- # @param request [Contrast::Agent::Request] a wrapper around the Rack::Request for the current request
93
89
  # @param finding [Contrast::Agent::Reporting::Finding]
94
90
  # @param rule [String]
95
91
  # return [Boolean]
96
- def assess_excluded_by_input_and_rule? request, finding, rule
92
+ def assess_excluded_by_input_and_rule?finding, rule
97
93
  return false if finding.events.empty?
98
94
 
99
95
  # We need to check for url exclusions here for the input rules as the url exclusions
100
96
  # that have already been checked didn't include the INPUT exclusions. So we look for
101
97
  # any INPUT exclusions that apply to the current url and the supplied rule.
102
- path = request.path
103
98
  rule_input_exclusions = assess_input_exclusions.select do |exclusion_matcher|
104
99
  (exclusion_matcher.protect_rules.empty? || exclusion_matcher.protect_rules.include?(rule)) &&
105
- path_match?(exclusion_matcher, path)
100
+ path_match?(exclusion_matcher)
106
101
  end
107
102
  return false if rule_input_exclusions.empty?
108
103
 
@@ -122,13 +117,12 @@ module Contrast
122
117
  # then we can avoid using the rule for the request.
123
118
  #
124
119
  # @param rule_id [String]
125
- # @param path [String]
126
120
  # return [Boolean]
127
- def protect_excluded_by_url? rule_id, path
121
+ def protect_excluded_by_url? rule_id
128
122
  protect_url_exclusions.any? do |exclusion_matcher|
129
123
  next unless exclusion_matcher.protection_rule?(rule_id)
130
124
 
131
- return true if path_match?(exclusion_matcher, path)
125
+ return true if path_match?(exclusion_matcher)
132
126
  end
133
127
  end
134
128
 
@@ -140,14 +134,13 @@ module Contrast
140
134
  #
141
135
  # @param exclusion_match [Contrast::Agent::ExclusionMatcher]
142
136
  # @param input_match [Boolean] does the input match the exclusion
143
- # @param request_path [String] Current request path
144
137
  # @return [Boolean]
145
- def input_match_strategy exclusion_match, input_match, request_path
138
+ def input_match_strategy exclusion_match, input_match
146
139
  # for ALL urls
147
140
  return input_match if exclusion_match.match_all?
148
141
 
149
142
  # for ONLY match we need to check if there is an input and url match.
150
- input_match && path_match?(exclusion_match, request_path)
143
+ input_match && path_match?(exclusion_match)
151
144
  end
152
145
 
153
146
  # @return [Array<Contrast::Agent::ExclusionMatcher>]
@@ -202,12 +195,25 @@ module Contrast
202
195
  end
203
196
  end
204
197
 
198
+ # Returns true if context.request.path matches any url exclusion.
199
+ #
205
200
  # @return [Boolean]
206
- def path_match? exclusion_matcher, path
207
- return false unless path
201
+ def path_match? exclusion_matcher
202
+ return false unless Contrast::Agent::REQUEST_TRACKER.current&.request&.path
203
+
204
+ return_cached_result(exclusion_matcher)
205
+ matches = 0
206
+ matches += 1 if exclusion_matcher.wildcard_url
207
+ exclusion_matcher.urls.any? do |url|
208
+ if url.match?(Contrast::Agent::REQUEST_TRACKER.current.request.path) ||
209
+ regexp_match?(url, Contrast::Agent::REQUEST_TRACKER.current.request.path)
210
+
211
+ matches += 1
212
+ end
213
+ end
208
214
 
209
- exclusion_matcher.wildcard_url ||
210
- exclusion_matcher.urls.any? { |url| url.match?(path) || regexp_match?(url, path) }
215
+ add_cached_path(exclusion_matcher, matches)
216
+ matches.positive?
211
217
  end
212
218
 
213
219
  # @param exclusion [Contrast::Agent::ExclusionMatcher]
@@ -301,6 +307,33 @@ module Contrast
301
307
  def cookie_types
302
308
  @_cookie_types ||= [COOKIE_NAME, COOKIE_VALUE].cs__freeze
303
309
  end
310
+
311
+ # Adds new cached result unless it already exists
312
+ #
313
+ # @param exclusion_matcher [Contrast::Agent::ExclusionMatcher]
314
+ # @param matches [Boolean] the result of last iteration
315
+ # @return [Hash, nil]
316
+ def add_cached_path exclusion_matcher, matches
317
+ return if cached_paths[Contrast::Agent::REQUEST_TRACKER.current.request.path.__id__]
318
+
319
+ cached_paths[Contrast::Agent::REQUEST_TRACKER.
320
+ current.request.path.__id__] = { matcher: exclusion_matcher.__id__, result: matches.positive? }
321
+ rescue StandardError
322
+ nil
323
+ end
324
+
325
+ # returns a cached result if current path and matcher are the same.
326
+ # @param exclusion_matcher [Contrast::Agent::ExclusionMatcher]
327
+ # @return [Boolean, nil]
328
+ def return_cached_result exclusion_matcher
329
+ return unless !cached_paths[Contrast::Agent::REQUEST_TRACKER.current.request.path.__id__].nil? &&
330
+ (cached_paths[Contrast::Agent::REQUEST_TRACKER.current.
331
+ request.path.__id__][:matcher] == exclusion_matcher.__id__)
332
+
333
+ cached_paths[Contrast::Agent::REQUEST_TRACKER.current.request.path.__id__][:result]
334
+ rescue StandardError
335
+ nil
336
+ end
304
337
  end
305
338
  end
306
339
  end
@@ -14,10 +14,13 @@ require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload_input
14
14
  require 'contrast/agent/protect/rule/unsafe_file_upload/unsafe_file_upload'
15
15
  require 'contrast/agent/protect/rule/path_traversal/path_traversal'
16
16
  require 'contrast/agent/protect/rule/path_traversal/path_traversal_input_classification'
17
+ require 'contrast/agent/protect/rule/input_classification/lru_cache'
18
+ require 'contrast/agent/protect/rule/input_classification/cached_result'
17
19
  require 'contrast/agent/protect/rule/xss/reflected_xss_input_classification'
18
20
  require 'contrast/agent/protect/rule/xss/xss'
19
21
  require 'contrast/components/logger'
20
22
  require 'contrast/utils/object_share'
23
+ require 'contrast/agent/protect/rule/input_classification/base64_statistic'
21
24
  require 'json'
22
25
 
23
26
  module Contrast
@@ -35,6 +38,8 @@ module Contrast
35
38
  ].cs__freeze
36
39
  POSTFILTER_RULES = %w[sql-injection cmd-injection reflected-xss path-traversal nosql-injection].cs__freeze
37
40
  AGENTLIB_TIMEOUT = 5.cs__freeze
41
+ TIMEOUT_ERROR_MESSAGE = '[AgentLib] Timed out when processing InputAnalysisResult'
42
+ STANDARD_ERROR_MESSAGE = '[InputAnalyzer] Exception raise while doing input analysis:'
38
43
 
39
44
  class << self
40
45
  include Contrast::Agent::Reporting::InputType
@@ -42,6 +47,18 @@ module Contrast
42
47
  include Contrast::Utils::ObjectShare
43
48
  include Contrast::Components::Logger::InstanceMethods
44
49
 
50
+ # Cache for storing the input analysis result per rule
51
+ #
52
+ # @return [Contrast::Agent::Protect::Rule::InputClassification::LRUCache]
53
+ def lru_cache
54
+ @_lru_cache ||= Contrast::Agent::Protect::Rule::InputClassification::LRUCache.new
55
+ end
56
+
57
+ # Input decoding statistic.
58
+ def base64_statistic
59
+ @_base64_statistic ||= Contrast::Agent::Protect::Rule::InputClassification::Base64Statistic.new
60
+ end
61
+
45
62
  # This method with analyze the user input from the context of the
46
63
  # current request and return new ia with extracted input types.
47
64
  #
@@ -51,13 +68,13 @@ module Contrast
51
68
  return unless Contrast::PROTECT.enabled?
52
69
  return if request.nil?
53
70
 
54
- inputs = extract_input(request)
71
+ inputs = extract_inputs(request)
55
72
  return unless inputs
56
73
 
57
74
  input_analysis = Contrast::Agent::Reporting::InputAnalysis.new
58
75
  input_analysis.request = request
59
76
  # Save those for trigger time
60
- input_analysis.inputs = extract_input(request)
77
+ input_analysis.inputs = inputs
61
78
  input_analysis
62
79
  end
63
80
 
@@ -69,16 +86,9 @@ module Contrast
69
86
  #
70
87
  # @param request [Contrast::Agent::Request] current request context.
71
88
  # @return inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
72
- def extract_input request
89
+ def extract_inputs request
73
90
  inputs = {}
74
- inputs[BODY] = request.body
75
- inputs[COOKIE_NAME] = request.cookies.keys
76
- inputs[COOKIE_VALUE] = request.cookies.values
77
- inputs[HEADER] = request.headers
78
- inputs[PARAMETER_NAME] = request.parameters.keys
79
- inputs[PARAMETER_VALUE] = request.parameters.values
80
- inputs[QUERYSTRING] = request.query_string
81
- inputs[METHOD] = request.request_method
91
+ extract_request_inputs(inputs, request)
82
92
  extract_multipart(inputs, request)
83
93
  inputs.compact!
84
94
  inputs
@@ -86,22 +96,29 @@ module Contrast
86
96
 
87
97
  # classify input by rule
88
98
  #
89
- # @param rule_id [String] name of the rule
90
- # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from
91
- # analyze method.
92
- def input_classification_for rule_id, input_analysis
99
+ # @param rule_id [String] name of the rule.
100
+ # @param input_analysis [Contrast::Agent::Reporting::InputAnalysis] from analyze method.
101
+ # @param interval [Integer] The timeout determined for the AgentLib analysis to be performed.
102
+ def input_classification_for rule_id, input_analysis, interval: AGENTLIB_TIMEOUT
93
103
  return unless input_analysis&.inputs
94
104
  return unless (protect_rule = Contrast::PROTECT.rule(rule_id)) && protect_rule.enabled?
95
105
 
96
106
  input_analysis.inputs.each do |input_type, value|
97
107
  next if value.nil? || value.empty?
98
108
 
99
- protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
109
+ Timeout.timeout(interval) do
110
+ protect_rule.classification.classify(rule_id, input_type, value, input_analysis)
111
+ end
100
112
  end
101
113
 
102
114
  input_analysis
103
115
  rescue StandardError => e
104
- logger.error('[INPUT_ANALYZER] Error', error: e)
116
+ if e.cs__class == Timeout::Error
117
+ log_error(rule_id, TIMEOUT_ERROR_MESSAGE, e)
118
+ else
119
+ log_error(rule_id, STANDARD_ERROR_MESSAGE, e, level: :error)
120
+ end
121
+ nil
105
122
  end
106
123
 
107
124
  # classify input by array of rules. There is a timeout for the AgentLib analysis if not set it
@@ -134,14 +151,9 @@ module Contrast
134
151
  # Check to see if rules is already triggered only for infilter:
135
152
  next if input_analysis.triggered_rules.include?(rule_id) && infilter
136
153
 
137
- Timeout.timeout(interval) do
138
- input_classification_for(rule_id, input_analysis)
139
- end
154
+ input_classification_for(rule_id, input_analysis, interval: interval)
140
155
  end
141
156
  input_analysis
142
- rescue Timeout::Error => e
143
- logger.warn('AgentLib timed out when processing InputAnalysisResult', e, ia_result)
144
- nil
145
157
  end
146
158
 
147
159
  private
@@ -158,6 +170,33 @@ module Contrast
158
170
  name = filename[DISPOSITION_NAME.to_sym]
159
171
  inputs[MULTIPART_NAME] = name if name
160
172
  end
173
+
174
+ # Extract the parameters and query string from the request context.
175
+ #
176
+ # @param inputs [Hash<Contrast::Agent::Protect::InputType => user_inputs>]
177
+ # @param request [Contrast::Agent::Request] current request context.
178
+ def extract_request_inputs inputs, request
179
+ inputs[BODY] = request.body
180
+ inputs[COOKIE_NAME] = request.cookies.keys
181
+ inputs[COOKIE_VALUE] = request.cookies.values
182
+ inputs[HEADER] = request.headers
183
+ inputs[METHOD] = request.request_method
184
+ inputs[PARAMETER_NAME] = request.parameters.keys
185
+ inputs[PARAMETER_VALUE] = request.parameters.values
186
+ inputs[QUERYSTRING] = request.query_string
187
+ end
188
+
189
+ # Logs any errrors that occur during the analysis
190
+ # Accepts a level parameter to determine if the error should be logged as an error or warning.
191
+ #
192
+ # @param rule_id [String] name of the rule.
193
+ def log_error rule_id, message, error, level: :error
194
+ if level == :error
195
+ logger.error(message, rule_id: rule_id, error: error)
196
+ else
197
+ logger.warn(message, rule_id: rule_id, error: error)
198
+ end
199
+ end
161
200
  end
162
201
  end
163
202
  end