contrast-agent 4.9.1 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/.rspec_parallel +6 -0
  4. data/ext/cs__contrast_patch/cs__contrast_patch.c +0 -1
  5. data/ext/cs__contrast_patch/cs__contrast_patch.h +0 -2
  6. data/lib/contrast/agent/assess/contrast_event.rb +0 -1
  7. data/lib/contrast/agent/assess/finalizers/hash.rb +0 -1
  8. data/lib/contrast/agent/assess/policy/patcher.rb +0 -1
  9. data/lib/contrast/agent/assess/policy/policy_scanner.rb +0 -2
  10. data/lib/contrast/agent/assess/policy/preshift.rb +8 -5
  11. data/lib/contrast/agent/assess/policy/propagation_method.rb +100 -57
  12. data/lib/contrast/agent/assess/policy/propagator/database_write.rb +0 -2
  13. data/lib/contrast/agent/assess/policy/propagator/match_data.rb +31 -11
  14. data/lib/contrast/agent/assess/policy/propagator/split.rb +3 -2
  15. data/lib/contrast/agent/assess/policy/propagator/substitution.rb +1 -0
  16. data/lib/contrast/agent/assess/policy/rewriter_patch.rb +0 -1
  17. data/lib/contrast/agent/assess/policy/source_method.rb +13 -17
  18. data/lib/contrast/agent/assess/policy/trigger/xpath.rb +0 -1
  19. data/lib/contrast/agent/assess/policy/trigger_method.rb +59 -83
  20. data/lib/contrast/agent/assess/property/evented.rb +2 -1
  21. data/lib/contrast/agent/assess/rule/provider/hardcoded_value_rule.rb +0 -1
  22. data/lib/contrast/agent/disable_reaction.rb +1 -1
  23. data/lib/contrast/agent/exclusion_matcher.rb +0 -4
  24. data/lib/contrast/agent/inventory/database_config.rb +117 -0
  25. data/lib/contrast/agent/inventory/dependency_usage_analysis.rb +5 -4
  26. data/lib/contrast/agent/inventory/policy/datastores.rb +2 -2
  27. data/lib/contrast/agent/middleware.rb +1 -0
  28. data/lib/contrast/agent/patching/policy/after_load_patch.rb +3 -0
  29. data/lib/contrast/agent/patching/policy/after_load_patcher.rb +18 -12
  30. data/lib/contrast/agent/patching/policy/module_policy.rb +2 -4
  31. data/lib/contrast/agent/patching/policy/patch.rb +5 -0
  32. data/lib/contrast/agent/patching/policy/patch_status.rb +3 -7
  33. data/lib/contrast/agent/patching/policy/patcher.rb +8 -8
  34. data/lib/contrast/agent/protect/policy/applies_no_sqli_rule.rb +1 -1
  35. data/lib/contrast/agent/protect/rule/no_sqli.rb +7 -53
  36. data/lib/contrast/agent/protect/rule/sql_sample_builder.rb +137 -0
  37. data/lib/contrast/agent/protect/rule/sqli.rb +7 -70
  38. data/lib/contrast/agent/reaction_processor.rb +1 -1
  39. data/lib/contrast/agent/request.rb +5 -2
  40. data/lib/contrast/agent/request_context.rb +19 -22
  41. data/lib/contrast/agent/static_analysis.rb +1 -1
  42. data/lib/contrast/agent/tracepoint_hook.rb +6 -1
  43. data/lib/contrast/agent/version.rb +1 -1
  44. data/lib/contrast/api/communication/messaging_queue.rb +12 -6
  45. data/lib/contrast/api/communication/service_lifecycle.rb +4 -1
  46. data/lib/contrast/api/communication/socket_client.rb +4 -4
  47. data/lib/contrast/api/decorators/agent_startup.rb +4 -4
  48. data/lib/contrast/api/decorators/application_startup.rb +6 -5
  49. data/lib/contrast/api/decorators/route_coverage.rb +24 -1
  50. data/lib/contrast/components/agent.rb +5 -2
  51. data/lib/contrast/components/assess.rb +6 -3
  52. data/lib/contrast/components/base.rb +2 -2
  53. data/lib/contrast/components/config.rb +1 -0
  54. data/lib/contrast/components/contrast_service.rb +4 -2
  55. data/lib/contrast/components/logger.rb +13 -8
  56. data/lib/contrast/components/scope.rb +9 -28
  57. data/lib/contrast/config/base_configuration.rb +14 -6
  58. data/lib/contrast/configuration.rb +19 -15
  59. data/lib/contrast/extension/assess/array.rb +1 -11
  60. data/lib/contrast/extension/assess/eval_trigger.rb +0 -20
  61. data/lib/contrast/extension/assess/fiber.rb +0 -11
  62. data/lib/contrast/extension/assess/hash.rb +0 -10
  63. data/lib/contrast/extension/assess/kernel.rb +1 -10
  64. data/lib/contrast/extension/assess/marshal.rb +3 -11
  65. data/lib/contrast/extension/assess/regexp.rb +0 -11
  66. data/lib/contrast/extension/assess/string.rb +1 -26
  67. data/lib/contrast/extension/extension.rb +61 -0
  68. data/lib/contrast/extension/protect/kernel.rb +0 -10
  69. data/lib/contrast/framework/grape/support.rb +174 -0
  70. data/lib/contrast/framework/manager.rb +42 -6
  71. data/lib/contrast/framework/rack/support.rb +1 -1
  72. data/lib/contrast/framework/rails/patch/assess_configuration.rb +0 -1
  73. data/lib/contrast/framework/rails/patch/support.rb +6 -3
  74. data/lib/contrast/framework/rails/railtie.rb +1 -1
  75. data/lib/contrast/framework/rails/rewrite/active_record_named.rb +1 -0
  76. data/lib/contrast/framework/rails/support.rb +60 -13
  77. data/lib/contrast/framework/sinatra/support.rb +1 -1
  78. data/lib/contrast/logger/log.rb +89 -15
  79. data/lib/contrast/utils/io_util.rb +1 -1
  80. data/lib/contrast/utils/ruby_ast_rewriter.rb +16 -13
  81. data/lib/contrast/utils/tag_util.rb +2 -1
  82. data/resources/assess/policy.json +197 -2
  83. data/resources/deadzone/policy.json +10 -0
  84. data/ruby-agent.gemspec +10 -1
  85. metadata +78 -12
  86. data/lib/contrast/utils/inventory_util.rb +0 -113
