threatstack-agent-ruby 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|