contrast-agent 4.9.1 → 4.10.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.
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