@@ -76,11 +76,12 @@ module Contrast
76
76
  return unless enabled?
77
77
  return unless activity
78
78
 
79
- # Copy gemdigest_cache and clear it in sync.
79
+ # Disconnect gemdigest_cache and replace it with an empty one; synch so new libs cannot be added between the
80
+ # assignment and the replace
80
81
  gem_spec_digest_to_files = @lock.synchronize do
81
- copy = @gemdigest_cache.dup
82
- @gemdigest_cache.clear
83
- copy
82
+ hold = @gemdigest_cache
83
+ @gemdigest_cache = Hash.new { |hash, key| hash[key] = Set.new }
84
+ hold
84
85
  end
85
86
 
86
87
  gem_spec_digest_to_files.each_pair do |digest, files|
@@ -2,7 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/components/logger'
5
- require 'contrast/utils/inventory_util'
5
+ require 'contrast/agent/inventory/database_config'
6
6
 
7
7
  module Contrast
8
8
  module Agent
@@ -42,7 +42,7 @@ module Contrast
42
42
  context.activity.query_count += 1
43
43
  return unless context.activity.query_count == 1
44
44
 
45
- Contrast::Utils::InventoryUtil.append_db_config(context.activity)
45
+ Contrast::Agent::Inventory::DatabaseConfig.append_db_config(context.activity)
46
46
  end
47
47
  end
48
48
  end
@@ -60,6 +60,7 @@ module Contrast
60
60
  handle_first_request
61
61
  call_with_agent(env)
62
62
  end
63
+ ::Contrast::Components::Logger.add_trace_log_timing_for(::Contrast::Agent::Middleware, :call)
63
64
 
64
65
  private
65
66
 
@@ -59,7 +59,10 @@ module Contrast
59
59
  end
60
60
 
61
61
  def instrument!
62
+ return if instrumenting_module == :'Contrast::Framework::Rails::Rewrite' && RAILS_VER >= '2.6.0'
63
+
62
64
  require instrumentation_file_path
