threatstack-agent-ruby 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +6 -0
- data/ext/libinjection/extconf.rb +4 -0
- data/ext/libinjection/libinjection.h +65 -0
- data/ext/libinjection/libinjection.i +13 -0
- data/ext/libinjection/libinjection_html5.c +850 -0
- data/ext/libinjection/libinjection_html5.h +54 -0
- data/ext/libinjection/libinjection_sqli.c +2325 -0
- data/ext/libinjection/libinjection_sqli.h +298 -0
- data/ext/libinjection/libinjection_sqli_data.h +9654 -0
- data/ext/libinjection/libinjection_wrap.c +2393 -0
- data/ext/libinjection/libinjection_xss.c +532 -0
- data/ext/libinjection/libinjection_xss.h +21 -0
- data/lib/constants.rb +110 -0
- data/lib/control.rb +61 -0
- data/lib/events/event_accumulator.rb +36 -0
- data/lib/events/models/attack_event.rb +58 -0
- data/lib/events/models/base_event.rb +41 -0
- data/lib/events/models/dependency_event.rb +93 -0
- data/lib/events/models/environment_event.rb +93 -0
- data/lib/events/models/instrumentation_event.rb +46 -0
- data/lib/exceptions/request_blocked_error.rb +11 -0
- data/lib/instrumentation/common.rb +172 -0
- data/lib/instrumentation/instrumenter.rb +144 -0
- data/lib/instrumentation/kernel.rb +45 -0
- data/lib/instrumentation/rails.rb +61 -0
- data/lib/jobs/delayed_job.rb +26 -0
- data/lib/jobs/event_submitter.rb +101 -0
- data/lib/jobs/job_queue.rb +38 -0
- data/lib/jobs/recurrent_job.rb +61 -0
- data/lib/threatstack-agent-ruby.rb +7 -0
- data/lib/utils/aws_utils.rb +46 -0
- data/lib/utils/formatter.rb +47 -0
- data/lib/utils/logger.rb +43 -0
- data/threatstack-agent-ruby.gemspec +35 -0
- metadata +221 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './base_event'
|
4
|
+
|
5
|
+
module Threatstack
|
6
|
+
module Events
|
7
|
+
|
8
|
+
# Instrumentation event model that inherits the common attributes and adds its own specifics
|
9
|
+
class InstrumentationEvent < BaseEvent
|
10
|
+
attr_accessor :module_name
|
11
|
+
attr_accessor :method_name
|
12
|
+
attr_accessor :file_path
|
13
|
+
attr_accessor :line_num
|
14
|
+
attr_accessor :arguments
|
15
|
+
|
16
|
+
# @param [Hash] args
|
17
|
+
# [String] args.module_name
|
18
|
+
# [String] args.method_name
|
19
|
+
# [String] args.file_path
|
20
|
+
# [Integer] args.line_num
|
21
|
+
# [Array] args.arguments
|
22
|
+
def initialize(args)
|
23
|
+
args[:event_type] = 'instrumentation'
|
24
|
+
@module_name = args[:module_name]
|
25
|
+
@method_name = args[:method_name]
|
26
|
+
@file_path = args[:file_path]
|
27
|
+
@line_num = args[:line_num]
|
28
|
+
@arguments = args[:arguments]
|
29
|
+
super args
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_hash
|
33
|
+
hash = to_core_hash
|
34
|
+
hash[:module_name] = @module_name
|
35
|
+
hash[:method_name] = @method_name
|
36
|
+
hash[:line_num] = @line_num
|
37
|
+
hash[:file_path] = @file_path
|
38
|
+
hash[:payload] = {
|
39
|
+
:arguments => @arguments
|
40
|
+
}
|
41
|
+
hash
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../ext/libinjection/libinjection'
|
4
|
+
require_relative '../events/models/instrumentation_event'
|
5
|
+
require_relative '../events/models/attack_event'
|
6
|
+
require_relative '../jobs/event_submitter'
|
7
|
+
require_relative '../utils/logger'
|
8
|
+
require_relative '../constants'
|
9
|
+
|
10
|
+
module Threatstack
|
11
|
+
module Instrumentation
|
12
|
+
include Threatstack::Constants
|
13
|
+
|
14
|
+
@@logger = Threatstack::Utils::TSLogger.create 'CommonInstrumentation'
|
15
|
+
@@submitter = Threatstack::Jobs::EventSubmitter.instance
|
16
|
+
|
17
|
+
def self.create_instrumentation_event(module_name, method_name, file_path, line_num, arguments)
|
18
|
+
data = {
|
19
|
+
:module_name => module_name,
|
20
|
+
:method_name => method_name,
|
21
|
+
:file_path => file_path,
|
22
|
+
:line_num => line_num,
|
23
|
+
:arguments => drop_sensitive_fields(arguments)
|
24
|
+
}
|
25
|
+
@@logger.debug "Creating instrumentation event with data: #{data}"
|
26
|
+
# create and submit the attack event
|
27
|
+
@@submitter.queue_event Threatstack::Events::InstrumentationEvent.new(drop_sensitive_fields(data))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.create_attack_event(payload, type, location, request, headers, backtrace)
|
31
|
+
is_blocked = (type == SQLI && BLOCK_SQLI) || (type == XSS && BLOCK_XSS)
|
32
|
+
data = {
|
33
|
+
:timestamp => Time.now.utc.strftime('%FT%T.%3NZ'),
|
34
|
+
:module_name => AGENT_NAME,
|
35
|
+
:request_ip => request.remote_ip,
|
36
|
+
:request_headers => headers,
|
37
|
+
:request_url => request.path,
|
38
|
+
:request_method => request.request_method,
|
39
|
+
:attack_message => is_blocked ? REQUEST_BLOCKED : DETECTED_NOT_BLOCKED,
|
40
|
+
:attack_stack => backtrace,
|
41
|
+
:attack_details => {
|
42
|
+
# TODO: get signature from libinjection
|
43
|
+
:details => [{ :signature => nil, :value => drop_sensitive_fields(payload) }],
|
44
|
+
:in => location,
|
45
|
+
:type => type,
|
46
|
+
:isBlocked => is_blocked,
|
47
|
+
:action => 'process_action'
|
48
|
+
}
|
49
|
+
}
|
50
|
+
@@logger.debug "Creating attack event with data: #{data}"
|
51
|
+
# create and submit the attack event
|
52
|
+
@@submitter.queue_event Threatstack::Events::AttackEvent.new(drop_sensitive_fields(data))
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.const_exist?(name)
|
56
|
+
resolve_const(name) && true
|
57
|
+
rescue NameError, ArgumentError
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.resolve_const(name)
|
62
|
+
raise ArgumentError if name.nil? || name.empty?
|
63
|
+
|
64
|
+
name.to_s.split('::').inject(Object) { |a, e| a.const_get(e) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.flatten(obj)
|
68
|
+
if obj.is_a?(Hash)
|
69
|
+
obj.each_with_object({}) do |(k, v), h|
|
70
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
71
|
+
flatten(v).map do |h_k, h_v|
|
72
|
+
h["#{k}.#{h_k}".to_sym] = h_v
|
73
|
+
end
|
74
|
+
else
|
75
|
+
h[k] = v
|
76
|
+
end
|
77
|
+
end
|
78
|
+
elsif obj.is_a?(Array)
|
79
|
+
h = {}
|
80
|
+
obj.each_with_index do |v, index|
|
81
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
82
|
+
flatten(v).map do |h_k, h_v|
|
83
|
+
h["#{index}.#{h_k}".to_sym] = h_v
|
84
|
+
end
|
85
|
+
else
|
86
|
+
h[index.to_s] = v
|
87
|
+
end
|
88
|
+
end
|
89
|
+
h
|
90
|
+
else
|
91
|
+
obj
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.drop_sensitive_fields(obj)
|
96
|
+
return obj if DROP_FIELDS.nil?
|
97
|
+
|
98
|
+
if obj.is_a?(Hash)
|
99
|
+
obj.each_with_object({}) do |(k, v), h|
|
100
|
+
if DROP_FIELDS[k] || DROP_FIELDS[k.to_s]
|
101
|
+
h[k] = REDACTED
|
102
|
+
next
|
103
|
+
end
|
104
|
+
|
105
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
106
|
+
h[k] = drop_sensitive_fields(v)
|
107
|
+
else
|
108
|
+
h[k] = v
|
109
|
+
end
|
110
|
+
end
|
111
|
+
elsif obj.is_a?(Array)
|
112
|
+
obj.each_with_object([]) do |v, arr|
|
113
|
+
if v.is_a?(Hash) || v.is_a?(Array)
|
114
|
+
arr.push drop_sensitive_fields(v)
|
115
|
+
else
|
116
|
+
arr.push v
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
obj
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def self.check_sqli_payload(param, name = nil)
|
125
|
+
if param.nil? || !param.kind_of?(String)
|
126
|
+
@@logger.debug "SQLI Check skipped for: #{name}" unless name.nil?
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
match = (Libinjection.libinjection_sqli(param, param.length, '') === 1 ? true : false)
|
130
|
+
@@logger.send(match ? :warn : :debug, "SQLI Check #{match ? 'positive' : 'negative'} for: #{name}") unless name.nil?
|
131
|
+
match
|
132
|
+
end
|
133
|
+
|
134
|
+
def self.check_xss_payload(param, name = nil)
|
135
|
+
if param.nil? || !param.kind_of?(String)
|
136
|
+
@@logger.debug "XSS Check skipped for: #{name}" unless name.nil?
|
137
|
+
return false
|
138
|
+
end
|
139
|
+
|
140
|
+
match = (Libinjection.libinjection_xss(param, param.length) === 1 ? true : false)
|
141
|
+
@@logger.send(match ? :warn : :debug, "XSS Check #{match ? 'positive' : 'negative'} for: #{name}") unless name.nil?
|
142
|
+
match
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.check_parameters(params, location, request, headers, backtrace, include_payload = true)
|
146
|
+
if params.nil? || !params.is_a?(Hash)
|
147
|
+
@@logger.debug "Skipping param check for: #{params}"
|
148
|
+
return nil
|
149
|
+
end
|
150
|
+
|
151
|
+
# flatten hash for easier checking
|
152
|
+
flattened = flatten params
|
153
|
+
|
154
|
+
# check each parameter value for dangerous payloads
|
155
|
+
sqli_found, xss_found = false, false
|
156
|
+
flattened.each do |key, val|
|
157
|
+
sqli_found = check_sqli_payload(val, key) unless sqli_found
|
158
|
+
xss_found = check_xss_payload(val, key) unless xss_found
|
159
|
+
end
|
160
|
+
|
161
|
+
# whether or not to include the payload in the event
|
162
|
+
payload = include_payload ? params : nil
|
163
|
+
|
164
|
+
# create the according attack event if the checks above returned positive
|
165
|
+
create_attack_event(payload, SQLI, location, request, headers, backtrace) if sqli_found
|
166
|
+
create_attack_event(payload, XSS, location, request, headers, backtrace) if xss_found
|
167
|
+
|
168
|
+
# return results
|
169
|
+
{ :sqli => sqli_found, :xss => xss_found }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../utils/logger'
|
4
|
+
require_relative '../jobs/job_queue'
|
5
|
+
|
6
|
+
module Threatstack
|
7
|
+
module Instrumentation
|
8
|
+
class Instrumenter
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
@@logger = Threatstack::Utils::TSLogger.create 'Instrumenter'
|
12
|
+
|
13
|
+
if RUBY_VERSION < '1.9'
|
14
|
+
def normalize_method_name(method)
|
15
|
+
method.to_s
|
16
|
+
end
|
17
|
+
else
|
18
|
+
def normalize_method_name(method)
|
19
|
+
method
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.define_callback(klass, method, &block)
|
24
|
+
backup_name = get_backup_name method
|
25
|
+
outer_block = block
|
26
|
+
Proc.new do |*args, &block|
|
27
|
+
@@logger.debug "Wrapped method called: #{klass}.#{method}"
|
28
|
+
caller_loc = caller_locations(1, 10)
|
29
|
+
if outer_block
|
30
|
+
# exec callback
|
31
|
+
outer_block.call(:caller_loc => caller_loc, :args => args, :method_name => method, :target_class => klass)
|
32
|
+
end
|
33
|
+
__send__(backup_name, *args, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def wrap_method(klass, method, &block)
|
38
|
+
if is_class_method?(klass, method)
|
39
|
+
wrap_class_method(klass, method, &block)
|
40
|
+
elsif is_instance_method?(klass, method)
|
41
|
+
@@logger.debug "Wrapping instance method: #{klass}.#{method}"
|
42
|
+
wrap_instance_method(klass, method, &block)
|
43
|
+
else
|
44
|
+
raise "#{klass}.#{method} is not a class nor instance method"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def wrap_class_method(klass, method, &block)
|
49
|
+
@@logger.debug "Wrapping class method: #{klass}.#{method}"
|
50
|
+
original_name = method.to_sym
|
51
|
+
backup_name = get_backup_name method
|
52
|
+
wrapped_name = get_wrapped_name method
|
53
|
+
|
54
|
+
klass.singleton_class.instance_eval do
|
55
|
+
alias_method backup_name, original_name
|
56
|
+
|
57
|
+
p = Instrumenter.define_callback(klass, method, &block)
|
58
|
+
define_method(wrapped_name, p)
|
59
|
+
|
60
|
+
private wrapped_name
|
61
|
+
|
62
|
+
method_kind = nil
|
63
|
+
if public_method_defined? original_name
|
64
|
+
method_kind = :public
|
65
|
+
elsif protected_method_defined? original_name
|
66
|
+
method_kind = :protected
|
67
|
+
elsif private_method_defined? original_name
|
68
|
+
method_kind = :private
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method original_name, wrapped_name
|
72
|
+
__send__(method_kind, original_name)
|
73
|
+
private backup_name
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def wrap_instance_method(klass, method, &block)
|
78
|
+
@@logger.debug "Wrapping instance method: #{klass}.#{method}"
|
79
|
+
backup_name = get_backup_name method
|
80
|
+
wrapped_name = get_wrapped_name method
|
81
|
+
|
82
|
+
private_methods = klass.private_instance_methods(false)
|
83
|
+
if private_methods.include?(backup_name)
|
84
|
+
@@logger.debug "#{klass}.#{method} already instrumented"
|
85
|
+
return backup_name
|
86
|
+
end
|
87
|
+
|
88
|
+
p = Instrumenter.define_callback(klass, method, &block)
|
89
|
+
visibility = nil
|
90
|
+
klass.class_eval do
|
91
|
+
alias_method backup_name, method
|
92
|
+
|
93
|
+
define_method(wrapped_name, p)
|
94
|
+
|
95
|
+
if public_method_defined?(method)
|
96
|
+
visibility = :public
|
97
|
+
elsif protected_method_defined?(method)
|
98
|
+
visibility = :protected
|
99
|
+
elsif private_method_defined?(method)
|
100
|
+
visibility = :private
|
101
|
+
end
|
102
|
+
|
103
|
+
alias_method method, wrapped_name
|
104
|
+
private backup_name
|
105
|
+
private wrapped_name
|
106
|
+
__send__(visibility, method)
|
107
|
+
end
|
108
|
+
backup_name
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.get_backup_name(method, suffix = nil)
|
112
|
+
"ts_#{method}_backup#{suffix ? "_#{suffix}" : ''}".to_sym
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.get_wrapped_name(method, suffix = nil)
|
116
|
+
"ts_#{method}_wrapped#{suffix ? "_#{suffix}" : ''}".to_sym
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_backup_name(method, suffix = nil)
|
120
|
+
Instrumenter.get_backup_name(method, suffix)
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_wrapped_name(method, suffix = nil)
|
124
|
+
Instrumenter.get_wrapped_name(method, suffix)
|
125
|
+
end
|
126
|
+
|
127
|
+
def is_instance_method?(klass, method)
|
128
|
+
method = normalize_method_name(method)
|
129
|
+
klass.instance_methods.include?(method) || klass.private_instance_methods.include?(method)
|
130
|
+
end
|
131
|
+
|
132
|
+
def is_class_method?(klass, method)
|
133
|
+
method = normalize_method_name(method)
|
134
|
+
klass.singleton_methods.include? method
|
135
|
+
end
|
136
|
+
|
137
|
+
def method_exists?(obj, method)
|
138
|
+
return true if is_class_method?(obj, method)
|
139
|
+
return false unless obj.respond_to?(:instance_methods)
|
140
|
+
is_instance_method?(obj, method)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './common.rb'
|
4
|
+
require_relative './instrumenter.rb'
|
5
|
+
require_relative '../utils/logger'
|
6
|
+
|
7
|
+
module Threatstack
|
8
|
+
module Instrumentation
|
9
|
+
module TSKernel
|
10
|
+
@@logger = Threatstack::Utils::TSLogger.create 'KernelInstrumentation'
|
11
|
+
|
12
|
+
# methods to wrap
|
13
|
+
METHOD_NAMES = ['exec', 'system', '`'].freeze
|
14
|
+
|
15
|
+
def self.wrap_methods
|
16
|
+
# executed every time a wrapped method is called
|
17
|
+
on_method_call = Proc.new do |params|
|
18
|
+
module_name = params[:target_class].name.downcase
|
19
|
+
method_name = params[:method_name].downcase
|
20
|
+
called_by = params[:caller_loc] ? params[:caller_loc].first : nil
|
21
|
+
file_path = called_by ? called_by.absolute_path : nil
|
22
|
+
# special case for ` method emulation
|
23
|
+
if method_name == '`' && !file_path.nil? && file_path =~ /.*\/kernel\/agnostics\.rb$/
|
24
|
+
called_by = params[:caller_loc][1]
|
25
|
+
file_path = called_by ? called_by.absolute_path : nil
|
26
|
+
end
|
27
|
+
line_num = called_by ? called_by.lineno : nil
|
28
|
+
|
29
|
+
arg = params[:args] ? params[:args].first : nil
|
30
|
+
args = arg ? [arg] : []
|
31
|
+
|
32
|
+
# create and queue the event
|
33
|
+
Threatstack::Instrumentation.create_instrumentation_event(module_name, method_name, file_path, line_num, args)
|
34
|
+
end
|
35
|
+
@@logger.info "Instrumenting Kernel methods: #{METHOD_NAMES}"
|
36
|
+
instrumenter = Threatstack::Instrumentation::Instrumenter.instance
|
37
|
+
METHOD_NAMES.each do |method_name|
|
38
|
+
instrumenter.wrap_class_method(Kernel, method_name, &on_method_call)
|
39
|
+
instrumenter.wrap_instance_method(Kernel, method_name, &on_method_call)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative '../constants'
|
6
|
+
require_relative '../exceptions/request_blocked_error'
|
7
|
+
require_relative './common'
|
8
|
+
require_relative '../utils/logger'
|
9
|
+
|
10
|
+
module Threatstack
|
11
|
+
module Instrumentation
|
12
|
+
module TSRails
|
13
|
+
@@logger = Threatstack::Utils::TSLogger.create 'RailsInstrumentation'
|
14
|
+
|
15
|
+
def self.patch_action_controller
|
16
|
+
@@logger.info 'Looking for Rails gem'
|
17
|
+
return unless defined?(::Rails) && defined?(::Rails::VERSION)
|
18
|
+
return unless defined?(ActionController) && defined?(ActionController::Base)
|
19
|
+
|
20
|
+
@@logger.info "Rails #{Rails::VERSION::MAJOR.to_s} gem found, instrumenting ActionController"
|
21
|
+
ActionController::Base.class_eval do
|
22
|
+
include Threatstack::Instrumentation::TSRails::TSActionController
|
23
|
+
end
|
24
|
+
@@logger.info 'Rails instrumentation done'
|
25
|
+
end
|
26
|
+
|
27
|
+
module TSActionController
|
28
|
+
include Threatstack::Constants
|
29
|
+
@@logger = Threatstack::Utils::TSLogger.create 'RailsInstrumentation'
|
30
|
+
|
31
|
+
def process_action(*args)
|
32
|
+
# we need the headers hack below because Rails adds a lot of internal headers
|
33
|
+
headers = request.headers.each_with_object({}) do |(k, v), obj|
|
34
|
+
obj[k] = v if k.in?(CGI_VARIABLES) || k =~ /^HTTP_/
|
35
|
+
end
|
36
|
+
@@logger.debug("Incoming request: #{{ :headers => headers, :path => request.path_parameters,
|
37
|
+
:query => Threatstack::Instrumentation.drop_sensitive_fields(request.query_parameters),
|
38
|
+
:body => Threatstack::Instrumentation.drop_sensitive_fields(request.request_parameters) }}")
|
39
|
+
backtrace = caller.join("\n")
|
40
|
+
|
41
|
+
# check path/query/body parameters
|
42
|
+
path_res = Threatstack::Instrumentation.check_parameters(request.path_parameters, 'path', request, headers, backtrace)
|
43
|
+
query_res = Threatstack::Instrumentation.check_parameters(request.query_parameters, 'query', request, headers, backtrace)
|
44
|
+
body_res = Threatstack::Instrumentation.check_parameters(request.request_parameters, 'body', request, headers, backtrace)
|
45
|
+
|
46
|
+
@@logger.debug "RequestStats -- Path: #{path_res}, Query: #{query_res}, Body: #{body_res}"
|
47
|
+
sqli_found = (path_res[:sqli] || query_res[:sqli] || body_res[:sqli])
|
48
|
+
xss_found = (path_res[:xss] || query_res[:xss] || body_res[:xss])
|
49
|
+
# raise an exception if any attack payloads were detected and blocking is enabled
|
50
|
+
if (BLOCK_SQLI && sqli_found) || (BLOCK_XSS && xss_found)
|
51
|
+
raise Threatstack::Exceptions::RequestBlockedError, REQUEST_BLOCKED
|
52
|
+
end
|
53
|
+
|
54
|
+
# continue processing the request normally if no issues were found
|
55
|
+
super
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|