65
+
63
66
  if instrumenting_module
64
67
  mod = Module.cs__const_get(instrumenting_module)
65
68
  with_contrast_scope { mod.instrument } if mod
@@ -4,6 +4,7 @@
4
4
  require 'contrast/agent/patching/policy/after_load_patch'
5
5
  require 'contrast/components/logger'
6
6
  require 'contrast/framework/manager'
7
+ require 'contrast/extension/extension'
7
8
 
8
9
  module Contrast
9
10
  module Agent
@@ -29,18 +30,23 @@ module Contrast
29
30
  # extensions.
30
31
  def apply_direct_patches!
31
32
  @_apply_direct_patches ||= begin
32
- Contrast::Extension::Assess::ArrayPropagator.instrument_array_track
33
- Contrast::Extension::Assess::EvalTrigger.instrument_basic_object_track
34
- Contrast::Extension::Assess::EvalTrigger.instrument_module_track
35
- Contrast::Extension::Assess::FiberPropagator.instrument_fiber_track
36
- Contrast::Extension::Assess::HashPropagator.instrument_hash_track
37
- Contrast::Extension::Assess::KernelPropagator.instrument_kernel_track
38
- Contrast::Extension::Assess::MarshalPropagator.instrument_marshal_load
39
- Contrast::Extension::Assess::RegexpPropagator.instrument_regexp_track
40
- Contrast::Extension::Assess::StringPropagator.instrument_string
41
- Contrast::Extension::Assess::StringPropagator.instrument_string_interpolation
42
-
43
- Contrast::Extension::Protect::Kernel.instrument
33
+ paths = %w[
34
+ array
35
+ basic_object
36
+ module
37
+ fiber_track
38
+ hash
39
+ kernel
40
+ marshal_module
41
+ regexp
42
+ string
43
+ string_interpolation26
44
+ ].cs__freeze
45
+ paths.each do |p|
46
+ path_part = "cs__assess_#{ p }"
47
+ Contrast::Extension::Assess::InstrumentHelper.instrument "#{ path_part }/#{ path_part }"
48
+ end
49
+ Contrast::Extension::Assess::InstrumentHelper.instrument 'cs__protect_kernel/cs__protect_kernel'
44
50
  true
45
51
  end
46
52
  end
@@ -12,11 +12,9 @@ module Contrast
12
12
  # rather than new.
13
13
  class ModulePolicy
14
14
  class << self
15
- # Given the name of a module, create a :ModulePolicy for it using
16
- # the Policy of each supported feature
15
+ # Given the name of a module, create a :ModulePolicy for it using the Policy of each supported feature.
17
16
  #
18
- # @param module_name [String] the name of the module to which the
19
- # policy applies
17
+ # @param module_name [String] the name of the module to which the policy applies.
20
18
  # @return [Contrast::Agent::Patching::Policy::ModulePolicy]
21
19
  def create_module_policy module_name
22
20
  module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.new
@@ -290,6 +290,11 @@ module Contrast
290
290
  # we've already patched this class, don't do it again
291
291
  return true if methods.include?(cs_method_name)
292
292
 
293
+ # that method is within Contrast definition so it should be skipped
294
+ method = mod.instance_method(method_policy.method_name) if method_policy.instance_method
295
+ method = mod.singleton_method(method_policy.method_name) unless method_policy.instance_method
296
+ return true if method.owner <= Contrast
297
+
293
298
  begin
294
299
  contrast_define_method(mod, method_policy, cs_method_name)
295
300
  rescue NameError => e
@@ -15,13 +15,9 @@ module Contrast
15
15
  # @param mod [Module] the Module for which the status is asked
16
16
  # @return [Contrast::Agent::Patching::Policy::PatchStatus]
17
17
  def get_status mod
18
- if mod.cs__const_defined?(status_key, false)
19
- mod.cs__const_get(status_key, false)
20
- else
21
- s = new
22
- mod.cs__const_set(status_key, s)
23
- s
24
- end
18
+ return mod.cs__const_get(status_key) if mod.cs__const_defined?(status_key, false)
19
+
20
+ mod.cs__const_set(status_key, new)
25
21
  end
26
22
 
27
23
  # Allows our C patches to look up the :MethodPolicy for a given
@@ -54,7 +54,8 @@ module Contrast
54
54
  def patch
55
55
  catchup_after_load_patches
56
56
  catchup_loaded_methods
57
- Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolations if RUBY_VERSION < '2.6.0' # TODO: RUBY-714 remove guard w/ EOL of 2.5
57
+ # TODO: RUBY-714 remove guard w/ EOL of 2.5
58
+ Contrast::Agent::Assess::Policy::RewriterPatch.rewrite_interpolations if RUBY_VERSION < '2.6.0'
58
59
  end
59
60
 
60
61
  # Hook to only monkeypatch Contrast. This will not trigger any
@@ -86,7 +87,7 @@ module Contrast
86
87
 
87
88
  load_patches_for_module(mod_name)
88
89
 
89
- return unless all_module_names.any?(mod_name)
90
+ return if all_module_names.none?(mod_name)
90
91
 
91
92
  module_data = Contrast::Agent::ModuleData.new(mod, mod_name)
92
93
  patch_into_module(module_data)
@@ -176,12 +177,13 @@ module Contrast
176
177
  # @param redo_patch [Boolean] a trigger to force patching regardless of the state of the
177
178
  # Contrast::Agent::Patching::Policy::PatchStatus status on the Module
178
179
  def patch_into_module module_data, redo_patch = false
179
- status = status_type.get_status(module_data.mod)
180
+ status = Contrast::Agent::Patching::Policy::PatchStatus.get_status(module_data.mod)
180
181
  return if (status&.patched? || status&.patching?) && !redo_patch
181
182
 
182
183
  # Begin patching our sources into the given module. Any patcher that has the name of the module will be
183
184
  # evaluated for patching. Find all the patchers that apply to this class, sorted by type.
184
185
  module_policy = Contrast::Agent::Patching::Policy::ModulePolicy.create_module_policy(module_data.mod_name)
186
+
185
187
  # If there's nothing to match, then set that status and exit
186
188
  if module_policy.empty?
187
189
  status.no_patch!
@@ -191,8 +193,8 @@ module Contrast
191
193
  status.patching!
192
194
  num_applied_patches = patch_into_instance_methods(module_data, module_policy)
193
195
  num_applied_patches += patch_into_singleton_methods(module_data, module_policy)
194
- if adjust_for_prepend(module_data) || module_policy.num_expected_patches == num_applied_patches
195
196
 
197
+ if adjust_for_prepend(module_data) || module_policy.num_expected_patches == num_applied_patches
196
198
  status.patched!
197
199
  else
198
200
  status.partial_patch!
@@ -258,8 +260,7 @@ module Contrast
258
260
  patch_into_methods(mod, methods, module_policy, false)
259
261
  end
260
262
 
261
- # We've found the patchers that apply to this class (or module). Now we'll
262
- # filter on the given method.
263
+ # We've found the patchers that apply to this class (or module). Now we'll filter on the given method.
263
264
  #
264
265
  # @param mod [Module] The module from which to retrieve instance methods.
265
266
  # @param methods [Array<Symbol>] The names of all the methods in in this module
@@ -276,8 +277,7 @@ module Contrast
276
277
  is_instance_method)
277
278
  next if method_policy.empty?
278
279
 
279
- patched = patch_method(mod, methods, method_policy)
280
- count += 1 if patched
280
+ count += 1 if patch_method(mod, methods, method_policy)
281
281
  end
282
282
  count
283
283
  end
@@ -14,7 +14,7 @@ module Contrast
14
14
  # infilter methods of the rule should be invoked.
15
15
  module AppliesNoSqliRule
16
16
  extend Contrast::Agent::Protect::Policy::RuleApplicator
17
-
17
+ DATABASE_NOSQL = 'MongoDB'
18
18
  class << self
19
19
  def invoke method, _exception, properties, _object, args
20
20
  return unless valid_input?(args)
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'contrast/agent/protect/rule/base_service'
5
+ require 'contrast/agent/protect/rule/sql_sample_builder'
5
6
 
6
7
  module Contrast
7
8
  module Agent
@@ -9,6 +10,12 @@ module Contrast
9
10
  module Rule
10
11
  # The Ruby implementation of the Protect NoSQL Injection rule.
11
12
  class NoSqli < Contrast::Agent::Protect::Rule::BaseService
13
+ # Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
14
+ # by TeamServer
15
+ include SqlSampleBuilder::NoSqliSample
16
+ # Defining build_attack_with_match method
17
+ include SqlSampleBuilder::AttackBuilder
18
+
12
19
  NAME = 'nosql-injection'
13
20
  BLOCK_MESSAGE = 'NoSQLi rule triggered. Response blocked.'
14
21
 
@@ -31,40 +38,6 @@ module Contrast
31
38
  raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
32
39
  end
33
40
 
34
- def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
35
- if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
36
- mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
37
-
38
- return result
39
- end
40
-
41
- attack_string = input_analysis_result.value
42
- regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
43
- return unless query_string.match?(regexp)
44
-
45
- scanner = select_scanner
46
- ss = StringScanner.new(query_string)
47
- length = attack_string.length
48
- while ss.scan_until(regexp)
49
- # the pos of StringScanner is at the end of the regexp (input string),
50
- # we need the beginning
51
- idx = ss.pos - attack_string.length
52
- last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
53
- next unless last_boundary && boundary
54
-
55
- kwargs[:start_idx] = idx
56
- kwargs[:end_idx] = idx + length
57
- kwargs[:boundary_overrun_idx] = boundary
58
- kwargs[:input_boundary_idx] = last_boundary
59
-
60
- result ||= build_attack_result(context)
61
- update_successful_attack_response(context, input_analysis_result, result, query_string)
62
- append_sample(context, input_analysis_result, result, query_string, **kwargs)
63
- end
64
-
65
- result
66
- end
67
-
68
41
  protected
69
42
 
70
43
  def find_attacker context, potential_attack_string, **kwargs
@@ -79,25 +52,6 @@ module Contrast
79
52
  end
80
53
  super(context, potential_attack_string, **kwargs)
81
54
  end
82
-
83
- def build_sample context, input_analysis_result, candidate_string, **kwargs
84
- input = input_analysis_result.value
85
-
86
- sample = build_base_sample(context, input_analysis_result)
87
- sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
88
- sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
89
- sample.no_sqli.start_idx = sample.no_sqli.query.index(input).to_i
90
- sample.no_sqli.boundary_overrun_idx = sample.no_sqli.start_idx + input.length
91
- sample.no_sqli.input_boundary_idx = kwargs[:boundary].to_i
92
- sample.no_sqli.input_boundary_idx = kwargs[:last_boundary].to_i
93
- sample
94
- end
95
-
96
- private
97
-
98
- def select_scanner
99
- @_select_scanner ||= Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
100
- end
101
55
  end
102
56
  end
103
57
  end
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
2
+ # frozen_string_literal: true
3
+
4
+ require 'contrast/agent/protect/rule/base'
5
+ require 'contrast/agent/protect/rule/base_service'
6
+
7
+ module Contrast
8
+ module Agent
9
+ module Protect
10
+ module Rule
11
+ module SqlSampleBuilder
12
+ # Generate a sample for the SQL injection detection rule, allowing for reporting to and rendering
13
+ # by TeamServer
14
+ #
15
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
16
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
17
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
18
+ # @candidate_string [String] the value of the input which may be an attack
19
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details
20
+ # to send to the Service to tell the story of the attack
21
+ # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
22
+ module SqliSample
23
+ def build_sample context, input_analysis_result, candidate_string, **kwargs
24
+ sqli_sample = build_base_sample(context, input_analysis_result)
25
+ sqli_sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
26
+ sqli_sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
27
+ sqli_sample.sqli.start_idx = kwargs[:start_idx]
28
+ sqli_sample.sqli.end_idx = kwargs[:end_idx]
29
+ sqli_sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
30
+ sqli_sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
31
+ sqli_sample
32
+ end
33
+ end
34
+
35
+ # Generate a sample for the No-SQL injection detection rule, allowing for reporting to and rendering
36
+ # by TeamServer
37
+ #
38
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
39
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
40
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
41
+ # @candidate_string [String] the value of the input which may be an attack
42
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details
43
+ # to send to the Service to tell the story of the attack
44
+ # @return [Contrast::Api::Dtm::RaspRuleSample] the sample from this attack
45
+ module NoSqliSample
46
+ def build_sample context, input_analysis_result, candidate_string, **kwargs
47
+ no_sqli_sample = build_base_sample(context, input_analysis_result)
48
+ no_sqli_sample.no_sqli = Contrast::Api::Dtm::NoSqlInjectionDetails.new
49
+ no_sqli_sample.no_sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
50
+ no_sqli_sample.no_sqli.start_idx = kwargs[:start_idx].to_i
51
+ no_sqli_sample.no_sqli.end_idx = kwargs[:end_idx].to_i
52
+ no_sqli_sample.no_sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
53
+ no_sqli_sample.no_sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
54
+ no_sqli_sample
55
+ end
56
+ end
57
+
58
+ # This Module is how we apply the attack fo NoSQL and SQL Injection rule.
59
+ # It includes methods for building attack with match and database scanners
60
+ module AttackBuilder
61
+ # Set up an attack result and assigns Database scanner for the No-SQL and SQLI injection detection rules
62
+ #
63
+ # @param context [Contrast::Agent::RequestContext] the context for the current request
64
+ # @param input_analysis_result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule,
65
+ # if one exists, in the case of multiple inputs being found to violate the protection criteria
66
+ # @param result [Contrast::Api::Dtm::AttackResult, nil] previous attack result for this rule, if one exists,
67
+ # in the case of multiple inputs being found to violate the protection criteria
68
+ # @query_string [string] he value of the input which may be an attack
69
+ # @kwargs [Hash] key - value pairs of context individual rules need to build out details to send
70
+ # to the Service to tell the story of the attack
71
+ # @return [Contrast::Api::Dtm::AttackResult] the result from this attack
72
+ def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
73
+ if mode == Contrast::Api::Settings::ProtectionRule::Mode::NO_ACTION ||
74
+ mode == Contrast::Api::Settings::ProtectionRule::Mode::PERMIT
75
+
76
+ return result
77
+ end
78
+
79
+ attack_string = input_analysis_result.value
80
+ regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
81
+
82
+ return unless query_string.match?(regexp)
83
+
84
+ database = kwargs[:database]
85
+ scanner = select_scanner(database)
86
+ ss = StringScanner.new(query_string)
87
+ length = attack_string.length
88
+ while ss.scan_until(regexp)
89
+ # the pos of StringScanner is at the end of the regexp (input string),
90
+ # we need the beginning
91
+ idx = ss.pos - attack_string.length
92
+ last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
93
+ next unless last_boundary && boundary
94
+
95
+ result ||= build_attack_result(context)
96
+
97
+ record_match(idx, length, boundary, last_boundary, kwargs)
98
+ append_match(context, input_analysis_result, result, query_string, **kwargs)
99
+ end
100
+
101
+ result
102
+ end
103
+
104
+ def select_scanner database
105
+ @scanners ||= {
106
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
107
+ Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
108
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
109
+ Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
110
+ Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
111
+ Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new,
112
+ Contrast::Agent::Protect::Policy::AppliesNoSqliRule::DATABASE_NOSQL =>
113
+ Contrast::Agent::Protect::Rule::NoSqli::MongoNoSqlScanner.new
114
+ }.cs__freeze
115
+
116
+ @default_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
117
+ @scanners[database.to_s] || @default_scanner
118
+ end
119
+
120
+ def record_match idx, length, boundary, last_boundary, kwargs
121
+ kwargs[:start_idx] = idx
122
+ kwargs[:end_idx] = idx + length
123
+ kwargs[:boundary_overrun_idx] = boundary
124
+ kwargs[:input_boundary_idx] = last_boundary
125
+ end
126
+
127
+ def append_match context, input_analysis_result, result, query_string, **kwargs
128
+ input_analysis_result.attack_count = input_analysis_result.attack_count + 1
129
+ update_successful_attack_response(context, input_analysis_result, result, query_string)
130
+ append_sample(context, input_analysis_result, result, query_string, **kwargs)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require 'contrast/agent/protect/rule/base_service'
5
5
  require 'contrast/agent/protect/policy/applies_sqli_rule'
6
+ require 'contrast/agent/protect/rule/sql_sample_builder'
6
7
 
7
8
  module Contrast
8
9
  module Agent
@@ -10,6 +11,12 @@ module Contrast
10
11
  module Rule
11
12
  # The Ruby implementation of the Protect SQL Injection rule.
12
13
  class Sqli < Contrast::Agent::Protect::Rule::BaseService
14
+ # Generate a sample for the SQLI injection detection rule, allowing for reporting to and rendering
15
+ # by TeamServer
16
+ include SqlSampleBuilder::SqliSample
17
+ # Defining build_attack_with_match method
18
+ include SqlSampleBuilder::AttackBuilder
19
+
13
20
  NAME = 'sql-injection'
14
21
  BLOCK_MESSAGE = 'SQLi rule triggered. Response blocked.'
15
22
 
@@ -31,76 +38,6 @@ module Contrast
31
38
 
32
39
  raise Contrast::SecurityException.new(self, BLOCK_MESSAGE) if blocked?
33
40
  end
34
-
35
- def build_attack_with_match context, input_analysis_result, result, query_string, **kwargs
36
- attack_string = input_analysis_result.value
37
- regexp = Regexp.new(Regexp.escape(attack_string), Regexp::IGNORECASE)
38
-
39
- return unless query_string.match?(regexp)
40
-
41
- database = kwargs[:database]
42
- scanner = select_scanner(database)
43
-
44
- ss = StringScanner.new(query_string)
45
- length = attack_string.length
46
- while ss.scan_until(regexp)
47
- # the pos of StringScanner is at the end of the regexp (input string),
48
- # we need the beginning
49
- idx = ss.pos - attack_string.length
50
- last_boundary, boundary = scanner.crosses_boundary(query_string, idx, input_analysis_result.value)
51
- next unless last_boundary && boundary
52
-
53
- result ||= build_attack_result(context)
54
- record_match(idx, length, boundary, last_boundary, kwargs)
55
- append_match(context, input_analysis_result, result, query_string, **kwargs)
56
- end
57
-
58
- result
59
- end
60
-
61
- protected
62
-
63
- def build_sample context, input_analysis_result, candidate_string, **kwargs
64
- input = input_analysis_result.value
65
-
66
- sample = build_base_sample(context, input_analysis_result)
67
- sample.sqli = Contrast::Api::Dtm::SqlInjectionDetails.new
68
- sample.sqli.query = Contrast::Utils::StringUtils.protobuf_safe_string(candidate_string)
69
- sample.sqli.start_idx = sample.sqli.query.index(input).to_i
70
- sample.sqli.end_idx = sample.sqli.start_idx + input.length
71
- sample.sqli.boundary_overrun_idx = kwargs[:boundary_overrun_idx].to_i
72
- sample.sqli.input_boundary_idx = kwargs[:input_boundary_idx].to_i
73
- sample
74
- end
75
-
76
- private
77
-
78
- def record_match idx, length, boundary, last_boundary, kwargs
79
- kwargs[:start_idx] = idx
80
- kwargs[:end_idx] = idx + length
81
- kwargs[:boundary_overrun_idx] = boundary
82
- kwargs[:input_boundary_idx] = last_boundary
83
- end
84
-
85
- def append_match context, input_analysis_result, result, query_string, **kwargs
86
- input_analysis_result.attack_count = input_analysis_result.attack_count + 1
87
- update_successful_attack_response(context, input_analysis_result, result, query_string)
88
- append_sample(context, input_analysis_result, result, query_string, **kwargs)
89
- end
90
-
91
- def select_scanner database
92
- @sql_scanners ||= {
93
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_MYSQL =>
94
- Contrast::Agent::Protect::Rule::Sqli::MysqlSqlScanner.new,
95
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_PG =>
96
- Contrast::Agent::Protect::Rule::Sqli::PostgresSqlScanner.new,
97
- Contrast::Agent::Protect::Policy::AppliesSqliRule::DATABASE_SQLITE =>
98
- Contrast::Agent::Protect::Rule::Sqli::SqliteSqlScanner.new
99
- }.cs__freeze
100
-
101
- @default_sql_scanner ||= Contrast::Agent::Protect::Rule::Sqli::DefaultSqlScanner.new
102
- @sql_scanners[database.to_s] || @default_sql_scanner
103
- end
104
41
  end
105
42
  end
106
43
  end